diff --git a/lib/linguist/languages.yml b/lib/linguist/languages.yml
index 765e6e96fc..6c7b00d5b7 100644
--- a/lib/linguist/languages.yml
+++ b/lib/linguist/languages.yml
@@ -718,6 +718,19 @@ LLVM:
extensions:
- .ll
+Lasso:
+ type: programming
+ lexer: Lasso
+ ace_mode: lasso
+ color: "#2584c3"
+ primary_extension: .lasso
+ extensions:
+ - .inc
+ - .las
+ - .lasso
+ - .lasso9
+ - .ldml
+
LilyPond:
lexer: Text only
primary_extension: .ly
diff --git a/samples/Lasso/database.inc b/samples/Lasso/database.inc
new file mode 100644
index 0000000000..1757cc074a
--- /dev/null
+++ b/samples/Lasso/database.inc
@@ -0,0 +1,1351 @@
+settable: removed reference for -table
+2009-09-18 JS Syntax adjustments for Lasso 9
+2009-06-26 JS ->nextrecord: Added deprecation warning
+2009-05-15 JS ->field: corrected the verification of the -index parameter
+2009-01-09 JS Added a check before calling resultset_count so it will not break in Lasso versions before 8.5
+2009-01-09 JS ->_unknowntag: fixed incorrect debug_trace
+2008-12-03 JS ->addrecord: improved how keyvalue is returned when adding records
+2008-12-03 JS ->addrecord: inserting a generated keyvalue can now be suppressed by specifying -keyvalue=false
+2008-12-03 JS ->saverecord and ->deleterecord will now use the current keyvalue (if any), so -keyvalue will not have to be specified in that case.
+2008-11-25 JS ->field and ->recorddata will no longer touch current_record if it was zero
+2008-11-24 JS ->field: Added -index parameter to be able to access any occurrence of the same field name
+2008-11-24 JS Added -> records that returns a new data type knop_databaserows
+2008-11-24 JS ->resultset_count: added support for -inlinename.
+2008-11-24 JS Changed ->nextrecord to ->next. ->nextrecord remains supported for backwards compatibility.
+2008-11-14 JS ->nextrecord resets the record pointer when reaching the last record
+2008-11-13 JS ->recorddata now honors the current record pointer (as incremented by -nextrecord)
+2008-11-13 JS ->recorddata: added -recordindex parameter so a specific record can be returned instead of the first found.
+2008-10-30 JS ->getrecord now REALLY works with integer keyvalues (double oops) - I thought I fixed it 2008-05-28 but misplaced a paren...
+2008-09-26 JS Added -> resultset_count corresponding to the same Lasso tag, so [resultset]...[/resultset] can now be used through the use of inlinename.
+2008-09-10 JS -> getrecord, ->saverecord, ->deleterecord: Corrected handling of lock user to work better with knop_user
+2008-07-09 JS ->saverecord: -keeplock now updates the lock timestamp
+2008-05-28 JS ->getrecord now works with integer keyvalues (oops)
+2008-05-27 JS ->get returns a new datatype knop_databaserow
+2008-05-27 JS Added ->size and ->get so a database object can be iterated. When iterating each row is returned as an array of field values.
+2008-05-27 JS Addedd ->nextrecord that increments the recordpointer each time it is called until the last record in the found set is reached. Returns true as long as there are more records. Useful in a while loop - see example below
+2008-05-27 JS Implemented record pointer 'current_record'. The record pointer is reset for each new query.
+2008-05-27 JS ->field: added -recordindex to get data from any record in the current found set
+2008-05-27 JS Added ->_unknowntag as shortcut to field
+2008-05-26 JS Removed onassign since it causes touble
+2008-05-26 JS Extended field_names to return the field names for any specified table, return field names also for db objects that have never been used for a database query and optionally return field types
+2008-01-29 JS ->getrecord now supports -sql. Make sure that the SQL statement includes the relevant keyfield (and lockfield if locking is used).
+2008-01-10 JS ->capturesearchvars: error_code and error_msg was mysteriously not set after database operations that caused errors.
+2008-01-08 JS ->saverecord: added flag -keeplock to be able to save a locked record without releasing the lock
+2007-12-15 JS Adding support for knop_user in record locking is in progress. Done for ->oncreate and ->getrecord.
+2007-12-11 JS Moved error_code and error_msg to knop_base
+2007-12-11 JS Added documentation as -description to most member tags, to be used by the new ->help tag
+2007-12-11 JS Moved ->help to knop_base
+2007-12-10 JS Added ->settable to be able to copy an existing database object and properly set a new table name for it. Faster than creating a new instance from scratch.
+2007-12-03 JS Corrected shown_first once again, hoping it's right this time
+2007-11-29 JS Added support for field_names and corresponding member tag ->field_names
+2007-11-05 JS Added var name to trace output
+2007-10-26 JS ->capturesearchvars: corrected shown_first when no records found
+2007-10-26 JS ->oncreate: added default value "keyfield" if the -keyfield parameter is not specified
+2007-09-06 JS Corrected self -> 'tagtime' typo
+2007-06-18 JS Added tag timer to most member tags
+2007-06-13 JS added inheritance from knop_base
+2007-06-11 JC added handling of xhtml output
+2007-05-30 JS Changed recordid_value to keyfield_value and -recordid to -keyvalue
+2007-05-28 JS ->oncreate: Added clearing of current error at beginning of tag
+2007-04-19 JS Corrected the handling of -maxrecords and -skiprecords for SQL selects that have LIMIT specified
+2007-04-19 JS Improved handling of foundrows so it finds any whitespace around SQL keywords, instead of just plain spaces
+2007-04-18 JS ->select now populates recorddata with all the fields for the first found record. Previously it only populated recorddata when there was 1 found record.
+2007-04-12 JS ->oncreate: Added authentication inline around Database_TableNames../Database_TableNames
+2007-04-10 JS ->oncreate: Improved validation of table name (table_realname can sometimes be null even for valid table names)
+2007-04-03 JS Changed namespace from mt_ to knop_
+2007-02-02 JS Improved reporting of Lasso error messaged in error_msg
+2007-01-30 JS Added real error codes and additional error data for some errors (like record locked)
+2007-01-30 JS Changed -keyvalue parameters to copy value instead of pass as reference, to not cause problems when using keyvalue from the same db object as is being updated, for example $db->(saverecord: -keyvalue=$db->keyvalue)
+2007-01-26 JS Adjusted affectedrecord_keyvalue so it's only captured for -add and -update
+2007-01-23 JS Supports -uselimit (or querys that use LIMIT) and still gets proper searchresult vars (using a separate COUNT(*) query) - may not always get the right result for example for queries with GROUP BY
+2007-01-23 JS -keyfield can be specified for saverecord to override the default
+2007-01-23 JS Changed name of ->updaterecord to ->saverecord
+2007-01-23 JS Fixed bug where keyfield was missing as returnfield when looking up locked record for deleterecord
+2007-01-23 JS Added ->field
+2007-01-19 JS Added maxrecords_value and skiprecords_value to searchresultvars
+2007-01-18 JS Added affectedrecord_keyvalue to make it possible to highlight affected record in record list (grid)
+
+
+TODO:
+Allow -keyfield to be specified for ->addrecord and ->deleterecord
+Add some Active Record similar functionality for editing
+Look at making it so -table can be set dynamically instead of fixed at oncreate, to eliminate the need for one db object for each table. This can cause problems with record locks and how they interact with knop_user
+datetime_create and datetime_mod, and also user_create and user_mod.
+ Use default field names but allow to override at oncreate, and verify them at oncreate before trying to use them.
+
+
+*/
+
+ // instance variables
+ // these variables are set once
+ local: 'database'=string,
+ 'table'=string,
+ 'table_realname'=string, // the actual table name, to be used in SQL statements (in case the table name is aliased in Lasso)
+ 'username'=string,
+ 'password'=string,
+ 'db_connect'=array,
+ 'host'=array, // add support for inline host method
+ 'datasource_name'=string,
+ 'isfilemaker'=false,
+ 'lock_expires'=1800, // seconds before a record lock expires
+ 'lock_seed'=knop_seed, // encryption seed for the record lock
+ 'error_lang'=(knop_lang: -default='en', -fallback),
+ 'user'=null, // knop_user that will be used for record locking
+ 'databaserows_map'=map; // map to hold databaserows for each inlinename
+
+ // these variables are set for each query
+ local: 'inlinename'=string, // the inlinename that holds the result of the latest db operation
+ 'keyfield'=string,
+ 'keyvalue'=null,
+ 'affectedrecord_keyvalue'=null, // keyvalue of last added or updated record (not reset by other db actions)
+ 'lockfield'=string,
+ 'lockvalue'=null,
+ 'lockvalue_encrypted'=null,
+ 'timestampfield'=string, // for optimistic locking
+ 'timestampvalue'=string,
+ 'searchparams'=string, // the resulting pair array used in the database action
+ 'querytime'=integer, // query time in ms
+ // 'tagtime'=integer, moved to knop_base
+ 'recorddata'=map, // for single record results, a map of all returned db fields
+ 'error_data'=map, // additional data for certain errors
+ 'message'=string, // user message for normal result
+ 'current_record'=integer, // index of the current record to get field values from a specific record
+ 'field_names_map'=map,
+ 'resultset_count_map'=map; // resultset_count stored for each inlinename
+ // these vars have directly corresponding Lasso tags so they can be set programatically
+ local: 'searchresultvars'=(array: 'action_statement', 'found_count', 'shown_first',
+ 'shown_last', 'shown_count', 'field_names', 'records_array', 'maxrecords_value', 'skiprecords_value');
+ iterate: #searchresultvars, (local: 'resultvar');
+ local(#resultvar = null);
+ /iterate;
+
+ local: 'errors_error_data'=(map: 7010, 7012, 7013, 7016, 7018, 7019); // these error codes can have more info in error_data map
+
+ define_tag: 'oncreate',
+ -required='database',
+ -required='table',
+ -optional='host', // add support for inline host method
+ -optional='username',
+ -optional='password',
+ -optional='keyfield',
+ -optional='lockfield',
+ -optional='user',
+ -optional='validate'; // validate the database connection info (adds the overhead of making a test connection to the database)
+ local: 'timer'=knop_timer;
+
+ // reset error
+ error_code = 0;
+ error_msg = error_noerror;
+
+ // validate database and table names to make sure they exist in Lasso
+ (self -> 'datasource_name') = Lasso_DatasourceModuleName: #database;
+ fail_if: error_code != 0, error_code, error_msg;
+
+ // store params as instance variables
+ local_defined('database') ? (self -> 'database') = @#database;
+ local_defined('table') ? (self -> 'table') = @#table;
+ local_defined('host') ? (self -> 'host') = @#host; // add support for inline host method
+ local_defined('username') ? (self -> 'username') = @#username;
+ local_defined('password') ? (self -> 'password') = @#password;
+ local_defined('lockfield') ? (self -> 'lockfield') = @#lockfield;
+ local_defined('user') ? (self -> 'user') = @#user;
+ // param has default value
+ (self -> 'keyfield') = (local_defined('keyfield')
+ ? @#keyfield // use parameter value
+ | 'keyfield'); // use default value
+
+
+ // build inline connection array
+ local_defined('database') ? (self -> 'db_connect') -> insert('-database' = @#database);
+ local_defined('table') ? (self -> 'db_connect') -> insert('-table' = @#table);
+ local_defined('host') ? (self -> 'db_connect') -> insert('-host' = @#host); // add support for inline host method
+ local_defined('username') ? (self -> 'db_connect') -> insert('-username' = @#username);
+ local_defined('password') ? (self -> 'db_connect') -> insert('-password' = @#password);
+
+ (self -> 'table_realname') = (table_realname: #database, #table);
+ if: (self -> 'table_realname') == null;
+ // verify that the table exists even if table_realname is null
+ inline: (self -> 'db_connect');
+ Database_TableNames: #database;
+ if: Database_TableNameItem == #table;
+ (self -> 'table_realname') = #table;
+ loop_abort;
+ /if;
+ /Database_TableNames;
+ /inline;
+ /if;
+ fail_if: (self -> 'table_realname') == null, 7001, self -> error_msg(7001); // The specified table was not found
+
+ if: (local_defined: 'validate');
+ // validate db connection
+ inline: (self -> 'db_connect');
+ fail_if: error_code != 0, error_code, error_msg;
+ /inline;
+ /if;
+
+ if: Lasso_DatasourceIsFilemaker: #database || Lasso_DatasourceIsFilemakerSA: #database;
+ (self -> 'isfilemaker') = true;
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': creating database object on ' + (self -> 'datasource_name') +', isfilemaker: ' + (self -> 'isfilemaker') + ' at ' + (date -> (format: '%Q %T')));
+
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+
+ /define_tag;
+
+ /*
+ define_tag: 'onassign', -required='value', -description='Internal, needed to restore references when ctype is defined as prototype';
+ // recreate references here
+ (self -> 'user') = @(#value -> 'user');
+ /define_tag;
+ */
+
+ define_tag('_unknowntag', -description='Shortcut to field');
+ if((self -> 'field_names_map') >> tag_name);
+ return(self -> field(tag_name));
+ else;
+ //fail(-9948, self -> type + '->' + tag_name + ' not known.');
+ (self -> 'debug_trace') -> insert(self -> type + '->' + tag_name + ' not known.');
+ /if;
+ /define_tag;
+
+ define_tag: 'settable', -description='Changes the current table for a database object. Useful to be able to create \
+ database objects faster by copying an existing object and just change the table name. This is a little bit faster \
+ than creating a new instance from scratch, but no table validation is performed. Only do this to add database \
+ objects for tables within the same database as the original database object. ',
+ -required='table', -type='string';
+ local: 'timer'=knop_timer;
+
+ (self -> 'error_code')=0;
+ (self -> 'error_msg')=string;
+ (self -> 'table_realname') = #table;
+ (self -> 'db_connect') -> removeall(#table);
+ (self -> 'db_connect') -> (insert: '-table' = #table);
+ (self -> 'table_realname') = (table_realname: self -> 'database', #table);
+ if: (self -> 'table_realname') == null;
+ // verify that the table exists even if table_realname is null
+ inline: (self -> 'db_connect');
+ Database_TableNames: (self -> 'database');
+ if: Database_TableNameItem == #table;
+ (self -> 'table_realname') = #table;
+ loop_abort;
+ /if;
+ /Database_TableNames;
+ /inline;
+ /if;
+ fail_if: (self -> 'table_realname') == null, 7001, self -> error_msg(7001); // The specified table was not found
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ define_tag: 'select', -description='perform database query, either Lasso-style pair array or SQL statement.\
+ ->recorddata returns a map with all the fields for the first found record. \
+ If multiple records are returned, the records can be accessed either through ->inlinename or ->records_array.\n\
+ Parameters:\n\
+ -search (optional array) Lasso-style search parameters in pair array\n\
+ -sql (optional string) Raw sql query\n\
+ -keyfield (optional) Overrides default keyfield, if any\n\
+ -keyvalue (optional)\n\
+ -inlinename (optional) Defaults to autocreated inlinename',
+ -optional='search', -type='array',
+ -optional='sql', -type='string',
+ -optional='keyfield',
+ -optional='keyvalue', -copy,
+ -optional='inlinename', -copy;
+
+ knop_debug(self->type + ' -> ' + tag_name, -open, -type=self->type);
+ handle;
+ //knop_debug(-close, -witherrors, -type=self->type);
+ knop_debug('Done with ' + self->type + ' -> ' + tag_name, -close, -witherrors, -time);
+ /handle;
+ local: 'timer'=knop_timer;
+
+ // clear all search result vars
+ self -> reset;
+
+ local: '_search'=(local: 'search'),
+ '_sql'=(local: 'sql');
+ if: #_search -> type != 'array';
+ #_search = array;
+ /if;
+ if: #_sql != '' && (self -> 'isfilemaker');
+ #_sql='';
+ fail: 7009, self -> error_msg(7009); // sql can not be used with filemaker
+ /if;
+ // inlinename defaults to a random string
+ (self -> 'inlinename') = ((local: 'inlinename') != '' ? #inlinename | 'inline_' + knop_unique);
+ #_search -> (removeall: -inlinename);
+ #_search -> (insert: -inlinename=(self -> 'inlinename'));
+
+ // remove all database actions from the search array
+ #_search -> (removeall: -search) & (removeall: -add) & (removeall: -delete) & (removeall: -update)
+ & (removeall: -sql) & (removeall: -nothing) & (removeall: -show)
+ // & (removeall: -table) // table is ok to override
+ & (removeall: -database);
+
+ if: (local: 'sql') != '' && (string_findregexp: #sql, -find='\\bLIMIT\\b', -ignorecase) -> size;
+ (self -> 'debug_trace') -> (insert: tag_name + ': grabbing -maxrecords and -skiprecords from search array');
+ // store maxrecords and skiprecords for later use
+ if: #_search >> '-maxrecords';
+ (self -> 'maxrecords_value') = #_search -> (find: '-maxrecords') -> last -> value;
+ (self -> 'debug_trace') -> (insert: tag_name + ': -maxrecords value found in search array ' + (self -> 'maxrecords_value'));
+ /if;
+ if: #_search >> '-skiprecords';
+ (self -> 'skiprecords_value') = #_search -> (find: '-skiprecords') -> last -> value;
+ (self -> 'debug_trace') -> (insert: tag_name + ': -skiprecords value found in search array ' + (self -> 'skiprecords_value'));
+ /if;
+ // remove skiprecords from the actual search parameters since it will conflict with LIMIT
+ #_search -> (removeall: '-skiprecords');
+ /if;
+
+ if: !(local_defined: 'keyfield') && (self -> 'keyfield') != '';
+ local: 'keyfield'=(self -> 'keyfield');
+ /if;
+ if: (local: 'keyfield') != '';
+ #_search -> (removeall: '-keyfield');
+ if: !(self -> 'isfilemaker');
+ #_search -> (insert: '-keyfield'=#keyfield);
+ /if;
+ if: (local: 'keyvalue') != '';
+ #_search -> (removeall: '-keyvalue');
+ if: (self -> 'isfilemaker');
+ #_search -> (insert: '-op'='eq');
+ #_search -> (insert: #keyfield=#keyvalue);
+ else;
+ #_search -> (insert: '-keyvalue'=#keyvalue);
+ /if;
+ /if;
+ /if;
+
+ // add sql action or normal search action
+ if: #_sql != '';
+ #_search -> (insert: '-sql'=#_sql);
+ else;
+ #_search -> (insert: '-search');
+ /if;
+ // perform database query, put connection parameters last to override any provided by the search parameters
+ //(self -> 'debug_trace') -> (insert: tag_name + ': search ' + #_search);
+ local: 'querytimer'=knop_timer;
+ inline: #_search,(self -> 'db_connect');
+ (self -> 'querytime') = integer: #querytimer;
+ (self -> 'searchparams') = #_search;
+ (self -> 'debug_trace') -> (insert: tag_name ': action_statement ' + action_statement);
+ knop_debug(action_statement, -sql);
+ knop_debug(found_count ' found');
+ self -> capturesearchvars;
+ /inline;
+
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ (self -> 'debug_trace') -> (insert: tag_name + ': found ' (self -> 'found_count') + ' records in ' + (self -> 'querytime') + ' ms, tag time ' + (self -> 'tagtime') + ' ms, ' + (self -> error_msg) + ' ' + (self -> error_code));
+ /define_tag;
+
+
+ define_tag: 'addrecord', -description='Add a new record to the database. A random string keyvalue will be generated unless a -keyvalue is specified. \n\
+ Parameters:\n\
+ -fields (required array) Lasso-style field values in pair array\n\
+ -keyvalue (optional) If -keyvalue is specified, it must not already exist in the database. Specify -keyvalue=false to prevent generating a keyvalue. \n\
+ -inlinename (optional) Defaults to autocreated inlinename',
+ -required='fields', -type='array',
+ -optional='keyvalue', -copy,
+ -optional='inlinename';
+ local: 'timer'=knop_timer;
+
+ // clear all search result vars
+ self -> reset;
+ local: '_fields'=#fields;
+
+ // remove all database actions from the search array
+ #_fields -> (removeall: '-search') & (removeall: '-add') & (removeall: '-delete') & (removeall: '-update')
+ & (removeall: '-sql') & (removeall: '-nothing') & (removeall: '-show')
+ // & (removeall: '-table') // table is ok to override
+ & (removeall: '-database');
+
+ inline: (self -> 'db_connect'); // connection wrapper
+ if: (local: 'keyvalue') != '' && (local: 'keyvalue') !== false && (self -> 'keyfield')!='';
+ // look for existing keyvalue
+ inline: -op='eq', (self -> 'keyfield')=#keyvalue,
+ -maxrecords=1,
+ -returnfield=(self -> 'keyfield'),
+ -search;
+ if: found_count > 0;
+ (self -> 'error_code') = 7017; // duplicate keyvalue
+ else;
+ (self -> 'keyvalue') = #keyvalue;
+ /if;
+ /inline;
+ /if;
+
+
+ if: (self -> 'error_code') == 0;
+ // proceed to add record
+
+ if: (self -> 'keyfield') != '';
+ if: (local: 'keyvalue') == '' && (local: 'keyvalue') !== false;
+ (self -> 'debug_trace') -> (insert: tag_name + ': generating keyvalue');
+ // create unique keyvalue
+ (self -> 'keyvalue')=knop_unique;
+ /if;
+ #_fields -> (removeall: (self -> 'keyfield'));
+ #_fields -> (removeall: '-keyfield') & (removeall: '-keyvalue');
+ #_fields -> (insert: '-keyfield'=(self -> 'keyfield'));
+ if: (local: 'keyvalue') !== false;
+ #_fields -> (insert: (self -> 'keyfield')=(self -> 'keyvalue'));
+ /if;
+ /if;
+
+ // inlinename defaults to a random string
+ (self -> 'inlinename') = ((local: 'inlinename') != '' ? #inlinename | 'inline_' + knop_unique);
+ #_fields -> (removeall: '-inlinename');
+ #_fields -> (insert: '-inlinename'=(self -> 'inlinename'));
+
+ local: 'querytimer'=knop_timer;
+ inline: #_fields, -add;
+ (self -> 'querytime') = integer: #querytimer;
+ (self -> 'searchparams') = #_fields;
+
+ self -> capturesearchvars;
+ if: error_code != 0;
+ (self -> 'keyvalue') = null;
+ /if;
+ /inline;
+ /if;
+ /inline;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ (self -> 'debug_trace') -> (insert: tag_name + ': ' + (self -> error_msg) + ' ' + (self -> error_code)
+ + ' keyvalue ' + (self -> 'keyvalue') + ' ' + (self -> 'tagtime') + ' ms');
+ /define_tag;
+
+
+ define_tag: 'getrecord', -description='Returns a single specific record from the database, optionally locking the record. \
+ If the keyvalue matches multiple records, an error is returned. \n\
+ Parameters:\n\
+ -keyvalue (optional) Uses a previously set keyvalue if not specified. If no keyvalue is available, an error is returned unless -sql is used. \n\
+ -keyfield (optional) Temporarily override of keyfield specified at oncreate\n\
+ -inlinename (optional) Defaults to autocreated inlinename\n\
+ -lock (optional flag) If flag is specified, a record lock will be set\n\
+ -user (optional) The user who is locking the record (required if using lock)\n\
+ -sql (optional) SQL statement to use instead of keyvalue. Must include the keyfield (and lockfield of locking is used).',
+ -optional='keyvalue', -copy,
+ -optional='keyfield',
+ -optional='inlinename', -copy,
+ -optional='lock',
+ -optional='user', -copy,
+ -optional='sql', -type='string';
+ local: 'timer'=knop_timer;
+
+ local: '_sql'=(local: 'sql');
+
+ if: #_sql != '' && (self -> 'isfilemaker');
+ #_sql='';
+ fail: 7009, self -> error_msg(7009); // sql can not be used with filemaker
+ /if;
+
+ // get existing record pointer if any
+ if: #_sql -> size == 0 && !(local_defined: 'keyvalue');
+ local: 'keyvalue'=(self -> 'keyvalue');
+ else: !(local_defined: 'keyvalue');
+ local: 'keyvalue'=string;
+ /if;
+
+ // clear all search result vars
+ self -> reset;
+
+ fail_if: !(local_defined: 'keyfield') && (self -> 'keyfield') == '', 7002, self -> error_msg(7002); // Keyfield not specified
+ if: (local_defined: 'lock') && #lock != false;
+ fail_if: (self -> 'lockfield') == '', 7003, self -> error_msg(7003); // Lockfield must be specified to get record with lock
+ if: !(local_defined: 'user') && ((self -> 'user') != '' || (self -> 'user') -> isa('user'));
+ // use user from database object
+ local('user' = (self -> 'user'));
+ /if;
+ fail_if: (local: 'user') == '' && !((local: 'user') -> isa('user')), 7004, self -> error_msg(7004); // User must be specified to get record with lock
+ (self -> 'debug_trace') -> insert(tag_name ': user is type ' + (#user -> type) + ', isa(user) = ' + (#user -> isa('user')) );
+ if: #user -> isa('user');
+ #user= #user -> id_user;
+ fail_if: #user == '', 7004, self -> error_msg(7004); // User must be logged in to get record with lock
+ /if;
+ (self -> 'debug_trace') -> insert(tag_name ': user id is ' + #user);
+ /if;
+ if: !(local_defined: 'keyfield') && (self -> 'keyfield') != '';
+ local: 'keyfield'=(self -> 'keyfield');
+ /if;
+ if: #_sql -> size == 0 && string(#keyvalue) -> size == 0;
+ (self -> 'error_code') = 7007; // keyvalue missing
+ /if;
+ if: (self -> 'error_code') == 0;
+ inline: (self -> 'db_connect'); // connection wrapper
+
+ if: #_sql -> size;
+ self -> (select: -sql=#_sql, -inlinename=(local: 'inlinename'));
+ #keyvalue = (self -> 'keyvalue');
+ else;
+ self -> (select: -keyfield=#keyfield, -keyvalue=#keyvalue, -inlinename=(local: 'inlinename'));
+ /if;
+ if: (self -> field_names) !>> #keyfield;
+ (self -> 'error_code') = 7020; // Keyfield not present in query
+ /if;
+ if: (self -> field_names) !>> (self -> 'lockfield') && (local_defined: 'lock') && #lock != false;
+ (self -> 'error_code') = 7021; // Lockfield not present in query
+ /if;
+
+ if: (self -> 'found_count') == 0 && (self -> 'error_code') == 0;
+ (self -> 'error_code') = -1728;
+ else: (self -> 'found_count') > 1 && (self -> 'error_code') == 0;
+ self -> reset;
+ (self -> 'error_code') = 7008; // keyvalue not unique
+ /if;
+
+
+ // handle record locking
+ if: (self -> 'error_code') == 0 && (local_defined: 'lock') && #lock != false;
+ // check for current lock
+ if: (self -> 'lockvalue') != '';
+ // there is a lock already set, check if it has expired or if it is the same user
+ local: 'lockvalue'=(self -> 'lockvalue') -> (split: '|');
+ local: 'lock_timestamp'=date: (#lockvalue->size > 1 ? #lockvalue -> (get: 2) | null);
+ local: 'lock_user'=#lockvalue -> first;
+ if: (date - #lock_timestamp) -> seconds < (self -> 'lock_expires')
+ && #lock_user != #user;
+ // the lock is still valid and it is locked by another user
+ // this is not a real error, more a warning condition
+ (self -> 'error_code') = 7010;
+ (self -> 'error_data') = (map: 'user' = #lock_user, 'timestamp' = #lock_timestamp);
+ (self -> 'keyvalue') = null;
+ (self -> 'debug_trace') -> (insert: tag_name ': record ' + #keyvalue + ' was already locked by ' + #lock_user + '.');
+ /if;
+ /if;
+ if: (self -> 'error_code') == 0;
+ // go ahead and lock record
+ (self -> 'lockvalue') = #user + '|' + (date -> format: '%Q %T');
+ (self -> 'lockvalue_encrypted') = (encrypt_blowfish: (self -> 'lockvalue'), -seed=(self -> 'lock_seed'));
+ local: 'keyvalue_temp'=#keyvalue;
+ if: (self -> 'isfilemaker');
+ // find internal keyvalue
+ inline: -op='eq', #keyfield=#keyvalue,
+ -search;
+ if: found_count == 1;
+ #keyvalue_temp=keyfield_value;
+ (self -> 'debug_trace') -> (insert: tag_name + ': will set record lock for FileMaker record id ' + keyfield_value + ' ' + error_msg + ' ' + error_code);
+ else;
+ (self -> 'debug_trace') -> (insert: tag_name + ': could not get record id for FileMaker record, ' found_count + ' found ' + + error_msg + ' ' + error_code);
+ /if;
+ /inline;
+ /if;
+ inline: -keyfield=#keyfield,
+ -keyvalue=#keyvalue_temp,
+ (self -> 'lockfield')=(self -> 'lockvalue'),
+ -update;
+ if: error_code;
+ (self -> 'error_code') = 7012; // could not set record lock
+ (self -> 'error_data') = (map: 'error_code'=error_code, 'error_msg'=error_msg);
+ (self -> 'lockvalue') = null;
+ (self -> 'lockvalue_encrypted') = null;
+ (self -> 'keyvalue') = null;
+ else;
+ // lock was set ok
+ (self -> 'debug_trace') -> (insert: tag_name + ': set record lock ' + (self -> 'lockvalue') + ' ' + (self -> 'lockvalue_encrypted'));
+ if: (self -> 'user') -> isa('user');
+ // tell user it has locked a record in this db object
+ (self -> 'user') -> addlock(-dbname=self -> varname);
+ /if;
+ /if;
+ /inline;
+ /if;
+ /if;
+
+ /inline;
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ (self -> 'debug_trace') -> (insert: tag_name + ': ' + (self -> error_msg) + ' ' + (self -> error_code) + ' ' + (self -> 'tagtime') + ' ms');
+ /define_tag;
+
+
+ define_tag: 'saverecord', -description='Updates a specific database record. \n\
+ Parameters:\n\
+ -fields (required array) Lasso-style field values in pair array\n\
+ -keyfield (optional) Keyfield is ignored if lockvalue is specified\n\
+ -keyvalue (optional) Keyvalue is ignored if lockvalue is specified\n\
+ -lockvalue (optional) Either keyvalue or lockvalue must be specified\n\
+ -keeplock (optional flag) Avoid clearing the record lock when saving. Updates the lock timestamp.\n\'
+ -user (optional) If lockvalue is specified, user must be specified as well\n\
+ -inlinename (optional) Defaults to autocreated inlinename',
+ -required='fields', -type='array',
+ -optional='keyfield',
+ -optional='keyvalue', -copy,
+ -optional='lockvalue', -copy,
+ -optional='keeplock',
+ -optional='user', -copy,
+ -optional='inlinename', -copy;
+
+ local: 'timer'=knop_timer;
+
+ if(!local_defined('keyvalue') && string(self -> 'keyvalue') -> size);
+ // use current record's keyvalue if any
+ local('keyvalue'=(self -> 'keyvalue'));
+ /if;
+
+ // clear all search result vars
+ self -> reset;
+
+ fail_if: !(local_defined: 'keyvalue') && !(local_defined: 'lockvalue'), 7005, self -> error_msg(7005); // Either keyvalue or lockvalue must be specified for update or delete
+ fail_if: (local_defined: 'keyvalue') && (self -> 'keyfield') == '' && (local: 'keyfield') == '', 7002, self -> error_msg(7002); // Keyfield not specified
+ if: (local_defined: 'lockvalue');
+ fail_if: (self -> 'lockfield') == '', 7003, self -> error_msg(7003); // Lockfield not specified
+ if: !(local_defined: 'user') && ((self -> 'user') != '' || (self -> 'user') -> isa('user'));
+ // use user from database object
+ local('user' = (self -> 'user'));
+ /if;
+ fail_if: (local: 'user') == '' && !((local: 'user') -> isa('user')), 7004, self -> error_msg(7004);
+ (self -> 'debug_trace') -> insert(tag_name ': user is type ' + (#user -> type) + ', isa(user) = ' + (#user -> isa('user')) );
+ if: #user -> isa('user');
+ #user= #user -> id_user;
+ fail_if: #user == '', 7004, self -> error_msg(7004); // User must be logged in to get record with lock
+ /if;
+ (self -> 'debug_trace') -> insert(tag_name ': user id is ' + #user);
+ /if;
+
+ !(local_defined: 'keyfield') ? local: 'keyfield'=self -> 'keyfield';
+
+ local: '_fields'=#fields;
+
+ // remove all database actions from the search array
+ #_fields -> (removeall: '-search') & (removeall: '-add') & (removeall: '-delete') & (removeall: '-update')
+ & (removeall: '-sql') & (removeall: '-nothing') & (removeall: '-show')
+ // & (removeall: '-table') // table is ok to override
+ & (removeall: '-database');
+ #_fields -> (removeall: '-keyfield') & (removeall: '-keyvalue');
+
+ inline: (self -> 'db_connect'); // connection wrapper
+
+ // handle record locking
+ if: (self -> 'error_code') == 0 && (local: 'lockvalue') != '';
+
+ // first check if record was locked by someone else, and that lock is still valid
+ local: 'lock'=(decrypt_blowfish: #lockvalue, -seed=(self -> 'lock_seed')) -> (split: '|');
+ local: 'lock_timestamp'=date: (#lock->size > 1 ? (#lock -> (get: 2)) | null);
+ local: 'lock_user'=#lock -> first;
+ if: (date - #lock_timestamp) -> seconds < (self -> 'lock_expires')
+ && #lock_user != #user;
+ // the lock is still valid and it is locked by another user
+ (self -> 'error_code') = 7010;
+ (self -> 'error_data') = (map: 'user' = #lock_user, 'timestamp' = #lock_timestamp);
+ /if;
+
+ // check that the current lock is still valid
+ if: (self -> 'error_code') == 0;
+ inline: -op='eq', (self -> 'lockfield')=#lock -> (join: '|'),
+ -maxrecords=1,
+ -returnfield=(self -> 'lockfield'),
+ -returnfield=(self -> 'keyfield'),
+ -search;
+ if: error_code == 0 && found_count != 1;
+ // lock is not valid any more
+ (self -> 'error_code') = 7011; // Update failed, record lock not valid any more
+ else: error_code != 0;
+ (self -> 'error_code') = 7018; // Update error
+ (self -> 'error_data') = (map: 'error_code'=error_code, 'error_msg'=error_msg);
+ else;
+ // lock OK, grab keyvalue for update
+ local: 'keyvalue'=(field: (self -> 'keyfield'));
+ /if;
+ /inline;
+ /if;
+
+ if: (self -> 'error_code') == 0;
+ // go ahead and release record lock by clearing the field value in the update fields array
+ #_fields -> (removeall: (self -> 'lockfield'));
+ if: ((local_defined: 'keeplock') && #keeplock != false);
+ // update the lock timestamp
+ (self -> 'lockvalue') = #user + '|' + (date -> format: '%Q %T');
+ (self -> 'lockvalue_encrypted') = (encrypt_blowfish: (self -> 'lockvalue'), -seed=(self -> 'lock_seed'));
+ #_fields -> (insert: (self -> 'lockfield')=(self -> 'lockvalue'));
+ else;
+ #_fields -> (insert: (self -> 'lockfield') = '');
+ /if;
+ /if;
+
+ /if;
+
+ if: (self -> 'error_code') == 0 && (local: 'keyvalue') != '';
+ if: (self -> 'isfilemaker');
+ inline: -op='eq', #keyfield=#keyvalue, -search;
+ if: found_count == 1;
+ #_fields -> (insert: '-keyvalue'=keyfield_value);
+ (self -> 'debug_trace') -> (insert: tag_name + ': FileMaker record id ' + keyfield_value);
+ /if;
+ /inline;
+ else;
+ #_fields -> (insert: '-keyfield'=#keyfield);
+ #_fields -> (insert: '-keyvalue'=#keyvalue);
+ /if;
+ /if;
+
+
+
+ if: (#_fields >> '-keyfield' && #_fields -> (find: '-keyfield') -> first -> value != '' || (self -> 'isfilemaker'))
+ && #_fields >> '-keyvalue' && #_fields -> (find: '-keyvalue') -> first -> value != '';
+ // ok to update
+ else: (self -> 'error_code') == 0;
+ (self -> 'error_code') = 7006; // Update failed, keyfield or keyvalue missing';
+ /if;
+
+ // update record
+ if: (self -> 'error_code') == 0;
+
+ // inlinename defaults to a random string
+ (self -> 'inlinename') = ((local: 'inlinename') != '' ? #inlinename | 'inline_' + knop_unique);
+ #_fields -> (removeall: '-inlinename');
+ #_fields -> (insert: '-inlinename'=(self -> 'inlinename'));
+
+ local: 'querytimer'=knop_timer;
+ inline: #_fields, -update;
+ (self -> 'querytime') = integer: #querytimer;
+ (self -> 'searchparams') = #_fields;
+ self -> capturesearchvars;
+ /inline;
+ /if;
+ /inline;
+
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ (self -> 'debug_trace') -> (insert: tag_name + ': ' + (self -> 'keyvalue') + ' '+ (self -> error_msg) + ' ' + (self -> error_code) + ' ' + (self -> 'tagtime') + ' ms');
+ /define_tag;
+
+
+ define_tag: 'deleterecord', -description='Deletes a specific database record. \n\
+ Parameters:\n\
+ -keyvalue (optional) Keyvalue is ignored if lockvalue is specified\n\
+ -lockvalue (optional) Either keyvalue or lockvalue must be specified\n\
+ -user (optional) If lockvalue is specified, user must be specified as well',
+ -optional='keyvalue', -copy,
+ -optional='lockvalue', -copy,
+ -optional='user';
+ local: 'timer'=knop_timer;
+
+ if(!local_defined('keyvalue') && string(self -> 'keyvalue') -> size);
+ // use current record's keyvalue if any
+ local('keyvalue'=(self -> 'keyvalue'));
+ /if;
+
+ // clear all search result vars
+ self -> reset;
+
+ fail_if: !(local_defined: 'keyvalue') && !(local_defined: 'lockvalue'), 7005, self -> error_msg(7005); // Either keyvalue or lockvalue must be specified for update or delete
+ fail_if: (local_defined: 'keyvalue') && (self -> 'keyfield') == '', 7002, self -> error_msg(7002); // Keyfield not specified
+ if: (local_defined: 'lockvalue');
+ fail_if: (self -> 'lockfield') == '', 7003, self -> error_msg(7003); // Lockfield not specified
+ if: !(local_defined: 'user') && ((self -> 'user') != '' || (self -> 'user') -> isa('user'));
+ // use user from database object
+ local('user' = (self -> 'user'));
+ /if;
+ fail_if: (local: 'user') == '' && !((local: 'user') -> isa('user')), 7004, self -> error_msg(7004);
+ (self -> 'debug_trace') -> insert(tag_name ': user is type ' + (#user -> type) + ', isa(user) = ' + (#user -> isa('user')) );
+ if: #user -> isa('user');
+ #user= #user -> id_user;
+ fail_if: #user == '', 7004, self -> error_msg(7004); // User must be logged in to get record with lock
+ /if;
+ (self -> 'debug_trace') -> insert(tag_name ': user id is ' + #user);
+ /if;
+
+ local: '_fields'=array;
+
+ inline: (self -> 'db_connect'); // connection wrapper
+
+ // handle record locking
+ if: (self -> 'error_code') == 0 && (local: 'lockvalue') != '';
+
+ // first check if record was locked by someone else, and that lock is still valid
+ local: 'lockvalue'=(decrypt_blowfish: #lockvalue, -seed=(self -> 'lock_seed')) -> (split: '|');
+ local: 'lock_timestamp'=date: (#lockvalue->size > 1 ? #lockvalue -> (get: 2) | null);
+ local: 'lock_user'=(#lockvalue -> first);
+ if: (date - #lock_timestamp) -> seconds < (self -> 'lock_expires')
+ && #lock_user != #user;
+ // the lock is still valid and it is locked by another user
+ (self -> 'error_code') = 7010; // Delete failed, record locked
+ (self -> 'error_data') = (map: 'user' = #lock_user, 'timestamp' = #lock_timestamp);
+ /if;
+
+ // check that the current lock is still valid
+ if: (self -> 'error_code') == 0;
+ inline: -op='eq', (self -> 'lockfield')=#lockvalue -> (join: '|'),
+ -maxrecords=1,
+ -returnfield=(self -> 'lockfield'),
+ -returnfield=(self -> 'keyfield'),
+ -search;
+ if: error_code == 0 && found_count != 1;
+ // lock is not valid any more
+ (self -> 'error_code') = 7011; // Delete failed, record lock not valid any more';
+ else: error_code != 0;
+ (self -> 'error_code') = 7019; // delete error
+ (self -> 'error_data') = (map: 'error_code'=error_code, 'error_msg'=error_msg);
+ else;
+ // lock OK, grab keyvalue for update
+ local: 'keyvalue'=(field: (self -> 'keyfield'));
+ (self -> 'debug_trace') -> (insert: tag_name + ': got keyvalue ' + #keyvalue + ' for keyfield ' + (self -> 'keyfield'));
+ /if;
+ /inline;
+ /if;
+
+ /if;
+
+ if: (self -> 'error_code') == 0 && (local: 'keyvalue') != '';
+ if: (self -> 'isfilemaker');
+ inline: -op='eq', (self -> 'keyfield')=#keyvalue, -search;
+ if: found_count == 1;
+ #_fields -> (insert: '-keyvalue'=keyfield_value);
+ (self -> 'debug_trace') -> (insert: tag_name + ': FileMaker record id ' + keyfield_value);
+ /if;
+ /inline;
+ else;
+ #_fields -> (insert: '-keyfield'=(self -> 'keyfield'));
+ #_fields -> (insert: '-keyvalue'=#keyvalue);
+ /if;
+ /if;
+
+ (self -> 'debug_trace') -> (insert: tag_name + ': will delete record with params ' + #_fields);
+
+ if: (#_fields >> '-keyfield' && #_fields -> (find: '-keyfield') -> first -> value != '' || (self -> 'isfilemaker'))
+ && #_fields >> '-keyvalue' && #_fields -> (find: '-keyvalue') -> first -> value != '';
+ // ok to delete
+ else;
+ (self -> 'error_code') = 7006; // Delete failed, keyfield or keyvalue missing
+ /if;
+
+ // delete record
+ if: (self -> 'error_code') == 0;
+
+ local: 'querytimer'=knop_timer;
+ inline: #_fields, -delete;
+ (self -> 'querytime') = integer: #querytimer;
+ (self -> 'searchparams') = #_fields;
+
+ self -> capturesearchvars;
+
+ /inline;
+ /if;
+ /inline;
+
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ (self -> 'debug_trace') -> (insert: tag_name + ': ' + (self -> error_msg) + ' ' + (self -> error_code) + ' ' + (self -> 'tagtime') + ' ms');
+ /define_tag;
+
+
+ define_tag: 'clearlocks', -description='Release all record locks for the specified user, suitable to use when showing record list. \n\
+ Parameters:\n\
+ -user (required) The user to unlock records for',
+ -required='user';
+ // release all record locks for the specified user, suitable to use when showing record list
+ local: 'timer'=knop_timer;
+
+ fail_if: (self -> 'lockfield') == '', 7003, self -> error_msg(7003); // Lockfield not specified
+ fail_if: #user == '', 7004, self -> error_msg(7004); // User not specified
+
+ if: (self -> 'isfilemaker');
+ inline: (self -> 'db_connect'),
+ -maxrecords=all,
+ (self -> 'lockfield')='"' + #user + '|"',
+ -search;
+ if: found_count > 0;
+ (self -> 'debug_trace') -> (insert: tag_name + ': clearing locks for ' + #user + ' in ' + found_count + ' FileMaker records ' + error_msg + ' ' + error_code);
+ records;
+ inline: -keyvalue=keyfield_value,
+ (self -> 'lockfield')='',
+ -update;
+ if: error_code;
+ (self -> 'error_code') = 7013; // Clearlocks failed
+ (self -> 'error_data') = (map: 'error_code'=error_code, 'error_msg'=error_msg);
+ (self -> 'debug_trace') -> (insert: tag_name + ': error when clearing lock on FileMaker record ' + keyfield_value + ' ' + error_msg + ' ' + error_code);
+ return;
+ /if;
+ /inline;
+ /records;
+ else: error_code;
+ (self -> 'error_code') = 7013; // Clearlocks failed
+ (self -> 'error_data') = (map: 'error_code'=error_code, 'error_msg'=error_msg);
+ /if;
+ /inline;
+ else;
+ inline: (self -> 'db_connect'),
+ -sql='UPDATE `' + (self -> 'table_realname') + '` SET `' + (self -> 'lockfield') + '`="" WHERE `' + (self -> 'lockfield')
+ + '` LIKE "' + (encode_sql: #user) + '|%"';
+ if: error_code != 0;
+ (self -> 'error_code') = 7013; // Clearlocks failed
+ (self -> 'error_data') = (map: 'error_code'=error_code, 'error_msg'=error_msg);
+ /if;
+ /inline;
+ (self -> 'debug_trace') -> (insert: tag_name + ': clearing all locks for ' + #user + ' ' + (self -> error_msg) + ' ' + (self -> error_code));
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ define_tag: 'action_statement'; return: (self -> 'action_statement'); /define_tag;
+ define_tag: 'found_count'; return: (self -> 'found_count'); /define_tag;
+ define_tag: 'shown_count'; return: (self -> 'shown_count'); /define_tag;
+ define_tag: 'shown_first'; return: (self -> 'shown_first'); /define_tag;
+ define_tag: 'shown_last'; return: (self -> 'shown_last'); /define_tag;
+ define_tag: 'maxrecords_value'; return: (self -> 'maxrecords_value'); /define_tag;
+ define_tag: 'skiprecords_value'; return: (self -> 'skiprecords_value'); /define_tag;
+ define_tag: 'keyfield'; return: (self -> 'keyfield'); /define_tag;
+ define_tag: 'keyvalue'; return: (self -> 'keyvalue'); /define_tag;
+ define_tag: 'lockfield'; return: (self -> 'lockfield'); /define_tag;
+ define_tag: 'lockvalue'; return: (self -> 'lockvalue'); /define_tag;
+ define_tag: 'lockvalue_encrypted'; return: (self -> 'lockvalue_encrypted'); /define_tag;
+ define_tag: 'querytime'; return: (self -> 'querytime'); /define_tag;
+ define_tag: 'inlinename'; return: (self -> 'inlinename'); /define_tag;
+ define_tag: 'searchparams'; return: (self -> 'searchparams'); /define_tag;
+ define_tag: 'resultset_count',
+ -optional='inlinename';
+ !local_defined('inlinename') ? local('inlinename'=(self -> 'inlinename'));
+ return((self -> 'resultset_count_map') -> find(#inlinename));
+ /define_tag;
+
+ define_tag('recorddata', -description='A map containing all fields, only available for single record results',
+ -optional='recordindex', -copy);
+ !local_defined('recordindex') ? local('recordindex'=(self -> 'current_record'));
+ #recordindex < 1 ? #recordindex = 1;
+ if(#recordindex == 1);
+ // return default (i.e. first) record
+ return(self -> 'recorddata');
+ else;
+ local('recorddata'=map);
+ iterate(self -> field_names, local('field_name'));
+ #recorddata -> insert(#field_name = (self -> 'records_array' -> get(#recordindex)
+ -> get(self -> 'field_names_map' -> find(#field_name))));
+ /iterate;
+ return(#recorddata);
+ /if;
+ /define_tag;
+
+ define_tag: 'records_array'; return: (self -> 'records_array'); /define_tag;
+
+ define_tag('field_names', -description='Returns an array of the field names from the last database query. If no database query has been performed, a "-show" request is performed. \n\
+ Parameters: \n\
+ -table (optional) Return the field names for the specified table\n\
+ -types (optional flag) If specified, returns a pair array with fieldname and corresponding Lasso data type',
+ -optional='table',
+ -optional='types');
+ !local_defined('table') ? local('table'=(self -> 'table'));
+ local('field_names'=(self -> 'field_names'));
+ if(#field_names -> size == 0 || (local_defined('types') && #types != false));
+ #field_names=array;
+ if(local_defined('types') && #types != false);
+ local('types_mapping'=map('text'='string', 'number'='decimal', 'date/time'='date'));
+ /if;
+ inline(self->'db_connect', -table=#table, -show);
+ if(local_defined('types') && #types != false);
+ loop(field_name(-count));
+ #field_names -> insert(field_name(loop_count) = #types_mapping->find(field_name(loop_count, -type)));
+ /loop;
+ else;
+ #field_names=field_names;
+ /if;
+ /inline;
+ /if;
+ return(@#field_names);
+ /define_tag;
+
+ define_tag('table_names', -description='Returns an array with all table names for the database');
+ local('table_names'=array);
+ inline(self -> 'db_connect');
+ Database_TableNames(self -> 'database');
+ #table_names -> insert(Database_TableNameItem);
+ /Database_TableNames;
+ /inline;
+ return(@#table_names);
+ /define_tag;
+
+ define_tag: 'error_data', -description='Returns more info for those errors that provide such';
+ if: (self -> 'errors_error_data') >> (self -> error_code);
+ return: (self -> 'error_data');
+ else;
+ return: map;
+ /if;
+ /define_tag;
+
+ define_tag('size');
+ return(self -> 'shown_count');
+ /define_tag;
+
+ define_tag('get', -required='index');
+ return(knop_databaserow(
+ -record_array=(self -> 'records_array' -> get(#index)),
+ -field_names=(self -> 'field_names')));
+ /define_tag;
+
+ define_tag('records', -description='Returns all found records as a knop_databaserows object',
+ -optional='inlinename');
+ !local_defined('inlinename') ? local('inlinename'=(self -> 'inlinename'));
+ if((self -> 'databaserows_map') !>> #inlinename);
+ // create knop_databaserows on demand
+ (self -> 'databaserows_map') -> insert(#inlinename = knop_databaserows(
+ -records_array=(self -> 'records_array'),
+ -field_names=(self -> 'field_names'))
+ );
+ /if;
+ return(@((self -> 'databaserows_map') -> find(#inlinename)));
+ /define_tag;
+
+ define_tag('field', -description='A shortcut to return a specific field from a single record result',
+ -required='fieldname',
+ -optional='recordindex',
+ -optional='index');
+ !local_defined('recordindex') ? local('recordindex'=(self -> 'current_record'));
+ #recordindex < 1 ? #recordindex = 1;
+ !local_defined('index') ? local('index'=1);
+ if(#recordindex == 1 && #index == 1);
+ // return first field occurrence from the default (i.e. first) record
+ return((self -> 'recorddata') -> find(#fieldname));
+ else(self -> 'field_names_map' >> #fieldname
+ && #recordindex >= 1
+ && #recordindex <= (self -> 'records_array') -> size);
+ // return specific record
+ if(#index==1);
+ // return first ocurrence of field name through the index map - this is faster
+ return(self -> 'records_array' -> get(#recordindex) -> get(self -> 'field_names_map' -> find(#fieldname)));
+ else;
+ // return another occurrence of the field - this is slightly slower
+ local('indexmatches'=(self -> 'field_names') -> findposition(#fieldname));
+ if(#index >= 1 && #index <= #indexmatches -> size);
+ return(self -> 'records_array' -> get(#recordindex) -> get(#indexmatches -> get(#index)));
+ /if;
+ /if;
+ /if;
+ /define_tag;
+
+ define_tag('next', -description='Increments the record pointer, returns true if there are more records to show, false otherwise.\n\
+ Useful as an alternative to a regular records loop:\n\
+ \t$database -> select;\n\
+ \twhile: $database -> next;\n\
+ \t\t$database -> field(\'name\');\' \';\n\
+ \t/while;');
+ if((self -> 'current_record') < (self -> 'shown_count'));
+ (self -> 'current_record') += 1;
+ return(true);
+ else;
+ // reset record pointer
+ (self -> 'current_record') = 0;
+ return(false);
+ /if;
+ /define_tag;
+
+ define_tag('nextrecord', -description='Deprecated synonym for ->next');
+ (self -> 'debug_trace') -> insert('*** DEPRECATION WARNING *** ' + tag_name + ' is deprecated, use ->next instead ');
+ return(self -> next);
+ /define_tag;
+
+ define_tag: 'trace',
+ -optional='html',
+ -optional='xhtml';
+
+ local: 'endslash' = ((self -> (xhtml: params)) ? ' /' | '');
+
+ local: 'eol'=(local_defined: 'html') || #endslash -> size ? ' \n' | '\n';
+
+ return: #eol + 'Debug trace for database $' + (self -> varname) + ' (' (self -> 'database') '.' (self -> 'table') + ')' + #eol
+ + (self -> 'debug_trace') -> (join: #eol) + #eol;
+
+ /define_tag;
+
+
+ // =========== Internal member tags ===============
+
+ define_tag: 'reset', -description='Internal, reset all search result vars';
+ // reset all search result vars
+ // searchresultvars
+ (self -> 'action_statement') = null;
+ (self -> 'found_count') = null;
+ (self -> 'shown_first') = null;
+ (self -> 'shown_last') = null;
+ (self -> 'shown_count') = null;
+ (self -> 'field_names') = null;
+ (self -> 'records_array') = null;
+ (self -> 'maxrecords_value') = null;
+ (self -> 'skiprecords_value') = null;
+
+ (self -> 'inlinename')=string;
+ (self -> 'keyvalue')=null;
+ (self -> 'lockvalue')=null;
+ (self -> 'lockvalue_encrypted')=null;
+ (self -> 'timestampfield')=string;
+ (self -> 'timestampvalue')=string;
+ (self -> 'searchparams')=string;
+ (self -> 'querytime')=integer;
+ (self -> 'recorddata')=map;
+ (self -> 'message')=string;
+ (self -> 'current_record')=0;
+ (self -> 'field_names_map')=map;
+
+ (self -> 'error_code')=0;
+ (self -> 'error_msg')=string;
+ /define_tag;
+
+ define_tag: 'capturesearchvars', -description='Internal';
+ // internal member tag
+
+ // capture various result variables like found_count, shown_first, shown_last, shown_count
+ // searchresultvars
+ (self -> 'action_statement') = action_statement;
+ (self -> 'found_count') = found_count;
+ (self -> 'shown_first') = shown_first;
+ (self -> 'shown_last') = shown_last;
+ (self -> 'shown_count') = shown_count;
+ (self -> 'field_names') = field_names;
+ (self -> 'records_array') = records_array;
+
+ !((self -> 'maxrecords_value') > 0) ? (self -> 'maxrecords_value') = maxrecords_value;
+ !((self -> 'skiprecords_value') > 0) ? (self -> 'skiprecords_value') = skiprecords_value;
+
+ lasso_tagexists('resultset_count') ? (self -> 'resultset_count_map') -> insert((self -> 'inlinename')=resultset_count);
+ iterate(field_names, local('field_name'));
+ (self -> 'field_names_map') !>> #field_name
+ ? (self -> 'field_names_map') -> insert(#field_name=loop_count);
+ /iterate;
+
+ (self -> 'error_code') = error_code;
+ error_code && error_msg -> size ? (self -> 'error_msg') = error_msg;
+
+
+ // handle queries that use LIMIT
+ if: !(self -> 'isfilemaker') && (string_findregexp: action_statement, -find= '\\sLIMIT\\s', -ignorecase) -> size;
+ (self -> 'debug_trace') -> (insert: tag_name + ': old found_count, shown_first and shown_last ' + (self -> 'found_count') + ' '+ (self -> 'shown_first') + ' '+ (self -> 'shown_last'));
+ (self -> 'found_count') = knop_foundrows;
+ // adjust shown_first and shown_last
+ (self -> 'shown_first') = ((self -> 'found_count') ? (self -> 'skiprecords_value') + 1 | 0);
+ (self -> 'shown_last') = integer(math_min(((self -> 'skiprecords_value') + (self -> 'maxrecords_value')), (self -> 'found_count')));
+ (self -> 'debug_trace') -> (insert: tag_name + ': new found_count, shown_first and shown_last ' + (self -> 'found_count') + ' '+ (self -> 'shown_first') + ' '+ (self -> 'shown_last'));
+ /if;
+
+ // capture some variables for single record results
+ if: found_count <= 1 // -update gives found_count 0 but still has one record result
+ && error_code == 0;
+ if((self -> 'keyfield') != '' && string(field(self -> 'keyfield')) -> size);
+ (self -> 'keyvalue')=field(self -> 'keyfield');
+ else: (self -> 'keyfield') != '' && (self -> 'keyvalue') == '' && !(self -> 'isfilemaker');
+ (self -> 'keyvalue')=keyfield_value;
+ /if;
+ if: lasso_currentaction == 'add' || lasso_currentaction == 'update';
+ (self -> 'affectedrecord_keyvalue') = (self -> 'keyvalue');
+ /if;
+ if: (self -> 'lockfield') != '';
+ (self -> 'lockvalue')=(field: (self -> 'lockfield'));
+ (self -> 'lockvalue_encrypted')=(encrypt_blowfish: (field: (self -> 'lockfield')), -seed=(self -> 'lock_seed'));
+ /if;
+ /if;
+ if: error_code == 0;
+ // populate recorddata with field values from the first found record
+ iterate: field_names, local: 'field_name';
+ (self -> 'recorddata') !>> #field_name
+ ? (self -> 'recorddata') -> (insert: #field_name = (field: #field_name) );
+ /iterate;
+ else;
+ (self -> 'debug_trace') -> (insert: tag_name + ': ' + error_msg);
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': found_count ' + (self -> 'found_count') + ' ' + (self -> 'keyfield') + ' '+ (field: (self -> 'keyfield')) + ' keyfield_value ' + keyfield_value + ' keyvalue ' + (self -> 'keyvalue') + ' fieldcount ' + (field_name: -count));
+
+ /define_tag;
+
+/define_type;
+
+
+define_type('databaserows',
+ -namespace='knop_');
+ local('version'='2009-01-08',
+ 'description'='Custom type to return all record rows from knop_database. Used as output for knop_database->records. ');
+/*
+
+CHANGE NOTES
+2009-01-08 JS ->_unknowntag: Added -index parameter
+2008-11-24 JS Created the type
+
+
+*/
+
+ local('records_array'=array,
+ 'field_names'=array,
+ 'field_names_map'=map,
+ 'current_record'=integer);
+
+ define_tag('oncreate', -description='Create a record rows object. \n\
+ Parameters:\n\
+ -records_array (array) Array of arrays with field values for all fields for each record of all found records
+ -field_names (array) Array with all the field names',
+ -required='records_array',
+ -required='field_names');
+ self -> 'records_array'=#records_array;
+ self -> 'field_names'=#field_names;
+ // store indexes to first occurrence of each field name for faster access
+ iterate(#field_names, local('field_name'));
+ (self -> 'field_names_map') !>> #field_name
+ ? (self -> 'field_names_map') -> insert(#field_name=loop_count);
+ /iterate;
+ /define_tag;
+
+ define_tag('_unknowntag', -description='Shortcut to field',
+ -optional='index');
+ !local_defined('index') ? local('index'=1);
+ if(self -> 'field_names' >> tag_name);
+ return(self -> field(tag_name(-index=#index)));
+ else;
+ //fail: -9948, self -> type + '->' + tag_name + ' not known.';
+ /if;
+ /define_tag;
+
+ define_tag('onconvert', -description='Output the current record as a plain array of field values');
+ !local_defined('recordindex') ? local('recordindex'=(self -> 'current_record'));
+ #recordindex < 1 ? #recordindex = 1;
+ if(#recordindex >= 1
+ && #recordindex <= (self -> 'records_array' -> size));
+ return(self -> 'records_array' -> get(#recordindex));
+ /if;
+ /define_tag;
+
+ define_tag('size');
+ return(self -> 'records_array' -> size);
+ /define_tag;
+
+ define_tag('get', -required='index');
+ return(knop_databaserow(-record_array=(self -> 'records_array' -> get(#index)), -field_names=(self -> 'field_names')));
+ /define_tag;
+
+ define_tag('field', -description='Return an individual field value',
+ -required='fieldname',
+ -optional='recordindex',
+ -optional='index');
+ !local_defined('recordindex') ? local('recordindex'=(self -> 'current_record'));
+ #recordindex < 1 ? #recordindex = 1;
+ !local_defined('index') ? local('index'=1);
+ if(self -> 'field_names_map' >> #fieldname
+ && #recordindex >= 1
+ && #recordindex <= (self -> 'records_array') -> size);
+ // return specific record
+ if(#index==1);
+ // return first ocurrence of field name through the index map - this is faster
+ return(self -> 'records_array' -> get(#recordindex) -> get(self -> 'field_names_map' -> find(#fieldname)));
+ else;
+ // return another occurrence of the field - this is slightly slower
+ local('indexmatches'=(self -> 'field_names') -> findposition(#fieldname));
+ if(#index >= 1 && #index <= #indexmatches -> size);
+ return(self -> 'records_array' -> get(#recordindex) -> get(#indexmatches -> get(#index)));
+ /if;
+ /if;
+ /if;
+ /define_tag;
+
+ define_tag('summary_header', -description='Returns true if the specified field name has changed since the previous record, or if we are at the first record',
+ -required='fieldname');
+ local('recordindex'=(self -> 'current_record'));
+ #recordindex < 1 ? #recordindex = 1;
+ if(#recordindex == 1 // first record
+ || self -> field(#fieldname) != self -> field(#fieldname, -recordindex=(#recordindex - 1)) ); // different than previous record (look behind)
+ return(true);
+ else;
+ return(false);
+ /if;
+ /define_tag;
+
+ define_tag('summary_footer', -description='Returns true if the specified field name will change in the following record, or if we are at the last record',
+ -required='fieldname');
+ local('recordindex'=(self -> 'current_record'));
+ #recordindex < 1 ? #recordindex = 1;
+ if(#recordindex == (self -> 'records_array') -> size // last record
+ || self -> field(#fieldname) != self -> field(#fieldname, -recordindex=(#recordindex + 1)) ); // different than next record (look ahead)
+ return(true);
+ else;
+ return(false);
+ /if;
+ /define_tag;
+
+
+ define_tag('next', -description='Increments the record pointer, returns true if there are more records to show, false otherwise.');
+ if((self -> 'current_record') < (self -> 'records_array') -> size);
+ (self -> 'current_record') += 1;
+ return(true);
+ else;
+ // reset record pointer
+ (self -> 'current_record') = 0;
+ return(false);
+ /if;
+ /define_tag;
+/define_type;
+
+
+
+define_type('databaserow',
+ -namespace='knop_',
+ //-prototype, // prototype prevents the namespace from unloading without restart
+ );
+ local: 'version'='2009-01-08',
+ 'description'='Custom type to return individual record rows from knop_database. Used as output for knop_database->get. ';
+/*
+
+CHANGE NOTES
+2009-01-08 JS ->_unknowntag: Added -index parameter
+2008-11-24 JS ->field: Added -index parameter to be able to access any occurrence of the same field name
+2008-05-29 JS Removed -prototype since it prevents unloading the namespace. It is recommended to turn it on for best performance
+2008-05-27 JS Created the type
+
+
+*/
+ local('record_array'=array,
+ 'field_names'=array);
+
+ define_tag('oncreate', -description='Create a record row object. \n\
+ Parameters:\n\
+ -record_array (array) Array with field values for all fields for the record
+ -field_names (array) Array with all the field names, should be same size as -record_array',
+ -required='record_array',
+ -required='field_names');
+ self -> 'record_array'=#record_array;
+ self -> 'field_names'=#field_names;
+ /define_tag;
+
+ define_tag('_unknowntag', -description='Shortcut to field',
+ -optional='index');
+ !local_defined('index') ? local('index'=1);
+ if(self -> 'field_names' >> tag_name);
+ return(self -> field(tag_name, -index=#index));
+ else;
+ //fail: -9948, self -> type + '->' + tag_name + ' not known.';
+ /if;
+ /define_tag;
+
+ define_tag('onconvert', -description='Output the record as a plain array of field values');
+ return(self -> 'record_array');
+ /define_tag;
+
+
+ define_tag('field', -description='Return an individual field value',
+ -required='fieldname',
+ -optional='index');
+ !local_defined('index') ? local('index'=1);
+ if(self -> 'field_names' >> #fieldname);
+ // return any occurrence of the field
+ local('indexmatches'=(self -> 'field_names') -> findposition(#fieldname));
+ if(#index >= 1 && #index <= #indexmatches -> size);
+ return((self -> 'record_array') -> get(#indexmatches -> get(#index)));
+ /if;
+ /if;
+ /define_tag;
+
+
+/define_type;
+?>
diff --git a/samples/Lasso/json.lasso b/samples/Lasso/json.lasso
new file mode 100644
index 0000000000..cb4e99c3be
--- /dev/null
+++ b/samples/Lasso/json.lasso
@@ -0,0 +1,301 @@
+
+ //
+ //
+ // This tag is now incorporated in Lasso 8.6.0.1
+ //
+
+If: (Lasso_TagExists: 'Encode_JSON') == False;
+
+ Define_Tag: 'JSON', -Namespace='Encode_', -Required='value', -Optional='options';
+
+ Local: 'escapes' = Map('\\' = '\\', '"' = '"', '\r' = 'r', '\n' = 'n', '\t' = 't', '\f' = 'f', '\b' = 'b');
+ Local: 'output' = '';
+ Local: 'newoptions' = (Array: -Internal);
+ If: !(Local_Defined: 'options') || (#options->(IsA: 'array') == False);
+ Local: 'options' = (Array);
+ /If;
+ If: (#options >> -UseNative) || (Params >> -UseNative);
+ #newoptions->(Insert: -UseNative);
+ /If;
+ If: (#options >> -NoNative) || (Params >> -NoNative);
+ #newoptions->(Insert: -NoNative);
+ /If;
+ If: (#options !>> -UseNative) && ((#value->(IsA: 'set')) || (#value->(IsA: 'list')) || (#value->(IsA: 'queue')) || (#value->(IsA: 'priorityqueue')) || (#value->(IsA: 'stack')));
+ #output += (Encode_JSON: Array->(insertfrom: #value->iterator) &, -Options=#newoptions);
+ Else: (#options !>> -UseNative) && (#value->(IsA: 'pair'));
+ #output += (Encode_JSON: (Array: #value->First, #value->Second));
+ Else: (#options !>> -Internal) && (#value->(Isa: 'array') == False) && (#value->(IsA: 'map') == False);
+ #output += '[' + (Encode_JSON: #value, -Options=#newoptions) + ']';
+ Else: (#value->(IsA: 'literal'));
+ #output += #value;
+ Else: (#value->(IsA: 'string'));
+ #output += '"';
+ Loop: (#value->Length);
+ Local('character' = #value->(Get: Loop_Count));
+ #output->(Append:
+ (Match_RegExp('[\\x{0020}-\\x{21}\\x{23}-\\x{5b}\\x{5d}-\\x{10fff}]') == #character) ? #character |
+ '\\' + (#escapes->(Contains: #character) ? #escapes->(Find: #character) | 'u' + String(Encode_Hex(#character))->PadLeading(4, '0')&)
+ );
+ /Loop;
+ #output += '"';
+ Else: (#value->(IsA: 'integer')) || (#value->(IsA: 'decimal')) || (#value->(IsA: 'boolean'));
+ #output += (String: #value);
+ Else: (#value->(IsA: 'null'));
+ #output += 'null';
+ Else: (#value->(IsA: 'date'));
+ If: #value->gmt;
+ #output += '"' + #value->(format: '%QT%TZ') + '"';
+ Else;
+ #output += '"' + #value->(format: '%QT%T') + '"';
+ /If;
+ Else: (#value->(IsA: 'array'));
+ #output += '[';
+ Iterate: #value, (Local: 'temp');
+ #output += (Encode_JSON: #temp, -Options=#newoptions);
+ If: #value->Size != Loop_Count;
+ #output += ', ';
+ /If;
+ /Iterate;
+ #output += ']';
+ Else: (#value->(IsA: 'object'));
+ #output += '{';
+ Iterate: #value, (Local: 'temp');
+ #output += #temp->First + ': ' + (Encode_JSON: #temp->Second, -Options=#newoptions);
+ If: (#value->Size != Loop_Count);
+ #output += ', ';
+ /If;
+ /Iterate;
+ #output += '}';
+ Else: (#value->(IsA: 'map'));
+ #output += '{';
+ Iterate: #value, (Local: 'temp');
+ #output += (Encode_JSON: #temp->First, -Options=#newoptions) + ': ' + (Encode_JSON: #temp->Second, -Options=#newoptions);
+ If: (#value->Size != Loop_Count);
+ #output += ', ';
+ /If;
+ /Iterate;
+ #output += '}';
+ Else: (#value->(IsA: 'client_ip')) || (#value->(IsA: 'client_address'));
+ #output += (Encode_JSON: (String: #value), -Options=#newoptions);
+ Else: (#options !>> -UseNative) && (#value->(IsA: 'set')) || (#value->(IsA: 'list')) || (#value->(IsA: 'queue')) || (#value->(IsA: 'priorityqueue')) || (#value->(IsA: 'stack'));
+ #output += (Encode_JSON: Array->(insertfrom: #value->iterator) &, -Options=#newoptions);
+ Else: (#options !>> -NoNative);
+ #output += (Encode_JSON: (Map: '__jsonclass__'=(Array:'deserialize',(Array:'' + #value->Serialize + ''))));
+ /If;
+ Return: @#output;
+
+ /Define_Tag;
+
+/If;
+
+If: (Lasso_TagExists: 'Decode_JSON') == False;
+
+ Define_Tag: 'JSON', -Namespace='Decode_', -Required='value';
+
+ (#value == '') ? Return: Null;
+
+ Define_Tag: 'consume_string', -Required='ibytes';
+ Local: 'unescapes' = (map: 34 = '"', 92 = '\\', 98 = '\b', 102 = '\f', 110 = '\n', 114 = '\r', 116 = '\t');
+ Local: 'temp' = 0, 'obytes' = Bytes;
+ While: ((#temp := #ibytes->export8bits) != 34); // '"'
+ If: (#temp === 92); // '\'
+ #temp = #ibytes->export8bits;
+ If: (#temp === 117); // 'u'
+ #obytes->(ImportString: (Decode_Hex: (String: #ibytes->(GetRange: #ibytes->Position + 1, 4)))->(ExportString: 'UTF-16'), 'UTF-8');
+ #ibytes->(SetPosition: #ibytes->Position + 4);
+ Else;
+ If: (#unescapes->(Contains: #temp));
+ #obytes->(ImportString: #unescapes->(Find: #temp), 'UTF-8');
+ Else;
+ #obytes->(Import8Bits: #temp);
+ /If;
+ /If;
+ Else;
+ #obytes->(Import8Bits: #temp);
+ /If;
+ /While;
+ Local('output' = #obytes->(ExportString: 'UTF-8'));
+ If: #output->(BeginsWith: '') && #output->(EndsWith: '');
+ Local: 'temp' = #output - '' - '';
+ Local: 'output' = null;
+ Protect;
+ #output->(Deserialize: #temp);
+ /Protect;
+ Else: (Valid_Date: #output, -Format='%QT%TZ');
+ Local: 'output' = (Date: #output, -Format='%QT%TZ');
+ Else: (Valid_Date: #output, -Format='%QT%T');
+ Local: 'output' = (Date: #output, -Format='%QT%T');
+ /If;
+ Return: @#output;
+ /Define_Tag;
+
+ Define_Tag: 'consume_token', -Required='ibytes', -required='temp';
+ Local: 'obytes' = bytes->(import8bits: #temp) &;
+ local: 'delimit' = (array: 9, 10, 13, 32, 44, 58, 93, 125); // \t\r\n ,:]}
+ While: (#delimit !>> (#temp := #ibytes->export8bits));
+ #obytes->(import8bits: #temp);
+ /While;
+ Local: 'output' = (String: #obytes);
+ If: (#output == 'true') || (#output == 'false');
+ Return: (Boolean: #output);
+ Else: (#output == 'null');
+ Return: Null;
+ Else: (String_IsNumeric: #output);
+ Return: (#output >> '.') ? (Decimal: #output) | (Integer: #output);
+ /If;
+ Return: @#output;
+ /Define_Tag;
+
+ Define_Tag: 'consume_array', -Required='ibytes';
+ Local: 'output' = array;
+ local: 'delimit' = (array: 9, 10, 13, 32, 44); // \t\r\n ,
+ local: 'temp' = 0;
+ While: ((#temp := #ibytes->export8bits) != 93); // ]
+ If: (#delimit >> #temp);
+ // Discard whitespace
+ Else: (#temp == 34); // "
+ #output->(insert: (consume_string: @#ibytes));
+ Else: (#temp == 91); // [
+ #output->(insert: (consume_array: @#ibytes));
+ Else: (#temp == 123); // {
+ #output->(insert: (consume_object: @#ibytes));
+ Else;
+ #output->(insert: (consume_token: @#ibytes, @#temp));
+ (#temp == 93) ? Loop_Abort;
+ /If;
+ /While;
+ Return: @#output;
+ /Define_Tag;
+
+ Define_Tag: 'consume_object', -Required='ibytes';
+ Local: 'output' = map;
+ local: 'delimit' = (array: 9, 10, 13, 32, 44); // \t\r\n ,
+ local: 'temp' = 0;
+ local: 'key' = null;
+ local: 'val' = null;
+ While: ((#temp := #ibytes->export8bits) != 125); // }
+ If: (#delimit >> #temp);
+ // Discard whitespace
+ Else: (#key !== null) && (#temp == 34); // "
+ #output->(insert: #key = (consume_string: @#ibytes));
+ #key = null;
+ Else: (#key !== null) && (#temp == 91); // [
+ #output->(insert: #key = (consume_array: @#ibytes));
+ #key = null;
+ Else: (#key !== null) && (#temp == 123); // {
+ #output->(insert: #key = (consume_object: @#ibytes));
+ #key = null;
+ Else: (#key !== null);
+ #output->(insert: #key = (consume_token: @#ibytes, @#temp));
+ (#temp == 125) ? Loop_abort;
+ #key = null;
+ Else;
+ #key = (consume_string: @#ibytes);
+ while(#delimit >> (#temp := #ibytes->export8bits));
+ /while;
+ #temp != 58 ? Loop_Abort;
+ /If;
+ /While;
+ If: (#output >> '__jsonclass__') && (#output->(Find: '__jsonclass__')->(isa: 'array')) && (#output->(Find: '__jsonclass__')->size >= 2) && (#output->(Find: '__jsonclass__')->First == 'deserialize');
+ Return: #output->(find: '__jsonclass__')->Second->First;
+ Else: (#output >> 'native') && (#output >> 'comment') && (#output->(find: 'comment') == 'http://www.lassosoft.com/json');
+ Return: #output->(find: 'native');
+ /If;
+ Return: @#output;
+ /Define_Tag;
+
+ Local: 'ibytes' = (bytes: #value);
+ Local: 'start' = 1;
+ #ibytes->removeLeading(BOM_UTF8);
+ Local: 'temp' = #ibytes->export8bits;
+ If: (#temp == 91); // [
+ Local: 'output' = (consume_array: @#ibytes);
+ Return: @#output;
+ Else: (#temp == 123); // {
+ Local: 'output' = (consume_object: @#ibytes);
+ Return: @#output;
+ /If;
+
+ /Define_Tag;
+
+/If;
+
+If: (Lasso_TagExists: 'Literal') == False;
+
+ Define_Type: 'Literal', 'String';
+ /Define_Type;
+
+/If;
+
+If: (Lasso_TagExists: 'Object') == False;
+
+ Define_Type: 'Object', 'Map';
+ /Define_Type;
+
+/If;
+
+If: (Lasso_TagExists: 'JSON_RPCCall') == False;
+
+ Define_Tag: 'RPCCall', -Namespace='JSON_',
+ -Required='method',
+ -Optional='params',
+ -Optional='id',
+ -Optional='host';
+
+ !(Local_Defined: 'host') ? Local: 'host' = 'http://localhost/lassoapps.8/rpc/rpc.lasso';
+ !(Local_Defined: 'id') ? Local: 'id' = Lasso_UniqueID;
+ Local: 'request' = (Map: 'method' = #method, 'params' = #params, 'id' = #id);
+ Local: 'request' = (Encode_JSON: #request);
+ Local: 'result' = (Include_URL: #host, -PostParams=#request);
+ Local: 'result' = (Decode_JSON: #result);
+ Return: @#result;
+
+ /Define_Tag;
+
+/If;
+
+If: (Lasso_TagExists: 'JSON_Records') == False;
+
+ Define_Tag: 'JSON_Records',
+ -Optional='KeyField',
+ -Optional='ReturnField',
+ -Optional='ExcludeField',
+ -Optional='Fields';
+
+ Local: '_fields' = (Local_Defined: 'fields') && #fields->(IsA: 'array') ? #fields | Field_Names;
+ Fail_If: #_fields->size == 0, -1, 'No fields found for [JSON_Records]';
+ Local: '_keyfield' = (Local: 'keyfield');
+ If: #_fields !>> #_keyfield;
+ Local: '_keyfield' = (KeyField_Name);
+ If: #_fields !>> #_keyfield;
+ Local: '_keyfield' = 'ID';
+ If: #_fields !>> #_keyfield;
+ Local: '_keyfield' = #_fields->First;
+ /If;
+ /If;
+ /If;
+ Local: '_index' = #_fields->(FindPosition: #_keyfield)->First;
+ Local: '_return' = (Local_Defined: 'returnfield') ? (Params->(Find: -ReturnField)->(ForEach: {Params->First = Params->First->Second; Return: True}) &) | @#_fields;
+ Local: '_exclude' = (Local_Defined: 'excludefield') ? (Params->(Find: -ExcludeField)->(ForEach: {Params->First = Params->First->Second; Return: True}) &) | Array;
+ Local: '_records' = Array;
+ Iterate: Records_Array, (Local: '_record');
+ Local: '_temp' = Map;
+ Iterate: #_fields, (Local: '_field');
+ ((#_return >> #_field) && (#_exclude !>> #_field)) ? #_temp->Insert(#_field = #_record->(Get: Loop_Count));
+ /Iterate;
+ #_records->Insert(#_temp);
+ /Iterate;
+ Local: '_output' = (Encode_JSON: (Object: 'error_msg'=Error_Msg, 'error_code'=Error_Code, 'found_count'=Found_Count, 'keyfield'=#_keyfield, 'rows'=#_records));
+ Return: @#_output;
+
+ /Define_Tag;
+
+/If;
+
+?>
diff --git a/samples/Lasso/json.lasso9 b/samples/Lasso/json.lasso9
new file mode 100644
index 0000000000..732ab2afbb
--- /dev/null
+++ b/samples/Lasso/json.lasso9
@@ -0,0 +1,213 @@
+
+/**
+ trait_json_serialize
+ Objects with this trait will be assumed to convert to json data
+ when its ->asString method is called
+*/
+define trait_json_serialize => trait {
+ require asString()
+}
+
+define json_serialize(e::bytes)::string => ('"' + (string(#e)->Replace(`\`, `\\`) & Replace('\"', '\\"') & Replace('\r', '\\r') & Replace('\n', '\\n') & Replace('\t', '\\t') & Replace('\f', '\\f') & Replace('\b', '\\b') &) + '"')
+define json_serialize(e::string)::string => ('"' + (string(#e)->Replace(`\`, `\\`) & Replace('\"', '\\"') & Replace('\r', '\\r') & Replace('\n', '\\n') & Replace('\t', '\\t') & Replace('\f', '\\f') & Replace('\b', '\\b') &) + '"')
+define json_serialize(e::json_literal)::string => (#e->asstring)
+define json_serialize(e::integer)::string => (#e->asstring)
+define json_serialize(e::decimal)::string => (#e->asstring)
+define json_serialize(e::boolean)::string => (#e->asstring)
+define json_serialize(e::null)::string => ('null')
+define json_serialize(e::date)::string => ('"' + #e->format(#e->gmt ? '%QT%TZ' | '%Q%T') + '"')
+/*
+define json_serialize(e::array)::string => {
+ local(output) = '';
+ local(delimit) = '';
+ #e->foreach => { #output += #delimit + json_serialize(#1); #delimit = ', '; }
+ return('[' + #output + ']');
+}
+define json_serialize(e::staticarray)::string => {
+ local(output) = '';
+ local(delimit) = '';
+ #e->foreach => { #output += #delimit + json_serialize(#1); #delimit = ', '; }
+ return('[' + #output + ']');
+}
+*/
+define json_serialize(e::trait_forEach)::string => {
+ local(output) = '';
+ local(delimit) = '';
+ #e->foreach => { #output += #delimit + json_serialize(#1); #delimit = ', '; }
+ return('[' + #output + ']');
+}
+define json_serialize(e::map)::string => {
+ local(output = with pr in #e->eachPair
+ select json_serialize(#pr->first->asString) + ': ' + json_serialize(#pr->second))
+ return '{' + #output->join(',') + '}'
+}
+define json_serialize(e::json_object)::string => {
+ local(output) = '';
+ local(delimit) = '';
+ #e->foreachpair => { #output += #delimit + #1->first + ': ' + json_serialize(#1->second); #delimit = ', '; }
+ return('{' + #output + '}');
+}
+define json_serialize(e::trait_json_serialize) => #e->asString
+define json_serialize(e::any)::string => json_serialize('' + #e->serialize + '')
+
+// Bil Corry fixes for decoding json
+define json_consume_string(ibytes::bytes) => {
+ local(obytes) = bytes;
+ local(temp) = 0;
+ while((#temp := #ibytes->export8bits) != 34);
+ #obytes->import8bits(#temp);
+ (#temp == 92) ? #obytes->import8bits(#ibytes->export8bits); // Escape \
+ /while;
+ local(output = string(#obytes)->unescape)
+ //Replace('\\"', '\"') & Replace('\\r', '\r') & Replace('\\n', '\n') & Replace('\\t', '\t') & Replace('\\f', '\f') & Replace('\\b', '\b') &;
+ if(#output->BeginsWith('') && #output->EndsWith(''));
+ Protect;
+ return serialization_reader(xml(#output - '' - ''))->read
+ /Protect;
+ else( (#output->size == 16 or #output->size == 15) and regexp(`\d{8}T\d{6}Z?`, '', #output)->matches)
+ return date(#output, -Format=#output->size == 16?`yyyyMMdd'T'HHmmssZ`|`yyyyMMdd'T'HHmmss`)
+ /if
+ return #output
+}
+
+// Bil Corry fix + Ke fix
+define json_consume_token(ibytes::bytes, temp::integer) => {
+
+ local(obytes = bytes->import8bits(#temp) &,
+ delimit = array(9, 10, 13, 32, 44, 58, 93, 125)) // \t\r\n ,:]}
+
+ while(#delimit !>> (#temp := #ibytes->export8bits))
+ #obytes->import8bits(#temp)
+ /while
+
+ #temp == 125? // }
+ #ibytes->marker -= 1
+//============================================================================
+// Is also end of token if end of array[]
+ #temp == 93? // ]
+ #ibytes->marker -= 1
+//............................................................................
+
+ local(output = string(#obytes))
+ #output == 'true'?
+ return true
+ #output == 'false'?
+ return false
+ #output == 'null'?
+ return null
+ string_IsNumeric(#output)?
+ return (#output >> '.')? decimal(#output) | integer(#output)
+
+ return #output
+}
+
+// Bil Corry fix
+define json_consume_array(ibytes::bytes)::array => {
+ Local(output) = array;
+ local(delimit) = array( 9, 10, 13, 32, 44); // \t\r\n ,
+ local(temp) = 0;
+ While((#temp := #ibytes->export8bits) != 93); // ]
+ If(#delimit >> #temp);
+ // Discard whitespace
+ Else(#temp == 34); // "
+ #output->insert(json_consume_string(#ibytes));
+ Else(#temp == 91); // [
+ #output->insert(json_consume_array(#ibytes));
+ Else(#temp == 123); // {
+ #output->insert(json_consume_object(#ibytes));
+ Else;
+ #output->insert(json_consume_token(#ibytes, #temp));
+ (#temp == 93) ? Loop_Abort;
+ /If;
+ /While;
+ Return(#output);
+}
+
+// Bil Corry fix
+define json_consume_object(ibytes::bytes)::map => {
+ Local('output' = map,
+ 'delimit' = array( 9, 10, 13, 32, 44), // \t\r\n ,
+ 'temp' = 0,
+ 'key' = null,
+ 'val' = null);
+ While((#temp := #ibytes->export8bits) != 125); // }
+ If(#delimit >> #temp);
+ // Discard whitespace
+ Else((#key !== null) && (#temp == 34)); // "
+ #output->insert(#key = json_consume_string(#ibytes));
+ #key = null;
+ Else((#key !== null) && (#temp == 91)); // [
+ #output->insert(#key = json_consume_array(#ibytes));
+ #key = null;
+ Else((#key !== null) && (#temp == 123)); // {
+ #output->insert(#key = json_consume_object(#ibytes));
+ #key = null;
+ Else((#key !== null));
+ #output->insert(#key = json_consume_token(#ibytes, #temp));
+ #key = null;
+ Else;
+ #key = json_consume_string(#ibytes);
+ while(#delimit >> (#temp := #ibytes->export8bits));
+ /while;
+ #temp != 58 ? Loop_Abort;
+ /If;
+ /While;
+
+ If((#output >> '__jsonclass__') && (#output->Find('__jsonclass__')->isa('array')) && (#output->Find('__jsonclass__')->size >= 2) && (#output->Find('__jsonclass__')->First == 'deserialize'));
+ Return(#output->find('__jsonclass__')->Second->First);
+ Else((#output >> 'native') && (#output >> 'comment') && (#output->find('comment') == 'http://www.lassosoft.com/json'));
+ Return(#output->find('native'));
+ /If;
+ Return(#output);
+}
+
+// Bil Corry fix + Ke fix
+define json_deserialize(ibytes::bytes)::any => {
+ #ibytes->removeLeading(bom_utf8);
+
+//============================================================================
+// Reset marker on provided bytes
+ #ibytes->marker = 0
+//............................................................................
+
+ Local(temp) = #ibytes->export8bits;
+ If(#temp == 91); // [
+ Return(json_consume_array(#ibytes));
+ Else(#temp == 123); // {
+ Return(json_consume_object(#ibytes));
+ else(#temp == 34) // "
+ return json_consume_string(#ibytes)
+ /If;
+}
+
+define json_deserialize(s::string) => json_deserialize(bytes(#s))
+
+/**! json_literal - This is a subclass of String used for JSON encoding.
+
+ A json_literal works exactly like a string, but will be inserted directly
+ rather than being encoded into JSON. This allows JavaScript elements
+ like functions to be inserted into JSON objects. This is most useful
+ when the JSON object will be used within a JavaScript on the local page.
+ [Map: 'fn'=Literal('function(){ ...})] => {'fn': function(){ ...}}
+**/
+define json_literal => type {
+ parent string
+}
+
+/**! json_object - This is a subclass of Map used for JSON encoding.
+
+ An object works exactly like a map, but when it is encoded into JSON all
+ of the keys will be inserted literally. This makes it easy to create a
+ JavaScript object without extraneous quote marks.
+ Object('name'='value') => {name: "value"}
+**/
+define json_object => type {
+ parent map
+ public onCreate(...) => ..onCreate(:#rest or (:))
+}
+
+define json_rpccall(method::string, params=map, id='', host='') => {
+ #id == '' ? #host = Lasso_UniqueID;
+ #host == '' ? #host = 'http://localhost/lassoapps.8/rpc/rpc.lasso';
+ Return(Decode_JSON(Include_URL(#host, -PostParams=Encode_JSON(Map('method' = #method, 'params' = #params, 'id' = #id)))));
+}
diff --git a/samples/Lasso/knop.las b/samples/Lasso/knop.las
new file mode 100644
index 0000000000..7aefe4e13d
--- /dev/null
+++ b/samples/Lasso/knop.las
@@ -0,0 +1,8342 @@
+[/*
+
+ On-Demand library for namespace knop
+ Namespace file built date 2012-06-10 02:05:30 by http://knop8/buildnamespace.lasso
+ Montania System AB
+
+*/]
+
+[
+//------------------------------------------------------------------
+// Begin knop custom tags in util.inc
+//------------------------------------------------------------------
+
+] split('`') -> first;
+ return(@#output);
+/define_tag;
+
+define_tag: 'unique', -description='Returns a very unique but still rather short random string',
+ -namespace='knop_',
+ -priority='replace';
+
+ // Johan Sölve 2006-09-20
+
+ local: 'output'=string,
+ 'seed'=integer,
+ 'charlist'='abcdefghijklmnopqrstuvwxyz0123456789';
+ local: 'base'=(#charlist -> size);
+ // start with the current date and time in a mixed up format as seed
+ #seed = integer: (date -> (format: '%S%y%m%d%H%M'));
+ // convert this integer to a string using base conversion
+ while: #seed>0;
+ #output = #charlist -> (get: (#seed % #base)+1) + #output;
+ #seed = #seed / #base;
+ /while;
+ // start over with a new chunk as seed
+ #seed = string: 1000+(date->millisecond);
+ #seed = #seed + string: (math_random: -lower=1000, -upper=9999);
+ #seed = integer: #seed;
+ // convert this integer to a string using base conversion
+ while: #seed>0;
+ #output = #charlist -> (get: (#seed % #base)+1) + #output;
+ #seed = #seed / #base;
+ /while;
+ return: #output;
+/define_tag;
+
+
+define_tag: 'seed',
+ -namespace='knop_',
+ -priority='replace';
+
+ local: 'seed'= (string: $__lassoservice_ip__) + response_localpath;
+ #seed -> removetrailing(response_filepath);
+ return: #seed;
+/define_tag;
+
+define_tag: 'foundrows', // http://tagswap.net/found_rows
+ -namespace='knop_',
+ -priority='replace';
+ local: 'sql'= action_statement;
+ if: (string_findregexp: #sql, -find= '\\sLIMIT\\s', -ignorecase) -> size == 0;
+ // || found_count < maxrecords_value; (this condition is inaccurate)
+ // found_count must be accurate
+ return: found_count;
+ /if;
+ if: (string_findregexp: #sql, -find= '\\s(GROUP\\s+BY|HAVING)\\s', -ignorecase) -> size == 0;
+ // Default method, usually the fastest. Can not be used with GROUP BY for example.
+ // First normalize whitespace around FROM in the expression
+ #sql = (string_replaceregexp: #sql, -find= '\\sFROM\\s', -replace=' FROM ', -ignorecase, -ReplaceOnlyOne);
+ #sql = 'SELECT COUNT(*) AS found_rows ' + #sql -> (substring: (#sql -> (find: ' FROM ')) + 1) ;
+ #sql = (string_replaceregexp: #sql, -find='\\sLIMIT\\s+[0-9,]+', -replace='');
+ if: (string_findregexp: #sql, -find= '\\sORDER\\s+BY\\s', -ignorecase) -> size;
+ // remove ORDER BY statement since it causes problems with field aliases
+ // first normalize the expression so we can find it with simple string expression later
+ #sql = (string_replaceregexp: #sql, -find= '\\sORDER\\s+BY\\s', -replace=' ORDER BY ', -ignorecase);
+ #sql = #sql -> (substring: 1, (#sql -> (find: ' ORDER BY ')) -1);
+ /if;
+ else; // query contains GROUP BY so use SQL_CALC_FOUND_ROWS which can be much slower, see http://bugs.mysql.com/bug.php?id=18454
+ #sql -> (removeleading: 'SELECT');
+ #sql = 'SELECT SQL_CALC_FOUND_ROWS ' + #sql + ';SELECT FOUND_ROWS() AS found_rows';
+ #sql = (string_replaceregexp: #sql, -find='\\sLIMIT\\s+[0-9,]+', -replace=' LIMIT 1', -ignorecase);
+ /if;
+ inline: -sql=#sql;
+ if: (field: 'found_rows') > 0;
+ return: integer: (field: 'found_rows'); // exit here normally
+ /if;
+ /inline;
+ // fallback
+ return: found_count;
+/define_tag;
+
+define_tag:'IDcrypt', -description='Encrypts or Decrypts integer values',
+ -namespace='knop_',
+ -required='value',
+ -optional='seed',
+ -priority='replace';
+/*
+
+[IDcrypt]
+Encrypts or Decrypts integer values
+
+Author: Pier Kuipers
+Last Modified: Jan. 29, 2007
+License: Public Domain
+
+Description:
+This tag was written to deal with "scraping" attacks where bots keep
+requesting the same page with incremental id parameters, corresponding to
+mysql id columns. Rather than introducing a new column with a unique id, this
+tag will "intelligently" blowfish encrypt or decrypt existing id values.
+
+
+Sample Usage:
+[local('myID' = (action_param('id')))]
+[IDcrypt(#myID)]
+
+[IDcrypt('35446')] -> j4b50f315238d68df
+
+[IDcrypt('j4b50f315238d68df')] -> 35446
+
+
+
+Downloaded from tagSwap.net on Feb. 07, 2007.
+Latest version available from .
+
+*/
+// if id values need to be retrieved from bookmarked urls, the tag's built-in seed value must be used,
+// or the seed value used must be guaranteed to be the same as when the value was encrypted!
+
+ local('cryptvalue' = string);
+ !local_defined('seed') ? local('seed' = knop_seed);
+ Local('RandChars' = 'AaBbCcDdEeFfGgHhiJjKkLmNnoPpQqRrSsTtUuVvWwXxYyZz');
+ Local('anyChar' = (#RandChars -> (Get:(Math_Random: -Min=1, -Max=(#RandChars->Size)))));
+// taken from Bil Corry's [lp_string_getNumeric]
+ local('numericValue' = (string_findregexp((string: #value), -find='\\d')->(join:'')));
+
+ if(
+ (#numericValue == (integer(#value)))
+ &&
+ (((string(#value))->length) == ((string(#numericValue)) -> length))
+ );
+// alpha character is inserted at beginning of encrypted string in case value needs to be
+// cast to a javascript variable, which cannot start with a number
+ #cryptvalue = (#anyChar + (Encrypt_Blowfish(#value, -seed=#seed)));
+ else(
+ ((((string(#value))->length) - 1) % 2 == 0)
+ &&
+ (((string(#value))->length) > 16)
+ );
+ #cryptvalue = (decrypt_blowfish((String_Remove: #value, -StartPosition=1, -EndPosition=1),-Seed=#seed));
+ else;
+ #cryptvalue = 0;
+ /if;
+
+ if(String_IsAlphaNumeric(#cryptvalue));
+ return(#cryptvalue);
+ else;
+// successfully decrypted values resulting in lots of strange characters are probably
+// the result of someone guessing a value
+ return(0);
+ /if;
+
+/define_tag;
+
+
+
+define_type: 'timer', -description='Utility type to provide a simple timer',
+ -namespace='knop_';
+ /*
+
+ CHANGE NOTES
+ 2007-06-17 JS Created the type
+
+ */
+
+ local: 't'=integer;
+ define_tag: 'oncreate';
+ (self -> 't') = _date_msec;
+ /define_tag;
+ define_tag: 'onconvert';
+ return: _date_msec - (self -> 't');
+ /define_tag;
+
+/define_type;
+
+define_tag: 'cachestore', -description='Stores all instances of page variables of the specified type in a cache object. Caches are stored \
+ in a global variable named by host name and document root to isolate the storage of different hosts. \n\
+ Parameters:\n\
+ -type (required string) Page variables of the specified type will be stored in cache. Data types can be specified with or without namespace.\n\
+ -expires (optional integer) The number of seconds that the cached data should be valid. Defaults to 600 (10 minutes)\n\
+ -session (optional string) The name of an existing session to use for cache storage instead of the global storage\n\
+ -name (optional string) Extra name parameter to be able to isolate the cache storage from other sites on the same virtual hosts, or caches for different uses. ',
+ -namespace='knop_',
+ -required='type', -type='string',
+ -optional='expires', -type='integer', // seconds
+ -optional='session', -type='string',
+ -optional='name', -type='string';
+
+ local: 'data'=map;
+ !(local_defined: 'expires') ? local: 'expires'=600; // default seconds
+ // store all page vars of the specified type
+ iterate: vars -> keys, local: 'item';
+ if: (var: #item) -> isa(#type);
+ #data -> insert(#item = (var: #item));
+ /if;
+ /iterate;
+ if: (local_defined: 'session');
+ //fail_if: (session_id: -name=#session) -> size == 0, -1, 'Cachestore with -session requires that the specified session is started';
+ local: 'cache_name' = '_knop_cache_' + (local: 'name');
+ session_addvar: -name=#session, #cache_name;
+ !((var: #cache_name) -> isa('map')) ? var: #cache_name = map;
+ (var: #cache_name) -> insert(#type = (map:
+ 'content'=#data,
+ 'timestamp'=date,
+ 'expires'=(date + (duration: -second=#expires))));
+ else;
+ local: 'cache_name'='knop_' + (local: 'name') + '_' + server_name + response_localpath;
+ #cache_name -> removetrailing(response_filepath);
+ // initiate thread RW lock
+ !(global: 'rwlock_' + #cache_name) -> isa('rwlock') ? global: 'rwlock_' + #cache_name=Thread_RWLock;
+ // create a reference to the lock
+ local: 'lock'=@(global: 'rwlock_' + #cache_name);
+ // lock for writing
+ #lock -> writelock;
+ // check and initiate the cache storage
+ !((global: #cache_name) -> isa('map')) ? global: #cache_name = map;
+ (global: #cache_name) -> insert(#type = (map:
+ 'content'=#data,
+ 'timestamp'=date,
+ 'expires'=(date + (duration: -second=#expires))));
+ // unlock
+ #lock -> writeunlock;
+ /if;
+/define_tag;
+
+define_tag: 'cachefetch', -description='Recreates page variables from previously cached instances of the specified type, returns true if successful or false if there was no valid \
+ existing cache for the specified type. Caches are stored in a global variable named by host name and document root to isolate the storage of different hosts. \n\
+ Parameters:\n\
+ -type (required string) Page variables of the specified type will be stored in cache. \n\
+ -session (optional string) The name of an existing session to use for cache storage instead of the global storage\n\
+ -name (optional string) Extra name parameter to be able to isolate the cache storage from other sites on the same virtual hosts. \n\
+ -maxage (optional date) Cache data older than the date/time specified in -maxage will not be used.',
+ -namespace='knop_',
+ -required='type', -type='string',
+ -optional='session', -type='string',
+ -optional='name', -type='string',
+ -optional='maxage', -type='date';
+
+
+ local: 'data'=null;
+ if: (local_defined: 'session');
+ //fail_if: (session_id: -name=#session) -> size == 0, -1, 'Cachefetch with -session requires that the specified session is started';
+ local: 'cache_name' = '_knop_cache_' + (local: 'name');
+ if: (var: #cache_name) -> isa('map')
+ && (var: #cache_name) >> #type
+ && (var: #cache_name) -> find(#type) -> find('expires') > date;
+ if(local_defined('maxage')
+ && var(#cache_name) -> find(#type) -> find('timestamp') < #maxage);
+ // cached data too old
+ else;
+ #data = (var: #cache_name) -> find(#type) -> find('content');
+ /if;
+ /if;
+ else;
+ local: 'cache_name'='knop_' + (local: 'name') + '_' + server_name + response_localpath;
+ #cache_name -> removetrailing(response_filepath);
+ // initiate thread RW lock
+ !(global: 'rwlock_' + #cache_name) -> isa('rwlock') ? global: 'rwlock_' + #cache_name=Thread_RWLock;
+ // create a reference to the lock
+ local: 'lock'=@(global: 'rwlock_' + #cache_name);
+ // lock for reading
+ #lock -> readlock;
+ if: (global: #cache_name) -> isa('map')
+ && (global: #cache_name) >> #type
+ && (global: #cache_name) -> find(#type) -> find('expires') > date;
+ if(local_defined('maxage')
+ && global(#cache_name) -> find(#type) -> find('timestamp') < #maxage);
+ // cached data too old
+ else;
+ #data = (global: #cache_name) -> find(#type) -> find('content');
+ /if;
+ /if;
+ // unlock
+ #lock -> readunlock;
+ /if;
+ if: #data -> isa('map');
+ iterate: #data, local: 'item';
+ var: (#item -> name) = #item -> value;
+ /iterate;
+ return: true;
+ else;
+ return: false;
+ /if;
+/define_tag;
+
+
+define_tag: 'cachedelete', -description='Deletes the cache for the specified name (and optionally name). \n\
+ Parameters:\n\
+ -type (required string) Page variables of the specified type will be stored in cache. \n\
+ -session (optional string) The name of an existing session to use for cache storage instead of the global storage\n\
+ -name (optional string) Extra name parameter to be able to isolate the cache storage from other sites on the same virtual hosts. ',
+ -namespace='knop_',
+ -required='type', -type='string',
+ -optional='session', -type='string',
+ -optional='name', -type='string'; // ignored for session
+ if: (local_defined: 'session');
+ //fail_if: (session_id: -name=#session) -> size == 0, -1, 'Cachestore with -session requires that the specified session is started';
+ local: 'cache_name' = '_knop_cache_' + (local: 'name');
+ session_addvar: -name=#session, #cache_name;
+ !((var: #cache_name) -> isa('map')) ? var: #cache_name = map;
+ (var: #cache_name) -> remove(#type);
+ else;
+ local: 'cache_name'='knop_' + (local: 'name') + '_' + server_name + response_localpath;
+ #cache_name -> removetrailing(response_filepath);
+ // initiate thread RW lock
+ !(global: 'rwlock_' + #cache_name) -> isa('rwlock') ? global: 'rwlock_' + #cache_name=Thread_RWLock;
+ // create a reference to the lock
+ local: 'lock'=@(global: 'rwlock_' + #cache_name);
+ // lock for writing
+ #lock -> writelock;
+ // check and initiate the cache storage
+ !((global: #cache_name) -> isa('map')) ? global: #cache_name = map;
+ (global: #cache_name) -> remove(#type);
+ // unlock
+ #lock -> writeunlock;
+ /if;
+
+/define_tag;
+
+
+?>
+[
+//------------------------------------------------------------------
+// End knop custom tags in util.inc
+//------------------------------------------------------------------
+
+//##################################################################
+
+][
+//------------------------------------------------------------------
+// Begin knop_base
+//------------------------------------------------------------------
+
+]error_msg: custom error numbers can now be added, even if the language already exists.
+2008-01-10 JS ->error_msg: improved reporting of custom error messages such as from bad database queries
+2007-12-13 JS Added -> error_lang to provide a reference to the knop_lang object for error messages, to be able to add localized error messages to any Knop type (except knop_lang and knop_base)
+2007-12-12 JS Added -html and -xhtml to ->help to get a nicely formatted output.
+2007-12-11 JS Centralized ->error_code and ->error_msg to knop_base. Moved all error codes to error_msg
+2007-12-06 JS Changed ->help to improve the self-documentation. It will now always return an up to date list of member tags and parameter.
+2007-11-05 JS Added var name to trace output
+2007-06-17 JS Added ->tagtime (was in nav earlier)
+2007-06-13 JS Added -> varname to be able to retreive the name of the page variable that a type instance is stored in.
+2007-06-13 JS Added -> xhtml to automatically sense if an xhtml doctype exists in the current page buffer. The result is cached in a page variable for performance.
+ This is for internal use for member tags that output html.
+2007-06-13 JS Introduced page variable $_knop_data for general page level storage and caching, common between different knop objects.
+2007-06-13 JS Created the data type
+
+TODO: ->help: add output option to format for Google Code Wiki
+->xhtml is not working properly when site is run by atbegin handler and explicitly writing to content_body
+
+
+*/
+
+ local: 'debug_trace'=array,
+ '_debug_trace'=array,
+ 'instance_unique'=null,
+ 'instance_varname'=null,
+ 'tagtime'=integer, // time for entire tag in ms
+ 'tagtime_tagname'=string,
+ 'error_code'=0,
+ 'error_msg'=string,
+ 'error_lang'=null, // must be defined as knop_lang in each type instead, to avoid recursion
+ ;
+
+ define_tag: 'ondeserialize', -description='Recreates transient variables after coming back from a session';
+ self -> properties -> first -> insert('_debug_trace'=array);
+ /define_tag;
+
+ define_tag: 'help', -description='Auto generates an overview of all member tags of a type, with all parameters specified for each member tag.',
+ -optional='html',
+ -optional='xhtml';
+ local: 'endslash' = ((self -> (xhtml: params)) ? ' /' | '');
+ local: 'eol'=(local_defined: 'html') || #endslash -> size ? ' \n' | '\n';
+
+ local: 'output'=string,
+ 'tags'=array,
+ 'description'=string,
+ 'parameters'=string;
+ #output += (self -> type) + ' - version ' + (self -> 'version') + '\n' ;
+ #output += (self -> 'description') + '\n\n';
+ iterate: (self -> properties -> second) , local: 't';
+ #tags -> (insert: #t);
+ /iterate;
+ if: (self -> parent -> type != 'null'); // this doesn't work
+ iterate: (self -> parent -> properties -> second) , local: 't';
+ #tags -> (insert: #t);
+ /iterate;
+ /if;
+ #tags -> sort;
+ iterate: #tags , local: 't';
+ #parameters = string;
+ #output += '-> ' + (#t -> name);
+ #description=(#t -> value -> description);
+ iterate: (#t -> value -> paraminfo) , local: 'p';
+ if: #description !>> '-' + (#p -> paramname);
+ #parameters += '-' + (#p -> paramname) + ' (' (#p -> isrequired ? 'required' | 'optional')
+ + (#p -> paramtype != 'null' && #p -> paramtype -> size ? ' ' + (#p -> paramtype)) + ')\n';
+ /if;
+ /iterate;
+ #output += (#description -> size || #parameters -> size ? '\n' + #description);
+ #output += (#description >> 'Parameters:' ? '\n');
+ #output += (#description !>> 'Parameters:' && #parameters -> size ? '\nParameters:\n');
+ #output += (#parameters -> size ? #parameters);
+ #output -> removetrailing('\n');
+ #output += '\n\n';
+ /iterate;
+ if: ((local_defined: 'html') && #html != false) || ((local_defined: 'xhtml') && #xhtml != false);
+ #output = encode_html: #output;
+ // normalize line breaks and convert to
+ #output -> (replace: '\r\n', '\n') & (replace: '\r', '\n') & (replace: '\n', #eol + '\n');
+ /if;
+ return: #output;
+ /define_tag;
+
+
+ define_tag: 'xhtml', -description='Internal. Finds out if xhtml output should be used. Looks at doctype unless -xhtml is specified \
+ in the params array. The result is cached in a page variable. \n\
+ Looking at doctype doesn\'t work when using atbegin driven solutions since content_body isn\'t filled with the page buffer until the page has already been processed. ',
+ -optional='params';
+ if: (local_defined: 'params') && #params >> '-xhtml';
+ local: 'xhtmlparam'=#params -> (find: '-xhtml') -> first;
+ if: #xhtmlparam -> type == 'pair'; // -xhtml=true / -xhtml=false
+ return: boolean: (#xhtmlparam -> value);
+ else; // plain -xhtml
+ return: true;
+ /if;
+ /if;
+ if: (var: '_knop_data') -> type != 'map';
+ $_knop_data = map;
+ /if;
+ if: $_knop_data !>> 'doctype_xhtml';
+ local: 'doctype' = content_body -> (substring: 1, (content_body -> (find: '>')));
+ $_knop_data -> (insert: 'doctype_xhtml' = (#doctype >> '> 'xhtml'));
+ /if;
+ return: $_knop_data -> (find: 'doctype_xhtml');
+ /define_tag;
+
+
+ define_tag: 'error_lang', -description='Returns a reference to the language object used for error codes, to be able to add localized error messages to any Knop type (except knop_lang and knop_base)';
+ return: @(self -> 'error_lang');
+ /define_tag;
+
+ define_tag: 'error_code', -description='Either proprietary error code or standard Lasso error code';
+ return: integer: (self -> 'error_code');
+ /define_tag;
+
+ define_tag: 'error_msg',
+ -optional='error_code', -type='integer', -copy;
+ !(local_defined: 'error_code') ? local: 'error_code'=(self -> error_code);
+ local: 'error_lang_custom'=(self -> 'error_lang');
+ local: 'error_lang'=(knop_lang: -default='en', -fallback);
+
+ local: 'errorcodes'=(map:
+ 0 = 'No error',
+ -1728 = 'No records found', // standard Lasso error code
+
+ // database errors 7000
+ 7001 ='The specified table was not found',
+ 7002 = 'Keyfield not specified',
+ 7003 = 'Lockfield not specified',
+ 7004 = 'User not specified for record lock',
+ 7005 = 'Either keyvalue or lockvalue must be specified for update or delete',
+ 7006 = 'Keyfield or keyvalue missing',
+ 7007 = 'Keyvalue missing',
+ 7008 = 'Keyvalue not unique',
+ 7009 = '-sql can not be used with FileMaker',
+ 7010 = 'Record locked by another user', // see error_data
+ 7011 = 'Record lock not valid any more',
+ 7012 = 'Could not set record lock', // see error_data
+ 7013 = 'Failed to clear record locks', // see error_data
+ 7016 = 'Add error', // see error_data
+ 7017 = 'Add failed, duplicate key value',
+ 7018 = 'Update error', // see error_data
+ 7019 = 'Delete error', // see error_data
+ 7020 = 'Keyfield not present in query',
+ 7021 = 'Lockfield not present in query',
+
+ // form errors 7100
+ 7101 ='Form validation failed',
+ 7102 = 'Unsupported field type',
+ 7103 = 'Form->process requires that a database object is defined for the form',
+ 7104 = 'Copyfield must copy to a different field name',
+
+ // grid errors 7200
+
+ // lang errors 7300
+
+ // nav errors 7400
+
+ // user errors 7500
+ 7501 = 'Authentication failed',
+ 7502 = 'Username or password missing',
+ 7503 = 'Client fingerprint has changed'
+
+ );
+ #error_lang -> (addlanguage: -language='en', -strings=@#errorcodes);
+ // add any custom error strings
+ iterate(#error_lang_custom -> 'strings', local('custom_language'));
+ if(#error_lang -> 'strings' !>> #custom_language -> name);
+ // add entire language at once
+ #error_lang -> addlanguage(-language=#custom_language -> name, -strings=#custom_language -> value);
+ else;
+ // add one string at a time
+ iterate(#custom_language -> value, local('custom_string'));
+ #error_lang -> insert(-language=#custom_language -> name,
+ -key=#custom_string -> name,
+ -value=#custom_string -> value);
+ /iterate;
+ /if;
+ /iterate;
+
+ if: #errorcodes >> #error_code;
+ // return error message defined by this tag
+ if: #error_lang -> keys >> #error_code;
+ return: #error_lang -> (getstring: #error_code);
+ else;
+ return: #errorcodes -> (find: #error_code);
+ /if;
+ else;
+ if: (self -> 'error_msg') != '';
+ // return literal error message
+ return: (self -> 'error_msg');
+ else;
+ // test for error known by lasso
+ error_code = #error_code;
+ // return Lasso error message
+ return: error_msg;
+ /if;
+ /if;
+ /define_tag;
+
+ define_tag: 'varname', -description='Returns the name of the variable that this type instance is stored in.';
+ local: 'timer'=knop_timer;
+ if: self -> 'instance_unique' == null;
+ self -> 'instance_unique' = knop_unique;
+ /if;
+ if: self -> 'instance_varname' == null;
+ // look for the var name and store it in instance variable
+ iterate: (vars -> keys), (local: 'varname');
+ if: (var: #varname) -> type == self -> type
+ && ((var: #varname) -> 'instance_unique') == (self -> 'instance_unique');
+ (self -> 'instance_varname')=#varname;
+ loop_abort;
+ /if;
+ /iterate;
+ /if;
+
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer;
+ return: self -> 'instance_varname';
+ /define_tag;
+
+ define_tag: 'trace', -description='Returns the debug trace for a type instance',
+ -optional='html',
+ -optional='xhtml';
+
+ local: 'endslash' = ((self -> (xhtml: params)) ? ' /' | '');
+ local: 'eol'=(local_defined: 'html') || #endslash -> size ? ' \n' | '\n';
+ local: 'trace'=(self -> 'debug_trace');
+ (self -> '_debug_trace') -> isa('array') ? #trace -> merge(self -> '_debug_trace');
+ return: #eol + 'Debug trace for ' + (self -> type ) + ' $' + (self -> varname) + #eol
+ + #trace -> (join: #eol) + #eol;
+
+ /define_tag;
+
+
+ define_tag: 'tagtime', -description='Returns the time it took to execute the last executed member tag for a type instance.',
+ -optional='html',
+ -optional='xhtml';
+ /* Standard timer code
+ At beginning of tag code:
+ local: 'timer'=knop_timer;
+
+ Before the end of tag code (before return):
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+
+ */
+ local: 'endslash' = ((self -> (xhtml: params)) ? ' /' | '');
+
+ ((local_defined: 'html') || (local_defined: 'xhtml')) ? return: (self -> type) + '->' + (self -> 'tagtime_tagname') + ': ' + (self -> 'tagtime') + ' ms ';
+ return: (self -> 'tagtime');
+ /define_tag;
+
+/define_type;
+
+
+
+?>
+[
+//------------------------------------------------------------------
+// End knop_base
+//------------------------------------------------------------------
+
+//##################################################################
+
+][
+//------------------------------------------------------------------
+// Begin knop_database
+//------------------------------------------------------------------
+
+]settable: removed reference for -table
+2009-09-18 JS Syntax adjustments for Lasso 9
+2009-06-26 JS ->nextrecord: Added deprecation warning
+2009-05-15 JS ->field: corrected the verification of the -index parameter
+2009-01-09 JS Added a check before calling resultset_count so it will not break in Lasso versions before 8.5
+2009-01-09 JS ->_unknowntag: fixed incorrect debug_trace
+2008-12-03 JS ->addrecord: improved how keyvalue is returned when adding records
+2008-12-03 JS ->addrecord: inserting a generated keyvalue can now be suppressed by specifying -keyvalue=false
+2008-12-03 JS ->saverecord and ->deleterecord will now use the current keyvalue (if any), so -keyvalue will not have to be specified in that case.
+2008-11-25 JS ->field and ->recorddata will no longer touch current_record if it was zero
+2008-11-24 JS ->field: Added -index parameter to be able to access any occurrence of the same field name
+2008-11-24 JS Added -> records that returns a new data type knop_databaserows
+2008-11-24 JS ->resultset_count: added support for -inlinename.
+2008-11-24 JS Changed ->nextrecord to ->next. ->nextrecord remains supported for backwards compatibility.
+2008-11-14 JS ->nextrecord resets the record pointer when reaching the last record
+2008-11-13 JS ->recorddata now honors the current record pointer (as incremented by -nextrecord)
+2008-11-13 JS ->recorddata: added -recordindex parameter so a specific record can be returned instead of the first found.
+2008-10-30 JS ->getrecord now REALLY works with integer keyvalues (double oops) - I thought I fixed it 2008-05-28 but misplaced a paren...
+2008-09-26 JS Added -> resultset_count corresponding to the same Lasso tag, so [resultset]...[/resultset] can now be used through the use of inlinename.
+2008-09-10 JS -> getrecord, ->saverecord, ->deleterecord: Corrected handling of lock user to work better with knop_user
+2008-07-09 JS ->saverecord: -keeplock now updates the lock timestamp
+2008-05-28 JS ->getrecord now works with integer keyvalues (oops)
+2008-05-27 JS ->get returns a new datatype knop_databaserow
+2008-05-27 JS Added ->size and ->get so a database object can be iterated. When iterating each row is returned as an array of field values.
+2008-05-27 JS Addedd ->nextrecord that increments the recordpointer each time it is called until the last record in the found set is reached. Returns true as long as there are more records. Useful in a while loop - see example below
+2008-05-27 JS Implemented record pointer 'current_record'. The record pointer is reset for each new query.
+2008-05-27 JS ->field: added -recordindex to get data from any record in the current found set
+2008-05-27 JS Added ->_unknowntag as shortcut to field
+2008-05-26 JS Removed onassign since it causes touble
+2008-05-26 JS Extended field_names to return the field names for any specified table, return field names also for db objects that have never been used for a database query and optionally return field types
+2008-01-29 JS ->getrecord now supports -sql. Make sure that the SQL statement includes the relevant keyfield (and lockfield if locking is used).
+2008-01-10 JS ->capturesearchvars: error_code and error_msg was mysteriously not set after database operations that caused errors.
+2008-01-08 JS ->saverecord: added flag -keeplock to be able to save a locked record without releasing the lock
+2007-12-15 JS Adding support for knop_user in record locking is in progress. Done for ->oncreate and ->getrecord.
+2007-12-11 JS Moved error_code and error_msg to knop_base
+2007-12-11 JS Added documentation as -description to most member tags, to be used by the new ->help tag
+2007-12-11 JS Moved ->help to knop_base
+2007-12-10 JS Added ->settable to be able to copy an existing database object and properly set a new table name for it. Faster than creating a new instance from scratch.
+2007-12-03 JS Corrected shown_first once again, hoping it's right this time
+2007-11-29 JS Added support for field_names and corresponding member tag ->field_names
+2007-11-05 JS Added var name to trace output
+2007-10-26 JS ->capturesearchvars: corrected shown_first when no records found
+2007-10-26 JS ->oncreate: added default value "keyfield" if the -keyfield parameter is not specified
+2007-09-06 JS Corrected self -> 'tagtime' typo
+2007-06-18 JS Added tag timer to most member tags
+2007-06-13 JS added inheritance from knop_base
+2007-06-11 JC added handling of xhtml output
+2007-05-30 JS Changed recordid_value to keyfield_value and -recordid to -keyvalue
+2007-05-28 JS ->oncreate: Added clearing of current error at beginning of tag
+2007-04-19 JS Corrected the handling of -maxrecords and -skiprecords for SQL selects that have LIMIT specified
+2007-04-19 JS Improved handling of foundrows so it finds any whitespace around SQL keywords, instead of just plain spaces
+2007-04-18 JS ->select now populates recorddata with all the fields for the first found record. Previously it only populated recorddata when there was 1 found record.
+2007-04-12 JS ->oncreate: Added authentication inline around Database_TableNames../Database_TableNames
+2007-04-10 JS ->oncreate: Improved validation of table name (table_realname can sometimes be null even for valid table names)
+2007-04-03 JS Changed namespace from mt_ to knop_
+2007-02-02 JS Improved reporting of Lasso error messaged in error_msg
+2007-01-30 JS Added real error codes and additional error data for some errors (like record locked)
+2007-01-30 JS Changed -keyvalue parameters to copy value instead of pass as reference, to not cause problems when using keyvalue from the same db object as is being updated, for example $db->(saverecord: -keyvalue=$db->keyvalue)
+2007-01-26 JS Adjusted affectedrecord_keyvalue so it's only captured for -add and -update
+2007-01-23 JS Supports -uselimit (or querys that use LIMIT) and still gets proper searchresult vars (using a separate COUNT(*) query) - may not always get the right result for example for queries with GROUP BY
+2007-01-23 JS -keyfield can be specified for saverecord to override the default
+2007-01-23 JS Changed name of ->updaterecord to ->saverecord
+2007-01-23 JS Fixed bug where keyfield was missing as returnfield when looking up locked record for deleterecord
+2007-01-23 JS Added ->field
+2007-01-19 JS Added maxrecords_value and skiprecords_value to searchresultvars
+2007-01-18 JS Added affectedrecord_keyvalue to make it possible to highlight affected record in record list (grid)
+
+
+TODO:
+Allow -keyfield to be specified for ->addrecord and ->deleterecord
+Add some Active Record similar functionality for editing
+Look at making it so -table can be set dynamically instead of fixed at oncreate, to eliminate the need for one db object for each table. This can cause problems with record locks and how they interact with knop_user
+datetime_create and datetime_mod, and also user_create and user_mod.
+ Use default field names but allow to override at oncreate, and verify them at oncreate before trying to use them.
+
+
+*/
+
+ // instance variables
+ // these variables are set once
+ local: 'database'=string,
+ 'table'=string,
+ 'table_realname'=string, // the actual table name, to be used in SQL statements (in case the table name is aliased in Lasso)
+ 'username'=string,
+ 'password'=string,
+ 'db_connect'=array,
+ 'host'=array, // add support for inline host method
+ 'datasource_name'=string,
+ 'isfilemaker'=false,
+ 'lock_expires'=1800, // seconds before a record lock expires
+ 'lock_seed'=knop_seed, // encryption seed for the record lock
+ 'error_lang'=(knop_lang: -default='en', -fallback),
+ 'user'=null, // knop_user that will be used for record locking
+ 'databaserows_map'=map; // map to hold databaserows for each inlinename
+
+ // these variables are set for each query
+ local: 'inlinename'=string, // the inlinename that holds the result of the latest db operation
+ 'keyfield'=string,
+ 'keyvalue'=null,
+ 'affectedrecord_keyvalue'=null, // keyvalue of last added or updated record (not reset by other db actions)
+ 'lockfield'=string,
+ 'lockvalue'=null,
+ 'lockvalue_encrypted'=null,
+ 'timestampfield'=string, // for optimistic locking
+ 'timestampvalue'=string,
+ 'searchparams'=string, // the resulting pair array used in the database action
+ 'querytime'=integer, // query time in ms
+ // 'tagtime'=integer, moved to knop_base
+ 'recorddata'=map, // for single record results, a map of all returned db fields
+ 'error_data'=map, // additional data for certain errors
+ 'message'=string, // user message for normal result
+ 'current_record'=integer, // index of the current record to get field values from a specific record
+ 'field_names_map'=map,
+ 'resultset_count_map'=map; // resultset_count stored for each inlinename
+ // these vars have directly corresponding Lasso tags so they can be set programatically
+ local: 'searchresultvars'=(array: 'action_statement', 'found_count', 'shown_first',
+ 'shown_last', 'shown_count', 'field_names', 'records_array', 'maxrecords_value', 'skiprecords_value');
+ iterate: #searchresultvars, (local: 'resultvar');
+ local(#resultvar = null);
+ /iterate;
+
+ local: 'errors_error_data'=(map: 7010, 7012, 7013, 7016, 7018, 7019); // these error codes can have more info in error_data map
+
+ define_tag: 'oncreate',
+ -required='database',
+ -required='table',
+ -optional='host', // add support for inline host method
+ -optional='username',
+ -optional='password',
+ -optional='keyfield',
+ -optional='lockfield',
+ -optional='user',
+ -optional='validate'; // validate the database connection info (adds the overhead of making a test connection to the database)
+ local: 'timer'=knop_timer;
+
+ // reset error
+ error_code = 0;
+ error_msg = error_noerror;
+
+ // validate database and table names to make sure they exist in Lasso
+ (self -> 'datasource_name') = Lasso_DatasourceModuleName: #database;
+ fail_if: error_code != 0, error_code, error_msg;
+
+ // store params as instance variables
+ local_defined('database') ? (self -> 'database') = @#database;
+ local_defined('table') ? (self -> 'table') = @#table;
+ local_defined('host') ? (self -> 'host') = @#host; // add support for inline host method
+ local_defined('username') ? (self -> 'username') = @#username;
+ local_defined('password') ? (self -> 'password') = @#password;
+ local_defined('lockfield') ? (self -> 'lockfield') = @#lockfield;
+ local_defined('user') ? (self -> 'user') = @#user;
+ // param has default value
+ (self -> 'keyfield') = (local_defined('keyfield')
+ ? @#keyfield // use parameter value
+ | 'keyfield'); // use default value
+
+
+ // build inline connection array
+ local_defined('database') ? (self -> 'db_connect') -> insert('-database' = @#database);
+ local_defined('table') ? (self -> 'db_connect') -> insert('-table' = @#table);
+ local_defined('host') ? (self -> 'db_connect') -> insert('-host' = @#host); // add support for inline host method
+ local_defined('username') ? (self -> 'db_connect') -> insert('-username' = @#username);
+ local_defined('password') ? (self -> 'db_connect') -> insert('-password' = @#password);
+
+ (self -> 'table_realname') = (table_realname: #database, #table);
+ if: (self -> 'table_realname') == null;
+ // verify that the table exists even if table_realname is null
+ inline: (self -> 'db_connect');
+ Database_TableNames: #database;
+ if: Database_TableNameItem == #table;
+ (self -> 'table_realname') = #table;
+ loop_abort;
+ /if;
+ /Database_TableNames;
+ /inline;
+ /if;
+ fail_if: (self -> 'table_realname') == null, 7001, self -> error_msg(7001); // The specified table was not found
+
+ if: (local_defined: 'validate');
+ // validate db connection
+ inline: (self -> 'db_connect');
+ fail_if: error_code != 0, error_code, error_msg;
+ /inline;
+ /if;
+
+ if: Lasso_DatasourceIsFilemaker: #database || Lasso_DatasourceIsFilemakerSA: #database;
+ (self -> 'isfilemaker') = true;
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': creating database object on ' + (self -> 'datasource_name') +', isfilemaker: ' + (self -> 'isfilemaker') + ' at ' + (date -> (format: '%Q %T')));
+
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+
+ /define_tag;
+
+ /*
+ define_tag: 'onassign', -required='value', -description='Internal, needed to restore references when ctype is defined as prototype';
+ // recreate references here
+ (self -> 'user') = @(#value -> 'user');
+ /define_tag;
+ */
+
+ define_tag('_unknowntag', -description='Shortcut to field');
+ if((self -> 'field_names_map') >> tag_name);
+ return(self -> field(tag_name));
+ else;
+ //fail(-9948, self -> type + '->' + tag_name + ' not known.');
+ (self -> 'debug_trace') -> insert(self -> type + '->' + tag_name + ' not known.');
+ /if;
+ /define_tag;
+
+ define_tag: 'settable', -description='Changes the current table for a database object. Useful to be able to create \
+ database objects faster by copying an existing object and just change the table name. This is a little bit faster \
+ than creating a new instance from scratch, but no table validation is performed. Only do this to add database \
+ objects for tables within the same database as the original database object. ',
+ -required='table', -type='string';
+ local: 'timer'=knop_timer;
+
+ (self -> 'error_code')=0;
+ (self -> 'error_msg')=string;
+ (self -> 'table_realname') = #table;
+ (self -> 'db_connect') -> removeall(#table);
+ (self -> 'db_connect') -> (insert: '-table' = #table);
+ (self -> 'table_realname') = (table_realname: self -> 'database', #table);
+ if: (self -> 'table_realname') == null;
+ // verify that the table exists even if table_realname is null
+ inline: (self -> 'db_connect');
+ Database_TableNames: (self -> 'database');
+ if: Database_TableNameItem == #table;
+ (self -> 'table_realname') = #table;
+ loop_abort;
+ /if;
+ /Database_TableNames;
+ /inline;
+ /if;
+ fail_if: (self -> 'table_realname') == null, 7001, self -> error_msg(7001); // The specified table was not found
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ define_tag: 'select', -description='perform database query, either Lasso-style pair array or SQL statement.\
+ ->recorddata returns a map with all the fields for the first found record. \
+ If multiple records are returned, the records can be accessed either through ->inlinename or ->records_array.\n\
+ Parameters:\n\
+ -search (optional array) Lasso-style search parameters in pair array\n\
+ -sql (optional string) Raw sql query\n\
+ -keyfield (optional) Overrides default keyfield, if any\n\
+ -keyvalue (optional)\n\
+ -inlinename (optional) Defaults to autocreated inlinename',
+ -optional='search', -type='array',
+ -optional='sql', -type='string',
+ -optional='keyfield',
+ -optional='keyvalue', -copy,
+ -optional='inlinename', -copy;
+
+ knop_debug(self->type + ' -> ' + tag_name, -open, -type=self->type);
+ handle;
+ //knop_debug(-close, -witherrors, -type=self->type);
+ knop_debug('Done with ' + self->type + ' -> ' + tag_name, -close, -witherrors, -time);
+ /handle;
+ local: 'timer'=knop_timer;
+
+ // clear all search result vars
+ self -> reset;
+
+ local: '_search'=(local: 'search'),
+ '_sql'=(local: 'sql');
+ if: #_search -> type != 'array';
+ #_search = array;
+ /if;
+ if: #_sql != '' && (self -> 'isfilemaker');
+ #_sql='';
+ fail: 7009, self -> error_msg(7009); // sql can not be used with filemaker
+ /if;
+ // inlinename defaults to a random string
+ (self -> 'inlinename') = ((local: 'inlinename') != '' ? #inlinename | 'inline_' + knop_unique);
+ #_search -> (removeall: -inlinename);
+ #_search -> (insert: -inlinename=(self -> 'inlinename'));
+
+ // remove all database actions from the search array
+ #_search -> (removeall: -search) & (removeall: -add) & (removeall: -delete) & (removeall: -update)
+ & (removeall: -sql) & (removeall: -nothing) & (removeall: -show)
+ // & (removeall: -table) // table is ok to override
+ & (removeall: -database);
+
+ if: (local: 'sql') != '' && (string_findregexp: #sql, -find='\\bLIMIT\\b', -ignorecase) -> size;
+ (self -> 'debug_trace') -> (insert: tag_name + ': grabbing -maxrecords and -skiprecords from search array');
+ // store maxrecords and skiprecords for later use
+ if: #_search >> '-maxrecords';
+ (self -> 'maxrecords_value') = #_search -> (find: '-maxrecords') -> last -> value;
+ (self -> 'debug_trace') -> (insert: tag_name + ': -maxrecords value found in search array ' + (self -> 'maxrecords_value'));
+ /if;
+ if: #_search >> '-skiprecords';
+ (self -> 'skiprecords_value') = #_search -> (find: '-skiprecords') -> last -> value;
+ (self -> 'debug_trace') -> (insert: tag_name + ': -skiprecords value found in search array ' + (self -> 'skiprecords_value'));
+ /if;
+ // remove skiprecords from the actual search parameters since it will conflict with LIMIT
+ #_search -> (removeall: '-skiprecords');
+ /if;
+
+ if: !(local_defined: 'keyfield') && (self -> 'keyfield') != '';
+ local: 'keyfield'=(self -> 'keyfield');
+ /if;
+ if: (local: 'keyfield') != '';
+ #_search -> (removeall: '-keyfield');
+ if: !(self -> 'isfilemaker');
+ #_search -> (insert: '-keyfield'=#keyfield);
+ /if;
+ if: (local: 'keyvalue') != '';
+ #_search -> (removeall: '-keyvalue');
+ if: (self -> 'isfilemaker');
+ #_search -> (insert: '-op'='eq');
+ #_search -> (insert: #keyfield=#keyvalue);
+ else;
+ #_search -> (insert: '-keyvalue'=#keyvalue);
+ /if;
+ /if;
+ /if;
+
+ // add sql action or normal search action
+ if: #_sql != '';
+ #_search -> (insert: '-sql'=#_sql);
+ else;
+ #_search -> (insert: '-search');
+ /if;
+ // perform database query, put connection parameters last to override any provided by the search parameters
+ //(self -> 'debug_trace') -> (insert: tag_name + ': search ' + #_search);
+ local: 'querytimer'=knop_timer;
+ inline: #_search,(self -> 'db_connect');
+ (self -> 'querytime') = integer: #querytimer;
+ (self -> 'searchparams') = #_search;
+ (self -> 'debug_trace') -> (insert: tag_name ': action_statement ' + action_statement);
+ knop_debug(action_statement, -sql);
+ knop_debug(found_count ' found');
+ self -> capturesearchvars;
+ /inline;
+
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ (self -> 'debug_trace') -> (insert: tag_name + ': found ' (self -> 'found_count') + ' records in ' + (self -> 'querytime') + ' ms, tag time ' + (self -> 'tagtime') + ' ms, ' + (self -> error_msg) + ' ' + (self -> error_code));
+ /define_tag;
+
+
+ define_tag: 'addrecord', -description='Add a new record to the database. A random string keyvalue will be generated unless a -keyvalue is specified. \n\
+ Parameters:\n\
+ -fields (required array) Lasso-style field values in pair array\n\
+ -keyvalue (optional) If -keyvalue is specified, it must not already exist in the database. Specify -keyvalue=false to prevent generating a keyvalue. \n\
+ -inlinename (optional) Defaults to autocreated inlinename',
+ -required='fields', -type='array',
+ -optional='keyvalue', -copy,
+ -optional='inlinename';
+ local: 'timer'=knop_timer;
+
+ // clear all search result vars
+ self -> reset;
+ local: '_fields'=#fields;
+
+ // remove all database actions from the search array
+ #_fields -> (removeall: '-search') & (removeall: '-add') & (removeall: '-delete') & (removeall: '-update')
+ & (removeall: '-sql') & (removeall: '-nothing') & (removeall: '-show')
+ // & (removeall: '-table') // table is ok to override
+ & (removeall: '-database');
+
+ inline: (self -> 'db_connect'); // connection wrapper
+ if: (local: 'keyvalue') != '' && (local: 'keyvalue') !== false && (self -> 'keyfield')!='';
+ // look for existing keyvalue
+ inline: -op='eq', (self -> 'keyfield')=#keyvalue,
+ -maxrecords=1,
+ -returnfield=(self -> 'keyfield'),
+ -search;
+ if: found_count > 0;
+ (self -> 'error_code') = 7017; // duplicate keyvalue
+ else;
+ (self -> 'keyvalue') = #keyvalue;
+ /if;
+ /inline;
+ /if;
+
+
+ if: (self -> 'error_code') == 0;
+ // proceed to add record
+
+ if: (self -> 'keyfield') != '';
+ if: (local: 'keyvalue') == '' && (local: 'keyvalue') !== false;
+ (self -> 'debug_trace') -> (insert: tag_name + ': generating keyvalue');
+ // create unique keyvalue
+ (self -> 'keyvalue')=knop_unique;
+ /if;
+ #_fields -> (removeall: (self -> 'keyfield'));
+ #_fields -> (removeall: '-keyfield') & (removeall: '-keyvalue');
+ #_fields -> (insert: '-keyfield'=(self -> 'keyfield'));
+ if: (local: 'keyvalue') !== false;
+ #_fields -> (insert: (self -> 'keyfield')=(self -> 'keyvalue'));
+ /if;
+ /if;
+
+ // inlinename defaults to a random string
+ (self -> 'inlinename') = ((local: 'inlinename') != '' ? #inlinename | 'inline_' + knop_unique);
+ #_fields -> (removeall: '-inlinename');
+ #_fields -> (insert: '-inlinename'=(self -> 'inlinename'));
+
+ local: 'querytimer'=knop_timer;
+ inline: #_fields, -add;
+ (self -> 'querytime') = integer: #querytimer;
+ (self -> 'searchparams') = #_fields;
+
+ self -> capturesearchvars;
+ if: error_code != 0;
+ (self -> 'keyvalue') = null;
+ /if;
+ /inline;
+ /if;
+ /inline;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ (self -> 'debug_trace') -> (insert: tag_name + ': ' + (self -> error_msg) + ' ' + (self -> error_code)
+ + ' keyvalue ' + (self -> 'keyvalue') + ' ' + (self -> 'tagtime') + ' ms');
+ /define_tag;
+
+
+ define_tag: 'getrecord', -description='Returns a single specific record from the database, optionally locking the record. \
+ If the keyvalue matches multiple records, an error is returned. \n\
+ Parameters:\n\
+ -keyvalue (optional) Uses a previously set keyvalue if not specified. If no keyvalue is available, an error is returned unless -sql is used. \n\
+ -keyfield (optional) Temporarily override of keyfield specified at oncreate\n\
+ -inlinename (optional) Defaults to autocreated inlinename\n\
+ -lock (optional flag) If flag is specified, a record lock will be set\n\
+ -user (optional) The user who is locking the record (required if using lock)\n\
+ -sql (optional) SQL statement to use instead of keyvalue. Must include the keyfield (and lockfield of locking is used).',
+ -optional='keyvalue', -copy,
+ -optional='keyfield',
+ -optional='inlinename', -copy,
+ -optional='lock',
+ -optional='user', -copy,
+ -optional='sql', -type='string';
+ local: 'timer'=knop_timer;
+
+ local: '_sql'=(local: 'sql');
+
+ if: #_sql != '' && (self -> 'isfilemaker');
+ #_sql='';
+ fail: 7009, self -> error_msg(7009); // sql can not be used with filemaker
+ /if;
+
+ // get existing record pointer if any
+ if: #_sql -> size == 0 && !(local_defined: 'keyvalue');
+ local: 'keyvalue'=(self -> 'keyvalue');
+ else: !(local_defined: 'keyvalue');
+ local: 'keyvalue'=string;
+ /if;
+
+ // clear all search result vars
+ self -> reset;
+
+ fail_if: !(local_defined: 'keyfield') && (self -> 'keyfield') == '', 7002, self -> error_msg(7002); // Keyfield not specified
+ if: (local_defined: 'lock') && #lock != false;
+ fail_if: (self -> 'lockfield') == '', 7003, self -> error_msg(7003); // Lockfield must be specified to get record with lock
+ if: !(local_defined: 'user') && ((self -> 'user') != '' || (self -> 'user') -> isa('user'));
+ // use user from database object
+ local('user' = (self -> 'user'));
+ /if;
+ fail_if: (local: 'user') == '' && !((local: 'user') -> isa('user')), 7004, self -> error_msg(7004); // User must be specified to get record with lock
+ (self -> 'debug_trace') -> insert(tag_name ': user is type ' + (#user -> type) + ', isa(user) = ' + (#user -> isa('user')) );
+ if: #user -> isa('user');
+ #user= #user -> id_user;
+ fail_if: #user == '', 7004, self -> error_msg(7004); // User must be logged in to get record with lock
+ /if;
+ (self -> 'debug_trace') -> insert(tag_name ': user id is ' + #user);
+ /if;
+ if: !(local_defined: 'keyfield') && (self -> 'keyfield') != '';
+ local: 'keyfield'=(self -> 'keyfield');
+ /if;
+ if: #_sql -> size == 0 && string(#keyvalue) -> size == 0;
+ (self -> 'error_code') = 7007; // keyvalue missing
+ /if;
+ if: (self -> 'error_code') == 0;
+ inline: (self -> 'db_connect'); // connection wrapper
+
+ if: #_sql -> size;
+ self -> (select: -sql=#_sql, -inlinename=(local: 'inlinename'));
+ #keyvalue = (self -> 'keyvalue');
+ else;
+ self -> (select: -keyfield=#keyfield, -keyvalue=#keyvalue, -inlinename=(local: 'inlinename'));
+ /if;
+ if: (self -> field_names) !>> #keyfield;
+ (self -> 'error_code') = 7020; // Keyfield not present in query
+ /if;
+ if: (self -> field_names) !>> (self -> 'lockfield') && (local_defined: 'lock') && #lock != false;
+ (self -> 'error_code') = 7021; // Lockfield not present in query
+ /if;
+
+ if: (self -> 'found_count') == 0 && (self -> 'error_code') == 0;
+ (self -> 'error_code') = -1728;
+ else: (self -> 'found_count') > 1 && (self -> 'error_code') == 0;
+ self -> reset;
+ (self -> 'error_code') = 7008; // keyvalue not unique
+ /if;
+
+
+ // handle record locking
+ if: (self -> 'error_code') == 0 && (local_defined: 'lock') && #lock != false;
+ // check for current lock
+ if: (self -> 'lockvalue') != '';
+ // there is a lock already set, check if it has expired or if it is the same user
+ local: 'lockvalue'=(self -> 'lockvalue') -> (split: '|');
+ local: 'lock_timestamp'=date: (#lockvalue->size > 1 ? #lockvalue -> (get: 2) | null);
+ local: 'lock_user'=#lockvalue -> first;
+ if: (date - #lock_timestamp) -> seconds < (self -> 'lock_expires')
+ && #lock_user != #user;
+ // the lock is still valid and it is locked by another user
+ // this is not a real error, more a warning condition
+ (self -> 'error_code') = 7010;
+ (self -> 'error_data') = (map: 'user' = #lock_user, 'timestamp' = #lock_timestamp);
+ (self -> 'keyvalue') = null;
+ (self -> 'debug_trace') -> (insert: tag_name ': record ' + #keyvalue + ' was already locked by ' + #lock_user + '.');
+ /if;
+ /if;
+ if: (self -> 'error_code') == 0;
+ // go ahead and lock record
+ (self -> 'lockvalue') = #user + '|' + (date -> format: '%Q %T');
+ (self -> 'lockvalue_encrypted') = (encrypt_blowfish: (self -> 'lockvalue'), -seed=(self -> 'lock_seed'));
+ local: 'keyvalue_temp'=#keyvalue;
+ if: (self -> 'isfilemaker');
+ // find internal keyvalue
+ inline: -op='eq', #keyfield=#keyvalue,
+ -search;
+ if: found_count == 1;
+ #keyvalue_temp=keyfield_value;
+ (self -> 'debug_trace') -> (insert: tag_name + ': will set record lock for FileMaker record id ' + keyfield_value + ' ' + error_msg + ' ' + error_code);
+ else;
+ (self -> 'debug_trace') -> (insert: tag_name + ': could not get record id for FileMaker record, ' found_count + ' found ' + + error_msg + ' ' + error_code);
+ /if;
+ /inline;
+ /if;
+ inline: -keyfield=#keyfield,
+ -keyvalue=#keyvalue_temp,
+ (self -> 'lockfield')=(self -> 'lockvalue'),
+ -update;
+ if: error_code;
+ (self -> 'error_code') = 7012; // could not set record lock
+ (self -> 'error_data') = (map: 'error_code'=error_code, 'error_msg'=error_msg);
+ (self -> 'lockvalue') = null;
+ (self -> 'lockvalue_encrypted') = null;
+ (self -> 'keyvalue') = null;
+ else;
+ // lock was set ok
+ (self -> 'debug_trace') -> (insert: tag_name + ': set record lock ' + (self -> 'lockvalue') + ' ' + (self -> 'lockvalue_encrypted'));
+ if: (self -> 'user') -> isa('user');
+ // tell user it has locked a record in this db object
+ (self -> 'user') -> addlock(-dbname=self -> varname);
+ /if;
+ /if;
+ /inline;
+ /if;
+ /if;
+
+ /inline;
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ (self -> 'debug_trace') -> (insert: tag_name + ': ' + (self -> error_msg) + ' ' + (self -> error_code) + ' ' + (self -> 'tagtime') + ' ms');
+ /define_tag;
+
+
+ define_tag: 'saverecord', -description='Updates a specific database record. \n\
+ Parameters:\n\
+ -fields (required array) Lasso-style field values in pair array\n\
+ -keyfield (optional) Keyfield is ignored if lockvalue is specified\n\
+ -keyvalue (optional) Keyvalue is ignored if lockvalue is specified\n\
+ -lockvalue (optional) Either keyvalue or lockvalue must be specified\n\
+ -keeplock (optional flag) Avoid clearing the record lock when saving. Updates the lock timestamp.\n\'
+ -user (optional) If lockvalue is specified, user must be specified as well\n\
+ -inlinename (optional) Defaults to autocreated inlinename',
+ -required='fields', -type='array',
+ -optional='keyfield',
+ -optional='keyvalue', -copy,
+ -optional='lockvalue', -copy,
+ -optional='keeplock',
+ -optional='user', -copy,
+ -optional='inlinename', -copy;
+
+ local: 'timer'=knop_timer;
+
+ if(!local_defined('keyvalue') && string(self -> 'keyvalue') -> size);
+ // use current record's keyvalue if any
+ local('keyvalue'=(self -> 'keyvalue'));
+ /if;
+
+ // clear all search result vars
+ self -> reset;
+
+ fail_if: !(local_defined: 'keyvalue') && !(local_defined: 'lockvalue'), 7005, self -> error_msg(7005); // Either keyvalue or lockvalue must be specified for update or delete
+ fail_if: (local_defined: 'keyvalue') && (self -> 'keyfield') == '' && (local: 'keyfield') == '', 7002, self -> error_msg(7002); // Keyfield not specified
+ if: (local_defined: 'lockvalue');
+ fail_if: (self -> 'lockfield') == '', 7003, self -> error_msg(7003); // Lockfield not specified
+ if: !(local_defined: 'user') && ((self -> 'user') != '' || (self -> 'user') -> isa('user'));
+ // use user from database object
+ local('user' = (self -> 'user'));
+ /if;
+ fail_if: (local: 'user') == '' && !((local: 'user') -> isa('user')), 7004, self -> error_msg(7004);
+ (self -> 'debug_trace') -> insert(tag_name ': user is type ' + (#user -> type) + ', isa(user) = ' + (#user -> isa('user')) );
+ if: #user -> isa('user');
+ #user= #user -> id_user;
+ fail_if: #user == '', 7004, self -> error_msg(7004); // User must be logged in to get record with lock
+ /if;
+ (self -> 'debug_trace') -> insert(tag_name ': user id is ' + #user);
+ /if;
+
+ !(local_defined: 'keyfield') ? local: 'keyfield'=self -> 'keyfield';
+
+ local: '_fields'=#fields;
+
+ // remove all database actions from the search array
+ #_fields -> (removeall: '-search') & (removeall: '-add') & (removeall: '-delete') & (removeall: '-update')
+ & (removeall: '-sql') & (removeall: '-nothing') & (removeall: '-show')
+ // & (removeall: '-table') // table is ok to override
+ & (removeall: '-database');
+ #_fields -> (removeall: '-keyfield') & (removeall: '-keyvalue');
+
+ inline: (self -> 'db_connect'); // connection wrapper
+
+ // handle record locking
+ if: (self -> 'error_code') == 0 && (local: 'lockvalue') != '';
+
+ // first check if record was locked by someone else, and that lock is still valid
+ local: 'lock'=(decrypt_blowfish: #lockvalue, -seed=(self -> 'lock_seed')) -> (split: '|');
+ local: 'lock_timestamp'=date: (#lock->size > 1 ? (#lock -> (get: 2)) | null);
+ local: 'lock_user'=#lock -> first;
+ if: (date - #lock_timestamp) -> seconds < (self -> 'lock_expires')
+ && #lock_user != #user;
+ // the lock is still valid and it is locked by another user
+ (self -> 'error_code') = 7010;
+ (self -> 'error_data') = (map: 'user' = #lock_user, 'timestamp' = #lock_timestamp);
+ /if;
+
+ // check that the current lock is still valid
+ if: (self -> 'error_code') == 0;
+ inline: -op='eq', (self -> 'lockfield')=#lock -> (join: '|'),
+ -maxrecords=1,
+ -returnfield=(self -> 'lockfield'),
+ -returnfield=(self -> 'keyfield'),
+ -search;
+ if: error_code == 0 && found_count != 1;
+ // lock is not valid any more
+ (self -> 'error_code') = 7011; // Update failed, record lock not valid any more
+ else: error_code != 0;
+ (self -> 'error_code') = 7018; // Update error
+ (self -> 'error_data') = (map: 'error_code'=error_code, 'error_msg'=error_msg);
+ else;
+ // lock OK, grab keyvalue for update
+ local: 'keyvalue'=(field: (self -> 'keyfield'));
+ /if;
+ /inline;
+ /if;
+
+ if: (self -> 'error_code') == 0;
+ // go ahead and release record lock by clearing the field value in the update fields array
+ #_fields -> (removeall: (self -> 'lockfield'));
+ if: ((local_defined: 'keeplock') && #keeplock != false);
+ // update the lock timestamp
+ (self -> 'lockvalue') = #user + '|' + (date -> format: '%Q %T');
+ (self -> 'lockvalue_encrypted') = (encrypt_blowfish: (self -> 'lockvalue'), -seed=(self -> 'lock_seed'));
+ #_fields -> (insert: (self -> 'lockfield')=(self -> 'lockvalue'));
+ else;
+ #_fields -> (insert: (self -> 'lockfield') = '');
+ /if;
+ /if;
+
+ /if;
+
+ if: (self -> 'error_code') == 0 && (local: 'keyvalue') != '';
+ if: (self -> 'isfilemaker');
+ inline: -op='eq', #keyfield=#keyvalue, -search;
+ if: found_count == 1;
+ #_fields -> (insert: '-keyvalue'=keyfield_value);
+ (self -> 'debug_trace') -> (insert: tag_name + ': FileMaker record id ' + keyfield_value);
+ /if;
+ /inline;
+ else;
+ #_fields -> (insert: '-keyfield'=#keyfield);
+ #_fields -> (insert: '-keyvalue'=#keyvalue);
+ /if;
+ /if;
+
+
+
+ if: (#_fields >> '-keyfield' && #_fields -> (find: '-keyfield') -> first -> value != '' || (self -> 'isfilemaker'))
+ && #_fields >> '-keyvalue' && #_fields -> (find: '-keyvalue') -> first -> value != '';
+ // ok to update
+ else: (self -> 'error_code') == 0;
+ (self -> 'error_code') = 7006; // Update failed, keyfield or keyvalue missing';
+ /if;
+
+ // update record
+ if: (self -> 'error_code') == 0;
+
+ // inlinename defaults to a random string
+ (self -> 'inlinename') = ((local: 'inlinename') != '' ? #inlinename | 'inline_' + knop_unique);
+ #_fields -> (removeall: '-inlinename');
+ #_fields -> (insert: '-inlinename'=(self -> 'inlinename'));
+
+ local: 'querytimer'=knop_timer;
+ inline: #_fields, -update;
+ (self -> 'querytime') = integer: #querytimer;
+ (self -> 'searchparams') = #_fields;
+ self -> capturesearchvars;
+ /inline;
+ /if;
+ /inline;
+
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ (self -> 'debug_trace') -> (insert: tag_name + ': ' + (self -> 'keyvalue') + ' '+ (self -> error_msg) + ' ' + (self -> error_code) + ' ' + (self -> 'tagtime') + ' ms');
+ /define_tag;
+
+
+ define_tag: 'deleterecord', -description='Deletes a specific database record. \n\
+ Parameters:\n\
+ -keyvalue (optional) Keyvalue is ignored if lockvalue is specified\n\
+ -lockvalue (optional) Either keyvalue or lockvalue must be specified\n\
+ -user (optional) If lockvalue is specified, user must be specified as well',
+ -optional='keyvalue', -copy,
+ -optional='lockvalue', -copy,
+ -optional='user';
+ local: 'timer'=knop_timer;
+
+ if(!local_defined('keyvalue') && string(self -> 'keyvalue') -> size);
+ // use current record's keyvalue if any
+ local('keyvalue'=(self -> 'keyvalue'));
+ /if;
+
+ // clear all search result vars
+ self -> reset;
+
+ fail_if: !(local_defined: 'keyvalue') && !(local_defined: 'lockvalue'), 7005, self -> error_msg(7005); // Either keyvalue or lockvalue must be specified for update or delete
+ fail_if: (local_defined: 'keyvalue') && (self -> 'keyfield') == '', 7002, self -> error_msg(7002); // Keyfield not specified
+ if: (local_defined: 'lockvalue');
+ fail_if: (self -> 'lockfield') == '', 7003, self -> error_msg(7003); // Lockfield not specified
+ if: !(local_defined: 'user') && ((self -> 'user') != '' || (self -> 'user') -> isa('user'));
+ // use user from database object
+ local('user' = (self -> 'user'));
+ /if;
+ fail_if: (local: 'user') == '' && !((local: 'user') -> isa('user')), 7004, self -> error_msg(7004);
+ (self -> 'debug_trace') -> insert(tag_name ': user is type ' + (#user -> type) + ', isa(user) = ' + (#user -> isa('user')) );
+ if: #user -> isa('user');
+ #user= #user -> id_user;
+ fail_if: #user == '', 7004, self -> error_msg(7004); // User must be logged in to get record with lock
+ /if;
+ (self -> 'debug_trace') -> insert(tag_name ': user id is ' + #user);
+ /if;
+
+ local: '_fields'=array;
+
+ inline: (self -> 'db_connect'); // connection wrapper
+
+ // handle record locking
+ if: (self -> 'error_code') == 0 && (local: 'lockvalue') != '';
+
+ // first check if record was locked by someone else, and that lock is still valid
+ local: 'lockvalue'=(decrypt_blowfish: #lockvalue, -seed=(self -> 'lock_seed')) -> (split: '|');
+ local: 'lock_timestamp'=date: (#lockvalue->size > 1 ? #lockvalue -> (get: 2) | null);
+ local: 'lock_user'=(#lockvalue -> first);
+ if: (date - #lock_timestamp) -> seconds < (self -> 'lock_expires')
+ && #lock_user != #user;
+ // the lock is still valid and it is locked by another user
+ (self -> 'error_code') = 7010; // Delete failed, record locked
+ (self -> 'error_data') = (map: 'user' = #lock_user, 'timestamp' = #lock_timestamp);
+ /if;
+
+ // check that the current lock is still valid
+ if: (self -> 'error_code') == 0;
+ inline: -op='eq', (self -> 'lockfield')=#lockvalue -> (join: '|'),
+ -maxrecords=1,
+ -returnfield=(self -> 'lockfield'),
+ -returnfield=(self -> 'keyfield'),
+ -search;
+ if: error_code == 0 && found_count != 1;
+ // lock is not valid any more
+ (self -> 'error_code') = 7011; // Delete failed, record lock not valid any more';
+ else: error_code != 0;
+ (self -> 'error_code') = 7019; // delete error
+ (self -> 'error_data') = (map: 'error_code'=error_code, 'error_msg'=error_msg);
+ else;
+ // lock OK, grab keyvalue for update
+ local: 'keyvalue'=(field: (self -> 'keyfield'));
+ (self -> 'debug_trace') -> (insert: tag_name + ': got keyvalue ' + #keyvalue + ' for keyfield ' + (self -> 'keyfield'));
+ /if;
+ /inline;
+ /if;
+
+ /if;
+
+ if: (self -> 'error_code') == 0 && (local: 'keyvalue') != '';
+ if: (self -> 'isfilemaker');
+ inline: -op='eq', (self -> 'keyfield')=#keyvalue, -search;
+ if: found_count == 1;
+ #_fields -> (insert: '-keyvalue'=keyfield_value);
+ (self -> 'debug_trace') -> (insert: tag_name + ': FileMaker record id ' + keyfield_value);
+ /if;
+ /inline;
+ else;
+ #_fields -> (insert: '-keyfield'=(self -> 'keyfield'));
+ #_fields -> (insert: '-keyvalue'=#keyvalue);
+ /if;
+ /if;
+
+ (self -> 'debug_trace') -> (insert: tag_name + ': will delete record with params ' + #_fields);
+
+ if: (#_fields >> '-keyfield' && #_fields -> (find: '-keyfield') -> first -> value != '' || (self -> 'isfilemaker'))
+ && #_fields >> '-keyvalue' && #_fields -> (find: '-keyvalue') -> first -> value != '';
+ // ok to delete
+ else;
+ (self -> 'error_code') = 7006; // Delete failed, keyfield or keyvalue missing
+ /if;
+
+ // delete record
+ if: (self -> 'error_code') == 0;
+
+ local: 'querytimer'=knop_timer;
+ inline: #_fields, -delete;
+ (self -> 'querytime') = integer: #querytimer;
+ (self -> 'searchparams') = #_fields;
+
+ self -> capturesearchvars;
+
+ /inline;
+ /if;
+ /inline;
+
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ (self -> 'debug_trace') -> (insert: tag_name + ': ' + (self -> error_msg) + ' ' + (self -> error_code) + ' ' + (self -> 'tagtime') + ' ms');
+ /define_tag;
+
+
+ define_tag: 'clearlocks', -description='Release all record locks for the specified user, suitable to use when showing record list. \n\
+ Parameters:\n\
+ -user (required) The user to unlock records for',
+ -required='user';
+ // release all record locks for the specified user, suitable to use when showing record list
+ local: 'timer'=knop_timer;
+
+ fail_if: (self -> 'lockfield') == '', 7003, self -> error_msg(7003); // Lockfield not specified
+ fail_if: #user == '', 7004, self -> error_msg(7004); // User not specified
+
+ if: (self -> 'isfilemaker');
+ inline: (self -> 'db_connect'),
+ -maxrecords=all,
+ (self -> 'lockfield')='"' + #user + '|"',
+ -search;
+ if: found_count > 0;
+ (self -> 'debug_trace') -> (insert: tag_name + ': clearing locks for ' + #user + ' in ' + found_count + ' FileMaker records ' + error_msg + ' ' + error_code);
+ records;
+ inline: -keyvalue=keyfield_value,
+ (self -> 'lockfield')='',
+ -update;
+ if: error_code;
+ (self -> 'error_code') = 7013; // Clearlocks failed
+ (self -> 'error_data') = (map: 'error_code'=error_code, 'error_msg'=error_msg);
+ (self -> 'debug_trace') -> (insert: tag_name + ': error when clearing lock on FileMaker record ' + keyfield_value + ' ' + error_msg + ' ' + error_code);
+ return;
+ /if;
+ /inline;
+ /records;
+ else: error_code;
+ (self -> 'error_code') = 7013; // Clearlocks failed
+ (self -> 'error_data') = (map: 'error_code'=error_code, 'error_msg'=error_msg);
+ /if;
+ /inline;
+ else;
+ inline: (self -> 'db_connect'),
+ -sql='UPDATE `' + (self -> 'table_realname') + '` SET `' + (self -> 'lockfield') + '`="" WHERE `' + (self -> 'lockfield')
+ + '` LIKE "' + (encode_sql: #user) + '|%"';
+ if: error_code != 0;
+ (self -> 'error_code') = 7013; // Clearlocks failed
+ (self -> 'error_data') = (map: 'error_code'=error_code, 'error_msg'=error_msg);
+ /if;
+ /inline;
+ (self -> 'debug_trace') -> (insert: tag_name + ': clearing all locks for ' + #user + ' ' + (self -> error_msg) + ' ' + (self -> error_code));
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ define_tag: 'action_statement'; return: (self -> 'action_statement'); /define_tag;
+ define_tag: 'found_count'; return: (self -> 'found_count'); /define_tag;
+ define_tag: 'shown_count'; return: (self -> 'shown_count'); /define_tag;
+ define_tag: 'shown_first'; return: (self -> 'shown_first'); /define_tag;
+ define_tag: 'shown_last'; return: (self -> 'shown_last'); /define_tag;
+ define_tag: 'maxrecords_value'; return: (self -> 'maxrecords_value'); /define_tag;
+ define_tag: 'skiprecords_value'; return: (self -> 'skiprecords_value'); /define_tag;
+ define_tag: 'keyfield'; return: (self -> 'keyfield'); /define_tag;
+ define_tag: 'keyvalue'; return: (self -> 'keyvalue'); /define_tag;
+ define_tag: 'lockfield'; return: (self -> 'lockfield'); /define_tag;
+ define_tag: 'lockvalue'; return: (self -> 'lockvalue'); /define_tag;
+ define_tag: 'lockvalue_encrypted'; return: (self -> 'lockvalue_encrypted'); /define_tag;
+ define_tag: 'querytime'; return: (self -> 'querytime'); /define_tag;
+ define_tag: 'inlinename'; return: (self -> 'inlinename'); /define_tag;
+ define_tag: 'searchparams'; return: (self -> 'searchparams'); /define_tag;
+ define_tag: 'resultset_count',
+ -optional='inlinename';
+ !local_defined('inlinename') ? local('inlinename'=(self -> 'inlinename'));
+ return((self -> 'resultset_count_map') -> find(#inlinename));
+ /define_tag;
+
+ define_tag('recorddata', -description='A map containing all fields, only available for single record results',
+ -optional='recordindex', -copy);
+ !local_defined('recordindex') ? local('recordindex'=(self -> 'current_record'));
+ #recordindex < 1 ? #recordindex = 1;
+ if(#recordindex == 1);
+ // return default (i.e. first) record
+ return(self -> 'recorddata');
+ else;
+ local('recorddata'=map);
+ iterate(self -> field_names, local('field_name'));
+ #recorddata -> insert(#field_name = (self -> 'records_array' -> get(#recordindex)
+ -> get(self -> 'field_names_map' -> find(#field_name))));
+ /iterate;
+ return(#recorddata);
+ /if;
+ /define_tag;
+
+ define_tag: 'records_array'; return: (self -> 'records_array'); /define_tag;
+
+ define_tag('field_names', -description='Returns an array of the field names from the last database query. If no database query has been performed, a "-show" request is performed. \n\
+ Parameters: \n\
+ -table (optional) Return the field names for the specified table\n\
+ -types (optional flag) If specified, returns a pair array with fieldname and corresponding Lasso data type',
+ -optional='table',
+ -optional='types');
+ !local_defined('table') ? local('table'=(self -> 'table'));
+ local('field_names'=(self -> 'field_names'));
+ if(#field_names -> size == 0 || (local_defined('types') && #types != false));
+ #field_names=array;
+ if(local_defined('types') && #types != false);
+ local('types_mapping'=map('text'='string', 'number'='decimal', 'date/time'='date'));
+ /if;
+ inline(self->'db_connect', -table=#table, -show);
+ if(local_defined('types') && #types != false);
+ loop(field_name(-count));
+ #field_names -> insert(field_name(loop_count) = #types_mapping->find(field_name(loop_count, -type)));
+ /loop;
+ else;
+ #field_names=field_names;
+ /if;
+ /inline;
+ /if;
+ return(@#field_names);
+ /define_tag;
+
+ define_tag('table_names', -description='Returns an array with all table names for the database');
+ local('table_names'=array);
+ inline(self -> 'db_connect');
+ Database_TableNames(self -> 'database');
+ #table_names -> insert(Database_TableNameItem);
+ /Database_TableNames;
+ /inline;
+ return(@#table_names);
+ /define_tag;
+
+ define_tag: 'error_data', -description='Returns more info for those errors that provide such';
+ if: (self -> 'errors_error_data') >> (self -> error_code);
+ return: (self -> 'error_data');
+ else;
+ return: map;
+ /if;
+ /define_tag;
+
+ define_tag('size');
+ return(self -> 'shown_count');
+ /define_tag;
+
+ define_tag('get', -required='index');
+ return(knop_databaserow(
+ -record_array=(self -> 'records_array' -> get(#index)),
+ -field_names=(self -> 'field_names')));
+ /define_tag;
+
+ define_tag('records', -description='Returns all found records as a knop_databaserows object',
+ -optional='inlinename');
+ !local_defined('inlinename') ? local('inlinename'=(self -> 'inlinename'));
+ if((self -> 'databaserows_map') !>> #inlinename);
+ // create knop_databaserows on demand
+ (self -> 'databaserows_map') -> insert(#inlinename = knop_databaserows(
+ -records_array=(self -> 'records_array'),
+ -field_names=(self -> 'field_names'))
+ );
+ /if;
+ return(@((self -> 'databaserows_map') -> find(#inlinename)));
+ /define_tag;
+
+ define_tag('field', -description='A shortcut to return a specific field from a single record result',
+ -required='fieldname',
+ -optional='recordindex',
+ -optional='index');
+ !local_defined('recordindex') ? local('recordindex'=(self -> 'current_record'));
+ #recordindex < 1 ? #recordindex = 1;
+ !local_defined('index') ? local('index'=1);
+ if(#recordindex == 1 && #index == 1);
+ // return first field occurrence from the default (i.e. first) record
+ return((self -> 'recorddata') -> find(#fieldname));
+ else(self -> 'field_names_map' >> #fieldname
+ && #recordindex >= 1
+ && #recordindex <= (self -> 'records_array') -> size);
+ // return specific record
+ if(#index==1);
+ // return first ocurrence of field name through the index map - this is faster
+ return(self -> 'records_array' -> get(#recordindex) -> get(self -> 'field_names_map' -> find(#fieldname)));
+ else;
+ // return another occurrence of the field - this is slightly slower
+ local('indexmatches'=(self -> 'field_names') -> findposition(#fieldname));
+ if(#index >= 1 && #index <= #indexmatches -> size);
+ return(self -> 'records_array' -> get(#recordindex) -> get(#indexmatches -> get(#index)));
+ /if;
+ /if;
+ /if;
+ /define_tag;
+
+ define_tag('next', -description='Increments the record pointer, returns true if there are more records to show, false otherwise.\n\
+ Useful as an alternative to a regular records loop:\n\
+ \t$database -> select;\n\
+ \twhile: $database -> next;\n\
+ \t\t$database -> field(\'name\');\' \';\n\
+ \t/while;');
+ if((self -> 'current_record') < (self -> 'shown_count'));
+ (self -> 'current_record') += 1;
+ return(true);
+ else;
+ // reset record pointer
+ (self -> 'current_record') = 0;
+ return(false);
+ /if;
+ /define_tag;
+
+ define_tag('nextrecord', -description='Deprecated synonym for ->next');
+ (self -> 'debug_trace') -> insert('*** DEPRECATION WARNING *** ' + tag_name + ' is deprecated, use ->next instead ');
+ return(self -> next);
+ /define_tag;
+
+ define_tag: 'trace',
+ -optional='html',
+ -optional='xhtml';
+
+ local: 'endslash' = ((self -> (xhtml: params)) ? ' /' | '');
+
+ local: 'eol'=(local_defined: 'html') || #endslash -> size ? ' \n' | '\n';
+
+ return: #eol + 'Debug trace for database $' + (self -> varname) + ' (' (self -> 'database') '.' (self -> 'table') + ')' + #eol
+ + (self -> 'debug_trace') -> (join: #eol) + #eol;
+
+ /define_tag;
+
+
+ // =========== Internal member tags ===============
+
+ define_tag: 'reset', -description='Internal, reset all search result vars';
+ // reset all search result vars
+ // searchresultvars
+ (self -> 'action_statement') = null;
+ (self -> 'found_count') = null;
+ (self -> 'shown_first') = null;
+ (self -> 'shown_last') = null;
+ (self -> 'shown_count') = null;
+ (self -> 'field_names') = null;
+ (self -> 'records_array') = null;
+ (self -> 'maxrecords_value') = null;
+ (self -> 'skiprecords_value') = null;
+
+ (self -> 'inlinename')=string;
+ (self -> 'keyvalue')=null;
+ (self -> 'lockvalue')=null;
+ (self -> 'lockvalue_encrypted')=null;
+ (self -> 'timestampfield')=string;
+ (self -> 'timestampvalue')=string;
+ (self -> 'searchparams')=string;
+ (self -> 'querytime')=integer;
+ (self -> 'recorddata')=map;
+ (self -> 'message')=string;
+ (self -> 'current_record')=0;
+ (self -> 'field_names_map')=map;
+
+ (self -> 'error_code')=0;
+ (self -> 'error_msg')=string;
+ /define_tag;
+
+ define_tag: 'capturesearchvars', -description='Internal';
+ // internal member tag
+
+ // capture various result variables like found_count, shown_first, shown_last, shown_count
+ // searchresultvars
+ (self -> 'action_statement') = action_statement;
+ (self -> 'found_count') = found_count;
+ (self -> 'shown_first') = shown_first;
+ (self -> 'shown_last') = shown_last;
+ (self -> 'shown_count') = shown_count;
+ (self -> 'field_names') = field_names;
+ (self -> 'records_array') = records_array;
+
+ !((self -> 'maxrecords_value') > 0) ? (self -> 'maxrecords_value') = maxrecords_value;
+ !((self -> 'skiprecords_value') > 0) ? (self -> 'skiprecords_value') = skiprecords_value;
+
+ lasso_tagexists('resultset_count') ? (self -> 'resultset_count_map') -> insert((self -> 'inlinename')=resultset_count);
+ iterate(field_names, local('field_name'));
+ (self -> 'field_names_map') !>> #field_name
+ ? (self -> 'field_names_map') -> insert(#field_name=loop_count);
+ /iterate;
+
+ (self -> 'error_code') = error_code;
+ error_code && error_msg -> size ? (self -> 'error_msg') = error_msg;
+
+
+ // handle queries that use LIMIT
+ if: !(self -> 'isfilemaker') && (string_findregexp: action_statement, -find= '\\sLIMIT\\s', -ignorecase) -> size;
+ (self -> 'debug_trace') -> (insert: tag_name + ': old found_count, shown_first and shown_last ' + (self -> 'found_count') + ' '+ (self -> 'shown_first') + ' '+ (self -> 'shown_last'));
+ (self -> 'found_count') = knop_foundrows;
+ // adjust shown_first and shown_last
+ (self -> 'shown_first') = ((self -> 'found_count') ? (self -> 'skiprecords_value') + 1 | 0);
+ (self -> 'shown_last') = integer(math_min(((self -> 'skiprecords_value') + (self -> 'maxrecords_value')), (self -> 'found_count')));
+ (self -> 'debug_trace') -> (insert: tag_name + ': new found_count, shown_first and shown_last ' + (self -> 'found_count') + ' '+ (self -> 'shown_first') + ' '+ (self -> 'shown_last'));
+ /if;
+
+ // capture some variables for single record results
+ if: found_count <= 1 // -update gives found_count 0 but still has one record result
+ && error_code == 0;
+ if((self -> 'keyfield') != '' && string(field(self -> 'keyfield')) -> size);
+ (self -> 'keyvalue')=field(self -> 'keyfield');
+ else: (self -> 'keyfield') != '' && (self -> 'keyvalue') == '' && !(self -> 'isfilemaker');
+ (self -> 'keyvalue')=keyfield_value;
+ /if;
+ if: lasso_currentaction == 'add' || lasso_currentaction == 'update';
+ (self -> 'affectedrecord_keyvalue') = (self -> 'keyvalue');
+ /if;
+ if: (self -> 'lockfield') != '';
+ (self -> 'lockvalue')=(field: (self -> 'lockfield'));
+ (self -> 'lockvalue_encrypted')=(encrypt_blowfish: (field: (self -> 'lockfield')), -seed=(self -> 'lock_seed'));
+ /if;
+ /if;
+ if: error_code == 0;
+ // populate recorddata with field values from the first found record
+ iterate: field_names, local: 'field_name';
+ (self -> 'recorddata') !>> #field_name
+ ? (self -> 'recorddata') -> (insert: #field_name = (field: #field_name) );
+ /iterate;
+ else;
+ (self -> 'debug_trace') -> (insert: tag_name + ': ' + error_msg);
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': found_count ' + (self -> 'found_count') + ' ' + (self -> 'keyfield') + ' '+ (field: (self -> 'keyfield')) + ' keyfield_value ' + keyfield_value + ' keyvalue ' + (self -> 'keyvalue') + ' fieldcount ' + (field_name: -count));
+
+ /define_tag;
+
+/define_type;
+
+
+define_type('databaserows',
+ -namespace='knop_');
+ local('version'='2009-01-08',
+ 'description'='Custom type to return all record rows from knop_database. Used as output for knop_database->records. ');
+/*
+
+CHANGE NOTES
+2009-01-08 JS ->_unknowntag: Added -index parameter
+2008-11-24 JS Created the type
+
+
+*/
+
+ local('records_array'=array,
+ 'field_names'=array,
+ 'field_names_map'=map,
+ 'current_record'=integer);
+
+ define_tag('oncreate', -description='Create a record rows object. \n\
+ Parameters:\n\
+ -records_array (array) Array of arrays with field values for all fields for each record of all found records
+ -field_names (array) Array with all the field names',
+ -required='records_array',
+ -required='field_names');
+ self -> 'records_array'=#records_array;
+ self -> 'field_names'=#field_names;
+ // store indexes to first occurrence of each field name for faster access
+ iterate(#field_names, local('field_name'));
+ (self -> 'field_names_map') !>> #field_name
+ ? (self -> 'field_names_map') -> insert(#field_name=loop_count);
+ /iterate;
+ /define_tag;
+
+ define_tag('_unknowntag', -description='Shortcut to field',
+ -optional='index');
+ !local_defined('index') ? local('index'=1);
+ if(self -> 'field_names' >> tag_name);
+ return(self -> field(tag_name(-index=#index)));
+ else;
+ //fail: -9948, self -> type + '->' + tag_name + ' not known.';
+ /if;
+ /define_tag;
+
+ define_tag('onconvert', -description='Output the current record as a plain array of field values');
+ !local_defined('recordindex') ? local('recordindex'=(self -> 'current_record'));
+ #recordindex < 1 ? #recordindex = 1;
+ if(#recordindex >= 1
+ && #recordindex <= (self -> 'records_array' -> size));
+ return(self -> 'records_array' -> get(#recordindex));
+ /if;
+ /define_tag;
+
+ define_tag('size');
+ return(self -> 'records_array' -> size);
+ /define_tag;
+
+ define_tag('get', -required='index');
+ return(knop_databaserow(-record_array=(self -> 'records_array' -> get(#index)), -field_names=(self -> 'field_names')));
+ /define_tag;
+
+ define_tag('field', -description='Return an individual field value',
+ -required='fieldname',
+ -optional='recordindex',
+ -optional='index');
+ !local_defined('recordindex') ? local('recordindex'=(self -> 'current_record'));
+ #recordindex < 1 ? #recordindex = 1;
+ !local_defined('index') ? local('index'=1);
+ if(self -> 'field_names_map' >> #fieldname
+ && #recordindex >= 1
+ && #recordindex <= (self -> 'records_array') -> size);
+ // return specific record
+ if(#index==1);
+ // return first ocurrence of field name through the index map - this is faster
+ return(self -> 'records_array' -> get(#recordindex) -> get(self -> 'field_names_map' -> find(#fieldname)));
+ else;
+ // return another occurrence of the field - this is slightly slower
+ local('indexmatches'=(self -> 'field_names') -> findposition(#fieldname));
+ if(#index >= 1 && #index <= #indexmatches -> size);
+ return(self -> 'records_array' -> get(#recordindex) -> get(#indexmatches -> get(#index)));
+ /if;
+ /if;
+ /if;
+ /define_tag;
+
+ define_tag('summary_header', -description='Returns true if the specified field name has changed since the previous record, or if we are at the first record',
+ -required='fieldname');
+ local('recordindex'=(self -> 'current_record'));
+ #recordindex < 1 ? #recordindex = 1;
+ if(#recordindex == 1 // first record
+ || self -> field(#fieldname) != self -> field(#fieldname, -recordindex=(#recordindex - 1)) ); // different than previous record (look behind)
+ return(true);
+ else;
+ return(false);
+ /if;
+ /define_tag;
+
+ define_tag('summary_footer', -description='Returns true if the specified field name will change in the following record, or if we are at the last record',
+ -required='fieldname');
+ local('recordindex'=(self -> 'current_record'));
+ #recordindex < 1 ? #recordindex = 1;
+ if(#recordindex == (self -> 'records_array') -> size // last record
+ || self -> field(#fieldname) != self -> field(#fieldname, -recordindex=(#recordindex + 1)) ); // different than next record (look ahead)
+ return(true);
+ else;
+ return(false);
+ /if;
+ /define_tag;
+
+
+ define_tag('next', -description='Increments the record pointer, returns true if there are more records to show, false otherwise.');
+ if((self -> 'current_record') < (self -> 'records_array') -> size);
+ (self -> 'current_record') += 1;
+ return(true);
+ else;
+ // reset record pointer
+ (self -> 'current_record') = 0;
+ return(false);
+ /if;
+ /define_tag;
+/define_type;
+
+
+
+define_type('databaserow',
+ -namespace='knop_',
+ //-prototype, // prototype prevents the namespace from unloading without restart
+ );
+ local: 'version'='2009-01-08',
+ 'description'='Custom type to return individual record rows from knop_database. Used as output for knop_database->get. ';
+/*
+
+CHANGE NOTES
+2009-01-08 JS ->_unknowntag: Added -index parameter
+2008-11-24 JS ->field: Added -index parameter to be able to access any occurrence of the same field name
+2008-05-29 JS Removed -prototype since it prevents unloading the namespace. It is recommended to turn it on for best performance
+2008-05-27 JS Created the type
+
+
+*/
+ local('record_array'=array,
+ 'field_names'=array);
+
+ define_tag('oncreate', -description='Create a record row object. \n\
+ Parameters:\n\
+ -record_array (array) Array with field values for all fields for the record
+ -field_names (array) Array with all the field names, should be same size as -record_array',
+ -required='record_array',
+ -required='field_names');
+ self -> 'record_array'=#record_array;
+ self -> 'field_names'=#field_names;
+ /define_tag;
+
+ define_tag('_unknowntag', -description='Shortcut to field',
+ -optional='index');
+ !local_defined('index') ? local('index'=1);
+ if(self -> 'field_names' >> tag_name);
+ return(self -> field(tag_name, -index=#index));
+ else;
+ //fail: -9948, self -> type + '->' + tag_name + ' not known.';
+ /if;
+ /define_tag;
+
+ define_tag('onconvert', -description='Output the record as a plain array of field values');
+ return(self -> 'record_array');
+ /define_tag;
+
+
+ define_tag('field', -description='Return an individual field value',
+ -required='fieldname',
+ -optional='index');
+ !local_defined('index') ? local('index'=1);
+ if(self -> 'field_names' >> #fieldname);
+ // return any occurrence of the field
+ local('indexmatches'=(self -> 'field_names') -> findposition(#fieldname));
+ if(#index >= 1 && #index <= #indexmatches -> size);
+ return((self -> 'record_array') -> get(#indexmatches -> get(#index)));
+ /if;
+ /if;
+ /define_tag;
+
+
+/define_type;
+?>
+[
+//------------------------------------------------------------------
+// End knop_database
+//------------------------------------------------------------------
+
+//##################################################################
+
+][
+//------------------------------------------------------------------
+// Begin knop_form
+//------------------------------------------------------------------
+
+]addfield: Added -template to specify field specific template
+2010-11-22 SP ->init: Correction of -lockvalue handling after L9 syntax adjustment
+2010-07-18 SP Added support for series for -options
+2010-06-10 JS ->renderform: avoid adding -upload parameters to post forms since it conflicts with file uploads (found by Steve Piercy)
+2010-04-21 JS ->renderhtml: removed encode_html for label
+2010-03-06 SP Changed default behavior of ->updatefields with -sql to add backticks between the table and column names. Now JOINs may be used.
+2010-03-06 SP Added ->updatefields with -removedotbackticks for backward compatibility for fields that contain periods. If you use periods in a fieldname then you cannot use a JOIN in Knop.
+2009-11-11 JS Added class and id to optiongroup div that surrounds for checkbox and radio
+2009-11-11 JS Corrected id for checkbox and radio option labels
+2009-10-02 JS Added id for labels, auto generated from the field's id with _label appended
+2009-09-16 JS Syntax adjustments for Lasso 9
+2009-09-04 JS Changed $__html_reply__ to content_body
+2009-09-04 JS ->renderhtml: corrected typ for autoparams
+2009-07-23 JS ->renderform: removed encode_html that somehow has reappeared for label.
+2009-07-10 SP added -maxlength option for text fields
+2009-06-26 JS ->oncreate: added deprecation warning for -action
+2009-06-22 JS ->addfield: corrected -options check to look for set instead of series (besides array)
+2009-04-16 JS ->loadfileds can now load field values from -params also inside an inline
+2009-03-20 JS Added around injected scripts for better xhtml compliance
+2009-01-08 JS ->getvalue and _unknowntag: added -index parameter to be able to get value for a specific field instance when there are multiple fields with the same name
+2009-01-08 JS ->loadfields: implemented support for multiple fields with the same name when loading field values from form submission where the number of same name fields matches
+2009-01-07 JS ->setvalue: added -index parameter to be able to set value for a specific field instance when there are multiple fields with the same name
+2008-12-08 JS ->renderform: Removed the onclick handlers for checkbox and radio since Safari now supports clicking the label text as click for the checkbox/radio control.
+2008-12-05 JS ->renderform: the fieldset and legend field types will now use id and class on the fieldset tag if specified
+2008-12-03 JS ->renderform: fields of type fieldset now uses value as legend (just as field type legend already did) instead of always using an empty legend
+2008-09-24 JS ->updatefields: Added protection against backtick sql injection in MySQL object names
+2008-09-17 JS ->renderform and ->renderhtml: -from and -to allows negative numbers to count from end of form instead
+2008-09-13 JS Added ->getlabel to return the display name for a field.
+2008-09-13 JS ->addfield and ->validate: Implemented -validate to specify a compound expression to validate the field input.
+2008-09-13 JS ->addfield and ->loadfields: Implemented -filter to specify a compound expression to filter the field input.
+2008-09-11 JS ->updatefields: fixed exclusion of special field types html, legend and fieldset.
+2008-09-11 JS ->renderform: Fixed missing value for password fields
+2008-07-02 JS ->renderform: Cleaned up the automatic adding of javascript code so it's not added if not needed. Also moved all scripts to the end of the page. More work with with the javascripts is needed.
+2008-06-03 JS ->renderform: corrected missing closing
+2008-05-15 JS ->renderform and ->renderhtml: adjusted the behavior for nested fieldsets
+2008-05-13 JS Implemented -legend for ->renderhtml, to make it consistent with the new legend field type
+2008-05-13 JS Implemented special field types html, fieldset and legend. Use -value to display data for these fields. A legend field also creates a fieldset (closes any previously open fieldsets). Use fieldset with -value=false to close a fieldset without opening a new one.
+2008-05-06 JS Added unknowntag as shortcut to getvalue
+2008-01-30 JS Removed duplicate endscript entries for if(dirty) {makedirty()};
+2007-12-13 JS Corrected ->addfield: -dbfield so empty dbfields are properly ignored by ->updatefields.
+2007-12-11 JS Moved error_msg to knop_base (special version of error_code stays here)
+2007-12-11 JS Added documentation as -description to most member tags, to be used by the new ->help tag
+2007-12-11 JS Moved ->help to knop_base
+2007-11-13 JS Added -buttontemplate to be able to specify separate template for buttons, defaults to no , but if template has been specified that will be used instead (for backwards compatibility)
+2007-11-12 JS ->process delete now works also when not using record locking (not specifying -user)
+2007-11-01 JS ->renderform: added support for -hint for textarea fields.
+2007-09-27 JS ->renderhtml: multiple values (array) for radio, checkbox and select are now rendered properly with either "," or depending on the presence of -linebreak, and with the display text instead of the actual option value
+2007-09-27 JS ->renderform: improved handling of multiple values for checkbox, radio and select
+2007-09-21 JS ->addfield: flag parameters now accept false as value
+2007-09-06 JS ->oncreate: changed name of -action to -formaction to make it more clear what it is. -action is still supported but deprecated.
+2007-09-06 JS ->renderform: Corrected the exception for -session... (duh)
+2007-08-08 JS ->renderform: Added exception for -session
+2007-06-18 JS Added tag timer to most member tags
+2007-06-13 JS added inheritance from knop_base
+2007-06-12 JC bugfixed -xhtml form rendering when called by quicksearch
+2007-06-11 JC added handling of xhtml output
+2007-04-19 JS ->loadfields: fixed -params that was broken when adding -database
+2007-04-19 JS ->renderform: removed invalid wrap="soft" from textarea
+2007-04-12 JS ->process: made -user optional (only needed when using record locking)
+2007-04-12 JS ->loadfields can now take a -database parameter, either as a flag (no value) where the database object connected to the form will be used, or by specifying a database object as value.
+2007-04-03 JS Changed namespace from mt_ to knop_
+2007-03-01 JS ->renderform fixed unsavedwarning on page load by moving checkdirty() to afterscript
+2007-03-01 JS ->formmode and ->init changed so it preserves the right mode after a failed add
+2007-02-27 JS ->renderform: added
around checkboxes and radios for css formating
+2007-02-26 JS ->oncreate: added -actionpath to specify the framework action path for the form instead of manually adding the -action hidden field
+2007-02-24 JS Corrected entersubmitblock behavior by adding onfocus handler on form and starting with submitBlock=false
+2007-02-23 JS Removed encode_html from form field labels
+2007-02-22 JS ->setformat: Added -legend
+2007-02-07 JS Added ->copyfield to copy a form field to a new name, with the same properties.
+2007-02-07 JS ->errors now returns empty array if validate has not been called, instead of performing validation
+2007-02-05 JS ->getbutton can now look for also button names that are not one of the built-in ones (for example button_apply)
+2007-02-05 JS The -keyvalue parameter can be given another name by specifying -keyparamname in oncreate
+2007-02-02 JS Added ->lockvalue_decrypted
+2007-02-02 JS ->addfield: -value is now stored as reference
+2007-02-02 JS error_code now returns an error for when the form contains validation errors
+2007-02-02 JS Improved reporting of Lasso error messaged in error_msg
+2007-02-02 JS Added real error codes
+2007-01-31 JS ->rederform action_params now also exclude "-" params that appear in the form action
+2007-01-29 JS ->renderform: The first field with input error will get focus when loading page
+2007-01-29 JS Added -focus to ->addfield to give default field focus when loading page with form
+2007-01-29 JS Added -disabled to ->addfield, and handling of it in ->renderform
+2007-01-29 JS Added -noautoparams to ->oncreate to disable the automatic passing of action_params that begin with "-"
+2007-01-29 JS ->renderform now renders label also for submit, reset to format properly with css
+2007-01-26 JS Added support for Safari specific
+2007-01-26 JS ->renderform action_params that begin with "-" now exclude params that exist in the form. Minor corrections to the behavior.
+2007-01-25 JS Added -nowarning to ->oncreate to disable unsaved warnings for the entire form
+2007-01-25 JS Added -required to ->oncreate (and a few more from ->setformat)
+2007-01-23 JS Autogenerates id for the form itself
+2007-01-23 JS Added ->getbutton to return the button that was clicked when submitting a form (cancel, add, save, delete)
+2007-01-23 JS Added auto conversion of options left hand pair member to string, to make comparsions work reliably. Integer zeros don't compare nicely to strings.
+2007-01-23 JS Added support for submit-on-enter prevention: specify -entersubmitblock at oncreate
+2007-01-19 JS Addes renderform: -legend to be able to group form fields at render time
+2007-01-19 JS added support for -optgroup in -options for select. Also works for radio and checkbox. Specify empty -optgroup to close optgroup in select without starting a new, or to add extra linebreak between checkboxes/radio buttons.
+2007-01-19 JS added -template for oncreate
+2007-01-19 JS added optional fieldset and legend to form, legend can be specified as -legend at oncreate. if -legend is specified, the form will be wrapped in a fieldset.
+2007-01-19 JS method now defaults to post
+2007-01-19 JS Corrected line separator for FileMaker checkboxes and added the same handling also for radio
+2007-01-18 JS renderform: any action_params that begin with "-" (except -keyvalue and -lockvalue) are added as form parameters
+2007-01-18 JS renderform: checkboxes and multiselects now show checked and selected properly when loading values from database
+2007-01-18 JS updatefields: added support for multiple values for one fieldname, like checkboxes (multiple fields in the update pair array, -sql generates comma separated values)
+2007-01-17 JS reset button now makes form undirty
+2007-01-17 JS addfield: -confirmmessage can now be specified for any submit or reset button
+2007-01-17 JS added addfield: -nowarning to avoid unsaved warning when the field is changed
+2007-01-17 JS changed default class name for unsaved marker from dirty to unsaved
+2007-01-17 JS changed name of -dirtymarker and -dirtymarkerclass to unsavedmarker and -unsavedmarkerclass for userfriendlyness
+2007-01-17 JS added setformat: -unsavedwarning to dynamically set the javascript form dirty warning message
+2007-01-17 JS renderform: -field changed to renderform: -name for consistency
+2007-01-16 JS renderform: -field with wrong field name does not output anything, instead of the entire form
+2007-01-16 JS fixed onbeforeunload in javascript form dirty handler
+
+TODO:
+->addfield: Add -format to manipulate the field value before it is displayed by ->renderform and ->renderhtml, much like -filter but only for display and without affecting input.
+->addfield: Add -fieldgroup to be able to group related fields together, useful for ->updatefields to return just fields that belong to a specific db table, or ->renderform as another way to render a form selectively
+->renderform needs a better way to display errors inline together with the fields
+Make _unknowntag also work as shortcut to setvalue if a value is specified
+Add a new special field type to the form object, let's say "data". That field type will not interact with forms and will never be touched by loadfields, but it will populate ->updatefields.
+Add -> searchfields, which will return a fulltext enabled pair array better suited for searchs than ->updatefields is. -fulltext needs to be specified per field.
+Review and clean up the javascripts inserted automatically by knop_form - partially done
+Option to let textarea grow automatically depending on the amount of text in it.
+Use http://bassistance.de/jquery-plugins/jquery-plugin-validation/ instead of client side validation
+Possibly add support for the same validation expressions as the jquery validation plugin uses, so server side a nd client side validation can be specified at once.
+Add -path as parameter for oncreate so the form action can be set with less confusion... In that case -formaction will be a physical url, while -path would be a framework path.
+Fix actionpath reference so it updates properly when altering the value (not possible?)
+Should loadfields load "-" params?
+Unsavedwarning made optional, does not seem to work properly now?
+More flexible error hightlighting
+Move templates to a member tag to be make it easier to subclass (Douglas Burchard)
+Add "button". . Subtypes are submit, reset and button. How to specify the subtype? (Douglas Burchard)
+Change ->addfield to ->insert and make ->addfield deprecated
+There is no src for input type image!
+Add ->size and ->get so the form object can be iterated
+Add -skipemtpy to to ->renderhtml
+Option for -> renderhtml to output without html encoding
+->renderhtml should never html encode fields of type html
+
+*/
+
+
+ // instance variables
+ local: 'fields'=array,
+ 'template'=string, // html template used to render the html form fields
+ 'buttontemplate'=string, // html template used to render the html buttons (submit, reset, image)
+ 'class'=string, // default class for all form fields, can be overridden field by field
+ 'errorclass'=string, // class used to highlight field labels when validation fails
+ 'formaction'=null,
+ 'method'='post',
+ 'fieldset'=false, // html form fieldset
+ 'legend'=null, // html form legend
+ 'name'=null,
+ 'id'=null,
+ 'raw'=null,
+ 'enctype'=null, // is automatically set to multipart/formdata if the form contains a file input
+ 'actionpath'=null,
+ 'noautoparams'=false, // if true then no parameters that begin with - will be automatically added to the form
+ 'fieldsource'=null, // the source of the latest -> loadfields, can be database, form or params
+ 'required'=string, // marker used to show fields that are required (html or plain string)
+ 'entersubmitblock'=false, // if true, a javascript will prevent form submit without clicking on submit button (like pressing enter key)
+ 'unsavedmarker'=null,
+ 'unsavedmarkerclass'=null,
+ 'unsavedwarning'=string, // must be specified, or else there is no unsaved warning for the form
+ 'database'=null,
+ 'keyparamname'=string, // param name to use instead of the default -keyvalue
+ 'formmode'=null, // whether the form is for editing an existing record or a blank for for adding a new record (edit/add)
+ // only valid if a database object is specified
+ 'formbutton'=null, // the button that was clicked when submitting a form (cancel, add, save, delete)
+ 'db_keyvalue'=null,
+ 'db_lockvalue'=null,
+
+ 'render_fieldset_open'=false, // used when rendering to keep track of if a fieldset from fieldset or legend field types is open so it can be closed properly
+ 'render_fieldset2_open'=false, // used when rendering to keep track of if a fieldset from renderform or renderhtml legend is open so it can be closed properly
+ 'noscript'=false, // when set to true, no scripts will be injected by renderform
+ 'error_lang'=(knop_lang: -default='en', -fallback);
+
+ local: 'errors'=null;
+
+
+ // config vars
+ local: 'validfieldtypes' = (map: 'text', 'password', 'checkbox', 'radio', 'textarea', 'select', 'file', 'search',
+ 'submit', 'reset', 'image', 'hidden',
+ 'fieldset', 'legend', 'html'), // special types
+ 'exceptionfieldtypes' = (map: 'file', 'submit', 'reset', 'image', 'addbutton', 'savebutton', 'deletebutton', 'cancelbutton',
+ 'fieldset', 'legend', 'html'); // special types
+ local: 'validfieldtypes_array'=array;
+ iterate: #validfieldtypes, (local: 'temp');
+ #validfieldtypes_array -> (insert: #temp -> name);
+ /iterate;
+ local: 'exceptionfieldtypes_array'=array;
+ iterate: #exceptionfieldtypes, (local: 'temp');
+ #exceptionfieldtypes_array -> (insert: #temp -> name);
+ /iterate;
+
+ // page var to keep track of the number of forms that have been rendered on a page
+ if: !(var_defined: 'knop_form_renderform_counter');
+ var: 'knop_form_renderform_counter'=0;
+ /if;
+
+
+ define_tag: 'oncreate', -description='Parameters:\n\
+ -formaction (optional) The action atribute in the form html tag\n\
+ -action (optional) Deprecated synonym to -formaction\n\
+ -method (optional) Defaults to post\n\
+ -name (optional)\n\
+ -id (optional)\n\
+ -raw (optional) Anything in this parameter will be put in the opening form tag\n\
+ -actionpath (optional) Knop action path\n\
+ -fieldset (optional)\n\
+ -legend (optional string) legend for the entire form - if specified, a fieldset will also be wrapped around the form\n\
+ -entersubmitblock (optional)\n\
+ -noautoparams (optional)\n\
+ -template (optional string) html template, defaults to #label# #field##required# \n\
+ -buttontemplate (optional string) html template for buttons, defaults to #field# but uses -template if specified\n\
+ -required (optional string) character(s) to display for required fields (used for #required#), defaults to *\n\
+ -class (optional string) css class name that will be used for the form element, default none\n\
+ -errorclass (optional string) css class name that will be used for the label to highlight input errors, if not defined style="color: red" will be used\n\
+ -unsavedmarker (optional string) id for html element that should be used to indicate when the form becomes dirty. \n\
+ -unsavedmarkerclass (optional string) class name to use for the html element. Defaults to "unsaved". \n\
+ -unsavedwarning (optional string)\n\
+ -keyparamname (optional)\n\
+ -noscript (optional flag) if specified, don\'t inject any javascript in the form. This will disable all client side functionality such as hints, focus and unsaved warnings. \n\
+ -database (optional database) Optional database object that the form object will interact with',
+ // parameters for form html tag attributes
+ -optional='formaction',
+ -optional='action',
+ -optional='method',
+ -optional='name',
+ -optional='id',
+ -optional='raw',
+
+ // knop parameters
+ -optional='actionpath',
+ -optional='fieldset',
+ -optional='legend',
+ -optional='entersubmitblock',
+ -optional='noautoparams',
+ -optional='template', -type='string',
+ -optional='buttontemplate', -type='string',
+ -optional='required', -type='string',
+ -optional='class', -type='string',
+ -optional='errorclass', -type='string',
+ -optional='unsavedmarker', -type='string',
+ -optional='unsavedmarkerclass', -type='string',
+ -optional='unsavedwarning', -type='string',
+ -optional='keyparamname',
+ -optional='noscript',
+ -optional='database', -type='database';
+ local: 'timer'=knop_timer;
+
+
+ local_defined('method') ? (self -> 'method') = #method;
+ local_defined('name') ? (self -> 'name') = #name;
+ local_defined('id') ? (self -> 'id') = #id;
+ local_defined('raw') ? (self -> 'raw') = #raw;
+ local_defined('legend') ? (self -> 'legend') = #legend;
+ local_defined('template') ? (self -> 'template') = #template;
+ local_defined('buttontemplate') ? (self -> 'buttontemplate') = #buttontemplate;
+ local_defined('required') ? (self -> 'required') = #required;
+ local_defined('class') ? (self -> 'class') = #class;
+ local_defined('errorclass') ? (self -> 'errorclass') = #errorclass;
+ local_defined('unsavedmarker') ? (self -> 'unsavedmarker') = #unsavedmarker;
+ local_defined('unsavedmarkerclass') ? (self -> 'unsavedmarkerclass') = #unsavedmarkerclass;
+ local_defined('unsavedwarning') ? (self -> 'unsavedwarning') = #unsavedwarning;
+ local_defined('keyparamname') ? (self -> 'keyparamname') = #keyparamname;
+
+ // the following params are stored as reference, so the values of the params can be altered after adding a field simply by changing the referenced variable.
+ local_defined('formaction') ? (self -> 'formaction') = @#formaction;
+ local_defined('actionpath') ? (self -> 'actionpath') = @#actionpath;
+ local_defined('database') ? (self -> 'database') = @#database;
+
+ if: !(local_defined: 'formaction') && (local_defined: 'action');
+ // keep support for old -action insead of -formaction
+ (self -> 'debug_trace') -> insert('*** DEPRECATION WARNING *** ' + tag_name + ' -action parameter is deprecated, use -formaction instead ');
+ (self -> 'formaction') = @#action;
+ /if;
+
+ (self -> 'noscript') = (local_defined('noscript') && #noscript != false);
+
+ // default value
+ !(local_defined: 'required') ? (self -> 'required' = '*');
+ !(local_defined: 'keyparamname') ? (self -> 'keyparamname' = '-keyvalue');
+
+ (self -> 'fieldset') = ((local_defined: 'fieldset') && #fieldset != false) || (self -> 'legend') != '';
+ (self -> 'entersubmitblock') = (local_defined: 'entersubmitblock');
+ (self -> 'noautoparams') = (local_defined: 'noautoparams');
+
+
+ if: (self -> 'unsavedmarker') != '' && (self -> 'unsavedmarkerclass') == '';
+ // set default unsavedmarkerclass
+ (self -> 'unsavedmarkerclass')='unsaved';
+ /if;
+
+ if: (self -> 'unsavedwarning') == '';
+ // set default dirtywarning message
+ //(self -> 'unsavedwarning')='Det finns ändringar som inte har sparats - vill du fortsätta utan att spara?';
+ /if;
+
+ // escape quotes for javascript
+ (self -> 'unsavedwarning') -> (replace: '\'', '\\\'');
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ /*
+ define_tag: 'onassign', -description='Internal, needed to restore references when ctype is defined as prototype',
+ -required='value';
+ // recreate references here
+
+ iterate: (array:
+ 'formaction',
+ 'actionpath',
+ 'database'), (local: 'param');
+ (self -> #param) = @(#value -> #param);
+ /iterate;
+
+ /define_tag;
+ */
+
+ define_tag: 'onconvert', -description='Outputs the form data in very basic form, just to see what it contains',
+ -optional='xhtml';
+ local: 'timer'=knop_timer;
+
+ local: 'endslash' = ((self -> (xhtml: params)) ? ' /' | '');
+
+ local: 'output'=string;
+ iterate: (self -> 'fields'), (local: 'fieldpair');
+ #output += #fieldpair -> name + ' = ' + #fieldpair -> value + ' \n';
+ /iterate;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ return: #output;
+ /define_tag;
+
+ define_tag: '_unknowntag', -description='Shortcut to getvalue',
+ -optional='index', -type='integer', -copy;
+ !local_defined('index') ? local('index') = 1;
+ if: (self -> 'fields') >> tag_name; // should be (self -> keys) but this is faster
+ return: (self -> (getvalue: tag_name, -index=#index));
+ else;
+ //fail: -9948, self -> type + '->' + tag_name + ' not known.';
+ (self -> '_debug_trace') -> insert(self -> type + '->' + tag_name + ' not known.');
+ /if;
+ /define_tag;
+
+ define_tag: 'addfield', -description='Inserts a form element in the form. \n\
+ Parameters:\n\
+ -type (required) Supported types are listed in form -> \'validfieldtypes_array\'. Also custom field types addbuton, savebutton or deletebutton are supported (translated to submit buttons with predefined names). \
+ For the field types html, fieldset and legend use -value to specify the data to display for these fields. A legend field automatically creates a fieldset (closes any previously open fieldsets). Use fieldset with -value=false to close a fieldset without opening a new one. \n\
+ -name (optional) Required for all input types except addbuton, savebutton, deletebutton, fieldset, legend and html\n\
+ -id (optional) id for the html object, will be autogenerated if not specified\n\
+ -dbfield (optional) Corresponding database field name (name is used if dbfield is not specified), or null/emtpy string if ignore this field for database\n\
+ -value (optional) Initial value for the field\n\
+ -hint (optional) Optional gray hint text to show in empty text field\n\
+ -options (optional) For select, checkbox and radio, must be array, set or series. For select, the array can contain -optgroup=label to create an optiongroup. \n\
+ -multiple (optional flag) Used for select\n\
+ -linebreak (optional flag) Put linebreaks between checkbox and radio values\n\
+ -default (optional) Default text to display in a popup menu, will be selected (with empty value) if no current value is set. Is followed by an empty option. \n\
+ -label (optional) Text label for the field\n\
+ -size (optional) Used for text and select\n\
+ -maxlength (optional) Used for text\n\
+ -rows (optional) Used for textarea\n\
+ -cols (optional) Used for textarea\n\
+ -focus (optional flag) The first text field with this parameter specified will get focus when page loads\n\
+ -class (optional)\n\
+ -disabled (optional flag) The form field will be rendered as disabled\n\
+ -raw (optional) Raw attributes that will be put in the html tag\n\
+ -confirmmessage (optional) Message to show in submit/reset confirm dialog (delete button always shows confirm dialog)\n\
+ -required (optional flag) If specified then the field must not be empty (very basic validation)\n\
+ -validate (optional) Compound expression to validate the field input. The input can be accessed as params inside the expression which should either return true for valid input or false for invalid, or return 0 for valid input or a non-zero error code or error message string for invalid input. \n\
+ -filter (optional) Compound expression to filter the input before it is loaded into the form by ->loadfields. The field value can be accessed as params inside the expression which should return the filtered field value. -filter is applied before validation. \n\
+ -nowarning (optional flag) If specified then changing the field will not trigger an unsaved warning\n\
+ -after (optional) Numeric index or name of field to insert after\n\
+ -template (optional) Format string that will override global template or buttontemplate',
+ -required='type',
+ -optional='name',
+ -optional='id',
+ -optional='dbfield',
+ -optional='value',
+ -optional='hint',
+ -optional='options',
+ -optional='multiple',
+ -optional='linebreak',
+ -optional='default',
+ -optional='label',
+ -optional='size',
+ -optional='maxlength',
+ -optional='rows',
+ -optional='cols',
+ -optional='focus',
+ -optional='class',
+ -optional='disabled',
+ -optional='raw',
+ -optional='confirmmessage',
+ -optional='required',
+ -optional='validate', -type='tag',
+ -optional='filter', -type='tag',
+ -optional='nowarning',
+ -optional='after',
+ -optional='template';
+ // TODO: add optiontemplate to be able to format individual options
+ local: 'timer'=knop_timer;
+
+ local: '_type'=(local: 'type'), '_name'=(local: 'name'), 'originaltype'=(local: 'type');
+ if: (map: 'addbutton', 'savebutton', 'deletebutton', 'cancelbutton') >> #_type;
+ #originaltype = #_type;
+ #_name = 'button_' + #_type;
+ #_name -> (removetrailing: 'button');
+ #_type = 'submit';
+ else: #_type == 'reset' && (local: 'name') == '';
+ #_name = 'button_' + #_type;
+ else: (map: 'legend', 'fieldset', 'html') >> #_type && (local: 'name') == '';
+ #_name = #_type;
+ else;
+ fail_if: (local: 'name') == '', -9956, 'form->addfield missing required parameter -name';
+ /if;
+
+
+ fail_if: !((self -> 'validfieldtypes') >> #_type), 7102, self -> error_msg(7202);
+ fail_if: (map: 'select', 'radio', 'checkbox') >> #_type
+ && (local: 'options') -> type != 'array'
+ && (local: 'options') -> type != 'set'
+ && (local: 'options') -> type != 'series',
+ -9956, 'Field type ' #_type ' requires -options array, set or series';
+ local: 'index'= (self -> 'fields') -> size + 1;
+ (local_defined: 'after') ? (#after -> type == 'string' && (self -> 'fields') >> #after
+ ? #index = (integer: ((self -> 'fields') -> (findindex: #after) -> first)) + 1
+ | #after -> type == 'integer' ? #index= #after + 1);
+ if: #_type == 'file';
+ (self -> 'enctype') ='multipart/form-data';
+ (self -> 'method') = 'post';
+ /if;
+ local: 'field'=(map:
+ 'required'=(local_defined: 'required') && #required != false,
+ 'multiple'=(local_defined: 'multiple') && #multiple != false,
+ 'linebreak'=(local_defined: 'linebreak') && #linebreak != false,
+ 'focus'=(local_defined: 'focus') && #focus != false,
+ 'nowarning'=(local_defined: 'nowarning') && #nowarning != false,
+ 'disabled'=(local_defined: 'disabled') && #disabled != false
+ );
+ if: (self -> 'exceptionfieldtypes') >> #_type;
+ // || (map: 'legend', 'fieldset', 'html') >> #_type;
+ // never make certain field types required
+ #field -> insert('required'=false);
+ /if;
+
+ #field -> (insert: 'type'=#_type);
+ #field -> (insert: 'name'=#_name);
+
+ local_defined('id') ? #field -> insert('id' = #id);
+ local_defined('hint') ? #field -> insert('hint' = #hint);
+ local_defined('default') ? #field -> insert('default' = #default);
+ local_defined('label') ? #field -> insert('label' = #label);
+ local_defined('size') ? #field -> insert('size' = #size);
+ local_defined('maxlength') ? #field -> insert('maxlength' = #maxlength);
+ local_defined('rows') ? #field -> insert('rows' = #rows);
+ local_defined('cols') ? #field -> insert('cols' = #cols);
+ local_defined('class') ? #field -> insert('class' = #class);
+ local_defined('raw') ? #field -> insert('raw' = #raw);
+ local_defined('confirmmessage') ? #field -> insert('confirmmessage' = #confirmmessage);
+ local_defined('originaltype') ? #field -> insert('originaltype' = #originaltype);
+ (local_defined: 'template') ? #field -> (insert: 'template'=#template);
+
+ #field -> (insert: 'dbfield'=( (local_defined: 'dbfield') ? #dbfield | #_name ) );
+ (local_defined: 'value') ? #field -> (insert: 'defaultvalue'=#value);
+
+ // the following params are stored as reference, so the values of the params can be altered after adding a field simply by changing the referenced variable.
+ local_defined('options') ? #field -> insert('options' = @#options);
+ local_defined('value') ? #field -> insert('value' = @#value);
+ local_defined('validate') ? #field -> insert('validate' = @#validate);
+ local_defined('filter') ? #field -> insert('filter' = @#filter);
+
+ (self -> 'fields') -> (insert: #_name = @#field, #index);
+
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ define_tag: 'copyfield', -description='Copies a form field to a new name.',
+ -required='name',
+ -required='newname';
+ local: 'timer'=knop_timer;
+ fail_if: #name == #newname, 7104, self -> error_msg(7104);
+ if: (self -> 'fields') >> #name;
+ local: 'copyfield'=(self -> 'fields') -> (find: #name) -> first -> value;
+ #copyfield -> (insert: 'name' = #newname);
+ (self -> 'fields') -> (insert: #newname = #copyfield);
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+
+ define_tag: 'init', -description='Initiates form to grab keyvalue and set formmode if we have a database connected to the form. \
+ Does nothing if no database is specified. ',
+ -optional='get',
+ -optional='post',
+ -optional='keyvalue';
+ local: 'timer'=knop_timer;
+ // Initiates form to grab keyvalue and set formmode if we have a database connected to the form.
+ // TODO: should we run init if form is not valid? Now we have a condition in lib before running init.
+ // TODO: how can we get the right formmode when showing an add form again after failed validation? Now we have an extra condition in lib for this
+
+ if: (self -> 'database') -> type == 'database';
+ (self -> 'db_keyvalue') = null;
+ (self -> 'db_lockvalue') = null;
+ local: '_params'=array,
+ 'source'='form',
+ 'field'=map;
+ #_params = array;
+ if: (local_defined: 'post');
+ #_params -> (merge: client_postparams);
+ /if;
+ if: (local_defined: 'get');
+ #_params -> (merge: client_getparams);
+ /if;
+ if: !(local_defined: 'post') && !(local_defined: 'get');
+ #_params -> (merge: client_postparams);
+ #_params -> (merge: client_getparams);
+ /if;
+ (self -> 'debug_trace') -> (insert: 'Init ');
+
+ if: #_params >> '-lockvalue';
+ if: #_params -> type == 'map';
+ (self -> 'db_lockvalue')=((#_params -> (find: '-lockvalue' ) ) != ''
+ ? (#_params -> (find: '-lockvalue' ) ) | null);
+ else;
+ (self -> 'db_lockvalue')=((#_params -> (find: '-lockvalue' ) -> first -> value) != ''
+ ? (#_params -> (find: '-lockvalue' ) -> first -> value) | null);
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': grabbing lockvalue from form ' + (self -> 'db_lockvalue'));
+ else: (local_defined: 'keyvalue');
+ (self -> 'db_keyvalue') = #keyvalue;
+ (self -> 'debug_trace') -> (insert: tag_name + ': grabbing keyvalue from parameter ' + (self -> 'db_keyvalue'));
+ else: #_params >> (self -> 'keyparamname');
+ if: #_params -> type == 'map';
+ (self -> 'db_keyvalue')=((#_params -> (find: (self -> 'keyparamname') ) ) != ''
+ ? (#_params -> (find: (self -> 'keyparamname') ) ) | null);
+ else;
+ (self -> 'db_keyvalue')=((#_params -> (find: (self -> 'keyparamname') ) -> first -> value) != ''
+ ? (#_params -> (find: (self -> 'keyparamname') ) -> first -> value) | null);
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': grabbing keyvalue from form ' + (self -> 'db_keyvalue'));
+ /if;
+ if: (self -> 'db_lockvalue') == '' && (self -> 'db_keyvalue') == '';
+ // we have no keyvalue or lockvalue - this must be an add operation
+ (self -> 'formmode') = 'add';
+ // create a keyvalue for the record to add
+ (self -> 'db_keyvalue') = knop_unique;
+ (self -> 'debug_trace') -> (insert: tag_name + ': generating keyvalue ' + (self -> 'db_keyvalue'));
+ else: (self -> getbutton) == 'add';
+ (self -> 'formmode') = 'add';
+ else: (self -> formmode)=='';
+ (self -> 'formmode') = 'edit';
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': formmode ' + (self -> formmode));
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+
+ define_tag: 'loadfields', -description='Overwrites all field values with values from either database, action_params or explicit -params. \
+ Auto-detects based on current lasso_currentaction.\n\
+ Parameters:\n\
+ -params (optional) Array or map to take field values from instead of database or submit (using dbnames)\n\
+ -get (optional flag) Only getparams will be used\n\
+ -post (optional flag) Only postparams will be used\n\
+ -inlinename (optional) The first record in the result from the specified inline will be used as field values\n\
+ -database (optional) If a database object is specified, the first record from the latest search result of the database object will be used. \
+ If -database is specified as flag (no value) and the form object has a database object attached to it, that database object will be used.',
+ -optional='params',
+ -optional='post',
+ -optional='get',
+ -optional='inlinename',
+ -optional='database';
+ local: 'timer'=knop_timer;
+ local: '_params'=array,
+ 'source'='form',
+ 'field'=map;
+ (self -> 'fieldsource') = null;
+ if: (local_defined: 'params');
+ (self -> 'fieldsource') = 'params';
+ local: 'source'='params';
+ #_params = #params;
+ else: (local_defined: 'database') && !(local_defined: 'inlinename');
+ if: #database -> type == 'database';
+ local: 'inlinename'=#database -> inlinename;
+ else: self -> 'database' -> type == 'database';
+ local: 'inlinename'=self -> 'database' -> inlinename;
+ /if;
+ /if;
+
+ if: (local_defined: 'inlinename');
+ (self -> 'fieldsource') = 'database';
+ local: 'source'='params';
+ #_params=map;
+ records: -inlinename=#inlinename;
+ loop: (field_name: -count);
+ #_params -> (insert: (field_name: loop_count) = (field: (field_name: loop_count)) );
+ /loop;
+ loop_abort;
+ /records;
+ else: (self -> 'fieldsource') == null && lasso_currentaction != 'nothing';
+ (self -> 'fieldsource') = 'database';
+ local: 'source'='database';
+ else: (self -> 'fieldsource') == null;
+ (self -> 'fieldsource') = 'form';
+ #_params = array;
+ if: (local_defined: 'post');
+ #_params -> (merge: client_postparams);
+ /if;
+ if: (local_defined: 'get');
+ #_params -> (merge: client_getparams);
+ /if;
+ if: !(local_defined: 'post') && !(local_defined: 'get');
+ #_params -> (merge: client_postparams);
+ #_params -> (merge: client_getparams);
+ /if;
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': loading field values from ' + (self -> 'fieldsource'));
+ local('fieldnames_done'=map, 'fields_samename'=array, 'params_fieldname'=array);
+ iterate: (self -> 'fields'), (local: 'fieldpair');
+ //#field = @(#fieldpair -> value);
+ if: (self -> 'exceptionfieldtypes') !>> #fieldpair -> value -> (find: 'type') // do not load data for excluded form fields (maybe it should do that in some cases???)
+ // && (map: 'legend', 'fieldset', 'html') !>> #fieldpair -> value -> (find: 'type')
+ && !(#fieldpair -> name -> (beginswith: '-')); // exclude field names that begin with "-"
+ if(#fieldnames_done !>> #fieldpair -> name); // check if we are already done with this field name (for multiple fields with the same name)
+ // find all fields with the same name
+ #fields_samename = @((self -> 'fields') -> find(#fieldpair -> name));
+ #params_fieldname = @(#_params -> find(#fieldpair -> name));
+ if: #source == 'database' && found_count > 0;
+ // load field values from database
+ if: (#fieldpair -> value -> find: 'dbfield') != '';
+ // first remove value to break reference
+ (#fieldpair -> value) -> (remove: 'value');
+ (#fieldpair -> value) -> (insert: 'value'=(field: (#fieldpair -> value -> find: 'dbfield')) );
+ /if;
+ else: #source == 'params';
+ // load field values from explicit -params using dbfield names
+ if: #_params >> (#fieldpair -> value -> find: 'dbfield') && (#fieldpair -> value -> find: 'dbfield') != '';
+ // first remove value to break reference
+ (#fieldpair -> value) -> (remove: 'value');
+ if(#_params -> isa('map'));
+ (#fieldpair -> value) -> (insert: 'value'=(#_params -> (find: (#fieldpair -> value -> find: 'dbfield') ) ) );
+ /*else: #_params -> (find: (#fieldpair -> value -> find: 'dbfield') ) -> size > 1;
+ // multiple field values
+ local: 'valuearray'=array;
+ iterate: #_params -> (find: (#fieldpair -> value -> find: 'dbfield')), (local: 'parampair');
+ #parampair -> value != '' ? #valuearray -> (insert: #parampair -> value);
+ /iterate;
+ (#fieldpair -> value) -> (insert: 'value'=#valuearray);*/
+ else(#_params -> isa('array'));
+ (#fieldpair -> value) -> (insert: 'value'=(#_params -> (find: (#fieldpair -> value -> find: 'dbfield')) -> first -> value) );
+ /if;
+ /if;
+ else: #source == 'form';
+ // load field values from form submission
+ iterate(#fields_samename, local('fieldpair_samename'));
+ // first remove value to break reference
+ (#fieldpair_samename -> value) -> (remove: 'value');
+ if(#params_fieldname -> size == #fields_samename -> size);
+ // the number of submitted fields match the number of fields in the form
+ (#fieldpair_samename -> value) -> (insert: 'value'=(#params_fieldname -> get(loop_count) -> value) );
+ else;
+ if: #params_fieldname -> size > 1;
+ // multiple field values
+ local: 'valuearray'=array;
+ iterate: #_params -> (find: (#fieldpair -> name)), (local: 'parampair');
+ #parampair -> value != '' ? #valuearray -> (insert: #parampair -> value);
+ /iterate;
+ (#fieldpair_samename -> value) -> (insert: 'value'=#valuearray);
+ else: #_params >> (#fieldpair -> name);
+ (#fieldpair_samename -> value) -> (insert: 'value'=(#_params -> (find: #fieldpair_samename -> name) -> first -> value) );
+ else;
+ (#fieldpair_samename -> value) -> (insert: 'value'='');
+ /if;
+ /if;
+ /iterate;
+ #fieldnames_done -> insert(#fieldpair -> name);
+ /if;
+ /if;
+ // apply filtering of field value (do this for all instances of the same field name, so outside of the #fieldnames_done check
+ if(#fieldpair -> value -> find('filter') -> isa('tag'));
+ (#fieldpair -> value) -> insert('value'= (#fieldpair -> value -> find('filter')) -> run(-params=(#fieldpair -> value -> find('value'))));
+ /if;
+ /if;
+ /iterate;
+
+ // capture keyvalue or lockvalue if we have a database object connected to the form
+ if: (self -> 'database') -> type == 'database';
+ //(self -> 'db_keyvalue') = null;
+ //(self -> 'db_lockvalue') = null;
+ if: (self -> 'fieldsource') == 'database';
+ if: (self -> 'database') -> lockfield != '' && (self -> 'database') -> lockvalue != '';
+ (self -> 'db_lockvalue') = (self -> 'database') -> lockvalue_encrypted;
+ (self -> 'debug_trace') -> (insert: tag_name + ': grabbing lockvalue from database ' + (self -> 'db_lockvalue'));
+ else: (self -> 'database') -> keyfield != '' && (self -> 'database') -> keyvalue != '';
+ (self -> 'db_keyvalue') = (self -> 'database') -> keyvalue;
+ (self -> 'debug_trace') -> (insert: tag_name + ': grabbing keyvalue from database ' + (self -> 'db_keyvalue'));
+ /if;
+ else;
+ if: #_params >> '-lockvalue';
+ if: #_params -> type == 'map';
+ (self -> 'db_lockvalue')=((#_params -> (find: '-lockvalue' ) ) != ''
+ ? (#_params -> (find: '-lockvalue' ) ) | null);
+ else;
+ (self -> 'db_lockvalue')=((#_params -> (find: '-lockvalue' ) -> first -> value) != ''
+ ? (#_params -> (find: '-lockvalue' ) -> first -> value) | null);
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': grabbing lockvalue from form ' + (self -> 'db_lockvalue'));
+ else: #_params >> (self -> 'keyparamname');
+ if: #_params -> type == 'map';
+ (self -> 'db_keyvalue')=((#_params -> (find: (self -> 'keyparamname') ) ) != ''
+ ? (#_params -> (find: (self -> 'keyparamname') ) ) | null);
+ else;
+ (self -> 'db_keyvalue')=((#_params -> (find: (self -> 'keyparamname') ) -> first -> value) != ''
+ ? (#_params -> (find: (self -> 'keyparamname') ) -> first -> value) | null);
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': grabbing keyvalue from form ' + (self -> 'db_keyvalue'));
+ /if;
+ /if;
+ if: (self -> 'db_lockvalue') == '' && (self -> 'db_keyvalue') == '';
+ // we have no keyvalue or lockvalue - this must be an add operation
+ (self -> 'formmode') = 'add';
+ // create a keyvalue for the record to add
+ (self -> 'db_keyvalue') = knop_unique;
+ (self -> 'debug_trace') -> (insert: tag_name + ': generating keyvalue ' + (self -> 'db_keyvalue'));
+ else: (self -> formmode) == '';
+ (self -> 'formmode') = 'edit';
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': formmode ' + (self -> formmode));
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ define_tag: 'clearfields', -description='Emtpies all form field values';
+ local: 'timer'=knop_timer;
+ iterate: (self -> 'fields'), (local: 'fieldpair');
+ if: (self -> 'exceptionfieldtypes') !>> #fieldpair -> value -> (find: 'type');
+ // && (map: 'legend', 'fieldset', 'html') !>> #fieldpair -> value -> (find: 'type');
+ // first remove value to break reference
+ (#fieldpair -> value) -> (remove: 'value');
+ (#fieldpair -> value) -> (insert: 'value'='');
+ /if;
+ /iterate;
+ if: (self -> 'database') -> type == 'database';
+ (self -> 'db_keyvalue') = null;
+ (self -> 'db_lockvalue') = null;
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ define_tag: 'resetfields', -description='Resets all form field values to their initial values';
+ local: 'timer'=knop_timer;
+ iterate: (self -> 'fields'), (local: 'fieldpair');
+ if: (self -> 'exceptionfieldtypes') !>> #fieldpair -> value -> (find: 'type');
+ //&& (map: 'legend', 'fieldset', 'html') !>> #fieldpair -> value -> (find: 'type');
+ // first remove value to break reference
+ (#fieldpair -> value) -> (remove: 'value');
+ (#fieldpair -> value) -> (insert: 'value'=#fieldpair -> value -> (find: 'defaultvalue'));
+ /if;
+ /iterate;
+ if: (self -> 'database') -> type == 'database';
+ (self -> 'db_keyvalue') = null;
+ (self -> 'db_lockvalue') = null;
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ define_tag: 'validate', -description='Performs validation and fills a transient array with field names that have input errors. \
+ form -> loadfields must be called first.';
+ local: 'timer'=knop_timer;
+
+ // Performs validation and fills a transient array with field names that have input errors.
+ // Must call -> loadfields first
+ if: (self -> 'errors') == null;
+ // initiate the errors array so we know validate has been performed
+ (self -> 'errors') = array;
+ iterate: (self -> 'fields'), (local: 'fieldpair');
+ if: !( (self -> 'exceptionfieldtypes') >> #fieldpair -> value -> (find: 'type') );
+ if: (#fieldpair -> value -> (find: 'required') )
+ && (#fieldpair -> value -> (find: 'value') ) == '';
+ (self -> 'errors') -> (insert: (#fieldpair -> value -> (find: 'name') ));
+ /if;
+ if(#fieldpair -> value -> find('validate') -> isa('tag'));
+ // perform validation expression on the field value
+ local('result'=(#fieldpair -> value -> find('validate')) -> run(-params=#fieldpair -> value -> find('value')));
+ if(#result === true || #result === 0);
+ // validation was ok
+ else(#result != 0 || #result -> size);
+ // validation result was an error code or message
+ (self -> 'errors') -> insert(#fieldpair -> value -> find('name') = #result);
+ else;
+ (self -> 'errors') -> insert(#fieldpair -> value -> find('name'));
+ /if;
+ /if;
+ /if;
+ /iterate;
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': form is valid ' + ((self -> 'errors') -> size == 0));
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ define_tag: 'isvalid', -description='Returns the result of form -> validate (true/false) without performing the validation again (unless it hasn\'t been performed already)';
+ local: 'timer'=knop_timer;
+ // Returns the result of -> validate (true/false) without performing the validation again (unless it is needed)
+ (self -> 'errors') == null ? self -> validate;
+ (self -> 'debug_trace') -> (insert: tag_name + ': form is valid ' + ((self -> 'errors') -> size == 0));
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ return: (self -> 'errors') -> size == 0;
+ /define_tag;
+
+
+ define_tag: 'adderror', -description='adds the name for a field that has validation error, used for custom field validation. \
+ calls form -> validate first if needed',
+ -required='fieldname';
+ local: 'timer'=knop_timer;
+ // adds a field that has error
+ // calls ->validate first if needed, to make sure self -> 'errors' is an array
+ (self -> 'errors') == null ? self -> validate;
+ (self -> 'errors') -> (insert: #fieldname);
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+
+ define_tag: 'errors', -description='Returns an array with fields that have input errors, or empty array if no errors or form has not been validated';
+ // returns an array with fields that have input errors, or emtpy array if no errors or form has not been validated
+ if: (self -> 'errors') == null;
+ return: array;
+ else;
+ return: (self -> 'errors');
+ /if;
+ /define_tag;
+
+
+
+ define_tag: 'updatefields', -description='Returns a pair array with fieldname=value, or optionally SQL string to be used in an update inline.\
+ form -> loadfields must be called first.\n\
+ Parameters:\n\
+ -sql (optional)\n\
+ -removedotbackticks (optional flag) Use with -sql for backward compatibility for fields that contain periods. If you use periods in a fieldname then you cannot use a JOIN in Knop.',
+ -optional='sql',
+ -optional='removedotbackticks';
+ local: 'timer'=knop_timer;
+ // Returns a pair array with fieldname=value, or optionally SQL string to be used in an update inline. Optionally use -removedotbackticks with -sql for backward compatibility with fields that contain periods.
+ // Must call ->loadfields first.
+ local: 'output'=array,
+ '_sql'=(local_defined: 'sql'),
+ '_removedotbackticks'=(local_defined: 'removedotbackticks'),
+ 'fieldvalue'=null, 'onevalue'=null;
+ iterate: (self -> 'fields'), (local: 'fieldpair');
+ if: !( (self -> 'exceptionfieldtypes') >> #fieldpair -> value -> (find: 'type') )
+ && !(#fieldpair -> value -> (find: 'name') -> (beginswith: '-'))
+ && (#fieldpair -> value -> (find: 'dbfield')) != '';
+ // don't use submit etc and exclude fields whose name begins with -
+ #fieldvalue = (#fieldpair -> value -> (find: 'value') );
+ if: #fieldvalue -> type != 'array';
+ // to support multiple values for one fieldname, like checkboxes
+ #fieldvalue = array: #fieldvalue;
+ /if;
+ if: #_sql;
+ if(#_removedotbackticks);
+ #output -> (insert: '`' + (encode_sql(knop_stripbackticks(#fieldpair -> value -> find('dbfield'))) ) + '`'
+ + '="' + (encode_sql: (#fieldvalue -> (join: ',')) ) + '"');
+ else;
+ #output -> (insert: '`' + (encode_sql(string_replace(knop_stripbackticks(#fieldpair -> value -> find('dbfield')), -find='.', -replace='`.`')) ) + '`'
+ + '="' + (encode_sql: (#fieldvalue -> (join: ',')) ) + '"');
+ /if;
+ else;
+ iterate: #fieldvalue, #onevalue;
+ #output -> (insert: (#fieldpair -> value -> (find: 'dbfield') )
+ = #onevalue );
+ /iterate;
+ /if;
+ /if;
+ /iterate;
+ if: #_sql;
+
+ #output = '(' + #output -> (join: ',') + ')';
+
+ /if;
+
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ return: @#output;
+
+ /define_tag;
+
+
+ define_tag: 'getbutton', -description='Returns what button was clicked on the form on the previous page. Assumes that submit buttons are named button_add etc. \
+ Returns add, update, delete, cancel or any custom submit button name that begins with button_.';
+ local: 'timer'=knop_timer;
+ if: (self -> 'formbutton') != '';
+ // we have already found out once what button was clicked
+ (self -> 'debug_trace') -> (insert: tag_name + ': cached ' + (self -> 'formbutton'));
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ return: (self -> 'formbutton');
+ /if;
+ local: 'clientparams'=client_getparams;
+ #clientparams -> (merge: client_postparams);
+ // look for submit buttons, the least destructive first
+ iterate: (array: 'cancel', 'save', 'add', 'delete'), (local: 'buttonname');
+ if: #clientparams >> 'button_' + #buttonname
+ || #clientparams >> 'button_' + #buttonname + '.x'
+ || #clientparams >> 'button_' + #buttonname + '.y';
+ (self -> 'debug_trace') -> (insert: tag_name + ': built-in button name ' + #buttonname);
+ (self -> 'formbutton') = #buttonname;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ return: #buttonname;
+ /if;
+ /iterate;
+ // no button found yet - look for custom button names
+ iterate: #clientparams, #buttonname;
+ #buttonname -> type == 'pair' ? #buttonname = #buttonname -> name;
+ if: #buttonname -> (beginswith: 'button_');
+ #buttonname -> (removeleading: 'button_') & (removetrailing: '.x') & (removetrailing: '.y');
+ (self -> 'debug_trace') -> (insert: tag_name + ': custom button name ' + #buttonname);
+ (self -> 'formbutton') = #buttonname;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ return: #buttonname;
+ /if;
+ /iterate;
+ (self -> 'debug_trace') -> (insert: tag_name + ': No button found');
+ /define_tag;
+
+ define_tag: 'process', -description='Automatically handles a form submission and handles add, update, or delete. \
+ Requires that a database object is specified for the form',
+ -optional='user',
+ -optional='lock',
+ -optional='keyvalue';
+ local: 'timer'=knop_timer;
+ fail_if: (self -> 'database') -> type != 'database', 7103, self -> error_msg(7103);
+
+ (self -> 'error_code') = 0;
+ (self -> 'error_msg') = string;
+
+ if: self -> getbutton == 'cancel';
+ // do nothing at all
+ (self -> 'debug_trace') -> (insert: tag_name + ': cancelling ');
+
+ else: self -> getbutton == 'save';
+ self -> loadfields;
+ if: self -> isvalid;
+ if: (local_defined: 'user') && (self -> lockvalue) != '';
+ (self -> database) -> (saverecord: (self -> updatefields), -lockvalue=(self -> lockvalue), -keyvalue=(self -> keyvalue), -user=#user);
+ else;
+ (self -> database) -> (saverecord: (self -> updatefields), -keyvalue=(self -> keyvalue));
+ /if;
+ if: self -> database -> error_code != 0;
+ (self -> 'error_code') = self -> database -> error_code;
+ (self -> 'error_msg') = 'Process: update record error ' + (self -> database -> error_msg);
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': updating record ' + (self -> database -> error_msg) + ' ' + (self -> database -> error_code));
+ else;
+ (self -> 'error_code') = 7101; // Process: update record did not pass form validation
+ (self -> 'debug_trace') -> (insert: tag_name + ': update record did not pass form validation');
+ /if;
+
+ else: self -> getbutton == 'add';
+ self -> loadfields;
+ if: self -> isvalid;
+ (self -> database) -> (addrecord: (self -> updatefields), -keyvalue=(self -> keyvalue));
+ if: self -> database -> error_code != 0;
+ (self -> 'error_code') = self -> database -> error_code;
+ (self -> 'error_msg') = 'Process: add record error ' + (self -> database -> error_msg);
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': adding record ' + (self -> database -> error_msg) + ' ' + (self -> database -> error_code));
+ else;
+ (self -> 'error_code') = 7101; // Process: add record did not pass form validation
+ (self -> 'debug_trace') -> (insert: tag_name + ': add record did not pass form validation');
+ (self -> 'debug_trace') -> (insert: tag_name + ': reverting form mode to add');
+ /if;
+
+ else: self -> getbutton == 'delete';
+ self -> loadfields;
+ (self -> 'debug_trace') -> (insert: tag_name + ': will delete record with keyvalue ' + (self -> keyvalue) + ' lockvalue ' + (self -> lockvalue));
+ if: (local_defined: 'user') && (self -> lockvalue) != '';
+ (self -> database) -> (deleterecord: -lockvalue=(self -> lockvalue), -keyvalue=(self -> keyvalue), -user=#user);
+ else;
+ (self -> database) -> (deleterecord: -keyvalue=(self -> keyvalue));
+ /if;
+ if: self -> database -> error_code == 0;
+ self -> resetfields;
+ else;
+ (self -> 'error_code') = self -> database -> error_code;
+ (self -> 'error_msg') = 'Process: delete record error ' + (self -> database -> error_msg);
+ /if;
+ (self -> 'debug_trace') -> (insert: tag_name + ': deleting record ' + (self -> database -> error_msg) + ' ' + (self -> database -> error_code));
+ else: false;
+ // do not go here, database record should be loaded with a separate call
+ if: (local_defined: 'lock');
+ self -> database ->(getrecord: (local: 'keyvalue'), -lock, -user=#user);
+ (self -> 'debug_trace') -> (insert: tag_name + ': loading record using lock' + (self -> database -> error_msg) + ' ' + (self -> database -> error_code));
+ else;
+ self -> database ->(getrecord: (local: 'keyvalue'), -user=#user);
+ (self -> 'debug_trace') -> (insert: tag_name + ': loading record' + (self -> database -> error_msg) + ' ' + (self -> database -> error_code));
+ /if;
+ self -> (loadfields: -inlinename=(self -> database -> inlinename));
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ define_tag: 'setformat', -description='Defines a html template for the form. \n\
+ Parameters:\n\
+ -template (optional string) html template, defaults to #label# #field##required# \n\
+ -buttontemplate (optional string) html template for buttons, defaults to #field#\n\
+ -required (optional string) character(s) to display for required fields (used for #required#), defaults to *\n\
+ -legend (optional string) legend for the entire form - if specified, a fieldset will also be wrapped around the form\n\
+ -class (optional string) css class name that will be used for the form element, default none\n\
+ -errorclass (optional string) css class name that will be used for the label to highlight input errors, if not defined style="color: red" will be used\n\
+ -unsavedmarker (optional string) \n\
+ -unsavedmarkerclass (optional string) \n\
+ -unsavedwarning (optional string)',
+ -optional='template', -type='string',
+ -optional='buttontemplate', -type='string',
+ -optional='required', -type='string',
+ -optional='legend', -type='string',
+ -optional='class', -type='string',
+ -optional='errorclass', -type='string',
+ -optional='unsavedmarker', -type='string',
+ -optional='unsavedmarkerclass', -type='string',
+ -optional='unsavedwarning', -type='string';
+ local: 'timer'=knop_timer;
+
+ local_defined('template') ? (self -> 'template') = #template;
+ local_defined('buttontemplate') ? (self -> 'buttontemplate') = #buttontemplate;
+ local_defined('required') ? (self -> 'required') = #required;
+ local_defined('legend') ? (self -> 'legend') = #legend;
+ local_defined('class') ? (self -> 'class') = #class;
+ local_defined('errorclass') ? (self -> 'errorclass') = #errorclass;
+ local_defined('unsavedmarker') ? (self -> 'unsavedmarker') = #unsavedmarker;
+ local_defined('unsavedmarkerclass') ? (self -> 'unsavedmarkerclass') = #unsavedmarkerclass;
+ local_defined('unsavedwarning') ? (self -> 'unsavedwarning') = #unsavedwarning;
+
+ if: local_defined: 'unsavedwarning';
+ // escape quotes for javascript
+ (self -> 'unsavedwarning') -> (replace: '\'', '\\\'');
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+
+ define_tag: 'renderform', -description='Outputs HTML for the form fields, a specific field, a range of fields or all fields of a specific type. \
+ Also inserts all needed javascripts into the page. \
+ Use form -> setformat first to specify the html format, otherwise default format #label# #field##required# is used. \n\
+ Parameters:\n\
+ -name (optional) Render only the specified field\n\
+ -from (optional) Render form fields from the specified number index or field name. Negative number count from the last field.\n\
+ -to (optional) Render form fields to the specified number index or field name. Negative number count from the last field.\n\
+ -type (optional) Only render fields of this or these types (string or array)\n\
+ -excludetype (optional) Render fields except of this or these types (string or array)\n\
+ -legend (optional) Groups the rendered fields in a fieldset and outputs a legend for the fieldset\n\
+ -start (optional) Only render the starting tag\n\
+ -xhtml (optional flag) XHTML valid output',
+ -optional='name', -copy, // field name
+ -optional='from', -copy, // number index or field name
+ -optional='to', -copy, // number index or field name
+ -optional='type', -copy, // only output fields of this or these types (string or array)
+ -optional='excludetype', -copy, // output fields except of this or these types (string or array)
+ -optional='legend', // groups the rendered fields in a fieldset and outputs a legend for the fieldset
+ -optional='start', // only output the starting tag
+ -optional='xhtml'; // boolean, if set to true adjust output for XHTML
+ local: 'timer'=knop_timer;
+ handle;knop_debug('Done with ' + self->type + ' -> ' + tag_name, -time, -type=self->type);/handle;
+
+ // Outputs HTML for the form fields
+
+ /*
+ TODO:
+ Handling of multiple fields with the same name
+ */
+ local: 'output'=string,
+ 'onefield'=map,
+ 'renderfield'=string,
+ 'renderfield_base'=string,
+ 'renderrow'=string,
+ 'formid'=null,
+ 'usehint'=array,
+ 'nowarning'=false,
+ 'fieldtype',
+ 'fieldvalue'=string,
+ 'fieldvalue_array'=array,
+ 'options'=array,
+ 'focusfield';
+
+ local: 'clientparams'=client_getparams;
+ #clientparams -> (merge: client_postparams);
+ #clientparams -> (removeall: (self -> 'keyparamname'));
+ #clientparams -> (removeall: '-lockvalue');
+ #clientparams -> (removeall: '-action');
+ #clientparams -> (removeall: '-xhtml');
+
+ // local var that adjust tag endings if rendered for XHTML
+ local: 'endslash' = ((self -> (xhtml: params)) ? ' /' | '');
+
+ // page var to keep track of the number of forms that has been rendered on a page
+ if: !(var_defined: 'knop_form_renderform_counter');
+ var: 'knop_form_renderform_counter'=0;
+ /if;
+
+ $knop_form_renderform_counter += 1;
+
+ if: (self -> 'id') != '';
+ #formid = (self -> 'id');
+ else: (self -> 'name') != '';
+ #formid = (self -> 'name');
+ else;
+ #formid = 'form' + $knop_form_renderform_counter;
+ /if;
+
+
+ local: 'renderformStartTag'=false, 'renderformEndTag'=false;
+ // remove params that should not stop formstarttag and formendtag from rendering
+ params -> type == 'array' ? params -> (removeall: '-legend') & (removeall: '-xhtml');
+ if: (self -> 'formaction') != null
+ && (params -> size == 0 || (local_defined: 'start') );
+ #renderformStartTag=true;
+ /if;
+ if: (self -> 'formaction') != null
+ && (params -> size == 0 || (local_defined: 'end') );
+ #renderformEndTag=true;
+ /if;
+ if: #renderformStartTag;
+ // render opening form tag
+
+ #output +='';
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ return: @#output;
+ /define_tag;
+
+ define_tag: 'renderhtml', -description='Outputs form data as plain HTML, a specific field, a range of fields or all fields of a specific type. \
+ Some form field types are excluded, such as submit, reset, file etc. \
+ Use form -> setformat first to specify the html format, otherwise default format #label#: #field# is used.\n\
+ Parameters:\n\
+ -name (optional) Render only the specified field\n\
+ -from (optional) Render fields from the specified number index or field name\n\
+ -to (optional) Render fields to the specified number index or field name\n\
+ -type (optional) Only render fields of this or these types (string or array)\n\
+ -excludetype (optional) Render fields except of this or these types (string or array)\n\
+ -legend (optional) Groups the rendered fields in a fieldset and outputs a legend for the fieldset\n\
+ -xhtml (optional flag) XHTML valid output',
+ -optional='name', -copy, // field name
+ -optional='from', -copy, // number index or field name
+ -optional='to', -copy, // number index or field name
+ -optional='type', -copy, // only output fields of this or these types (string or array)
+ -optional='excludetype', -copy, // do not output fields of this or these types (string or array)
+ -optional='legend', // groups the rendered fields in a fieldset and outputs a legend for the fieldset
+ -optional='xhtml'; // boolean, if set to true adjust output for XHTML
+ local: 'timer'=knop_timer;
+
+ local: 'output'=string,
+ 'onefield'=map,
+ 'renderfield'=string,
+ 'renderfield_base'=string,
+ 'renderrow'=string,
+ 'fieldvalue'=string,
+ 'fieldvalue_array'=array,
+ 'options'=array,
+ 'usehint'=array;
+
+
+ // local var that adjust tag endings if rendered for XHTML
+ local: 'endslash' = ((self -> (xhtml: params)) ? ' /' | '');
+
+ (local_defined: 'name') && !((self -> 'fields') >> #name) ? return;
+
+ (local_defined: 'name') ? local: 'from'=#name, 'to'=#name;
+ !(local_defined: 'from') ? local: 'from'=1;
+ !(local_defined: 'to') ? local: 'to'=(self -> 'fields') -> size;
+ !(local_defined: 'type') ? local: 'type'=(self -> 'validfieldtypes');
+ !(local_defined: 'excludetype') ? local: 'excludetype'=map;
+ #type -> type == 'string' ? #type = (map: #type);
+ #excludetype -> type == 'string' ? #excludetype = (map: #excludetype);
+
+ // use field name if #from is a string
+ #from -> type == 'string' ? #from = integer: ((self -> 'fields') -> (findindex: #from) -> first);
+ #from == 0 ? #from = 1;
+ // negative numbers count from the end
+ #from < 0 ? #from = (self -> 'fields') -> size + #from;
+
+ // use field name if #to is a string
+ #to -> type == 'string' ? #to = integer: ((self -> 'fields') -> (findindex: #to) -> last);
+ #to == 0 ? #to = (self -> 'fields') -> size;
+ // negative numbers count from the end
+ #to < 0 ? #to = (self -> 'fields') -> size + #to;
+
+ //Sanity check
+ #from > #to ? #to = #from;
+
+ local: 'template'=( (self -> 'template') != ''
+ ? (self -> 'template')
+ | '#label#: #field# \n' );
+ local: 'buttontemplate'=( (self -> 'buttontemplate') != ''
+ ? (self -> 'buttontemplate')
+ | (self -> 'template') != ''
+ ? (self -> 'template')
+ | '#field#\n' );
+ local: 'defaultclass'=( (self -> 'class') != ''
+ ? (self -> 'class')
+ | '');
+ if: (local_defined: 'legend');
+ #output += '\n';
+ (self -> 'render_fieldset_open') = false;
+ /if;
+ #output += '\n';
+ (self -> 'render_fieldset_open') = false;
+ /if;
+ if: #fieldvalue != false;
+ #output += '\n';
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ return: #output;
+ /define_tag;
+
+ define_tag: 'getvalue', -description='Returns the current value of a form field. Returns an array for repeated form fields. ',
+ -required='name', -type='string',
+ -optional='index', -type='integer', -copy;
+ !local_defined('index') ? local('index' = 1);
+ #index < 1 ? #index = 1;
+ if: (self -> 'fields') >> #name;
+ if(#index > (self -> 'fields') -> find(#name) -> size);
+ return;
+ /if;
+ return: (self -> 'fields') -> (find: #name) -> get(#index) -> value -> (find: 'value');
+ /if;
+ /define_tag;
+
+ define_tag: 'getlabel', -description='Returns the label for a form field. ',
+ -required='name', -type='string';
+ if: (self -> 'fields') >> #name;
+ return: (self -> 'fields') -> (find: #name) -> first -> value -> (find: 'label');
+ /if;
+ /define_tag;
+
+ define_tag: 'setvalue', -description='Sets the value for a form field. \
+ Either form -> (setvalue: fieldname=newvalue) or form -> (setvalue: -name=fieldname, -value=newvalue)',
+ -required='name',
+ -optional='value',
+ -optional='index', -type='integer', -copy;
+ local: 'timer'=knop_timer;
+ // either -> (setvalue: 'fieldname'='newvalue') or -> (setvalue: -name='fieldname', -value='newvalue')
+ local: '_name'=#name, '_value'=(local: 'value');
+ !local_defined('index') ? local('index' = 1);
+ #index < 1 ? #index = 1;
+ if: #name -> type == 'pair';
+ #_name = #name -> name;
+ #_value = #name -> value;
+ /if;
+ if: (self -> 'fields') >> #_name;
+ if(#index > (self -> 'fields') -> find(#_name) -> size);
+ return;
+ /if;
+ // first remove value to break reference
+ ((self -> 'fields') -> get((self -> 'fields') -> (findindex: #_name) -> get(#index)) -> value) -> (remove: 'value');
+ ((self -> 'fields') -> get((self -> 'fields') -> (findindex: #_name) -> get(#index)) -> value) -> (insert: 'value'=#_value);
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ define_tag: 'removefield', -description='Removes all form elements with the specified name from the form',
+ -required='name', -type='string';
+ local: 'timer'=knop_timer;
+ (self -> 'fields') -> (removeall: #name);
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ /define_tag;
+
+ define_tag: 'keys', -description='Returns an array of all field names';
+ local: 'timer'=knop_timer;
+ local: 'output'=array;
+ iterate: (self -> 'fields'), (local: 'fieldpair');
+ #output -> (insert: #fieldpair -> name);
+ /iterate;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ return: #output;
+ /define_tag;
+
+
+ define_tag: 'keyvalue'; return: (self -> 'db_keyvalue'); /define_tag;
+ define_tag: 'lockvalue'; return: (self -> 'db_lockvalue'); /define_tag;
+ define_tag: 'lockvalue_decrypted';
+ (self -> 'database') -> type != 'database' ? return;
+ return: (decrypt_blowfish: (self -> 'db_lockvalue'), -seed=(self -> 'database' -> 'lock_seed'));
+ /define_tag;
+ define_tag: 'database'; return: (self -> 'database'); /define_tag;
+
+ define_tag: 'formmode', -description='Returns add or edit after for -> init has been called';
+ local: 'timer'=knop_timer;
+ if: (self -> getbutton) == 'add';
+ // this is needed to keep the right form mode after a failed add
+ (self -> 'formmode') = 'add';
+ /if;
+ self -> 'tagtime_tagname'=tag_name;
+ self -> 'tagtime'=integer: #timer; // cast to integer to trigger onconvert and to "stop timer"
+ return: (self -> 'formmode');
+ /define_tag;
+
+ define_tag: 'error_code';
+ // custom error_code for knop_form
+ if: (self -> 'error_code');
+ return: integer: (self -> 'error_code');
+ else: (self -> 'errors') -> type == 'array' && (self -> 'errors') -> size > 0;
+ (self -> 'error_code') = 7101;
+ return: (self -> 'error_code');
+ else;
+ return: 0;
+ /if;
+ /define_tag;
+
+
+
+ define_tag: 'afterhandler', -description='Internal member tag. Adds needed javascripts through an atend handler that will be processed when the entire page is done. \n\
+ Parameters:\n\
+ -headscript (optional) A single script, will be placed before (or at top of page if is missing)\n\
+ -endscript (optional) Multiple scripts (no duplicates), will be placed before