LP1779158 Angular7 and ng-lint updates
[Evergreen.git] / Open-ILS / src / c-apps / oils_sql.c
1 /**
2         @file oils_sql.c
3         @brief Utility routines for translating JSON into SQL.
4 */
5
6 #include <stdlib.h>
7 #include <string.h>
8 #include <time.h>
9 #include <ctype.h>
10 #include <dbi/dbi.h>
11 #include "opensrf/utils.h"
12 #include "opensrf/log.h"
13 #include "opensrf/osrf_application.h"
14 #include "openils/oils_utils.h"
15 #include "openils/oils_sql.h"
16
17 // The next four macros are OR'd together as needed to form a set
18 // of bitflags.  SUBCOMBO enables an extra pair of parentheses when
19 // nesting one UNION, INTERSECT or EXCEPT inside another.
20 // SUBSELECT tells us we're in a subquery, so don't add the
21 // terminal semicolon yet.
22 #define SUBCOMBO    8
23 #define SUBSELECT   4
24 #define DISABLE_I18N    2
25 #define SELECT_DISTINCT 1
26
27 #define AND_OP_JOIN     0
28 #define OR_OP_JOIN      1
29
30 struct ClassInfoStruct;
31 typedef struct ClassInfoStruct ClassInfo;
32
33 #define ALIAS_STORE_SIZE 16
34 #define CLASS_NAME_STORE_SIZE 16
35
36 struct ClassInfoStruct {
37         char* alias;
38         char* class_name;
39         char* source_def;
40         osrfHash* class_def;      // Points into IDL
41         osrfHash* fields;         // Points into IDL
42         osrfHash* links;          // Points into IDL
43
44         // The remaining members are private and internal.  Client code should not
45         // access them directly.
46
47         ClassInfo* next;          // Supports linked list of joined classes
48         int in_use;               // boolean
49
50         // We usually store the alias and class name in the following arrays, and
51         // point the corresponding pointers at them.  When the string is too big
52         // for the array (which will probably never happen in practice), we strdup it.
53
54         char alias_store[ ALIAS_STORE_SIZE + 1 ];
55         char class_name_store[ CLASS_NAME_STORE_SIZE + 1 ];
56 };
57
58 struct QueryFrameStruct;
59 typedef struct QueryFrameStruct QueryFrame;
60
61 struct QueryFrameStruct {
62         ClassInfo core;
63         ClassInfo* join_list;  // linked list of classes joined to the core class
64         QueryFrame* next;      // implements stack as linked list
65         int in_use;            // boolean
66 };
67
68 static int timeout_needs_resetting;
69 static time_t time_next_reset;
70
71 static int verifyObjectClass ( osrfMethodContext*, const jsonObject* );
72
73 static void setXactId( osrfMethodContext* ctx );
74 static inline const char* getXactId( osrfMethodContext* ctx );
75 static inline void clearXactId( osrfMethodContext* ctx );
76
77 static jsonObject* doFieldmapperSearch ( osrfMethodContext* ctx, osrfHash* class_meta,
78                 jsonObject* where_hash, jsonObject* query_hash, int* err );
79 static jsonObject* oilsMakeFieldmapperFromResult( dbi_result, osrfHash* );
80 static jsonObject* oilsMakeJSONFromResult( dbi_result );
81
82 static char* searchSimplePredicate ( const char* op, const char* class_alias,
83                                 osrfHash* field, const jsonObject* node );
84 static char* searchFunctionPredicate ( const char*, osrfHash*, const jsonObject*, const char* );
85 static char* searchFieldTransform ( const char*, osrfHash*, const jsonObject* );
86 static char* searchFieldTransformPredicate ( const ClassInfo*, osrfHash*, const jsonObject*,
87                 const char* );
88 static char* searchBETWEENPredicate ( const char*, osrfHash*, const jsonObject* );
89 static char* searchINPredicate ( const char*, osrfHash*,
90                                                                  jsonObject*, const char*, osrfMethodContext* );
91 static char* searchPredicate ( const ClassInfo*, osrfHash*, jsonObject*, osrfMethodContext* );
92 static char* searchJOIN ( const jsonObject*, const ClassInfo* left_info );
93 static char* searchWHERE ( const jsonObject* search_hash, const ClassInfo*, int, osrfMethodContext* );
94 static char* buildSELECT( const jsonObject*, jsonObject* rest_of_query,
95         osrfHash* meta, osrfMethodContext* ctx );
96 static char* buildOrderByFromArray( osrfMethodContext* ctx, const jsonObject* order_array );
97
98 char* buildQuery( osrfMethodContext* ctx, jsonObject* query, int flags );
99
100 char* SELECT ( osrfMethodContext*, jsonObject*, const jsonObject*, const jsonObject*,
101         const jsonObject*, const jsonObject*, const jsonObject*, const jsonObject*, int );
102
103 static osrfStringArray* getPermLocationCache( osrfMethodContext*, const char* );
104 static void setPermLocationCache( osrfMethodContext*, const char*, osrfStringArray* );
105
106 void userDataFree( void* );
107 static void sessionDataFree( char*, void* );
108 static void pcacheFree( char*, void* );
109 static int obj_is_true( const jsonObject* obj );
110 static const char* json_type( int code );
111 static const char* get_primitive( osrfHash* field );
112 static const char* get_datatype( osrfHash* field );
113 static void pop_query_frame( void );
114 static void push_query_frame( void );
115 static int add_query_core( const char* alias, const char* class_name );
116 static inline ClassInfo* search_alias( const char* target );
117 static ClassInfo* search_all_alias( const char* target );
118 static ClassInfo* add_joined_class( const char* alias, const char* classname );
119 static void clear_query_stack( void );
120
121 static const jsonObject* verifyUserPCRUD( osrfMethodContext* );
122 static const jsonObject* verifyUserPCRUDfull( osrfMethodContext*, int );
123 static int verifyObjectPCRUD( osrfMethodContext*, osrfHash*, const jsonObject*, int );
124 static const char* org_tree_root( osrfMethodContext* ctx );
125 static jsonObject* single_hash( const char* key, const char* value );
126
127 static int child_initialized = 0;   /* boolean */
128
129 static dbi_conn writehandle; /* our MASTER db connection */
130 static dbi_conn dbhandle; /* our CURRENT db connection */
131 //static osrfHash * readHandles;
132
133 // The following points to the top of a stack of QueryFrames.  It's a little
134 // confusing because the top level of the query is at the bottom of the stack.
135 static QueryFrame* curr_query = NULL;
136
137 static dbi_conn writehandle; /* our MASTER db connection */
138 static dbi_conn dbhandle; /* our CURRENT db connection */
139 //static osrfHash * readHandles;
140
141 static int max_flesh_depth = 100;
142
143 static int perm_at_threshold = 5;
144 static int enforce_pcrud = 0;     // Boolean
145 static char* modulename = NULL;
146
147 int writeAuditInfo( osrfMethodContext* ctx, const char* user_id, const char* ws_id);
148
149 static char* _sanitize_tz_name( const char* tz );
150 static char* _sanitize_savepoint_name( const char* sp );
151
152 /**
153         @brief Connect to the database.
154         @return A database connection if successful, or NULL if not.
155 */
156 dbi_conn oilsConnectDB( const char* mod_name ) {
157
158         osrfLogDebug( OSRF_LOG_MARK, "Attempting to initialize libdbi..." );
159         if( dbi_initialize( NULL ) == -1 ) {
160                 osrfLogError( OSRF_LOG_MARK, "Unable to initialize libdbi" );
161                 return NULL;
162         } else
163                 osrfLogDebug( OSRF_LOG_MARK, "... libdbi initialized." );
164
165         char* driver = osrf_settings_host_value( "/apps/%s/app_settings/driver", mod_name );
166         char* user   = osrf_settings_host_value( "/apps/%s/app_settings/database/user", mod_name );
167         char* host   = osrf_settings_host_value( "/apps/%s/app_settings/database/host", mod_name );
168         char* port   = osrf_settings_host_value( "/apps/%s/app_settings/database/port", mod_name );
169         char* db     = osrf_settings_host_value( "/apps/%s/app_settings/database/db", mod_name );
170         char* pw     = osrf_settings_host_value( "/apps/%s/app_settings/database/pw", mod_name );
171         char* pg_app = osrf_settings_host_value( "/apps/%s/app_settings/database/application_name", mod_name );
172
173         osrfLogDebug( OSRF_LOG_MARK, "Attempting to load the database driver [%s]...", driver );
174         dbi_conn handle = dbi_conn_new( driver );
175
176         if( !handle ) {
177                 osrfLogError( OSRF_LOG_MARK, "Error loading database driver [%s]", driver );
178                 return NULL;
179         }
180         osrfLogDebug( OSRF_LOG_MARK, "Database driver [%s] seems OK", driver );
181
182         osrfLogInfo(OSRF_LOG_MARK, "%s connecting to database.  host=%s, "
183                 "port=%s, user=%s, db=%s", mod_name, host, port, user, db );
184
185         if( host )   dbi_conn_set_option( handle, "host", host );
186         if( port )   dbi_conn_set_option_numeric( handle, "port", atoi( port ));
187         if( user )   dbi_conn_set_option( handle, "username", user );
188         if( pw )     dbi_conn_set_option( handle, "password", pw );
189         if( db )     dbi_conn_set_option( handle, "dbname", db );
190         if( pg_app ) dbi_conn_set_option( handle, "pgsql_application_name", pg_app );
191
192         free( user );
193         free( host );
194         free( port );
195         free( db );
196         free( pw );
197         free( pg_app );
198
199         if( dbi_conn_connect( handle ) < 0 ) {
200                 sleep( 1 );
201                 if( dbi_conn_connect( handle ) < 0 ) {
202                         const char* msg;
203                         dbi_conn_error( handle, &msg );
204                         osrfLogError( OSRF_LOG_MARK, "Error connecting to database: %s",
205                                 msg ? msg : "(No description available)" );
206                         return NULL;
207                 }
208         }
209
210         osrfLogInfo( OSRF_LOG_MARK, "%s successfully connected to the database", mod_name );
211
212         return handle;
213 }
214
215 /**
216         @brief Select some options.
217         @param module_name: Name of the server.
218         @param do_pcrud: Boolean.  True if we are to enforce PCRUD permissions.
219
220         This source file is used (at this writing) to implement three different servers:
221         - open-ils.reporter-store
222         - open-ils.pcrud
223         - open-ils.cstore
224
225         These servers behave mostly the same, but they implement different combinations of
226         methods, and open-ils.pcrud enforces a permissions scheme that the other two don't.
227
228         Here we use the server name in messages to identify which kind of server issued them.
229         We use do_crud as a boolean to control whether or not to enforce the permissions scheme.
230 */
231 void oilsSetSQLOptions( const char* module_name, int do_pcrud, int flesh_depth ) {
232         if( !module_name )
233                 module_name = "open-ils.cstore";   // bulletproofing with a default
234
235         if( modulename )
236                 free( modulename );
237
238         modulename = strdup( module_name );
239         enforce_pcrud = do_pcrud;
240         max_flesh_depth = flesh_depth;
241 }
242
243 /**
244         @brief Install a database connection.
245         @param conn Pointer to a database connection.
246
247         In some contexts, @a conn may merely provide a driver so that we can process strings
248         properly, without providing an open database connection.
249 */
250 void oilsSetDBConnection( dbi_conn conn ) {
251         dbhandle = writehandle = conn;
252 }
253
254 /**
255         @brief Determine whether a database connection is alive.
256         @param handle Handle for a database connection.
257         @return 1 if the connection is alive, or zero if it isn't.
258 */
259 int oilsIsDBConnected( dbi_conn handle ) {
260         // Do an innocuous SELECT.  If it succeeds, the database connection is still good.
261         dbi_result result = dbi_conn_query( handle, "SELECT 1;" );
262         if( result ) {
263                 dbi_result_free( result );
264                 return 1;
265         } else {
266                 // This is a terrible, horrible, no good, very bad kludge.
267                 // Sometimes the SELECT 1 query fails, not because the database connection is dead,
268                 // but because (due to a previous error) the database is ignoring all commands,
269                 // even innocuous SELECTs, until the current transaction is rolled back.  The only
270                 // known way to detect this condition via the dbi library is by looking at the error
271                 // message.  This approach will break if the language or wording of the message ever
272                 // changes.
273                 // Note: the dbi_conn_ping function purports to determine whether the database
274                 // connection is live, but at this writing this function is unreliable and useless.
275                 static const char* ok_msg = "ERROR:  current transaction is aborted, commands "
276                         "ignored until end of transaction block\n";
277                 const char* msg;
278                 dbi_conn_error( handle, &msg );
279                 // Newer versions of dbi_conn_error return codes within the error msg.
280                 // E.g. 3624914: ERROR:  current transaction is aborted, commands ignored until end of transaction block
281                 // Substring test should work regardless.
282                 const char* substr = strstr(msg, ok_msg);
283                 if( substr == NULL ) {
284                         osrfLogError( OSRF_LOG_MARK, "Database connection isn't working : %s", msg );
285                         return 0;
286                 } else
287                         return 1;   // ignoring SELECT due to previous error; that's okay
288         }
289 }
290
291 /**
292         @brief Get a table name, view name, or subquery for use in a FROM clause.
293         @param class Pointer to the IDL class entry.
294         @return A table name, a view name, or a subquery in parentheses.
295
296         In some cases the IDL defines a class, not with a table name or a view name, but with
297         a SELECT statement, which may be used as a subquery.
298 */
299 char* oilsGetRelation( osrfHash* classdef ) {
300
301         char* source_def = NULL;
302         const char* tabledef = osrfHashGet( classdef, "tablename" );
303
304         if( tabledef ) {
305                 source_def = strdup( tabledef );   // Return the name of a table or view
306         } else {
307                 tabledef = osrfHashGet( classdef, "source_definition" );
308                 if( tabledef ) {
309                         // Return a subquery, enclosed in parentheses
310                         source_def = safe_malloc( strlen( tabledef ) + 3 );
311                         source_def[ 0 ] = '(';
312                         strcpy( source_def + 1, tabledef );
313                         strcat( source_def, ")" );
314                 } else {
315                         // Not found: return an error
316                         const char* classname = osrfHashGet( classdef, "classname" );
317                         if( !classname )
318                                 classname = "???";
319                         osrfLogError(
320                                 OSRF_LOG_MARK,
321                                 "%s ERROR No tablename or source_definition for class \"%s\"",
322                                 modulename,
323                                 classname
324                         );
325                 }
326         }
327
328         return source_def;
329 }
330
331 /**
332         @brief Add datatypes from the database to the fields in the IDL.
333         @param handle Handle for a database connection
334         @return Zero if successful, or 1 upon error.
335
336         For each relevant class in the IDL: ask the database for the datatype of every field.
337         In particular, determine which fields are text fields and which fields are numeric
338         fields, so that we know whether to enclose their values in quotes.
339 */
340 int oilsExtendIDL( dbi_conn handle ) {
341         osrfHashIterator* class_itr = osrfNewHashIterator( oilsIDL() );
342         osrfHash* class = NULL;
343         growing_buffer* query_buf = buffer_init( 64 );
344         int results_found = 0;   // boolean
345
346         // For each class in the IDL...
347         while( (class = osrfHashIteratorNext( class_itr ) ) ) {
348                 const char* classname = osrfHashIteratorKey( class_itr );
349                 osrfHash* fields = osrfHashGet( class, "fields" );
350
351                 // If the class is virtual, ignore it
352                 if( str_is_true( osrfHashGet(class, "virtual") ) ) {
353                         osrfLogDebug(OSRF_LOG_MARK, "Class %s is virtual, skipping", classname );
354                         continue;
355                 }
356
357                 char* tabledef = oilsGetRelation( class );
358                 if( !tabledef )
359                         continue;   // No such relation -- a query of it would be doomed to failure
360
361                 buffer_reset( query_buf );
362                 buffer_fadd( query_buf, "SELECT * FROM %s AS x WHERE 1=0;", tabledef );
363
364                 free(tabledef );
365
366                 osrfLogDebug( OSRF_LOG_MARK, "%s Investigatory SQL = %s",
367                                 modulename, OSRF_BUFFER_C_STR( query_buf ) );
368
369                 dbi_result result = dbi_conn_query( handle, OSRF_BUFFER_C_STR( query_buf ) );
370                 if( result ) {
371
372                         results_found = 1;
373                         int columnIndex = 1;
374                         const char* columnName;
375                         while( (columnName = dbi_result_get_field_name(result, columnIndex)) ) {
376
377                                 osrfLogInternal( OSRF_LOG_MARK, "Looking for column named [%s]...",
378                                                 columnName );
379
380                                 /* fetch the fieldmapper index */
381                                 osrfHash* _f = osrfHashGet(fields, columnName);
382                                 if( _f ) {
383
384                                         osrfLogDebug(OSRF_LOG_MARK, "Found [%s] in IDL hash...", columnName);
385
386                                         /* determine the field type and storage attributes */
387
388                                         switch( dbi_result_get_field_type_idx( result, columnIndex )) {
389
390                                                 case DBI_TYPE_INTEGER : {
391
392                                                         if( !osrfHashGet(_f, "primitive") )
393                                                                 osrfHashSet(_f, "number", "primitive");
394
395                                                         int attr = dbi_result_get_field_attribs_idx( result, columnIndex );
396                                                         if( attr & DBI_INTEGER_SIZE8 )
397                                                                 osrfHashSet( _f, "INT8", "datatype" );
398                                                         else
399                                                                 osrfHashSet( _f, "INT", "datatype" );
400                                                         break;
401                                                 }
402                                                 case DBI_TYPE_DECIMAL :
403                                                         if( !osrfHashGet( _f, "primitive" ))
404                                                                 osrfHashSet( _f, "number", "primitive" );
405
406                                                         osrfHashSet( _f, "NUMERIC", "datatype" );
407                                                         break;
408
409                                                 case DBI_TYPE_STRING :
410                                                         if( !osrfHashGet( _f, "primitive" ))
411                                                                 osrfHashSet( _f, "string", "primitive" );
412
413                                                         osrfHashSet( _f,"TEXT", "datatype" );
414                                                         break;
415
416                                                 case DBI_TYPE_DATETIME :
417                                                         if( !osrfHashGet( _f, "primitive" ))
418                                                                 osrfHashSet( _f, "string", "primitive" );
419
420                                                         osrfHashSet( _f, "TIMESTAMP", "datatype" );
421                                                         break;
422
423                                                 case DBI_TYPE_BINARY :
424                                                         if( !osrfHashGet( _f, "primitive" ))
425                                                                 osrfHashSet( _f, "string", "primitive" );
426
427                                                         osrfHashSet( _f, "BYTEA", "datatype" );
428                                         }
429
430                                         osrfLogDebug(
431                                                 OSRF_LOG_MARK,
432                                                 "Setting [%s] to primitive [%s] and datatype [%s]...",
433                                                 columnName,
434                                                 osrfHashGet( _f, "primitive" ),
435                                                 osrfHashGet( _f, "datatype" )
436                                         );
437                                 }
438                                 ++columnIndex;
439                         } // end while loop for traversing columns of result
440                         dbi_result_free( result  );
441                 } else {
442                         const char* msg;
443                         int errnum = dbi_conn_error( handle, &msg );
444                         osrfLogDebug( OSRF_LOG_MARK, "No data found for class [%s]: %d, %s", classname,
445                                 errnum, msg ? msg : "(No description available)" );
446                         // We don't check the database connection here.  It's routine to get failures at
447                         // this point; we routinely try to query tables that don't exist, because they
448                         // are defined in the IDL but not in the database.
449                 }
450         } // end for each class in IDL
451
452         buffer_free( query_buf );
453         osrfHashIteratorFree( class_itr );
454         child_initialized = 1;
455
456         if( !results_found ) {
457                 osrfLogError( OSRF_LOG_MARK,
458                         "No results found for any class -- bad database connection?" );
459                 return 1;
460         } else if( ! oilsIsDBConnected( handle )) {
461                 osrfLogError( OSRF_LOG_MARK,
462                         "Unable to extend IDL: database connection isn't working" );
463                 return 1;
464         }
465         else
466                 return 0;
467 }
468
469 /**
470         @brief Free an osrfHash that stores a transaction ID.
471         @param blob A pointer to the osrfHash to be freed, cast to a void pointer.
472
473         This function is a callback, to be called by the application session when it ends.
474         The application session stores the osrfHash via an opaque pointer.
475
476         If the osrfHash contains an entry for the key "xact_id", it means that an
477         uncommitted transaction is pending.  Roll it back.
478 */
479 void userDataFree( void* blob ) {
480         osrfHash* hash = (osrfHash*) blob;
481         if( osrfHashGet( hash, "xact_id" ) && writehandle ) {
482                 if( !dbi_conn_query( writehandle, "ROLLBACK;" )) {
483                         const char* msg;
484                         int errnum = dbi_conn_error( writehandle, &msg );
485                         osrfLogWarning( OSRF_LOG_MARK, "Unable to perform rollback: %d %s",
486                                 errnum, msg ? msg : "(No description available)" );
487                 };
488         }
489         if( writehandle ) {
490                 if( !dbi_conn_query( writehandle, "SELECT auditor.clear_audit_info();" ) ) {
491                         const char* msg;
492                         int errnum = dbi_conn_error( writehandle, &msg );
493                         osrfLogWarning( OSRF_LOG_MARK, "Unable to perform audit info clearing: %d %s",
494                                 errnum, msg ? msg : "(No description available)" );
495                 }
496         }
497
498         osrfHashFree( hash );
499 }
500
501 /**
502         @name Managing session data
503         @brief Maintain data stored via the userData pointer of the application session.
504
505         Currently, session-level data is stored in an osrfHash.  Other arrangements are
506         possible, and some would be more efficient.  The application session calls a
507         callback function to free userData before terminating.
508
509         Currently, the only data we store at the session level is the transaction id.  By this
510         means we can ensure that any pending transactions are rolled back before the application
511         session terminates.
512 */
513 /*@{*/
514
515 /**
516         @brief Free an item in the application session's userData.
517         @param key The name of a key for an osrfHash.
518         @param item An opaque pointer to the item associated with the key.
519
520         We store an osrfHash as userData with the application session, and arrange (by
521         installing userDataFree() as a different callback) for the session to free that
522         osrfHash before terminating.
523
524         This function is a callback for freeing items in the osrfHash.  Currently we store
525         two things:
526         - Transaction id of a pending transaction; a character string.  Key: "xact_id".
527         - Authkey; a character string.  Key: "authkey".
528         - User object from the authentication server; a jsonObject.  Key: "user_login".
529
530         If we ever store anything else in userData, we will need to revisit this function so
531         that it will free whatever else needs freeing.
532 */
533 static void sessionDataFree( char* key, void* item ) {
534         if( !strcmp( key, "xact_id" ) || !strcmp( key, "authkey" ) || !strncmp( key, "rs_size_", 8) ) 
535                 free( item );
536         else if( !strcmp( key, "user_login" ) )
537                 jsonObjectFree( (jsonObject*) item );
538         else if( !strcmp( key, "pcache" ) )
539                 osrfHashFree( (osrfHash*) item );
540 }
541
542 static void pcacheFree( char* key, void* item ) {
543         osrfStringArrayFree( (osrfStringArray*) item );
544 }
545
546 /**
547         @brief Initialize session cache.
548         @param ctx Pointer to the method context.
549
550         Create a cache for the session by making the session's userData member point
551         to an osrfHash instance.
552 */
553 static osrfHash* initSessionCache( osrfMethodContext* ctx ) {
554         ctx->session->userData = osrfNewHash();
555         osrfHashSetCallback( (osrfHash*) ctx->session->userData, &sessionDataFree );
556         ctx->session->userDataFree = &userDataFree;
557         return ctx->session->userData;
558 }
559
560 /**
561         @brief Save a transaction id.
562         @param ctx Pointer to the method context.
563
564         Save the session_id of the current application session as a transaction id.
565 */
566 static void setXactId( osrfMethodContext* ctx ) {
567         if( ctx && ctx->session ) {
568                 osrfAppSession* session = ctx->session;
569
570                 osrfHash* cache = session->userData;
571
572                 // If the session doesn't already have a hash, create one.  Make sure
573                 // that the application session frees the hash when it terminates.
574                 if( NULL == cache )
575                         cache = initSessionCache( ctx );
576
577                 // Save the transaction id in the hash, with the key "xact_id"
578                 osrfHashSet( cache, strdup( session->session_id ), "xact_id" );
579         }
580 }
581
582 /**
583         @brief Get the transaction ID for the current transaction, if any.
584         @param ctx Pointer to the method context.
585         @return Pointer to the transaction ID.
586
587         The return value points to an internal buffer, and will become invalid upon issuing
588         a commit or rollback.
589 */
590 static inline const char* getXactId( osrfMethodContext* ctx ) {
591         if( ctx && ctx->session && ctx->session->userData )
592                 return osrfHashGet( (osrfHash*) ctx->session->userData, "xact_id" );
593         else
594                 return NULL;
595 }
596
597 /**
598         @brief Clear the current transaction id.
599         @param ctx Pointer to the method context.
600 */
601 static inline void clearXactId( osrfMethodContext* ctx ) {
602         if( ctx && ctx->session && ctx->session->userData )
603                 osrfHashRemove( ctx->session->userData, "xact_id" );
604 }
605 /*@}*/
606
607 /**
608         @brief Stash the location for a particular perm in the sessionData cache
609         @param ctx Pointer to the method context.
610         @param perm Name of the permission we're looking at
611         @param array StringArray of perm location ids
612 */
613 static void setPermLocationCache( osrfMethodContext* ctx, const char* perm, osrfStringArray* locations ) {
614         if( ctx && ctx->session ) {
615                 osrfAppSession* session = ctx->session;
616
617                 osrfHash* cache = session->userData;
618
619                 // If the session doesn't already have a hash, create one.  Make sure
620                 // that the application session frees the hash when it terminates.
621                 if( NULL == cache )
622                         cache = initSessionCache( ctx );
623
624                 osrfHash* pcache = osrfHashGet(cache, "pcache");
625
626                 if( NULL == pcache ) {
627                         pcache = osrfNewHash();
628                         osrfHashSetCallback( pcache, &pcacheFree );
629                         osrfHashSet( cache, pcache, "pcache" );
630                 }
631
632                 if( perm && locations )
633                         osrfHashSet( pcache, locations, strdup(perm) );
634         }
635 }
636
637 /**
638         @brief Grab stashed location for a particular perm in the sessionData cache
639         @param ctx Pointer to the method context.
640         @param perm Name of the permission we're looking at
641 */
642 static osrfStringArray* getPermLocationCache( osrfMethodContext* ctx, const char* perm ) {
643         if( ctx && ctx->session ) {
644                 osrfAppSession* session = ctx->session;
645                 osrfHash* cache = session->userData;
646                 if( cache ) {
647                         osrfHash* pcache = osrfHashGet(cache, "pcache");
648                         if( pcache ) {
649                                 return osrfHashGet( pcache, perm );
650                         }
651                 }
652         }
653
654         return NULL;
655 }
656
657 /**
658         @brief Save the user's login in the userData for the current application session.
659         @param ctx Pointer to the method context.
660         @param user_login Pointer to the user login object to be cached (we cache the original,
661         not a copy of it).
662
663         If @a user_login is NULL, remove the user login if one is already cached.
664 */
665 static void setUserLogin( osrfMethodContext* ctx, jsonObject* user_login ) {
666         if( ctx && ctx->session ) {
667                 osrfAppSession* session = ctx->session;
668
669                 osrfHash* cache = session->userData;
670
671                 // If the session doesn't already have a hash, create one.  Make sure
672                 // that the application session frees the hash when it terminates.
673                 if( NULL == cache )
674                         cache = initSessionCache( ctx );
675
676                 if( user_login )
677                         osrfHashSet( cache, user_login, "user_login" );
678                 else
679                         osrfHashRemove( cache, "user_login" );
680         }
681 }
682
683 /**
684         @brief Get the user login object for the current application session, if any.
685         @param ctx Pointer to the method context.
686         @return Pointer to the user login object if found; otherwise NULL.
687
688         The user login object was returned from the authentication server, and then cached so
689         we don't have to call the authentication server again for the same user.
690 */
691 static const jsonObject* getUserLogin( osrfMethodContext* ctx ) {
692         if( ctx && ctx->session && ctx->session->userData )
693                 return osrfHashGet( (osrfHash*) ctx->session->userData, "user_login" );
694         else
695                 return NULL;
696 }
697
698 /**
699         @brief Save a copy of an authkey in the userData of the current application session.
700         @param ctx Pointer to the method context.
701         @param authkey The authkey to be saved.
702
703         If @a authkey is NULL, remove the authkey if one is already cached.
704 */
705 static void setAuthkey( osrfMethodContext* ctx, const char* authkey ) {
706         if( ctx && ctx->session && authkey ) {
707                 osrfAppSession* session = ctx->session;
708                 osrfHash* cache = session->userData;
709
710                 // If the session doesn't already have a hash, create one.  Make sure
711                 // that the application session frees the hash when it terminates.
712                 if( NULL == cache )
713                         cache = initSessionCache( ctx );
714
715                 // Save the transaction id in the hash, with the key "xact_id"
716                 if( authkey && *authkey )
717                         osrfHashSet( cache, strdup( authkey ), "authkey" );
718                 else
719                         osrfHashRemove( cache, "authkey" );
720         }
721 }
722
723 /**
724         @brief Reset the login timeout.
725         @param authkey The authentication key for the current login session.
726         @param now The current time.
727         @return Zero if successful, or 1 if not.
728
729         Tell the authentication server to reset the timeout so that the login session won't
730         expire for a while longer.
731
732         We could dispense with the @a now parameter by calling time().  But we just called
733         time() in order to decide whether to reset the timeout, so we might as well reuse
734         the result instead of calling time() again.
735 */
736 static int reset_timeout( const char* authkey, time_t now ) {
737         jsonObject* auth_object = jsonNewObject( authkey );
738
739         // Ask the authentication server to reset the timeout.  It returns an event
740         // indicating success or failure.
741         jsonObject* result = oilsUtilsQuickReq( "open-ils.auth",
742                 "open-ils.auth.session.reset_timeout", auth_object );
743         jsonObjectFree( auth_object );
744
745         if( !result || result->type != JSON_HASH ) {
746                 osrfLogError( OSRF_LOG_MARK,
747                          "Unexpected object type receieved from open-ils.auth.session.reset_timeout" );
748                 jsonObjectFree( result );
749                 return 1;       // Not the right sort of object returned
750         }
751
752         const jsonObject* ilsevent = jsonObjectGetKeyConst( result, "ilsevent" );
753         if( !ilsevent || ilsevent->type != JSON_NUMBER ) {
754                 osrfLogError( OSRF_LOG_MARK, "ilsevent is absent or malformed" );
755                 jsonObjectFree( result );
756                 return 1;    // Return code from method not available
757         }
758
759         if( jsonObjectGetNumber( ilsevent ) != 0.0 ) {
760                 const char* desc = jsonObjectGetString( jsonObjectGetKeyConst( result, "desc" ));
761                 if( !desc )
762                         desc = "(No reason available)";    // failsafe; shouldn't happen
763                 osrfLogInfo( OSRF_LOG_MARK, "Failure to reset timeout: %s", desc );
764                 jsonObjectFree( result );
765                 return 1;
766         }
767
768         // Revise our local proxy for the timeout deadline
769         // by a smallish fraction of the timeout interval
770         const char* timeout = jsonObjectGetString( jsonObjectGetKeyConst( result, "payload" ));
771         if( !timeout )
772                 timeout = "1";   // failsafe; shouldn't happen
773         time_next_reset = now + atoi( timeout ) / 15;
774
775         jsonObjectFree( result );
776         return 0;     // Successfully reset timeout
777 }
778
779 /**
780         @brief Get the authkey string for the current application session, if any.
781         @param ctx Pointer to the method context.
782         @return Pointer to the cached authkey if found; otherwise NULL.
783
784         If present, the authkey string was cached from a previous method call.
785 */
786 static const char* getAuthkey( osrfMethodContext* ctx ) {
787         if( ctx && ctx->session && ctx->session->userData ) {
788                 const char* authkey = osrfHashGet( (osrfHash*) ctx->session->userData, "authkey" );
789         // LFW recent changes mean the userData hash gets set up earlier, but
790         // doesn't necessarily have an authkey yet
791         if (!authkey)
792             return NULL;
793
794                 // Possibly reset the authentication timeout to keep the login alive.  We do so
795                 // no more than once per method call, and not at all if it has been only a short
796                 // time since the last reset.
797
798                 // Here we reset explicitly, if at all.  We also implicitly reset the timeout
799                 // whenever we call the "open-ils.auth.session.retrieve" method.
800                 if( timeout_needs_resetting ) {
801                         time_t now = time( NULL );
802                         if( now >= time_next_reset && reset_timeout( authkey, now ) )
803                                 authkey = NULL;    // timeout has apparently expired already
804                 }
805
806                 timeout_needs_resetting = 0;
807                 return authkey;
808         }
809         else
810                 return NULL;
811 }
812
813 /**
814         @brief Implement the transaction.begin method.
815         @param ctx Pointer to the method context.
816         @return Zero if successful, or -1 upon error.
817
818         Start a transaction.  Save a transaction ID for future reference.
819
820         Method parameters:
821         - authkey (PCRUD only)
822
823         Return to client: Transaction ID
824 */
825 int beginTransaction( osrfMethodContext* ctx ) {
826         if(osrfMethodVerifyContext( ctx )) {
827                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
828                 return -1;
829         }
830
831         const char* tz = _sanitize_tz_name(ctx->session->session_tz);
832
833         if( enforce_pcrud ) {
834                 timeout_needs_resetting = 1;
835                 const jsonObject* user = verifyUserPCRUD( ctx );
836                 if( !user )
837                         return -1;
838         }
839
840         dbi_result result = dbi_conn_query( writehandle, "START TRANSACTION;" );
841         if( !result ) {
842                 const char* msg;
843                 int errnum = dbi_conn_error( writehandle, &msg );
844                 osrfLogError( OSRF_LOG_MARK, "%s: Error starting transaction: %d %s",
845                         modulename, errnum, msg ? msg : "(No description available)" );
846                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
847                         "osrfMethodException", ctx->request, "Error starting transaction" );
848                 if( !oilsIsDBConnected( writehandle ))
849                         osrfAppSessionPanic( ctx->session );
850                 return -1;
851         } else {
852                 dbi_result_free( result );
853                 setXactId( ctx );
854                 jsonObject* ret = jsonNewObject( getXactId( ctx ) );
855                 osrfAppRespondComplete( ctx, ret );
856                 jsonObjectFree( ret );
857
858         }
859
860         if (tz) {
861                 setenv("TZ",tz,1);
862                 tzset();
863                 dbi_result tz_res = dbi_conn_queryf( writehandle, "SET LOCAL timezone TO '%s'; -- cstore", tz );
864                 if( !tz_res ) {
865                         osrfLogError( OSRF_LOG_MARK, "%s: Error setting timezone %s", modulename, tz);
866                         if( !oilsIsDBConnected( writehandle )) {
867                                 osrfAppSessionPanic( ctx->session );
868                                 return -1;
869                         }
870                 } else {
871                         dbi_result_free( tz_res );
872                 }
873         } else {
874                 unsetenv("TZ");
875                 tzset();
876                 dbi_result res = dbi_conn_queryf( writehandle, "SET timezone TO DEFAULT; -- no tz" );
877                 if( !res ) {
878                         osrfLogError( OSRF_LOG_MARK, "%s: Error resetting timezone", modulename);
879                         if( !oilsIsDBConnected( writehandle )) {
880                                 osrfAppSessionPanic( ctx->session );
881                                 return -1;
882                         }
883                 } else {
884                         dbi_result_free( res );
885                 }
886         }
887
888         return 0;
889 }
890
891 /**
892         @brief Implement the savepoint.set method.
893         @param ctx Pointer to the method context.
894         @return Zero if successful, or -1 if not.
895
896         Issue a SAVEPOINT to the database server.
897
898         Method parameters:
899         - authkey (PCRUD only)
900         - savepoint name
901
902         Return to client: Savepoint name
903 */
904 int setSavepoint( osrfMethodContext* ctx ) {
905         if(osrfMethodVerifyContext( ctx )) {
906                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
907                 return -1;
908         }
909
910         int spNamePos = 0;
911         if( enforce_pcrud ) {
912                 spNamePos = 1;
913                 timeout_needs_resetting = 1;
914                 const jsonObject* user = verifyUserPCRUD( ctx );
915                 if( !user )
916                         return -1;
917         }
918
919         // Verify that a transaction is pending
920         const char* trans_id = getXactId( ctx );
921         if( NULL == trans_id ) {
922                 osrfAppSessionStatus(
923                         ctx->session,
924                         OSRF_STATUS_INTERNALSERVERERROR,
925                         "osrfMethodException",
926                         ctx->request,
927                         "No active transaction -- required for savepoints"
928                 );
929                 return -1;
930         }
931
932         // Get the savepoint name from the method params
933         const char* spName = jsonObjectGetString( jsonObjectGetIndex(ctx->params, spNamePos) );
934
935         if (!spName) {
936                 osrfLogWarning(OSRF_LOG_MARK, "savepoint.set called with no name");
937                 return -1;
938         }
939
940         char *safeSpName = _sanitize_savepoint_name( spName );
941
942         dbi_result result = dbi_conn_queryf( writehandle, "SAVEPOINT \"%s\";", safeSpName );
943         free( safeSpName );
944         if( !result ) {
945                 const char* msg;
946                 int errnum = dbi_conn_error( writehandle, &msg );
947                 osrfLogError(
948                         OSRF_LOG_MARK,
949                         "%s: Error creating savepoint %s in transaction %s: %d %s",
950                         modulename,
951                         spName,
952                         trans_id,
953                         errnum,
954                         msg ? msg : "(No description available)"
955                 );
956                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
957                         "osrfMethodException", ctx->request, "Error creating savepoint" );
958                 if( !oilsIsDBConnected( writehandle ))
959                         osrfAppSessionPanic( ctx->session );
960                 return -1;
961         } else {
962                 dbi_result_free( result );
963                 jsonObject* ret = jsonNewObject( spName );
964                 osrfAppRespondComplete( ctx, ret );
965                 jsonObjectFree( ret  );
966                 return 0;
967         }
968 }
969
970 /**
971         @brief Implement the savepoint.release method.
972         @param ctx Pointer to the method context.
973         @return Zero if successful, or -1 if not.
974
975         Issue a RELEASE SAVEPOINT to the database server.
976
977         Method parameters:
978         - authkey (PCRUD only)
979         - savepoint name
980
981         Return to client: Savepoint name
982 */
983 int releaseSavepoint( osrfMethodContext* ctx ) {
984         if(osrfMethodVerifyContext( ctx )) {
985                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
986                 return -1;
987         }
988
989         int spNamePos = 0;
990         if( enforce_pcrud ) {
991                 spNamePos = 1;
992                 timeout_needs_resetting = 1;
993                 const jsonObject* user = verifyUserPCRUD( ctx );
994                 if(  !user )
995                         return -1;
996         }
997
998         // Verify that a transaction is pending
999         const char* trans_id = getXactId( ctx );
1000         if( NULL == trans_id ) {
1001                 osrfAppSessionStatus(
1002                         ctx->session,
1003                         OSRF_STATUS_INTERNALSERVERERROR,
1004                         "osrfMethodException",
1005                         ctx->request,
1006                         "No active transaction -- required for savepoints"
1007                 );
1008                 return -1;
1009         }
1010
1011         // Get the savepoint name from the method params
1012         const char* spName = jsonObjectGetString( jsonObjectGetIndex(ctx->params, spNamePos) );
1013
1014         if (!spName) {
1015                 osrfLogWarning(OSRF_LOG_MARK, "savepoint.release called with no name");
1016                 return -1;
1017         }
1018
1019         char *safeSpName = _sanitize_savepoint_name( spName );
1020
1021         dbi_result result = dbi_conn_queryf( writehandle, "RELEASE SAVEPOINT \"%s\";", safeSpName );
1022         free( safeSpName );
1023         if( !result ) {
1024                 const char* msg;
1025                 int errnum = dbi_conn_error( writehandle, &msg );
1026                 osrfLogError(
1027                         OSRF_LOG_MARK,
1028                         "%s: Error releasing savepoint %s in transaction %s: %d %s",
1029                         modulename,
1030                         spName,
1031                         trans_id,
1032                         errnum,
1033                         msg ? msg : "(No description available)"
1034                 );
1035                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
1036                         "osrfMethodException", ctx->request, "Error releasing savepoint" );
1037                 if( !oilsIsDBConnected( writehandle ))
1038                         osrfAppSessionPanic( ctx->session );
1039                 return -1;
1040         } else {
1041                 dbi_result_free( result );
1042                 jsonObject* ret = jsonNewObject( spName );
1043                 osrfAppRespondComplete( ctx, ret );
1044                 jsonObjectFree( ret );
1045                 return 0;
1046         }
1047 }
1048
1049 /**
1050         @brief Implement the savepoint.rollback method.
1051         @param ctx Pointer to the method context.
1052         @return Zero if successful, or -1 if not.
1053
1054         Issue a ROLLBACK TO SAVEPOINT to the database server.
1055
1056         Method parameters:
1057         - authkey (PCRUD only)
1058         - savepoint name
1059
1060         Return to client: Savepoint name
1061 */
1062 int rollbackSavepoint( osrfMethodContext* ctx ) {
1063         if(osrfMethodVerifyContext( ctx )) {
1064                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
1065                 return -1;
1066         }
1067
1068         int spNamePos = 0;
1069         if( enforce_pcrud ) {
1070                 spNamePos = 1;
1071                 timeout_needs_resetting = 1;
1072                 const jsonObject* user = verifyUserPCRUD( ctx );
1073                 if( !user )
1074                         return -1;
1075         }
1076
1077         // Verify that a transaction is pending
1078         const char* trans_id = getXactId( ctx );
1079         if( NULL == trans_id ) {
1080                 osrfAppSessionStatus(
1081                         ctx->session,
1082                         OSRF_STATUS_INTERNALSERVERERROR,
1083                         "osrfMethodException",
1084                         ctx->request,
1085                         "No active transaction -- required for savepoints"
1086                 );
1087                 return -1;
1088         }
1089
1090         // Get the savepoint name from the method params
1091         const char* spName = jsonObjectGetString( jsonObjectGetIndex(ctx->params, spNamePos) );
1092
1093         if (!spName) {
1094                 osrfLogWarning(OSRF_LOG_MARK, "savepoint.rollback called with no name");
1095                 return -1;
1096         }
1097
1098         char *safeSpName = _sanitize_savepoint_name( spName );
1099
1100         dbi_result result = dbi_conn_queryf( writehandle, "ROLLBACK TO SAVEPOINT \"%s\";", safeSpName );
1101         free( safeSpName );
1102         if( !result ) {
1103                 const char* msg;
1104                 int errnum = dbi_conn_error( writehandle, &msg );
1105                 osrfLogError(
1106                         OSRF_LOG_MARK,
1107                         "%s: Error rolling back savepoint %s in transaction %s: %d %s",
1108                         modulename,
1109                         spName,
1110                         trans_id,
1111                         errnum,
1112                         msg ? msg : "(No description available)"
1113                 );
1114                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
1115                         "osrfMethodException", ctx->request, "Error rolling back savepoint" );
1116                 if( !oilsIsDBConnected( writehandle ))
1117                         osrfAppSessionPanic( ctx->session );
1118                 return -1;
1119         } else {
1120                 dbi_result_free( result );
1121                 jsonObject* ret = jsonNewObject( spName );
1122                 osrfAppRespondComplete( ctx, ret );
1123                 jsonObjectFree( ret );
1124                 return 0;
1125         }
1126 }
1127
1128 /**
1129         @brief Implement the transaction.commit method.
1130         @param ctx Pointer to the method context.
1131         @return Zero if successful, or -1 if not.
1132
1133         Issue a COMMIT to the database server.
1134
1135         Method parameters:
1136         - authkey (PCRUD only)
1137
1138         Return to client: Transaction ID.
1139 */
1140 int commitTransaction( osrfMethodContext* ctx ) {
1141         if(osrfMethodVerifyContext( ctx )) {
1142                 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
1143                 return -1;
1144         }
1145
1146         if( enforce_pcrud ) {
1147                 timeout_needs_resetting = 1;
1148                 const jsonObject* user = verifyUserPCRUD( ctx );
1149                 if( !user )
1150                         return -1;
1151         }
1152
1153         // Verify that a transaction is pending
1154         const char* trans_id = getXactId( ctx );
1155         if( NULL == trans_id ) {
1156                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
1157                                 "osrfMethodException", ctx->request, "No active transaction to commit" );
1158                 return -1;
1159         }
1160
1161         dbi_result result = dbi_conn_query( writehandle, "COMMIT;" );
1162         if( !result ) {
1163                 const char* msg;
1164                 int errnum = dbi_conn_error( writehandle, &msg );
1165                 osrfLogError( OSRF_LOG_MARK, "%s: Error committing transaction: %d %s",
1166                         modulename, errnum, msg ? msg : "(No description available)" );
1167                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
1168                         "osrfMethodException", ctx->request, "Error committing transaction" );
1169                 if( !oilsIsDBConnected( writehandle ))
1170                         osrfAppSessionPanic( ctx->session );
1171                 return -1;
1172         } else {
1173                 dbi_result_free( result );
1174                 jsonObject* ret = jsonNewObject( trans_id );
1175                 osrfAppRespondComplete( ctx, ret );
1176                 jsonObjectFree( ret );
1177                 clearXactId( ctx );
1178                 return 0;
1179         }
1180 }
1181
1182 /**
1183         @brief Implement the transaction.rollback method.
1184         @param ctx Pointer to the method context.
1185         @return Zero if successful, or -1 if not.
1186
1187         Issue a ROLLBACK to the database server.
1188
1189         Method parameters:
1190         - authkey (PCRUD only)
1191
1192         Return to client: Transaction ID
1193 */
1194 int rollbackTransaction( osrfMethodContext* ctx ) {
1195         if( osrfMethodVerifyContext( ctx )) {
1196                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
1197                 return -1;
1198         }
1199
1200         if( enforce_pcrud ) {
1201                 timeout_needs_resetting = 1;
1202                 const jsonObject* user = verifyUserPCRUD( ctx );
1203                 if( !user )
1204                         return -1;
1205         }
1206
1207         // Verify that a transaction is pending
1208         const char* trans_id = getXactId( ctx );
1209         if( NULL == trans_id ) {
1210                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
1211                                 "osrfMethodException", ctx->request, "No active transaction to roll back" );
1212                 return -1;
1213         }
1214
1215         dbi_result result = dbi_conn_query( writehandle, "ROLLBACK;" );
1216         if( !result ) {
1217                 const char* msg;
1218                 int errnum = dbi_conn_error( writehandle, &msg );
1219                 osrfLogError( OSRF_LOG_MARK, "%s: Error rolling back transaction: %d %s",
1220                         modulename, errnum, msg ? msg : "(No description available)" );
1221                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
1222                         "osrfMethodException", ctx->request, "Error rolling back transaction" );
1223                 if( !oilsIsDBConnected( writehandle ))
1224                         osrfAppSessionPanic( ctx->session );
1225                 return -1;
1226         } else {
1227                 dbi_result_free( result );
1228                 jsonObject* ret = jsonNewObject( trans_id );
1229                 osrfAppRespondComplete( ctx, ret );
1230                 jsonObjectFree( ret );
1231                 clearXactId( ctx );
1232                 return 0;
1233         }
1234 }
1235
1236 /**
1237         @brief Implement the "search" method.
1238         @param ctx Pointer to the method context.
1239         @return Zero if successful, or -1 if not.
1240
1241         Method parameters:
1242         - authkey (PCRUD only)
1243         - WHERE clause, as jsonObject
1244         - Other SQL clause(s), as a JSON_HASH: joins, SELECT list, LIMIT, etc.
1245
1246         Return to client: rows of the specified class that satisfy a specified WHERE clause.
1247         Optionally flesh linked fields.
1248 */
1249 int doSearch( osrfMethodContext* ctx ) {
1250         if( osrfMethodVerifyContext( ctx )) {
1251                 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
1252                 return -1;
1253         }
1254
1255         if( enforce_pcrud )
1256                 timeout_needs_resetting = 1;
1257
1258         jsonObject* where_clause;
1259         jsonObject* rest_of_query;
1260
1261         if( enforce_pcrud ) {
1262                 where_clause  = jsonObjectGetIndex( ctx->params, 1 );
1263                 rest_of_query = jsonObjectGetIndex( ctx->params, 2 );
1264         } else {
1265                 where_clause  = jsonObjectGetIndex( ctx->params, 0 );
1266                 rest_of_query = jsonObjectGetIndex( ctx->params, 1 );
1267         }
1268
1269         if( !where_clause ) { 
1270                 osrfLogError( OSRF_LOG_MARK, "No WHERE clause parameter supplied" );
1271                 return -1;
1272         }
1273
1274         // Get the class metadata
1275         osrfHash* method_meta = (osrfHash*) ctx->method->userData;
1276         osrfHash* class_meta = osrfHashGet( method_meta, "class" );
1277
1278         // Do the query
1279         int err = 0;
1280         jsonObject* obj = doFieldmapperSearch( ctx, class_meta, where_clause, rest_of_query, &err );
1281         if( err ) {
1282                 osrfAppRespondComplete( ctx, NULL );
1283                 return -1;
1284         }
1285
1286         // doFieldmapperSearch() now takes care of our responding for us
1287 //      // Return each row to the client
1288 //      jsonObject* cur = 0;
1289 //      unsigned long res_idx = 0;
1290 //
1291 //      while((cur = jsonObjectGetIndex( obj, res_idx++ ) )) {
1292 //              // We used to discard based on perms here, but now that's
1293 //              // inside doFieldmapperSearch()
1294 //              osrfAppRespond( ctx, cur );
1295 //      }
1296
1297         jsonObjectFree( obj );
1298
1299         osrfAppRespondComplete( ctx, NULL );
1300         return 0;
1301 }
1302
1303 /**
1304         @brief Implement the "id_list" method.
1305         @param ctx Pointer to the method context.
1306         @param err Pointer through which to return an error code.
1307         @return Zero if successful, or -1 if not.
1308
1309         Method parameters:
1310         - authkey (PCRUD only)
1311         - WHERE clause, as jsonObject
1312         - Other SQL clause(s), as a JSON_HASH: joins, LIMIT, etc.
1313
1314         Return to client: The primary key values for all rows of the relevant class that
1315         satisfy a specified WHERE clause.
1316
1317         This method relies on the assumption that every class has a primary key consisting of
1318         a single column.
1319 */
1320 int doIdList( osrfMethodContext* ctx ) {
1321         if( osrfMethodVerifyContext( ctx )) {
1322                 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
1323                 return -1;
1324         }
1325
1326         if( enforce_pcrud )
1327                 timeout_needs_resetting = 1;
1328
1329         jsonObject* where_clause;
1330         jsonObject* rest_of_query;
1331
1332         // We use the where clause without change.  But we need to massage the rest of the
1333         // query, so we work with a copy of it instead of modifying the original.
1334
1335         if( enforce_pcrud ) {
1336                 where_clause  = jsonObjectGetIndex( ctx->params, 1 );
1337                 rest_of_query = jsonObjectClone( jsonObjectGetIndex( ctx->params, 2 ) );
1338         } else {
1339                 where_clause  = jsonObjectGetIndex( ctx->params, 0 );
1340                 rest_of_query = jsonObjectClone( jsonObjectGetIndex( ctx->params, 1 ) );
1341         }
1342
1343         if( !where_clause ) { 
1344                 osrfLogError( OSRF_LOG_MARK, "No WHERE clause parameter supplied" );
1345                 return -1;
1346         }
1347
1348         // Eliminate certain SQL clauses, if present.
1349         if( rest_of_query ) {
1350                 jsonObjectRemoveKey( rest_of_query, "select" );
1351                 jsonObjectRemoveKey( rest_of_query, "no_i18n" );
1352                 jsonObjectRemoveKey( rest_of_query, "flesh" );
1353                 jsonObjectRemoveKey( rest_of_query, "flesh_fields" );
1354         } else {
1355                 rest_of_query = jsonNewObjectType( JSON_HASH );
1356         }
1357
1358         jsonObjectSetKey( rest_of_query, "no_i18n", jsonNewBoolObject( 1 ) );
1359
1360         // Get the class metadata
1361         osrfHash* method_meta = (osrfHash*) ctx->method->userData;
1362         osrfHash* class_meta = osrfHashGet( method_meta, "class" );
1363
1364         // Build a SELECT list containing just the primary key,
1365         // i.e. like { "classname":["keyname"] }
1366         jsonObject* col_list_obj = jsonNewObjectType( JSON_ARRAY );
1367
1368         // Load array with name of primary key
1369         jsonObjectPush( col_list_obj, jsonNewObject( osrfHashGet( class_meta, "primarykey" ) ) );
1370         jsonObject* select_clause = jsonNewObjectType( JSON_HASH );
1371         jsonObjectSetKey( select_clause, osrfHashGet( class_meta, "classname" ), col_list_obj );
1372
1373         jsonObjectSetKey( rest_of_query, "select", select_clause );
1374
1375         // Do the query
1376         int err = 0;
1377         jsonObject* obj =
1378                 doFieldmapperSearch( ctx, class_meta, where_clause, rest_of_query, &err );
1379
1380         jsonObjectFree( rest_of_query );
1381         if( err ) {
1382                 osrfAppRespondComplete( ctx, NULL );
1383                 return -1;
1384         }
1385
1386         // Return each primary key value to the client
1387         jsonObject* cur;
1388         unsigned long res_idx = 0;
1389         while((cur = jsonObjectGetIndex( obj, res_idx++ ) )) {
1390                 // We used to discard based on perms here, but now that's
1391                 // inside doFieldmapperSearch()
1392                 osrfAppRespond( ctx,
1393                         oilsFMGetObject( cur, osrfHashGet( class_meta, "primarykey" ) ) );
1394         }
1395
1396         jsonObjectFree( obj );
1397         osrfAppRespondComplete( ctx, NULL );
1398         return 0;
1399 }
1400
1401 /**
1402         @brief Verify that we have a valid class reference.
1403         @param ctx Pointer to the method context.
1404         @param param Pointer to the method parameters.
1405         @return 1 if the class reference is valid, or zero if it isn't.
1406
1407         The class of the method params must match the class to which the method id devoted.
1408         For PCRUD there are additional restrictions.
1409 */
1410 static int verifyObjectClass ( osrfMethodContext* ctx, const jsonObject* param ) {
1411
1412         osrfHash* method_meta = (osrfHash*) ctx->method->userData;
1413         osrfHash* class = osrfHashGet( method_meta, "class" );
1414
1415         // Compare the method's class to the parameters' class
1416         if( !param->classname || (strcmp( osrfHashGet(class, "classname"), param->classname ))) {
1417
1418                 // Oops -- they don't match.  Complain.
1419                 growing_buffer* msg = buffer_init( 128 );
1420                 buffer_fadd(
1421                         msg,
1422                         "%s: %s method for type %s was passed a %s",
1423                         modulename,
1424                         osrfHashGet( method_meta, "methodtype" ),
1425                         osrfHashGet( class, "classname" ),
1426                         param->classname ? param->classname : "(null)"
1427                 );
1428
1429                 char* m = buffer_release( msg );
1430                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
1431                                 ctx->request, m );
1432                 free( m );
1433
1434                 return 0;
1435         }
1436
1437         if( enforce_pcrud )
1438                 return verifyObjectPCRUD( ctx, class, param, 1 );
1439         else
1440                 return 1;
1441 }
1442
1443 /**
1444         @brief (PCRUD only) Verify that the user is properly logged in.
1445         @param ctx Pointer to the method context.
1446         @return If the user is logged in, a pointer to the user object from the authentication
1447         server; otherwise NULL.
1448 */
1449 static const jsonObject* verifyUserPCRUD( osrfMethodContext* ctx ) {
1450         return verifyUserPCRUDfull( ctx, 0 );
1451 }
1452
1453 static const jsonObject* verifyUserPCRUDfull( osrfMethodContext* ctx, int anon_ok ) {
1454
1455         // Get the authkey (the first method parameter)
1456         const char* auth = jsonObjectGetString( jsonObjectGetIndex( ctx->params, 0 ) );
1457
1458         jsonObject* user = NULL;
1459
1460         // If we are /not/ in anonymous mode
1461         if( strcmp( "ANONYMOUS", auth ) ) {
1462                 // See if we have the same authkey, and a user object,
1463                 // locally cached from a previous call
1464                 const char* cached_authkey = getAuthkey( ctx );
1465                 if( cached_authkey && !strcmp( cached_authkey, auth ) ) {
1466                         const jsonObject* cached_user = getUserLogin( ctx );
1467                         if( cached_user )
1468                                 return cached_user;
1469                 }
1470
1471                 // We have no matching authentication data in the cache.  Authenticate from scratch.
1472                 jsonObject* auth_object = jsonNewObject( auth );
1473         
1474                 // Fetch the user object from the authentication server
1475                 user = oilsUtilsQuickReq( "open-ils.auth", "open-ils.auth.session.retrieve", auth_object );
1476                 jsonObjectFree( auth_object );
1477         
1478                 if( !user->classname || strcmp(user->classname, "au" )) {
1479         
1480                         growing_buffer* msg = buffer_init( 128 );
1481                         buffer_fadd(
1482                                 msg,
1483                                 "%s: permacrud received a bad auth token: %s",
1484                                 modulename,
1485                                 auth
1486                         );
1487         
1488                         char* m = buffer_release( msg );
1489                         osrfAppSessionStatus( ctx->session, OSRF_STATUS_UNAUTHORIZED, "osrfMethodException",
1490                                         ctx->request, m );
1491         
1492                         free( m );
1493                         jsonObjectFree( user );
1494                         user = NULL;
1495                 } else if( writeAuditInfo( ctx, oilsFMGetStringConst( user, "id" ), oilsFMGetStringConst( user, "wsid" ) ) ) {
1496                         // Failed to set audit information - But note that write_audit_info already set error information.
1497                         jsonObjectFree( user );
1498                         user = NULL;
1499                 }
1500
1501
1502         } else if ( anon_ok ) { // we /are/ (attempting to be) anonymous
1503                 user = jsonNewObjectType(JSON_ARRAY);
1504                 jsonObjectSetClass( user, "aou" );
1505                 oilsFMSetString(user, "id", "-1");
1506         }
1507
1508         setUserLogin( ctx, user );
1509         setAuthkey( ctx, auth );
1510
1511         // Allow ourselves up to a second before we have to reset the login timeout.
1512         // It would be nice to use some fraction of the timeout interval enforced by the
1513         // authentication server, but that value is not readily available at this point.
1514         // Instead, we use a conservative default interval.
1515         time_next_reset = time( NULL ) + 1;
1516
1517         return user;
1518 }
1519
1520 /**
1521         @brief For PCRUD: Determine whether the current user may access the current row.
1522         @param ctx Pointer to the method context.
1523         @param class Same as ctx->method->userData's item for key "class" except when called in recursive doFieldmapperSearch
1524         @param obj Pointer to the row being potentially accessed.
1525         @return 1 if access is permitted, or 0 if it isn't.
1526
1527         The @a obj parameter points to a JSON_HASH of column values, keyed on column name.
1528 */
1529 static int verifyObjectPCRUD ( osrfMethodContext* ctx, osrfHash *class, const jsonObject* obj, int rs_size ) {
1530
1531         dbhandle = writehandle;
1532
1533         // Figure out what class and method are involved
1534         osrfHash* method_metadata = (osrfHash*) ctx->method->userData;
1535         const char* method_type = osrfHashGet( method_metadata, "methodtype" );
1536
1537         if (!rs_size) {
1538                 int *rs_size_from_hash = osrfHashGetFmt( (osrfHash *) ctx->session->userData, "rs_size_req_%d", ctx->request );
1539                 if (rs_size_from_hash) {
1540                         rs_size = *rs_size_from_hash;
1541                         osrfLogDebug(OSRF_LOG_MARK, "used rs_size from request-scoped hash: %d", rs_size);
1542                 }
1543         }
1544
1545         // Set fetch to 1 in all cases except for inserts, meaning that for local or foreign
1546         // contexts we will do another lookup of the current row, even if we already have a
1547         // previously fetched row image, because the row image in hand may not include the
1548         // foreign key(s) that we need.
1549
1550         // This is a quick fix with a bludgeon.  There are ways to avoid the extra lookup,
1551         // but they aren't implemented yet.
1552
1553         int fetch = 0;
1554         if( *method_type == 's' || *method_type == 'i' ) {
1555                 method_type = "retrieve"; // search and id_list are equivalent to retrieve for this
1556                 fetch = 1;
1557         } else if( *method_type == 'u' || *method_type == 'd' ) {
1558                 fetch = 1; // MUST go to the db for the object for update and delete
1559         }
1560
1561         // In retrieve or search ONLY we allow anon. Later perm checks will fail as they should,
1562         // in the face of a fake user but required permissions.
1563         int anon_ok = 0;
1564         if( *method_type == 'r' )
1565                 anon_ok = 1;
1566
1567         // Get the appropriate permacrud entry from the IDL, depending on method type
1568         osrfHash* pcrud = osrfHashGet( osrfHashGet( class, "permacrud" ), method_type );
1569         if( !pcrud ) {
1570                 // No permacrud for this method type on this class
1571
1572                 growing_buffer* msg = buffer_init( 128 );
1573                 buffer_fadd(
1574                         msg,
1575                         "%s: %s on class %s has no permacrud IDL entry",
1576                         modulename,
1577                         osrfHashGet( method_metadata, "methodtype" ),
1578                         osrfHashGet( class, "classname" )
1579                 );
1580
1581                 char* m = buffer_release( msg );
1582                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_FORBIDDEN,
1583                                 "osrfMethodException", ctx->request, m );
1584
1585                 free( m );
1586
1587                 return 0;
1588         }
1589
1590         // Get the user id, and make sure the user is logged in
1591         const jsonObject* user = verifyUserPCRUDfull( ctx, anon_ok );
1592         if( !user )
1593                 return 0;    // Not logged in or anon?  No access.
1594
1595         int userid = atoi( oilsFMGetStringConst( user, "id" ) );
1596
1597         // Get a list of permissions from the permacrud entry.
1598         osrfStringArray* permission = osrfHashGet( pcrud, "permission" );
1599         if( permission->size == 0 ) {
1600                 osrfLogDebug(
1601                         OSRF_LOG_MARK,
1602                         "No permissions required for this action (class %s), passing through",
1603                         osrfHashGet(class, "classname")
1604                 );
1605                 return 1;
1606         }
1607
1608         // But, if there are perms and the user is anonymous ... FAIL
1609         if ( -1 == userid )
1610                 return 0;
1611
1612         // Build a list of org units that own the row.  This is fairly convoluted because there
1613         // are several different ways that an org unit may own the row, as defined by the
1614         // permacrud entry.
1615
1616         // Local context means that the row includes a foreign key pointing to actor.org_unit,
1617         // identifying an owning org_unit..
1618         osrfStringArray* local_context = osrfHashGet( pcrud, "local_context" );
1619
1620         // Foreign context adds a layer of indirection.  The row points to some other row that
1621         // an org unit may own.  The "jump" attribute, if present, adds another layer of
1622         // indirection.
1623         osrfHash* foreign_context = osrfHashGet( pcrud, "foreign_context" );
1624
1625         // The following string array stores the list of org units.  (We don't have a thingie
1626         // for storing lists of integers, so we fake it with a list of strings.)
1627         osrfStringArray* context_org_array = osrfNewStringArray( 1 );
1628
1629         const char* context_org = NULL;
1630     const char* pkey = NULL;
1631     jsonObject *param = NULL;
1632         const char* perm = NULL;
1633         int OK = 0;
1634         int i = 0;
1635         int err = 0;
1636         const char* pkey_value = NULL;
1637         if( str_is_true( osrfHashGet(pcrud, "global_required") ) ) {
1638                 // If the global_required attribute is present and true, then the only owning
1639                 // org unit is the root org unit, i.e. the one with no parent.
1640                 osrfLogDebug( OSRF_LOG_MARK,
1641                                 "global-level permissions required, fetching top of the org tree" );
1642
1643                 // no need to check perms for org tree root retrieval
1644                 osrfHashSet((osrfHash*) ctx->session->userData, "1", "inside_verify");
1645                 // check for perm at top of org tree
1646                 const char* org_tree_root_id = org_tree_root( ctx );
1647                 osrfHashSet((osrfHash*) ctx->session->userData, "0", "inside_verify");
1648
1649                 if( org_tree_root_id ) {
1650                         osrfStringArrayAdd( context_org_array, org_tree_root_id );
1651                         osrfLogDebug( OSRF_LOG_MARK, "top of the org tree is %s", org_tree_root_id );
1652                 } else  {
1653                         osrfStringArrayFree( context_org_array );
1654                         return 0;
1655                 }
1656
1657         } else {
1658                 // If the global_required attribute is absent or false, then we look for
1659                 // local and/or foreign context.  In order to find the relevant foreign
1660                 // keys, we must either read the relevant row from the database, or look at
1661                 // the image of the row that we already have in memory.
1662
1663                 // Even if we have an image of the row in memory, that image may not include the
1664                 // foreign key column(s) that we need.  So whenever possible, we do a fresh read
1665                 // of the row to make sure that we have what we need.
1666
1667             osrfLogDebug( OSRF_LOG_MARK, "global-level permissions not required, "
1668                                 "fetching context org ids" );
1669
1670         pkey = osrfHashGet( class, "primarykey" );
1671
1672                 if( !pkey ) {
1673                         // There is no primary key, so we can't do a fresh lookup.  Use the row
1674                         // image that we already have.  If it doesn't have everything we need, too bad.
1675                         fetch = 0;
1676                         param = jsonObjectClone( obj );
1677                         osrfLogDebug( OSRF_LOG_MARK, "No primary key; using clone of object" );
1678                 } else if( obj->classname ) {
1679                         pkey_value = oilsFMGetStringConst( obj, pkey );
1680                         if( !fetch )
1681                                 param = jsonObjectClone( obj );
1682                         osrfLogDebug( OSRF_LOG_MARK, "Object supplied, using primary key value of %s",
1683                                 pkey_value );
1684                 } else {
1685                         pkey_value = jsonObjectGetString( obj );
1686                         fetch = 1;
1687                         osrfLogDebug( OSRF_LOG_MARK, "Object not supplied, using primary key value "
1688                                 "of %s and retrieving from the database", pkey_value );
1689                 }
1690
1691                 if( fetch ) {
1692                         // Fetch the row so that we can look at the foreign key(s)
1693                         osrfHashSet((osrfHash*) ctx->session->userData, "1", "inside_verify");
1694                         jsonObject* _tmp_params = single_hash( pkey, pkey_value );
1695                         jsonObject* _list = doFieldmapperSearch( ctx, class, _tmp_params, NULL, &err );
1696                         jsonObjectFree( _tmp_params );
1697                         osrfHashSet((osrfHash*) ctx->session->userData, "0", "inside_verify");
1698
1699                         param = jsonObjectExtractIndex( _list, 0 );
1700                         jsonObjectFree( _list );
1701
1702             fetch = 0;
1703                 }
1704
1705                 if( !param ) {
1706                         // The row doesn't exist.  Complain, and deny access.
1707                         osrfLogDebug( OSRF_LOG_MARK,
1708                                         "Object not found in the database with primary key %s of %s",
1709                                         pkey, pkey_value );
1710
1711                         growing_buffer* msg = buffer_init( 128 );
1712                         buffer_fadd(
1713                                 msg,
1714                                 "%s: no object found with primary key %s of %s",
1715                                 modulename,
1716                                 pkey,
1717                                 pkey_value
1718                         );
1719
1720                         char* m = buffer_release( msg );
1721                         osrfAppSessionStatus(
1722                                 ctx->session,
1723                                 OSRF_STATUS_INTERNALSERVERERROR,
1724                                 "osrfMethodException",
1725                                 ctx->request,
1726                                 m
1727                         );
1728
1729                         free( m );
1730                         return 0;
1731                 }
1732
1733                 if( local_context && local_context->size > 0 ) {
1734                         // The IDL provides a list of column names for the foreign keys denoting
1735                         // local context, i.e. columns identifying owing org units directly.  Look up
1736                         // the value of each one, and if it isn't null, add it to the list of org units.
1737                         osrfLogDebug( OSRF_LOG_MARK, "%d class-local context field(s) specified",
1738                                 local_context->size );
1739                         int i = 0;
1740                         const char* lcontext = NULL;
1741                         while ( (lcontext = osrfStringArrayGetString(local_context, i++)) ) {
1742                                 const char* fkey_value = oilsFMGetStringConst( param, lcontext );
1743                                 if( fkey_value ) {    // if not null
1744                                         osrfStringArrayAdd( context_org_array, fkey_value );
1745                                         osrfLogDebug(
1746                                                 OSRF_LOG_MARK,
1747                                                 "adding class-local field %s (value: %s) to the context org list",
1748                                                 lcontext,
1749                                                 osrfStringArrayGetString( context_org_array, context_org_array->size - 1 )
1750                                         );
1751                                 }
1752                         }
1753                 }
1754
1755                 if( foreign_context ) {
1756                         unsigned long class_count = osrfHashGetCount( foreign_context );
1757                         osrfLogDebug( OSRF_LOG_MARK, "%d foreign context classes(s) specified", class_count );
1758
1759                         if( class_count > 0 ) {
1760
1761                                 // The IDL provides a list of foreign key columns pointing to rows that
1762                                 // an org unit may own.  Follow each link, identify the owning org unit,
1763                                 // and add it to the list.
1764                                 osrfHash* fcontext = NULL;
1765                                 osrfHashIterator* class_itr = osrfNewHashIterator( foreign_context );
1766                                 while( (fcontext = osrfHashIteratorNext( class_itr )) ) {
1767                                         // For each class to which a foreign key points:
1768                                         const char* class_name = osrfHashIteratorKey( class_itr );
1769                                         osrfHash* fcontext = osrfHashGet( foreign_context, class_name );
1770
1771                                         osrfLogDebug(
1772                                                 OSRF_LOG_MARK,
1773                                                 "%d foreign context fields(s) specified for class %s",
1774                                                 ((osrfStringArray*)osrfHashGet(fcontext,"context"))->size,
1775                                                 class_name
1776                                         );
1777
1778                                         // Get the name of the key field in the foreign table
1779                                         const char* foreign_pkey = osrfHashGet( fcontext, "field" );
1780
1781                                         // Get the value of the foreign key pointing to the foreign table
1782                                         char* foreign_pkey_value =
1783                                                         oilsFMGetString( param, osrfHashGet( fcontext, "fkey" ));
1784                                         if( !foreign_pkey_value )
1785                                                 continue;    // Foreign key value is null; skip it
1786
1787                                         // Look up the row to which the foreign key points
1788                                         jsonObject* _tmp_params = single_hash( foreign_pkey, foreign_pkey_value );
1789
1790                                         osrfHashSet((osrfHash*) ctx->session->userData, "1", "inside_verify");
1791                                         jsonObject* _list = doFieldmapperSearch(
1792                                                 ctx, osrfHashGet( oilsIDL(), class_name ), _tmp_params, NULL, &err );
1793                                         osrfHashSet((osrfHash*) ctx->session->userData, "0", "inside_verify");
1794
1795                                         jsonObject* _fparam = NULL;
1796                                         if( _list && JSON_ARRAY == _list->type && _list->size > 0 )
1797                                                 _fparam = jsonObjectExtractIndex( _list, 0 );
1798
1799                                         jsonObjectFree( _tmp_params );
1800                                         jsonObjectFree( _list );
1801
1802                                         // At this point _fparam either points to the row identified by the
1803                                         // foreign key, or it's NULL (no such row found).
1804
1805                                         osrfStringArray* jump_list = osrfHashGet( fcontext, "jump" );
1806
1807                                         const char* bad_class = NULL;  // For noting failed lookups
1808                                         if( ! _fparam )
1809                                                 bad_class = class_name;    // Referenced row not found
1810                                         else if( jump_list ) {
1811                                                 // Follow a chain of rows, linked by foreign keys, to find an owner
1812                                                 const char* flink = NULL;
1813                                                 int k = 0;
1814                                                 while ( (flink = osrfStringArrayGetString(jump_list, k++)) && _fparam ) {
1815                                                         // For each entry in the jump list.  Each entry (i.e. flink) is
1816                                                         // the name of a foreign key column in the current row.
1817
1818                                                         // From the IDL, get the linkage information for the next jump
1819                                                         osrfHash* foreign_link_hash =
1820                                                                         oilsIDLFindPath( "/%s/links/%s", _fparam->classname, flink );
1821
1822                                                         // Get the class metadata for the class
1823                                                         // to which the foreign key points
1824                                                         osrfHash* foreign_class_meta = osrfHashGet( oilsIDL(),
1825                                                                         osrfHashGet( foreign_link_hash, "class" ));
1826
1827                                                         // Get the name of the referenced key of that class
1828                                                         foreign_pkey = osrfHashGet( foreign_link_hash, "key" );
1829
1830                                                         // Get the value of the foreign key pointing to that class
1831                                                         free( foreign_pkey_value );
1832                                                         foreign_pkey_value = oilsFMGetString( _fparam, flink );
1833                                                         if( !foreign_pkey_value )
1834                                                                 break;    // Foreign key is null; quit looking
1835
1836                                                         // Build a WHERE clause for the lookup
1837                                                         _tmp_params = single_hash( foreign_pkey, foreign_pkey_value );
1838
1839                                                         // Do the lookup
1840                                                         osrfHashSet((osrfHash*) ctx->session->userData, "1", "inside_verify");
1841                                                         _list = doFieldmapperSearch( ctx, foreign_class_meta,
1842                                                                         _tmp_params, NULL, &err );
1843                                                         osrfHashSet((osrfHash*) ctx->session->userData, "0", "inside_verify");
1844
1845                                                         // Get the resulting row
1846                                                         jsonObjectFree( _fparam );
1847                                                         if( _list && JSON_ARRAY == _list->type && _list->size > 0 )
1848                                                                 _fparam = jsonObjectExtractIndex( _list, 0 );
1849                                                         else {
1850                                                                 // Referenced row not found
1851                                                                 _fparam = NULL;
1852                                                                 bad_class = osrfHashGet( foreign_link_hash, "class" );
1853                                                         }
1854
1855                                                         jsonObjectFree( _tmp_params );
1856                                                         jsonObjectFree( _list );
1857                                                 }
1858                                         }
1859
1860                                         if( bad_class ) {
1861
1862                                                 // We had a foreign key pointing to such-and-such a row, but then
1863                                                 // we couldn't fetch that row.  The data in the database are in an
1864                                                 // inconsistent state; the database itself may even be corrupted.
1865                                                 growing_buffer* msg = buffer_init( 128 );
1866                                                 buffer_fadd(
1867                                                         msg,
1868                                                         "%s: no object of class %s found with primary key %s of %s",
1869                                                         modulename,
1870                                                         bad_class,
1871                                                         foreign_pkey,
1872                                                         foreign_pkey_value ? foreign_pkey_value : "(null)"
1873                                                 );
1874
1875                                                 char* m = buffer_release( msg );
1876                                                 osrfAppSessionStatus(
1877                                                         ctx->session,
1878                                                         OSRF_STATUS_INTERNALSERVERERROR,
1879                                                         "osrfMethodException",
1880                                                         ctx->request,
1881                                                         m
1882                                                 );
1883
1884                                                 free( m );
1885                                                 osrfHashIteratorFree( class_itr );
1886                                                 free( foreign_pkey_value );
1887                                                 jsonObjectFree( param );
1888
1889                                                 return 0;
1890                                         }
1891
1892                                         free( foreign_pkey_value );
1893
1894                                         if( _fparam ) {
1895                                                 // Examine each context column of the foreign row,
1896                                                 // and add its value to the list of org units.
1897                                                 int j = 0;
1898                                                 const char* foreign_field = NULL;
1899                                                 osrfStringArray* ctx_array = osrfHashGet( fcontext, "context" );
1900                                                 while ( (foreign_field = osrfStringArrayGetString( ctx_array, j++ )) ) {
1901                                                         osrfStringArrayAdd( context_org_array,
1902                                                                 oilsFMGetStringConst( _fparam, foreign_field ));
1903                                                         osrfLogDebug( OSRF_LOG_MARK,
1904                                                                 "adding foreign class %s field %s (value: %s) "
1905                                                                         "to the context org list",
1906                                                                 class_name,
1907                                                                 foreign_field,
1908                                                                 osrfStringArrayGetString(
1909                                                                         context_org_array, context_org_array->size - 1 )
1910                                                         );
1911                                                 }
1912
1913                                                 jsonObjectFree( _fparam );
1914                                         }
1915                                 }
1916
1917                                 osrfHashIteratorFree( class_itr );
1918                         }
1919                 }
1920         }
1921
1922     // If there is an owning_user attached to the action, we allow that user and users with
1923     // object perms on the object. CREATE can't use this. We only do this when we're not
1924     // ignoring object perms.
1925     char* owning_user_field = osrfHashGet( pcrud, "owning_user" );
1926     if (
1927         *method_type != 'c' &&
1928         (!str_is_true( osrfHashGet(pcrud, "ignore_object_perms") ) || // Always honor
1929         owning_user_field)
1930     ) {
1931         if (owning_user_field) { // see if we can short-cut by comparing the owner to the requestor
1932
1933             if (!param) { // We didn't get it during the context lookup
1934                             pkey = osrfHashGet( class, "primarykey" );
1935                 
1936                                 if( !pkey  ) {
1937                                         // There is no primary key, so we can't do a fresh lookup.  Use the row
1938                                         // image that we already have.  If it doesn't have everything we need, too bad.
1939                                         fetch = 0;
1940                                         param = jsonObjectClone( obj );
1941                                         osrfLogDebug( OSRF_LOG_MARK, "No primary key; using clone of object" );
1942                                 } else if( obj->classname ) {
1943                                         pkey_value = oilsFMGetStringConst( obj, pkey );
1944                                         if( !fetch )
1945                                                 param = jsonObjectClone( obj );
1946                                         osrfLogDebug( OSRF_LOG_MARK, "Object supplied, using primary key value of %s",
1947                                                 pkey_value );
1948                                 } else {
1949                                         pkey_value = jsonObjectGetString( obj );
1950                                         fetch = 1;
1951                                         osrfLogDebug( OSRF_LOG_MARK, "Object not supplied, using primary key value "
1952                                                 "of %s and retrieving from the database", pkey_value );
1953                                 }
1954                 
1955                                 if( fetch ) {
1956                                         // Fetch the row so that we can look at the foreign key(s)
1957                                         osrfHashSet((osrfHash*) ctx->session->userData, "1", "inside_verify");
1958                                         jsonObject* _tmp_params = single_hash( pkey, pkey_value );
1959                                         jsonObject* _list = doFieldmapperSearch( ctx, class, _tmp_params, NULL, &err );
1960                                         jsonObjectFree( _tmp_params );
1961                                         osrfHashSet((osrfHash*) ctx->session->userData, "0", "inside_verify");
1962                 
1963                                         param = jsonObjectExtractIndex( _list, 0 );
1964                                         jsonObjectFree( _list );
1965                                 }
1966             }
1967         
1968                         if( !param ) {
1969                                 // The row doesn't exist.  Complain, and deny access.
1970                                 osrfLogDebug( OSRF_LOG_MARK,
1971                                                 "Object not found in the database with primary key %s of %s",
1972                                                 pkey, pkey_value );
1973         
1974                                 growing_buffer* msg = buffer_init( 128 );
1975                                 buffer_fadd(
1976                                         msg,
1977                                         "%s: no object found with primary key %s of %s",
1978                                         modulename,
1979                                         pkey,
1980                                         pkey_value
1981                                 );
1982         
1983                                 char* m = buffer_release( msg );
1984                                 osrfAppSessionStatus(
1985                                         ctx->session,
1986                                         OSRF_STATUS_INTERNALSERVERERROR,
1987                                         "osrfMethodException",
1988                                         ctx->request,
1989                                         m
1990                                 );
1991         
1992                                 free( m );
1993                                 return 0;
1994                         } else {
1995
1996                 int ownerid = atoi( oilsFMGetStringConst( param, owning_user_field ) );
1997
1998                 // Allow the owner to do whatever
1999                 if (ownerid == userid)
2000                     OK = 1;
2001                         }
2002                 }
2003
2004         i = 0;
2005         while(  !OK &&
2006                                 (perm = osrfStringArrayGetString(permission, i++)) &&
2007                                 !str_is_true( osrfHashGet(pcrud, "ignore_object_perms"))
2008                 ) {
2009                 dbi_result result;
2010
2011                         osrfLogDebug(
2012                                 OSRF_LOG_MARK,
2013                                 "Checking object permission [%s] for user %d "
2014                                                 "on object %s (class %s)",
2015                                 perm,
2016                                 userid,
2017                                 pkey_value,
2018                                 osrfHashGet( class, "classname" )
2019                         );
2020
2021                         result = dbi_conn_queryf(
2022                                 writehandle,
2023                                 "SELECT permission.usr_has_object_perm(%d, '%s', '%s', '%s') AS has_perm;",
2024                                 userid,
2025                                 perm,
2026                                 osrfHashGet( class, "classname" ),
2027                                 pkey_value
2028                         );
2029
2030                         if( result ) {
2031                                 osrfLogDebug(
2032                                         OSRF_LOG_MARK,
2033                                         "Received a result for object permission [%s] "
2034                                                         "for user %d on object %s (class %s)",
2035                                         perm,
2036                                         userid,
2037                                         pkey_value,
2038                                         osrfHashGet( class, "classname" )
2039                                 );
2040
2041                                 if( dbi_result_first_row( result )) {
2042                                         jsonObject* return_val = oilsMakeJSONFromResult( result );
2043                                         const char* has_perm = jsonObjectGetString(
2044                                                         jsonObjectGetKeyConst( return_val, "has_perm" ));
2045
2046                                         osrfLogDebug(
2047                                                 OSRF_LOG_MARK,
2048                                                 "Status of object permission [%s] for user %d "
2049                                                                 "on object %s (class %s) is %s",
2050                                                 perm,
2051                                                 userid,
2052                                                 pkey_value,
2053                                                 osrfHashGet(class, "classname"),
2054                                                 has_perm
2055                                         );
2056
2057                                         if( *has_perm == 't' )
2058                                                 OK = 1;
2059                                         jsonObjectFree( return_val );
2060                                 }
2061
2062                                 dbi_result_free( result );
2063                                 if( OK )
2064                     break;
2065                         } else {
2066                                 const char* msg;
2067                                 int errnum = dbi_conn_error( writehandle, &msg );
2068                                 osrfLogWarning( OSRF_LOG_MARK,
2069                                         "Unable to call check object permissions: %d, %s",
2070                                         errnum, msg ? msg : "(No description available)" );
2071                                 if( !oilsIsDBConnected( writehandle ))
2072                                         osrfAppSessionPanic( ctx->session );
2073                         }
2074         }
2075     }
2076
2077         // For every combination of permission and context org unit: call a stored procedure
2078         // to determine if the user has this permission in the context of this org unit.
2079         // If the answer is yes at any point, then we're done, and the user has permission.
2080         // In other words permissions are additive.
2081         i = 0;
2082         while( !OK && (perm = osrfStringArrayGetString(permission, i++)) ) {
2083                 dbi_result result;
2084
2085         osrfStringArray* pcache = NULL;
2086         if (rs_size > perm_at_threshold) { // grab and cache locations of user perms
2087                         pcache = getPermLocationCache(ctx, perm);
2088
2089                         if (!pcache) {
2090                         pcache = osrfNewStringArray(0);
2091         
2092                                 result = dbi_conn_queryf(
2093                                         writehandle,
2094                                         "SELECT permission.usr_has_perm_at_all(%d, '%s') AS at;",
2095                                         userid,
2096                                         perm
2097                                 );
2098                 
2099                                 if( result ) {
2100                                         osrfLogDebug(
2101                                                 OSRF_LOG_MARK,
2102                                                 "Received a result for permission [%s] for user %d",
2103                                                 perm,
2104                                                 userid
2105                                         );
2106                 
2107                                         if( dbi_result_first_row( result )) {
2108                             do {
2109                                                 jsonObject* return_val = oilsMakeJSONFromResult( result );
2110                                                 osrfStringArrayAdd( pcache, jsonObjectGetString( jsonObjectGetKeyConst( return_val, "at" ) ) );
2111                                 jsonObjectFree( return_val );
2112                                             } while( dbi_result_next_row( result ));
2113
2114                                                 setPermLocationCache(ctx, perm, pcache);
2115                                         }
2116                 
2117                                         dbi_result_free( result );
2118                     }
2119                         }
2120         }
2121
2122                 int j = 0;
2123                 while( (context_org = osrfStringArrayGetString( context_org_array, j++ )) ) {
2124
2125             if (rs_size > perm_at_threshold) {
2126                 if (osrfStringArrayContains( pcache, context_org )) {
2127                     OK = 1;
2128                     break;
2129                 }
2130             }
2131
2132                         if(
2133                 pkey_value &&
2134                 !str_is_true( osrfHashGet(pcrud, "ignore_object_perms") ) && // Always honor
2135                 (
2136                     !str_is_true( osrfHashGet(pcrud, "global_required") ) ||
2137                     osrfHashGet(pcrud, "owning_user") 
2138                 )
2139             ) {
2140                                 osrfLogDebug(
2141                                         OSRF_LOG_MARK,
2142                                         "Checking object permission [%s] for user %d "
2143                                                         "on object %s (class %s) at org %d",
2144                                         perm,
2145                                         userid,
2146                                         pkey_value,
2147                                         osrfHashGet( class, "classname" ),
2148                                         atoi( context_org )
2149                                 );
2150
2151                                 result = dbi_conn_queryf(
2152                                         writehandle,
2153                                         "SELECT permission.usr_has_object_perm(%d, '%s', '%s', '%s', %d) AS has_perm;",
2154                                         userid,
2155                                         perm,
2156                                         osrfHashGet( class, "classname" ),
2157                                         pkey_value,
2158                                         atoi( context_org )
2159                                 );
2160
2161                                 if( result ) {
2162                                         osrfLogDebug(
2163                                                 OSRF_LOG_MARK,
2164                                                 "Received a result for object permission [%s] "
2165                                                                 "for user %d on object %s (class %s) at org %d",
2166                                                 perm,
2167                                                 userid,
2168                                                 pkey_value,
2169                                                 osrfHashGet( class, "classname" ),
2170                                                 atoi( context_org )
2171                                         );
2172
2173                                         if( dbi_result_first_row( result )) {
2174                                                 jsonObject* return_val = oilsMakeJSONFromResult( result );
2175                                                 const char* has_perm = jsonObjectGetString(
2176                                                                 jsonObjectGetKeyConst( return_val, "has_perm" ));
2177
2178                                                 osrfLogDebug(
2179                                                         OSRF_LOG_MARK,
2180                                                         "Status of object permission [%s] for user %d "
2181                                                                         "on object %s (class %s) at org %d is %s",
2182                                                         perm,
2183                                                         userid,
2184                                                         pkey_value,
2185                                                         osrfHashGet(class, "classname"),
2186                                                         atoi(context_org),
2187                                                         has_perm
2188                                                 );
2189
2190                                                 if( *has_perm == 't' )
2191                                                         OK = 1;
2192                                                 jsonObjectFree( return_val );
2193                                         }
2194
2195                                         dbi_result_free( result );
2196                                         if( OK )
2197                                                 break;
2198                                 } else {
2199                                         const char* msg;
2200                                         int errnum = dbi_conn_error( writehandle, &msg );
2201                                         osrfLogWarning( OSRF_LOG_MARK,
2202                                                 "Unable to call check object permissions: %d, %s",
2203                                                 errnum, msg ? msg : "(No description available)" );
2204                                         if( !oilsIsDBConnected( writehandle ))
2205                                                 osrfAppSessionPanic( ctx->session );
2206                                 }
2207                         }
2208
2209             if (rs_size > perm_at_threshold) break;
2210
2211                         osrfLogDebug( OSRF_LOG_MARK,
2212                                         "Checking non-object permission [%s] for user %d at org %d",
2213                                         perm, userid, atoi(context_org) );
2214                         result = dbi_conn_queryf(
2215                                 writehandle,
2216                                 "SELECT permission.usr_has_perm(%d, '%s', %d) AS has_perm;",
2217                                 userid,
2218                                 perm,
2219                                 atoi( context_org )
2220                         );
2221
2222                         if( result ) {
2223                                 osrfLogDebug( OSRF_LOG_MARK,
2224                                         "Received a result for permission [%s] for user %d at org %d",
2225                                         perm, userid, atoi( context_org ));
2226                                 if( dbi_result_first_row( result )) {
2227                                         jsonObject* return_val = oilsMakeJSONFromResult( result );
2228                                         const char* has_perm = jsonObjectGetString(
2229                                                 jsonObjectGetKeyConst( return_val, "has_perm" ));
2230                                         osrfLogDebug( OSRF_LOG_MARK,
2231                                                 "Status of permission [%s] for user %d at org %d is [%s]",
2232                                                 perm, userid, atoi( context_org ), has_perm );
2233                                         if( *has_perm == 't' )
2234                                                 OK = 1;
2235                                         jsonObjectFree( return_val );
2236                                 }
2237
2238                                 dbi_result_free( result );
2239                                 if( OK )
2240                                         break;
2241                         } else {
2242                                 const char* msg;
2243                                 int errnum = dbi_conn_error( writehandle, &msg );
2244                                 osrfLogWarning( OSRF_LOG_MARK, "Unable to call user object permissions: %d, %s",
2245                                         errnum, msg ? msg : "(No description available)" );
2246                                 if( !oilsIsDBConnected( writehandle ))
2247                                         osrfAppSessionPanic( ctx->session );
2248                         }
2249
2250                 }
2251
2252                 if( OK )
2253                         break;
2254         }
2255
2256         osrfStringArrayFree( context_org_array );
2257
2258         return OK;
2259 }
2260
2261 /**
2262         @brief Look up the root of the org_unit tree.
2263         @param ctx Pointer to the method context.
2264         @return The id of the root org unit, as a character string.
2265
2266         Query actor.org_unit where parent_ou is null, and return the id as a string.
2267
2268         This function assumes that there is only one root org unit, i.e. that we
2269         have a single tree, not a forest.
2270
2271         The calling code is responsible for freeing the returned string.
2272 */
2273 static const char* org_tree_root( osrfMethodContext* ctx ) {
2274
2275         static char cached_root_id[ 32 ] = "";  // extravagantly large buffer
2276         static time_t last_lookup_time = 0;
2277         time_t current_time = time( NULL );
2278
2279         if( cached_root_id[ 0 ] && ( current_time - last_lookup_time < 3600 ) ) {
2280                 // We successfully looked this up less than an hour ago.
2281                 // It's not likely to have changed since then.
2282                 return strdup( cached_root_id );
2283         }
2284         last_lookup_time = current_time;
2285
2286         int err = 0;
2287         jsonObject* where_clause = single_hash( "parent_ou", NULL );
2288         jsonObject* result = doFieldmapperSearch(
2289                 ctx, osrfHashGet( oilsIDL(), "aou" ), where_clause, NULL, &err );
2290         jsonObjectFree( where_clause );
2291
2292         jsonObject* tree_top = jsonObjectGetIndex( result, 0 );
2293
2294         if( !tree_top ) {
2295                 jsonObjectFree( result );
2296
2297                 growing_buffer* msg = buffer_init( 128 );
2298                 OSRF_BUFFER_ADD( msg, modulename );
2299                 OSRF_BUFFER_ADD( msg,
2300                                 ": Internal error, could not find the top of the org tree (parent_ou = NULL)" );
2301
2302                 char* m = buffer_release( msg );
2303                 osrfAppSessionStatus( ctx->session,
2304                                 OSRF_STATUS_INTERNALSERVERERROR, "osrfMethodException", ctx->request, m );
2305                 free( m );
2306
2307                 cached_root_id[ 0 ] = '\0';
2308                 return NULL;
2309         }
2310
2311         const char* root_org_unit_id = oilsFMGetStringConst( tree_top, "id" );
2312         osrfLogDebug( OSRF_LOG_MARK, "Top of the org tree is %s", root_org_unit_id );
2313
2314         strcpy( cached_root_id, root_org_unit_id );
2315         jsonObjectFree( result );
2316         return cached_root_id;
2317 }
2318
2319 /**
2320         @brief Create a JSON_HASH with a single key/value pair.
2321         @param key The key of the key/value pair.
2322         @param value the value of the key/value pair.
2323         @return Pointer to a newly created jsonObject of type JSON_HASH.
2324
2325         The value of the key/value is either a string or (if @a value is NULL) a null.
2326 */
2327 static jsonObject* single_hash( const char* key, const char* value ) {
2328         // Sanity check
2329         if( ! key ) key = "";
2330
2331         jsonObject* hash = jsonNewObjectType( JSON_HASH );
2332         jsonObjectSetKey( hash, key, jsonNewObject( value ) );
2333         return hash;
2334 }
2335
2336
2337 int doCreate( osrfMethodContext* ctx ) {
2338         if(osrfMethodVerifyContext( ctx )) {
2339                 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
2340                 return -1;
2341         }
2342
2343         if( enforce_pcrud )
2344                 timeout_needs_resetting = 1;
2345
2346         osrfHash* meta = osrfHashGet( (osrfHash*) ctx->method->userData, "class" );
2347         jsonObject* target = NULL;
2348         jsonObject* options = NULL;
2349
2350         if( enforce_pcrud ) {
2351                 target = jsonObjectGetIndex( ctx->params, 1 );
2352                 options = jsonObjectGetIndex( ctx->params, 2 );
2353         } else {
2354                 target = jsonObjectGetIndex( ctx->params, 0 );
2355                 options = jsonObjectGetIndex( ctx->params, 1 );
2356         }
2357
2358         if( !verifyObjectClass( ctx, target )) {
2359                 osrfAppRespondComplete( ctx, NULL );
2360                 return -1;
2361         }
2362
2363         osrfLogDebug( OSRF_LOG_MARK, "Object seems to be of the correct type" );
2364
2365         const char* trans_id = getXactId( ctx );
2366         if( !trans_id ) {
2367                 osrfLogError( OSRF_LOG_MARK, "No active transaction -- required for CREATE" );
2368
2369                 osrfAppSessionStatus(
2370                         ctx->session,
2371                         OSRF_STATUS_BADREQUEST,
2372                         "osrfMethodException",
2373                         ctx->request,
2374                         "No active transaction -- required for CREATE"
2375                 );
2376                 osrfAppRespondComplete( ctx, NULL );
2377                 return -1;
2378         }
2379
2380         // The following test is harmless but redundant.  If a class is
2381         // readonly, we don't register a create method for it.
2382         if( str_is_true( osrfHashGet( meta, "readonly" ) ) ) {
2383                 osrfAppSessionStatus(
2384                         ctx->session,
2385                         OSRF_STATUS_BADREQUEST,
2386                         "osrfMethodException",
2387                         ctx->request,
2388                         "Cannot INSERT readonly class"
2389                 );
2390                 osrfAppRespondComplete( ctx, NULL );
2391                 return -1;
2392         }
2393
2394         // Set the last_xact_id
2395         int index = oilsIDL_ntop( target->classname, "last_xact_id" );
2396         if( index > -1 ) {
2397                 osrfLogDebug(OSRF_LOG_MARK, "Setting last_xact_id to %s on %s at position %d",
2398                         trans_id, target->classname, index);
2399                 jsonObjectSetIndex( target, index, jsonNewObject( trans_id ));
2400         }
2401
2402         osrfLogDebug( OSRF_LOG_MARK, "There is a transaction running..." );
2403
2404         dbhandle = writehandle;
2405
2406         osrfHash* fields = osrfHashGet( meta, "fields" );
2407         char* pkey       = osrfHashGet( meta, "primarykey" );
2408         char* seq        = osrfHashGet( meta, "sequence" );
2409
2410         growing_buffer* table_buf = buffer_init( 128 );
2411         growing_buffer* col_buf   = buffer_init( 128 );
2412         growing_buffer* val_buf   = buffer_init( 128 );
2413
2414         OSRF_BUFFER_ADD( table_buf, "INSERT INTO " );
2415         OSRF_BUFFER_ADD( table_buf, osrfHashGet( meta, "tablename" ));
2416         OSRF_BUFFER_ADD_CHAR( col_buf, '(' );
2417         buffer_add( val_buf,"VALUES (" );
2418
2419
2420         int first = 1;
2421         osrfHash* field = NULL;
2422         osrfHashIterator* field_itr = osrfNewHashIterator( fields );
2423         while( (field = osrfHashIteratorNext( field_itr ) ) ) {
2424
2425                 const char* field_name = osrfHashIteratorKey( field_itr );
2426
2427                 if( str_is_true( osrfHashGet( field, "virtual" ) ) )
2428                         continue;
2429
2430                 const jsonObject* field_object = oilsFMGetObject( target, field_name );
2431
2432                 char* value;
2433                 if( field_object && field_object->classname ) {
2434                         value = oilsFMGetString(
2435                                 field_object,
2436                                 (char*)oilsIDLFindPath( "/%s/primarykey", field_object->classname )
2437                         );
2438                 } else if( field_object && JSON_BOOL == field_object->type ) {
2439                         if( jsonBoolIsTrue( field_object ) )
2440                                 value = strdup( "t" );
2441                         else
2442                                 value = strdup( "f" );
2443                 } else {
2444                         value = jsonObjectToSimpleString( field_object );
2445                 }
2446
2447                 if( first ) {
2448                         first = 0;
2449                 } else {
2450                         OSRF_BUFFER_ADD_CHAR( col_buf, ',' );
2451                         OSRF_BUFFER_ADD_CHAR( val_buf, ',' );
2452                 }
2453
2454                 buffer_add( col_buf, field_name );
2455
2456                 if( !field_object || field_object->type == JSON_NULL ) {
2457                         buffer_add( val_buf, "DEFAULT" );
2458
2459                 } else if( !strcmp( get_primitive( field ), "number" )) {
2460                         const char* numtype = get_datatype( field );
2461                         if( !strcmp( numtype, "INT8" )) {
2462                                 buffer_fadd( val_buf, "%lld", atoll( value ));
2463
2464                         } else if( !strcmp( numtype, "INT" )) {
2465                                 buffer_fadd( val_buf, "%d", atoi( value ));
2466
2467                         } else if( !strcmp( numtype, "NUMERIC" )) {
2468                                 buffer_fadd( val_buf, "%f", atof( value ));
2469                         }
2470                 } else {
2471                         if( dbi_conn_quote_string( writehandle, &value )) {
2472                                 OSRF_BUFFER_ADD( val_buf, value );
2473
2474                         } else {
2475                                 osrfLogError( OSRF_LOG_MARK, "%s: Error quoting string [%s]", modulename, value );
2476                                 osrfAppSessionStatus(
2477                                         ctx->session,
2478                                         OSRF_STATUS_INTERNALSERVERERROR,
2479                                         "osrfMethodException",
2480                                         ctx->request,
2481                                         "Error quoting string -- please see the error log for more details"
2482                                 );
2483                                 free( value );
2484                                 buffer_free( table_buf );
2485                                 buffer_free( col_buf );
2486                                 buffer_free( val_buf );
2487                                 osrfAppRespondComplete( ctx, NULL );
2488                                 return -1;
2489                         }
2490                 }
2491
2492                 free( value );
2493         }
2494
2495         osrfHashIteratorFree( field_itr );
2496
2497         OSRF_BUFFER_ADD_CHAR( col_buf, ')' );
2498         OSRF_BUFFER_ADD_CHAR( val_buf, ')' );
2499
2500         char* table_str = buffer_release( table_buf );
2501         char* col_str   = buffer_release( col_buf );
2502         char* val_str   = buffer_release( val_buf );
2503         growing_buffer* sql = buffer_init( 128 );
2504         buffer_fadd( sql, "%s %s %s;", table_str, col_str, val_str );
2505         free( table_str );
2506         free( col_str );
2507         free( val_str );
2508
2509         char* query = buffer_release( sql );
2510
2511         osrfLogDebug( OSRF_LOG_MARK, "%s: Insert SQL [%s]", modulename, query );
2512
2513         jsonObject* obj = NULL;
2514         int rc = 0;
2515
2516         dbi_result result = dbi_conn_query( writehandle, query );
2517         if( !result ) {
2518                 obj = jsonNewObject( NULL );
2519                 const char* msg;
2520                 int errnum = dbi_conn_error( writehandle, &msg );
2521                 osrfLogError(
2522                         OSRF_LOG_MARK,
2523                         "%s ERROR inserting %s object using query [%s]: %d %s",
2524                         modulename,
2525                         osrfHashGet(meta, "fieldmapper"),
2526                         query,
2527                         errnum,
2528                         msg ? msg : "(No description available)"
2529                 );
2530                 osrfAppSessionStatus(
2531                         ctx->session,
2532                         OSRF_STATUS_INTERNALSERVERERROR,
2533                         "osrfMethodException",
2534                         ctx->request,
2535                         "INSERT error -- please see the error log for more details"
2536                 );
2537                 if( !oilsIsDBConnected( writehandle ))
2538                         osrfAppSessionPanic( ctx->session );
2539                 rc = -1;
2540         } else {
2541                 dbi_result_free( result );
2542
2543                 char* id = oilsFMGetString( target, pkey );
2544                 if( !id ) {
2545                         unsigned long long new_id = dbi_conn_sequence_last( writehandle, seq );
2546                         growing_buffer* _id = buffer_init( 10 );
2547                         buffer_fadd( _id, "%lld", new_id );
2548                         id = buffer_release( _id );
2549                 }
2550
2551                 // Find quietness specification, if present
2552                 const char* quiet_str = NULL;
2553                 if( options ) {
2554                         const jsonObject* quiet_obj = jsonObjectGetKeyConst( options, "quiet" );
2555                         if( quiet_obj )
2556                                 quiet_str = jsonObjectGetString( quiet_obj );
2557                 }
2558
2559                 if( str_is_true( quiet_str )) {  // if quietness is specified
2560                         obj = jsonNewObject( id );
2561                 }
2562                 else {
2563
2564                         // Fetch the row that we just inserted, so that we can return it to the client
2565                         jsonObject* where_clause = jsonNewObjectType( JSON_HASH );
2566                         jsonObjectSetKey( where_clause, pkey, jsonNewObject( id ));
2567
2568                         int err = 0;
2569                         jsonObject* list = doFieldmapperSearch( ctx, meta, where_clause, NULL, &err );
2570                         if( err )
2571                                 rc = -1;
2572                         else
2573                                 obj = jsonObjectClone( jsonObjectGetIndex( list, 0 ));
2574
2575                         jsonObjectFree( list );
2576                         jsonObjectFree( where_clause );
2577                 }
2578
2579                 free( id );
2580         }
2581
2582         free( query );
2583         osrfAppRespondComplete( ctx, obj );
2584         jsonObjectFree( obj );
2585         return rc;
2586 }
2587
2588 /**
2589         @brief Implement the retrieve method.
2590         @param ctx Pointer to the method context.
2591         @param err Pointer through which to return an error code.
2592         @return If successful, a pointer to the result to be returned to the client;
2593         otherwise NULL.
2594
2595         From the method's class, fetch a row with a specified value in the primary key.  This
2596         method relies on the database design convention that a primary key consists of a single
2597         column.
2598
2599         Method parameters:
2600         - authkey (PCRUD only)
2601         - value of the primary key for the desired row, for building the WHERE clause
2602         - a JSON_HASH containing any other SQL clauses: select, join, etc.
2603
2604         Return to client: One row from the query.
2605 */
2606 int doRetrieve( osrfMethodContext* ctx ) {
2607         if(osrfMethodVerifyContext( ctx )) {
2608                 osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
2609                 return -1;
2610         }
2611
2612         if( enforce_pcrud )
2613                 timeout_needs_resetting = 1;
2614
2615         int id_pos = 0;
2616         int order_pos = 1;
2617
2618         if( enforce_pcrud ) {
2619                 id_pos = 1;
2620                 order_pos = 2;
2621         }
2622
2623         // Get the class metadata
2624         osrfHash* class_def = osrfHashGet( (osrfHash*) ctx->method->userData, "class" );
2625
2626         // Get the value of the primary key, from a method parameter
2627         const jsonObject* id_obj = jsonObjectGetIndex( ctx->params, id_pos );
2628
2629         osrfLogDebug(
2630                 OSRF_LOG_MARK,
2631                 "%s retrieving %s object with primary key value of %s",
2632                 modulename,
2633                 osrfHashGet( class_def, "fieldmapper" ),
2634                 jsonObjectGetString( id_obj )
2635         );
2636
2637         // Build a WHERE clause based on the key value
2638         jsonObject* where_clause = jsonNewObjectType( JSON_HASH );
2639         jsonObjectSetKey(
2640                 where_clause,
2641                 osrfHashGet( class_def, "primarykey" ),  // name of key column
2642                 jsonObjectClone( id_obj )                // value of key column
2643         );
2644
2645         jsonObject* rest_of_query = jsonObjectGetIndex( ctx->params, order_pos );
2646
2647         // Do the query
2648         int err = 0;
2649         jsonObject* list = doFieldmapperSearch( ctx, class_def, where_clause, rest_of_query, &err );
2650
2651         jsonObjectFree( where_clause );
2652         if( err ) {
2653                 osrfAppRespondComplete( ctx, NULL );
2654                 return -1;
2655         }
2656
2657         jsonObject* obj = jsonObjectExtractIndex( list, 0 );
2658         jsonObjectFree( list );
2659
2660         if( enforce_pcrud ) {
2661                 // no result, skip this entirely
2662                 if(NULL != obj && !verifyObjectPCRUD( ctx, class_def, obj, 1 )) {
2663                         jsonObjectFree( obj );
2664
2665                         growing_buffer* msg = buffer_init( 128 );
2666                         OSRF_BUFFER_ADD( msg, modulename );
2667                         OSRF_BUFFER_ADD( msg, ": Insufficient permissions to retrieve object" );
2668
2669                         char* m = buffer_release( msg );
2670                         osrfAppSessionStatus( ctx->session, OSRF_STATUS_NOTALLOWED, "osrfMethodException",
2671                                         ctx->request, m );
2672                         free( m );
2673
2674                         osrfAppRespondComplete( ctx, NULL );
2675                         return -1;
2676                 }
2677         }
2678
2679         // doFieldmapperSearch() now does the responding for us
2680         //osrfAppRespondComplete( ctx, obj );
2681         osrfAppRespondComplete( ctx, NULL );
2682
2683         jsonObjectFree( obj );
2684         return 0;
2685 }
2686
2687 /**
2688         @brief Translate a numeric value to a string representation for the database.
2689         @param field Pointer to the IDL field definition.
2690         @param value Pointer to a jsonObject holding the value of a field.
2691         @return Pointer to a newly allocated string.
2692
2693         The input object is typically a JSON_NUMBER, but it may be a JSON_STRING as long as
2694         its contents are numeric.  A non-numeric string is likely to result in invalid SQL.
2695
2696         If the datatype of the receiving field is not numeric, wrap the value in quotes.
2697
2698         The calling code is responsible for freeing the resulting string by calling free().
2699 */
2700 static char* jsonNumberToDBString( osrfHash* field, const jsonObject* value ) {
2701         growing_buffer* val_buf = buffer_init( 32 );
2702
2703         // If the value is a number and the DB field is numeric, no quotes needed
2704         if( value->type == JSON_NUMBER && !strcmp( get_primitive( field ), "number") ) {
2705                 buffer_fadd( val_buf, jsonObjectGetString( value ) );
2706         } else {
2707                 // Presumably this was really intended to be a string, so quote it
2708                 char* str = jsonObjectToSimpleString( value );
2709                 if( dbi_conn_quote_string( dbhandle, &str )) {
2710                         OSRF_BUFFER_ADD( val_buf, str );
2711                         free( str );
2712                 } else {
2713                         osrfLogError( OSRF_LOG_MARK, "%s: Error quoting key string [%s]", modulename, str );
2714                         free( str );
2715                         buffer_free( val_buf );
2716                         return NULL;
2717                 }
2718         }
2719
2720         return buffer_release( val_buf );
2721 }
2722
2723 static char* searchINPredicate( const char* class_alias, osrfHash* field,
2724                 jsonObject* node, const char* op, osrfMethodContext* ctx ) {
2725         growing_buffer* sql_buf = buffer_init( 32 );
2726
2727         buffer_fadd(
2728                 sql_buf,
2729                 "\"%s\".%s ",
2730                 class_alias,
2731                 osrfHashGet( field, "name" )
2732         );
2733
2734         if( !op ) {
2735                 buffer_add( sql_buf, "IN (" );
2736         } else if( !strcasecmp( op,"not in" )) {
2737                 buffer_add( sql_buf, "NOT IN (" );
2738         } else {
2739                 buffer_add( sql_buf, "IN (" );
2740         }
2741
2742         if( node->type == JSON_HASH ) {
2743                 // subquery predicate
2744                 char* subpred = buildQuery( ctx, node, SUBSELECT );
2745                 if( ! subpred ) {
2746                         buffer_free( sql_buf );
2747                         return NULL;
2748                 }
2749
2750                 buffer_add( sql_buf, subpred );
2751                 free( subpred );
2752
2753         } else if( node->type == JSON_ARRAY ) {
2754                 // literal value list
2755                 int in_item_index = 0;
2756                 int in_item_first = 1;
2757                 const jsonObject* in_item;
2758                 while( (in_item = jsonObjectGetIndex( node, in_item_index++ )) ) {
2759
2760                         if( in_item_first )
2761                                 in_item_first = 0;
2762                         else
2763                                 buffer_add( sql_buf, ", " );
2764
2765                         // Sanity check
2766                         if( in_item->type != JSON_STRING && in_item->type != JSON_NUMBER ) {
2767                                 osrfLogError( OSRF_LOG_MARK,
2768                                                 "%s: Expected string or number within IN list; found %s",
2769                                                 modulename, json_type( in_item->type ) );
2770                                 buffer_free( sql_buf );
2771                                 return NULL;
2772                         }
2773
2774                         // Append the literal value -- quoted if not a number
2775                         if( JSON_NUMBER == in_item->type ) {
2776                                 char* val = jsonNumberToDBString( field, in_item );
2777                                 OSRF_BUFFER_ADD( sql_buf, val );
2778                                 free( val );
2779
2780                         } else if( !strcmp( get_primitive( field ), "number" )) {
2781                                 char* val = jsonNumberToDBString( field, in_item );
2782                                 OSRF_BUFFER_ADD( sql_buf, val );
2783                                 free( val );
2784
2785                         } else {
2786                                 char* key_string = jsonObjectToSimpleString( in_item );
2787                                 if( dbi_conn_quote_string( dbhandle, &key_string )) {
2788                                         OSRF_BUFFER_ADD( sql_buf, key_string );
2789                                         free( key_string );
2790                                 } else {
2791                                         osrfLogError( OSRF_LOG_MARK,
2792                                                         "%s: Error quoting key string [%s]", modulename, key_string );
2793                                         free( key_string );
2794                                         buffer_free( sql_buf );
2795                                         return NULL;
2796                                 }
2797                         }
2798                 }
2799
2800                 if( in_item_first ) {
2801                         osrfLogError(OSRF_LOG_MARK, "%s: Empty IN list", modulename );
2802                         buffer_free( sql_buf );
2803                         return NULL;
2804                 }
2805         } else {
2806                 osrfLogError( OSRF_LOG_MARK, "%s: Expected object or array for IN clause; found %s",
2807                         modulename, json_type( node->type ));
2808                 buffer_free( sql_buf );
2809                 return NULL;
2810         }
2811
2812         OSRF_BUFFER_ADD_CHAR( sql_buf, ')' );
2813
2814         return buffer_release( sql_buf );
2815 }
2816
2817 // Receive a JSON_ARRAY representing a function call.  The first
2818 // entry in the array is the function name.  The rest are parameters.
2819 static char* searchValueTransform( const jsonObject* array ) {
2820
2821         if( array->size < 1 ) {
2822                 osrfLogError( OSRF_LOG_MARK, "%s: Empty array for value transform", modulename );
2823                 return NULL;
2824         }
2825
2826         // Get the function name
2827         jsonObject* func_item = jsonObjectGetIndex( array, 0 );
2828         if( func_item->type != JSON_STRING ) {
2829                 osrfLogError( OSRF_LOG_MARK, "%s: Error: expected function name, found %s",
2830                         modulename, json_type( func_item->type ));
2831                 return NULL;
2832         }
2833
2834         growing_buffer* sql_buf = buffer_init( 32 );
2835
2836         OSRF_BUFFER_ADD( sql_buf, jsonObjectGetString( func_item ) );
2837         OSRF_BUFFER_ADD( sql_buf, "( " );
2838
2839         // Get the parameters
2840         int func_item_index = 1;   // We already grabbed the zeroth entry
2841         while( (func_item = jsonObjectGetIndex( array, func_item_index++ )) ) {
2842
2843                 // Add a separator comma, if we need one
2844                 if( func_item_index > 2 )
2845                         buffer_add( sql_buf, ", " );
2846
2847                 // Add the current parameter
2848                 if( func_item->type == JSON_NULL ) {
2849                         buffer_add( sql_buf, "NULL" );
2850                 } else {
2851                         if( func_item->type == JSON_BOOL ) {
2852                                 if( jsonBoolIsTrue(func_item) ) {
2853                                         buffer_add( sql_buf, "TRUE" );
2854                                 } else {
2855                                         buffer_add( sql_buf, "FALSE" );
2856                                 }
2857                         } else {
2858                                 char* val = jsonObjectToSimpleString( func_item );
2859                                 if( dbi_conn_quote_string( dbhandle, &val )) {
2860                                         OSRF_BUFFER_ADD( sql_buf, val );
2861                                         free( val );
2862                                 } else {
2863                                         osrfLogError( OSRF_LOG_MARK, 
2864                                                 "%s: Error quoting key string [%s]", modulename, val );
2865                                         buffer_free( sql_buf );
2866                                         free( val );
2867                                         return NULL;
2868                                 }
2869                         }
2870                 }
2871         }
2872
2873         buffer_add( sql_buf, " )" );
2874
2875         return buffer_release( sql_buf );
2876 }
2877
2878 static char* searchFunctionPredicate( const char* class_alias, osrfHash* field,
2879                 const jsonObject* node, const char* op ) {
2880
2881         if( ! is_good_operator( op ) ) {
2882                 osrfLogError( OSRF_LOG_MARK, "%s: Invalid operator [%s]", modulename, op );
2883                 return NULL;
2884         }
2885
2886         char* val = searchValueTransform( node );
2887         if( !val )
2888                 return NULL;
2889
2890         const char* right_percent = "";
2891         const char* real_op       = op;
2892
2893         if( !strcasecmp( op, "startwith") ) {
2894                 real_op = "like";
2895                 right_percent = "|| '%'";
2896         }
2897
2898         growing_buffer* sql_buf = buffer_init( 32 );
2899         buffer_fadd(
2900                 sql_buf,
2901                 "\"%s\".%s %s %s%s",
2902                 class_alias,
2903                 osrfHashGet( field, "name&qu