Index: tags/2.2.3/bin/_config.inc.php
===================================================================
--- tags/2.2.3/bin/_config.inc.php	(revision 632)
+++ tags/2.2.3/bin/_config.inc.php	(revision 632)
@@ -0,0 +1,103 @@
+<?php
+/*
+* The Strangecode Codebase - a general application development framework for PHP
+* For details visit the project site: <http://trac.strangecode.com/>
+* Copyright Â© 2015 Strangecode, LLC
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/*
+* Small config file for cli scripts in the codebase/bin directory.
+*
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    12 Jul 2015 22:48:13
+*/
+
+// The name of the CLI.
+$cli_executed = basename($_SERVER['argv'][0]);
+
+// Enforce "relaxed" directory location, if set by the called script.
+// This script can be executed in any directory that is an ancestor 2 directories above the db_auth file.
+if (!defined('COMMON_BASE')) {
+    define('COMMON_BASE', realpath('.'));
+}
+
+// Use codebase files relative to this file.
+define('CODEBASE_PATH', realpath(dirname(__FILE__) . '/../'));
+
+
+$db_auth_file = false;
+if (!defined('_NODB')) {
+    $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(COMMON_BASE));
+    $rii->setMaxDepth(2);
+    foreach ($rii as $filename => $file) {
+        if (in_array(basename($filename), ['db_auth.inc.php', 'db_auth.json'])) {
+            $db_auth_file = $filename;
+            break;
+        }
+    }
+    if (!$db_auth_file) {
+        die(sprintf("%s error: the current directory must be common site directory (i.e. the parent directory of the document root) AND a db_auth file must exist within two directory levels.\n", $cli_executed));
+    }
+    if (fileowner($db_auth_file) != getmyuid()) {
+        die(sprintf("%s error: you must execute this script as the owner of the web files.\n", $cli_executed));
+    }
+}
+
+// Set include path.
+ini_set('include_path', get_include_path() . PATH_SEPARATOR . COMMON_BASE);
+
+/********************************************************************
+* CONFIG
+********************************************************************/
+
+// Include core libraries.
+require_once CODEBASE_PATH . '/lib/App.inc.php';
+require_once CODEBASE_PATH . '/lib/Utilities.inc.php';
+
+define('_CLI', true);
+$app =& App::getInstance('codebase');
+$app->setParam(array(
+    'site_name' => 'codebase',
+    'site_email' => 'codebase@strangecode.com',
+    'enable_session' => false,
+    'enable_db' => defined('_NODB') ? false : true,
+    'db_create_tables' => true,
+    'db_auth_file' => $db_auth_file,
+    'db_always_debug' => false,
+    'db_debug' => true,
+    'db_die_on_failure' => true,
+    'display_errors' => true,
+    'error_reporting' => E_ALL,
+    'log_file_priority' => LOG_DEBUG,
+    'log_screen_priority' => LOG_INFO,
+    'log_directory' => (is_dir('./log') ? './log' : '/tmp'), // If ./log exists, use it, otherwise /tmp.
+    'log_filename' => 'site_log',
+));
+
+if (false !== $db_auth_file && strtolower(getFilenameExtension($db_auth_file)) != 'json') {
+    // DB auth file is not JSON, and will not be automatically loaded by $app's 'db_auth_file' parser.
+    require_once $db_auth_file;
+}
+
+// Start application-based functionality: database, session, environment, ini setup, etc.
+// Most configuration parameters must be set before starting the App.
+$app->start();
+
+if (!defined('_NODB')) {
+    // Global DB object. Automatically pre-configured by $app->start().
+    $db =& DB::getInstance();
+}
Index: tags/2.2.3/bin/acl.cli.php
===================================================================
--- tags/2.2.3/bin/acl.cli.php	(revision 632)
+++ tags/2.2.3/bin/acl.cli.php	(revision 632)
@@ -0,0 +1,318 @@
+#!/usr/bin/env php
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+* acl.cli.php
+*
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.1
+* @since    14 Jun 2006 23:10:45
+*/
+
+/********************************************************************
+* CONFIG
+********************************************************************/
+
+require_once dirname(__FILE__) . '/_config.inc.php';
+
+// Give them a fighting chance. Show the help message. ;P
+if ($_SERVER['argc'] <= 1) {
+    help();
+}
+
+// ACL!
+require_once CODEBASE_PATH . '/lib/ACL.inc.php';
+$acl =& ACL::getInstance();
+$acl->setParam(array(
+    'create_table' => false,
+    'enable_cache' => false,
+));
+
+
+/********************************************************************
+* MAIN
+********************************************************************/
+
+if (!$db->tableExists('acl_tbl')) {
+    printf("This project doesn't appear to be using ACL (there is no acl_tbl in the %s DB).\n", $app->getParam('db_name'));
+    $app->stop();
+    die;
+}
+
+$op = $_SERVER['argv'][1];
+switch ($op) {
+case 'list' :
+    $type = isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : null;
+    switch ($type) {
+    case 'aro' :
+    case 'aco' :
+    case 'axo' :
+        listObjects($type);
+        break;
+    case 'all' :
+        listObjects('aro');
+        listObjects('aco');
+        listObjects('axo');
+        break;
+    case 'perms' :
+    default :
+        listPerms();
+        break;
+    }
+    break;
+
+case 'addaro' :
+case 'addaco' :
+case 'addaxo' :
+    $object = isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : null;
+    $parent = isset($_SERVER['argv'][3]) ? $_SERVER['argv'][3] : null;
+    if (!isset($object)) {
+        echo "'add*' commands require at least one argument. Try 'help' if you are lost.\n";
+        break;
+    }
+    echo $acl->add($object, $parent, str_replace('add', '', $op)) ? "Ok\n" : "Error!\n";
+    break;
+
+case 'mvaro' :
+case 'mvaco' :
+case 'mvaxo' :
+    $object = isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : null;
+    $parent = isset($_SERVER['argv'][3]) ? $_SERVER['argv'][3] : null;
+    if (!isset($object)) {
+        echo "'mv*' commands require at least one argument. Try 'help' if you are lost.\n";
+        break;
+    }
+    echo $acl->move($object, $parent, str_replace('mv', '', $op)) ? "Ok\n" : "Error!\n";
+    break;
+
+case 'rmaro' :
+case 'rmaco' :
+case 'rmaxo' :
+    $object = isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : null;
+    if (!isset($object)) {
+        echo "'add*' commands require at least one argument. Try 'help' if you are lost.\n";
+        break;
+    }
+    echo $acl->remove($object, str_replace('rm', '', $op)) ? "Ok\n" : "Error!\n";
+    break;
+
+case 'initdb' :
+    echo $acl->initDB(true) ? "Ok\n" : "Error!\n";
+    break;
+
+case 'grant' :
+    $aro = isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : null;
+    $aco = isset($_SERVER['argv'][3]) ? $_SERVER['argv'][3] : null;
+    $axo = isset($_SERVER['argv'][4]) ? $_SERVER['argv'][4] : null;
+    if (!isset($aro)) {
+        echo "'grant' command require at least one argument. Try 'help' if you are lost.\n";
+        break;
+    }
+    echo $acl->grant($aro, $aco, $axo) ? "Ok\n" : "Error!\n";
+    break;
+
+case 'revoke' :
+    $aro = isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : null;
+    $aco = isset($_SERVER['argv'][3]) ? $_SERVER['argv'][3] : null;
+    $axo = isset($_SERVER['argv'][4]) ? $_SERVER['argv'][4] : null;
+    if (!isset($aro)) {
+        echo "'revoke' command require at least one argument. Try 'help' if you are lost.\n";
+        break;
+    }
+    echo $acl->revoke($aro, $aco, $axo) ? "Ok\n" : "Error!\n";
+    break;
+
+case 'delete' :
+    $aro = isset($_SERVER['argv'][2]) && 'null' != $_SERVER['argv'][2] ? $_SERVER['argv'][2] : null;
+    $aco = isset($_SERVER['argv'][3]) && 'null' != $_SERVER['argv'][3] ? $_SERVER['argv'][3] : null;
+    $axo = isset($_SERVER['argv'][4]) && 'null' != $_SERVER['argv'][4] ? $_SERVER['argv'][4] : null;
+    if (!isset($_SERVER['argv'][2]) || !isset($_SERVER['argv'][3]) || !isset($_SERVER['argv'][4])) {
+        echo "'delete' command require all three arguments to be specified. Try 'help' if you are lost.\n";
+        break;
+    }
+    echo $acl->delete($aro, $aco, $axo) ? "Ok\n" : "Error!\n";
+    break;
+
+case 'check' :
+    $aro = isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : null;
+    $aco = isset($_SERVER['argv'][3]) ? $_SERVER['argv'][3] : null;
+    $axo = isset($_SERVER['argv'][4]) ? $_SERVER['argv'][4] : null;
+    if (!isset($aro)) {
+        echo "'check' command require at least one argument. Try 'help' if you are lost.\n";
+        break;
+    }
+    echo $acl->check($aro, $aco, $axo) ? "allow\n" : "deny\n";
+    break;
+
+case 'help' :
+    help();
+    break;
+
+default :
+    echo "'$op' is not an understood command. Try 'help' if you are lost.\n";
+    break;
+}
+
+$app->stop();
+die;
+
+
+/********************************************************************
+* FUNCTIONS
+********************************************************************/
+
+function help()
+{
+    global $cli_executed;
+
+    ?>
+Access Control List command line tool.
+
+This script must be run in the common site directory (i.e. the parent
+directory of the document root). DB credentials are retrieved from:
+global/db_auth.inc.php so this file must exist. Furthermore this script
+must be executed as the owner of the db_auth.inc.php file.
+
+Three types of objects are managed by this interface:
+
+  ARO - Access Request Objects
+  ACO - Access Control Objects
+  AXO - Access Xtra Objects
+
+These are most often used as a USER -> ACTION -> OBJECT model,
+but could just as easily be SPICES -> CUISINES -> DISHES. A privilege is
+allowed if a user (ARO) can perform an action (ACO) on something (AXO).
+For example, with an `ARO->ACO->AXO` of `Bob->edit->4`, Bob can edit article 4.
+If the AXO were omitted (i.e. just `Bob->edit`), this becomes "Bob can edit"
+(he can edit any object).
+
+Each access object is stored as a node in hierarchical tree structure.
+A permission granted to a node is applied to all its children. If a child
+node is specified with a permission more specific than its ancestors, the
+child will take precedence. If no permission is specified, root is used,
+implying access to any object of that type.
+
+Usage: <?php echo $cli_executed; ?> command [args]
+
+Where command is any of the following (with arguments):
+
+    initdb
+    list [aro | aco | axo | all | perms]
+    check aro [aco] [axo]
+    addaro aro [parent]
+    addaco aco [parent]
+    addaxo axo [parent]
+    mvaro aro [parent]
+    mvaco aco [parent]
+    mvaxo axo [parent]
+    rmaro aro
+    rmaco aco
+    rmaxo axo
+    grant aro [aco] [axo]
+    revoke aro [aco] [axo]
+    delete aro aco axo
+
+For the add*, mv*, grant, and revoke commands if any of the optional
+args are not provided, 'root' is assumed. The delete command requires
+all object types to be specified; Passing the string 'null' will cause
+all matches in that column to be deleted. run with 'grants' to view what
+can be deleted.
+<?php
+    die;
+}
+
+
+/*
+* Print the tree structure of a specified table (aro_tbl, aco_tbl, or axo_tbl).
+*
+* @access   public
+* @param    string $type Table to call, one of: aro, aco, or axo.
+* @param    string $root Root node from which to begin calculating.
+* @return   bool Returns false on error.
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    17 Jun 2006 23:41:22
+*/
+function listObjects($type, $root=null)
+{
+    global $acl;
+
+    $app =& App::getInstance();
+    $db =& DB::getInstance();
+
+    switch ($type) {
+    case 'aro' :
+        printf("\n%-45s %s\n", 'Request objects', 'Added');
+        break;
+    case 'aco' :
+        printf("\n%-45s %s\n", 'Control objects', 'Added');
+        break;
+    case 'axo' :
+        printf("\n%-45s %s\n", 'Xtra objects', 'Added');
+        break;
+    default :
+        $app->logMsg(sprintf('Invalid access object type: %s', $type), LOG_ERR, __FILE__, __LINE__);
+        return false;
+    }
+
+    echo "---------------------------------------------------------------------\n";
+
+    foreach ($acl->getList($type, $root) as $o) {
+        // Display indented node title.
+        printf("%-45s %s\n", str_repeat('    ', $o['depth']) . $o['name'], date($app->getParam('date_format') . ' ' . $app->getParam('time_format'), strtotime($o['added_datetime'])));
+    }
+
+    echo "\n";
+}
+
+/*
+* List all entries in the acl_tbl.
+*
+* @access   public
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    17 Jun 2006 15:11:53
+*/
+function listPerms()
+{
+    $app =& App::getInstance();
+    $db =& DB::getInstance();
+
+    // Retrieve access value from db.
+    $qid = $db->query("
+        SELECT aro_tbl.name AS aro, aco_tbl.name AS aco, axo_tbl.name AS axo, acl_tbl.access, acl_tbl.added_datetime
+        FROM acl_tbl
+        LEFT JOIN aro_tbl ON (acl_tbl.aro_id = aro_tbl.aro_id)
+        LEFT JOIN aco_tbl ON (acl_tbl.aco_id = aco_tbl.aco_id)
+        LEFT JOIN axo_tbl ON (acl_tbl.axo_id = axo_tbl.axo_id)
+        ORDER BY aro_tbl.lft ASC, aco_tbl.lft ASC, axo_tbl.lft ASC
+    ");
+    echo "\n";
+    printf("%-25s %-25s %-25s %-6s %-10s\n", 'Request objects', 'Control objects', 'Xtra objects', 'Grant', 'Added');
+    echo "------------------------------------------------------------------------------------------------\n";
+    while ($p = mysql_fetch_assoc($qid)) {
+        printf("%-25s %-25s %-25s \033[0;%sm%-6s\033[0m %-10s\n", $p['aro'], $p['aco'], $p['axo'], ('allow' == $p['access'] ? '32' : '31'), $p['access'], date($app->getParam('date_format'), strtotime($p['added_datetime'])));
+    }
+}
+
+
Index: tags/2.2.3/bin/extract_gettext.pl
===================================================================
--- tags/2.2.3/bin/extract_gettext.pl	(revision 632)
+++ tags/2.2.3/bin/extract_gettext.pl	(revision 632)
@@ -0,0 +1,38 @@
+#!/usr/bin/perl
+#
+# Perl script to extract strings from all the files and print to stdout for use with xgettext.
+
+use FileHandle;
+use File::Basename;
+use File::Find;
+use Cwd;
+
+use strict;
+use vars qw($exts %strings);
+
+if (!@ARGV) {
+    die "No directory paths specified";
+}
+
+$exts = '(\.php$|\.ihtml$)';
+
+foreach (@ARGV) {
+    my $fulldir = cwd() . '/' . $_;
+    find(\&extract, $fulldir);
+}
+print join("\n", sort keys %strings), "\n";
+
+sub extract
+{
+    my $file = $File::Find::name;
+    my $fd = new FileHandle;
+
+    if ($file =~ /$exts/) {
+        open($fd, basename($file));
+        my $data = join('', <$fd>);
+        while ($data =~ s/_\("(.*?)"\)//s) {
+            $strings{"_(\"$1\")"}++;
+        }
+        close($fd);
+    }
+}
Index: tags/2.2.3/bin/init_codebase_tables.cli.php
===================================================================
--- tags/2.2.3/bin/init_codebase_tables.cli.php	(revision 632)
+++ tags/2.2.3/bin/init_codebase_tables.cli.php	(revision 632)
@@ -0,0 +1,198 @@
+#!/usr/bin/env php
+<?php
+/*
+* The Strangecode Codebase - a general application development framework for PHP
+* For details visit the project site: <http://trac.strangecode.com/>
+* Copyright Â© 2015 Strangecode, LLC
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/*
+* Initialize tables managed by Codebase classes.
+*
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  2.0
+* @since    05 Mar 2016 19:12:09
+*/
+
+/********************************************************************
+* MAIN
+********************************************************************/
+
+// Process command line options.
+$opt = getopt('fhc:');
+
+// Get name of class to initialize (or 'all' for all).
+$class = strtolower(end($_SERVER['argv']));
+
+// Give them a fighting chance. Show the help message.
+if ($_SERVER['argc'] <= 1 || isset($opt['h'])) {
+    help();
+    exit(1);
+}
+
+if (isset($opt['c']) && is_file($opt['c'])) {
+    require_once $opt['c'];
+    $app->logMsg(sprintf('Loaded custom config file: %s', $opt['c']), LOG_INFO, __FILE__, __LINE__);
+}
+require_once dirname(__FILE__) . '/_config.inc.php';
+
+if ('all' == $class) {
+    initClassDB('acl');
+    initClassDB('auth');
+    initClassDB('lock');
+    initClassDB('prefs');
+    initClassDB('version');
+    initClassDB('session');
+} else {
+    initClassDB($class);
+}
+
+$app->stop();
+die;
+
+/********************************************************************
+* FUNCTIONS
+********************************************************************/
+
+/*
+*
+*
+* @access   public
+* @param
+* @return
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    12 Jul 2015 23:16:02
+*/
+function help()
+{
+    global $cli_executed;
+    ?>
+This script initializes DB tables managed by Codebase classes. If database schema is changed a log message will display.
+
+Usage: <?php echo $cli_executed; ?> [OPTIONS] CLASSNAME
+
+OPTIONS
+
+    -c FILE Path to include custom site _config.inc.php file.
+    -f      Force recreation of tables, if they already exist. WARNING: this will delete existing records.
+    -h      Show this help message.
+
+CLASSNAME is one of:
+
+    all         Special case: create tables for all classes.
+    acl         Create tables for ACL
+    auth        Create tables for Auth_SQL
+    lock        Create tables for Lock
+    prefs       Create tables for Prefs
+    version     Create tables for Version
+    session     Create tables for DBSessionHandler
+<?php
+}
+
+/*
+*
+*
+* @access   public
+* @param
+* @return
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    12 Jul 2015 23:25:35
+*/
+function initClassDB($class)
+{
+    global $opt;
+    $app =& App::getInstance();
+    $db =& DB::getInstance();
+
+    $app->logMsg(sprintf('Running %s->initDB() on database %s', $class, $db->getParam('db_name')), LOG_INFO, __FILE__, __LINE__);
+
+    switch ($class) {
+        case 'acl':
+            // ACL!
+            require_once CODEBASE_PATH . '/lib/ACL.inc.php';
+            $acl =& ACL::getInstance();
+            $acl->setParam(array('create_table' => true));
+            $acl->initDB(isset($opt['f']));
+            break;
+
+        case 'auth':
+            // Auth_SQL!
+            require_once CODEBASE_PATH . '/lib/Auth_SQL.inc.php';
+            $auth = new Auth_SQL('codebase');
+            $auth->setParam(array('create_table' => true));
+            if (!isset($opt['c'])) {
+                // User didn't provide custom config. Use sane defaults.
+                $auth->setParam(array('abuse_detection' => true));
+            }
+            $auth->initDB(isset($opt['f']));
+            break;
+
+        case 'lock':
+            // Lock!
+            require_once CODEBASE_PATH . '/lib/Lock.inc.php';
+            require_once CODEBASE_PATH . '/lib/Auth_SQL.inc.php';
+            $lock =& Lock::getInstance(new Auth_SQL('codebase'));
+            $lock->setParam(array('create_table' => true));
+            $lock->initDB(isset($opt['f']));
+            break;
+
+        case 'prefs':
+            // Prefs!
+            require_once CODEBASE_PATH . '/lib/Prefs.inc.php';
+            $prefs = new Prefs('codebase');
+            $prefs->setParam(array('create_table' => true));
+            $prefs->initDB(isset($opt['f']));
+            break;
+
+        case 'version':
+            // Version!
+            require_once CODEBASE_PATH . '/lib/Version.inc.php';
+            require_once CODEBASE_PATH . '/lib/Auth_SQL.inc.php';
+            $version = Version::getInstance(new Auth_SQL('codebase'));
+            $version->setParam(array('create_table' => true));
+            $version->initDB(isset($opt['f']));
+            break;
+
+        case 'session':
+            // DBSessionHandler!
+
+            // We need to hack the app to allow sessions to run in a CLI.
+            $app->stop();
+            $app->cli = false;
+            $app->setParam(array('enable_session' => true));
+            $app->start();
+
+            if (!isset($opt['c'])) {
+                // User didn't provide custom config. Use sane defaults.
+                require_once CODEBASE_PATH . '/lib/DBSessionHandler.inc.php';
+                $db_session = new DBSessionHandler($app->db, array(
+                    'db_table' => 'session_tbl',
+                    'create_table' => true,
+                ));
+                $db_session->initDB(isset($opt['f']));
+            } else if (true === $app->getParam('enable_db_session_handler') && true === $app->getParam('enable_db') && isset($app->db_session)) {
+                // Only init if db_session is enabled in config.
+                $app->db_session->initDB(isset($opt['f']));
+            }
+            break;
+
+        default:
+            echo "The class $class is not setup to initClassDB().\n";
+            exit(1);
+    }
+}
Index: tags/2.2.3/bin/module_maker/form_template.cli.php
===================================================================
--- tags/2.2.3/bin/module_maker/form_template.cli.php	(revision 632)
+++ tags/2.2.3/bin/module_maker/form_template.cli.php	(revision 632)
@@ -0,0 +1,258 @@
+#!/usr/bin/env php
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * form_template.cli.php
+ */
+
+if ($_SERVER['argc'] > 1 && isset($_SERVER['argv'][1]) && '' != $_SERVER['argv'][1] && is_dir($_SERVER['argv'][1])) {
+    // First arg is path to current site. Realpath removes trailing /s
+    define('COMMON_BASE', realpath($_SERVER['argv'][1]));
+} else {
+    die("Error: First argument must be the directory path to an existing site (ex: /home/sc/www.strangecode.com).\n");
+}
+
+include_once dirname(__FILE__) . '/../_config.inc.php';
+
+// Test arguments.
+if (isset($_SERVER['argv'][2])) {
+    // Second arg is db table.
+    $db_tbl = $_SERVER['argv'][2];
+} else {
+    die(sprintf("Usage: %s site_directory db_table\n", basename($_SERVER['argv'][0])));
+}
+
+// Get DB tables.
+$qid = $db->query("SHOW TABLES");
+while (list($row) = mysql_fetch_row($qid)) {
+    $tables[] = $row;
+}
+
+// Make sure requested table is in database.
+if (!in_array($db_tbl, $tables)) {
+    die(sprintf("%s Warning: %s does not exist in database %s. Please select one of: \n\n%s\n\n", basename($_SERVER['argv'][0]), $db_tbl, $app->getParam('db_name'), join("\n", $tables)));
+}
+
+// Get DB table column info.
+$qid = $db->query("DESCRIBE " . $db->escapeString($db_tbl));
+while ($row = mysql_fetch_row($qid)) {
+    $cols[] = $row;
+}
+
+$exclude = array('added_by_user_id', 'added_datetime', 'hit_count', 'modified_datetime', 'modified_by_user_id');
+$primary_key = '__///__';
+$output = array();
+
+// Loop through columns
+if (is_array($cols) && !empty($cols)) {
+    foreach ($cols as $col) {
+
+        // Human readable.
+        $field = $col[0];
+        $title = ucfirst(str_replace('_', ' ', $field));
+        $type = preg_replace('/^(\w+).*$/', '\\1', $col[1]);
+        if ('PRI' == $col[3]) {
+            $primary_key = $field;
+        }
+
+        // Column types like this are usually single toggle checkboxes.
+        if (preg_match("/enum\('true'\)/", $col[1])) {
+            $type = 'toggle';
+        }
+
+        if (in_array($field, $exclude) || $primary_key == $field) {
+            // Don't add a field for this column.
+            continue;
+        }
+
+        // Select menu from the column of a related database table.
+        if (preg_match('/.*_id$/i', $field)) {
+            $output[$field] = <<<E_O_F
+
+<div class="sc-form-row<\x3fphp \$fv->err('$field'); \x3f>">
+    <label for="$field"><\x3fphp echo _("$title"); \x3f></label>
+    <select name="$field" id="$field" class="sc-small"><\x3fphp printSelectForm('__///___tbl', "CONCAT(__///___id, '&mdash;', __///__)", '$field', \$frm['$field'], true, 'ORDER BY $field ASC'); \x3f></select>
+    <a href="<\x3fphp echo \$app->oHREF('/admin/__///__.php?op=add&boomerang=true'); \x3f>" class="sc-warn sc-add-link"><img src="/admin/i/plus.gif" width="10" height="10" alt="[+]" /></a>
+    <a href="<\x3fphp echo \$app->oHREF('/admin/__///__.php?op=edit&boomerang=true&__///___id=' . \$frm['__///___id']); \x3f>" class="sc-warn sc-edit-link"><img src="/admin/i/pen.gif" width="12" height="12" alt="Edit" /></a>
+</div>
+E_O_F;
+            continue;
+        }
+
+        // File upload.
+        if (preg_match('/file|image/i', $field)) {
+            $output[$field] = <<<E_O_F
+
+<div class="sc-form-row<\x3fphp \$fv->err('$field'); \x3f>">
+    <label for="$field"><\x3fphp echo _("$title"); \x3f></label>
+    <input type="file" name="$field" id="$field" />
+    <\x3fphp if ('' != \$upload->getFilenameGlob(getFormData('$primary_key') . '_*') && (getFormData('op') == 'edit' || getFormData('op') == 'update')) { \x3f>
+        <div class="sc-help"><\x3fphp printf(_("The current file <a href=\"%s/%2\\\$s\"><strong>%2\\\$s</strong></a> will be deleted if a new file is selected for upload."), '/_db_files/__///__', \$upload->getFilenameGlob(getFormData('$primary_key') . '_*')) \x3f></div>
+    <\x3fphp } \x3f>
+    <div class="sc-help"><\x3fphp printf(_("Allowed file types: %s."), join(', ', \$upload->getParam('valid_file_extensions'))) \x3f></div>
+</div>
+E_O_F;
+            continue;
+        }
+
+        // Password field.
+        if (preg_match('/pass/i', $field)) {
+            $output[$field] = <<<E_O_F
+
+<div class="sc-form-row<\x3fphp \$fv->err('$field'); \x3f>">
+    <label for="$field"><\x3fphp echo _("$title"); \x3f></label>
+    <input type="password" name="$field" id="$field" class="sc-medium" value="<\x3fphp echo oTxt(\$frm['$field']); \x3f>" />
+</div>
+E_O_F;
+            continue;
+        }
+
+        switch ($type) {
+
+        // Select menu (or radio buttons)
+        case 'enum' :
+            $output[$field] = <<<E_O_F
+
+<div class="sc-form-row<\x3fphp \$fv->err('$field'); \x3f>">
+    <label for="$field"><\x3fphp echo _("$title"); \x3f></label>
+    <select name="$field" id="$field"><\x3fphp printSetSelectForm('$db_tbl', '$field', \$frm['$field'], true); \x3f></select>
+</div>
+E_O_F;
+            break;
+
+        // Set checkboxes
+        case 'set' :
+            $output[$field] = <<<E_O_F
+
+<div class="sc-form-row<\x3fphp \$fv->err('$field'); \x3f>">
+    <label for="$field"><\x3fphp echo _("$title"); \x3f></label>
+    <\x3fphp printSetCheckboxes('$db_tbl', '$field', \$frm['$field'], 1) \x3f>
+</div>
+E_O_F;
+            break;
+
+        // Single checkbox
+        case 'toggle' :
+            $output[$field] = <<<E_O_F
+
+<div class="sc-form-row<\x3fphp \$fv->err('$field'); \x3f>">
+    <label for="$field"><\x3fphp echo _("$title"); \x3f></label>
+    <label><input type="checkbox" name="$field" id="$field" <\x3fphp frmChecked(!empty(\$frm['$field'])) \x3f> /><\x3fphp echo _("Check this box to $title"); \x3f></label>
+</div>
+E_O_F;
+            break;
+
+        // Textarea small
+        case 'tinytiny' :
+        case 'text' :
+        case 'tinyblob' :
+        case 'blob' :
+            $output[$field] = <<<E_O_F
+
+<div class="sc-form-row<\x3fphp \$fv->err('$field'); \x3f>">
+    <label for="$field"><\x3fphp echo _("$title"); \x3f></label>
+    <textarea name="$field" id="$field" rows="8" cols="40" class="sc-tall sc-medium"><\x3fphp echo oTxt(\$frm['$field']); \x3f></textarea>
+</div>
+E_O_F;
+            break;
+
+        // Textarea big
+        case 'mediumtext' :
+        case 'longtext' :
+        case 'mediumblob' :
+        case 'longblob' :
+            $output[$field] = <<<E_O_F
+
+<div class="sc-form-row<\x3fphp \$fv->err('$field'); \x3f>">
+    <label for="$field"><\x3fphp echo _("$title"); \x3f></label>
+    <textarea name="$field" id="$field" rows="8" cols="40" class="sc-fullscreen sc-medium"><\x3fphp echo oTxt(\$frm['$field']); \x3f></textarea>
+</div>
+E_O_F;
+            break;
+
+        // Number
+        case 'tinyint' :
+        case 'bit' :
+        case 'bool' :
+        case 'smallint' :
+        case 'mediumint' :
+        case 'int' :
+        case 'integer' :
+        case 'bigint' :
+
+        case 'float' :
+        case 'float' :
+        case 'double' :
+        case 'double' :
+        case 'real' :
+        case 'decimal' :
+        case 'dec' :
+        case 'numeric' :
+        default :
+            $output[$field] = <<<E_O_F
+
+<div class="sc-form-row<\x3fphp \$fv->err('$field'); \x3f>">
+    <label for="$field"><\x3fphp echo _("$title"); \x3f></label>
+    <input type="text" name="$field" id="$field" class="sc-tiny" value="<\x3fphp echo oTxt(\$frm['$field']); \x3f>" />
+</div>
+E_O_F;
+            break;
+
+        // Date
+        case 'date' :
+        case 'datetime' :
+        case 'timestamp' :
+        case 'time' :
+        case 'year' :
+        default :
+            $output[$field] = <<<E_O_F
+
+<div class="sc-form-row<\x3fphp \$fv->err('$field'); \x3f>">
+    <label for="$field"><\x3fphp echo _("$title"); \x3f></label>
+    <input type="text" name="$field" id="$field" class="sc-small" value="<\x3fphp echo oTxt(\$frm['$field']); \x3f>" />
+</div>
+E_O_F;
+            break;
+
+        // Text
+        case 'char' :
+        case 'varchar' :
+        case 'tinytext' :
+        case 'tinyblob' :
+        default :
+            $output[$field] = <<<E_O_F
+
+<div class="sc-form-row<\x3fphp \$fv->err('$field'); \x3f>">
+    <label for="$field"><\x3fphp echo _("$title"); \x3f></label>
+    <input type="text" name="$field" id="$field" class="sc-medium" value="<\x3fphp echo oTxt(\$frm['$field']); \x3f>" />
+</div>
+E_O_F;
+            break;
+        }
+    }
+} else {
+    die(basename($_SERVER['argv'][0]) . " Warning: $db_tbl does not have any columns.\n");
+}
+
+
+echo join("\n", $output);
Index: tags/2.2.3/bin/module_maker/list_template.cli.php
===================================================================
--- tags/2.2.3/bin/module_maker/list_template.cli.php	(revision 632)
+++ tags/2.2.3/bin/module_maker/list_template.cli.php	(revision 632)
@@ -0,0 +1,221 @@
+#!/usr/bin/env php
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * list_template.cli.php
+ */
+
+if ($_SERVER['argc'] > 1 && isset($_SERVER['argv'][1]) && '' != $_SERVER['argv'][1] && is_dir($_SERVER['argv'][1])) {
+    // First arg is path to current site. Realpath removes trailing /s
+    define('COMMON_BASE', realpath($_SERVER['argv'][1]));
+} else {
+    die("Error: First argument must be the directory path to an existing site (ex: /home/sc/www.strangecode.com).\n");
+}
+
+include_once dirname(__FILE__) . '/../_config.inc.php';
+
+$op = null;
+$valid_ops = array('headerrows', 'listrows');
+
+// Test arguments.
+if (isset($_SERVER['argv'][2])) {
+    // Second arg is db table.
+    $db_tbl = $_SERVER['argv'][2];
+} else {
+    die(sprintf("Usage: %s site_directory db_table [operation]\nValid operations include: %s\n", basename($_SERVER['argv'][0]), join(', ', $valid_ops)));
+}
+
+// Test for operation.
+if (isset($_SERVER['argv'][3])) {
+    // Optional third arg is op.
+    $op = $_SERVER['argv'][3];
+    // Make sure op is valid.
+    if (!in_array($op, $valid_ops)) {
+        die(basename($_SERVER['argv'][0]) . " Warning: Operation '$op' is not something I know how to do Please select one of: " . join(", ", $valid_ops) . "\n");
+    }
+}
+
+// Get DB tables.
+$qid = $db->query("SHOW TABLES");
+while (list($row) = mysql_fetch_row($qid)) {
+    $tables[] = $row;
+}
+
+// Make sure requested table is in database.
+if (!in_array($db_tbl, $tables)) {
+    die(sprintf("%s Warning: %s does not exist in database %s. Please select one of: \n\n%s\n\n", basename($_SERVER['argv'][0]), $db_tbl, $app->getParam('db_name'), join("\n", $tables)));
+}
+
+// Get DB table column info.
+$qid = $db->query("DESCRIBE " . $db->escapeString($db_tbl));
+while ($row = mysql_fetch_row($qid)) {
+    $cols[] = $row;
+}
+
+$primary_key = '__///__';
+
+// Loop through columns
+if (is_array($cols) && !empty($cols)) {
+    foreach ($cols as $col) {
+
+        // Human readable.
+        $field = $col[0];
+        $type = preg_replace('/^(\w+).*$/', '\\1', $col[1]);
+        $title = ucfirst(str_replace('_', ' ', $field));
+
+        switch ($field) {
+        case 'added_by_user_id' :
+        case 'modified_by_user_id' :
+            $title = "by";
+            break;
+
+        case 'added_datetime' :
+        case 'modified_datetime' :
+            $title = str_replace(' datetime', '', $title);
+            break;
+        }
+
+        // Get primary key.
+        if ('PRI' == $col[3]) {
+            $primary_key = $field;
+        }
+
+        // Column headers.
+        $headers[$field] = $title;
+
+        // Column data.
+        if (preg_match("/enum\('true'\)/", $col[1])) {
+            $listrows[] = "<\x3fphp echo (!empty(\$list[\$i]['$field'])) ? '&bull;' : ''; \x3f>";
+        } else if (
+            'tinytext' == $type ||
+            'text' == $type ||
+            'mediumtext' == $type ||
+            'longtext' == $type ||
+            'tinyblob' == $type ||
+            'blob' == $type ||
+            'mediumblob' == $type ||
+            'longblob' == $type
+        ) {
+            $listrows[] = "<\x3fphp echo mb_strlen(\$list[\$i]['$field'])<50 \x3f oTxt(\$list[\$i]['$field'], true) : oTxt(trim(mb_substr(\$list[\$i]['$field'], 0, 50)) . '...'); \x3f>";
+        } else if (preg_match('/.*(begin|start).*date.*/i', $field)) {
+            $listrows[] = "<\x3fphp echo \$db->getParam('zero_date') == \$list[\$i]['$field'] ? '' : date(\$app->getParam('date_format'), strtotime(\$list[\$i]['$field'])); \x3f>";
+        } else if (preg_match('/.*(end|expire).*date.*/i', $field)) {
+            $listrows[] = "<\x3fphp echo '9999-12-31' == \$list[\$i]['$field'] ? '' : date(\$app->getParam('date_format'), strtotime(\$list[\$i]['$field'])); \x3f>";
+        } else if (preg_match('/datetime/i', $type)) {
+            $listrows[] = "<\x3fphp echo Validator::validateStrDate(\$list[\$i]['$field']) ? date(\$app->getParam('date_format'), strtotime(\$list[\$i]['$field'])) : ''; \x3f>";
+        } else if (preg_match('/date/i', $type)) {
+            $listrows[] = "<\x3fphp echo Validator::validateStrDate(\$list[\$i]['$field']) ? date(\$app->getParam('date_format'), strtotime(\$list[\$i]['$field'])) : ''; \x3f>";
+        } else if (preg_match('/(amount|_rate)/i', $field)) {
+            $listrows[] = "<\x3fphp printf('$%01.2f', \$list[\$i]['$field']); \x3f>";
+        } else if (preg_match('/(added_by_user_id)/i', $field)) {
+            $listrows[] = "<\x3fphp echo oTxt(\$list[\$i]['added_by_username']); \x3f>";
+        } else if (preg_match('/(modified_by_user_id)/i', $field)) {
+            $listrows[] = "<\x3fphp echo oTxt(\$list[\$i]['modified_by_username']); \x3f>";
+        } else if ('rank' == $field) {
+            $listrows[] = "<input type=\"text\" name=\"rank[<\x3fphp echo \$list[\$i]['$primary_key']; \x3f>]\" value=\"<\x3fphp echo \$list[\$i]['rank']; \x3f>\" size=\"5\" />";
+        } else {
+            $listrows[] = "<\x3fphp echo oTxt(\$list[\$i]['$field'], true); \x3f>";
+        }
+    }
+} else {
+    die(basename($_SERVER['argv'][0]) . " Warning: $db_tbl does not have any columns.\n");
+}
+
+// Print the template out.
+echo isset($op) ? '' : <<<E_O_F
+
+<\x3fphp \$fv->printErrorMessages(); \x3f>
+
+<div class="commandbox">
+<form action="<\x3fphp echo oTxt(\$_SERVER['PHP_SELF']); \x3f>" method="get">
+<\x3fphp \$app->printHiddenSession(false); \x3f>
+    <span class="sc-nowrap commandtext"><a href="<\x3fphp echo \$app->oHREF(\$_SERVER['PHP_SELF'] . '?op=add'); \x3f>"><\x3fphp echo _("Add __///__"); \x3f></a></span>
+    <br />
+
+    <input type="text" class="sc-small" size="20" name="search_query" value="<\x3fphp echo getFormData('search_query'); \x3f>" title="<\x3fphp echo oTxt(_("Fields searched: __///__.")); \x3f>" />
+    <select name="filter___///__">
+        <\x3fphp // printSelectForm('__///___tbl', "CONCAT(__///___id, '&mdash;', city, '&mdash;', title)", '__///___id', getFormData('filter___///__'), array('' => 'Any __///__'), 'ORDER BY __///__ ASC'); \x3f>
+    </select>
+    <input type="submit" name="list" value="<\x3fphp echo _("Search"); \x3f>" />
+</form>
+</div>
+
+<?php include 'list_info.ihtml'; \x3f>
+
+<form action="<\x3fphp echo oTxt(\$_SERVER['PHP_SELF']); \x3f>" method="post">
+<table class="list">
+    <tr>
+        <th>&nbsp;</th>
+        <th>&nbsp;</th>
+
+E_O_F;
+
+
+// Print header rows.
+if (!isset($op) || 'headerrows' == $op) {
+    foreach ($headers as $field=>$title) {
+        if (preg_match('/(_date|_datetime|_time|price|value|quantity)/', $field)) {
+            $ordering = 'DESC';
+        } else {
+            $ordering = 'ASC';
+        }
+
+        if ($field == $primary_key) {
+            echo "        <th><\x3fphp echo \$so->printSortHeader('$db_tbl.$field', _(\"ID\"), 'DESC'); \x3f></th>\n";
+        } else {
+            echo "        <th><\x3fphp echo \$so->printSortHeader('$db_tbl.$field', _(\"$title\"), '$ordering'); \x3f></th>\n";
+        }
+    }
+}
+
+echo isset($op) ? '' : <<<E_O_F
+        <th>&nbsp;</th>
+    </tr>
+    <\x3fphp for (\$i = 0; \$i <= \$page->last_item - \$page->first_item && \$page->total_items > 0; \$i++) { \x3f>
+    <tr>
+        <td class="sc-nowrap"><a title="<\x3fphp printf(_("Edit %s"), oTxt(\$list[\$i]['__///__'])) \x3f>" href="<\x3fphp echo \$app->oHREF(\$_SERVER['PHP_SELF'] . '?op=edit&$primary_key=' . \$list[\$i]['$primary_key']); \x3f>"><img src="/admin/i/pen.gif" alt="Edit" width="12" height="12" border="0" /></a> &nbsp;</td>
+        <td class="sc-nowrap"><a title="<\x3fphp printf(_("Versions of %s"), oTxt(\$list[\$i]['__///__'])) \x3f>" href="<\x3fphp echo \$app->oHREF("/admin/versions.php?record_table=$db_tbl&record_key=$primary_key&boomerang=true&record_val=" . \$list[\$i]['$primary_key']); \x3f>"><img src="/admin/i/multiple.png" alt="" width="12" height="12" border="0" /></a> &nbsp;</td>
+
+E_O_F;
+
+// Print List rows.
+if (!isset($op) || 'listrows' == $op) {
+    foreach ($listrows as $col_data) {
+?>
+        <td class="sc-nowrap"><?php echo $col_data; ?> &nbsp;</td>
+<?php
+    }
+}
+
+echo isset($op) ? '' : <<<E_O_F
+        <td class="sc-nowrap" align="right"><a title="<\x3fphp printf(_("Delete %s"), oTxt(\$list[\$i]['__///__'])) \x3f>" href="<\x3fphp echo \$app->oHREF(\$_SERVER['PHP_SELF'] . "?op=del&$primary_key=" . \$list[\$i]['$primary_key']); \x3f>" onclick="javascript:return confirm('<\x3fphp printf(_("Are you sure you want to delete the record %s? This action is permanent and cannot be undone."), oTxt(\$list[\$i]['__///__'])) \x3f>');"><img src="/admin/i/trash.gif" alt="Delete" width="10" height="10" border="0" /></a> &nbsp;</td>
+    </tr>
+    <\x3fphp } \x3f>
+</table>
+
+<\x3fphp if (\$page->total_pages > 1) { \x3f>
+<div class="sc-nowrap commandtext" style="float: right;"><\x3fphp echo _("Pages:"); \x3f>&nbsp;<\x3fphp \$page->printPageNumbers() \x3f></div>
+<\x3fphp } \x3f>
+</form>
+
+E_O_F;
Index: tags/2.2.3/bin/module_maker/module.cli.php
===================================================================
--- tags/2.2.3/bin/module_maker/module.cli.php	(revision 632)
+++ tags/2.2.3/bin/module_maker/module.cli.php	(revision 632)
@@ -0,0 +1,612 @@
+#!/usr/bin/env php
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * module.cli.php
+ */
+
+if ($_SERVER['argc'] > 1 && isset($_SERVER['argv'][1]) && '' != $_SERVER['argv'][1] && is_dir($_SERVER['argv'][1])) {
+    // First arg is path to current site. Realpath removes trailing /s
+    define('COMMON_BASE', realpath($_SERVER['argv'][1]));
+} else {
+    die("Error: First argument must be the directory path to an existing site (ex: /home/sc/www.strangecode.com).\n");
+}
+
+include_once dirname(__FILE__) . '/../_config.inc.php';
+
+$op = null;
+$valid_ops = array('clean', 'var', 'test', 'admin', 'public');
+
+// Test for arguments.
+if (isset($_SERVER['argv'][2]) && isset($_SERVER['argv'][3])) {
+    $module_name_singular = $_SERVER['argv'][2];
+    $module_name_plural = $_SERVER['argv'][3];
+} else {
+    die(basename($_SERVER['argv'][0]) . " error: missing arguments.
+
+Usage: " . basename($_SERVER['argv'][0]) . " SITE_DIRECTORY NAME_SINGULAR NAME_PLURAL OPERATION
+
+Example: export DB_NAME=mydb; " . basename($_SERVER['argv'][0]) . " . order orders admin
+
+Valid operations include: " . join(', ', $valid_ops) . "
+
+The following files will be generated by this script:
+
+âadminâ operation:
+SITE_DIRECTORY/admin/nameplural.php
+SITE_DIRECTORY/admin/_templates/namesingular_list.ihtml
+SITE_DIRECTORY/admin/_templates/namesingular_form.ihtml
+
+âpublicâ operation:
+SITE_DIRECTORY/html/nameplural.php
+SITE_DIRECTORY/html/_templates/namesingular_list.ihtml
+SITE_DIRECTORY/html/_templates/namesingular.ihtml
+
+");
+}
+
+// Test for valid operation.
+$op = $_SERVER['argv'][4];
+// Make sure op is valid.
+if (!in_array($op, $valid_ops)) {
+    die(basename($_SERVER['argv'][0]) . " Warning: Operation '$op' is not something I know how to do Please select one of: " . join(", ", $valid_ops) . "\n");
+}
+
+switch ($op) {
+case 'test' :
+    echo "RUNNING MODULE MAKER IN TEST MODE.\n\n";
+    break;
+case 'admin' :
+case 'public' :
+    echo "Generating only $op filesâŠ\n\n";
+    break;
+}
+
+
+/********************************************************************
+* CONFIG
+********************************************************************/
+
+// Where deleted files go:
+$user_trash_folder = $_SERVER['HOME'] . '/.Trash';
+
+// Directories
+$skel_dir = realpath(dirname(__FILE__) . '/skel');
+$admin_dir = realpath(COMMON_BASE . '/admin');
+$admin_tpl_dir = realpath(COMMON_BASE . '/admin/_templates');
+$public_dir = realpath(COMMON_BASE . '/html');
+$public_tpl_dir = realpath(COMMON_BASE . '/html/_templates');
+
+// Names
+$module_title = ucwords(str_replace('_', ' ', $module_name_plural));
+$item_title = ucwords(str_replace('_', ' ', $module_name_singular));
+$module_name_upper = mb_strtoupper(str_replace('_', ' ', $module_name_plural));
+
+// Admin files
+$admin_script = $module_name_plural . '.php';
+$admin_list_template = $module_name_singular . '_list.ihtml';
+$admin_form_template = $module_name_singular . '_form.ihtml';
+
+// Public files
+$public_script = $module_name_plural . '.php';
+$public_list_template = $module_name_singular . '_list.ihtml';
+$public_detail_template = $module_name_singular . '_view.ihtml';
+
+// Database names
+$db_tbl = $module_name_singular . '_tbl';
+$primary_key = $module_name_singular . '_id';
+
+// Only after we've defined essential files can we clean.
+if ('clean' == $op) {
+    echo "Beginning file cleanup\n";
+    trashFile("$admin_dir/$admin_script");
+    trashFile("$admin_tpl_dir/$admin_list_template");
+    trashFile("$admin_tpl_dir/$admin_form_template");
+    trashFile("$public_dir/$public_script");
+    trashFile("$public_tpl_dir/$public_list_template");
+    trashFile("$public_tpl_dir/$public_detail_template");
+    echo "End file cleanup\n";
+    die;
+}
+
+// Go!
+echo 'Running Module Maker. Using database: ' . $app->getParam('db_name') . "\n";
+
+
+/********************************************************************
+* PREPROCESSING
+********************************************************************/
+
+// Ensure skel files exist.
+if ('admin' == $op && (!file_exists("$skel_dir/admin.php") || !file_exists("$skel_dir/adm_list.ihtml") || !file_exists("$skel_dir/adm_form.ihtml"))) {
+    die(basename($_SERVER['argv'][0]) . " Warning: one or more admin skeleton source files missing. Please check directory: $skel_dir.\n");
+}
+if ('public' == $op && (!file_exists("$skel_dir/public.php") || !file_exists("$skel_dir/public.ihtml") || !file_exists("$skel_dir/public_list.ihtml"))) {
+    die(basename($_SERVER['argv'][0]) . " Warning: one or more public skeleton source files missing. Please check directory: $skel_dir.\n");
+}
+
+if (!isset($_SERVER['argv'][4]) || 'var' != $_SERVER['argv'][4]) {
+    // Ensure essential directories exist, except for 'var' op.
+    if ('admin' == $op && !is_dir("$admin_dir")) {
+        die(basename($_SERVER['argv'][0]) . " Error: admin_dir '$admin_dir' directory not found.\n");
+    }
+    if ('admin' == $op && !is_dir("$admin_tpl_dir")) {
+        die(basename($_SERVER['argv'][0]) . " Error: admin_tpl_dir '$admin_tpl_dir' directory not found.\n");
+    }
+    if ('public' == $op && !is_dir("$public_dir")) {
+        die(basename($_SERVER['argv'][0]) . " Error: public_dir '$public_dir' directory not found.\n");
+    }
+    if ('public' == $op && !is_dir("$public_tpl_dir")) {
+        die(basename($_SERVER['argv'][0]) . " Error: public_tpl_dir '$public_tpl_dir' directory not found.\n");
+    }
+}
+
+// Get DB tables.
+$qid = $db->query("SHOW TABLES");
+while (list($row) = mysql_fetch_row($qid)) {
+    $tables[] = $row;
+}
+
+// Make sure requested table is in database.
+if (!isset($tables)) {
+    die(sprintf("%s Warning: %s does not exist in database %s. No DB tables found!", basename($_SERVER['argv'][0]), $db_tbl, $app->getParam('db_name')));
+}
+if (!in_array($db_tbl, $tables)) {
+    die(sprintf("%s Warning: %s does not exist in database %s. Please select one of: \n\n%s\n\n", basename($_SERVER['argv'][0]), $db_tbl, $app->getParam('db_name'), join("\n", $tables)));
+}
+
+// Ensure requested table contains columns.
+// Get DB table column info.
+$qid = $db->query("DESCRIBE " . $db->escapeString($db_tbl));
+while ($row = mysql_fetch_row($qid)) {
+    $cols[] = $row;
+}
+if (!is_array($cols) || empty($cols)) {
+    die(basename($_SERVER['argv'][0]) . " Warning: $db_tbl does not have any columns.\n");
+}
+
+
+/******************************************************************************
+ * SETUP VARIABLES
+ *****************************************************************************/
+
+
+// Loop through columns
+$upload_file_capability = false;
+$headers = array();
+$public_list_page_vars = array();
+$public_detail_page_vars = array();
+$set_values_default = array();
+if (is_array($cols) && !empty($cols)) {
+    foreach ($cols as $col) {
+
+        // Human readable.
+        $field = $col[0];
+        $field_title = ucfirst(str_replace('_', ' ', $field));
+        $type = preg_replace('/^(\w+).*$/', '\\1', $col[1]);
+        $default = $col[4];
+
+        if (mb_strpos($default, '0000') !== false || '0' == $default) {
+            $default = '';
+        }
+
+        // Get primary key.
+//         if ('PRI' == $col[3]) {
+//             $primary_key = $field;
+//         }
+
+        // Our form will require type="multipart/form-data".
+        if (preg_match('/file|image/i', $field)) {
+            $upload_file_capability = true;
+        }
+
+        // Column headers.
+        $headers[$field] = $field_title;
+
+        // Get php code for printing variables.
+        $public_list_page_vars[] = "<\x3fphp echo oTxt(\$" . $module_name_singular . "_list[\$i]['$field']); \x3f>";
+        $public_detail_page_vars[] = "<\x3fphp echo oTxt(\$item['$field']); \x3f>";
+        $set_values_default[] = "'$field' => '$default'";
+    }
+}
+
+// -------------FILES-------------
+
+$skel_files = array();
+$skel_files['skel_admin_script'] = file_get_contents($skel_dir . '/admin.php');
+$skel_files['skel_admin_list_template'] = file_get_contents($skel_dir . '/adm_list.ihtml');
+$skel_files['skel_admin_form_template'] = file_get_contents($skel_dir . '/adm_form.ihtml');
+$skel_files['skel_public_script'] = file_get_contents($skel_dir . '/public.php');
+$skel_files['skel_public_list_template'] = file_get_contents($skel_dir . '/public_list.ihtml');
+$skel_files['skel_public_detail_template'] = file_get_contents($skel_dir . '/public.ihtml');
+
+
+// Search-replace variables ----------------------------------
+
+// Admin script file upload replacement routines...
+
+$search['admin_form_tag_init'] = '/%ADMIN_FORM_TAG_INIT%/';
+$replace['admin_form_tag_init'] = "<form method=\"post\" action=\"<\x3fphp echo oTxt(\$_SERVER['PHP_SELF']); \x3f>\" class=\"sc-form\">";
+$search['admin_upload_include'] = '/%ADMIN_UPLOAD_INCLUDE%/';
+$replace['admin_upload_include'] = '';
+$search['admin_upload_config'] = '/%ADMIN_UPLOAD_CONFIG%/';
+$replace['admin_upload_config'] = '';
+$search['admin_upload_init'] = '/%ADMIN_UPLOAD_INIT%/';
+$replace['admin_upload_init'] = '';
+$search['admin_upload_del'] = '/%ADMIN_UPLOAD_DEL%/';
+$replace['admin_upload_del'] = '';
+$search['admin_upload_insert'] = '/%ADMIN_UPLOAD_INSERT%/';
+$replace['admin_upload_insert'] = '';
+$search['admin_upload_update'] = '/%ADMIN_UPLOAD_UPDATE%/';
+$replace['admin_upload_update'] = '';
+
+if ($upload_file_capability) {
+    // Form arguments
+    $replace['admin_form_tag_init'] = "<form enctype=\"multipart/form-data\" method=\"post\" action=\"<\x3fphp echo oTxt(\$_SERVER['PHP_SELF']); \x3f>\" class=\"sc-form\">\n<input type=\"hidden\" name=\"MAX_FILE_SIZE\" value=\"<\x3fphp echo intval(ini_get('upload_max_filesize')) * 1024 * 1024; \x3f>\" />";
+
+    // Include statement.
+    $replace['admin_upload_include'] = "require_once 'codebase/lib/Upload.inc.php';\n";
+
+    // Config
+    $replace['admin_upload_config'] = <<<E_O_F
+
+// This module has file upload capability.
+\$upload = new Upload();
+\$upload->setParam(array(
+    'upload_path' => COMMON_BASE . '/html/_db_files/__///__',
+    'dest_file_perms' => 0666,
+    'allow_overwriting' => false,
+    'valid_file_extensions' => array('jpg', 'gif', 'png', 'jpeg'),
+));
+
+E_O_F;
+
+    // Main init.
+    $replace['admin_upload_init'] = <<<E_O_F
+
+// Copy uploaded image name into form data.
+\$_POST['__///__'] = isset(\$_FILES['__///__']) ? \$_FILES['__///__']['name'] : '';
+
+
+E_O_F;
+
+    // Delete.
+    $replace['admin_upload_del'] = <<<E_O_F
+
+    // Delete file.
+    if ('' != \$upload->getFilenameGlob(getFormData('%PRIMARY_KEY%') . '_*')) {
+        \$upload->deleteFile(\$upload->getFilenameGlob(getFormData('%PRIMARY_KEY%') . '_*'));
+    }
+E_O_F;
+
+    // Insert 1.
+    $replace['admin_upload_insert'] = <<<E_O_F
+
+        // Upload files with prepended primary key.
+        \$new_file = \$upload->process('__///__',  \$%PRIMARY_KEY% . '_' . getFormData('__///__'));
+
+        // If file upload errors, redirect to edit operation for the inserted record.
+        if (\$upload->anyErrors() || false === \$new_file) {
+            \$app->dieURL(\$_SERVER['PHP_SELF'] . '?op=edit&%PRIMARY_KEY%=' . \$%PRIMARY_KEY%);
+        }
+E_O_F;
+
+    // Update.
+    $replace['admin_upload_update'] = <<<E_O_F
+
+        // Upload new files.
+        if (getFormData('__///__')) {
+            // Get old file names for deletion.
+            \$old_file = \$upload->getFilenameGlob(getFormData('%PRIMARY_KEY%') . '_*');
+            // Process new file upload with prepended primary key.
+            \$new_file = \$upload->process('__///__',  getFormData('%PRIMARY_KEY%') . '_' . getFormData('__///__'));
+            if (false === \$new_file || \$upload->anyErrors()) {
+                // Upload failed. Reload form. Display errors.
+                \$frm =& editRecordForm(getFormData('%PRIMARY_KEY%'));
+                \$frm = array_merge(\$frm, getFormData());
+                \$nav->add(_("Edit %ITEM_TITLE%"));
+                \$main_template = '%ADMIN_FORM_TEMPLATE%';
+                break;
+            } else {
+                // Upload succeeded. Delete old files.
+                if ('' != \$old_file && \$old_file != \$new_file[0]['name']) {
+                    \$upload->deleteFile(\$old_file);
+                }
+            }
+        }
+E_O_F;
+} // End upload_file_capability.
+
+
+// Simple...
+
+$search['date'] = '/%DATE%/';
+$replace['date'] = date($app->getParam('date_format'));
+
+$search['name_plural'] = '/%NAME_PLURAL%/';
+$replace['name_plural'] = $module_name_plural;
+
+$search['name_singular'] = '/%NAME_SINGULAR%/';
+$replace['name_singular'] = $module_name_singular;
+
+$search['title'] = '/%TITLE%/';
+$replace['title'] = $module_title;
+
+$search['item_title'] = '/%ITEM_TITLE%/';
+$replace['item_title'] = $item_title;
+
+$search['primary_key'] = '/%PRIMARY_KEY%/';
+$replace['primary_key'] = $primary_key;
+
+$search['db_tbl'] = '/%DB_TBL%/';
+$replace['db_tbl'] = $db_tbl;
+
+$search['name_upper'] = '/%NAME_UPPER%/';
+$replace['name_upper'] = $module_name_upper;
+
+$search['admin_script'] = '/%ADMIN_SCRIPT%/';
+$replace['admin_script'] = $admin_script;
+
+$search['admin_list_template'] = '/%ADMIN_LIST_TEMPLATE%/';
+$replace['admin_list_template'] = $admin_list_template;
+
+$search['admin_form_template'] = '/%ADMIN_FORM_TEMPLATE%/';
+$replace['admin_form_template'] = $admin_form_template;
+
+$search['public_script'] = '/%PUBLIC_SCRIPT%/';
+$replace['public_script'] = $public_script;
+
+$search['public_list_template'] = '/%PUBLIC_LIST_TEMPLATE%/';
+$replace['public_list_template'] = $public_list_template;
+
+$search['public_detail_template'] = '/%PUBLIC_DETAIL_TEMPLATE%/';
+$replace['public_detail_template'] = $public_detail_template;
+
+$search['public_detail_page_vars'] = '/%PUBLIC_DETAIL_PAGE_VARS%/';
+$replace['public_detail_page_vars'] = join("\n", $public_detail_page_vars);
+
+$search['public_list_page_vars'] = '/%PUBLIC_LIST_PAGE_VARS%/';
+$replace['public_list_page_vars'] = join("\n", $public_list_page_vars);
+
+$search['set_values_default'] = '/%SET_VALUES_DEFAULT%/';
+$replace['set_values_default'] = join(",\n        ", $set_values_default);
+
+$search['search_fields'] = '/%SEARCH_FIELDS%/';
+$replace['search_fields'] = join(", ", $headers);
+
+
+
+// Complex....
+
+echo "Generating admin form table rows...\n";
+$output = array();
+exec(dirname($_SERVER['argv'][0]) . "/form_template.cli.php " . COMMON_BASE . " $db_tbl", $output, $return_val);
+if ($return_val == 0) {
+    $search['adm_form_table_rows'] = '/%ADM_FORM_TABLE_ROWS%/';
+    $replace['adm_form_table_rows'] = join("\n", $output);
+} else {
+    die(basename($_SERVER['argv'][0]) . " Error: could not execute form_template.cli.php.\n");
+}
+
+echo "Generating admin list table header rows...\n";
+$output = array();
+exec(dirname($_SERVER['argv'][0]) . "/list_template.cli.php " . COMMON_BASE . " $db_tbl headerrows", $output, $return_val);
+if ($return_val == 0) {
+    $search['adm_list_header_rows'] = '/%ADM_LIST_HEADER_ROWS%/';
+    $replace['adm_list_header_rows'] = join("\n", $output);
+} else {
+    die(basename($_SERVER['argv'][0]) . " Error: could not execute list_template.cli.php.\n");
+}
+
+echo "Generating admin list table rows...\n";
+$output = array();
+exec(dirname($_SERVER['argv'][0]) . "/list_template.cli.php " . COMMON_BASE . " $db_tbl listrows", $output, $return_val);
+if ($return_val == 0) {
+    $search['adm_list_rows'] = '/%ADM_LIST_ROWS%/';
+    $replace['adm_list_rows'] = join("\n", $output);
+} else {
+    die(basename($_SERVER['argv'][0]) . " Error: could not execute list_template.cli.php.\n");
+}
+
+echo "Generating sortorder...\n";
+$output = array();
+exec(dirname($_SERVER['argv'][0]) . "/sql.cli.php " . COMMON_BASE . " $db_tbl sortorder", $output, $return_val);
+if ($return_val == 0) {
+    $search['sort_order'] = '/%SORT_ORDER%/';
+    $replace['sort_order'] = join("\n", $output);
+} else {
+    die(basename($_SERVER['argv'][0]) . " Error: could not execute sql.cli.php.\n");
+}
+
+echo "Generating insert data...\n";
+$output = array();
+exec(dirname($_SERVER['argv'][0]) . "/sql.cli.php " . COMMON_BASE . " $db_tbl insert", $output, $return_val);
+if ($return_val == 0) {
+    $search['insert'] = '/%INSERT%/';
+    $replace['insert'] = join("\n", $output);
+} else {
+    die(basename($_SERVER['argv'][0]) . " Error: could not execute sql.cli.php.\n");
+}
+
+echo "Generating update data...\n";
+$output = array();
+exec(dirname($_SERVER['argv'][0]) . "/sql.cli.php " . COMMON_BASE . " $db_tbl update", $output, $return_val);
+if ($return_val == 0) {
+    $search['update'] = '/%UPDATE%/';
+    $replace['update'] = join("\n", $output);
+} else {
+    die(basename($_SERVER['argv'][0]) . " Error: could not execute sql.cli.php.\n");
+}
+
+echo "Generating search data...\n";
+$output = array();
+exec(dirname($_SERVER['argv'][0]) . "/sql.cli.php " . COMMON_BASE . " $db_tbl search", $output, $return_val);
+if ($return_val == 0) {
+    $search['search'] = '/%SEARCH%/';
+    $replace['search'] = join("\n", $output);
+} else {
+    die(basename($_SERVER['argv'][0]) . " Error: could not execute sql.cli.php.\n");
+}
+
+echo "Generating form validation data...\n";
+$output = array();
+exec(dirname($_SERVER['argv'][0]) . "/validation.cli.php " . COMMON_BASE . " $db_tbl", $output, $return_val);
+if ($return_val == 0) {
+    $search['form_validation'] = '/%FORM_VALIDATION%/';
+    $replace['form_validation'] = join("\n", $output);
+} else {
+    die(basename($_SERVER['argv'][0]) . " Error: could not execute validation.cli.php.\n");
+}
+
+/******************************************************************************
+ * PRINT VAR INSTEAD.
+ *****************************************************************************/
+
+if ('var' == $op) {
+    if (isset($_SERVER['argv'][5]) && isset($replace[$_SERVER['argv'][5]])) {
+        echo "\n\n" . $replace[$_SERVER['argv'][5]] . "\n\n";
+    } else if (isset($_SERVER['argv'][5]) && isset($skel_files[$_SERVER['argv'][5]])) {
+        echo "\n\n" . preg_replace($search, $replace, $skel_files[$_SERVER['argv'][5]]) . "\n\n";
+    } else {
+        die(basename($_SERVER['argv'][0]) . " Error: variable " . (isset($_SERVER['argv'][5]) ? $_SERVER['argv'][5] : '') . " not defined. Please choose one of:\n" . join(', ', array_keys(array_merge($replace, $skel_files))) . "\n");
+    }
+    die;
+}
+
+
+/******************************************************************************
+ * WRITE FILES
+ *****************************************************************************/
+
+if ('admin' == $op) {
+    // (1) Admin script.
+    if (file_exists("$admin_dir/$admin_script")) {
+        echo "Admin script already exists: $admin_dir/$admin_script\n";
+    } else {
+        echo "Writing admin script: $admin_dir/$admin_script\n";
+        if ('test' != $op) {
+            $fp = fopen("$admin_dir/$admin_script", "w");
+            fwrite($fp, preg_replace($search, $replace, $skel_files['skel_admin_script']));
+            fclose($fp);
+        }
+    }
+
+    // (2) Admin list template.
+    if (file_exists("$admin_tpl_dir/$admin_list_template")) {
+        echo "Admin list template already exists: $admin_tpl_dir/$admin_list_template\n";
+    } else {
+        echo "Writing admin list template: $admin_tpl_dir/$admin_list_template\n";
+        if ('test' != $op) {
+            $fp = fopen("$admin_tpl_dir/$admin_list_template", "w");
+            fwrite($fp, preg_replace($search, $replace, $skel_files['skel_admin_list_template']));
+            fclose($fp);
+        }
+    }
+
+    // (3) Admin form template.
+    if (file_exists("$admin_tpl_dir/$admin_form_template")) {
+        echo "Admin form template already exists: $admin_tpl_dir/$admin_form_template\n";
+    } else {
+        echo "Writing admin form template: $admin_tpl_dir/$admin_form_template\n";
+        if ('test' != $op) {
+            $fp = fopen("$admin_tpl_dir/$admin_form_template", "w");
+            fwrite($fp, preg_replace($search, $replace, $skel_files['skel_admin_form_template']));
+            fclose($fp);
+        }
+    }
+}
+
+if ('public' == $op) {
+    // (4) Public script.
+    if (file_exists("$public_dir/$public_script")) {
+        echo "Public script already exists: $public_dir/$public_script\n";
+    } else {
+        echo "Writing public script: $public_dir/$public_script\n";
+        if ('test' != $op) {
+            $fp = fopen("$public_dir/$public_script", "w");
+            fwrite($fp, preg_replace($search, $replace, $skel_files['skel_public_script']));
+            fclose($fp);
+        }
+    }
+
+    // (5) Public list template.
+    if (file_exists("$public_tpl_dir/$public_list_template")) {
+        echo "Public list template already exists: $public_tpl_dir/$public_list_template\n";
+    } else {
+        echo "Writing public list template: $public_tpl_dir/$public_list_template\n";
+        if ('test' != $op) {
+            $fp = fopen("$public_tpl_dir/$public_list_template", "w");
+            fwrite($fp, preg_replace($search, $replace, $skel_files['skel_public_list_template']));
+            fclose($fp);
+        }
+    }
+
+    // (6) Public detail template.
+    if (file_exists("$public_tpl_dir/$public_detail_template")) {
+        echo "Public detail template already exists: $public_tpl_dir/$public_detail_template\n";
+    } else {
+        echo "Writing public detail template: $public_tpl_dir/$public_detail_template\n";
+        if ('test' != $op) {
+            $fp = fopen("$public_tpl_dir/$public_detail_template", "w");
+            fwrite($fp, preg_replace($search, $replace, $skel_files['skel_public_detail_template']));
+            fclose($fp);
+        }
+    }
+
+}
+echo "Done!\n";
+
+
+/********************************************************************
+* FUNCTIONS
+********************************************************************/
+
+function trashFile($file_path_name)
+{
+    global $user_trash_folder;
+    static $file_prefix;
+
+    if (!isset($file_prefix)) {
+        $file_prefix = time();
+    } else {
+        $file_prefix++;
+    }
+
+    // Make user trash folder.
+    if (!is_dir($user_trash_folder)) {
+        echo "Attempting to create user trash folder: $user_trash_folder/\n";
+        mkdir($user_trash_folder);
+        chmod($user_trash_folder, 0700);
+    }
+    if (!is_dir("$user_trash_folder/") || !is_writable("$user_trash_folder/")) {
+        die("User trash directory not available: $user_trash_folder/\n");
+    }
+
+    // Move file to trash.
+    if (file_exists($file_path_name)) {
+        rename($file_path_name, sprintf('%s/%s_%s', $user_trash_folder, $file_prefix, basename($file_path_name)));
+        printf("Moved to trash: %s -> %s\n", $file_path_name, sprintf('%s/%s_%s', $user_trash_folder, $file_prefix, basename($file_path_name)));
+    } else {
+        printf("File not found: %s\n", $file_path_name);
+    }
+}
Index: tags/2.2.3/bin/module_maker/skel/adm_form.ihtml
===================================================================
--- tags/2.2.3/bin/module_maker/skel/adm_form.ihtml	(revision 632)
+++ tags/2.2.3/bin/module_maker/skel/adm_form.ihtml	(revision 632)
@@ -0,0 +1,17 @@
+<?php $fv->printErrorMessages(); ?>
+
+%ADMIN_FORM_TAG_INIT%
+<?php $app->printHiddenSession(); ?>
+<input type="hidden" name="op" value="<?php echo $frm['new_op']; ?>" />
+<input type="hidden" name="%PRIMARY_KEY%" value="<?php echo $frm['%PRIMARY_KEY%']; ?>" />
+
+<div class="sc-form-row sc-submit-buttons">
+    <?php printSubmitButtons($frm['submit_buttons']); ?>
+</div>
+
+%ADM_FORM_TABLE_ROWS%
+
+<div class="sc-form-row sc-submit-buttons">
+    <?php printSubmitButtons($frm['submit_buttons']); ?>
+</div>
+</form>
Index: tags/2.2.3/bin/module_maker/skel/adm_list.ihtml
===================================================================
--- tags/2.2.3/bin/module_maker/skel/adm_list.ihtml	(revision 632)
+++ tags/2.2.3/bin/module_maker/skel/adm_list.ihtml	(revision 632)
@@ -0,0 +1,42 @@
+
+<?php $fv->printErrorMessages(); ?>
+
+<div class="commandbox">
+    <span class="sc-nowrap commandtext"><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?op=add'); ?>"><?php echo _("Add %ITEM_TITLE%"); ?></a></span>
+    <form action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" method="get" class="sc-form">
+        <?php $app->printHiddenSession(false); ?>
+        <input type="text" class="sc-small" size="20" name="search_query" value="<?php echo getFormData('search_query'); ?>" title="<?php echo oTxt(_("Fields searched: __///__ %SEARCH_FIELDS%.")); ?>" />
+        <select name="filter___///__" class="sc-small">
+            <?php // printSelectForm('__///___tbl', "CONCAT(__///___id, '&mdash;', city, '&mdash;', title)", '__///___id', getFormData('filter___///__'), array('' => 'Any __///__'), 'ORDER BY __///__ ASC'); ?>
+        </select>
+        <input type="submit" value="<?php echo _("Filter"); ?>" />
+    </form>
+</div>
+
+<form action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" method="post">
+<?php $app->printHiddenSession(); ?>
+<?php include 'list_info.ihtml'; ?>
+<table class="list">
+    <tr>
+        <th>&nbsp;</th>
+        <th>&nbsp;</th>
+%ADM_LIST_HEADER_ROWS%
+        <th>&nbsp;</th>
+    </tr>
+    <?php for ($i = 0; $i <= $page->last_item - $page->first_item && $page->total_items > 0; $i++) { ?>
+    <tr>
+        <td class="sc-padleft sc-nowrap"><a title="<?php printf(_("Edit %s"), oTxt($list[$i]['__///__'])) ?>" href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?op=edit&%PRIMARY_KEY%=' . $list[$i]['%PRIMARY_KEY%']); ?>"><img src="/admin/i/pen.gif" alt="Edit" width="12" height="12" border="0" /></a> &nbsp;</td>
+        <td class="sc-padleft sc-nowrap"><a title="<?php printf(_("Versions of %s"), oTxt($list[$i]['__///__'])) ?>" href="<?php echo $app->oHREF('/admin/versions.php?record_table=%DB_TBL%&record_key=%PRIMARY_KEY%&boomerang=true&record_val=' . $list[$i]['%PRIMARY_KEY%']); ?>"><img src="/admin/i/multiple.png" alt="" width="12" height="12" border="0" /></a> &nbsp;</td>
+%ADM_LIST_ROWS%
+        <td class="sc-padleft sc-nowrap" align="right"><a title="<?php printf(_("Delete %s"), oTxt($list[$i]['__///__'])) ?>" href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . "?op=del&%PRIMARY_KEY%=" . $list[$i]['%PRIMARY_KEY%']); ?>" onclick="javascript:return confirm('<?php printf(_("Are you sure you want to delete the record %s? This action is permanent and cannot be undone."), oTxt($list[$i]['__///__'])) ?>');"><img src="/admin/i/trash.gif" alt="Delete" width="10" height="10" border="0" /></a> &nbsp;</td>
+    </tr>
+    <?php } ?>
+</table>
+
+<?php if ($page->total_pages > 1) { ?>
+<div class="sc-nowrap commandtext" style="float: right;"><?php echo _("Pages:"); ?>&nbsp;<?php $page->printPageNumbers() ?></div>
+<?php } ?>
+<?php if ($page->total_items > 0) { ?>
+<div class="commanditem"><input type="submit" name="op" value="<?php echo _("Save rank"); ?>" /></div>
+<?php } ?>
+</form>
Index: tags/2.2.3/bin/module_maker/skel/admin.php
===================================================================
--- tags/2.2.3/bin/module_maker/skel/admin.php	(revision 632)
+++ tags/2.2.3/bin/module_maker/skel/admin.php	(revision 632)
@@ -0,0 +1,476 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * %ADMIN_SCRIPT%
+ *
+ * Generated by module_maker.cli.php on %DATE%
+ */
+
+require_once dirname(__FILE__) . '/_config.inc.php';
+
+$auth->requireLogin();
+$app->sslOn();
+
+require_once 'codebase/lib/PageNumbers.inc.php';
+require_once 'codebase/lib/Cache.inc.php';
+require_once 'codebase/lib/FormValidator.inc.php';
+require_once 'codebase/lib/SortOrder.inc.php';
+require_once 'codebase/lib/TemplateGlue.inc.php';
+require_once 'codebase/lib/Prefs.inc.php';
+require_once 'codebase/lib/Lock.inc.php';
+require_once 'codebase/lib/Version.inc.php';
+%ADMIN_UPLOAD_INCLUDE%
+
+/********************************************************************
+* CONFIG
+********************************************************************/
+
+// Titles and navigation header.
+$nav->add(_("%TITLE%"), null);
+
+// The object to validate form input.
+$fv = new FormValidator();
+
+// Configure the prefs object.
+$tmp_prefs = new Prefs('%NAME_PLURAL%');
+$tmp_prefs->setParam(array('persistent' => false));
+
+// Configure the cache object.
+$cache = new Cache('%NAME_PLURAL%');
+$cache->setParam(array('enabled' => true));
+
+%SORT_ORDER%
+
+// Instantiate page numbers. Total items are set and calculation is done in the getCachedList function.
+$page = new PageNumbers();
+$page->setPerPage(getFormData('per_page'), 100);
+$page->setPageNumber(getFormData('page_number'));
+
+// Query parameters to retain always.
+$app->carryQuery(array(
+    'filter___///__',
+));
+
+// Query parameters to retain only locally.
+$locally_carried_queries = array(
+    'search_query',
+);
+%ADMIN_UPLOAD_CONFIG%
+/********************************************************************
+* MAIN
+********************************************************************/
+ %ADMIN_UPLOAD_INIT%
+// We may want to use the add/edit interface from another script, so this
+// allows us to remember which page we came from so we can go back there.
+if (getFormData('boomerang', false) && isset($_SERVER['HTTP_REFERER'])) {
+    $app->setBoomerangURL($_SERVER['HTTP_REFERER'], '%NAME_PLURAL%');
+}
+
+if (getFormData('break_list_cache', false)) {
+    // Remove any stale cached list data.
+    $cache->delete('list');
+}
+
+// What action to take.
+switch (getFormData('op')) {
+
+case 'add' :
+    // Initialize variables for the form template.
+    $frm =& addRecordForm();
+    $nav->add(_("Add %ITEM_TITLE%"));
+    $main_template = '%ADMIN_FORM_TEMPLATE%';
+    break;
+
+case 'edit' :
+    // Initialize variables for the form template.
+    $frm =& editRecordForm(getFormData('%PRIMARY_KEY%'));
+    $nav->add(_("Edit %ITEM_TITLE%"));
+    $main_template = '%ADMIN_FORM_TEMPLATE%';
+    break;
+
+case 'del' :
+    deleteRecord(getFormData('%PRIMARY_KEY%'));%ADMIN_UPLOAD_DEL%
+    if ($app->validBoomerangURL('%NAME_PLURAL%')) {
+        // Display boomerang page.
+        $app->dieBoomerangURL('%NAME_PLURAL%', $locally_carried_queries);
+    }
+    // Display default page.
+    $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
+    break;
+
+case 'insert' :
+    if (getFormdata('cancel', false)) {
+        if ($app->validBoomerangURL('%NAME_PLURAL%')) {
+            // Display boomerang page.
+            $app->dieBoomerangURL('%NAME_PLURAL%', $locally_carried_queries);
+        }
+        // Display default page.
+        $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
+    }
+    validateInput();
+    if ($fv->anyErrors()) {
+        $frm =& addRecordForm();
+        $frm = array_merge($frm, getFormData());
+        $nav->add(_("Add %ITEM_TITLE%"));
+        $main_template = '%ADMIN_FORM_TEMPLATE%';
+    } else {
+        $%PRIMARY_KEY% = insertRecord(getFormData());%ADMIN_UPLOAD_INSERT%
+        if (getFormdata('repeat', false)) {
+            // Display function again.
+            $app->dieURL($_SERVER['PHP_SELF'] . '?op=add', $locally_carried_queries);
+        } else if ($app->validBoomerangURL('%NAME_PLURAL%')) {
+            // Display boomerang page.
+            $app->dieBoomerangURL('%NAME_PLURAL%', $locally_carried_queries);
+        }
+        // Display default page.
+        $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
+    }
+    break;
+
+case 'update' :
+    if (getFormdata('reset', false)) {
+        $app->raiseMsg(_("Saved values have been reloaded."), MSG_NOTICE, __FILE__, __LINE__);
+        $app->dieURL($_SERVER['PHP_SELF'] . '?op=edit&%PRIMARY_KEY%=' . getFormData('%PRIMARY_KEY%'), $locally_carried_queries);
+    }
+    if (getFormdata('cancel', false)) {
+        // Remove lock
+        $lock->select('%DB_TBL%', '%PRIMARY_KEY%', getFormData('%PRIMARY_KEY%'));
+        $lock->remove();
+        if ($app->validBoomerangURL('%NAME_PLURAL%')) {
+            // Display boomerang page.
+            $app->dieBoomerangURL('%NAME_PLURAL%', $locally_carried_queries);
+        }
+        // Display default page.
+        $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
+    }
+    validateInput();
+    if ($fv->anyErrors()) {
+        $frm =& editRecordForm(getFormData('%PRIMARY_KEY%'));
+        $frm = array_merge($frm, getFormData());
+        $nav->add(_("Edit %ITEM_TITLE%"));
+        $main_template = '%ADMIN_FORM_TEMPLATE%';
+    } else {%ADMIN_UPLOAD_UPDATE%
+        updateRecord(getFormData());
+        if (getFormdata('repeat', false)) {
+            // Display edit function with next available ID.
+            $qid = $db->query("SELECT %PRIMARY_KEY% FROM %DB_TBL% WHERE %PRIMARY_KEY% > '" . $db->escapeString(getFormData('%PRIMARY_KEY%')) . "' ORDER BY %PRIMARY_KEY% ASC LIMIT 1");
+            if (list($next_id) = mysql_fetch_row($qid)) {
+                $app->dieURL($_SERVER['PHP_SELF'] . '?op=edit&%PRIMARY_KEY%=' . $next_id, $locally_carried_queries);
+            } else {
+                $app->raiseMsg(_("Cannot edit next, the end of the list was reached"), MSG_NOTICE, __FILE__, __LINE__);
+            }
+        } else if ($app->validBoomerangURL('%NAME_PLURAL%')) {
+            // Display boomerang page.
+            $app->dieBoomerangURL('%NAME_PLURAL%', $locally_carried_queries);
+        }
+        // Display default page.
+        $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
+    }
+    break;
+
+case _("Save rank") :
+    updateRank(getFormData('rank'));
+    $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
+    break;
+
+default :
+    $list =& getCachedList();
+    $main_template = '%ADMIN_LIST_TEMPLATE%';
+    break;
+}
+
+/********************************************************************
+* OUTPUT
+********************************************************************/
+
+include 'header.ihtml';
+$app->carryQuery($locally_carried_queries);
+include $main_template;
+include 'footer.ihtml';
+
+/********************************************************************
+* FUNCTIONS
+********************************************************************/
+
+%FORM_VALIDATION%
+
+function &addRecordForm()
+{
+    // Set default values for the reset of the fields.
+    $frm = array(
+        %SET_VALUES_DEFAULT%,
+        'new_op' => 'insert',
+        'submit_buttons' => array(
+            array('name' => 'submit', 'value' => _("Add %ITEM_TITLE%"), 'accesskey' => 's'),
+            array('name' => 'repeat', 'value' => _("Add &amp; repeat"), 'accesskey' => 'r'),
+            array('name' => 'cancel', 'value' => _("Cancel"), 'accesskey' => 'c'),
+        ),
+    );
+
+    return $frm;
+}
+
+function &editRecordForm($id)
+{
+    global $lock, $locally_carried_queries;
+    $db =& DB::getInstance();
+    $app =& App::getInstance();
+
+    $lock->select('%DB_TBL%', '%PRIMARY_KEY%', $id);
+    if ($lock->isLocked() && !$lock->isMine()) {
+        $lock->dieErrorPage();
+    }
+
+    // Get the information for the form.
+    $qid = $db->query("
+        SELECT *
+        FROM %DB_TBL%
+        WHERE %PRIMARY_KEY% = '" . $db->escapeString($id) . "'
+    ");
+    if (!$frm = mysql_fetch_assoc($qid)) {
+        $app->logMsg('Could not find record with %PRIMARY_KEY%: ' . $id, LOG_WARNING, __FILE__, __LINE__);
+        $app->raiseMsg(sprintf(_("The requested record %s could not be found."), $id), MSG_ERR, __FILE__, __LINE__);
+        $app->dieBoomerangURL('%NAME_PLURAL%', $locally_carried_queries);
+    }
+
+    // Lock this record.
+    $lock->set('%DB_TBL%', '%PRIMARY_KEY%', $id, $frm['__///__']);
+
+    // Set misc values for the form.
+    $frm = array_merge(array(
+        %SET_VALUES_DEFAULT%,
+        'new_op' => 'update',
+        'submit_buttons' => array(
+            array('name' => 'submit', 'value' => _("Save changes"), 'accesskey' => 's'),
+            array('name' => 'repeat', 'value' => _("Save & edit next"), 'accesskey' => 'e'),
+            array('name' => 'reset', 'value' => _("Reset"), 'accesskey' => 'r'),
+            array('name' => 'cancel', 'value' => _("Cancel"), 'accesskey' => 'c'),
+        ),
+    ), $frm);
+
+    return $frm;
+}
+
+function deleteRecord($id)
+{
+    global $lock, $cache, $locally_carried_queries;
+    $db =& DB::getInstance();
+    $app =& App::getInstance();
+
+    $lock->select('%DB_TBL%', '%PRIMARY_KEY%', $id);
+    if ($lock->isLocked() && !$lock->isMine()) {
+        $lock->dieErrorPage();
+    }
+
+    // Remove any stale cached list data.
+    $cache->delete('list');
+
+    // Get the information for this object.
+    $qid = $db->query("
+        SELECT __///__
+        FROM %DB_TBL%
+        WHERE %PRIMARY_KEY% = '" . $db->escapeString($id) . "'
+    ");
+    if (! list($name) = mysql_fetch_row($qid)) {
+        $app->logMsg('Could not find record with %PRIMARY_KEY%: ' . $id, LOG_WARNING, __FILE__, __LINE__);
+        $app->raiseMsg(sprintf(_("The requested record %s could not be found."), $id), MSG_ERR, __FILE__, __LINE__);
+        $app->dieBoomerangURL('%NAME_PLURAL%', $locally_carried_queries);
+    }
+
+    // Delete the record.
+    $db->query("DELETE FROM %DB_TBL% WHERE %PRIMARY_KEY% = '" . $db->escapeString($id) . "'");
+
+    $app->raiseMsg(sprintf(_("The %ITEM_TITLE% <em>%s</em> has been deleted."), $name), MSG_SUCCESS, __FILE__, __LINE__);
+
+    // Unlock record.
+    $lock->remove();
+}
+
+function insertRecord($frm)
+{
+    global $auth;
+    global $cache;
+    $db =& DB::getInstance();
+    $app =& App::getInstance();
+
+    // Remove any stale cached list data.
+    $cache->delete('list');
+
+%INSERT%
+    $last_insert_id = mysql_insert_id($db->getDBH());
+
+    // Create version.
+    $version = Version::getInstance($auth);
+    $version->create('%DB_TBL%', '%PRIMARY_KEY%', $last_insert_id, $frm['__///__']);
+
+    $app->raiseMsg(sprintf(_("The %ITEM_TITLE% <em>%s</em> has been added."), $frm['__///__']), MSG_SUCCESS, __FILE__, __LINE__);
+
+    return $last_insert_id;
+}
+
+function updateRecord($frm)
+{
+    global $auth;
+    global $lock;
+    global $cache;
+    $db =& DB::getInstance();
+    $app =& App::getInstance();
+
+    $lock->select('%DB_TBL%', '%PRIMARY_KEY%', $frm['%PRIMARY_KEY%']);
+    if ($lock->isLocked() && !$lock->isMine()) {
+        $lock->dieErrorPage();
+    }
+
+    // Remove any stale cached list data.
+    $cache->delete('list');
+
+%UPDATE%
+
+    // Create version.
+    $version = Version::getInstance($auth);
+    $version->create('%DB_TBL%', '%PRIMARY_KEY%', $frm['%PRIMARY_KEY%'], $frm['__///__']);
+
+    $app->raiseMsg(sprintf(_("The %ITEM_TITLE% <em>%s</em> has been updated."), $frm['__///__']), MSG_SUCCESS, __FILE__, __LINE__);
+
+    // Unlock record.
+    $lock->remove();
+}
+
+function &getCachedList()
+{
+    global $page;
+    global $so;
+    global $tmp_prefs;
+    global $cache;
+    $db =& DB::getInstance();
+    $app =& App::getInstance();
+
+    $where_clause = '';
+
+    // Build search query if available.
+    if (getFormData('search_query', false)) {
+        $qry_words = preg_split('/[^\w]/', getFormData('search_query'));
+        for ($i=0; $i<sizeof($qry_words); $i++) {
+%SEARCH%
+        }
+    }
+
+    if (getFormData('filter___///__', false)) {
+        // Limit by filter.
+        $where_clause .= (empty($where_clause) ? 'WHERE' : ' AND') . " __///__ = '" . $db->escapeString(getFormData('filter___///__')) . "'";
+    }
+
+    // Count the total number of records so we can do something about the page numbers.
+    $qid = $db->query("
+        SELECT COUNT(*)
+        FROM %DB_TBL%
+        $where_clause
+    ");
+    list($num_results) = mysql_fetch_row($qid);
+
+    // Set page numbers now we know (needed for next step).
+    $page->setTotalItems($num_results);
+    $page->calculate();
+
+    // Final SQL, with sort and page limiters.
+    $sql = "
+        SELECT
+            %DB_TBL%.*,
+            a1.username AS added_by_username,
+            a2.username AS modified_by_username
+        FROM %DB_TBL%
+        LEFT JOIN user_tbl a1 ON (%DB_TBL%.added_by_user_id = a1.user_id)
+        LEFT JOIN user_tbl a2 ON (%DB_TBL%.modified_by_user_id = a2.user_id)
+        $where_clause
+        " . $so->getSortOrderSQL() . "
+        " . $page->getLimitSQL() . "
+    ";
+
+    // Use a cache hash to determine if the result-set has changed.
+    // A unique key for this query, with the total_items in case db records
+    // were added since the last cache. This identifies a unique set of
+    // cached data, but we must refer to the list that is cached by a more
+    // generic name. so that we can flush the cache (if records updated)
+    // without knowing the hash.
+    $cache_hash = md5($sql . '|' . $page->total_items);
+    if ($tmp_prefs->get('cache_hash') != $cache_hash) {
+        $cache->delete('list');
+        $tmp_prefs->set('cache_hash', $cache_hash);
+    }
+
+    // First try to return from the cache.
+    if ($cache->exists('list')) {
+        $list = $cache->get('list');
+        return $list;
+    }
+
+    // The list was not cached, so issue the real query.
+    $qid = $db->query($sql);
+    while ($row = mysql_fetch_assoc($qid)) {
+        $list[] = $row;
+    }
+
+    // Save this list into the cache.
+    if (isset($list) && !empty($list)) {
+        $cache->set('list', $list);
+    }
+
+    return $list;
+}
+
+function updateRank($ranks)
+{
+    global $cache;
+    $db =& DB::getInstance();
+    $app =& App::getInstance();
+
+    if (!is_array($ranks)) {
+        $app->logMsg('Saving rank failed, data posted is not an array: ' . $ranks, LOG_ERR, __FILE__, __LINE__);
+        return false;
+    }
+
+    // Remove any stale cached list data.
+    $cache->delete('list');
+
+    // Count the ranks with invalid numbers
+    $unspecified_counter = 0;
+
+    // Go through the array of new ranks.
+    foreach ($ranks as $id => $new_rank) {
+        if ('' == trim($new_rank) || !is_numeric($new_rank) || $new_rank > 2147483646) {
+            // Unspecified entries receive a sort order of 10000.
+            $new_rank = 10000;
+            $unspecified_counter++;
+        }
+        $db->query("
+            UPDATE %DB_TBL% SET
+                rank = '" . $db->escapeString($new_rank) . "'
+            WHERE %PRIMARY_KEY% = '" . $db->escapeString($id) . "'
+        ");
+    }
+
+    $app->raiseMsg(_("Records have been reordered with the new rank."), MSG_SUCCESS, __FILE__, __LINE__);
+    if ($unspecified_counter > 0) {
+        $app->raiseMsg(sprintf(_("%s items with unspecified ranks were automatically assigned a rank of 10000."), $unspecified_counter), MSG_NOTICE, __FILE__, __LINE__);
+    }
+}
Index: tags/2.2.3/bin/module_maker/skel/public.ihtml
===================================================================
--- tags/2.2.3/bin/module_maker/skel/public.ihtml	(revision 632)
+++ tags/2.2.3/bin/module_maker/skel/public.ihtml	(revision 632)
@@ -0,0 +1,6 @@
+<p><?php echo nl2br(fancyTxt(oTxt($item['content'], true))); ?></p>
+<p><em><?php echo sprintf(_("Published %s"), date($app->getParam('date_format'), strtotime($item['__///__']))); ?></em></p>
+
+<!--
+%PUBLIC_DETAIL_PAGE_VARS%
+-->
Index: tags/2.2.3/bin/module_maker/skel/public.php
===================================================================
--- tags/2.2.3/bin/module_maker/skel/public.php	(revision 632)
+++ tags/2.2.3/bin/module_maker/skel/public.php	(revision 632)
@@ -0,0 +1,146 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * %PUBLIC_SCRIPT%
+ *
+ * Generated by module_maker.cli.php on %DATE%
+ */
+
+require_once dirname(__FILE__) . '/_config.inc.php';
+require_once 'codebase/lib/PageNumbers.inc.php';
+require_once 'codebase/lib/SortOrder.inc.php';
+
+
+/******************************************************************************
+ * CODE CONFIG
+ *****************************************************************************/
+
+// Titles and navigation header.
+$nav->add(sprintf(_("%TITLE%"), null));
+$nav->setParam(array(
+    'title' => true,
+));
+
+// Instantiate page numbers. Total items are set and calculation is done in the getRecordList function.
+$page = new PageNumbers();
+$page->setPerPage(getFormData('per_page'), 50);
+$page->setPageNumber(getFormData('page_number'));
+
+%SORT_ORDER%
+
+/********************************************************************
+* MAIN
+********************************************************************/
+
+if (getFormData('%PRIMARY_KEY%', false)) {
+
+    // Get requested record.
+    $qid = $db->query("
+        SELECT * FROM %DB_TBL%
+        WHERE %PRIMARY_KEY% = '" . $db->escapeString(getFormData('%PRIMARY_KEY%')) . "'
+        AND publish = 'true'
+        __///__AND (publish_date <= CURDATE() OR publish_date = '" . $db->escapeString($db->getParam('zero_date')) . "')
+        __///__AND (expire_date > CURDATE() OR expire_date = '" . $db->escapeString($db->getParam('zero_date')) . "')
+    ");
+    if (!$item = mysql_fetch_assoc($qid)) {
+        $app->raiseMsg(_("Sorry that %ITEM_TITLE% could not be found"), MSG_WARNING, __FILE__, __LINE__);
+        $app->dieBoomerangURL();
+    }
+
+    // Update the hit counter for this record.
+    $db->query("
+        UPDATE %DB_TBL%
+        SET hit_count = hit_count + 1
+        WHERE %PRIMARY_KEY% = '" . $db->escapeString(getFormData('%PRIMARY_KEY%')) . "'
+    ");
+
+    // Set title and main template.
+    $nav->add($item['__///__']);
+    $main_template = '%PUBLIC_DETAIL_TEMPLATE%';
+
+} else {
+
+    // Get the DEFAULT list.
+    $%NAME_SINGULAR%_list = array();
+    $qid = $db->query("
+        SELECT *
+        FROM %DB_TBL%
+        WHERE publish = 'true'
+        " . $so->getSortOrderSQL() . "
+        LIMIT 100
+    ");
+    while ($row = mysql_fetch_assoc($qid)) {
+        $%NAME_SINGULAR%_list[] = $row;
+    }
+
+    // Set page numbers for default list.
+    $page->setTotalItems(sizeof($%NAME_SINGULAR%_list));
+    $page->calculate();
+
+
+    // Get the FEATURED list.
+    $featured_list = array();
+    $qid = $db->query("
+        SELECT *
+        FROM %DB_TBL%
+        WHERE publish = 'true'
+        AND featured = 'true'
+        ORDER BY %PRIMARY_KEY% DESC LIMIT 10
+    ");
+    while ($row = mysql_fetch_assoc($qid)) {
+        $featured_list[] = $row;
+    }
+
+    // Get the POPULAR list.
+    $popular_list = array();
+    $qid = $db->query("
+        SELECT *
+        FROM %DB_TBL%
+        WHERE publish = 'true'
+        ORDER BY hit_count DESC LIMIT 10
+    ");
+    while ($row = mysql_fetch_assoc($qid)) {
+        $popular_list[] = $row;
+    }
+
+    // Get the RECENT list.
+    $recent_list = array();
+    $qid = $db->query("
+        SELECT *
+        FROM %DB_TBL%
+        WHERE publish = 'true'
+        ORDER BY added_datetime DESC LIMIT 10
+    ");
+    while ($row = mysql_fetch_assoc($qid)) {
+        $recent_list[] = $row;
+    }
+
+    // Set main template.
+    $main_template = '%PUBLIC_LIST_TEMPLATE%';
+}
+
+// We have the data, and no errors, so here come the templates.
+include 'header.ihtml';
+include $main_template;
+include 'footer.ihtml';
+
Index: tags/2.2.3/bin/module_maker/skel/public_list.ihtml
===================================================================
--- tags/2.2.3/bin/module_maker/skel/public_list.ihtml	(revision 632)
+++ tags/2.2.3/bin/module_maker/skel/public_list.ihtml	(revision 632)
@@ -0,0 +1,66 @@
+<?php
+// Print list of ALL %NAME_SINGULAR% records.
+if (is_array($%NAME_SINGULAR%_list) && !empty($%NAME_SINGULAR%_list)) {
+    for ($i = 0; $i <= $page->last_item - $page->first_item && $page->total_items > 0; $i++) {
+        ?>
+        <?php if (mb_strlen(trim($%NAME_SINGULAR%_list[$i]['content'])) > 0) { // Link only if content is available. ?>
+            <h4><a href="<?php echo $app->oHREF('/%PUBLIC_SCRIPT%?%PRIMARY_KEY%=' . $%NAME_SINGULAR%_list[$i]['%PRIMARY_KEY%']); ?>"><?php echo fancyTxt(oTxt($%NAME_SINGULAR%_list[$i]['title'])); ?></a></h4>
+        <?php } else { ?>
+            <h4><?php echo fancyTxt(oTxt($%NAME_SINGULAR%_list[$i]['title'])); ?></h4>
+        <?php } ?>
+        <p>
+        <em><?php echo date($app->getParam('date_format'), strtotime($%NAME_SINGULAR%_list[$i]['__///__'])); ?></em> &mdash; <?php echo nl2br(fancyTxt(oTxt($%NAME_SINGULAR%_list[$i]['summary'], true))); ?>
+
+        <?php if (mb_strlen(trim($%NAME_SINGULAR%_list[$i]['content'])) > 0) { // Link only if content is available. ?>
+            <br /><a href="<?php echo $app->oHREF('/%PUBLIC_SCRIPT%?%PRIMARY_KEY%=' . $%NAME_SINGULAR%_list[$i]['%PRIMARY_KEY%']); ?>"><?php echo _("Read more"); ?></a>
+        <?php } ?>
+        </p>
+        <?php
+    }
+    if ($page->total_pages > 1) {
+        ?><div class="commanditem commandtext"><?php echo _("Pages:"); ?>&nbsp;<?php $page->printPageNumbers() ?></div><?php
+    }
+}
+?>
+
+
+<?php
+// Print list of FEATURED items.
+if (is_array($featured_list) && !empty($featured_list)) {
+    ?>
+    <h2><?php echo _("Featured %NAME_PLURAL%"); ?></h2>
+    <?php
+    foreach ($featured_list as $item) {
+        ?><h4><a href="<?php echo $app->oHREF('/%PUBLIC_SCRIPT%?%PRIMARY_KEY%=' . $item['%PRIMARY_KEY%']); ?>"><?php echo fancyTxt(oTxt($item['title'])); ?></a></h4><?php
+    }
+}
+?>
+
+<?php
+// Print list of POPULAR items.
+if (is_array($popular_list) && !empty($popular_list)) {
+    ?>
+    <h2><?php echo _("Popular %NAME_PLURAL%"); ?></h2>
+    <?php
+    foreach ($popular_list as $item) {
+        ?><h4><a href="<?php echo $app->oHREF('/%PUBLIC_SCRIPT%?%PRIMARY_KEY%=' . $item['%PRIMARY_KEY%']); ?>"><?php echo fancyTxt(oTxt($item['title'])); ?></a> </h4><?php
+    }
+}
+?>
+
+<?php
+// Print list of RECENT items.
+if (is_array($recent_list) && !empty($recent_list)) {
+    ?>
+    <h2><?php echo _("Recent %NAME_PLURAL%"); ?></h2>
+    <?php
+    foreach ($recent_list as $item) {
+        ?><h4><a href="<?php echo $app->oHREF('/%PUBLIC_SCRIPT%?%PRIMARY_KEY%=' . $item['%PRIMARY_KEY%']); ?>"><?php echo fancyTxt(oTxt($item['title'])); ?></a> </h4><?php
+    }
+}
+?>
+
+
+<!--
+%PUBLIC_LIST_PAGE_VARS%
+-->
Index: tags/2.2.3/bin/module_maker/sql.cli.php
===================================================================
--- tags/2.2.3/bin/module_maker/sql.cli.php	(revision 632)
+++ tags/2.2.3/bin/module_maker/sql.cli.php	(revision 632)
@@ -0,0 +1,226 @@
+#!/usr/bin/env php
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * sql.cli.php
+ */
+
+if ($_SERVER['argc'] > 1 && isset($_SERVER['argv'][1]) && '' != $_SERVER['argv'][1] && is_dir($_SERVER['argv'][1])) {
+    // First arg is path to current site. Realpath removes trailing /s
+    define('COMMON_BASE', realpath($_SERVER['argv'][1]));
+} else {
+    die("Error: First argument must be the directory path to an existing site (ex: /home/sc/www.strangecode.com).\n");
+}
+
+include_once dirname(__FILE__) . '/../_config.inc.php';
+
+$op = null;
+$valid_ops = array('sortorder', 'insert', 'update', 'delete', 'search', 'filter');
+
+// Test arguments.
+if (isset($_SERVER['argv'][2])) {
+    // Second arg is db table.
+    $db_tbl = $_SERVER['argv'][2];
+} else {
+    die(sprintf("Usage: %s site_directory db_table [operation]\nValid operations include: %s\n", basename($_SERVER['argv'][0]), join(', ', $valid_ops)));
+}
+
+// Test for operation.
+if (isset($_SERVER['argv'][3])) {
+    // Optional third arg is op.
+    $op = $_SERVER['argv'][3];
+    // Make sure op is valid.
+    if (!in_array($op, $valid_ops)) {
+        die(basename($_SERVER['argv'][0]) . " Warning: Operation '$op' is not something I know how to do Please select one of: " . join(", ", $valid_ops) . "\n");
+    }
+}
+
+// Get DB tables.
+$qid = $db->query("SHOW TABLES");
+while (list($row) = mysql_fetch_row($qid)) {
+    $tables[] = $row;
+}
+
+// Make sure requested table is in database.
+if (!in_array($db_tbl, $tables)) {
+    die(sprintf("%s Warning: %s does not exist in database %s. Please select one of: \n\n%s\n\n", basename($_SERVER['argv'][0]), $db_tbl, $app->getParam('db_name'), join("\n", $tables)));
+}
+
+// Make sure op is valid.
+if (isset($op) && !in_array($op, $valid_ops)) {
+    die(basename($_SERVER['argv'][0]) . " Warning: Operation '$op' is not something I know how to do Please select one of: " . join(", ", $valid_ops) . "\n");
+}
+
+// Get DB table column info.
+$qid = $db->query("DESCRIBE " . $db->escapeString($db_tbl));
+while ($row = mysql_fetch_row($qid)) {
+    $cols[] = $row;
+}
+
+$sort_columns = '';
+$primary_key = '__///__';
+
+// Loop through columns
+if (is_array($cols) && !empty($cols)) {
+    foreach ($cols as $col) {
+
+        // Human readable.
+        $field = $col[0];
+        $type = preg_replace('/^(\w+).*$/', '\\1', $col[1]);
+        $is_primary_key = ('PRI' == $col[3]);
+
+        $sort_columns .= "\$so->setColumn('$field', '$field ASC', '$field DESC');\n";
+
+        if ($is_primary_key) {
+            // This is the primary key. Deal with separately.
+            $primary_key = $field;
+        } else if ('set' == $type) {
+            // Set types usually need to be converted to comma-delimited lists.
+            $c[$field] = "'\" . escapedList(array_keys(\$frm['$field'])) . \"'";
+        } else if ('featured' == $field || 'publish' == $field || preg_match("/enum\('true'\)/", $col[1])) {
+            // Toggle types.
+            $c[$field] = "'\" . isset(\$frm['$field']) . \"'";
+        } else if ('added_by_user_id' == $field || 'modified_by_user_id' == $field) {
+            // Expects a user_id.
+            $c[$field] = "'\" . \$db->escapeString(\$auth->get('user_id')) . \"'";
+        } else if ('added_datetime' == $field || 'modified_datetime' == $field) {
+            // DB record insertion datetime.
+            $c[$field] = "NOW()";
+        } else if (preg_match('/date_|_date/', $field)) {
+            // This is a date field. Convert to SQL date.
+            $c[$field] = "'\" . \$db->escapeString(strToSQLDate(\$frm['$field'])) . \"'";
+        } else {
+            // Default. Just insert data.
+            $c[$field] = "'\" . \$db->escapeString(\$frm['$field']) . \"'";
+        }
+    }
+
+} else {
+    die(basename($_SERVER['argv'][0]) . " Warning: $db_tbl does not have any columns.\n");
+}
+
+
+echo isset($op) ? '' : "\n\n\n";
+
+// Insert SQL.
+if (!isset($op) || 'insert' == $op) {
+$insert_skip_columns = array('modified_datetime', 'modified_by_user_id');
+$insert_c = array();
+foreach ($c as $k=>$v) {
+    if (in_array($k, $insert_skip_columns)) {
+        continue;
+    }
+    $insert_c[$k] = $v;
+}
+$db_keys = join(",\n            ", array_keys($insert_c));
+$db_vals = join(",\n            ", $insert_c);
+echo <<<E_O_F
+    // Insert record data.
+    \$db->query("
+        INSERT INTO $db_tbl (
+            $db_keys
+        ) VALUES (
+            $db_vals
+        )
+    ");
+E_O_F;
+}
+
+echo isset($op) ? '' : "\n\n\n";
+
+// Update SQL.
+if (!isset($op) || 'update' == $op) {
+$update_skip_columns = array('added_datetime', 'added_by_user_id');
+$comma = '';
+$key_eq_val = '';
+foreach ($c as $k=>$v) {
+    if (in_array($k, $update_skip_columns)) {
+        continue;
+    }
+    $key_eq_val .= $comma . "\n            $k = $v";
+    $comma = ',';
+}
+echo <<<E_O_F
+    // Update record data.
+    \$db->query("
+        UPDATE $db_tbl SET$key_eq_val
+        WHERE $primary_key = '" . \$db->escapeString(\$frm['$primary_key']) . "'
+    ");
+E_O_F;
+}
+
+echo isset($op) ? '' : "\n\n\n";
+
+// Delete SQL.
+if (!isset($op) || 'delete' == $op) {
+$where_clause = '';
+$delete_skip_columns = array();
+$delim = 'WHERE';
+if (!empty($primary_key)) {
+    $where_clause = "            $delim $primary_key = '\" . \$db->escapeString(\$frm['$primary_key']) . \"'\n";
+    $delim = 'AND';
+}
+foreach ($c as $k=>$ignore) {
+    if (in_array($k, $delete_skip_columns)) {
+        continue;
+    }
+    $where_clause .= "            $delim $k = '\" . \$db->escapeString(\$frm['$k']) . \"'\n";
+    $delim = 'AND';
+}
+echo <<<E_O_F
+        // Delete record data.
+        \$db->query("
+            DELETE FROM $db_tbl
+$where_clause        ");
+E_O_F;
+}
+
+echo isset($op) ? '' : "\n\n\n";
+
+// SortOrder methods SQL.
+if (!isset($op) || 'sortorder' == $op) {
+    echo "// Instantiate a sorting object with the default sort and order. Add SQL for each column.\n";
+    echo "\$so = new SortOrder('$db_tbl.$primary_key', 'DESC');\n";
+    echo "\$so->setColumn('$db_tbl.$primary_key', '$db_tbl.$primary_key ASC', '$db_tbl.$primary_key DESC');\n";
+    foreach ($c as $k=>$v) {
+        echo "\$so->setColumn('$db_tbl.$k', '$db_tbl.$k ASC', '$db_tbl.$k DESC');\n";
+    }
+}
+
+echo isset($op) ? '' : "\n\n\n";
+
+// Search SQL
+if (!isset($op) || 'search' == $op) {
+$search_skip_columns = array('added_datetime', 'added_by_user_id', 'modified_datetime', 'modified_by_user_id', 'publish', 'featured');
+$search_columns = $db_tbl . '.' . join(" LIKE '%\" . \$db->escapeString(\$qry_words[\$i]) . \"%'\n                    OR $db_tbl.", array_diff(array_keys($c), $search_skip_columns));
+echo <<<E_O_F
+            \$where_clause .= (empty(\$where_clause) ? 'WHERE' : 'AND') . "
+                (
+                    $search_columns LIKE '%" . \$db->escapeString(\$qry_words[\$i]) . "%'
+                )
+            ";
+E_O_F;
+}
+
+
+echo isset($op) ? '' : "\n\n\n";
Index: tags/2.2.3/bin/module_maker/validation.cli.php
===================================================================
--- tags/2.2.3/bin/module_maker/validation.cli.php	(revision 632)
+++ tags/2.2.3/bin/module_maker/validation.cli.php	(revision 632)
@@ -0,0 +1,352 @@
+#!/usr/bin/env php
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * validation.cli.php
+ */
+
+if ($_SERVER['argc'] > 1 && isset($_SERVER['argv'][1]) && '' != $_SERVER['argv'][1] && is_dir($_SERVER['argv'][1])) {
+    // First arg is path to current site. Realpath removes trailing /s
+    define('COMMON_BASE', realpath($_SERVER['argv'][1]));
+} else {
+    die("Error: First argument must be the directory path to an existing site (ex: /home/sc/www.strangecode.com).\n");
+}
+
+include_once dirname(__FILE__) . '/../_config.inc.php';
+
+// Test arguments.
+if (isset($_SERVER['argv'][2])) {
+    // Second arg is db table.
+    $db_tbl = $_SERVER['argv'][2];
+} else {
+    die(sprintf("Usage: %s site_directory db_table\n", basename($_SERVER['argv'][0])));
+}
+
+// Get DB tables.
+$qid = $db->query("SHOW TABLES");
+while (list($row) = mysql_fetch_row($qid)) {
+    $tables[] = $row;
+}
+
+// Make sure requested table is in database.
+if (!in_array($db_tbl, $tables)) {
+    die(sprintf("%s Warning: %s does not exist in database %s. Please select one of: \n\n%s\n\n", basename($_SERVER['argv'][0]), $db_tbl, $app->getParam('db_name'), join("\n", $tables)));
+}
+
+// Get DB table column info.
+$qid = $db->query("DESCRIBE " . $db->escapeString($db_tbl));
+while ($row = mysql_fetch_row($qid)) {
+    $cols[] = $row;
+}
+
+// Exclude these fields.
+$exclude = array('added_by_user_id', 'added_datetime', 'hit_count', 'modified_datetime', 'modified_by_user_id');
+
+// Loop through columns
+if (is_array($cols) && !empty($cols)) {
+
+    $o = array();
+
+    foreach ($cols as $col) {
+
+        // Human readable.
+        $field = $col[0];
+        $title = ucfirst(str_replace('_', ' ', $field));
+        $type = preg_replace('/^(\w+).*$/', '$1', $col[1]);
+        $max_dig = preg_replace('/^\w+\((\d+)(?:,\d+)?\).*$/', '$1', $col[1]);
+        $max_dec = preg_replace('/^\w+\((?:\d+)(?:,(\d+))?\).*$/', '$1', $col[1]);
+        $is_primary_key = ('PRI' == $col[3]);
+        $unsigned = preg_match('/\s+unsigned\s*$/i', $col[1]);
+
+        if (in_array($field, $exclude)) {
+            continue;
+        }
+
+        // ----------- isEmpty ------------
+        $o[] = "\$fv->notEmpty('$field', sprintf(_(\"%s cannot be blank.\"), _(\"$title\")));";
+
+        // ----------- stringLength ------------
+        $max_length = null;
+        $min = null;
+        $max = null;
+        $len_type = null;
+        switch ($type) {
+        case 'enum' :
+        case 'set' :
+            $max_length = 255;
+            $len_type = 'setenum';
+            break;
+
+        case 'date' :
+        case 'datetime' :
+        case 'timestamp' :
+        case 'time' :
+        case 'year' :
+            $len_type = 'string';
+            $max_length = 127;
+            break;
+
+        case 'tinytext' :
+        case 'tinyblob' :
+        case 'char' :
+        case 'varchar' :
+            $len_type = 'string';
+            $max_length = '' != $max_dig ? $max_dig : 255;
+            break;
+
+        case 'text' :
+        case 'blob' :
+            $len_type = 'string';
+            $max_length = 65535;
+            break;
+
+        case 'mediumtext' :
+        case 'mediumblob' :
+            $len_type = 'string';
+            $max_length = 16777215;
+            break;
+
+        case 'longtext' :
+        case 'longblob' :
+            $len_type = 'string';
+            $max_length = 4294967295;
+            break;
+
+        case 'bit' :
+            $len_type = 'num';
+            $min = 0;
+            $max = '' != $max_dig ? $max_dig : 64;
+            break;
+
+        case 'tinyint' :
+        case 'bool' :
+            $len_type = 'num';
+            if ($unsigned) {
+                $min = 0;
+                $max = 255;
+            } else {
+                $min = -128;
+                $max = 127;
+            }
+            break;
+
+        case 'smallint' :
+            $len_type = 'num';
+            if ($unsigned) {
+                $min = 0;
+                $max = 65536;
+            } else {
+                $min = -32768;
+                $max = 32767;
+            }
+            break;
+
+        case 'mediumint' :
+            $len_type = 'num';
+            if ($unsigned) {
+                $min = 0;
+                $max = 16777215;
+            } else {
+                $min = -8388608;
+                $max = 8388607;
+            }
+            break;
+
+        case 'int' :
+        case 'integer' :
+            $len_type = 'num';
+            if ($unsigned) {
+                $min = 0;
+                $max = 4294967295;
+            } else {
+                $min = -2147483648;
+                $max = 2147483647;
+            }
+            break;
+
+        case 'bigint' :
+            $len_type = 'num';
+            if ($unsigned) {
+                $min = 0;
+                $max = 1.84467E+19;
+            } else {
+                $min = -9.22337E+18;
+                $max = 9.22337E+18;
+            }
+            break;
+
+        case 'float' :
+            $len_type = 'num';
+            if ($unsigned) {
+                $min = 0;
+                $max = 3.40282E+38;
+            } else {
+                $min = -3.40282E+38;
+                $max = 3.40282E+38;
+            }
+            break;
+
+        case 'double' :
+        case 'double precision' :
+            $len_type = 'num';
+            if ($unsigned) {
+                $min = 0;
+                $max = 1.7976931348623157E+308;
+            } else {
+                $min = -1.7976931348623157E+308;
+                $max = 1.7976931348623157E+308;
+            }
+            break;
+
+        case 'real' :
+        case 'decimal' :
+        case 'dec' :
+        case 'numeric' :
+        case 'fixed' :
+            $len_type = 'decimal'; // This doesn't match anything, on purpose.
+            break;
+
+        default :
+            $len_type = null;
+            break;
+        }
+        if ($max_length > 0 && $len_type == 'setenum') {
+            $o[] = "\$fv->stringLength('$field', 0, $max_length, sprintf(_(\"%s has an invalid selection.\"), _(\"$title\")));";
+        }
+        if ($max_length > 0 && $len_type == 'string') {
+            $o[] = "\$fv->stringLength('$field', 0, $max_length, sprintf(_(\"%s must be %d-to-%d characters in length.\"), _(\"$title\"), 0, $max_length));";
+        }
+        if ($len_type == 'num') {
+            $o[] = "\$fv->numericRange('$field', $min, $max, sprintf(_(\"%s must be a number between %d and %d.\"), _(\"$title\"), $min, $max));";
+        }
+
+        // ----------- type check ------------
+        switch ($type) {
+
+        case 'enum' :
+
+        case 'set' :
+            break;
+
+        case 'char' :
+        case 'varchar' :
+
+        case 'tinytext' :
+        case 'text' :
+        case 'mediumtext' :
+        case 'longtext' :
+        case 'tinyblob' :
+        case 'blob' :
+        case 'mediumblob' :
+        case 'longblob' :
+//             $o[] = "\$fv->isString('$field', _(\"<strong>$title</strong> must be a string.\"));"; // Pretty useless
+            break;
+
+        case 'tinyint' :
+        case 'bit' :
+        case 'bool' :
+        case 'smallint' :
+        case 'mediumint' :
+        case 'int' :
+        case 'integer' :
+        case 'bigint' :
+            $negative_ok = $unsigned ? '' : ', true';
+            $o[] = "\$fv->isInteger('$field', sprintf(_(\"%s must be an integer.\"), _(\"$title\"))$negative_ok);";
+            break;
+
+        case 'float' :
+        case 'double' :
+            $negative_ok = $unsigned ? '' : ', true';
+            $o[] = "\$fv->isFloat('$field', sprintf(_(\"%s must be a valid number.\"), _(\"$title\"))$negative_ok);";
+            break;
+
+        case 'real' :
+        case 'decimal' :
+        case 'dec' :
+        case 'numeric' :
+        case 'float' :
+            $max = str_repeat('9', $max_dig - $max_dec);
+            if ($max_dec > 0) {
+                $max .= '.' . str_repeat('9', $max_dec);
+            }
+            if ($unsigned) {
+                $negative_ok = 'false';
+                $min = 0;
+            } else {
+                $negative_ok = 'true';
+                $min = -$max;
+            }
+            $negative_ok = $unsigned ? 'false' : 'true';
+            $o[] = "\$fv->isDecimal('$field', sprintf(_(\"%s must be a number between %d and %d.\"), _(\"$title\"), $min, $max), $negative_ok, $max_dig, $max_dec);";
+            break;
+
+        case 'date' :
+        case 'datetime' :
+            // \$fv->validateStrDate('$field', sprintf(_(\"%s must be a valid date in YYYY-MM-DD format.\"), _(\"$title\")));
+            $o[] = "\$fv->validateStrDate('$field', sprintf(_(\"%s must be a valid date in YYYY-MM-DD format.\"), _(\"$title\")));";
+            $o[] = "\$fv->checkRegex('$field', '/^\d{4}-\d{2}-\d{2}$/', true, sprintf(_(\"%s must be in YYYY-MM-DD format.\"), _(\"$title\")));";
+            break;
+
+        case 'timestamp' :
+            $o[] = "\$fv->checkRegex('$field', '/^\d{14}$/', true, sprintf(_(\"%s must be a valid mysql timestamp in YYYYMMDDhhmmss format.\"), _(\"$title\")));";
+            break;
+
+        case 'time' :
+            $o[] = "\$fv->checkRegex('$field', '/^\d{1,3}:\d{2}:\d{2}$/', true, sprintf(_(\"%s must be a valid time in hh:mm:ss format.\"), _(\"$title\")));";
+            break;
+
+        case 'year' :
+            $o[] = "\$fv->checkRegex('$field', '/^\d{4}$/', true, sprintf(_(\"%s must be a valid year in YYYY format.\"), _(\"$title\")));";
+            break;
+
+        default :
+            break;
+        }
+
+        if (preg_match('/email/', $field)) {
+            $o[] = "\$fv->validateEmail('$field');";
+        } else if (preg_match('/phone/', $field)) {
+            $o[] = "\$fv->validatePhone('$field');";
+        }
+
+        // Blank between cols?
+        $o[] = '';
+    }
+} else {
+    die(sprintf("%s Warning: %s does not have any columns.\n", basename($_SERVER['argv'][0]), $db_tbl));
+}
+
+// Sort?
+// natsort($o);
+
+// Remove last empty element.
+if ('' == end($o)) {
+    array_pop($o);
+}
+
+echo "function validateInput()
+{
+    global \$fv;
+
+    " . join("\n    ", $o) . "
+}";
Index: tags/2.2.3/bin/pedit_permissions.sh
===================================================================
--- tags/2.2.3/bin/pedit_permissions.sh	(revision 632)
+++ tags/2.2.3/bin/pedit_permissions.sh	(revision 632)
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+dirroot=$1
+
+if [ -f "$dirroot/_config.inc.php" ]
+then
+    cd $dirroot
+    find . -type d | xargs chmod 777
+    find . -type f | xargs chmod 666
+    chmod 644 login.php
+    chmod 644 _config.inc.php
+    echo "Permissions fixed!"
+else
+    echo "$0: This script must be called with the web directory root as an argument. (eg. '$0 /home/user/www'). And the web directory root must contain a _config.inc.php file (just a simple test to make sure we run this script in the correct place)."
+    exit;
+fi
Index: tags/2.2.3/composer.json
===================================================================
--- tags/2.2.3/composer.json	(revision 632)
+++ tags/2.2.3/composer.json	(revision 632)
@@ -0,0 +1,20 @@
+{
+    "name": "strangecode/codebase",
+    "type": "project",
+    "description": "The Strangecode Codebase - a general application framework for PHP",
+	"license": "GPL-3.0+",
+    "authors": [
+        {
+            "name": "Quinn Comendant",
+            "email": "quinn@strangecode.com",
+            "homepage": "http://www.strangecode.com/",
+            "role": "Developer"
+        }
+    ],
+    "require": {
+        "php": "^5.3.7"
+    },
+    "require-dev": {
+        "phpunit/phpunit": "~3.7.0"
+    }
+}
Index: tags/2.2.3/css/admin.inc.css
===================================================================
--- tags/2.2.3/css/admin.inc.css	(revision 632)
+++ tags/2.2.3/css/admin.inc.css	(revision 632)
@@ -0,0 +1,313 @@
+/**
+ * admin.inc.css
+ *
+ * CSS for a Strangecode administration site.
+ */
+
+/*_____________________ GLOBAL ____________________*/
+body {
+    background: #fff;
+    margin: 0;
+    padding: 0;
+    font-family: verdana,geneva,arial,sans-serif;
+    color: #000;
+}
+
+a:link,
+a:visited { color: #336; text-decoration: underline; }
+a:hover   { color: #C30; text-decoration: none; }
+a:active  { color: #336; }
+
+pre { margin: 0; }
+
+/*_____________________ MISC ____________________*/
+
+/* The following are here in addition to in utilities.inc.css only because I want the font size 70%. Is there a more general way to do this? There currently are NO font-size specifications in utilities.inc.css or codebase.inc.css. That seems safest, but requires that they are redefined anyways (like here for eg). Have you figured out how to do font sizes correctly? */
+
+/* Why does having a form change the presentation of the page? Don't do that. */
+form { margin: 0; }
+
+form label {
+    font-size: 70%;
+    max-width: 25em;
+    font-weight: bold;
+}
+
+.help,
+.sc-help,
+.commandtext,
+.commanditem { font-size: 70%; }
+
+/* For fv err applied to tds. */
+td.sc-msg-warning,
+td.sc-msg-error,
+td.sc-msg-success,
+td.sc-msg-notice {
+	background: #fff;
+	color: #f00;
+	border: none;
+}
+
+
+/*_____________________ HEADER ____________________*/
+#userinfo {
+    width: 98%;
+    margin: 4px auto 2px;
+    background-color: #fff;
+    color: #333;
+    text-align: right;
+}
+
+#userinfo p {
+    margin: 0;
+    padding: 0;
+}
+
+#container {
+    width: 98%;
+    margin: 0 auto 10px;
+    background-color: #EEE;
+    border: 1px solid gray;
+}
+
+#top {
+    border-bottom: 1px solid gray;
+    background-color: #CCC;
+    padding: .5em;
+}
+
+#top h1 {
+    margin: 0;
+    padding: 0;
+    font-size: 150%;
+    color: #000;
+}
+
+/*_____________________ NAVIGATION ____________________*/
+#leftnav {
+    float: left;
+    width: 9em;
+    margin: 0 0 10px 0;
+    border-right: 1px solid gray;
+    padding: 0;
+    background-color: #EEE;
+}
+
+#leftnav ul {
+    padding: 0;
+    margin: 0;
+    width: 9em;
+}
+
+#leftnav #navlist li {
+    list-style: none;
+    margin: 0;
+    border-bottom: 1px solid #ccc;
+    margin-left: 0em;
+    padding: 1px 0 2px 6px;
+    font-size: 70%;
+    font-weight: bold;
+}
+
+#leftnav #navlist li a {
+    text-decoration: none;
+    padding: 1px;
+}
+
+#leftnav #navlist li.label {
+    color: #999;
+	padding-top: 15px;
+	font-size: 90%;
+    font-weight: bold;
+}
+
+#leftnav a.addlink {
+    margin-left: 0.5em;
+    font-weight: normal;
+}
+
+#leftnav #navlist li a:link, #navlist li a:visited {
+    color: #336;
+}
+
+#leftnav #navlist li a:hover, #navlist li a:active {
+    color: #C00;
+}
+
+/*_____________________ CONTENT ____________________*/
+#content {
+    margin-left: 9em;
+    border-left: 1px solid gray;
+    padding: 0.75em 0.5em 2em 0.5em;
+    background-color: #FFF;
+}
+
+#content h1, #content h2, #content h3, #content h4, #content h5 {
+    margin: 0 0 .5em 0;
+}
+#content h1 { font-size: 130%; }
+#content h2 { font-size: 110%; }
+#content h3 { font-size: 90%; }
+#content h4 { font-size: 75%; }
+
+#content p {
+    margin: 0 0 .5em 0;
+    font-size: 70%;
+    line-height: 130%;
+}
+
+#content li {
+    margin: 0 0 0 0;
+    font-size: 70%;
+    line-height: 100%;
+}
+
+/*_____________________ TABLES ____________________*/
+/* BEAU: I copied this from NEXT. Please help! */
+table {
+	border-size: 1px;
+	border-collapse: collapse;
+	border-spacing: 0;
+}
+
+th {
+	padding: 2px;
+	text-align: left;
+	vertical-align: middle;
+	white-space: nowrap;
+}
+
+td {
+	vertical-align: top;
+    padding: 8px 6px 0 0;
+}
+
+table.list {
+	border-size: 1px;
+	width: 100%;
+	margin: 4px 0;
+}
+
+table.list th {
+	padding: 0 6px 1px 2px;
+	font-size: 70%;
+    border-top: 2px solid #eee;
+    border-bottom: 2px solid #eee;
+    background-color: #ddd;
+/*  border-bottom: 1px solid #000; */
+}
+
+table.list td, #commandbox table td {
+	font-size: 70%;
+	padding: 2px 6px 2px 2px;
+	border-bottom: 1px dotted #ccc;
+	vertical-align: top;
+}
+
+table.list tr:hover, #commandbox table tr:hover {
+    background: #FAF8C7;
+}
+
+
+/* BEAU: The old stuff that was here...
+#content table {
+         background-color: #FFF;
+}
+
+#content th {
+    padding: 2px;
+    text-align: left;
+    vertical-align: middle;
+    white-space: nowrap;
+}
+
+#content td {
+    vertical-align: top;
+}
+
+#content table.list {
+    width: 99%;
+    margin: 4px 0;
+    background-color: #FFF;
+}
+
+#content table.list th {
+    padding-right: 6px;
+    font-size: 70%;
+    background-color: #CCC;
+    border-bottom: 1px solid #000;
+}
+
+#content table.list td {
+    font-size: 70%;
+    padding: 2px;
+    border-bottom: 1px dotted #000;
+}
+*/
+
+/*_____________________ COMMANDS ____________________*/
+
+/* Should this be an ID??? */
+#commandbox, .commandbox
+{
+	padding: 3px 5px 5px 5px;
+	margin: 0 0 10px 0;
+    border: 2px solid #eee;
+    background-color: #ddd;
+	color: inherit;
+}
+
+.commanditem
+{
+	padding: 0 10px 5px 0;
+	white-space: nowrap;
+}
+
+#commandbox .form, .commandbox .form, #commandbox form, .commandbox form, {
+    margin-top: 8px;
+}
+
+/* I replaced all instances of commandtext with commanditem on the SC admin, but leave it here for sites I didn't. */
+.commandtext
+{
+	padding-right: 1em;
+}
+
+.command_left
+{
+	float: left;
+	white-space: nowrap;
+}
+
+.command_right
+{
+	position: relative;
+	visibility: visible;
+	white-space: nowrap;
+	float: right;
+}
+
+
+/*_____________________ FOOTER ____________________*/
+#footer {
+    clear: both;
+    margin: 0;
+    padding: 6px;
+    color: #333;
+    background-color: #DDD;
+    border-top: 1px solid gray;
+    font-size: 70%;
+    line-height: 100%;
+}
+
+#footer a:link,
+#footer a:visited {
+    color: #333;
+    text-decoration: none;
+}
+
+#footer a:active,
+#footer a:hover {
+    color: #333;
+    text-decoration: underline;
+}
Index: tags/2.2.3/css/admin2.inc.css
===================================================================
--- tags/2.2.3/css/admin2.inc.css	(revision 632)
+++ tags/2.2.3/css/admin2.inc.css	(revision 632)
@@ -0,0 +1,394 @@
+/**
+ * admin2.inc.css
+ *
+ * CSS for the Strangecode administration sites.
+ */
+
+/*_____________________ GLOBAL ____________________*/
+body {
+    background: #fff;
+    margin: 0;
+    padding: 0;
+    font-family: verdana,geneva,arial,sans-serif;
+    color: #000;
+}
+
+a:link,
+a:visited { color: #336; text-decoration: underline; }
+a:hover   { color: #C30; text-decoration: none; }
+a:active  { color: #336; }
+
+pre { margin: 0; }
+
+/*_____________________ MISC ____________________*/
+
+/* The following are here in addition to in utilities.inc.css only because I want the font size 70%. Is there a more general way to do this? There currently are NO font-size specifications in utilities.inc.css or codebase.inc.css. That seems safest, but requires that they are redefined anyways (like here for eg). Have you figured out how to do font sizes correctly? */
+
+/* Why does having a form change the presentation of the page? Don't do that. */
+/* form { margin: 0; }
+
+form label {
+    font-size: 70%;
+    max-width: 25em;
+    font-weight: bold;
+}
+ */
+.help,
+.sc-help,
+.commandtext,
+.commanditem { font-size: 0.7rem; }
+
+/* For fv err applied to tds. */
+td.sc-msg-warning,
+td.sc-msg-error,
+td.sc-msg-success,
+td.sc-msg-notice {
+	background: #fff;
+	color: #f00;
+	border: none;
+}
+
+img {
+    border: 0;
+	vertical-align: middle;
+}
+
+#container-columns {
+    width: 100%;
+}
+
+/*_____________________ HEADER ____________________*/
+#userinfo {
+    width: 98%;
+    margin: 4px auto 2px;
+    color: #333;
+    text-align: right;
+}
+
+#userinfo p {
+    margin: 0;
+    padding: 0;
+}
+
+#container {
+    width: 98%;
+    margin: 0 auto 10px;
+    background-color: #fff;
+    border: 1px solid gray;
+}
+
+#top {
+    border-bottom: 1px solid gray;
+    background-color: #CCC;
+    padding: .5em;
+}
+
+#top h1 {
+    margin: 0;
+    padding: 0;
+    font-size: 1.5rem;
+    color: #000;
+}
+
+/*_____________________ NAVIGATION ____________________*/
+#leftnav {
+/*     float: left; */
+    min-width: 10em;
+    width: 10em;
+    _width: 10em;  /* IE6 hack */
+    margin: 0 0 10px 0;
+    padding: 0;
+    background-color: #EEE;
+}
+
+#leftnav ul {
+    padding: 0;
+    margin: 0;
+}
+
+#leftnav #navlist li {
+    list-style: none;
+    margin: 0;
+    border-bottom: 1px solid #ccc;
+    margin-left: 0em;
+    padding: 1px 0 2px 6px;
+    font-size: 0.7rem;
+    font-weight: bold;
+}
+
+#leftnav #navlist li a {
+    text-decoration: none;
+    padding: 1px;
+}
+
+#leftnav #navlist li.label {
+    color: #999;
+	padding-top: 15px;
+	font-size: 0.9rem;
+    font-weight: bold;
+}
+
+#leftnav a.addlink {
+    margin-left: 0.25em;
+    font-weight: normal;
+}
+
+#leftnav #navlist li a:link, #navlist li a:visited {
+    color: #336;
+}
+
+#leftnav #navlist li a:hover, #navlist li a:active {
+    color: #C00;
+}
+
+/*_____________________ CONTENT ____________________*/
+
+#content-table {
+    width: 100%;
+}
+
+#content {
+/*     width: 100%; */
+/*     margin-left: 1em; */
+/*     border-left: 1px solid gray; */
+    padding: 0.75em 0.5em 15px 0.75em;
+    background-color: #FFF;
+}
+
+#content h1, #content h2, #content h3, #content h4, #content h5 {
+    margin: 0 0 .5em 0;
+}
+#content h1 { font-size: 1.3rem; }
+#content h2 { font-size: 1.1rem; }
+#content h3 { font-size: 0.9rem; }
+#content h4 { font-size: 0.75rem; }
+
+#content p {
+    margin: 0 0 .5em 0;
+    font-size: 0.7rem;
+    line-height: 130%;
+}
+
+#content li {
+    margin: 0 0 0 0;
+    font-size: 0.7rem;
+    line-height: 100%;
+}
+
+/*_____________________ TABLES ____________________*/
+/* BEAU: I copied this from NEXT. Please help! */
+table {
+	border-size: 1px;
+	border-collapse: collapse;
+	border-spacing: 0;
+}
+
+th {
+	padding: 2px;
+	text-align: left;
+	vertical-align: middle;
+	white-space: nowrap;
+}
+
+td {
+	vertical-align: top;
+    padding: 8px 6px 0 0;
+}
+
+table.list {
+	border-size: 1px;
+	width: 100%;
+	margin: 6px 0;
+}
+
+table.list th {
+	padding: 0 6px 1px 2px;
+	font-size: 0.7rem;
+    border-top: 2px solid #eee;
+    border-bottom: 2px solid #eee;
+    background-color: #ddd;
+/*  border-bottom: 1px solid #000; */
+}
+
+table.list td, #commandbox table td, .commandbox table td {
+	font-size: 0.7rem;
+	padding: 2px 6px 2px 2px;
+	border-bottom: 1px dotted #ccc;
+	vertical-align: top;
+}
+
+table.list tr:hover, #commandbox table tr:hover, .commandbox table tr:hover {
+    background: #FAF8C7;
+}
+
+/*_____________________ COMMANDS ____________________*/
+
+/* We were using an ID, now using class. */
+#commandbox, .commandbox
+{
+	padding: 3px 5px 5px 5px;
+	margin: 0 0 10px 0;
+    border: 2px solid #eee;
+    background-color: #ddd;
+	color: inherit;
+}
+
+.commanditem
+{
+	padding: 0 10px 5px 0;
+	white-space: nowrap;
+}
+
+#commandbox form, .commandbox form {
+    margin: 8px 0 0 0;
+}
+
+/* I replaced all instances of commandtext with commanditem on the SC admin, but leave it here for sites I didn't. */
+.commandtext
+{
+	padding-right: 1em;
+}
+
+.command_left
+{
+	float: left;
+	white-space: nowrap;
+}
+
+.command_right
+{
+	position: relative;
+	visibility: visible;
+	white-space: nowrap;
+	float: right;
+}
+
+
+/*_____________________ FOOTER ____________________*/
+#footer {
+    clear: both;
+    margin: 0;
+    padding: 6px;
+    color: #333;
+    background-color: #DDD;
+    border-top: 1px solid gray;
+    font-size: 0.7rem;
+    line-height: 100%;
+}
+
+#footer a:link,
+#footer a:visited {
+    color: #333;
+    text-decoration: none;
+}
+
+#footer a:active,
+#footer a:hover {
+    color: #333;
+    text-decoration: underline;
+}
+
+/* Form Defaults ---------------------------------------------------- */
+
+.sc-form {
+    width: 100%;
+    margin: 1em 0;
+}
+
+.sc-form-row {
+    margin: .5em 0;
+}
+.sc-form-row:after { content: " "; display: block; visibility: hidden; clear: both; height: 0.1px; font-size: 0.1em; line-height: 0; } /* clear floats - Ydnar Hack */
+
+fieldset {
+    margin: 1em 0 0 0;
+    padding: 0;
+	border: 0;
+}
+
+legend {
+    font-size: 90%;
+    font-weight: bold;
+    padding: 0;
+	margin: 0;
+}
+
+.sc-form textarea,
+.sc-tiny,
+.sc-small,
+.sc-medium,
+.sc-large,
+.sc-full,
+#sc-captcha-input,
+input[type="text"],
+input[type="password"],
+textarea,
+select {
+    border: 1px solid #ccc;
+    margin: 0 0.5rem 0 0;
+    padding: 2px 0.25rem;
+    box-sizing: border-box;
+}
+label.sc-full {
+    border: none;
+}
+
+textarea {
+    padding: 0 2px;
+}
+
+.sc-form-row label {
+    font-size: 0.7rem;
+    padding-top: 0.25em;
+}
+
+.sc-form-row label { /* Modified by Matt. Was .sc-form-row>label:first-child */
+    display: block;
+    width: 120px;
+    float: left;
+    text-align: right;
+    padding-right: 10px;
+    font-style: italic;
+}
+
+.sc-form-row label.sc-label-secondary {
+    display: inline;
+    width: auto;
+    float: none;
+    text-align: left;
+    padding-right: 10px;
+    font-style: normal;
+}
+
+.sc-form label span {
+    visibility: hidden;
+}
+
+/*.sc-form-row input + input {
+    margin-top: .5rem;
+    margin-bottom: .5rem;
+} */
+
+input[type="submit"],
+input[type="button"] {
+    margin-right: 5px;
+    padding: 0.4em 0.6em;
+    border: 1px solid #ccc;
+    background-color: #eee;
+}
+
+.sc-submit-buttons {
+    margin: 20px 0 20px 130px;
+}
+.sc-form-row .sc-help {
+    margin-left: 130px;
+}
+
+label.sc-radio-label {
+    width: auto;
+}
+
+/* Hide foundation elements for sites still using old admin css. */
+.sc-msg .close {
+    display: none;
+}
Index: tags/2.2.3/css/codebase.inc.css
===================================================================
--- tags/2.2.3/css/codebase.inc.css	(revision 632)
+++ tags/2.2.3/css/codebase.inc.css	(revision 632)
@@ -0,0 +1,77 @@
+/**
+ * codebase.inc.css
+ *
+ * CSS used in codebase templates and scripts that generate HTML. Do not arbitrarily
+ * edit this code! This file is used between many sites and modifying it may result
+ * in something breaking. Instead, overwrite these values in a specific site by
+ * adding new definitions to a screen.css file.
+ */
+
+/* App.inc.php ------------------------------------------------------ */
+.sc-msg {
+    z-index: 2000000000;  /* Close to the maximum of a 32-bit integer. */
+    margin: 1em 0;
+    background: #fafafa;
+    padding: 0;
+    border: none;
+}
+form .sc-msg-success label { color: #76B15B; font-weight: bold; }
+form .sc-msg-notice label { color: #B19332; font-weight: bold; }
+form .sc-msg-warning label { color: #9200AE; font-weight: bold; }
+form .sc-msg-error label { color: #b34; font-weight: bold; }
+.sc-msg > .sc-msg-success { color: #76B15B; font-weight: bold; }
+.sc-msg > .sc-msg-notice { color: #B19332; font-weight: bold; }
+.sc-msg > .sc-msg-warning { color: #9200AE; font-weight: bold; }
+.sc-msg > .sc-msg-error { color: #b34; font-weight: bold; }
+.sc-msg > .sc-msg-success,
+.sc-msg > .sc-msg-notice,
+.sc-msg > .sc-msg-warning,
+.sc-msg > .sc-msg-error {
+    padding: .25em .5em;
+    margin: 5px 0;
+    color: #fff;
+}
+.sc-msg > .sc-msg-success a,
+.sc-msg > .sc-msg-notice a,
+.sc-msg > .sc-msg-warning a,
+.sc-msg > .sc-msg-error a {
+    color: #fff;
+    text-decoration: underline;
+}
+.sc-msg > .sc-msg-success { background-color: #76B15B; }
+.sc-msg > .sc-msg-notice { background-color: #B19332; }
+.sc-msg > .sc-msg-warning { background-color: #9200AE; }
+.sc-msg > .sc-msg-error { background-color: #b34; }
+
+/* PEdit.inc.php ---------------------------------------------------- */
+.sc-pedit-form {}
+.sc-pedit-item,
+.sc-pedit-buttons { margin: .75em 0; }
+.sc-pedit-buttons { clear: both; }
+#sc-pedit-versions-table {
+    margin-bottom: .75em;
+    border-collapse: collapse;
+    border-spacing: 0;
+}
+#sc-pedit-versions-table th,
+#sc-pedit-versions-table td {
+    border-bottom: 1px solid #CC9;
+    margin: 0;
+    padding: 2px 10px;
+    vertical-align: baseline;
+}
+#sc-pedit-versions-table th {
+    white-space: nowrap;
+}
+
+/* Utilities.inc.php ------------------------------------------------ */
+.sc-highlightwords {
+    background-color: #EBFE00;
+    color: #000;
+}
+
+/* Captcha.inc.php -------------------------------------------------- */
+#sc-captcha {
+    font-size: 0.5em;
+    line-height: 0.8em;
+}
Index: tags/2.2.3/css/utilities.inc.css
===================================================================
--- tags/2.2.3/css/utilities.inc.css	(revision 632)
+++ tags/2.2.3/css/utilities.inc.css	(revision 632)
@@ -0,0 +1,83 @@
+/**
+ * utilities.inc.css
+ *
+ * CSS used often in Strangecode applications. Do not arbitrarily
+ * edit this code! This file is used between many sites and modifying it may result
+ * in something breaking. Instead, overwrite these values in a specific site by
+ * adding new definitions to a screen.css file.
+ */
+
+/*_____________________ FORMS ____________________*/
+
+input.sc-micro, textarea.sc-micro, select.sc-micro,
+input.sc-tiny, textarea.sc-tiny, select.sc-tiny,
+input.sc-small, textarea.sc-small, select.sc-small,
+input.sc-medium, textarea.sc-medium, select.sc-medium,
+input.sc-large, textarea.sc-large, select.sc-large,
+input.sc-full, textarea.sc-full, select.sc-full {
+    margin: 0 0.5rem 0 0;
+    padding: 2px 0.25rem;
+    border-width: 1px;
+    box-sizing: border-box;
+}
+
+/*
+The following widths are calculated to include a small right-margin and be equally divisible so the right edges align.
+The formula is `width = (maxwidth - (rightmargin * multiple)) / multiple`
+where width is the sought value, maxwidth is the width of a sc-large, and multiple is the number
+of times an element fits within maxwidth (i.e. sc-micro has a multiple of 16).
+The values are dependent on the margin, padding, and border set above.
+(44-(0.5*1))/1 = 43.5
+(44-(0.5*2))/2 = 21.5
+(44-(0.5*4))/4 = 10.5
+(44-(0.5*8))/8 = 5
+(44-(0.5*8))/16 = 2.5
+Note: avoid adding whitespace between input elements, or add `display: flex;` to the inputs' parent.
+*/
+input.sc-micro, textarea.sc-micro, select.sc-micro, label.sc-micro      { width: 2.5rem; }
+input.sc-tiny, textarea.sc-tiny, select.sc-tiny, label.sc-tiny          { width: 5rem; }
+input.sc-small, textarea.sc-small, select.sc-small, label.sc-small      { width: 10.5rem; }
+input.sc-medium, textarea.sc-medium, select.sc-medium, label.sc-medium  { width: 21.5rem; }
+input.sc-large, textarea.sc-large, select.sc-large, label.sc-large      { width: 43.5rem; }
+input.sc-full, textarea.sc-full, select.sc-full, label.sc-full          { width: 100%; }
+
+select.sc-small     { width: 10rem; }
+select.sc-medium    { width: 20.5rem; }
+
+textarea.sc-twolines, select.sc-twolines              { height: 3rem; }
+textarea.sc-short, select.sc-short                    { height: 8rem; }
+textarea.sc-tall, select.sc-tall                      { height: 25rem; }
+textarea.sc-fullscreen, select.sc-fullscreen          { height: 40rem; }
+
+/*_____________________ UTILITIES ____________________*/
+.sc-center { text-align: center; }
+.sc-clearboth { clear: both; }
+.sc-help { color: #999; font-weight: normal; }
+.sc-hidden { display: none; }
+.sc-monospaced { font-family: monospace; }
+.sc-padleft { padding-left: 10px; }
+.sc-padright { padding-right: 10px; }
+.sc-marginleft { margin-left: 10px; }
+.sc-marginright { margin-right: 10px; }
+.sc-right { text-align: right; }
+.sc-normal { font-weight: normal; }
+.sc-nowrap { white-space: nowrap; }
+.sc-dim {
+    filter:alpha(opacity=33);
+    -moz-opacity:.33;
+    opacity:.33;
+}
+.sc-note:hover { cursor: help; }
+.sc-ellipsis {
+    overflow: hidden;
+    white-space: nowrap;
+    text-overflow: ellipsis;
+}
+
+/* .sc-pkg class wraps enclosing block element around inner floated elements */
+.sc-pkg:after { content: " "; display: block; visibility: hidden; clear: both; height: 0.1px; font-size: 0.1em; line-height: 0.0; }
+* html .sc-pkg { display: block; }
+/* no ie mac \*/
+* html .sc-pkg { height: 1%; }
+.sc-pkg { display: block; }
+/* */
Index: tags/2.2.3/docs/codebase_v1-to-v2_upgrade_checklist.txt
===================================================================
--- tags/2.2.3/docs/codebase_v1-to-v2_upgrade_checklist.txt	(revision 632)
+++ tags/2.2.3/docs/codebase_v1-to-v2_upgrade_checklist.txt	(revision 632)
@@ -0,0 +1,303 @@
+=====================================================================
+In General
+=====================================================================
+
+1. Include methods have changed. Instead of accessing file paths absolutely, all files are searched in the include_path. So, instead of:
+
+    include SITE_BASE . '/_templates/header.ihtml';
+
+do this:
+
+    include 'header.ihtml';
+
+
+2. CODE_BASE is a defunct constant. The location of the codebase is no longer important as long as it can be found in the currently configured include_path. This will usually include the local site directory (where the codebase normally would be found) as well as the server-wide /usr/lib/php directory where the codebase might be included for the whole server. So now, instead of this:
+
+    require_once CODE_BASE . '/lib/Utilities.inc.php';
+
+do this:
+
+    require_once 'codebase/lib/Utilities.inc.php';
+
+
+3. $CFG-> variables are gone. Most of these should be converted into their $app and $auth equivalents. If a $CFG variable is NOT something used by the codebase but is still needed by the website application, I suggest converting these values to a $cfg array. For example, this:
+
+    $CFG->gallery_images_url = '/gallery_images';
+
+should become:
+
+    $cfg['gallery_images_url'] = '/gallery_images';
+
+And of course change the code where they are used to support the array instead of object-properties. If the array is inside of double-quotes it should be written like:
+
+    "{$cfg['gallery_images_url']}/my/path".
+
+If the value used is now to be retrieved from a $object->getParam(...) method call, you'll need to do this:
+
+    $object->getParam('gallery_images_url') . '/my/path'
+
+
+4. $CFG->site_url is not needed when referencing URLs. Change this:
+
+    <a href="<?php echo ohref("$CFG->site_url/my/file.php"); ?>">
+
+to this:
+
+    <a href="<?php echo $app->ohref("/my/file.php"); ?>">
+
+(In other words, the URL should be a not-fully-qualified URL starting with a slash.)
+
+
+// 5. Expect formatting inconsistencies! When doing global search-replace expect whitespace to be erratic, variable names to change, and lines to be otherwise inconsistent. Here's a good example of a safe way to match a line:
+
+Searching for "$CFG->ssl_domain = 'www.example.com';":
+
+//    $CFG->(\w+)\s*=\s*['"](\w+)['"];
+
+Replace:
+
+    $app->setParam(array(
+        '\1' => '\2'
+    ));
+
+
+6. Many classes now require object-method calls, and the object must be globally scoped. For example, to call the $cache->exists() method inside a function, be sure to add:
+
+    global $cache;
+
+at the top of the function.
+
+
+
+=====================================================================
+App
+=====================================================================
+
+The _config.inc.php file is 99% different. Start over from scratch using codebase/docs/example_config.inc.php.
+
+---------------------------------------------------------------------
+$CFG global variables are converted to object properties specific to their usage.
+
+For example:
+
+    $CFG->ssl_domain = 'www.example.com';
+    $CFG->login_timeout = 21600;
+
+are converted to:
+
+    $app->setParam(array(
+        'ssl_domain' => 'www.example.com'
+    ));
+    $auth->setParam(array(
+        'login_timeout'     => 21600,
+    ));
+
+---------------------------------------------------------------------
+Convert functions to methods.
+
+raiseMsg(...)                           $app->raiseMsg(...)
+logMsg(...)                             $app->logMsg(...)
+include 'message_header.ihtml';         $app->printRaisedMessages();
+$carry_queries = array(... , ...);      $app->carryQuery(...);  //call for each value in array
+ohref(...)                              $app->ohref(...)
+printHiddenSession(...);                $app->printHiddenSession(...);
+dieURL(...);                            $app->dieURL(...);
+dieBoomerangURL(...);                   $app->dieBoomerangURL(...);
+setBoomerangURL(...);                   $app->setBoomerangURL(...);
+getBoomerangURL(...);                   $app->getBoomerangURL(...);
+validBoomerangURL(...);                 $app->validBoomerangURL(...);
+deleteBoomerangURL(...);                $app->deleteBoomerangURL(...);
+sslOn();                                $app->sslOn();
+sslOff();                               $app->sslOff();
+
+The following regex will find any of the above:
+(?<!raiseMsg|logMsg|include|ohref|printHiddenSession|dieURL|dieBoomerangURL|setBoomerangURL|getBoomerangURL|validBoomerangURL|deleteBoomerangURL|sslOn|sslOff)
+
+Also convert all site config variables, as earlier:
+$CFG->site_name                         $app->getParam('site_name')
+
+=====================================================================
+DB
+=====================================================================
+
+dbQuery(...)                            $db->query(...)
+dbTableExists(...)                      $db->tableExists(...)
+addslashes(...)                         $db->escapeString(...)
+
+$dbh
+$GLOBALS['dbh']                         $db->getDBH()
+
+=====================================================================
+Auth_SQL
+=====================================================================
+
+$auth->clearAuth()                      $auth->clear()
+$auth->setVal(...)                      $auth->set(...)
+$auth->getVal(...)                      $auth->get(...)
+$auth->setFeature(...)                  $auth->setParam(...)
+$auth->getFeature(...)                  $auth->getParam(...)
+
+
+=====================================================================
+ImageThumb
+=====================================================================
+
+$thumb->setSourceDirectory(...)         $thumb->setParam(array('source_dir' => ...))
+$thumb->createDestDirs(...)             $this->_createDestDirs(...) NOW PRIVATE!
+$thumb->validFileExtension(...)         $this->_validFileExtension(...) NOW PRIVATE!
+
+=====================================================================
+MySQLSessionHandler
+=====================================================================
+
+Renamed to DBSessionHandler.inc.php
+Interface totally changed. See example in App.inc.php.
+
+=====================================================================
+Navigation
+=====================================================================
+
+Renamed to Navigation.inc.php
+
+$nav->addPage(...)                      $nav->add(...)
+$nav->setFeature(array(...))            $nav->setParam(...)
+$nav->getFeature(...)                   $nav->getParam(...)
+$nav->getTitle()                        $nav->get('title')
+$nav->printTitle()                      echo $nav->get('title')
+$nav->getPath()                         $nav->get('path')
+$nav->printPath()                       echo $nav->get('path')
+$nav->printBreadcrumbs()                echo $nav->getBreadcrumbs()
+
+$nav->path_delimiter                    $nav->getParam('path_delimiter')
+NOTE: this applies to any object property that has been converted to a param.
+
+
+=====================================================================
+PEdit
+=====================================================================
+
+Massive changes. See Quinn.
+
+=====================================================================
+Prefs
+=====================================================================
+
+$prefs = new Prefs($dbh, $params)       $prefs = new Prefs('namespace')
+NOTE: new instantiation interface
+
+$prefs->setDefault(..., scope)                 $prefs->setDefaults(array(...))
+NOTE: plurality - function need not be called once-per-default.
+
+$prefs->setValue(..., $scope)           $prefs->set(...)
+$prefs->getValue(..., $scope)           $prefs->get(...)
+$prefs->clearValue(..., $scope)         $prefs->delete(...)
+NOTE: $scope no longer used in the above methods.
+
+$prefs->retrieve()                      $prefs->load()
+
+=====================================================================
+Lock
+=====================================================================
+
+Renamed from RecordLock.inc.php to Lock.inc.php
+
+Only one major interface change: instead of calling $lock->dieErrorPage() you will wrap the header/footer includes around the method call $lock->printErrorHTML().
+
+Also, change instantiation call from:
+
+    $lock = new RecordLock($GLOBALS['_admin']);
+
+to:
+
+    global $lock;
+    global $auth;
+    $lock =& Lock::getInstance($auth);
+
+And instantiate the original global $lock object in _config.inc.php as follows:
+
+    // Global record-locking object.
+    $lock =& Lock::getInstance($auth);
+    $lock->setParam(array(
+        'timeout' => 0,
+        'auto_timeout' => 1800,
+        'error_url' => '/lock.php',
+    ));
+
+
+=====================================================================
+Version
+=====================================================================
+
+Renamed from RecordVersion.inc.php to Version.inc.php
+
+Change usage from:
+
+    $version = new RecordVersion();
+
+to:
+
+    $version =& Version::getInstance($auth);
+
+
+=====================================================================
+Cache
+=====================================================================
+
+Renamed from SessionCache.inc.php to Cache.inc.php
+
+Changed all method calls to now require object calls rather than static calls. In other words, change this:
+
+    if (SessionCache::isCached('mydata')) {
+        $list = SessionCache::getCache('mydata');
+    }
+
+to:
+
+    if ($cache->exists('mydata')) {
+        $list = $cache->get('mydata');
+    }
+
+And have $cache instantiated in _config.inc.php like this:
+
+    // Global cache object.
+    $cache = new Cache('global');
+    $cache->setParam(array('enabled' => true));
+
+
+SessionCache::putCache($val, $key)      $cache->set($key, $val)
+NOTE: notice method arguments have been reversed.
+
+SessionCache::getCache(...)             $cache->get(...)
+SessionCache::isCached(...)             $cache->exists(...)
+SessionCache::breakCache(...)           $cache->delete(...)
+
+
+=====================================================================
+Upload
+=====================================================================
+
+Change all object properties from direct access to method-access:
+
+$file->allow_overwriting = true;        $file->setParam('allow_overwriting', true);
+$file->valid_file_extensions = true;        $file->setParam('valid_file_extensions', true);
+$file->dest_file_perms = 0666;        $file->setParam('dest_file_perms', 0666);
+...etc.
+
+$file->setUploadPath(...)               $file->setParam(array('upload_path' => ...))
+
+
+=====================================================================
+Utilities
+=====================================================================
+
+humanFileSize($size, $unit, $format)    humanFileSize($size, $format, $max_unit)
+NOTE: changed interface
+
+dbArrayToList(...)                      escapedList(...)
+NOTE: The functionality of this has changed, better check output is valid.
+
+=====================================================================
+FormValidator
+=====================================================================
+
+include_once 'form_error_header.ihtml';      $fv->printErrorMessages();
Index: tags/2.2.3/docs/coding_standards.txt
===================================================================
--- tags/2.2.3/docs/coding_standards.txt	(revision 632)
+++ tags/2.2.3/docs/coding_standards.txt	(revision 632)
@@ -0,0 +1,388 @@
+Strangecode coding standards
+29 Aug 2005 20:12:49
+
+
+======================================================================
+Preamble
+======================================================================
+
+Our standards follow closely the PEAR coding standards.
+
+Essential reading:
+http://pear.php.net/manual/en/standards.php
+
+
+======================================================================
+File Naming Conventions
+======================================================================
+
+script.php              Public accessible scripts.
+
+Auth_SQL.inc.php        One PHP Class to be included. The filename is the
+                        type of class, underscore, name. Or if in subdirs,
+                        this could be /Auth/SQL.inc.php while the class name
+                        remains "Auth_SQL"
+
+some_file.inc.html      HTML file, which may or may not have some PHP,
+                        but will be included by some other PHP file. Never
+                        includes sensitive code as these files may be accessed
+                        directly in the web root.
+
+script.cli.php          A command-line executable script, possibly executed
+                        with CRON. Outputs TEXT if any instead of HTML.
+
+script.inc.eml          Text file to be sent by email.
+
+schema.mysql            Database schema file that goes with the application.
+
+main.screen.css         CSS file with media: screen/print/all
+main.print.css
+
+
+======================================================================
+Indenting and Wrap
+======================================================================
+
+Use an indent of 4 spaces, with no tabs. Code and especially comments should
+usually be wrapped <= 80 characters. Exceptions are made in the case where 
+code readability is significantly improved with longer lines.
+
+
+======================================================================
+Control Structures
+======================================================================
+
+These include if, for, while, switch, etc. Here is an example if statement,
+since it is the most complicated of them:
+
+    if ((condition1) || (condition2)) {
+        action1;
+    } else if ((condition3) && (condition4)) {
+        action2;
+    } else {
+        defaultaction;
+    }
+
+Control statements should have one space between the control keyword
+and opening parenthesis, to distinguish them from function calls.
+
+Even in the case of an if with no else clauses, and only a 1 line action,
+the curly braces should still be used:
+
+    if (condition) {
+        action;
+    }
+
+This improves readability and allows easy extension of the clause.
+
+For switch statements:
+
+    switch (condition) {
+    case 1:
+        action1;
+        break;
+
+    case 2:
+        action2;
+        break;
+
+    default:
+        defaultaction;
+        break;
+
+    }
+
+
+======================================================================
+Function Calls
+======================================================================
+
+Functions should be called with no spaces between the function name,
+the opening parenthesis, and the first parameter; spaces between commas
+and each parameter, and no space between the last parameter, the
+closing parenthesis, and the semicolon. Here's an example:
+
+    $var = foo($bar, $baz, $quux);
+
+As displayed above, there should be one space on either side of an
+equals sign used to assign the return value of a function to a
+variable. In the case of a block of related assignments, more space
+may be inserted to promote readability:
+
+    $short         = foo($bar);
+    $long_variable = foo($baz);
+
+
+======================================================================
+Function and Method Definitions
+======================================================================
+
+Function declaractions follow the "one true brace" convention:
+
+    function fooFunction($arg1, $arg2 = '')
+    {
+        if (condition) {
+            statement;
+        }
+        return $val;
+    }
+
+Arguments with default values go at the end of the argument list. Always
+attempt to return a meaningful value from a function if one is appropriate.
+
+Use lowerCamelCase for function and method names to distinguish from php
+internal functions which standard on underscore_space_style_names().
+
+
+======================================================================
+Return Values
+======================================================================
+
+When functions return boolean values, use 'return false;' or 'return true;'
+as opposed to 'return 0;' or 'return 1;' or 'return(-1);', except when
+boolean plurality is necessary.
+
+
+======================================================================
+String Concatenation
+======================================================================
+
+Always include a space before and after the concatenation operator '.' which
+improves readability and improves search-and-replace of adjacent elements.
+
+    $something = $blah . funky() . ".\".=" . $blab;
+
+is better than:
+
+    $something = $blah.funky().".\".=".$blab;
+
+
+======================================================================
+Quote Marks
+======================================================================
+
+Use the single quote marks ' to enclose simple strings whenever possible.
+Double quote marks " require extra parsing and thus slow things down, but
+are necessary if entities there must be swapped-out such as variables or
+control characters.
+
+    $var['singlequote'] = 'singlequote';
+    $var["doublequote-$i"] = "$vars and \n funny \t %s things need doublequotes";
+    $var['doublequote-' . $i] = $var . 'you can do this' . "\t %s" . $var2 .
+    'but it isn\'t any better';
+
+
+======================================================================
+Printing HTML
+======================================================================
+
+For large or complex blocks of HTML, using the following method is much faster
+and safer than echo because the php processor is bypassed and content goes
+directly to output:
+
+    <?php
+    if ($something) {
+        // Here comes multi-line HTML...
+        ?>
+        <div class="sc-tiny">
+            <a href="<?php echo $app->ohref('/contact.php') ?>">Contact us</a>
+        </div>
+        <?php
+    } else if ($somethingelse) {
+        ?><h1>Just a single-line of html</h1><?php
+    }
+    ?>
+
+
+======================================================================
+Comments
+======================================================================
+
+Function comments should follow the Javadoc standard, with detailed
+function comments and one-line pointers along the way:
+http://java.sun.com/products/jdk/javadoc/writingdoccomments/index.html
+
+Here is an example of the codebase method Email::validEmail():
+
+    <?php
+    /**
+     * Validates an email address based on the recommendations in RFC 3696.
+     * Is more loose than restrictive, to allow the many valid variants of
+     * email addresses while catching the most common mistakes. Checks an array too.
+     * http://www.faqs.org/rfcs/rfc822.html
+     * http://www.faqs.org/rfcs/rfc2822.html
+     * http://www.faqs.org/rfcs/rfc3696.html
+     * http://www.faqs.org/rfcs/rfc1035.html
+     *
+     * @access  public
+     * @param   mixed  $email  Address to check, string or array.
+     * @return  bool    Validity of address.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   30 Nov 2005 22:00:50
+     */
+    function validEmail($email)
+    {
+		$app =& App::getInstance();
+    
+        // If an array, check values recursively.
+        if (is_array($email)) {
+            foreach ($email as $e) {
+                if (!$this->validEmail($e)) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            // To be valid email address must match regex and fit within the length constraints.
+            if (preg_match($this->getParam('regex'), $email, $e_parts) && mb_strlen($e_parts[2]) < 64 && mb_strlen($e_parts[3]) < 255) {
+                return true;
+            } else {
+                $app->logMsg(sprintf('Invalid email: %s', $email), LOG_INFO, __FILE__, __LINE__);
+                return false;
+            }
+        }
+    }
+    ?>
+
+Single line comments should be of the double-slash variety, and be on the line above the
+code being commented upon:
+
+    <?php
+    // If the last word in $var is peanuts, set $need_peanuts to TRUE.
+    $need_peanuts = preg_match('/peanuts$/i', $var);
+    ?>
+
+
+======================================================================
+Including Code
+======================================================================
+
+If you are including a class, function library, or anything else which would
+cause a parse error if included twice, always use include_once. This will
+ensure that no matter how many factory methods we use or how much dynamic
+inclusion we do, the library will only be included once.
+
+If you are including a static filename, such as a conf file or a template
+that is _always_ used, use require.
+
+If you are dynamically including a filename, or want the code to only be
+used conditionally (an optional template), use include.
+
+
+======================================================================
+PHP Code Tags
+======================================================================
+
+Always use <?php ?> to delimit PHP code, not the <? ?> shorthand. Even
+use <?php echo $name; ?> instead of <?=$name?>.
+
+
+======================================================================
+php.ini Settings
+======================================================================
+
+All code should work with register_globals = Off. This means using
+$_REQUEST, $_GET, $_POST, $_COOKIE, $_SESSION, $_SERVER, and $_ENV
+to access all request, get, post, cookie, session, server, and 
+environment data, respectively.
+
+All code should work with error_reporting = E_ALL. Failure to do so would
+result in ugly output, error logs getting filled with lots of warning messages,
+or even downright broken scripts.
+
+All code should work regardless of the setting of magic_quotes_gpc.
+Form data should be passed through stripslashes() if necessary.
+
+No code should assume that '.' is in the include path. Always
+specify './' in front of a filename when you are including a file in
+the same directory. Or better yet, for less abiguity, use:
+include dirname(__FILE__) . '/include_me.inc.php';
+
+
+======================================================================
+XHTML 1.0 Compliance
+======================================================================
+
+All HTML should be valid XHTML 1.0 verified with the
+W3C Markup Validation Service: http://validator.w3.org/
+
+All tag names and parameters must be lower case including javascript
+event handlers:
+
+    <div class="mofogo">...</div>
+    <a href="http://www.example.com/" onmouseover="status=''" onmouseout="status=''">...</a>
+
+All tag parameters must be of a valid parameter="value" form (numeric
+values must also be surrounded by quotes).  For parameters that had no
+value in HTML, the parameter name is the value.  For example:
+
+    <input type="checkbox" checked="checked" />
+    <select name="example">
+        <option selected="selected" value="1">Example</option>
+    </select>
+    <td nowrap="nowrap">Example</td>
+
+All tags must be properly closed. Tags without a closing part must follow the
+XHTML convention and end with a space and a slash:
+
+    <br />
+    <hr />
+    <img src="example.gif" alt="Example" />
+    <input type="submit" value="Example" />
+
+All form definitions must be on their own line and either fully defined within
+a <td></td> pair or be outside table tags. Forms must also always have an action
+parameter:
+
+    <form method="post" action="http://example.com/example.cgi">
+    <table>
+        <tr><td>example</td></tr>
+    </table>
+    </from>
+
+    <table>
+        <tr><td>
+            <form action="javascript:void(0)" onsubmit="return false;">
+            </form>
+        </td></tr>
+    </table>
+
+All JavaScript tags must have a valid language and type parameters:
+
+    <script language="JavaScript" type="text/javascript">
+    <!--
+    ...
+    // -->
+    </script>
+
+Nothing may appear after </html>, therefore include any common footers after
+all other output.
+
+All images must have an alt parameter:
+
+    <img src="example.gif" alt="<?php echo _("Example"); ?>" />
+
+Input field of type "image" does not allow the border parameter, and may render
+with a border on some browsers.  Use the following instead:
+
+   <a href="" onclick="document.formname.submit(); return false;"><img src="example.gif" alt="<?php echo _("Example"); ?>" /></a>
+
+
+======================================================================
+Database Naming Conventions
+======================================================================
+
+All database tables need to make sure that their table and field names work in all
+databases. Many databases reserve words like 'uid', 'user', etc. for
+internal use, and forbid words that are SQL keywords (select, where,
+etc.). Adding '_tbl' to the end of the table name is a good idea for this reason
+and also improves searching for the table names in the code.
+
+A good way to do this for field names is to make the field name
+table_name_field_name.
+
+When building queries referencing two-or-more tables always preface columns
+with the table containing the column (admin_tbl.username, location_tbl.location_address).
+This method is much preferred over using aliases of table names (a.username, l.location_address).
+
+Table names should be singular (user_tbl instead of users_tbl).
Index: tags/2.2.3/docs/examples/_config.inc.php
===================================================================
--- tags/2.2.3/docs/examples/_config.inc.php	(revision 632)
+++ tags/2.2.3/docs/examples/_config.inc.php	(revision 632)
@@ -0,0 +1,177 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * _config.inc.php lives in the document root of the site/application. It is the beginning of everything.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.3
+ * @since   03 Dec 2005 19:09:32
+ */
+
+// The constant __FILE__ must be an absolute directory path, starting with / on unix or C: on windows.
+// To work around a PHP bug always include this config file with: require_once dirname(__FILE__) . '/_config.inc.php';
+if (!preg_match('!^(/|[A-Z]:)!', __FILE__)) {
+    trigger_error('_config.inc.php include must be specified with an absolute file path (eg: "require_once dirname(__FILE__) . \'/_config.inc.php\';"', E_USER_ERROR);
+}
+
+// First things first. Define the globally used directory paths.
+// The parent directory of all application DocRoots.
+define('COMMON_BASE', realpath(dirname(__FILE__) . '/../'));
+
+// The DocRoot for this application. SITE_BASE is different from $_SERVER['DOCUMENT_ROOT'] because the
+// latter does not change when using the apache Alias directive or URL Rewriting to define a site.
+define('SITE_BASE', dirname(__FILE__));
+
+// Set include path for all templates and libraries.
+ini_set('include_path', join(PATH_SEPARATOR, array(
+    COMMON_BASE,
+    SITE_BASE . '/_templates',
+    get_include_path(),
+)));
+
+// Include core libraries.
+require_once 'codebase/lib/Utilities.inc.php';
+require_once 'codebase/lib/App.inc.php';
+
+// Primary application class.
+$app =& App::getInstance('public');
+$app->setParam(array(
+    'site_name' => 'WWW Public Site',
+    'site_email' => 'hello@example.com',
+    'redirect_home_url' => '/',
+    'images_path' => '/i',
+
+    'date_format' => 'd M Y',
+    'sql_date_format' => '%e %b %Y',
+    'sql_time_format' => '%k:%i',
+    'character_set' => 'utf-8',
+
+    'enable_session' => defined('_NOSESSION') ? false : true, // Enable session unless _NOSESSION is defined.
+    'enable_db_session_handler' => false,
+    'session_use_cookies' => true,
+    'session_use_trans_sid' => false, // Disable this for high-security sites where session-ID theft is a risk.
+
+    // 'ssl_domain' => 'www.example.com',
+    // 'ssl_enabled' => ($_SERVER['SERVER_NAME'] == 'www.example.com'),
+
+    'enable_db' => true,
+    'db_always_debug' => false,
+    'db_debug' => true,
+    'db_die_on_failure' => true,
+    'db_create_tables' => true, // Disable after site launch.
+
+    'display_errors' => true,
+    'error_reporting' => E_ALL & ~E_DEPRECATED & ~E_STRICT,
+
+    'log_directory' => '/tmp',
+    'log_filename' => 'site_log',
+    'log_file_priority' => LOG_DEBUG,
+    'log_email_priority' => false,
+    'log_sms_priority' => false,
+    'log_screen_priority' => false,
+    // Email address to receive log event emails. Use multiple addresses by separating them with commas.
+    'log_to_email_address' => 'my-emailalert-address@example.com',
+    // SMS Email address to receive log event SMS messages. Use multiple addresses by separating them with commas.
+    'log_to_sms_address' => 'my-smsalert-address@example.com',
+));
+
+if ($app->cli) {
+    // DB credentials for command line scripts stored in a file with read rights
+    // given only to the user who will be executing the scripts: -rw-------
+    // This file includes $app-> method calls so this must be included after App::getInstance().
+    require_once 'global/db_auth.inc.php';
+}
+
+// Start application-based functionality: database, session, environment, ini setup, etc.
+// Most configuration parameters must be set before starting the App.
+$app->start();
+
+// Global DB object. Automatically pre-configured by $app->start().
+$db =& DB::getInstance();
+
+// Global Auth object.
+require_once 'codebase/lib/Auth_SQL.inc.php';
+$auth = new Auth_SQL('public');
+$auth->setParam(array(
+    'db_table'          => 'user_tbl',
+    'db_primary_key'    => 'user_id',
+    'login_url'         => '/login.php',
+    'login_timeout' => 260000, // 72 hours
+    'idle_timeout' => 86400, // 24 hours
+));
+
+// Load preferences for the user.
+require_once 'codebase/lib/Prefs.inc.php';
+$prefs = new Prefs('permanent', array(
+    'storagetype' => ($auth->isLoggedIn() ? 'database' : 'session'),
+    'user_id' => $auth->get('user_id'),
+));
+$prefs->setDefaults(array(
+    // ...
+));
+$prefs->load();
+// Load preferences for temporary usage.
+require_once 'codebase/lib/Prefs.inc.php';
+$c_prefs = new Prefs('config');
+$c_prefs->setParam(array(
+    'storagetype' => 'cookie',
+));
+// Temporary prefs.
+$c_prefs = new Prefs('settings');
+$c_prefs->setDefaults(array(
+));
+
+// Global record-locking object.
+require_once 'codebase/lib/Lock.inc.php';
+$lock =& Lock::getInstance($auth);
+$lock->setParam(array(
+    'timeout' => 0,
+    'auto_timeout' => 1800,
+    'error_url' => '/lock.php',
+));
+
+// Global cache object.
+require_once 'codebase/lib/Cache.inc.php';
+$cache = new Cache('global');
+$cache->setParam(array('enabled' => true)); // TODO: Enable caching after site launch.
+
+// Setup CSS files to include. These will always be available.
+require_once 'codebase/lib/CSS.inc.php';
+$css = new CSS();
+$css->setParam(array('cache_css' => false)); // TODO: Enable caching after site launch.
+$css->setFile('codebase/css/codebase.inc.css');
+$css->setFile('codebase/css/utilities.inc.css');
+// $css->setFile('html/css/screen.inc.css');
+
+// Nav class for titles, breadcrumbs, and page features.
+// Global navigation titles, breadcrumbs, and page features.
+require_once 'codebase/lib/Navigation.inc.php';
+$nav = new Navigation(array(
+    'path_delimiter' => ' / ',
+    'last_crumb_format' => '<b>%s</b>',
+));
+
+// Global site-specific configuration.
+// $cfg = array();
+// require_once 'global/config.inc.php';
+
Index: tags/2.2.3/docs/examples/contact_form/contact.eml
===================================================================
--- tags/2.2.3/docs/examples/contact_form/contact.eml	(revision 632)
+++ tags/2.2.3/docs/examples/contact_form/contact.eml	(revision 632)
@@ -0,0 +1,4 @@
+Message from {NAME} <{EMAIL}>:
+
+{MESSAGE}
+
Index: tags/2.2.3/docs/examples/contact_form/contact.ihtml
===================================================================
--- tags/2.2.3/docs/examples/contact_form/contact.ihtml	(revision 632)
+++ tags/2.2.3/docs/examples/contact_form/contact.ihtml	(revision 632)
@@ -0,0 +1,19 @@
+<form method="post" action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" class="sc-form">
+    <?php $app->printHiddenSession(); ?>
+
+    <div class="sc-form-row<?php $fv->err('name'); ?>">
+        <label for="name"><?php echo _("Name"); ?></label>
+        <input type="text" name="name" id="name" class="sc-medium" value="<?php echo oTxt($frm['name']); ?>" />
+    </div>
+    <div class="sc-form-row<?php $fv->err('email'); ?>">
+        <label for="email"><?php echo _("Email"); ?></label>
+        <input type="text" name="email" id="email" class="sc-medium" value="<?php echo oTxt($frm['email']); ?>" />
+    </div>
+    <div class="sc-form-row<?php $fv->err('message'); ?>">
+        <label for="message"><?php echo _("Message"); ?></label>
+        <textarea name="message" id="message" rows="8" cols="40" class="sc-short sc-medium"><?php echo oTxt($frm['message']); ?></textarea>
+    </div>
+    <div class="sc-form-row sc-submit-buttons">
+        <input type="submit" value="<?php echo _("Send"); ?>" />
+    </div>
+</form>
Index: tags/2.2.3/docs/examples/contact_form/contact.php
===================================================================
--- tags/2.2.3/docs/examples/contact_form/contact.php	(revision 632)
+++ tags/2.2.3/docs/examples/contact_form/contact.php	(revision 632)
@@ -0,0 +1,120 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+* contact.php
+*
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    01 Aug 2006 09:35:14
+*/
+
+require_once dirname(__FILE__) . '/_config.inc.php';
+
+
+/********************************************************************
+* CONFIG
+********************************************************************/
+
+require_once 'codebase/lib/FormValidator.inc.php';
+require_once 'codebase/lib/Email.inc.php';
+
+$fv = new FormValidator();
+
+// Set this to the person who should receive emails.
+$email_to = 'The Form Admin <me@example.com>';
+
+
+/********************************************************************
+* MAIN
+********************************************************************/
+
+if (!empty($_POST)) {
+    // Validate submitted data.
+    validateInput();
+    if ($fv->anyErrors()) {
+        // Re-populate form values with the user data so they can make corrections.
+        $frm = resetForm(getFormData());
+    } else {
+        // Setup email object.
+        $email = new Email(array(
+            'to' => $email_to,
+            'from' => sprintf('"%s" <%s>', addcslashes(getFormData('name'), '"'), getFormData('email')),
+            'subject' => 'Contact form email',
+        ));
+        $email->setTemplate('contact.eml');
+        $email->replace(array(
+            'name' => getFormData('name'),
+            'email' => getFormData('email'),
+            'message' => getFormData('message'),
+        ));
+        // Send email!
+        $email->send();
+
+        // Reset form values to display a blank form.
+        $frm = resetForm();
+        $app->raiseMsg(_("<strong>Thank You!</strong> Your message has been sent."), MSG_SUCCESS, __FILE__, __LINE__);
+    }
+} else {
+    // Reset form values to display a blank form.
+    $frm = resetForm();
+}
+
+
+/********************************************************************
+* OUTPUT
+********************************************************************/
+
+include 'header.ihtml';
+include 'contact.ihtml';
+include 'footer.ihtml';
+
+
+/********************************************************************
+* FUNCTIONS
+********************************************************************/
+
+function resetForm($new_values = array())
+{
+    $default = array(
+        'name' => '',
+        'email' => '',
+        'message' => '',
+    );
+    return array_merge($default, $new_values);
+}
+
+function validateInput()
+{
+    global $fv;
+
+    $fv->notEmpty('name', sprintf(_("%s cannot be blank."), _("Name")));
+    $fv->stringLength('name', 0, 255, sprintf(_("%s must be %d-to-%d characters in length."), _("Name"), 0, 255));
+
+    $fv->notEmpty('email', sprintf(_("%s cannot be blank."), _("Email")));
+    $fv->stringLength('email', 0, 255, sprintf(_("%s must be %d-to-%d characters in length."), _("Email"), 0, 255));
+    $fv->validateEmail('email');
+
+    $fv->notEmpty('message', sprintf(_("%s cannot be blank."), _("Message")));
+    $fv->stringLength('message', 0, 32000, sprintf(_("%s must be %d-to-%d characters in length."), _("Message"), 0, 32000));
+}
+
Index: tags/2.2.3/docs/examples/contact_form/footer.ihtml
===================================================================
--- tags/2.2.3/docs/examples/contact_form/footer.ihtml	(revision 632)
+++ tags/2.2.3/docs/examples/contact_form/footer.ihtml	(revision 632)
@@ -0,0 +1,16 @@
+<div id="footer">
+    <div class="nav">
+        <a href="/one" class="first">One</a> &middot;
+        <a href="/two">Two</a> &middot;
+        <a href="/three">Three</a>
+    </div>
+    <div id="addr">
+        123 Main Street, Chico, CA 95927 â <span class="sc-email">hello at example dot com</span>
+    </div>
+</div>
+</body>
+</html>
+<?php
+// The end is nigh!
+$app->stop();
+?>
Index: tags/2.2.3/docs/examples/contact_form/header.ihtml
===================================================================
--- tags/2.2.3/docs/examples/contact_form/header.ihtml	(revision 632)
+++ tags/2.2.3/docs/examples/contact_form/header.ihtml	(revision 632)
@@ -0,0 +1,21 @@
+<<?php echo '?xml version="1.0" encoding="' . $app->getParam('character_set') . '"?'; ?>>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+    <meta http-equiv="content-type" content="text/html; charset=<?php echo $app->getParam('character_set'); ?>" />
+    <title><?php printf('%s %s %s', $app->getParam('site_name'), $nav->getParam('path_delimiter'), strip_tags($nav->getPath('head_title'))) ?></title> 
+</head>
+<body<?php echo $nav->get('body_id') ? ' id="' . $nav->get('body_id') . '"' : ''; ?>>
+    <div id="header">
+        <ul class="nav">
+            <li><a href="/one" class="first">One</a></li>
+            <li><a href="/two">Two</a></li>
+            <li><a href="/three">Three</a></li>
+        </ul>
+    </div>
+    <?php if ($nav->getParam('body_title')) { ?>
+        <h1><?php echo $nav->get('body_title'); ?></h1>
+    <?php } ?>
+    <?php $app->printRaisedMessages(); ?>
+        
Index: tags/2.2.3/docs/examples/db_auth.inc.php
===================================================================
--- tags/2.2.3/docs/examples/db_auth.inc.php	(revision 632)
+++ tags/2.2.3/docs/examples/db_auth.inc.php	(revision 632)
@@ -0,0 +1,36 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* 
+* The credentials here are to be loaded only by command-line scripts executed 
+* on the command line or via cron. This script should be not be readable by apache. 
+* Ownership should be the web owner, with only user-readability, such as:
+* -rw-------@  1 bob  bob   416 Mar 10 16:59 global/db_auth.inc.php
+*/
+require_once 'codebase/lib/App.inc.php';
+$app =& App::getInstance();
+
+$app->setParam(array(
+    'db_name' => '',
+    'db_user' => '',
+    'db_pass' => '',
+));
Index: tags/2.2.3/docs/examples/example.cli.php
===================================================================
--- tags/2.2.3/docs/examples/example.cli.php	(revision 632)
+++ tags/2.2.3/docs/examples/example.cli.php	(revision 632)
@@ -0,0 +1,150 @@
+#!/usr/bin/env php
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+* example.cli.php - An example of a command-line interface script using the Strangecode Codebase.
+*
+*
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    28 Oct 2008 16:44:24
+*/
+
+
+/********************************************************************
+* STARTUP
+********************************************************************/
+
+$this_script = basename($_SERVER['argv'][0]);
+
+// First things first. Define the globally used directory paths.
+// The parent directory of all application DocRoots.
+define('COMMON_BASE', realpath(dirname(__FILE__) . '/../'));
+
+// Set include path for all templates and libraries.
+ini_set('include_path', get_include_path()
+    . PATH_SEPARATOR . COMMON_BASE
+);
+
+
+/********************************************************************
+* CONFIG
+********************************************************************/
+
+// This will disable sessions and things not needed by a cli script.
+define('_CLI', true);
+
+// Include core libraries.
+require_once 'codebase/lib/App.inc.php';
+require_once 'codebase/lib/Utilities.inc.php';
+
+// Primary application class.
+$app =& App::getInstance('cli');
+$app->setParam(array(
+    'site_name' => 'CLI Script',
+    'site_email' => 'codebase@strangecode.com',
+    'enable_session' => false,
+    'enable_db' => false,
+    'db_always_debug' => false,
+    'db_debug' => true,
+    'db_die_on_failure' => true,
+    'display_errors' => true,
+    'error_reporting' => E_ALL,
+    'log_file_priority' => LOG_INFO,
+    'log_screen_priority' => LOG_DEBUG,
+    'log_directory' => COMMON_BASE . '/log',
+    'log_filename' => 'cli_log',
+));
+
+// DB credentials for command line scripts stored in a file with read rights
+// given only to the user who will be executing the scripts: -rw-------
+// This file includes $app-> method calls so this must be included after App::getInstance().
+// require_once 'global/db_auth.inc.php';
+
+// Start application-based functionality: database, session, environment, ini setup, etc.
+// Most configuration parameters must be set before starting the App.
+$app->start();
+
+// Global DB object. Automatically pre-configured by $app->start().
+// $db =& DB::getInstance();
+
+
+/********************************************************************
+* MAIN
+********************************************************************/
+
+$options = getopt("habc");
+
+// Give them a fighting chance. Show the help message. ;P
+if (!$options) {
+    printf("%s: Syntax or usage error. Try '-h' if you are lost.\n", $this_script);
+    die(1);
+}
+
+if (isset($options['h'])) {
+    help();
+    die(1);
+}
+
+if (isset($options['a'])) {
+    // Run code associated with option -a
+    echo "Option a\n";
+}
+
+if (isset($options['b'])) {
+    // Run code associated with option -b
+    echo "Option b\n";
+}
+
+if (isset($options['c'])) {
+    // Run code associated with option -c
+    echo "Option c\n";
+}
+
+
+/********************************************************************
+* FUNCTIONS
+********************************************************************/
+
+function help()
+{
+    global $this_script;
+
+    ?>
+Description of this script goes here...
+
+Usage: <?php echo $this_script; ?> [OPTIONS]
+
+OPTIONS:
+
+    -a  Blah blah blah
+    -b  Blah blah blah
+    -c  Blah blah blah
+
+Strangecode :: www.strangecode.com
+<?php
+}
+
+
+
+
Index: tags/2.2.3/docs/examples/js.php
===================================================================
--- tags/2.2.3/docs/examples/js.php	(revision 632)
+++ tags/2.2.3/docs/examples/js.php	(revision 632)
@@ -0,0 +1,7 @@
+<?php
+/*
+* This file sources the js.php codebase service, which outputs javascript content managed by an JS.inc.php object.
+*/
+define('_CLI', true); // Define _CLI to disable sessions.
+require_once dirname(__FILE__) . '/_config.inc.php';
+include 'codebase/services/js.php';
Index: tags/2.2.3/docs/examples/script_template.php
===================================================================
--- tags/2.2.3/docs/examples/script_template.php	(revision 632)
+++ tags/2.2.3/docs/examples/script_template.php	(revision 632)
@@ -0,0 +1,61 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * __///__
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.0
+ * @since   __///__
+ */
+
+require_once dirname(__FILE__) . '/_config.inc.php';
+
+$auth->requireLogin();
+$app->sslOn();
+
+require_once 'codebase/lib/FormValidator.inc.php';
+
+
+/********************************************************************
+* CONFIG
+********************************************************************/
+
+
+
+/********************************************************************
+* MAIN
+********************************************************************/
+
+
+
+/********************************************************************
+* OUTPUT
+********************************************************************/
+
+
+
+/********************************************************************
+* FUNCTIONS
+********************************************************************/
+
+
Index: tags/2.2.3/docs/examples/virtualhost_directive.conf
===================================================================
--- tags/2.2.3/docs/examples/virtualhost_directive.conf	(revision 632)
+++ tags/2.2.3/docs/examples/virtualhost_directive.conf	(revision 632)
@@ -0,0 +1,14 @@
+<VirtualHost 123.123.123.123:80>
+    ServerName example.com
+    ServerAlias *.example.com
+    DocumentRoot /home/user/www.example.com/html
+    CustomLog /home/user/www.example.com/log/access_log combined
+    ErrorLog /home/user/www.example.com/log/error_log
+    SetEnv DB_NAME carl
+    SetEnv DB_USER user_asdf
+    SetEnv DB_PASS pass_asdf
+    SetEnv SIGNING_KEY kjhu3hf4gpoh2p9fgh8223t8h120
+    <Directory "/home/user/www.example.com">
+        php_admin_value open_basedir "/home/user/www.example.com:/usr/local/lib/php:/tmp"
+    </Directory>
+</VirtualHost>
Index: tags/2.2.3/docs/file_layout.txt
===================================================================
--- tags/2.2.3/docs/file_layout.txt	(revision 632)
+++ tags/2.2.3/docs/file_layout.txt	(revision 632)
@@ -0,0 +1,115 @@
+Updated: 11 Dec 2006 16:25:10
+(This document is probably out-of-date.)
+=====================================================================
+
+codebase/
+    bin/ (generic command-line scripts and applications)
+        acl.cli.php (Tool for managing the permissions of an ACL.inc.php based system)
+        extract_gettext.pl (tool for exporting all the strings called by the _("Gettext function") )
+        file_importer.php
+        install.sh
+        module_maker/ (scripts for generating modules)
+            _config.inc.php
+            form_template.cli.php
+            list_template.cli.php
+            module.cli.php
+            skel/ (templates for generating modules)
+                adm_form.ihtml
+                adm_list.ihtml
+                admin.php
+                public_list.ihtml
+                public.ihtml
+                public.php
+            sql.cli.php
+            validation.cli.php
+        pedit_permissions.sh
+    css/ (generic CSS files to be included using lib/CSS.inc.php)
+        admin.inc.css (CSS for the generic Strangecode administration system)
+        codebase.inc.css (CSS for html output by codebase libraries)
+        utilities.inc.css (generic CSS tools)
+    docs/
+        coding_standards.txt
+        example_config.inc.php (an example of the _config.inc.php site-boot document)
+        example_script_template.php
+        example_virtualhost_directive.txt
+        file_layout.txt
+        gpl.txt
+        revision_history.txt
+        version.txt (the version number of this codebase)
+    lib/
+        ACL.inc.php (provides Access Control List functionality)
+        App.inc.php (provides global application functionality: sessions, href/url/IP manipulation, directories)
+        Auth_File.inc.php (file-based authentication system)
+        Auth_SQL.inc.php (sql-based authentication system)
+        AuthorizeNet.inc.php (routines for connection to authorize.net's advanced processing interface)
+        Cache.inc.php (provides session-based caching functionality for quick retrieval of all PHP data types)
+        CSS.inc.php (manage and deliver CSS data)
+        DB.inc.php (minimal database abstraction layer)
+        DBSessionHandler.inc.php (database session handler)
+        Email.inc.php (provides email validation and sending functionality)
+        FormValidator.inc.php (validation routines used to test incoming user data)
+        Google_API.inc.php (class for connecting to, querying, and dealing with data of the Google API)
+        Heirarchy.php (class for manipulation of hierarchies)
+        Image.inc.php (manage printing of images related to db records)
+        ImageThumb.inc.php (automated image thumbnailing routines)
+        Lock.inc.php (db record locking system)
+        MCVE.inc.php
+        Navigation.inc.php (navigation element management class))
+        PageNumbers.inc.php (class for managing and printing various elements of page numbers and pagination)
+        PageSequence.inc.php (manage a sequence of steps and data from step form elements, like order checkout or signup)
+        PayPal.inc.php
+        PEdit.inc.php (page-by-page file-based content management system)
+        Prefs.inc.php (class for semi-permanent storage of values)
+        ScriptTimer.inc.php (timer for scripts)
+        SortOrder.inc.php (class dealing with sorting of columns in database generated lists)
+        SpellCheck.inc.php
+        TemplateGlue.inc.php (functions that generating html)
+        Upload.inc.php (class that manages uploading of files)
+        Utilities.inc.php (functions for general utility: setting defaults, num/string functions, etc)
+        Version.inc.php (db record versioning system)
+    services/ (codebase scripts that are only useful when web accessible, i.e. to be used in a doc root)
+        admins.php
+        css.php
+        lock.php
+        login.php
+        logout.php
+        logs.php
+        password.php
+        phpinfo.php
+        provides
+        templates/
+        versions.php
+    tests/ (unit-based tests for all codebase libraries)
+
+===================================================================================
+Group of applications and related files, usually installed at /home/user/www.domain.com/
+
+
+global/ (global level configuration and libraries for a group of sites/apps)
+    config.inc.php (system-wide configuration)
+    db_auth.inc.php (db-authentication for CLI scripts. chown'ed by user executing cron, NOT apache).
+
+docs/ (documents specific to a group of sites/apps)
+    schema.mysql
+
+bin/ (global level command-line scripts)
+
+admin/ (DocumentRoot for the admin application)
+    _config.inc.php
+    _templates/ (templates used for the admin docroot)
+    _widgets/ (graphic elements used by the admin docroot)
+    admins.php (list, view, edit, delete administrators)
+    css.php (style sheets loader)
+    index.php
+    lock.php (manage locked db records)
+    login.php (login to the admin system)
+    logout.php (accessing logs-out an admin)
+    logs.php (view logs)
+    password.php (change password of currently logged-in admin)
+    phpinfo.php
+    versions.php (manage db record versions)
+
+html/ (DocumentRoot of main site application)
+    _config.inc.php (configuration options and defaults specific to this site. included first in each script)
+    _templates/ (site specific templates. templates here override global codebase templates with same name)
+    css.php (style sheets loader)
Index: tags/2.2.3/docs/gpl.txt
===================================================================
--- tags/2.2.3/docs/gpl.txt	(revision 632)
+++ tags/2.2.3/docs/gpl.txt	(revision 632)
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
Index: tags/2.2.3/docs/readme.txt
===================================================================
--- tags/2.2.3/docs/readme.txt	(revision 632)
+++ tags/2.2.3/docs/readme.txt	(revision 632)
@@ -0,0 +1,31 @@
+The Strangecode Codebase - a general application development framework for PHP
+For details visit the project site: <http://trac.strangecode.com/codebase/>
+Copyright 2001-2012 Strangecode, LLC
+
+
+This program is free software: you can redistribute it and/or modify it under the
+terms of the GNU General Public License as published by the Free Software
+Foundation, either version 3 of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along with this
+program (docs/gpl.txt). If not, see <http://www.gnu.org/licenses/>.
+
+
+Strangecode offers website hosting, development, and consulting services for 
+businesses across the globe. For project inquiries please find us at:
+
+http://www.strangecode.com/
+http://twitter.com/strangecode
+
+hello@strangecode.com
++1 530 636 2633
+
+Strangecode, LLC
+340 S Lemon Ave #2633
+Walnut CA 91789
+United States
+
Index: tags/2.2.3/docs/revision_history.txt
===================================================================
--- tags/2.2.3/docs/revision_history.txt	(revision 632)
+++ tags/2.2.3/docs/revision_history.txt	(revision 632)
@@ -0,0 +1,86 @@
+﻿---------------------------------------------------------------------
+Strangecode codebase 2.1
+---------------------------------------------------------------------
+
+css/utilities.inc.php and css/codebase.inc.php now have sc- prepended to all selectors. This regex will convert an existing site to the new sc- format:
+s/class="([\w-]+ )*(tiny|small|medium|large|full|twolines|short|tall|fullscreen|nowrap|clearboth|center|right|padright|padleft|padleft|help|monospaced|pkg)( [\w-]+)*"/class="$1sc-$2$3"/gi
+
+
+
+---------------------------------------------------------------------
+Strangecode codebase 2.0
+---------------------------------------------------------------------
+
+Codebase self contained. runs without reference to external files or info.
+    - can be located in /usr/local/php/lib/php for server-wide access
+    - does not include any files outside codebase
+    - does not write to files outside codebase
+    - does not try to determine it's location or site parameters. this will minimize it's failure to glean this info.
+    - will still run if info missing: db creds, log dir, session info.
+    - can be located in /usr/local/lib/php if necessary, so accessible by all sites.
+
+remove interdependencies:
+    - App() is the primary dependency for other classes. It includes Utilities.inc.php silently.
+    - Updated classes requiring external files, such as html templates and email templates, classes now self-contained.
+
+Things with modified interfaces:
+    - App
+    - Auth_SQL
+    - DB
+    - DBSessionHandler
+    - Lock
+    - Version
+    - Cache
+    - Upload
+
+One application will always have distinct:
+    - Configurations:
+        - log dir
+        - log config
+        - debugging
+        - charset
+        - session data
+    - Objects:
+        - auth
+        - db
+    - Environment:
+        - seperate codebase installs if necessary for each app
+        - unique include path
+        - site/doc/web root
+        - lang
+    - $dbh database handler
+
+App object:
+    - object belongs to a realm, keeping messages and session data within specific site/app.
+    - uses the singleton pattern so only one instance of App exists for each app.
+    - app object can be accessed globally without reference to object name by calling methods statically, e.g. App:logMsg()
+    - seperate application and codebase parameters from site configuration variables.
+      Use $app->getParam('var') or $app->getParam('var') to get app params.
+      F or site configurations do whatever you want (I'm using $cfg['node']['features'] format for configurations)
+
+App configuration parameters
+    - defaults part of the app class
+    - do not need to be set in any specific order
+    - missing vars are dynamically accounted for.
+
+Auth_SQL object:
+    - Modified to set the auth-type within the object. We needed to use one object name ($auth) for all instances so we can use template that use this variable name commonly. login.php, lock.php, versions.php, password.php -- all in the codebase services will be used globally on multiple sites and need this variable name set.
+
+DB object:
+    - Self contained class for DB functions. $db->query is the only necessary function. Maintains connections and state independent of App.
+
+Full test suite for all codebase libraries. Run from the command line: "codebase/tests/run_tests.sh;"
+
+Classes that access database tables have a function initDB() that will setup the basic schema necessary to run. This includes Auth_SQL, Version, Lock, and DBSessionHandler.
+
+And much much more still to document...
+
+
+
+---------------------------------------------------------------------
+Strangecode codebase 1.0.0 release
+---------------------------------------------------------------------
+
+This is the primary stable release used before we started using Subversion. It was maintained erratically by one erratic person. No revision info documented, sorry.
+
+
Index: tags/2.2.3/docs/upgrade_v1-to-v2.mysql
===================================================================
--- tags/2.2.3/docs/upgrade_v1-to-v2.mysql	(revision 632)
+++ tags/2.2.3/docs/upgrade_v1-to-v2.mysql	(revision 632)
@@ -0,0 +1,28 @@
+## Update user_tbl.
+## (If it exists!)
+ALTER TABLE `user_tbl` ADD modified_by_user_id smallint(11) default NULL AFTER added_by_user_id;
+ALTER TABLE `user_tbl` MODIFY added_datetime datetime NOT NULL default '0000-00-00 00:00:00' AFTER modified_by_user_id;
+ALTER TABLE `user_tbl` ADD modified_datetime datetime NOT NULL default '0000-00-00 00:00:00' AFTER added_datetime;
+
+## Update admin_tbl.
+## NOTE: This first one might need customizing.
+ALTER TABLE `admin_tbl` CHANGE priv user_type enum('root','admin','editor') default NULL;
+
+ALTER TABLE `admin_tbl` ADD login_abuse_exempt enum('true') default NULL AFTER user_type;
+ALTER TABLE `admin_tbl` ADD blocked enum('true') default NULL AFTER login_abuse_exempt;
+ALTER TABLE `admin_tbl` ADD blocked_reason varchar(255) NOT NULL default '' AFTER blocked;
+ALTER TABLE `admin_tbl` ADD abuse_warning_level tinyint(4) NOT NULL default '0' AFTER blocked_reason;
+ALTER TABLE `admin_tbl` ADD added_by_user_id smallint(11) default NULL AFTER last_login_ip;
+ALTER TABLE `admin_tbl` ADD modified_by_user_id smallint(11) default NULL AFTER added_by_user_id;
+ALTER TABLE `admin_tbl` MODIFY added_datetime datetime NOT NULL default '0000-00-00 00:00:00' AFTER modified_by_user_id;
+ALTER TABLE `admin_tbl` ADD modified_datetime datetime NOT NULL default '0000-00-00 00:00:00' AFTER added_datetime;
+
+## Upgrade version_tbl.
+ALTER TABLE `version_tbl` ADD version_number SMALLINT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER version_title;
+ALTER TABLE `version_tbl` ADD version_notes VARCHAR(255) NOT NULL DEFAULT '' AFTER version_number;
+ALTER TABLE `version_tbl` CHANGE saved_by_admin_id saved_by_user_id smallint(11) default NULL;
+
+## Rename added_by_admin_id -> added_by_user_id in all tables.
+## Changing these will require source code modifications.
+## This is just an EXAMPLE!
+# ALTER TABLE `client_tbl` CHANGE added_by_admin_id added_by_user_id smallint(11) default NULL;
Index: tags/2.2.3/docs/version.txt
===================================================================
--- tags/2.2.3/docs/version.txt	(revision 632)
+++ tags/2.2.3/docs/version.txt	(revision 632)
@@ -0,0 +1,1 @@
+2.2.3
Index: tags/2.2.3/js/Msg.js
===================================================================
--- tags/2.2.3/js/Msg.js	(revision 632)
+++ tags/2.2.3/js/Msg.js	(revision 632)
@@ -0,0 +1,111 @@
+/*
+* The Strangecode Codebase - a general application development framework for PHP
+* For details visit the project site: <http://trac.strangecode.com/codebase/>
+* Copyright Â© 2014 Strangecode, LLC
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// Codebase functions will be under the Strangecode namespace, unless they are added to the jQuery object for chaining.
+var Strangecode = Strangecode || {};
+
+/*
+* Class for managing user messages. This is the constructor function.
+*
+* @access   public
+* @param    object containing object options:
+{
+    container: '#sc-msg-formvalidator',
+    above_msg: 'Oops!'
+}
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    18 Mar 2014 23:24:15
+*
+* Example of use:
+---------------------------------------------------------------------
+var sc_msg = new Strangecode.Msg({
+    container: '#sc-msg-formvalidator',
+    above_msg: 'The following errors occurred:'
+});
+sc_msg.raise('Oops, you fuxt up!', 'sc-msg-error');
+sc_msg.clear();
+sc_msg.raise('Ok, now youâre cool.', 'sc-msg-success');
+---------------------------------------------------------------------
+*/
+Strangecode.Msg = function (options) {
+    // Merge options with defaults
+    var options = this.o = $.extend({
+        container: '.sc-msg:last',
+        above_msg: '',
+        gotohash: true
+    }, options || {});
+
+    document.addEventListener('DOMContentLoaded', function (e) {
+        // Warn if the target doesn't exist.
+        if (!$(options.container).length) {
+            console.warn('Strangecode.Msg container not found: ' + options.container);
+        }
+    });
+};
+
+/*
+* Raises a message to the user.
+*
+* @access   public
+* @param    string message  The message to display.
+* @param    string class    The class to apply to the message div (when using codebase CSS, it is useful to use one of: sc-msg-success, sc-msg-notice, sc-msg-warning, sc-msg-error)
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    18 Mar 2014 23:24:15
+*/
+Strangecode.Msg.prototype.raise = function (message, msg_class) {
+    // If an "above msg" is provided, and one doesn't already exist, add it to the message box.
+    if (this.o.above_msg.length && !$(this.o.container).find('.sc-above').length) {
+        $(this.o.container).append($('<div class="sc-js-msg sc-above"></div>').text(this.o.above_msg));
+    }
+    // Use 'sc-msg-error' as default class string.
+    var msg_class = (typeof msg_class === 'undefined') ? 'sc-msg-error' : msg_class;
+    // Append this raised message to the sc-msg stack, if it doesn't exist already.
+    if (!$(this.o.container).find(':contains("' + message + '")').length) {
+        $(this.o.container).append($('<div data-alert class="alert-box sc-js-msg ' + msg_class + '"></div>').hide().html(message).append('<a href="#" class="close">Ã</a>'));
+        $('.sc-js-msg').slideDown('fast');
+        if ($.fn.foundation) {
+            $(document).foundation('reflow');
+        }
+    }
+
+    if (this.o.gotohash) {
+        $(document.body).animate({
+            'scrollTop': $(this.o.container).offset().top
+        }, 'fast');
+    }
+};
+
+/*
+* Removes all previously raised messages. By default, only messages raised by this JS class
+* are cleared (matching class="sc-js-msg"), but you can clear *all* messages by assigning a more general
+* class (e.g., "sc-msg-error").
+*
+* @access   public
+* @param    string message  The message to display.
+* @param    string class    Remove message divs matching this class (default: sc-js-msg; or use one of: sc-msg-success, sc-msg-notice, sc-msg-warning, sc-msg-error)
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.1
+* @since    18 Mar 2014 23:24:15
+*/
+Strangecode.Msg.prototype.clear = function (msg_class) {
+    var msg_class = (typeof msg_class === 'undefined') ? 'sc-js-msg' : msg_class;
+    $(this.o.container).find('.' + msg_class).remove();
+};
Index: tags/2.2.3/js/Utilities.js
===================================================================
--- tags/2.2.3/js/Utilities.js	(revision 632)
+++ tags/2.2.3/js/Utilities.js	(revision 632)
@@ -0,0 +1,158 @@
+/*
+* The Strangecode Codebase - a general application development framework for PHP
+* For details visit the project site: <http://trac.strangecode.com/codebase/>
+* Copyright Â© 2014 Strangecode, LLC
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+// Codebase functions will be under the Strangecode namespace, unless they are added to the jQuery object for chaining.
+var Strangecode = Strangecode || {};
+
+/*
+Emulates a sprintf function. Placeholders are {1}âŠ{N}.
+Some versions of this function were zero-indexed; this one is not.
+---------------------------------------------------------------------
+"{1} is dead, but {2} is alive! {1} {3}".format("ASP", "ASP.NET")
+
+outputs
+
+ASP is dead, but ASP.NET is alive! ASP {3}
+---------------------------------------------------------------------
+*
+* @access   public
+* @param    string multiple Strings to pass to the formatted string.
+* @author   http://stackoverflow.com/a/4673436/277303
+* @version  1.0
+* @since    30 May 2014 18:02:39
+*/
+if (!String.prototype.format) {
+    String.prototype.format = function() {
+        var args = arguments;
+        return this.replace(/{(\d+)}/g, function(match, number) {
+            return typeof args[number-1] != 'undefined' ? args[number-1] : match;
+        });
+    };
+}
+
+/*
+* Displays 'user at domain dot com' as 'user@domain.com'.
+---------------------------------------------------------------------
+<span class="sc-email">user at domain dot com</span>
+<a href="mailto:user at domain dot com" class="sc-email">Email me</a>
+<script>
+$('.sc-email').nospam();
+</script>
+---------------------------------------------------------------------
+*
+* @access   public
+* @version  2.0
+* @since    30 Jun 2008 12:32:19
+*/
+jQuery.fn.nospam = function() {
+	return this.each(function(){
+		$(this).text($(this).text().replace(' at ', '@').replace(' dot ', '.'));
+        if (this.href) {
+            this.href = this.href.replace(' at ', '@').replace(' dot ', '.');
+        }
+	});
+};
+
+/*
+* Encode html entities by specific mapping table.
+* Decode HTML by proxying content via an in-memory div, setting its inner text which jQuery automatically encodes.
+Then we pull the encoded contents back out. The div never exists on the page.
+---------------------------------------------------------------------
+$('input').val(Strangecode.htmlEncode(string));
+---------------------------------------------------------------------
+
+@access   public
+@version  2.0
+@since    30 Jun 2013
+*/
+Strangecode.htmlEncode = function (str) {
+    var entityMap = {
+        '&': '&amp;',
+        '<': '&lt;',
+        '>': '&gt;',
+        '"': '&quot;',
+        "'": '&#39;',
+        '/': '&#x2F;',
+        '`': '&DiacriticalGrave;'
+    };
+	return String(str).replace(/[&<>"'\/`]/g, function (s) {
+		return entityMap[s];
+	});
+};
+Strangecode.htmlDecode = function (str) {
+    return $('<div>').html(str).text();
+};
+
+
+/*
+Returns a string with URL-unsafe characters removed.
+---------------------------------------------------------------------
+var urlslug = $('.url').val().slug();
+---------------------------------------------------------------------
+* @access   public
+* @version  1.0
+* @since    30 Jun 2013
+*/
+$.fn.slug = function() {
+    str = this.text().trim().toLowerCase();
+    var from = 'Ã¡Ã©Ã­Ã³ÃºÃ ÃšÃ¬Ã²Ã¹Ã€Ã«Ã¯Ã¶ÃŒÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃ¢ÃªÃ®ÃŽÃ»ÃÃÃÃÃÃ±Ã§Ã@Â·/_,:;';
+    var to   = 'aeiouaeiouaeiouAEIOUAEIOUAEIOUaeiouAEIOUncCa------';
+    for (var i=0, l=from.length; i<l; i++) {
+        str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
+    }
+    return str.replace(/[^a-z0-9 -]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-');
+};
+
+
+
+/*
+Remove rounding errors caused by representation of finite binary floating point numbers.
+---------------------------------------------------------------------
+> (.1*.2)
+0.020000000000000004
+> (.1*.2).trim()
+0.02
+---------------------------------------------------------------------
+* @access   public
+* @version  1.0
+* @since    24 Nov 2015
+*/
+if (!Number.prototype.trim) {
+    Number.prototype.trim = function (precision) {
+        var precision = precision || 11;
+        return Math.round(this * Math.pow(10, precision)) / Math.pow(10, precision);
+    };
+}
+
+/*
+Uppercase the first letter of string.
+---------------------------------------------------------------------
+> 'hello world'.trim()
+Hello world
+---------------------------------------------------------------------
+* @access   public
+* @version  1.0
+* @since    24 Nov 2015
+*/
+if (!String.prototype.ucfirst) {
+    String.prototype.ucfirst = function() {
+        return this.charAt(0).toUpperCase() + this.slice(1);
+    };
+}
+
Index: tags/2.2.3/lib/ACL.inc.php
===================================================================
--- tags/2.2.3/lib/ACL.inc.php	(revision 632)
+++ tags/2.2.3/lib/ACL.inc.php	(revision 632)
@@ -0,0 +1,810 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+* ACL.inc.php
+*
+* Uses the ARO/ACO/AXO model of Access Control Lists.
+* Uses Modified Preorder Tree Traversal to maintain a tree-structure.
+* See: http://www.sitepoint.com/print/hierarchical-data-database
+* Includes a command-line tool for managing rights (codebase/bin/acl.cli.php).
+*
+*
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    14 Jun 2006 22:35:11
+*/
+
+require_once dirname(__FILE__) . '/Cache.inc.php';
+
+class ACL
+{
+
+    // A place to keep an object instance for the singleton pattern.
+    protected static $instance = null;
+
+    // Configuration parameters for this object.
+    protected $_params = array(
+        // If false nothing will be cached or retrieved. Useful for testing realtime data requests.
+        'enable_cache' => true,
+
+        // Automatically create table and verify columns. Better set to false after site launch.
+        'create_table' => false,
+
+        // Maximum allowed length of names.
+        // This value can be increased only if {aro,aco,axo}_tbl.name VARCHAR length is increased.
+        'name_max_length' => 32.
+    );
+
+    /**
+     * Constructor.
+     */
+    public function __construct()
+    {
+        $app =& App::getInstance();
+
+        // Configure the cache object.
+        $this->cache = new Cache('acl');
+
+        // Get create tables config from global context.
+        if (!is_null($app->getParam('db_create_tables'))) {
+            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
+        }
+    }
+
+    /**
+     * This method enforces the singleton pattern for this class.
+     *
+     * @return  object  Reference to the global ACL object.
+     * @access  public
+     * @static
+     */
+    public static function &getInstance()
+    {
+        if (self::$instance === null) {
+            self::$instance = new self();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     *
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Some params require special processing. Catch those in a loop and process individually.
+            foreach ($params as $key => $val) {
+                switch ($key) {
+                case 'enable_cache':
+                    $this->cache->setParam(array('enabled' => $val));
+                    break;
+                }
+
+            }
+            unset($key, $value);
+
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_NOTICE, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Setup the database tables for this class.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   04 Jun 2006 16:41:42
+     */
+    public function initDB($recreate_db=false)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        static $_db_tested = false;
+
+        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
+
+            if ($recreate_db) {
+                $db->query("DROP TABLE IF EXISTS acl_tbl");
+                $db->query("DROP TABLE IF EXISTS aro_tbl");
+                $db->query("DROP TABLE IF EXISTS aco_tbl");
+                $db->query("DROP TABLE IF EXISTS axo_tbl");
+                $app->logMsg(sprintf('Dropping and recreating tables acl_tbl, aro_tbl, aco_tbl, axo_tbl.', null), LOG_INFO, __FILE__, __LINE__);
+            }
+
+            // acl_tbl
+            $db->query(sprintf("
+                CREATE TABLE IF NOT EXISTS acl_tbl (
+                    aro_id SMALLINT UNSIGNED NOT NULL DEFAULT '0',
+                    aco_id SMALLINT UNSIGNED NOT NULL DEFAULT '0',
+                    axo_id SMALLINT UNSIGNED NOT NULL DEFAULT '0',
+                    access ENUM('allow', 'deny') DEFAULT NULL,
+                    added_datetime DATETIME NOT NULL DEFAULT '%s 00:00:00',
+                    UNIQUE KEY (aro_id, aco_id, axo_id),
+                    KEY (access)
+                ) ENGINE=MyISAM
+            ", $db->getParam('zero_date')));
+            if (!$db->columnExists('acl_tbl', array(
+                'aro_id',
+                'aco_id',
+                'axo_id',
+                'access',
+                'added_datetime',
+            ), false, false)) {
+                $app->logMsg(sprintf('Database table acl_tbl has invalid columns. Please update this table manually.', null), LOG_ALERT, __FILE__, __LINE__);
+                trigger_error(sprintf('Database table acl_tbl has invalid columns. Please update this table manually.', null), E_USER_ERROR);
+            } else {
+                // Insert root node data if nonexistant, dely all by default.
+                $qid = $db->query("SELECT 1 FROM acl_tbl");
+                if (mysql_num_rows($qid) == 0) {
+                    $qid = $db->query("REPLACE INTO acl_tbl VALUES ('1', '1', '1', 'deny', NOW())");
+                }
+            }
+
+            // aro_tbl, aco_tbl, axo_tbl
+            foreach (array('aro', 'aco', 'axo') as $a_o) {
+                $db->query(sprintf("
+                    CREATE TABLE IF NOT EXISTS %1\$s_tbl (
+                        %1\$s_id SMALLINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+                        name VARCHAR(%2\$s) NOT NULL DEFAULT '',
+                        lft MEDIUMINT UNSIGNED NOT NULL DEFAULT '0',
+                        rgt MEDIUMINT UNSIGNED NOT NULL DEFAULT '0',
+                        added_datetime DATETIME NOT NULL DEFAULT '%3\$s 00:00:00',
+                        UNIQUE KEY name (name(15)),
+                        KEY transversal (lft, rgt)
+                    ) ENGINE=MyISAM;
+                ", $a_o, $this->getParam('name_max_length'), $db->getParam('zero_date')));
+
+                if (!$db->columnExists("{$a_o}_tbl", array(
+                    "{$a_o}_id",
+                    'name',
+                    'lft',
+                    'rgt',
+                    'added_datetime',
+                ), false, false)) {
+                    $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', "{$a_o}_tbl"), LOG_ALERT, __FILE__, __LINE__);
+                    trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', "{$a_o}_tbl"), E_USER_ERROR);
+                } else {
+                    // Insert root node data if nonexistent.
+                    $qid = $db->query("SELECT 1 FROM {$a_o}_tbl WHERE name = 'root'");
+                    if (mysql_num_rows($qid) == 0) {
+                        $qid = $db->query("REPLACE INTO {$a_o}_tbl (name, lft, rgt, added_datetime) VALUES ('root', 1, 2, NOW())");
+                    }
+                }
+
+            }
+        }
+        $_db_tested = true;
+        return true;
+    }
+
+    /*
+    * Add a node to one of the aro/aco/axo tables.
+    *
+    * @access   public
+    * @param    string $name A unique identifier for the new node.
+    * @param    string $parent The name of the parent under-which to attach the new node.
+    * @param    string $type The tree to add to, one of: aro, aco, axo.
+    * @return   bool | int False on error, or the last_insert_id primary key of the new node.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    14 Jun 2006 22:39:29
+    */
+    public function add($name, $parent=null, $type)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        switch ($type) {
+        case 'aro' :
+            $tbl = 'aro_tbl';
+            break;
+        case 'aco' :
+            $tbl = 'aco_tbl';
+            break;
+        case 'axo' :
+            $tbl = 'axo_tbl';
+            break;
+        default :
+            $app->logMsg(sprintf('Invalid access object type: %s', $type), LOG_ERR, __FILE__, __LINE__);
+            return false;
+            break;
+        }
+
+        // If $parent is null, use root object.
+        if (is_null($parent)) {
+            $parent = 'root';
+        }
+
+        // Ensure node and parent name aren't empty.
+        if ('' == trim($name) || '' == trim($parent)) {
+            $app->logMsg(sprintf('Cannot add node, parent (%s) or name (%s) missing.', $name, $parent), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Ensure node node name fits in the column size.
+        // This value can be increased if {aro,aco.axo}_tbl.name VARCHAR length is increased.
+        if (strlen(trim($name)) > $this->getParam('name_max_length')) {
+            $app->logMsg(sprintf('Cannot add node, %s character limit exceeded for name "%s"', $this->getParam('name_max_length'), $name, $parent), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Ensure node is unique.
+        $qid = $db->query("SELECT 1 FROM $tbl WHERE name = '" . $db->escapeString($name) . "'");
+        if (mysql_num_rows($qid) > 0) {
+            $app->logMsg(sprintf('Cannot add %s node, already exists: %s', $type, $name), LOG_INFO, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Select the rgt of $parent.
+        $qid = $db->query("SELECT rgt FROM $tbl WHERE name = '" . $db->escapeString($parent) . "'");
+        if (!list($parent_rgt) = mysql_fetch_row($qid)) {
+            $app->logMsg(sprintf('Cannot add %s node to nonexistent parent: %s', $type, $parent), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Update transversal numbers for all nodes to the rgt of $parent.
+        $db->query("UPDATE $tbl SET lft = lft + 2 WHERE lft >= $parent_rgt");
+        $db->query("UPDATE $tbl SET rgt = rgt + 2 WHERE rgt >= $parent_rgt");
+
+        // Insert new node just below parent. Lft is parent's old rgt.
+        $db->query("
+            INSERT INTO $tbl (name, lft, rgt, added_datetime)
+            VALUES ('" . $db->escapeString($name) . "', $parent_rgt, $parent_rgt + 1, NOW())
+        ");
+
+        $app->logMsg(sprintf('Added %s node %s to parent %s', $type, $name, $parent), LOG_INFO, __FILE__, __LINE__);
+        return mysql_insert_id($db->getDBH());
+    }
+
+    // Alias functions for the different object types.
+    public function addRequestObject($name, $parent=null)
+    {
+        return $this->add($name, $parent, 'aro');
+    }
+    public function addControlObject($name, $parent=null)
+    {
+        return $this->add($name, $parent, 'aco');
+    }
+    public function addXtraObject($name, $parent=null)
+    {
+        return $this->add($name, $parent, 'axo');
+    }
+
+    /*
+    * Remove a node from one of the aro/aco/axo tables.
+    *
+    * @access   public
+    * @param    string $name The identifier for the node to remove.
+    * @param    string $type The tree to modify, one of: aro, aco, axo.
+    * @return   bool | int False on error, or true on success.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    14 Jun 2006 22:39:29
+    */
+    public function remove($name, $type)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        switch ($type) {
+        case 'aro' :
+            $tbl = 'aro_tbl';
+            $primary_key = 'aro_id';
+            break;
+        case 'aco' :
+            $tbl = 'aco_tbl';
+            $primary_key = 'aco_id';
+            break;
+        case 'axo' :
+            $tbl = 'axo_tbl';
+            $primary_key = 'axo_id';
+            break;
+        default :
+            $app->logMsg(sprintf('Invalid access object type: %s', $type), LOG_ERR, __FILE__, __LINE__);
+            return false;
+            break;
+        }
+
+        // Ensure node name isn't empty.
+        if ('' == trim($name)) {
+            $app->logMsg(sprintf('Cannot add node, name missing.', null), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Select the lft and rgt of $name to use for selecting children and reordering transversals.
+        $qid = $db->query("SELECT lft, rgt FROM $tbl WHERE name = '" . $db->escapeString($name) . "'");
+        if (!list($lft, $rgt) = mysql_fetch_row($qid)) {
+            $app->logMsg(sprintf('Cannot delete nonexistent %s name: %s', $type, $name), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Remove node and all children of node, as well as acl_tbl links.
+        $db->query("
+            DELETE $tbl, acl_tbl
+            FROM $tbl
+            LEFT JOIN acl_tbl ON ($tbl.$primary_key = acl_tbl.$primary_key)
+            WHERE $tbl.lft BETWEEN $lft AND $rgt
+        ");
+        $num_deleted_nodes = mysql_affected_rows($db->getDBH());
+
+        // Update transversal numbers for all nodes to the rgt of $parent, taking into account the absence of its children.
+        $db->query("UPDATE $tbl SET lft = lft - ($rgt - $lft + 1) WHERE lft > $lft");
+        $db->query("UPDATE $tbl SET rgt = rgt - ($rgt - $lft + 1) WHERE rgt > $rgt");
+
+        $app->logMsg(sprintf('Removed %s node %s along with %s children.', $type, $name, $num_deleted_nodes - 1), LOG_INFO, __FILE__, __LINE__);
+        return true;
+    }
+
+    // Alias functions for the different object types.
+    public function removeRequestObject($name)
+    {
+        return $this->remove($name, 'aro');
+    }
+    public function removeControlObject($name)
+    {
+        return $this->remove($name, 'aco');
+    }
+    public function removeXtraObject($name)
+    {
+        return $this->remove($name, 'axo');
+    }
+
+    /*
+    * Move a node to a new parent in one of the aro/aco/axo tables.
+    *
+    * @access   public
+    * @param    string $name The identifier for the node to remove.
+    * @param    string $new_parent The name of the parent under-which to attach the new node.
+    * @param    string $type The tree to modify, one of: aro, aco, axo.
+    * @return   bool | int False on error, or the last_insert_id primary key of the new node.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    14 Jun 2006 22:39:29
+    */
+    public function move($name, $new_parent, $type)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        switch ($type) {
+        case 'aro' :
+            $tbl = 'aro_tbl';
+            $primary_key = 'aro_id';
+            break;
+        case 'aco' :
+            $tbl = 'aco_tbl';
+            $primary_key = 'aco_id';
+            break;
+        case 'axo' :
+            $tbl = 'axo_tbl';
+            $primary_key = 'axo_id';
+            break;
+        default :
+            $app->logMsg(sprintf('Invalid access object type: %s', $type), LOG_ERR, __FILE__, __LINE__);
+            return false;
+            break;
+        }
+
+        // If $new_parent is null, use root object.
+        if (is_null($new_parent)) {
+            $new_parent = 'root';
+        }
+
+        // Ensure node and parent name aren't empty.
+        if ('' == trim($name) || '' == trim($new_parent)) {
+            $app->logMsg(sprintf('Cannot add node, parent (%s) or name (%s) missing.', $name, $new_parent), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Select the lft and rgt of $name to use for selecting children and reordering transversals.
+        $qid = $db->query("SELECT lft, rgt FROM $tbl WHERE name = '" . $db->escapeString($name) . "'");
+        if (!list($lft, $rgt) = mysql_fetch_row($qid)) {
+            $app->logMsg(sprintf('Cannot move nonexistent %s name: %s', $type, $name), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Total number of transversal values (that is, the count of self plus all children times two).
+        $total_transversal_value = ($rgt - $lft + 1);
+
+        // Select the rgt of the new parent.
+        $qid = $db->query("SELECT rgt FROM $tbl WHERE name = '" . $db->escapeString($new_parent) . "'");
+        if (!list($new_parent_rgt) = mysql_fetch_row($qid)) {
+            $app->logMsg(sprintf('Cannot move %s node to nonexistent parent: %s', $type, $new_parent), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Ensure the new parent is not a child of the node being moved.
+        if ($new_parent_rgt <= $rgt && $new_parent_rgt >= $lft) {
+            $app->logMsg(sprintf('Cannot move %s node %s to parent %s because it is a child of itself.', $type, $name, $new_parent), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Collect unique ids of all nodes being moved. The transversal numbers will become duplicated so these will be needed to identify these.
+        $qid = $db->query("
+            SELECT $primary_key
+            FROM $tbl
+            WHERE lft BETWEEN $lft AND $rgt
+            AND rgt BETWEEN $lft AND $rgt
+        ");
+        $ids = array();
+        while (list($id) = mysql_fetch_row($qid)) {
+            $ids[] = $id;
+        }
+
+        // Update transversal numbers for all nodes to the rgt of the node being moved, taking in to account the absence of it's children.
+        // This will temporarily "remove" the node from the tree, and its transversal values will be duplicated.
+        $db->query("UPDATE $tbl SET lft = lft - $total_transversal_value WHERE lft > $rgt");
+        $db->query("UPDATE $tbl SET rgt = rgt - $total_transversal_value WHERE rgt > $rgt");
+
+        // Apply transformation to new parent rgt also.
+        $new_parent_rgt = $new_parent_rgt > $rgt ? $new_parent_rgt - $total_transversal_value : $new_parent_rgt;
+
+        // Update transversal values of moved node and children.
+        $db->query("
+            UPDATE $tbl SET
+                lft = lft - ($lft - $new_parent_rgt),
+                rgt = rgt - ($lft - $new_parent_rgt)
+            WHERE $primary_key IN ('" . join("','", $ids) . "')
+        ");
+
+        // Update transversal values of all nodes to the rgt of moved node.
+        $db->query("UPDATE $tbl SET lft = lft + $total_transversal_value WHERE lft >= $new_parent_rgt AND $primary_key NOT IN ('" . join("','", $ids) . "')");
+        $db->query("UPDATE $tbl SET rgt = rgt + $total_transversal_value WHERE rgt >= $new_parent_rgt AND $primary_key NOT IN ('" . join("','", $ids) . "')");
+
+        $app->logMsg(sprintf('Moved %s node %s to new parent %s', $type, $name, $new_parent), LOG_INFO, __FILE__, __LINE__);
+        return true;
+    }
+
+    // Alias functions for the different object types.
+    public function moveRequestObject($name, $new_parent=null)
+    {
+        return $this->move($name, $new_parent, 'aro');
+    }
+    public function moveControlObject($name, $new_parent=null)
+    {
+        return $this->move($name, $new_parent, 'aco');
+    }
+    public function moveXtraObject($name, $new_parent=null)
+    {
+        return $this->move($name, $new_parent, 'axo');
+    }
+
+    /*
+    * Add an entry to the acl_tbl to allow (or deny) a truple with the specified
+    * ARO -> ACO -> AXO entry.
+    *
+    * @access   public
+    * @param    string|null $aro Identifier of an existing ARO object (or null to use root).
+    * @param    string|null $aco Identifier of an existing ACO object (or null to use root).
+    * @param    string|null $axo Identifier of an existing AXO object (or null to use root).
+    * @return   bool False on error, true on success.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Jun 2006 01:58:48
+    */
+    public function grant($aro=null, $aco=null, $axo=null, $access='allow')
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // If any access objects are null, assume using root values.
+        // However if they're empty we don't want to escalate the grant command to root!
+        $aro = is_null($aro) ? 'root' : $aro;
+        $aco = is_null($aco) ? 'root' : $aco;
+        $axo = is_null($axo) ? 'root' : $axo;
+
+        // Flush old cached values.
+        $cache_hash = $aro . '|' . $aco . '|' . $axo;
+        $this->cache->delete($cache_hash);
+
+        // Ensure values exist.
+        $qid = $db->query("SELECT aro_tbl.aro_id FROM aro_tbl WHERE aro_tbl.name = '" . $db->escapeString($aro) . "'");
+        if (!list($aro_id) = mysql_fetch_row($qid)) {
+            $app->logMsg(sprintf('Grant failed, aro_tbl.name = "%s" does not exist.', $aro), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+        $qid = $db->query("SELECT aco_tbl.aco_id FROM aco_tbl WHERE aco_tbl.name = '" . $db->escapeString($aco) . "'");
+        if (!list($aco_id) = mysql_fetch_row($qid)) {
+            $app->logMsg(sprintf('Grant failed, aco_tbl.name = "%s" does not exist.', $aco), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+        $qid = $db->query("SELECT axo_tbl.axo_id FROM axo_tbl WHERE axo_tbl.name = '" . $db->escapeString($axo) . "'");
+        if (!list($axo_id) = mysql_fetch_row($qid)) {
+            $app->logMsg(sprintf('Grant failed, axo_tbl.name = "%s" does not exist.', $axo), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Access must be 'allow' or 'deny'.
+        $allow = 'allow' == $access ? 'allow' : 'deny';
+
+        $db->query("REPLACE INTO acl_tbl VALUES ('$aro_id', '$aco_id', '$axo_id', '$allow', NOW())");
+        $app->logMsg(sprintf('Set %s: %s -> %s -> %s', $allow, $aro, $aco, $axo), LOG_INFO, __FILE__, __LINE__);
+
+        return true;
+    }
+
+    /*
+    * Add an entry to the acl_tbl to deny a truple with the specified
+    * ARO -> ACO -> AXO entry. This calls the ACL::grant function to create the entry
+    * but uses 'deny' as the fourth argument.
+    *
+    * @access   public
+    * @param    string|null $aro Identifier of an existing ARO object (or null to use root).
+    * @param    string|null $aco Identifier of an existing ACO object (or null to use root).
+    * @param    string|null $axo Identifier of an existing AXO object (or null to use root).
+    * @return   bool False on error, true on success.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Jun 2006 04:35:54
+    */
+    public function revoke($aro=null, $aco=null, $axo=null)
+    {
+        return $this->grant($aro, $aco, $axo, 'deny');
+    }
+
+    /*
+    * Delete an entry from the acl_tbl completely to allow other permissions to cascade down.
+    * Null values act as a "wildcard" and will cause ALL matches in that column to be deleted.
+    *
+    * @access   public
+    * @param    string|null $aro Identifier of an existing ARO object (or null for *).
+    * @param    string|null $aco Identifier of an existing ACO object (or null for *).
+    * @param    string|null $axo Identifier of an existing AXO object (or null for *).
+    * @return   bool False on error, true on success.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    20 Jun 2006 20:16:12
+    */
+    public function delete($aro=null, $aco=null, $axo=null)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // If any access objects are null, assume using root values.
+        // However if they're empty we don't want to escalate the grant command to root!
+        $where = array();
+        $where[] = is_null($aro) ? "aro_tbl.name IS NOT NULL" : "aro_tbl.name = '" . $db->escapeString($aro) . "' ";
+        $where[] = is_null($aco) ? "aco_tbl.name IS NOT NULL" : "aco_tbl.name = '" . $db->escapeString($aco) . "' ";
+        $where[] = is_null($axo) ? "axo_tbl.name IS NOT NULL" : "axo_tbl.name = '" . $db->escapeString($axo) . "' ";
+
+        // If any access objects are null, assume using root values.
+        // However if they're empty we don't want to escalate the grant command to root!
+        $aro = is_null($aro) ? 'root' : $aro;
+        $aco = is_null($aco) ? 'root' : $aco;
+        $axo = is_null($axo) ? 'root' : $axo;
+
+        // Flush old cached values.
+        $cache_hash = $aro . '|' . $aco . '|' . $axo;
+        $this->cache->delete($cache_hash);
+
+        $final_where = join(' AND ', $where);
+        if (mb_substr_count($final_where, 'IS NOT NULL') == 3) {
+            // Null on all three tables will delete ALL entries including the root -> root -> root = deny.
+            $app->logMsg('Cannot allow deletion of ALL acl entries.', LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        $qid = $db->query("
+            DELETE acl_tbl
+            FROM acl_tbl
+            LEFT JOIN aro_tbl ON (acl_tbl.aro_id = aro_tbl.aro_id)
+            LEFT JOIN aco_tbl ON (acl_tbl.aco_id = aco_tbl.aco_id)
+            LEFT JOIN axo_tbl ON (acl_tbl.axo_id = axo_tbl.axo_id)
+            WHERE $final_where
+        ");
+
+        $app->logMsg(sprintf('Deleted %s acl_tbl links: %s -> %s -> %s', mysql_affected_rows($db->getDBH()), $aro, $aco, $axo), LOG_INFO, __FILE__, __LINE__);
+
+        return true;
+    }
+
+    /*
+    * Calculates the most specific cascading privilege found for a requested
+    * ARO -> ACO -> AXO entry. Returns FALSE if the entry is denied. By default,
+    * all entries are denied, unless some point in the hierarchy is set to "allow."
+    *
+    * @access   public
+    * @param    string $aro Identifier of an existing ARO object.
+    * @param    string $aco Identifier of an existing ACO object (or null to use root).
+    * @param    string $axo Identifier of an existing AXO object (or null to use root).
+    * @return   bool False if denied, true on allowed.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Jun 2006 03:58:23
+    */
+    public function check($aro, $aco=null, $axo=null)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // If any access objects are null or empty, assume using root values.
+        $aro = is_null($aro) || '' == trim($aro) ? 'root' : $aro;
+        $aco = is_null($aco) || '' == trim($aco) ? 'root' : $aco;
+        $axo = is_null($axo) || '' == trim($axo) ? 'root' : $axo;
+
+        $cache_hash = $aro . '|' . $aco . '|' . $axo;
+        if (true === $this->getParam('enable_cache') && $this->cache->exists($cache_hash)) {
+            // Access value is cached.
+            $access = $this->cache->get($cache_hash);
+        } else {
+            // Retrieve access value from db.
+            $qid = $db->query("
+                SELECT acl_tbl.access
+                FROM acl_tbl
+                LEFT JOIN aro_tbl ON (acl_tbl.aro_id = aro_tbl.aro_id)
+                LEFT JOIN aco_tbl ON (acl_tbl.aco_id = aco_tbl.aco_id)
+                LEFT JOIN axo_tbl ON (acl_tbl.axo_id = axo_tbl.axo_id)
+                WHERE (aro_tbl.lft <= (SELECT lft FROM aro_tbl WHERE name = '" . $db->escapeString($aro) . "') AND aro_tbl.rgt >= (SELECT rgt FROM aro_tbl WHERE name = '" . $db->escapeString($aro) . "'))
+                AND (aco_tbl.lft <= (SELECT lft FROM aco_tbl WHERE name = '" . $db->escapeString($aco) . "') AND aco_tbl.rgt >= (SELECT rgt FROM aco_tbl WHERE name = '" . $db->escapeString($aco) . "'))
+                AND (axo_tbl.lft <= (SELECT lft FROM axo_tbl WHERE name = '" . $db->escapeString($axo) . "') AND axo_tbl.rgt >= (SELECT rgt FROM axo_tbl WHERE name = '" . $db->escapeString($axo) . "'))
+                ORDER BY aro_tbl.lft DESC, aco_tbl.lft DESC, axo_tbl.lft DESC
+                LIMIT 1
+            ");
+            if (!list($access) = mysql_fetch_row($qid)) {
+                $this->cache->set($cache_hash, 'deny');
+                $app->logMsg(sprintf('Access denied: %s -> %s -> %s (no records found).', $aro, $aco, $axo), LOG_WARNING, __FILE__, __LINE__);
+                return false;
+            }
+            $this->cache->set($cache_hash, $access);
+        }
+
+        if ('allow' == $access) {
+            $app->logMsg(sprintf('Access granted: %s -> %s -> %s', $aro, $aco, $axo), LOG_DEBUG, __FILE__, __LINE__);
+            return true;
+        } else {
+            $app->logMsg(sprintf('Access denied: %s -> %s -> %s', $aro, $aco, $axo), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /*
+    * Bounce user if they are denied access. Because this function calls dieURL() it must be called before any other HTTP header output.
+    *
+    * @access   public
+    * @param    string $aro Identifier of an existing ARO object.
+    * @param    string $aco Identifier of an existing ACO object (or null to use root).
+    * @param    string $axo Identifier of an existing AXO object (or null to use root).
+    * @param    string $message The text description of a message to raise.
+    * @param    int    $type    The type of message: MSG_NOTICE,
+    *                           MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
+    * @param    string $file    __FILE__.
+    * @param    string $line    __LINE__.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    20 Jan 2014 12:09:03
+    */
+    public function requireAllow($aro, $aco=null, $axo=null, $message='', $type=MSG_NOTICE, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+
+        if (!$this->check($aro, $aco, $axo)) {
+            $message = '' == trim($message) ? sprintf(_("Sorry, you have insufficient privileges for <em>%s %s</em>."), $aco, $axo) : $message;
+            $app->raiseMsg($message, $type, $file, $line);
+            $app->dieBoomerangURL();
+        }
+    }
+
+    /*
+    * Returns an array of the specified object type starting specified root.
+    *
+    * @access   public
+    * @param    string $type Table to list, one of: aro, aco, or axo.
+    * @param    string $root Root node from which to begin from.
+    * @return   mixed        Returns a multidimensional array of objects, or false on error.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    17 Jun 2006 23:41:22
+    */
+    function getList($type, $root=null)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        switch ($type) {
+        case 'aro' :
+            $tbl = 'aro_tbl';
+            break;
+        case 'aco' :
+            $tbl = 'aco_tbl';
+            break;
+        case 'axo' :
+            $tbl = 'axo_tbl';
+            break;
+        default :
+            $app->logMsg(sprintf('Invalid access object type: %s', $type), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // By default start with the 'root' node.
+        $root = !isset($root) ? 'root' : $root;
+
+        // Retrieve the left and right value of the $root node.
+        $qid = $db->query("SELECT lft, rgt FROM $tbl WHERE name = '" . $db->escapeString($root) . "'");
+        list($lft, $rgt) = mysql_fetch_row($qid);
+
+        $results = array();
+        $depth = array();
+
+        // Retrieve all descendants of the root node
+        $qid = $db->query("SELECT name, lft, rgt, added_datetime FROM $tbl WHERE lft BETWEEN $lft AND $rgt ORDER BY lft ASC");
+        while (list($name, $lft, $rgt, $added_datetime) = mysql_fetch_row($qid)) {
+            // If the last element of $depth is less than the current rgt it means we finished with a set of children nodes.
+            while (sizeof($depth) > 0 && end($depth) < $rgt) {
+                array_pop($depth);
+            }
+
+            $results[] = array(
+                'name' => $name,
+                'added_datetime' => $added_datetime,
+                'depth' => sizeof($depth),
+            );
+
+            // Add this node to the stack.
+            $depth[] = $rgt;
+        }
+
+        return $results;
+    }
+
+} // End class.
Index: tags/2.2.3/lib/App.inc.php
===================================================================
--- tags/2.2.3/lib/App.inc.php	(revision 632)
+++ tags/2.2.3/lib/App.inc.php	(revision 632)
@@ -0,0 +1,1677 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * App.inc.php
+ *
+ * Primary application framework class.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 2.1
+ */
+
+// Message Types.
+define('MSG_ERR', 1);
+define('MSG_ERROR', MSG_ERR);
+define('MSG_WARNING', 2);
+define('MSG_NOTICE', 4);
+define('MSG_SUCCESS', 8);
+define('MSG_ALL', MSG_SUCCESS | MSG_NOTICE | MSG_WARNING | MSG_ERROR);
+
+require_once dirname(__FILE__) . '/Utilities.inc.php';
+
+class App
+{
+    // Minimum version of PHP required for this version of the Codebase.
+    const CODEBASE_MIN_PHP_VERSION = '5.3.0';
+
+    // A place to keep an object instance for the singleton pattern.
+    protected static $instance = null;
+
+    // Namespace of this application instance.
+    protected $_ns;
+
+    // If $app->start has run successfully.
+    public $running = false;
+
+    // Instance of database object.
+    public $db;
+
+    // Instance of database session handler object.
+    public $db_session;
+
+    // Array of query arguments will be carried persistently between requests.
+    protected $_carry_queries = array();
+
+    // Array of raised message counters.
+    protected $_raised_msg_counter = array(MSG_NOTICE => 0, MSG_SUCCESS => 0, MSG_WARNING => 0, MSG_ERR => 0);
+
+    // We're running as CLI. Public because we must force this as false when testing sessions via CLI.
+    public $cli = false;
+
+    // Dictionary of global application parameters.
+    protected $_params = array();
+
+    // Default parameters.
+    protected $_param_defaults = array(
+
+        // Public name and email address for this application.
+        'site_name' => null,
+        'site_email' => '', // Set to no-reply@HTTP_HOST if not set here.
+        'site_url' => '', // URL to the root of the site (created during App->start()).
+        'page_url' => '', // URL to the current page (created during App->start()).
+        'images_path' => '', // Location for codebase-generated interface widgets (ex: "/admin/i").
+        'site_version' => '', // Version of this application (set automatically during start() if site_version_file is used).
+        'site_version_file' => 'docs/version.txt', // File containing version number of this app, relative to the include path.
+
+        // The location the user will go if the system doesn't know where else to send them.
+        'redirect_home_url' => '/',
+
+        // SSL URL used when redirecting with $app->sslOn().
+        'ssl_domain' => null,
+        'ssl_enabled' => false,
+
+        // Use CSRF tokens. See notes in the getCSRFToken() method.
+        'csrf_token_enabled' => true,
+        // Form tokens will expire after this duration, in seconds.
+        'csrf_token_timeout' => 259200, // 259200 seconds = 3 days.
+        'csrf_token_name' => 'csrf_token',
+
+        // HMAC signing method
+        'signing_method' => 'sha512+base64',
+
+        // Content type of output sent in the Content-type: http header.
+        'content_type' => 'text/html',
+
+        // Allow HTTP caching with max-age setting. Possible values:
+        //  >= 1    Allow HTTP caching with this value set as the max-age (in seconds, i.e., 3600 = 1 hour).
+        //  0       Disallow HTTP caching.
+        //  false   Don't send any cache-related HTTP headers (if you want to control this via server config or custom headers)
+        // This should be '0' for websites that use authentication or have frequently changing dynamic content.
+        'http_cache_headers' => 0,
+
+        // Character set for page output. Used in the Content-Type header and the HTML <meta content-type> tag.
+        'character_set' => 'utf-8',
+
+        // Human-readable format used to display dates.
+        'date_format' => 'd M Y',
+        'time_format' => 'h:i:s A',
+        'sql_date_format' => '%e %b %Y',
+        'sql_time_format' => '%k:%i',
+
+        // Use php sessions?
+        'enable_session' => false,
+        'session_name' => '_session',
+        'session_use_cookies' => true,
+
+        // Pass the session-id through URLs if cookies are not enabled?
+        // Disable this to prevent session ID theft.
+        'session_use_trans_sid' => false,
+
+        // Use database?
+        'enable_db' => false,
+
+        // Use db-based sessions?
+        'enable_db_session_handler' => false,
+
+        // DB credentials should be set as apache environment variables in httpd.conf, readable only by root.
+        'db_server' => null,
+        'db_name' => null,
+        'db_user' => null,
+        'db_pass' => null,
+
+        // And for CLI scripts, which should include a JSON file at this specified location in the include path.
+        'db_auth_file' => 'db_auth.json',
+
+        // Database debugging.
+        'db_always_debug' => false, // TRUE = display all SQL queries.
+        'db_debug' => false, // TRUE = display db errors.
+        'db_die_on_failure' => false, // TRUE = script stops on db error.
+
+        // For classes that require db tables, do we check that a table exists and create if missing?
+        'db_create_tables' => true,
+
+        // The level of error reporting. Don't change this to suppress messages, instead use display_errors to control display.
+        'error_reporting' => E_ALL,
+
+        // Don't display errors by default; it is preferable to log them to a file. For CLI scripts, set this to the string 'stderr'.
+        'display_errors' => false,
+
+        // Directory in which to store log files.
+        'log_directory' => '',
+
+        // PHP error log.
+        'php_error_log' => 'php_error_log',
+
+        // General application log.
+        'log_filename' => 'app_log',
+
+        // Don't email or SMS duplicate messages that happen more often than this value (in seconds).
+        'log_multiple_timeout' => 3600, // Hourly
+
+        // Logging priority can be any of the following, or false to deactivate:
+        // LOG_EMERG     system is unusable
+        // LOG_ALERT     action must be taken immediately
+        // LOG_CRIT      critical conditions
+        // LOG_ERR       error conditions
+        // LOG_WARNING   warning conditions
+        // LOG_NOTICE    normal, but significant, condition
+        // LOG_INFO      informational message
+        // LOG_DEBUG     debug-level message
+        'log_file_priority' => LOG_INFO,
+        'log_email_priority' => false,
+        'log_sms_priority' => false,
+        'log_screen_priority' => false,
+
+        // Email address to receive log event emails. Use multiple addresses by separating them with commas.
+        'log_to_email_address' => null,
+
+        // SMS Email address to receive log event SMS messages. Use multiple addresses by separating them with commas.
+        'log_to_sms_address' => null,
+
+        // Should we avoid logging repeated logMsg() events? You might want to set this false if you need to see more accurate logging, particularly for long-running scripts.
+        'log_ignore_repeated_events' => true,
+
+        // Maximum length of log messages, in bytes.
+        'log_message_max_length' => 1024,
+
+        // Strip line-endings from log messages.
+        'log_serialize' => true,
+
+        // Temporary files directory.
+        'tmp_dir' => '/tmp',
+
+        // A key for calculating simple cryptographic signatures. Set using as an environment variables in the httpd.conf with 'SetEnv SIGNING_KEY <key>'.
+        // Existing password hashes rely on the same key/salt being used to compare encryptions.
+        // Don't change this unless you know existing hashes or signatures will not be affected!
+        'signing_key' => 'aae6abd6209d82a691a9f96384a7634a',
+
+        // Force getFormData, getPost, and getGet to always run dispelMagicQuotes() with stripslashes().
+        // This should be set to 'true' when using the codebase with Wordpress because
+        // WP forcefully adds slashes to all input despite the setting of magic_quotes_gpc.
+        'always_dispel_magicquotes' => false,
+    );
+
+    /**
+     * Constructor.
+     */
+    public function __construct($namespace='')
+    {
+        // Initialize default parameters.
+        $this->_params = array_merge($this->_params, array('namespace' => $namespace), $this->_param_defaults);
+
+        // Begin timing script.
+        require_once dirname(__FILE__) . '/ScriptTimer.inc.php';
+        $this->timer = new ScriptTimer();
+        $this->timer->start('_app');
+
+        // Are we running as a CLI?
+        $this->cli = ('cli' === php_sapi_name() || defined('_CLI'));
+    }
+
+    /**
+     * This method enforces the singleton pattern for this class. Only one application is running at a time.
+     *
+     * $param   string  $namespace  Name of this application.
+     * @return  object  Reference to the global Cache object.
+     * @access  public
+     * @static
+     */
+    public static function &getInstance($namespace='')
+    {
+        if (self::$instance === null) {
+            // FIXME: Yep, having a namespace with one singleton instance is not very useful. This is a design flaw with the Codebase.
+            // We're currently getting instances of App throughout the codebase using `$app =& App::getInstance();`
+            // with no way to determine what the namespace of the containing application is (e.g., `public` vs. `admin`).
+            // Option 1: provide the project namespace to all classes that use App, and then instantiate with `$app =& App::getInstance($this->_ns);`.
+            // In this case the namespace of the App and the namespace of the auxiliary class must match.
+            // Option 2: may be to clone a specific instance to the "default" instance, so, e.g., `$app =& App::getInstance();`
+            // refers to the same namespace as `$app =& App::getInstance('admin');`
+            // Option 3: is to check if there is only one instance, and return it if an unspecified namespace is requested.
+            // However, in the case when multiple namespaces are in play at the same time, this will fail; unspecified namespaces
+            // would cause the creation of an additional instance, since there would not be an obvious named instance to return.
+            self::$instance = new self($namespace);
+        }
+
+        if ('' != $namespace) {
+            // We may not be able to request a specific instance, but we can specify a specific namespace.
+            // We're ignoring all instance requests with a blank namespace, so we use the last given one.
+            self::$instance->_ns = $namespace;
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $param     Array of parameters (key => val pairs).
+     */
+    public function setParam($param=null)
+    {
+        if (isset($param) && is_array($param)) {
+            // Merge new parameters with old overriding old ones that are passed.
+            $this->_params = array_merge($this->_params, $param);
+
+            if ($this->running) {
+                // Params that require additional processing if set during runtime.
+                foreach ($param as $key => $val) {
+                    switch ($key) {
+                    case 'namespace':
+                        $this->logMsg(sprintf('Setting namespace not allowed', null), LOG_WARNING, __FILE__, __LINE__);
+                        return false;
+
+                    case 'session_name':
+                        session_name($val);
+                        return true;
+
+                    case 'session_use_cookies':
+                        ini_set('session.use_cookies', $val);
+                        return true;
+
+                    case 'error_reporting':
+                        ini_set('error_reporting', $val);
+                        return true;
+
+                    case 'display_errors':
+                        ini_set('display_errors', $val);
+                        return true;
+
+                    case 'log_errors':
+                        ini_set('log_errors', true);
+                        return true;
+
+                    case 'log_directory':
+                        if (is_dir($val) && is_writable($val)) {
+                            ini_set('error_log', $val . '/' . $this->getParam('php_error_log'));
+                        }
+                        return true;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Return the value of a parameter.
+     *
+     * @access  public
+     * @param   string  $param      The key of the parameter to return.
+     * @return  mixed               Parameter value, or null if not existing.
+     */
+    public function getParam($param=null)
+    {
+        if ($param === null) {
+            return $this->_params;
+        } else if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Begin running this application.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   15 Jul 2005 00:32:21
+     */
+    public function start()
+    {
+        if ($this->running) {
+            return false;
+        }
+
+        // Error reporting.
+        ini_set('error_reporting', $this->getParam('error_reporting'));
+        ini_set('display_errors', $this->getParam('display_errors'));
+        ini_set('log_errors', true);
+        if (is_dir($this->getParam('log_directory')) && is_writable($this->getParam('log_directory'))) {
+            ini_set('error_log', $this->getParam('log_directory') . '/' . $this->getParam('php_error_log'));
+        }
+
+        // Set character set to use for multi-byte string functions.
+        mb_internal_encoding($this->getParam('character_set'));
+        ini_set('default_charset', $this->getParam('character_set'));
+        switch (mb_strtolower($this->getParam('character_set'))) {
+        case 'utf-8' :
+            mb_language('uni');
+            break;
+
+        case 'iso-2022-jp' :
+            mb_language('ja');
+            break;
+
+        case 'iso-8859-1' :
+        default :
+            mb_language('en');
+            break;
+        }
+
+        /**
+         * 1. Start Database.
+         */
+
+        if (true === $this->getParam('enable_db')) {
+
+            // DB connection parameters taken from environment variables in the server httpd.conf file (readable only by root)âŠ
+            if (!empty($_SERVER['DB_SERVER']) && !$this->getParam('db_server')) {
+                $this->setParam(array('db_server' => $_SERVER['DB_SERVER']));
+            }
+            if (!empty($_SERVER['DB_NAME']) && !$this->getParam('db_name')) {
+                $this->setParam(array('db_name' => $_SERVER['DB_NAME']));
+            }
+            if (!empty($_SERVER['DB_USER']) && !$this->getParam('db_user')) {
+                $this->setParam(array('db_user' => $_SERVER['DB_USER']));
+            }
+            if (!empty($_SERVER['DB_PASS']) && !$this->getParam('db_pass')) {
+                $this->setParam(array('db_pass' => $_SERVER['DB_PASS']));
+            }
+
+            // DB credentials for CLI scripts stored in a JSON file with read rights given only to the user who will be executing the scripts: -r--------
+            // But not if all DB credentials have been defined already by other means.
+            if ($this->cli && (!$this->getParam('db_server') || !$this->getParam('db_name') || !$this->getParam('db_user') || !$this->getParam('db_pass'))) {
+                if (false !== $db_auth_file = stream_resolve_include_path($this->getParam('db_auth_file'))) {
+                    if (is_readable($db_auth_file)) {
+                        $this->setParam(json_decode(file_get_contents($db_auth_file), true));
+                    } else {
+                        $this->logMsg(sprintf('Unable to read DB auth file: %s', $db_auth_file), LOG_NOTICE, __FILE__, __LINE__);
+                    }
+                } else {
+                    $this->logMsg(sprintf('DB auth file not found: %s', $this->getParam('db_auth_file')), LOG_NOTICE, __FILE__, __LINE__);
+                }
+            }
+
+            // There will ever only be one instance of the DB object, and here is where it is instantiated.
+            require_once dirname(__FILE__) . '/DB.inc.php';
+            $this->db =& DB::getInstance();
+            $this->db->setParam(array(
+                'db_server' => $this->getParam('db_server'),
+                'db_name' => $this->getParam('db_name'),
+                'db_user' => $this->getParam('db_user'),
+                'db_pass' => $this->getParam('db_pass'),
+                'db_always_debug' => $this->getParam('db_always_debug'),
+                'db_debug' => $this->getParam('db_debug'),
+                'db_die_on_failure' => $this->getParam('db_die_on_failure'),
+            ));
+
+            // Connect to database.
+            $this->db->connect();
+        }
+
+
+        /**
+         * 2. Start PHP session.
+         */
+
+        // Use sessions if enabled and not a CLI script.
+        if (true === $this->getParam('enable_session') && !$this->cli) {
+
+            // Session parameters.
+            ini_set('session.gc_probability', 1);
+            ini_set('session.gc_divisor', 1000);
+            ini_set('session.gc_maxlifetime', 43200); // 12 hours
+            ini_set('session.use_cookies', $this->getParam('session_use_cookies'));
+            ini_set('session.use_trans_sid', false);
+            ini_set('session.entropy_file', '/dev/urandom');
+            ini_set('session.entropy_length', '512');
+            ini_set('session.cookie_httponly', true);
+            session_name($this->getParam('session_name'));
+
+            if (true === $this->getParam('enable_db_session_handler') && true === $this->getParam('enable_db')) {
+                // Database session handling.
+                require_once dirname(__FILE__) . '/DBSessionHandler.inc.php';
+                $this->db_session = new DBSessionHandler($this->db, array(
+                    'db_table' => 'session_tbl',
+                    'create_table' => $this->getParam('db_create_tables'),
+                ));
+            }
+
+            // Start the session.
+            session_start();
+
+            if (!isset($_SESSION['_app'][$this->_ns])) {
+                // Access session data using: $_SESSION['...'].
+                // Initialize here _after_ session has started.
+                $_SESSION['_app'][$this->_ns] = array(
+                    'messages' => array(),
+                    'boomerang' => array(),
+                );
+            }
+        }
+
+
+        /**
+         * 3. Misc setup.
+         */
+
+        // Site URL will become something like http://host.name.tld (no ending slash)
+        // and is used whenever a URL need be used to the current site.
+        // Not available on CLI scripts obviously.
+        if (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST'] && '' == $this->getParam('site_url')) {
+            $this->setParam(array('site_url' => sprintf('%s://%s', (getenv('HTTPS') ? 'https' : 'http'), getenv('HTTP_HOST'))));
+        }
+
+        // Page URL will become a permalink to the current page.
+        // Also not available on CLI scripts obviously.
+        if (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST']) {
+            $this->setParam(array('page_url' => sprintf('%s://%s%s', (getenv('HTTPS') ? 'https' : 'http'), getenv('HTTP_HOST'), getenv('REQUEST_URI'))));
+        }
+
+        // In case site_email isn't set, use something halfway presentable.
+        if (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST'] && '' == $this->getParam('site_email')) {
+            $this->setParam(array('site_email' => sprintf('no-reply@%s', getenv('HTTP_HOST'))));
+        }
+
+        // A key for calculating simple cryptographic signatures.
+        if (isset($_SERVER['SIGNING_KEY'])) {
+            $this->setParam(array('signing_key' => $_SERVER['SIGNING_KEY']));
+        }
+
+        // Character set. This should also be printed in the html header template.
+        if (!$this->cli) {
+            if (!headers_sent($h_file, $h_line)) {
+                header(sprintf('Content-type: %s; charset=%s', $this->getParam('content_type'), $this->getParam('character_set')));
+            } else {
+                $this->logMsg(sprintf('Unable to set Content-type; headers already sent (output started in %s : %s)', $h_file, $h_line), LOG_DEBUG, __FILE__, __LINE__);
+            }
+        }
+
+        // Cache control headers.
+        if (!$this->cli && false !== $this->getParam('http_cache_headers')) {
+            if (!headers_sent($h_file, $h_line)) {
+                if ($this->getParam('http_cache_headers') > 0) {
+                    // Allow HTTP caching, for this many seconds.
+                    header(sprintf('Cache-Control: no-transform, public, max-age=%d', $this->getParam('http_cache_headers')));
+                    header('Vary: Accept-Encoding');
+                } else {
+                    // Disallow HTTP caching entirely. http://stackoverflow.com/a/2068407
+                    header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1.
+                    header('Pragma: no-cache'); // HTTP 1.0.
+                    header('Expires: 0'); // Proxies.
+                }
+            } else {
+                $this->logMsg(sprintf('Unable to set Cache-Control; headers already sent (output started in %s : %s)', $h_file, $h_line), LOG_DEBUG, __FILE__, __LINE__);
+            }
+        }
+
+        // Set the version of the codebase we're using.
+        $codebase_version_file = dirname(__FILE__) . '/../docs/version.txt';
+        $codebase_version = '';
+        if (is_readable($codebase_version_file) && !is_dir($codebase_version_file)) {
+            $codebase_version = trim(file_get_contents($codebase_version_file));
+            $this->setParam(array('codebase_version' => $codebase_version));
+            if (!$this->cli && $this->getParam('codebase_version')) {
+                if (!headers_sent($h_file, $h_line)) {
+                    header(sprintf('X-Codebase-Version: %s', $this->getParam('codebase_version')));
+                } else {
+                    $this->logMsg(sprintf('Unable to set X-Codebase-Version; headers already sent (output started in %s : %s)', $h_file, $h_line), LOG_DEBUG, __FILE__, __LINE__);
+                }
+            }
+        }
+
+        if (version_compare(PHP_VERSION, self::CODEBASE_MIN_PHP_VERSION, '<')) {
+            $this->logMsg(sprintf('PHP %s required for Codebase %s, using %s; some things will break.', self::CODEBASE_MIN_PHP_VERSION, $codebase_version, PHP_VERSION), LOG_NOTICE, __FILE__, __LINE__);
+        }
+
+        // Set the application version if defined.
+        if (false !== $site_version_file = stream_resolve_include_path($this->getParam('site_version_file'))) {
+            if (mb_strpos($site_version_file, '.json') !== false) {
+                $version_json = json_decode(trim(file_get_contents($site_version_file)), true);
+                $site_version = isset($version_json['version']) ? $version_json['version'] : null;
+            } else {
+                $site_version = trim(file_get_contents($site_version_file));
+            }
+            $this->setParam(array('site_version' => $site_version));
+        }
+        if (!$this->cli && $this->getParam('site_version')) {
+            if (!headers_sent($h_file, $h_line)) {
+                header(sprintf('X-Site-Version: %s', $this->getParam('site_version')));
+            } else {
+                $this->logMsg(sprintf('Unable to set X-Site-Version; headers already sent (output started in %s : %s)', $h_file, $h_line), LOG_DEBUG, __FILE__, __LINE__);
+            }
+        }
+
+        // Unset environment variables we're done with.
+        unset($_SERVER['DB_SERVER'], $_SERVER['DB_NAME'], $_SERVER['DB_USER'], $_SERVER['DB_PASS'], $_SERVER['SIGNING_KEY']);
+
+        $this->running = true;
+        return true;
+    }
+
+    /**
+     * Stop running this application.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   17 Jul 2005 17:20:18
+     */
+    public function stop()
+    {
+        session_write_close();
+        $this->running = false;
+        $num_queries = 0;
+        if (is_a($this->db, 'DB') && true === $this->getParam('enable_db')) {
+            $num_queries = $this->db->numQueries();
+            $this->db->close();
+        }
+        $mem_current = memory_get_usage();
+        $mem_peak = memory_get_peak_usage();
+        $this->timer->stop('_app');
+        $this->logMsg(sprintf('Script ended gracefully. Execution time: %s. Number of db queries: %s. Memory usage: %s. Peak memory: %s.', $this->timer->getTime('_app'), $num_queries, $mem_current, $mem_peak), LOG_DEBUG, __FILE__, __LINE__);
+    }
+
+
+    /**
+     * Add a message to the session, which is printed in the header.
+     * Just a simple way to print messages to the user.
+     *
+     * @access public
+     *
+     * @param string $message The text description of the message.
+     * @param int    $type    The type of message: MSG_NOTICE,
+     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
+     * @param string $file    __FILE__.
+     * @param string $line    __LINE__.
+     */
+    public function raiseMsg($message, $type=MSG_NOTICE, $file=null, $line=null)
+    {
+        $message = trim($message);
+
+        if (!$this->running) {
+            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (!$this->getParam('enable_session')) {
+            $this->logMsg(sprintf('Canceled %s, session not enabled.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        if ('' == trim($message)) {
+            $this->logMsg(sprintf('Raised message is an empty string.', null), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Avoid duplicate full-stops..
+        $message = trim(preg_replace('/\.{2}$/', '.', $message));
+
+        // Save message in session under unique key to avoid duplicate messages.
+        $msg_id = md5($type . $message);
+        if (!isset($_SESSION['_app'][$this->_ns]['messages'][$msg_id])) {
+            $_SESSION['_app'][$this->_ns]['messages'][$msg_id] = array(
+                'type'    => $type,
+                'message' => $message,
+                'file'    => $file,
+                'line'    => $line,
+                'count'   => (isset($_SESSION['_app'][$this->_ns]['messages'][$msg_id]['count']) ? (1 + $_SESSION['_app'][$this->_ns]['messages'][$msg_id]['count']) : 1)
+            );
+        }
+
+        if (!in_array($type, array(MSG_NOTICE, MSG_SUCCESS, MSG_WARNING, MSG_ERR))) {
+            $this->logMsg(sprintf('Invalid MSG_* type: %s', $type), LOG_NOTICE, __FILE__, __LINE__);
+        }
+
+        // Increment the counter for this message type.
+        $this->_raised_msg_counter[$type] += 1;
+    }
+
+    /*
+    * Returns the number of raised message (all, or by type) for the current script execution (this number may not match the total number of messages stored in session for multiple script executions)
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    30 Apr 2015 17:13:03
+    */
+    public function getRaisedMessageCount($type='all')
+    {
+        if ('all' == $type) {
+            return array_sum($this->_raised_msg_counter);
+        } else if (isset($this->_raised_msg_counter[$type])) {
+            return $this->_raised_msg_counter[$type];
+        } else {
+            $this->logMsg(sprintf('Cannot return count of unknown raised message type: %s', $type), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Returns an array of the raised messages.
+     *
+     * @access  public
+     * @return  array   List of messages in FIFO order.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   21 Dec 2005 13:09:20
+     */
+    public function getRaisedMessages()
+    {
+        if (!$this->running) {
+            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+        return isset($_SESSION['_app'][$this->_ns]['messages']) ? $_SESSION['_app'][$this->_ns]['messages'] : array();
+    }
+
+    /**
+     * Resets the message list.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   21 Dec 2005 13:21:54
+     */
+    public function clearRaisedMessages()
+    {
+        if (!$this->running) {
+            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        $_SESSION['_app'][$this->_ns]['messages'] = array();
+    }
+
+    /**
+     * Prints the HTML for displaying raised messages.
+     *
+     * @param   string  $above    Additional message to print above error messages (e.g. "Oops!").
+     * @param   string  $below    Additional message to print below error messages (e.g. "Please fix and resubmit").
+     * @param   string  $print_gotohash_js  Print a line of javascript that scrolls the browser window down to view any error messages.
+     * @param   string  $hash     The #hashtag to scroll to.
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   15 Jul 2005 01:39:14
+     */
+    public function printRaisedMessages($above='', $below='', $print_gotohash_js=false, $hash='sc-msg')
+    {
+
+        if (!$this->running) {
+            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        $messages = $this->getRaisedMessages();
+        if (!empty($messages)) {
+            ?><div id="sc-msg" class="sc-msg"><?php
+            if ('' != $above) {
+                ?><div class="sc-above"><?php echo oTxt($above); ?></div><?php
+            }
+            foreach ($messages as $m) {
+                if (error_reporting() > 0 && $this->getParam('display_errors') && isset($m['file']) && isset($m['line'])) {
+                    echo "\n<!-- [" . $m['file'] . ' : ' . $m['line'] . '] -->';
+                }
+                switch ($m['type']) {
+                case MSG_ERR:
+                    echo '<div data-alert class="sc-msg-error alert-box alert">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
+                    break;
+
+                case MSG_WARNING:
+                    echo '<div data-alert class="sc-msg-warning alert-box warning">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
+                    break;
+
+                case MSG_SUCCESS:
+                    echo '<div data-alert class="sc-msg-success alert-box success">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
+                    break;
+
+                case MSG_NOTICE:
+                default:
+                    echo '<div data-alert class="sc-msg-notice alert-box info">' . $m['message'] . '<a href="#" class="close">&times;</a></div>';
+                    break;
+                }
+            }
+            if ('' != $below) {
+                ?><div class="sc-below"><?php echo oTxt($below); ?></div><?php
+            }
+            ?></div><?php
+            if ($print_gotohash_js) {
+                ?>
+                <script type="text/javascript">
+                /* <![CDATA[ */
+                window.location.hash = '#<?php echo urlencode($hash); ?>';
+                /* ]]> */
+                </script>
+                <?php
+            }
+        }
+        $this->clearRaisedMessages();
+    }
+
+    /**
+     * Logs messages to defined channels: file, email, sms, and screen. Repeated messages are
+     * not repeated but printed once with count. Log events that match a sendable channel (email or SMS)
+     * are sent once per 'log_multiple_timeout' setting (to avoid a flood of error emails).
+     *
+     * @access public
+     * @param string $message   The text description of the message.
+     * @param int    $priority  The type of message priority (in descending order):
+     *                          LOG_EMERG     0 system is unusable
+     *                          LOG_ALERT     1 action must be taken immediately
+     *                          LOG_CRIT      2 critical conditions
+     *                          LOG_ERR       3 error conditions
+     *                          LOG_WARNING   4 warning conditions
+     *                          LOG_NOTICE    5 normal, but significant, condition
+     *                          LOG_INFO      6 informational message
+     *                          LOG_DEBUG     7 debug-level message
+     * @param string $file      The file where the log event occurs.
+     * @param string $line      The line of the file where the log event occurs.
+     * @param string $url       The URL where the log event occurs ($_SERVER['REQUEST_URI'] will be used if left null).
+     */
+    public function logMsg($message, $priority=LOG_INFO, $file=null, $line=null, $url=null)
+    {
+        static $previous_events = array();
+
+        // If priority is not specified, assume the worst.
+        if (!$this->logPriorityToString($priority)) {
+            $this->logMsg(sprintf('Log priority %s not defined. (Message: %s)', $priority, $message), LOG_EMERG, $file, $line);
+            $priority = LOG_EMERG;
+        }
+
+        // In case __FILE__ and __LINE__ are not provided, note that fact.
+        $file = '' == $file ? 'unknown-file' : $file;
+        $line = '' == $line ? 'unknown-line' : $line;
+
+        // Get the URL, or used the provided value.
+        if (!isset($url)) {
+            $url = isset($_SERVER['REQUEST_URI']) ? mb_substr($_SERVER['REQUEST_URI'], 0, $this->getParam('log_message_max_length')) : '';
+        }
+
+        // If log file is not specified, don't log to a file.
+        if (!$this->getParam('log_directory') || !$this->getParam('log_filename') || !is_dir($this->getParam('log_directory')) || !is_writable($this->getParam('log_directory'))) {
+            $this->setParam(array('log_file_priority' => false));
+            // We must use trigger_error to report this problem rather than calling $app->logMsg, which might lead to an infinite loop.
+            trigger_error(sprintf('Codebase error: log directory (%s) not found or writable.', $this->getParam('log_directory')), E_USER_NOTICE);
+        }
+
+        // Before we get any further, let's see if ANY log events are configured to be reported.
+        if ((false === $this->getParam('log_file_priority') || $priority > $this->getParam('log_file_priority'))
+        && (false === $this->getParam('log_email_priority') || $priority > $this->getParam('log_email_priority'))
+        && (false === $this->getParam('log_sms_priority') || $priority > $this->getParam('log_sms_priority'))
+        && (false === $this->getParam('log_screen_priority') || $priority > $this->getParam('log_screen_priority'))) {
+            // This event would not be recorded, skip it entirely.
+            return false;
+        }
+
+        if ($this->getParam('log_serialize')) {
+            // Serialize multi-line messages.
+            $message = preg_replace('/\s+/m', ' ', trim($message));
+        }
+
+        // Store this event under a unique key, counting each time it occurs so that it only gets reported a limited number of times.
+        $msg_id = md5($message . $priority . $file . $line);
+        if ($this->getParam('log_ignore_repeated_events') && isset($previous_events[$msg_id])) {
+            $previous_events[$msg_id]++;
+            if ($previous_events[$msg_id] == 2) {
+                $this->logMsg(sprintf('%s (Event repeated %s or more times)', $message, $previous_events[$msg_id]), $priority, $file, $line);
+            }
+            return false;
+        } else {
+            $previous_events[$msg_id] = 1;
+        }
+
+        // For email and SMS notification types use "lock" files to prevent sending email and SMS notices ad infinitum.
+        if ((false !== $this->getParam('log_email_priority') && $priority <= $this->getParam('log_email_priority'))
+        || (false !== $this->getParam('log_sms_priority') && $priority <= $this->getParam('log_sms_priority'))) {
+            // This event will generate a "send" notification. Prepare lock file.
+            $site_hash = md5(empty($_SERVER['SERVER_NAME']) ? $_SERVER['SCRIPT_FILENAME'] : $_SERVER['SERVER_NAME']);
+            $lock_dir = $this->getParam('tmp_dir') . "/codebase_msgs_$site_hash/";
+            // Just use the file and line for the msg_id to limit the number of possible messages
+            // (the message string itself shan't be used as it may contain innumerable combinations).
+            $lock_file = $lock_dir . md5($file . ':' . $line);
+            if (!is_dir($lock_dir)) {
+                mkdir($lock_dir);
+            }
+            $send_notifications = true;
+            if (is_file($lock_file)) {
+                $msg_last_sent = filectime($lock_file);
+                // Has this message been sent more recently than the timeout?
+                if ((time() - $msg_last_sent) <= $this->getParam('log_multiple_timeout')) {
+                    // This message was already sent recently.
+                    $send_notifications = false;
+                } else {
+                    // Timeout has expired; send notifications again and reset timeout.
+                    touch($lock_file);
+                }
+            } else {
+                touch($lock_file);
+            }
+        }
+
+        // Make sure to log in the system's locale.
+        $locale = setlocale(LC_TIME, 0);
+        setlocale(LC_TIME, 'C');
+
+        // Data to be stored for a log event.
+        $event = array(
+            'date'      => date('Y-m-d H:i:s'),
+            'remote ip' => getRemoteAddr(),
+            'pid'       => getmypid(),
+            'type'      => $this->logPriorityToString($priority),
+            'file:line' => "$file : $line",
+            'url'       => $url,
+            'message'   => mb_substr($message, 0, $this->getParam('log_message_max_length')),
+        );
+        // Here's a shortened version of event data.
+        $event_short = $event;
+        $event_short['url'] = truncate($event_short['url'], 120);
+
+        // Restore original locale.
+        setlocale(LC_TIME, $locale);
+
+        // FILE ACTION
+        if (false !== $this->getParam('log_file_priority') && $priority <= $this->getParam('log_file_priority')) {
+            $event_str = '[' . join('] [', $event_short) . ']';
+            error_log("$event_str\n", 3, $this->getParam('log_directory') . '/' . $this->getParam('log_filename'));
+        }
+
+        // EMAIL ACTION
+        if (false !== $this->getParam('log_email_priority') && $priority <= $this->getParam('log_email_priority') && $send_notifications) {
+            $hostname = (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : php_uname('n');
+            $subject = sprintf('[%s %s] %s', $hostname, $event['type'], mb_substr($event['message'], 0, 64));
+            $email_msg = sprintf("A log event of type '%s' occurred on %s\n\n", $event['type'], $hostname);
+            $headers = 'From: ' . $this->getParam('site_email');
+            foreach ($event as $k=>$v) {
+                $email_msg .= sprintf("%-16s %s\n", $k, $v);
+            }
+            $email_msg .= sprintf("%-16s %s\n", 'codebase version', $this->getParam('codebase_version'));
+            $email_msg .= sprintf("%-16s %s\n", 'site version', $this->getParam('site_version'));
+            mb_send_mail($this->getParam('log_to_email_address'), $subject, $email_msg, $headers);
+        }
+
+        // SMS ACTION
+        if (false !== $this->getParam('log_sms_priority') && $priority <= $this->getParam('log_sms_priority') && $send_notifications) {
+            $hostname = (isset($_SERVER['HTTP_HOST']) && '' != $_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : php_uname('n');
+            $subject = sprintf('[%s %s]', $hostname, $priority);
+            $sms_msg = sprintf('%s [%s:%s]', mb_substr($event_short['message'], 0, 64), basename($file), $line);
+            $headers = 'From: ' . $this->getParam('site_email');
+            mb_send_mail($this->getParam('log_to_sms_address'), $subject, $sms_msg, $headers);
+        }
+
+        // SCREEN ACTION
+        if (false !== $this->getParam('log_screen_priority') && $priority <= $this->getParam('log_screen_priority')) {
+            file_put_contents('php://stderr', "[{$event['type']}] [{$event['message']}]\n", FILE_APPEND);
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the string representation of a LOG_* integer constant.
+     * Updated: also returns the LOG_* integer constant if passed a string log value ('info' returns 6).
+     *
+     * @param int  $priority  The LOG_* integer constant (to return the string name), or a string name of a log priority to return the integer LOG_* value..
+     *
+     * @return                The string representation of $priority (if integer given), or integer representation (if string given).
+     */
+    public function logPriorityToString($priority) {
+        $priorities = array(
+            LOG_EMERG   => 'emergency',
+            LOG_ALERT   => 'alert',
+            LOG_CRIT    => 'critical',
+            LOG_ERR     => 'error',
+            LOG_WARNING => 'warning',
+            LOG_NOTICE  => 'notice',
+            LOG_INFO    => 'info',
+            LOG_DEBUG   => 'debug'
+        );
+        if (isset($priorities[$priority])) {
+            return $priorities[$priority];
+        } else if (is_string($priority) && false !== ($key = array_search($priority, $priorities))) {
+            return $key;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Forcefully set a query argument even if one currently exists in the request.
+     * Values in the _carry_queries array will be copied to URLs (via $app->url()) and
+     * to hidden input values (via printHiddenSession()).
+     *
+     * @access  public
+     * @param   mixed   $query_key  The key (or keys, as an array) of the query argument to save.
+     * @param   mixed   $val        The new value of the argument key.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   13 Oct 2007 11:34:51
+     */
+    public function setQuery($query_key, $val)
+    {
+        if (!is_array($query_key)) {
+            $query_key = array($query_key);
+        }
+        foreach ($query_key as $k) {
+            // Set the value of the specified query argument into the _carry_queries array.
+            $this->_carry_queries[$k] = $val;
+        }
+    }
+
+    /**
+     * Specify which query arguments will be carried persistently between requests.
+     * Values in the _carry_queries array will be copied to URLs (via $app->url()) and
+     * to hidden input values (via printHiddenSession()).
+     *
+     * @access  public
+     * @param   mixed   $query_key   The key (or keys, as an array) of the query argument to save.
+     * @param   mixed   $default    If the key is not available, set to this default value.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   14 Nov 2005 19:24:52
+     */
+    public function carryQuery($query_key, $default=false)
+    {
+        if (!is_array($query_key)) {
+            $query_key = array($query_key);
+        }
+        foreach ($query_key as $k) {
+            // If not already set, and there is a non-empty value provided in the request...
+            if (isset($k) && '' != $k && !isset($this->_carry_queries[$k]) && false !== getFormData($k, $default)) {
+                // Copy the value of the specified query argument into the _carry_queries array.
+                $this->_carry_queries[$k] = getFormData($k, $default);
+                $this->logMsg(sprintf('Carrying query: %s => %s', $k, truncate(getDump($this->_carry_queries[$k], true), 128, 'end')), LOG_DEBUG, __FILE__, __LINE__);
+            }
+        }
+    }
+
+    /**
+     * dropQuery() is the opposite of carryQuery(). The specified value will not appear in
+     * url()/ohref()/printHiddenSession() modified URLs unless explicitly written in.
+     *
+     * @access  public
+     * @param   mixed   $query_key  The key (or keys, as an array) of the query argument to remove.
+     * @param   bool    $unset      Remove any values set in the request matching the given $query_key.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   18 Jun 2007 20:57:29
+     */
+    public function dropQuery($query_key, $unset=false)
+    {
+        if (!is_array($query_key)) {
+            $query_key = array($query_key);
+        }
+        foreach ($query_key as $k) {
+            if (array_key_exists($k, $this->_carry_queries)) {
+                // Remove the value of the specified query argument from the _carry_queries array.
+                $this->logMsg(sprintf('Dropping carried query: %s => %s', $k, $this->_carry_queries[$k]), LOG_DEBUG, __FILE__, __LINE__);
+                unset($this->_carry_queries[$k]);
+            }
+            if ($unset && (isset($_REQUEST) && array_key_exists($k, $_REQUEST))) {
+                unset($_REQUEST[$k], $_GET[$k], $_POST[$k], $_COOKIE[$k]);
+            }
+        }
+    }
+
+    /**
+     * Outputs a fully qualified URL with a query of all the used (ie: not empty)
+     * keys and values, including optional queries. This allows mindless retention
+     * of query arguments across page requests. If cookies are not
+     * used and session_use_trans_sid=true the session id will be propagated in the URL.
+     *
+     * @param  string $url              The initial url
+     * @param  mixed  $carry_args       Additional url arguments to carry in the query,
+     *                                  or FALSE to prevent carrying queries. Can be any of the following formats:
+     *                                      array('key1', key2', key3')  <-- to save these keys if in the form data.
+     *                                      array('key1'=>'value', key2'='value')  <-- to set keys to default values if not present in form data.
+     *                                      false  <-- To not carry any queries. If URL already has queries those will be retained.
+     *
+     * @param  mixed  $always_include_sid  Always add the session id, even if using_trans_sid = true. This is required when
+     *                                     URL starts with http, since PHP using_trans_sid doesn't do those and also for
+     *                                     header('Location...') redirections.
+     *
+     * @param   bool    $include_csrf_token     Set to true to include the csrf_token in the form. Only use this for forms with action="post" to prevent the token from being revealed in the URL.
+     * @return string url with attached queries and, if not using cookies, the session id
+     */
+    public function url($url, $carry_args=null, $always_include_sid=false, $include_csrf_token=false)
+    {
+        if (!$this->running) {
+            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        if ($this->getParam('csrf_token_enabled') && $include_csrf_token) {
+            // Include the csrf_token as a carried query argument.
+            // This token can be validated upon form submission with $app->verifyCSRFToken() or $app->requireValidCSRFToken()
+            $carry_args = is_array($carry_args) ? $carry_args : array();
+            $carry_args = array_merge($carry_args, array($this->getParam('csrf_token_name') => $this->getCSRFToken()));
+        }
+
+        // Get any provided query arguments to include in the final URL.
+        // If FALSE is a provided here, DO NOT carry the queries.
+        $do_carry_queries = true;
+        $one_time_carry_queries = array();
+        if (!is_null($carry_args)) {
+            if (is_array($carry_args)) {
+                if (!empty($carry_args)) {
+                    foreach ($carry_args as $key=>$arg) {
+                        // Get query from appropriate source.
+                        if (false === $arg) {
+                            $do_carry_queries = false;
+                        } else if (false !== getFormData($arg, false)) {
+                            $one_time_carry_queries[$arg] = getFormData($arg); // Set arg to form data if available.
+                        } else if (!is_numeric($key) && '' != $arg) {
+                            $one_time_carry_queries[$key] = getFormData($key, $arg); // Set to arg to default if specified (overwritten by form data).
+                        }
+                    }
+                }
+            } else if (false !== getFormData($carry_args, false)) {
+                $one_time_carry_queries[$carry_args] = getFormData($carry_args);
+            } else if (false === $carry_args) {
+                $do_carry_queries = false;
+            }
+        }
+
+        // Get the first delimiter that is needed in the url.
+        $delim = mb_strpos($url, '?') !== false ? ini_get('arg_separator.output') : '?';
+
+        $q = '';
+        if ($do_carry_queries) {
+            // Join the global _carry_queries and local one_time_carry_queries.
+            $query_args = urlEncodeArray(array_merge($this->_carry_queries, $one_time_carry_queries));
+            foreach ($query_args as $key=>$val) {
+                // Avoid indexed-array query params because in a URL array param keys should all match.
+                // I.e, we want to use `array[]=A&array[]=B` instead of `array[0]=A&array[1]=B`.
+                $key = preg_replace('/\[\d+\]$/', '[]', $key);
+                // Check value is set and value does not already exist in the url.
+                if (!preg_match('/[?&]' . preg_quote($key) . '=/', $url)) {
+                    $q .= $delim . $key . '=' . $val;
+                    $delim = ini_get('arg_separator.output');
+                }
+            }
+        }
+
+        // Pop off any named anchors to push them back on after appending additional query args.
+        $parts = explode('#', $url, 2);
+        $url = $parts[0];
+        $anchor = isset($parts[1]) ? $parts[1] : '';
+
+        // $anchor =
+
+        // Include the necessary SID if the following is true:
+        // - no cookie in http request OR cookies disabled in App
+        // - sessions are enabled
+        // - the link stays on our site
+        // - transparent SID propagation with session.use_trans_sid is not being used OR url begins with protocol (using_trans_sid has no effect here)
+        // OR
+        // - we must include the SID because we say so (it's used in a context where cookies will not be effective, ie. moving from http to https)
+        // AND
+        // - the SID is not already in the query.
+        if (
+            (
+                (
+                    (
+                        !isset($_COOKIE[session_name()])
+                        || !$this->getParam('session_use_cookies')
+                    )
+                    && $this->getParam('session_use_trans_sid')
+                    && $this->getParam('enable_session')
+                    && isMyDomain($url)
+                    && (
+                        !ini_get('session.use_trans_sid')
+                        || preg_match('!^(http|https)://!i', $url)
+                    )
+                )
+                || $always_include_sid
+            )
+            && !preg_match('/[?&]' . preg_quote(session_name()) . '=/', $url)
+        ) {
+            $url = sprintf('%s%s%s%s=%s%s', $url, $q, $delim, session_name(), session_id(), ('' == $anchor ? '' : "#$anchor"));
+        } else {
+            $url = sprintf('%s%s%s', $url, $q, ('' == $anchor ? '' : "#$anchor"));
+        }
+
+        return $url;
+    }
+
+    /**
+     * Returns a HTML-friendly URL processed with $app->url and & replaced with &amp;
+     *
+     * @access  public
+     * @param   (see param reference for url() method)
+     * @return  string          URL passed through $app->url() with ampersands transformed to $amp;
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   09 Dec 2005 17:58:45
+     */
+    public function oHREF($url, $carry_args=null, $always_include_sid=false, $include_csrf_token=false)
+    {
+        // Process the URL.
+        $url = $this->url($url, $carry_args, $always_include_sid, $include_csrf_token);
+
+        // Replace any & not followed by an html or unicode entity with its &amp; equivalent.
+        $url = preg_replace('/&(?![\w\d#]{1,10};)/', '&amp;', $url);
+
+        return $url;
+    }
+
+    /**
+     * Prints a hidden form element with the PHPSESSID when cookies are not used, as well
+     * as hidden form elements for GET_VARS that might be in use.
+     *
+     * @param  mixed  $carry_args        Additional url arguments to carry in the query,
+     *                                   or FALSE to prevent carrying queries. Can be any of the following formats:
+     *                                      array('key1', key2', key3')  <-- to save these keys if in the form data.
+     *                                      array('key1'=>'value', key2'='value')  <-- to set keys to default values if not present in form data.
+     *                                      false  <-- To not carry any queries. If URL already has queries those will be retained.
+     * @param   bool    $include_csrf_token     Set to true to include the csrf_token in the form. Only use this for forms with action="post" to prevent the token from being revealed in the URL.
+     */
+    public function printHiddenSession($carry_args=null, $include_csrf_token=false)
+    {
+        if (!$this->running) {
+            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Get any provided query arguments to include in the final hidden form data.
+        // If FALSE is a provided here, DO NOT carry the queries.
+        $do_carry_queries = true;
+        $one_time_carry_queries = array();
+        if (!is_null($carry_args)) {
+            if (is_array($carry_args)) {
+                if (!empty($carry_args)) {
+                    foreach ($carry_args as $key=>$arg) {
+                        // Get query from appropriate source.
+                        if (false === $arg) {
+                            $do_carry_queries = false;
+                        } else if (false !== getFormData($arg, false)) {
+                            $one_time_carry_queries[$arg] = getFormData($arg); // Set arg to form data if available.
+                        } else if (!is_numeric($key) && '' != $arg) {
+                            $one_time_carry_queries[$key] = getFormData($key, $arg); // Set to arg to default if specified (overwritten by form data).
+                        }
+                    }
+                }
+            } else if (false !== getFormData($carry_args, false)) {
+                $one_time_carry_queries[$carry_args] = getFormData($carry_args);
+            } else if (false === $carry_args) {
+                $do_carry_queries = false;
+            }
+        }
+
+        // For each existing request value, we create a hidden input to carry it through a form.
+        if ($do_carry_queries) {
+            // Join the global _carry_queries and local one_time_carry_queries.
+            // urlencode is not used here, not for form data!
+            $query_args = array_merge($this->_carry_queries, $one_time_carry_queries);
+            foreach ($query_args as $key => $val) {
+                if (is_array($val)) {
+                    foreach ($val as $subval) {
+                        if ('' != $key && '' != $subval) {
+                            printf('<input type="hidden" name="%s[]" value="%s" />', $key, $subval);
+                        }
+                    }
+                } else if ('' != $key && '' != $val) {
+                    printf('<input type="hidden" name="%s" value="%s" />', $key, $val);
+                }
+            }
+            unset($query_args, $key, $val, $subval);
+        }
+
+        // Include the SID if:
+        // * cookies are disabled
+        // * the system isn't automatically adding trans_sid
+        // * the session is enabled
+        // * and we're configured to use trans_sid
+        if (!isset($_COOKIE[session_name()])
+        && !ini_get('session.use_trans_sid')
+        && $this->getParam('enable_session')
+        && $this->getParam('session_use_trans_sid')
+        ) {
+            printf('<input type="hidden" name="%s" value="%s" />', session_name(), session_id());
+        }
+
+        // Include the csrf_token in the form.
+        // This token can be validated upon form submission with $app->verifyCSRFToken() or $app->requireValidCSRFToken()
+        if ($this->getParam('csrf_token_enabled') && $include_csrf_token) {
+            printf('<input type="hidden" name="%s" value="%s" />', $this->getParam('csrf_token_name'), $this->getCSRFToken());
+        }
+    }
+
+    /*
+    * Return a URL with a version number attached. This is useful for overriding network caches ("cache buster") for sourced media, e.g., /style.css?812763482
+    *
+    * @access   public
+    * @param    string  $url    URL to media (e.g., /foo.js)
+    * @return   string          URL with cache-busting version appended (/foo.js?v=1234567890)
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    03 Sep 2014 22:40:24
+    */
+    public function cacheBustURL($url)
+    {
+        // Get the first delimiter that is needed in the url.
+        $delim = mb_strpos($url, '?') !== false ? ini_get('arg_separator.output') : '?';
+        $v = crc32($this->getParam('codebase_version') . '|' . $this->getParam('site_version'));
+        return sprintf('%s%sv=%s', $url, $delim, $v);
+    }
+
+    /*
+    * Generate a csrf_token if it doesn't exist or is expired, save it to the session and return its value.
+    * Otherwise just return the current token.
+    * Details on the synchronizer token pattern:
+    * https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#General_Recommendation:_Synchronizer_Token_Pattern
+    *
+    * @access   public
+    * @return   string The new or current csrf_token
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Nov 2014 17:57:17
+    */
+    public function getCSRFToken()
+    {
+        if (!isset($_SESSION['_app'][$this->_ns]['csrf_token']) || (removeSignature($_SESSION['_app'][$this->_ns]['csrf_token']) + $this->getParam('csrf_token_timeout') < time())) {
+            // No token, or token is expired; generate one and return it.
+            return $_SESSION['_app'][$this->_ns]['csrf_token'] = addSignature(time(), null, 64);
+        }
+        // Current token is not expired; return it.
+        return $_SESSION['_app'][$this->_ns]['csrf_token'];
+    }
+
+    /*
+    * Compares the given csrf_token with the current or previous one saved in the session.
+    *
+    * @access   public
+    * @param    string  $user_submitted_csrf_token The user-submitted token to compare with the session token.
+    * @param    string  $csrf_token     The token to compare with the session token.
+    * @return   bool    True if the tokens match, false otherwise.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Nov 2014 18:06:55
+    */
+    public function verifyCSRFToken($user_submitted_csrf_token)
+    {
+
+        if (!$this->getParam('csrf_token_enabled')) {
+            $this->logMsg(sprintf('%s called, but csrf_token_enabled=false', __METHOD__), LOG_ERR, __FILE__, __LINE__);
+            return true;
+        }
+        if ('' == trim($user_submitted_csrf_token)) {
+            $this->logMsg(sprintf('Empty string failed CSRF verification.', null), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+        if (!verifySignature($user_submitted_csrf_token, null, 64)) {
+            $this->logMsg(sprintf('Input failed CSRF verification (invalid signature in %s).', $user_submitted_csrf_token), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+        $csrf_token = $this->getCSRFToken();
+        if ($user_submitted_csrf_token != $csrf_token) {
+            $this->logMsg(sprintf('Input failed CSRF verification (%s not in %s).', $user_submitted_csrf_token, $csrf_token), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+        $this->logMsg(sprintf('Verified CSRF token %s', $user_submitted_csrf_token), LOG_DEBUG, __FILE__, __LINE__);
+        return true;
+    }
+
+    /*
+    * Bounce user if they submit a token that doesn't match the one saved in the session.
+    * Because this function calls dieURL() it must be called before any other HTTP header output.
+    *
+    * @access   public
+    * @param    string  $message    Optional message to display to the user (otherwise default message will display). Set to an empty string to display no message.
+    * @param    int    $type    The type of message: MSG_NOTICE,
+    *                           MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
+    * @param    string $file    __FILE__.
+    * @param    string $line    __LINE__.
+    * @return   void
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Nov 2014 18:10:17
+    */
+    public function requireValidCSRFToken($message=null, $type=MSG_NOTICE, $file=null, $line=null)
+    {
+        if (!$this->verifyCSRFToken(getFormData($this->getParam('csrf_token_name')))) {
+            $message = isset($message) ? $message : _("Sorry, the form token expired. Please try again.");
+            $this->raiseMsg($message, $type, $file, $line);
+            $this->dieBoomerangURL();
+        }
+    }
+
+    /**
+     * Uses an http header to redirect the client to the given $url. If sessions are not used
+     * and the session is not already defined in the given $url, the SID is appended as a URI query.
+     * As with all header generating functions, make sure this is called before any other output.
+     *
+     * @param   string  $url                    The URL the client will be redirected to.
+     * @param   mixed   $carry_args             Additional url arguments to carry in the query,
+     *                                          or FALSE to prevent carrying queries. Can be any of the following formats:
+     *                                          -array('key1', key2', key3')  <-- to save these keys if in the form data.
+     *                                          -array('key1' => 'value', key2' => 'value')  <-- to set keys to default values if not present in form data.
+     *                                          -false  <-- To not carry any queries. If URL already has queries those will be retained.
+     * @param   bool    $always_include_sid     Force session id to be added to Location header.
+     */
+    public function dieURL($url, $carry_args=null, $always_include_sid=false)
+    {
+        if (!$this->running) {
+            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (!$url) {
+            // If URL is not specified, use the redirect_home_url.
+            $url = $this->getParam('redirect_home_url');
+        }
+
+        if (preg_match('!^/!', $url)) {
+            // If relative URL is given, prepend correct local hostname.
+            $scheme = getenv('HTTPS') ? 'https' : 'http';
+            $host = getenv('HTTP_HOST');
+            $url = sprintf('%s://%s%s', $scheme, $host, $url);
+        }
+
+        $url = $this->url($url, $carry_args, $always_include_sid);
+
+        // Should we send a "303 See Other" header here instead of relying on the 302 sent automatically by PHP?
+        if (!headers_sent($h_file, $h_line)) {
+            header(sprintf('Location: %s', $url));
+            $this->logMsg(sprintf('dieURL: %s', $url), LOG_DEBUG, __FILE__, __LINE__);
+        } else {
+            // Fallback: die using meta refresh instead.
+            printf('<meta http-equiv="refresh" content="0;url=%s" />', $url);
+            $this->logMsg(sprintf('dieURL (refresh): %s; headers already sent (output started in %s : %s)', $url, $h_file, $h_line), LOG_NOTICE, __FILE__, __LINE__);
+        }
+
+        // End application.
+        // Recommended, although I'm not sure it's necessary: http://cn2.php.net/session_write_close
+        $this->stop();
+        die;
+    }
+
+    /*
+    * Redirects a user by calling $app->dieURL(). It will use:
+    * 1. the stored boomerang URL, it it exists
+    * 2. a specified $default_url, it it exists
+    * 3. the referring URL, it it exists.
+    * 4. redirect_home_url configuration variable.
+    *
+    * @access   public
+    * @param    string  $id             Identifier for this script.
+    * @param    mixed   $carry_args     Additional arguments to carry in the URL automatically (see $app->url()).
+    * @param    string  $default_url    A default URL if there is not a valid specified boomerang URL.
+    * @param    bool    $queryless_referrer_comparison   Exclude the URL query from the refererIsMe() comparison.
+    * @return   bool                    False if the session is not running. No return otherwise.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    31 Mar 2006 19:17:00
+    */
+    public function dieBoomerangURL($id=null, $carry_args=null, $default_url=null, $queryless_referrer_comparison=false)
+    {
+        if (!$this->running) {
+            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Get URL from stored boomerang. Allow non specific URL if ID not valid.
+        if ($this->validBoomerangURL($id, true)) {
+            if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang'][$id])) {
+                $url = $_SESSION['_app'][$this->_ns]['boomerang'][$id]['url'];
+                $this->logMsg(sprintf('dieBoomerangURL(%s) found: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+            } else {
+                $url = end($_SESSION['_app'][$this->_ns]['boomerang'])['url'];
+                $this->logMsg(sprintf('dieBoomerangURL(%s) using: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+            }
+            // Delete stored boomerang.
+            $this->deleteBoomerangURL($id);
+        } else if (isset($default_url)) {
+            $url = $default_url;
+        } else if (!refererIsMe(true === $queryless_referrer_comparison) && '' != ($url = getenv('HTTP_REFERER'))) {
+            // Ensure that the redirecting page is not also the referrer.
+            $this->logMsg(sprintf('dieBoomerangURL(%s) using referrer: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+        } else {
+            // If URL is not specified, use the redirect_home_url.
+            $url = $this->getParam('redirect_home_url');
+            $this->logMsg(sprintf('dieBoomerangURL(%s) using redirect_home_url: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+        }
+
+        // A redirection will never happen immediately twice. Set the time so we can ensure this doesn't happen.
+        $_SESSION['_app'][$this->_ns]['boomerang_last_redirect_time'] = time();
+
+        // Do it.
+        $this->dieURL($url, $carry_args);
+    }
+
+    /**
+     * Set the URL to return to when $app->dieBoomerangURL() is called.
+     *
+     * @param string  $url  A fully validated URL.
+     * @param bool  $id     An identification tag for this url.
+     * FIXME: url garbage collection?
+     */
+    public function setBoomerangURL($url=null, $id=null)
+    {
+        if (!$this->running) {
+            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        if ('' != $url && is_string($url)) {
+            // Delete any boomerang request keys in the query string (along with any trailing delimiters after the deletion).
+            $url = preg_replace(array('/([&?])boomerang=[^&?]+[&?]?/', '/[&?]$/'), array('$1', ''), $url);
+
+            if (isset($_SESSION['_app'][$this->_ns]['boomerang']) && is_array($_SESSION['_app'][$this->_ns]['boomerang']) && !empty($_SESSION['_app'][$this->_ns]['boomerang'])) {
+                // If the ID already exists in the boomerang array, delete it.
+                foreach (array_keys($_SESSION['_app'][$this->_ns]['boomerang']) as $existing_id) {
+                    // $existing_id could be null if existing boomerang URL was set without an ID.
+                    if ($existing_id === $id) {
+                        $this->logMsg(sprintf('Deleted existing boomerang URL matching ID: %s=>%s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+                        unset($_SESSION['_app'][$this->_ns]['boomerang'][$existing_id]);
+                    }
+                }
+            }
+
+            // A redirection will never happen immediately after setting the boomerang URL.
+            // Set the time so ensure this doesn't happen. See $app->validBoomerangURL for more.
+            if (isset($id)) {
+                $_SESSION['_app'][$this->_ns]['boomerang'][$id] = array(
+                    'url' => $url,
+                    'added_time' => time(),
+                );
+            } else {
+                $_SESSION['_app'][$this->_ns]['boomerang'][] = array(
+                    'url' => $url,
+                    'added_time' => time(),
+                );
+            }
+
+            $this->logMsg(sprintf('setBoomerangURL(%s): %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+            return true;
+        } else {
+            $this->logMsg(sprintf('setBoomerangURL(%s) is empty!', $id, $url), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Return the URL set for the specified $id, or an empty string if one isn't set.
+     *
+     * @param string  $id     An identification tag for this url.
+     */
+    public function getBoomerangURL($id=null)
+    {
+        if (!$this->running) {
+            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (isset($id)) {
+            if (isset($_SESSION['_app'][$this->_ns]['boomerang'][$id])) {
+                return $_SESSION['_app'][$this->_ns]['boomerang'][$id]['url'];
+            } else {
+                return '';
+            }
+        } else if (isset($_SESSION['_app'][$this->_ns]['boomerang']) && is_array($_SESSION['_app'][$this->_ns]['boomerang']) && !empty($_SESSION['_app'][$this->_ns]['boomerang'])) {
+            return end($_SESSION['_app'][$this->_ns]['boomerang'])['url'];
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Delete the URL set for the specified $id.
+     *
+     * @param string  $id     An identification tag for this url.
+     */
+    public function deleteBoomerangURL($id=null)
+    {
+        if (!$this->running) {
+            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang'][$id])) {
+            $url = $this->getBoomerangURL($id);
+            unset($_SESSION['_app'][$this->_ns]['boomerang'][$id]);
+        } else if (is_array($_SESSION['_app'][$this->_ns]['boomerang'])) {
+            $url = array_pop($_SESSION['_app'][$this->_ns]['boomerang'])['url'];
+        }
+        $this->logMsg(sprintf('deleteBoomerangURL(%s): %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+    }
+
+    /**
+     * Check if a valid boomerang URL value has been set. A boomerang URL is considered
+     * valid if: 1) it is not empty, 2) it is not the current URL, and 3) has not been accessed within n seconds.
+     *
+     * @return bool  True if it is set and valid, false otherwise.
+     */
+    public function validBoomerangURL($id=null, $use_nonspecificboomerang=false)
+    {
+        if (!$this->running) {
+            $this->logMsg(sprintf('Canceled %s, application not running.', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (!isset($_SESSION['_app'][$this->_ns]['boomerang']) || !is_array($_SESSION['_app'][$this->_ns]['boomerang']) || empty($_SESSION['_app'][$this->_ns]['boomerang'])) {
+            $this->logMsg(sprintf('validBoomerangURL(%s) no boomerang URL set, not an array, or empty.', $id), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        $url = '';
+        if (isset($id) && isset($_SESSION['_app'][$this->_ns]['boomerang'][$id])) {
+            $url = $_SESSION['_app'][$this->_ns]['boomerang'][$id]['url'];
+            $added_time = $_SESSION['_app'][$this->_ns]['boomerang'][$id]['added_time'];
+        } else if (!isset($id) || $use_nonspecificboomerang) {
+            // Use most recent, non-specific boomerang if available.
+            $url = end($_SESSION['_app'][$this->_ns]['boomerang'])['url'];
+            $added_time = end($_SESSION['_app'][$this->_ns]['boomerang'])['added_time'];
+        }
+
+        if ('' == trim($url)) {
+            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, empty!', $id), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        if ($url == absoluteMe()) {
+            // The URL we are directing to is the current page.
+            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, same as absoluteMe: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Last redirect time is the time stamp of the last boomerangURL redirection, if any. A boomerang redirection should always occur at least several seconds after the last boomerang redirect (time it takes to load a page and receive user interaction).
+        $boomerang_last_redirect_time = isset($_SESSION['_app'][$this->_ns]['boomerang_last_redirect_time']) ? $_SESSION['_app'][$this->_ns]['boomerang_last_redirect_time'] : null;
+        if (isset($boomerang_last_redirect_time) && $boomerang_last_redirect_time >= (time() - 2)) {
+            // Last boomerang direction was less than 2 seconds ago.
+            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, boomerang_last_redirect_time too short: %s seconds', $id, time() - $boomerang_last_redirect_time), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (isset($added_time) && $added_time < (time() - 72000)) {
+            // Last boomerang direction was more than 20 hours ago.
+            $this->logMsg(sprintf('validBoomerangURL(%s) not valid, added_time too old: %s seconds', $id, time() - $added_time), LOG_DEBUG, __FILE__, __LINE__);
+            // Delete this defunct boomerang.
+            $this->deleteBoomerangURL($id);
+            return false;
+        }
+
+        $this->logMsg(sprintf('validBoomerangURL(%s) is valid: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+        return true;
+    }
+
+    /**
+     * This function has changed to do nothing. SSL redirection should happen at the server layer, doing so here may result in a redirect loop.
+     */
+    public function sslOn()
+    {
+        $app =& App::getInstance();
+        $app->logMsg(sprintf('sslOn was called and ignored.', null), LOG_DEBUG, __FILE__, __LINE__);
+    }
+
+    /**
+     * This function has changed to do nothing. There is no reason to prefer a non-SSL connection, and doing so may result in a redirect loop.
+     */
+    public function sslOff()
+    {
+        $app =& App::getInstance();
+        $app->logMsg(sprintf('sslOff was called and ignored.', null), LOG_DEBUG, __FILE__, __LINE__);
+    }
+
+    /*
+    * Sets a cookie, with error checking and some sane defaults.
+    *
+    * @access   public
+    * @param    string  $name       The name of the cookie.
+    * @param    string  $value      The value of the cookie.
+    * @param    string  $expire     The time the cookie expires, as a unix timestamp or string value passed to strtotime.
+    * @param    string  $path       The path on the server in which the cookie will be available on.
+    * @param    string  $domain     The domain that the cookie is available to.
+    * @param    bool    $secure     Indicates that the cookie should only be transmitted over a secure HTTPS connection from the client.
+    * @param    bool    $httponly   When TRUE the cookie will be made accessible only through the HTTP protocol (makes cookies unreadable to javascript).
+    * @return   bool                True on success, false on error.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    02 May 2014 16:36:34
+    */
+    public function setCookie($name, $value, $expire='+10 years', $path='/', $domain=null, $secure=null, $httponly=null)
+    {
+        if (!is_scalar($name)) {
+            $this->logMsg(sprintf('Cookie name must be scalar, is not: %s', getDump($name)), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+        if (!is_scalar($value)) {
+            $this->logMsg(sprintf('Cookie "%s" value must be scalar, is not: %s', $name, getDump($value)), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Defaults.
+        $expire = (is_numeric($expire) ? $expire : (is_string($expire) ? strtotime($expire) : $expire));
+        $secure = $secure ?: (getenv('HTTPS') && $this->getParam('ssl_enabled'));
+        $httponly = $httponly ?: true;
+
+        // Make sure the expiration date is a valid 32bit integer.
+        if (is_int($expire) && $expire > 2147483647) {
+            $this->logMsg(sprintf('Cookie "%s" expire time exceeds a 32bit integer (%s)', $key, date('r', $expire)), LOG_NOTICE, __FILE__, __LINE__);
+        }
+
+        // Measure total cookie length and warn if larger than max recommended size of 4093.
+        // https://stackoverflow.com/questions/640938/what-is-the-maximum-size-of-a-web-browsers-cookies-key
+        // The date the header name include 51 bytes: Set-Cookie: ; expires=Fri, 03-May-2024 00:04:47 GMT
+        $cookielen = strlen($name . $value . $path . $domain . ($secure ? '; secure' : '') . ($httponly ? '; httponly' : '')) + 51;
+        if ($cookielen > 4093) {
+            $this->logMsg(sprintf('Cookie "%s" has a size greater than 4093 bytes (is %s bytes)', $key, $cookielen), LOG_NOTICE, __FILE__, __LINE__);
+        }
+
+        // Ensure PHP version allow use of httponly.
+        if (version_compare(PHP_VERSION, '5.2.0', '>=')) {
+            $ret = setcookie($name, $value, $expire, $path, $domain, $secure, $httponly);
+        } else {
+            $ret = setcookie($name, $value, $expire, $path, $domain, $secure);
+        }
+
+        if (false === $ret) {
+            $this->logMsg(sprintf('Failed to set cookie (%s=%s) probably due to output before headers.', $name, $value), LOG_NOTICE, __FILE__, __LINE__);
+        }
+        return $ret;
+    }
+} // End.
Index: tags/2.2.3/lib/Auth_File.inc.php
===================================================================
--- tags/2.2.3/lib/Auth_File.inc.php	(revision 632)
+++ tags/2.2.3/lib/Auth_File.inc.php	(revision 632)
@@ -0,0 +1,413 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Auth_File.inc.php
+ *
+ * The Auth_File class provides a htpasswd file implementation for
+ * authentication.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.2
+ */
+
+// Usage example:
+// $auth = new Auth_File();
+// $auth->setParam(array(
+//     'htpasswd_file' => COMMON_BASE . '/global/site_users.htpasswd',
+//     'login_timeout' => 21600,
+//     'idle_timeout' => 3600,
+//     'login_url' => '/login.php'
+// ));
+
+class Auth_File
+{
+
+    // Available encryption types for class Auth_File.
+    const ENCRYPT_MD5 = 'md5';
+    const ENCRYPT_CRYPT = 'crypt';
+    const ENCRYPT_SHA1 = 'sha1';
+    const ENCRYPT_PLAINTEXT = 'plaintext';
+
+    // Namespace of this auth object.
+    protected $_ns;
+
+    // Parameters to be specified by setParam().
+    protected $_params = array();
+    protected $_default_params = array(
+
+        // Full path to htpasswd file.
+        'htpasswd_file' => null,
+
+        // The type of encryption to use for passwords stored in the db_table. Use one of the self::ENCRYPT_* types specified above.
+        'encryption_type' => self::ENCRYPT_CRYPT,
+
+        // The URL to the login script.
+        'login_url' => '/',
+
+        // The maximum amount of time a user is allowed to be logged in. They will be forced to login again if they expire.
+        // This applies to admins and users. In seconds. 21600 seconds = 6 hours.
+        'login_timeout' => 21600,
+
+        // The maximum amount of time a user is allowed to be idle before their session expires. They will be forced to login again if they expire.
+        // This applies to admins and users. In seconds. 3600 seconds = 1 hour.
+        'idle_timeout' => 3600,
+
+        // An array of IP blocks that are bypass the remote_ip comparison check. Useful for dynamic IPs or those behind proxy servers.
+        'trusted_networks' => array(),
+    );
+
+    // Associative array of usernames to hashed passwords.
+    protected $_users = array();
+
+    /*
+    * Constructs a new htpasswd authentication object.
+    *
+    * @access public
+    *
+    * @param optional array $params  A hash containing parameters.
+    */
+    public function __construct($namespace='')
+    {
+        $this->_ns = $namespace;
+
+        // Initialize default parameters.
+        $this->setParam($this->_default_params);
+    }
+
+    /*
+    * Set the params of an auth object.
+    *
+    * @param  array $params   Array of parameter keys and value to set.
+    * @return bool true on success, false on failure
+    */
+    public function setParam($params)
+    {
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        }
+    }
+
+    /*
+    * Return the value of a parameter, if it exists.
+    *
+    * @access public
+    * @param string $param        Which parameter to return.
+    * @return mixed               Configured parameter value.
+    */
+    public function getParam($param)
+    {
+        $app = &App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /*
+    * Clear any authentication tokens in the current session. A.K.A. logout.
+    *
+    * @access public
+    */
+    public function clear()
+    {
+        $app =& App::getInstance();
+
+        $_SESSION['_auth_file'][$this->_ns] = array('authenticated' => false);
+
+        $app->logMsg(sprintf('Cleared %s auth', $this->_ns), LOG_DEBUG, __FILE__, __LINE__);
+    }
+
+    /*
+    * Sets a variable into a registered auth session.
+    *
+    * @access public
+    * @param mixed $key      Which value to set.
+    * @param mixed $val      Value to set variable to.
+    */
+    public function set($key, $val)
+    {
+        if (!isset($_SESSION['_auth_file'][$this->_ns]['user_data'])) {
+            $_SESSION['_auth_file'][$this->_ns]['user_data'] = array();
+        }
+        $_SESSION['_auth_file'][$this->_ns]['user_data'][$key] = $val;
+    }
+
+    /*
+    * Returns a specified value from a registered auth session.
+    *
+    * @access public
+    * @param mixed $key      Which value to return.
+    * @param mixed $default  Value to return if key not found in user_data.
+    * @return mixed          Value stored in session.
+    */
+    public function get($key, $default='')
+    {
+        if (isset($_SESSION['_auth_file'][$this->_ns][$key])) {
+            return $_SESSION['_auth_file'][$this->_ns][$key];
+        } else if (isset($_SESSION['_auth_file'][$this->_ns]['user_data'][$key])) {
+            return $_SESSION['_auth_file'][$this->_ns]['user_data'][$key];
+        } else {
+            return $default;
+        }
+    }
+
+    /*
+    * Find out if a set of login credentials are valid. Only supports
+    * htpasswd files with DES passwords right now.
+    *
+    * @access public
+    *
+    * @param string $username      The username to check.
+    * @param array $password      The password to compare to username.
+    *
+    * @return boolean  Whether or not the credentials are valid.
+    */
+    public function authenticate($username, $password)
+    {
+        $app = &App::getInstance();
+
+        if ('' == trim($password)) {
+            $app->logMsg(_("No password provided for authentication."), LOG_INFO, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Load users file.
+        $this->_loadHTPasswdFile();
+
+        if (!isset($this->_users[$username])) {
+            $app->logMsg(_("User ID provided does not exist."), LOG_INFO, __FILE__, __LINE__);
+            return false;
+        }
+
+        if ($this->_encrypt($password, $this->_users[$username]) != $this->_users[$username]) {
+            $app->logMsg(sprintf('Authentication failed for user %s', $username), LOG_INFO, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Authentication successful!
+        return true;
+    }
+
+    /*
+    * If user passes authentication create authenticated session.
+    *
+    * @access public
+    *
+    * @param string $username     The username to check.
+    * @param array $password     The password to compare to username.
+    *
+    * @return boolean  Whether or not the credentials are valid.
+    */
+    public function login($username, $password)
+    {
+        $username = mb_strtolower(trim($username));
+
+        $this->clear();
+
+        if (!$this->authenticate($username, $password)) {
+            // No login: failed authentication!
+            return false;
+        }
+
+        $_SESSION['_auth_file'][$this->_ns] = array(
+            'authenticated' => true,
+            'username' => $username,
+            'login_datetime' => date('Y-m-d H:i:s'),
+            'last_access_datetime' => date('Y-m-d H:i:s'),
+            'remote_ip' => getRemoteAddr()
+        );
+
+        // We're logged-in!
+        return true;
+    }
+
+    /*
+    * Test if user has a currently logged-in session.
+    *  - authentication flag set to true
+    *  - username not empty
+    *  - total logged-in time is not greater than login_timeout
+    *  - idle time is not greater than idle_timeout
+    *  - remote address is the same as the login remote address.
+    *
+    * @access public
+    */
+    public function isLoggedIn()
+    {
+        $app = &App::getInstance();
+
+        // Some users will access from networks with a changing IP number (i.e. behind a proxy server). These users must be allowed entry by adding their IP to the list of trusted_networks.
+        if ($trusted_net = ipInRange(getRemoteAddr(), $this->_params['trusted_networks'])) {
+            $user_in_trusted_network = true;
+            $app->logMsg(sprintf('User %s accessing from trusted network %s', $_SESSION['_auth_file'][$this->_ns]['username'], $trusted_net), LOG_DEBUG, __FILE__, __LINE__);
+        } else {
+            $user_in_trusted_network = false;
+        }
+
+        // Test login with information stored in session. Skip IP matching for users from trusted networks.
+        if (isset($_SESSION['_auth_file'][$this->_ns])
+        && true === $_SESSION['_auth_file'][$this->_ns]['authenticated']
+        && !empty($_SESSION['_auth_file'][$this->_ns]['username'])
+        && strtotime($_SESSION['_auth_file'][$this->_ns]['login_datetime']) > time() - $this->_params['login_timeout']
+        && strtotime($_SESSION['_auth_file'][$this->_ns]['last_access_datetime']) > time() - $this->_params['idle_timeout']
+        && ($_SESSION['_auth_file'][$this->_ns]['remote_ip'] == getRemoteAddr() || $user_in_trusted_network)
+        ) {
+            // User is authenticated!
+            $_SESSION['_auth_file'][$this->_ns]['last_access_datetime'] = date('Y-m-d H:i:s');
+            return true;
+        } else if (isset($_SESSION['_auth_file'][$this->_ns]) && true === $_SESSION['_auth_file'][$this->_ns]['authenticated']) {
+            if (strtotime($_SESSION['_auth_file'][$this->_ns]['last_access_datetime']) > time() - 43200) {
+                // Only raise message if last session is less than 12 hours old.
+                $app->raiseMsg(_("Your session has closed. You need to log-in again."), MSG_NOTICE, __FILE__, __LINE__);
+            }
+
+            // Log the reason for login expiration.
+            $expire_reasons = array();
+            if (empty($_SESSION['_auth_file'][$this->_ns]['username'])) {
+                $expire_reasons[] = 'username not found';
+            }
+            if (strtotime($_SESSION['_auth_file'][$this->_ns]['login_datetime']) <= time() - $this->_params['login_timeout']) {
+                $expire_reasons[] = 'login_timeout expired';
+            }
+            if (strtotime($_SESSION['_auth_file'][$this->_ns]['last_access_datetime']) <= time() - $this->_params['idle_timeout']) {
+                $expire_reasons[] = 'idle_timeout expired';
+            }
+            if ($_SESSION['_auth_file'][$this->_ns]['remote_ip'] != getRemoteAddr() && !$user_in_trusted_network) {
+                $expire_reasons[] = sprintf('remote_ip not matched (%s != %s)', $_SESSION['_auth_file'][$this->_ns]['remote_ip'], getRemoteAddr());
+            }
+            $app->logMsg(sprintf('User %s session expired: %s', $_SESSION['_auth_file'][$this->_ns]['username'], join(', ', $expire_reasons)), LOG_INFO, __FILE__, __LINE__);
+        }
+
+        return false;
+    }
+
+    /*
+    * Redirect user to login page if they are not logged in.
+    *
+    * @param string $message The text description of a message to raise.
+    * @param int    $type    The type of message: MSG_NOTICE,
+    *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
+    * @param string $file    __FILE__.
+    * @param string $line    __LINE__.
+    * @access public
+    */
+    public function requireLogin($message='', $type=MSG_NOTICE, $file=null, $line=null)
+    {
+        $app = &App::getInstance();
+
+        if (!$this->isLoggedIn()) {
+            // Display message for requiring login. (RaiseMsg will ignore empty strings.)
+            $app->raiseMsg($message, $type, $file, $line);
+
+            // Login scripts must have the same 'login' tag for boomerangURL verification/manipulation.
+            $app->setBoomerangURL(absoluteMe(), 'login');
+            $app->dieURL($this->_params['login_url']);
+        }
+    }
+
+    /*
+    * Wrapper function for compatibility with lib/Lock.inc.php.
+    *
+    * @param  string  $username    Username to return.
+    * @return string               Username, or false if none found.
+    */
+    public function getUsername($username) {
+        if ('' != $username) {
+            return $username;
+        } else {
+            return false;
+        }
+    }
+
+    /*
+    * Reads the configured htpasswd file into the _users array.
+    *
+    * @access   public
+    * @return   false on error, true on success.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    18 Apr 2006 18:17:48
+    */
+    protected function _loadHTPasswdFile()
+    {
+        $app = &App::getInstance();
+
+        static $users = null;
+
+        if (!file_exists($this->_params['htpasswd_file'])) {
+            $app->logMsg(sprintf('htpasswd file missing or not specified: %s', $this->_params['htpasswd_file']), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (!isset($users)) {
+            if (false === ($users = file($this->_params['htpasswd_file']))) {
+                $app->logMsg(sprintf('Could not read htpasswd file: %s', $this->_params['htpasswd_file']), LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        if (is_array($users)) {
+            foreach ($users as $u) {
+                list($user, $pass) = explode(':', $u, 2);
+                $this->_users[trim($user)] = trim($pass);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /*
+    * Hash a given password according to the configured encryption
+    * type.
+    *
+    * @param string $password              The password to encrypt.
+    * @param string $encrypted_password    The currently encrypted password to use as salt, if needed.
+    *
+    * @return string  The hashed password.
+    */
+    protected function _encrypt($password, $encrypted_password=null)
+    {
+        switch ($this->_params['encryption_type']) {
+        case self::ENCRYPT_PLAINTEXT :
+            return $password;
+            break;
+
+        case self::ENCRYPT_SHA1 :
+            return sha1($password);
+            break;
+
+        case self::ENCRYPT_MD5 :
+            return md5($password);
+            break;
+
+        case self::ENCRYPT_CRYPT :
+        default :
+            return crypt($password, $encrypted_password);
+            break;
+        }
+    }
+
+} // end class
Index: tags/2.2.3/lib/Auth_SQL.inc.php
===================================================================
--- tags/2.2.3/lib/Auth_SQL.inc.php	(revision 632)
+++ tags/2.2.3/lib/Auth_SQL.inc.php	(revision 632)
@@ -0,0 +1,1112 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+* The Auth_SQL class provides a SQL implementation for authentication.
+*
+* @author  Quinn Comendant <quinn@strangecode.com>
+* @version 2.1
+*/
+
+require_once dirname(__FILE__) . '/Email.inc.php';
+
+class Auth_SQL
+{
+    // Available hash types for class Auth_SQL.
+    const ENCRYPT_PLAINTEXT = 1;
+    const ENCRYPT_CRYPT = 2;
+    const ENCRYPT_SHA1 = 3;
+    const ENCRYPT_SHA1_HARDENED = 4;
+    const ENCRYPT_MD5 = 5;
+    const ENCRYPT_MD5_HARDENED = 6;
+    const ENCRYPT_PASSWORD_BCRYPT = 7;
+    const ENCRYPT_PASSWORD_DEFAULT = 8;
+
+    // Namespace of this auth object.
+    protected $_ns;
+
+    // Static var for test.
+    protected $_authentication_tested;
+
+    // Parameters to be configured by setParam.
+    protected $_params = array();
+    protected $_default_params = array(
+
+        // Automatically create table and verify columns. Better set to false after site launch.
+        // This value is overwritten by the $app->getParam('db_create_tables') setting if it is available.
+        'create_table' => true,
+
+        // The database table containing users to authenticate.
+        'db_table' => 'user_tbl',
+
+        // The name of the primary key for the db_table.
+        'db_primary_key' => 'user_id',
+
+        // The name of the username key for the db_table.
+        'db_username_column' => 'username',
+
+        // If using the db_login_table feature, specify the db_login_table. The primary key must match the primary key for the db_table.
+        'db_login_table' => 'user_login_tbl',
+
+        // The type of hash to use for passwords stored in the db_table. Use one of the Auth_SQL::ENCRYPT_* types specified above.
+        // Hardened password hashes rely on the same key/salt being used to compare hashes.
+        // Be aware that when using one of the hardened types the App signing_key or $more_salt below cannot change!
+        'hash_type' => self::ENCRYPT_MD5,
+        'encryption_type' => null, // Backwards misnomer compatibility.
+
+        // Automatically update stored user hashes when the user next authenticates if the hash type changes (requires user_tbl with populated userpass_hashtype column).
+        'hash_type_autoupdate' => true,
+
+        // The URL to the login script.
+        'login_url' => '/',
+
+        // The maximum amount of time a user is allowed to be logged in. They will be forced to login again if they expire.
+        // In seconds. 21600 seconds = 6 hours.
+        'login_timeout' => 21600,
+
+        // The maximum amount of time a user is allowed to be idle before their session expires. They will be forced to login again if they expire.
+        // In seconds. 3600 seconds = 1 hour.
+        'idle_timeout' => 3600,
+
+        // The period of time to compare login abuse attempts. If a threshold of logins is reached in this amount of time the account is blocked.
+        // Days and hours, like this: 'DD:HH'
+        'login_abuse_timeframe' => '04:00',
+
+        // The number of warnings a user will receive (and their password reset each time) before their account is completely blocked.
+        'login_abuse_warnings' => 3,
+
+        // The maximum number of IP addresses a user can login with over the timeout period before their account is blocked.
+        'login_abuse_max_ips' => 5,
+
+        // The IP address subnet size threshold. Uses a CIDR notation network mask (see CIDR cheat-sheet at bottom).
+        // Any integer between 0 and 32 is permitted. Setting this to '24' permits any address in a
+        // class C network (255.255.255.0) to be considered the same. Setting to '32' compares each IP absolutely.
+        // Setting to '0' ignores all IPs, thus disabling login_abuse checking.
+        'login_abuse_ip_bitmask' => 32,
+
+        // Specify usernames to exclude from the account abuse detection system. This is specified as a hardcoded array provided at
+        // class instantiation time, or can be saved in the db_table under the login_abuse_exempt field.
+        'login_abuse_exempt_usernames' => array(),
+
+        // Specify usernames to exclude from remote_ip matching. Users behind proxy servers should be appended to this array so their shifting remote IP will not log them out.
+        'match_remote_ip_exempt_usernames' => array(),
+
+        // Match the user's current remote IP against the one they logged in with.
+        'match_remote_ip' => true,
+
+        // An array of IP blocks that are bypass the remote_ip comparison check. Useful for dynamic IPs or those behind proxy servers.
+        'trusted_networks' => array(),
+
+        // Allow user accounts to be blocked? Requires the user table to have the columns 'blocked' and 'blocked_reason'
+        'blocking' => false,
+
+        // Use a db_login_table to detect excessive logins. This requires blocking to be enabled.
+        'abuse_detection' => false,
+
+        // Allow users to save login form passwords in their browser? Setting to 'true' may pose a potential security risk.
+        'login_form_allow_autocomplete' => false,
+    );
+
+    /**
+     * Constructs a new authentication object.
+     *
+     * @access public
+     * @param optional array $params  A hash containing parameters.
+     */
+    public function __construct($namespace='')
+    {
+        $app =& App::getInstance();
+
+        $this->_ns = $namespace;
+
+        // Initialize default parameters.
+        $this->setParam($this->_default_params);
+
+        // Get create tables config from global context.
+        if (!is_null($app->getParam('db_create_tables'))) {
+            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
+        }
+
+        if (!isset($_SESSION['_auth_sql'][$this->_ns])) {
+            $this->clear();
+        }
+    }
+
+    /**
+     * Setup the database tables for this class.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   26 Aug 2005 17:09:36
+     */
+    public function initDB($recreate_db=false)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        static $_db_tested = false;
+
+        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
+
+            // User table.
+            if ($recreate_db) {
+                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
+                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__);
+            }
+
+            // The minimal columns for a table compatible with the Auth_SQL class.
+            $db->query(sprintf(
+                "CREATE TABLE IF NOT EXISTS %1\$s (
+                    %2\$s MEDIUMINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+                    %3\$s varchar(255) NOT NULL default '',
+                    userpass VARCHAR(255) NOT NULL DEFAULT '',
+                    userpass_hashtype TINYINT UNSIGNED NOT NULL DEFAULT '0',
+                    first_name VARCHAR(50) NOT NULL DEFAULT '',
+                    last_name VARCHAR(50) NOT NULL DEFAULT '',
+                    email VARCHAR(255) NOT NULL DEFAULT '',
+                    login_abuse_exempt ENUM('true') DEFAULT NULL,
+                    blocked ENUM('true') DEFAULT NULL,
+                    blocked_reason VARCHAR(255) NOT NULL DEFAULT '',
+                    abuse_warning_level TINYINT NOT NULL DEFAULT '0',
+                    seconds_online INT NOT NULL DEFAULT '0',
+                    last_login_datetime DATETIME NOT NULL DEFAULT '%4\$s 00:00:00',
+                    last_access_datetime DATETIME NOT NULL DEFAULT '%4\$s 00:00:00',
+                    last_login_ip VARCHAR(45) NOT NULL DEFAULT '0.0.0.0',
+                    added_by_user_id SMALLINT DEFAULT NULL,
+                    modified_by_user_id SMALLINT DEFAULT NULL,
+                    added_datetime DATETIME NOT NULL DEFAULT '%4\$s 00:00:00',
+                    modified_datetime DATETIME NOT NULL DEFAULT '%4\$s 00:00:00',
+                    KEY %5\$s (%5\$s),
+                    KEY userpass (userpass),
+                    KEY email (email),
+                    KEY last_login_datetime (last_login_datetime),
+                    KEY last_access_datetime (last_access_datetime)
+                )",
+                $db->escapeString($this->getParam('db_table')),
+                $this->getParam('db_primary_key'),
+                $this->getParam('db_username_column'),
+                $db->getParam('zero_date'),
+                $this->getParam('db_username_column')
+            ));
+
+            if (!$db->columnExists($this->getParam('db_table'), array(
+                $this->getParam('db_primary_key'),
+                $this->getParam('db_username_column'),
+                'userpass',
+                'first_name',
+                'last_name',
+                'email',
+                'login_abuse_exempt',
+                'blocked',
+                'blocked_reason',
+                'abuse_warning_level',
+                'seconds_online',
+                'last_login_datetime',
+                'last_access_datetime',
+                'last_login_ip',
+                'added_by_user_id',
+                'modified_by_user_id',
+                'added_datetime',
+                'modified_datetime',
+            ), false, false)) {
+                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
+                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
+            }
+
+            // Login table is used for abuse_detection features.
+            if ($this->getParam('abuse_detection')) {
+                if ($recreate_db) {
+                    $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_login_table'));
+                    $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_login_table')), LOG_INFO, __FILE__, __LINE__);
+                }
+                $db->query(sprintf(
+                    "CREATE TABLE IF NOT EXISTS %1\$s (
+                        %2\$s MEDIUMINT UNSIGNED NOT NULL DEFAULT '0',
+                        login_datetime DATETIME NOT NULL DEFAULT '%3\$s 00:00:00',
+                        remote_ip_binary CHAR(32) NOT NULL DEFAULT '',
+                        KEY %4\$s (%4\$s),
+                        KEY login_datetime (login_datetime),
+                        KEY remote_ip_binary (remote_ip_binary)
+                    )",
+                    $this->getParam('db_login_table'),
+                    $this->getParam('db_primary_key'),
+                    $db->getParam('zero_date'),
+                    $this->getParam('db_primary_key')
+                ));
+
+                if (!$db->columnExists($this->getParam('db_login_table'), array(
+                    $this->getParam('db_primary_key'),
+                    'login_datetime',
+                    'remote_ip_binary',
+                ), false, false)) {
+                    $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), LOG_ALERT, __FILE__, __LINE__);
+                    trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_login_table')), E_USER_ERROR);
+                }
+            }
+        }
+        $_db_tested = true;
+    }
+
+    /**
+     * Set the params of an auth object.
+     *
+     * @param  array $params   Array of parameter keys and value to set.
+     * @return bool true on success, false on failure
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params['match_remote_ip_exempt_usernames'])) {
+            $params['match_remote_ip_exempt_usernames'] = array_map('strtolower', $params['match_remote_ip_exempt_usernames']);
+        }
+        if (isset($params['login_abuse_exempt_usernames'])) {
+            $params['login_abuse_exempt_usernames'] = array_map('strtolower', $params['login_abuse_exempt_usernames']);
+        }
+        if (isset($params['encryption_type'])) {
+            // Backwards misnomer compatibility.
+            $params['hash_type'] = $params['encryption_type'];
+        }
+        if (isset($params['hash_type']) && version_compare(PHP_VERSION, '5.5.0', '<') && in_array($params['hash_type'], array(self::ENCRYPT_PASSWORD_BCRYPT, self::ENCRYPT_PASSWORD_DEFAULT))) {
+            // These hash types require the password_* userland lib in PHP < 5.5.0
+            $pw_compat_lib = 'vendor/ircmaxell/password-compat/lib/password.php';
+            if (false !== stream_resolve_include_path($pw_compat_lib)) {
+                include_once $pw_compat_lib;
+            } else {
+                $app->logMsg(sprintf('Hash type %s requires password-compat lib in PHP < 5.5.0; falling back to ENCRYPT_SHA1_HARDENED', $params['hash_type']), LOG_ERR, __FILE__, __LINE__);
+                $params['hash_type'] = self::ENCRYPT_SHA1_HARDENED;
+            }
+        }
+        if (isset($params['hash_type']) && !in_array($params['hash_type'], array(self::ENCRYPT_PLAINTEXT, self::ENCRYPT_CRYPT, self::ENCRYPT_SHA1, self::ENCRYPT_SHA1_HARDENED, self::ENCRYPT_MD5, self::ENCRYPT_MD5_HARDENED, self::ENCRYPT_PASSWORD_BCRYPT, self::ENCRYPT_PASSWORD_DEFAULT))) {
+            $app->logMsg(sprintf('Invalid hash type %s; falling back to ENCRYPT_SHA1_HARDENED', $params['hash_type']), LOG_ERR, __FILE__, __LINE__);
+            $params['hash_type'] = self::ENCRYPT_SHA1_HARDENED;
+        }
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Clear any authentication tokens in the current session. A.K.A. logout.
+     *
+     * @access public
+     */
+    public function clear()
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        if ($this->get('user_id', false)) {
+
+            $this->initDB();
+
+            // FIX ME: Should we check if the session is active?
+            $db->query(sprintf(
+                "UPDATE %s SET
+                    seconds_online = seconds_online + ABS(UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)),
+                    last_login_datetime = '%s 00:00:00'
+                    WHERE %s = '%s'
+                ",
+                $this->_params['db_table'],
+                $db->getParam('zero_date'),
+                $this->_params['db_primary_key'],
+                $this->get('user_id')
+            ));
+        }
+        $_SESSION['_auth_sql'][$this->_ns] = array(
+            'authenticated'         => false,
+            'user_id'               => null,
+            'username'              => null,
+            'login_datetime'        => null,
+            'last_access_datetime'  => null,
+            'remote_ip'             => getRemoteAddr(),
+            'login_abuse_exempt'    => null,
+            'match_remote_ip_exempt'=> null,
+            'user_data'             => null,
+        );
+
+        $app->logMsg(sprintf('Cleared %s auth', $this->_ns), LOG_DEBUG, __FILE__, __LINE__);
+    }
+
+    /**
+     * Sets a variable into a registered auth session.
+     *
+     * @access public
+     * @param mixed $key      Which value to set.
+     * @param mixed $val      Value to set variable to.
+     */
+    public function set($key, $val)
+    {
+        if (!isset($_SESSION['_auth_sql'][$this->_ns]['user_data'])) {
+            $_SESSION['_auth_sql'][$this->_ns]['user_data'] = array();
+        }
+        $_SESSION['_auth_sql'][$this->_ns]['user_data'][$key] = $val;
+    }
+
+    /**
+     * Returns a specified value from a registered auth session.
+     *
+     * @access public
+     * @param mixed $key      Which value to return.
+     * @param mixed $default  Value to return if key not found in user_data.
+     * @return mixed          Value stored in session.
+     */
+    public function get($key, $default='')
+    {
+        if (isset($_SESSION['_auth_sql'][$this->_ns][$key])) {
+            return $_SESSION['_auth_sql'][$this->_ns][$key];
+        } else if (isset($_SESSION['_auth_sql'][$this->_ns]['user_data'][$key])) {
+            return $_SESSION['_auth_sql'][$this->_ns]['user_data'][$key];
+        } else {
+            return $default;
+        }
+    }
+
+    /**
+     * Retrieve and verify the given username and password against a matching user record in the database.
+     *
+     * @access private
+     * @param string $username      The username to check.
+     * @param string $password      The password to compare to username.
+     * @return mixed  False if credentials not found in DB, or returns DB row matching credentials.
+     */
+    public function authenticate($username, $password)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Get user data for specified username.
+        $qid = $db->query("
+            SELECT *, " . $this->_params['db_primary_key'] . " AS user_id
+            FROM " . $this->_params['db_table'] . "
+            WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
+        ");
+        if (!$user_data = mysql_fetch_assoc($qid)) {
+            $app->logMsg(sprintf('Username %s not found for authentication', $username), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Check given password against hashed DB password.
+        $old_hash_type = isset($user_data['userpass_hashtype']) && !empty($user_data['userpass_hashtype']) ? $user_data['userpass_hashtype'] : $this->getParam('hash_type');
+        if ($this->verifyPassword($password, $user_data['userpass'], $old_hash_type)) {
+            $app->logMsg(sprintf('Authentication successful for %s (user_id=%s)', $username, $user_data['user_id']), LOG_INFO, __FILE__, __LINE__);
+            unset($user_data['userpass']); // Avoid revealing the encrypted password in the $user_data.
+            if ($this->getParam('hash_type_autoupdate') && $old_hash_type != $this->getParam('hash_type')) {
+                // Let's update user's password hash to new type (just run setPassword with this authenticated passwordâŠ).
+                $this->setPassword($user_data['user_id'], $password);
+                $app->logMsg(sprintf('User %s password hash type updated from %s to %s', $username, $old_hash_type, $this->getParam('hash_type')), LOG_INFO, __FILE__, __LINE__);
+            }
+            return $user_data;
+        }
+
+        $app->logMsg(sprintf('Authentication failed for %s (user_id=%s)', $username, $user_data['user_id']), LOG_NOTICE, __FILE__, __LINE__);
+        return false;
+    }
+
+    /**
+     * Check username and password, and create new session if authenticated.
+     *
+     * @access private
+     * @param string $username     The username to check.
+     * @param string $password     The password to compare for username.
+     * @return boolean  Whether or not the credentials are valid.
+     */
+    public function login($username, $password)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        if ($user_data = $this->authenticate($username, $password)) {
+            // The credentials match. Now setup the session.
+            return $this->createSession($user_data);
+        }
+        // No login: failed authentication!
+        return false;
+    }
+
+    /**
+     * Create new login session for given user.
+     *
+     * @access private
+     * @param string $user_data User data that is normally returned from this->authenticate(). If provided manually:
+     *                          Required array values:
+     *                              'user_id' => '1'
+     *                              'username' => 'name'
+     *                          Optional array values:
+     *                              'match_remote_ip_exempt' => true
+     *                              'login_abuse_exempt' => true
+     *                              'abuse_warning_level' => true
+     *                              'blocked' => true
+     *                              'blocked_reason' => ''
+     *                              'âŠ' => 'âŠ' (any other values that should be retrievable via this->get())
+     * @return boolean          Whether or not the session was created. It will return true unless abuse detection is enabled and triggered.
+     */
+    public function createSession($user_data)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        $this->clear();
+
+        // Convert 'priv' to 'user_type' nomenclature to support older implementations.
+        if (isset($user_data['priv'])) {
+            $user_data['user_type'] = $user_data['priv'];
+        }
+
+        // Register authenticated session.
+        $_SESSION['_auth_sql'][$this->_ns] = array(
+            'authenticated'         => true,
+            'user_id'               => $user_data['user_id'],
+            'username'              => $user_data['username'],
+            'login_datetime'        => date('Y-m-d H:i:s'),
+            'last_access_datetime'  => date('Y-m-d H:i:s'),
+            'remote_ip'             => getRemoteAddr(),
+            'login_abuse_exempt'    => isset($user_data['login_abuse_exempt']) ? !empty($user_data['login_abuse_exempt']) : in_array(strtolower($user_data['username']), $this->_params['login_abuse_exempt_usernames']),
+            'match_remote_ip_exempt'=> isset($user_data['match_remote_ip_exempt']) ? !empty($user_data['match_remote_ip_exempt']) : in_array(strtolower($user_data['username']), $this->_params['match_remote_ip_exempt_usernames']),
+            'user_data'             => $user_data
+        );
+
+        /**
+         * Check if the account is blocked, respond in context to reason. Cancel the login if blocked.
+         */
+        if ($this->getParam('blocking')) {
+            if (isset($user_data['blocked']) && !empty($user_data['blocked'])) {
+                switch ($this->get('blocked_reason')) {
+                case 'account abuse' :
+                    $app->raiseMsg(sprintf(_("This account has been blocked due to possible account abuse. Please contact an administrator to reactivate."), null), MSG_WARNING, __FILE__, __LINE__);
+                    break;
+                default :
+                    $app->raiseMsg(sprintf(_("This account is currently not active. %s"), $this->get('blocked_reason')), MSG_WARNING, __FILE__, __LINE__);
+                    break;
+                }
+
+                // No login: user is blocked!
+                $app->logMsg(sprintf('User_id %s (%s) login failed due to blocked account: %s', $this->get('user_id'), $this->get('username'), $this->get('blocked_reason')), LOG_NOTICE, __FILE__, __LINE__);
+                $this->clear();
+                return false;
+            }
+        }
+
+        /**
+         * Check the db_login_table for too many logins under this account.
+         * (1) Count the number of unique IP addresses that logged in under this user within the login_abuse_timeframe
+         * (2) If this number exceeds the login_abuse_max_ips, assume multiple people are logging in under the same account.
+        **/
+        // TODO: make this ipv6 compatible. At the moment, ipv6 addresses are converted into zero for remote_ip_binary.
+        // http://www.highonphp.com/5-tips-for-working-with-ipv6-in-php
+        // https://stackoverflow.com/questions/444966/working-with-ipv6-addresses-in-php
+        if ($this->getParam('abuse_detection') && !$this->get('login_abuse_exempt')) {
+            $qid = $db->query("
+                SELECT COUNT(DISTINCT LEFT(remote_ip_binary, " . $this->_params['login_abuse_ip_bitmask'] . "))
+                FROM " . $this->_params['db_login_table'] . "
+                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
+                AND DATE_ADD(login_datetime, INTERVAL '" . $this->_params['login_abuse_timeframe'] . "' DAY_HOUR) > NOW()
+            ");
+            list($distinct_ips) = mysql_fetch_row($qid);
+            if ($distinct_ips > $this->_params['login_abuse_max_ips']) {
+                if ($this->get('abuse_warning_level') < $this->_params['login_abuse_warnings']) {
+                    // Warn the user with a password reset.
+                    $this->resetPassword(null, _("This is a security precaution. We have detected this account has been accessed from multiple computers simultaneously. It is against policy to share credentials with others. If further account abuse is detected this account will be blocked."));
+                    $app->raiseMsg(_("Your password has been reset as a security precaution. Please check your email for more information."), MSG_NOTICE, __FILE__, __LINE__);
+                    $app->logMsg(sprintf('Account abuse detected for user_id %s (%s) from IP %s', $this->get('user_id'), $this->get('username'), $this->get('remote_ip')), LOG_WARNING, __FILE__, __LINE__);
+                } else {
+                    // Block the account with the reason of account abuse.
+                    $this->blockAccount(null, 'account abuse');
+                    $app->raiseMsg(_("Your account has been blocked as a security precaution. Please contact us for more information."), MSG_NOTICE, __FILE__, __LINE__);
+                    $app->logMsg(sprintf('Account blocked for user_id %s (%s) from IP %s', $this->get('user_id'), $this->get('username'), $this->get('remote_ip')), LOG_ALERT, __FILE__, __LINE__);
+                }
+                // Increment user's warning level.
+                $db->query("UPDATE " . $this->_params['db_table'] . " SET abuse_warning_level = abuse_warning_level + 1 WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'");
+                // Reset the login counter for this user.
+                $db->query("DELETE FROM " . $this->_params['db_login_table'] . " WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'");
+                // No login: reset password because of account abuse!
+                $this->clear();
+                return false;
+            }
+
+            // Update the login counter table with this login access. Convert IP to binary.
+            // TODO: this query could benefit from INSERT DELAYED.
+            $db->query("
+                INSERT INTO " . $this->_params['db_login_table'] . " (
+                    " . $this->_params['db_primary_key'] . ",
+                    login_datetime,
+                    remote_ip_binary
+                ) VALUES (
+                    '" . $this->get('user_id') . "',
+                    '" . $this->get('login_datetime') . "',
+                    '" . sprintf('%032b', ip2long($this->get('remote_ip'))) . "'
+                )
+            ");
+        }
+
+        // Update user table with this login.
+        $db->query("
+            UPDATE " . $this->_params['db_table'] . " SET
+                last_login_datetime = '" . $this->get('login_datetime') . "',
+                last_access_datetime = '" . $this->get('login_datetime') . "',
+                last_login_ip = '" . $this->get('remote_ip') . "'
+            WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
+        ");
+
+        // Session created! We're logged-in!
+        return true;
+    }
+
+    /**
+     * Test if user has a currently logged-in session.
+     *  - authentication flag set to true
+     *  - username not empty
+     *  - total logged-in time is not greater than login_timeout
+     *  - idle time is not greater than idle_timeout
+     *  - remote address is the same as the login remote address
+     *
+     * @access public
+     */
+    public function isLoggedIn($user_id=null)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        if (isset($user_id)) {
+            // Check the login status of a specific user.
+            $qid = $db->query("
+                SELECT 1 FROM " . $this->_params['db_table'] . "
+                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
+                AND last_login_datetime > DATE_SUB(NOW(), INTERVAL '" . $this->_params['login_timeout'] . "' SECOND)
+                AND last_access_datetime > DATE_SUB(NOW(), INTERVAL '" . $this->_params['idle_timeout'] . "' SECOND)
+            ");
+            $login_status = (mysql_num_rows($qid) > 0);
+            $app->logMsg(sprintf('Returning %s login status for user_id %s', ($login_status ? 'true' : 'false'), $user_id), LOG_DEBUG, __FILE__, __LINE__);
+            return $login_status;
+        }
+
+        // User login test need only be run once per script execution. We cache the result in the session.
+        if ($this->_authentication_tested && isset($_SESSION['_auth_sql'][$this->_ns]['authenticated'])) {
+            $app->logMsg(sprintf('Returning cached authentication status: %s', ($_SESSION['_auth_sql'][$this->_ns]['authenticated'] ? 'true' : 'false')), LOG_DEBUG, __FILE__, __LINE__);
+            return $_SESSION['_auth_sql'][$this->_ns]['authenticated'];
+        }
+
+        // Testing login should occur once. This is the first time. Set flag.
+        $this->_authentication_tested = true;
+
+        // Some users will access from networks with a changing IP number (i.e. behind a proxy server).
+        // These users must be allowed entry by adding their IP to the list of trusted_networks, or their usernames to the list of match_remote_ip_exempt_usernames.
+        if ($trusted_net = ipInRange(getRemoteAddr(), $this->_params['trusted_networks'])) {
+            $user_in_trusted_network = true;
+            $app->logMsg(sprintf('User_id %s accessing from trusted network %s',
+                ($this->get('user_id') ? $this->get('user_id') . ' (' .  $this->get('username') . ')' : 'unknown'),
+                $trusted_net
+            ), LOG_DEBUG, __FILE__, __LINE__);
+        } else {
+            $user_in_trusted_network = false;
+        }
+
+        // Do we match the user's remote IP at all? Yes, if set in config and not disabled for specific user.
+        if ($this->getParam('match_remote_ip') && !$this->get('match_remote_ip_exempt')) {
+            $remote_ip_is_matched = (isset($_SESSION['_auth_sql'][$this->_ns]['remote_ip']) && $_SESSION['_auth_sql'][$this->_ns]['remote_ip'] == getRemoteAddr()) || $user_in_trusted_network;
+        } else {
+            $app->logMsg(sprintf('User_id %s exempt from remote_ip match (comparing %s == %s)',
+                ($this->get('user_id') ? $this->get('user_id') . ' (' .  $this->get('username') . ')' : 'unknown'),
+                $_SESSION['_auth_sql'][$this->_ns]['remote_ip'],
+                getRemoteAddr()
+            ), LOG_DEBUG, __FILE__, __LINE__);
+            $remote_ip_is_matched = true;
+        }
+
+        // Test login with information stored in session. Skip IP matching for users from trusted networks.
+        if (isset($_SESSION['_auth_sql'][$this->_ns]['authenticated'])
+            && true === $_SESSION['_auth_sql'][$this->_ns]['authenticated']
+            && isset($_SESSION['_auth_sql'][$this->_ns]['username'])
+            && !empty($_SESSION['_auth_sql'][$this->_ns]['username'])
+            && isset($_SESSION['_auth_sql'][$this->_ns]['login_datetime'])
+            && strtotime($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) > (time() - $this->_params['login_timeout'])
+            && isset($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime'])
+            && strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) > (time() - $this->_params['idle_timeout'])
+            && $remote_ip_is_matched
+        ) {
+            // User is authenticated!
+            $_SESSION['_auth_sql'][$this->_ns]['last_access_datetime'] = date('Y-m-d H:i:s');
+
+            // Update the DB with the last_access_datetime and increment the seconds_online.
+            $db->query("
+                UPDATE " . $this->_params['db_table'] . " SET
+                seconds_online = seconds_online + ABS(UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)) + 1,
+                last_access_datetime = '" . $this->get('last_access_datetime') . "'
+                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->get('user_id') . "'
+            ");
+            if (mysql_affected_rows($db->getDBH()) > 0) {
+                // User record still exists in DB. Do this to ensure user was not delete from DB between accesses. Notice "+ 1" in SQL above to ensure record is modified.
+                return true;
+            } else {
+                $app->logMsg(sprintf('Session update failed; record not found for user_id %s (%s).', $this->get('user_id'), $this->get('username')), LOG_NOTICE, __FILE__, __LINE__);
+            }
+        } else if (isset($_SESSION['_auth_sql'][$this->_ns]['authenticated']) && true === $_SESSION['_auth_sql'][$this->_ns]['authenticated']) {
+            // User is authenticated, but login has expired.
+
+            // Log the reason for login expiration.
+            $expire_reasons = array();
+            $user_notified = false;
+            if (!isset($_SESSION['_auth_sql'][$this->_ns]['username']) || empty($_SESSION['_auth_sql'][$this->_ns]['username'])) {
+                $expire_reasons[] = 'username not found';
+            }
+            if (!isset($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) || strtotime($_SESSION['_auth_sql'][$this->_ns]['login_datetime']) <= (time() - $this->_params['login_timeout'])) {
+                $expire_reasons[] = sprintf('login_timeout expired (%s older than %s seconds ago)', $_SESSION['_auth_sql'][$this->_ns]['login_datetime'], $this->_params['login_timeout']);
+            }
+            if (!isset($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) || strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) <= (time() - $this->_params['idle_timeout'])) {
+                $expire_reasons[] = sprintf('idle_timeout expired (%s older than %s seconds ago)', $_SESSION['_auth_sql'][$this->_ns]['last_access_datetime'], $this->_params['idle_timeout']);
+                if (strtotime($_SESSION['_auth_sql'][$this->_ns]['last_access_datetime']) > (time() - 43200)) {
+                    // Only raise message if last session is less than 12 hours old.
+                    // Notify user why they were logged out if they haven't yet been given a reason.
+                    $user_notified || $app->raiseMsg(sprintf(_("For your security we logged you out after being idle for %s. Please log in again."), humanTime($this->_params['idle_timeout'], 'hour', '%01.0f')), MSG_NOTICE, __FILE__, __LINE__);
+                    $user_notified = true;
+                }
+            }
+            if (!isset($_SESSION['_auth_sql'][$this->_ns]['remote_ip']) || $_SESSION['_auth_sql'][$this->_ns]['remote_ip'] != getRemoteAddr()) {
+                if ($this->getParam('match_remote_ip') && !$this->get('match_remote_ip_exempt') && !$user_in_trusted_network) {
+                    // There are three cases when a remote IP match will be the cause of a session termination:
+                    //   1. match_remote_ip config is enabled
+                    //   2. user is not match_remote_ip_exempt (set in the user_data, or in the match_remote_ip_exempt_usernames list)
+                    //   3. the user is connecting from a trusted network (their IP is listed in the trusted_networks)
+                    $expire_reasons[] = sprintf('remote_ip not matched (%s != %s)', $_SESSION['_auth_sql'][$this->_ns]['remote_ip'], getRemoteAddr());
+                    // Notify user why they were logged out if they haven't yet been given a reason.
+                    $user_notified || $app->raiseMsg(sprintf(_("For your security we logged you out because your IP address has changed. Please log in again."), null), MSG_NOTICE, __FILE__, __LINE__);
+                    $user_notified = true;
+                } else {
+                    $expire_reasons[] = sprintf('remote_ip not matched but user was exempt from this check (%s != %s)', $_SESSION['_auth_sql'][$this->_ns]['remote_ip'], getRemoteAddr());
+                }
+            }
+            $app->logMsg(sprintf('User_id %s (%s) session expired: %s', $this->get('user_id'), $this->get('username'), join(', ', $expire_reasons)), LOG_INFO, __FILE__, __LINE__);
+        } else {
+            $app->logMsg('Session is not authenticated', LOG_DEBUG, __FILE__, __LINE__);
+        }
+
+        // User is not authenticated.
+        $this->clear();
+        return false;
+    }
+
+    /**
+     * Redirect user to login page if they are not logged in.
+     *
+     * @param string $message The text description of a message to raise.
+     * @param int    $type    The type of message: MSG_NOTICE,
+     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
+     * @param string $file    __FILE__.
+     * @param string $line    __LINE__.
+     * @access public
+     */
+    public function requireLogin($message='', $type=MSG_NOTICE, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+
+        if (!$this->isLoggedIn()) {
+            // Display message for requiring login. (RaiseMsg will ignore empty strings.)
+            if ('' != $message) {
+                $app->raiseMsg($message, $type, $file, $line);
+            }
+
+            // Login scripts must have the same 'login' tag for boomerangURL verification/manipulation.
+            $app->setBoomerangURL(absoluteMe(), 'login');
+            $app->dieURL($this->_params['login_url']);
+        }
+    }
+
+    /**
+     * This sets the 'blocked' field for a user in the db_table, and also
+     * adds an optional reason
+     *
+     * @param  string   $reason      The reason for blocking the account.
+     */
+    public function blockAccount($user_id=null, $reason='')
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        if ($this->getParam('blocking')) {
+            if (mb_strlen($db->escapeString($reason)) > 255) {
+                // blocked_reason field is varchar(255).
+                $app->logMsg(sprintf('Blocked reason provided is greater than 255 characters: %s', $reason), LOG_WARNING, __FILE__, __LINE__);
+            }
+
+            // Get user_id if specified.
+            $user_id = isset($user_id) ? $user_id : $this->get('user_id');
+            $db->query("
+                UPDATE " . $this->_params['db_table'] . " SET
+                blocked = 'true',
+                blocked_reason = '" . $db->escapeString($reason) . "'
+                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
+            ");
+        }
+    }
+
+    /**
+     * Tests if the "blocked" flag is set for a user.
+     *
+     * @param  int      $user_id    User id to look for.
+     * @return boolean              True if the user is blocked, false otherwise.
+     */
+    public function isBlocked($user_id=null)
+    {
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        if ($this->getParam('blocking')) {
+            // Get user_id if specified.
+            $user_id = isset($user_id) ? $user_id : $this->getVal('user_id');
+            $qid = $db->query("
+                SELECT 1
+                FROM " . $this->_params['db_table'] . "
+                WHERE blocked = 'true'
+                AND " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
+            ");
+            return mysql_num_rows($qid) === 1;
+        }
+    }
+
+    /**
+     * Unblocks a user in the db_table, and clears any blocked_reason.
+     */
+    public function unblockAccount($user_id=null)
+    {
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        if ($this->getParam('blocking')) {
+            // Get user_id if specified.
+            $user_id = isset($user_id) ? $user_id : $this->get('user_id');
+            $db->query("
+                UPDATE " . $this->_params['db_table'] . " SET
+                blocked = NULL,
+                blocked_reason = ''
+                WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
+            ");
+        }
+    }
+
+    /**
+     * Returns true if username already exists in database.
+     *
+     * @param  string  $username    Username to look for.
+     * @return bool                 True if username exists.
+     */
+    public function usernameExists($username)
+    {
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        $qid = $db->query("
+            SELECT 1
+            FROM " . $this->_params['db_table'] . "
+            WHERE " . $this->_params['db_username_column'] . " = '" . $db->escapeString($username) . "'
+        ");
+        return (mysql_num_rows($qid) > 0);
+    }
+
+    /**
+     * Returns a username for a specified user id.
+     *
+     * @param  string  $user_id     User id to look for.
+     * @return string               Username, or false if none found.
+     */
+    public function getUsername($user_id)
+    {
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        $qid = $db->query("
+            SELECT " . $this->_params['db_username_column'] . "
+            FROM " . $this->_params['db_table'] . "
+            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
+        ");
+        if (list($username) = mysql_fetch_row($qid)) {
+            return $username;
+        } else {
+            return false;
+        }
+    }
+
+    /*
+    * Generate a cryptographically secure, random password.
+    *
+    * @access   public
+    * @param    int  $bytes     Length of password (in bytes)
+    * @return   string          Random string of characters.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Nov 2014 20:30:27
+    */
+    public function generatePassword($bytes=10)
+    {
+        $app =& App::getInstance();
+
+        $bytes = is_numeric($bytes) ? $bytes : 10;
+        $string = strtok(base64_encode(openssl_random_pseudo_bytes($bytes, $strong)), '=');
+        if (!$strong) {
+            $app->logMsg(sprintf('Password generated was not "cryptographically strong"; check your openssl.', null), LOG_NOTICE, __FILE__, __LINE__);
+        }
+
+        return $string;
+    }
+
+    /**
+     *
+     */
+    public function encryptPassword($password, $salt=null, $hash_type=null)
+    {
+        $app =& App::getInstance();
+
+        $password = (string)$password;
+
+        // Existing password hashes rely on the same key/salt being used to compare hashs.
+        // Don't change this (or the value applied to signing_key) unless you know existing hashes or signatures will not be affected!
+        $more_salt = 'B36D18E5-3FE4-4D58-8150-F26642852B81';
+
+        $hash_type = isset($hash_type) && !empty($hash_type) ? $hash_type : $this->getParam('hash_type');
+
+        switch ($hash_type) {
+        case self::ENCRYPT_PLAINTEXT :
+            $encrypted_password = $password;
+            break;
+
+        case self::ENCRYPT_CRYPT :
+            // If comparing password with an existing hashed password, provide the hashed password as the salt.
+            $encrypted_password = isset($salt) ? crypt($password, $salt) : crypt($password);
+            break;
+
+        case self::ENCRYPT_SHA1 :
+            $encrypted_password = sha1($password);
+            break;
+
+        case self::ENCRYPT_SHA1_HARDENED :
+            $encrypted_password = sha1($app->getParam('signing_key') . $password . $more_salt);
+            for ($i=0; $i < pow(2, 20); $i++) {
+                $encrypted_password = sha1($password . $encrypted_password);
+            }
+            break;
+
+        case self::ENCRYPT_MD5 :
+            $encrypted_password = md5($password);
+            break;
+
+        case self::ENCRYPT_MD5_HARDENED :
+            $encrypted_password = md5($app->getParam('signing_key') . $password . $more_salt);
+            for ($i=0; $i < pow(2, 20); $i++) {
+                $encrypted_password = md5($password . $encrypted_password);
+            }
+            break;
+
+        case self::ENCRYPT_PASSWORD_BCRYPT :
+            $encrypted_password = password_hash($password, PASSWORD_BCRYPT, array('cost' => 12));
+            break;
+
+        case self::ENCRYPT_PASSWORD_DEFAULT :
+            $encrypted_password = password_hash($password, PASSWORD_DEFAULT, array('cost' => 12));
+            break;
+
+        default :
+            $app->logMsg(sprintf('Unknown hash type: %s', $hash_type), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // In case our hashing function returns 'false' or another empty value, bail out.
+        if ('' == trim((string)$encrypted_password)) {
+            $app->logMsg(sprintf('Invalid password hash returned ("%s") for hash type %s; check yo crypto!', $encrypted_password, $hash_type), LOG_ALERT, __FILE__, __LINE__);
+            return false;
+        }
+
+        return $encrypted_password;
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Nov 2014 21:37:28
+    */
+    public function verifyPassword($password, $encrypted_password, $hash_type=null)
+    {
+        $app =& App::getInstance();
+
+        $hash_type = isset($hash_type) && !empty($hash_type) ? $hash_type : $this->getParam('hash_type');
+
+        switch ($hash_type) {
+        case self::ENCRYPT_CRYPT :
+            return $this->encryptPassword($password, $encrypted_password, $hash_type) == $encrypted_password;
+
+        case self::ENCRYPT_PLAINTEXT :
+        case self::ENCRYPT_MD5 :
+        case self::ENCRYPT_MD5_HARDENED :
+        case self::ENCRYPT_SHA1 :
+        case self::ENCRYPT_SHA1_HARDENED :
+            return $this->encryptPassword($password, $encrypted_password, $hash_type) == $encrypted_password;
+
+        case self::ENCRYPT_PASSWORD_BCRYPT :
+        case self::ENCRYPT_PASSWORD_DEFAULT :
+            return password_verify($password, $encrypted_password);
+
+        default :
+            $app->logMsg(sprintf('Unknown hash type: %s', $hash_type), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+    }
+
+    /**
+     *
+     */
+    public function setPassword($user_id=null, $password, $hash_type=null)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Get user_id if specified.
+        $user_id = isset($user_id) ? $user_id : $this->get('user_id');
+
+        // New hash type.
+        $hash_type = isset($hash_type) ? $hash_type : $this->getParam('hash_type');
+
+        // Save the hash method used if a table exists for it.
+        $userpass_hashtype_clause = '';
+        if ($db->columnExists($this->_params['db_table'], 'userpass_hashtype', false)) {
+            $userpass_hashtype_clause = ", userpass_hashtype = '" . $db->escapeString($hash_type) . "'";
+        }
+
+        // Issue the password change query.
+        $db->query("
+            UPDATE " . $this->_params['db_table'] . " SET
+                userpass = '" . $db->escapeString($this->encryptPassword($password, null, $hash_type)) . "',
+                modified_datetime = NOW(),
+                modified_by_user_id = '" . $db->escapeString($user_id) . "'
+                $userpass_hashtype_clause
+            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
+        ");
+
+        if (mysql_affected_rows($db->getDBH()) != 1) {
+            $app->logMsg(sprintf('Failed to update password for user_id %s (no affected rows)', $user_id), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        $app->logMsg(sprintf('Password change successful for user_id %s', $user_id), LOG_INFO, __FILE__, __LINE__);
+        return true;
+    }
+
+    /**
+     * Resets the password for the user with the specified id.
+     *
+     * @param  string $user_id   The id of the user to reset.
+     * @param  string $reason    Additional message to add to the reset email.
+     * @return string            The user's new password.
+     */
+    public function resetPassword($user_id=null, $reason='')
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Get user_id if specified.
+        $user_id = isset($user_id) ? $user_id : $this->get('user_id');
+
+        // Reset password of a specific user.
+        $qid = $db->query("
+            SELECT * FROM " . $this->_params['db_table'] . "
+            WHERE " . $this->_params['db_primary_key'] . " = '" . $db->escapeString($user_id) . "'
+        ");
+        if (!$user_data = mysql_fetch_assoc($qid)) {
+            $app->logMsg(sprintf('Reset password failed. User_id %s not found.', $user_id), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Get new password.
+        $password = $this->generatePassword();
+
+        // Update password query.
+        $this->setPassword($user_id, $password);
+
+        // Make sure user has an email on record before continuing.
+        if (!isset($user_data['email']) || '' == trim($user_data['email'])) {
+            $app->logMsg(sprintf('Password reset but notification failed, no email address for user_id %s (%s).', $user_data[$this->_params['db_primary_key']], $user_data[$this->_params['db_username_column']]), LOG_NOTICE, __FILE__, __LINE__);
+        } else {
+            // Send the new password in an email.
+            $email = new Email(array(
+                'to' => $user_data['email'],
+                'from' => sprintf('"%s" <%s>', addcslashes($app->getParam('site_name'), '"'), $app->getParam('site_email')),
+                'subject' => sprintf('%s password change', $app->getParam('site_name'))
+            ));
+            $email->setTemplate('codebase/services/templates/email_reset_password.txt');
+            $email->replace(array(
+                'SITE_NAME' => $app->getParam('site_name'),
+                'SITE_URL' => $app->getParam('site_url'),
+                'SITE_EMAIL' => $app->getParam('site_email'),
+                'NAME' => ('' != $user_data['first_name'] . $user_data['last_name'] ? $user_data['first_name'] . ' ' . $user_data['last_name'] : $user_data[$this->_params['db_username_column']]),
+                'USERNAME' => $user_data[$this->_params['db_username_column']],
+                'PASSWORD' => $password,
+                'REASON' => ('' == trim($reason) ? '' : trim($reason) . ' '), // Add a space after the reason if it exists for better formatting.
+            ));
+            $email->send();
+        }
+
+        return array(
+            'username' => $user_data[$this->_params['db_username_column']],
+            'userpass' => $password
+        );
+    }
+
+} // end class
Index: tags/2.2.3/lib/AuthorizeNet.inc.php
===================================================================
--- tags/2.2.3/lib/AuthorizeNet.inc.php	(revision 632)
+++ tags/2.2.3/lib/AuthorizeNet.inc.php	(revision 632)
@@ -0,0 +1,317 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * AuthorizeNet.inc.php
+ *
+ * The AuthorizeNet class provides an abstract interface for communicating
+ * with authorize.net's AIM interface. Supports Auth.Net v3.1
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.0
+ * @date 2004-04-06
+ */
+
+// Example usage
+// require_once 'codebase/lib/AuthorizeNet.inc.php';
+//
+// $authorizenet = new AuthorizeNet();
+// $authorizenet->setParam(array(
+//     'x_Login' => 'myaccount',
+//     'x_Test_Request' => 'TRUE',
+//     'x_First_Name' => 'John',
+//     'x_Last_Name' => 'Doe',
+//     'x_Amount' => '1.20',
+//     'x_Card_Num' => '4111111111111111',
+//     'x_Card_Code' => '123',
+//     'x_Exp_Date' => '042008',
+//     'x_Invoice_Num' => '100',
+//     'x_Address' => '10 rue LevouvÃ©',
+//     'x_City' => 'SomeCity',
+//     'x_State' => 'CA',
+//     'x_Zip' => '75010',
+// ));
+//
+// $result_code = $authorizenet->process(); // Returns one of: false = error, 1 = accepted, 2 = declined, 3 = error
+// $result_array = $authorizenet->getResults();
+//
+// foreach ($result_array as $key => $value) {
+//     print "$key: $value<br>\n";
+// }
+
+class AuthorizeNet
+{
+
+    public $post_url = ''; // The URL to post data to.
+    protected $_results = array();
+    protected $_params = array();
+    protected $_default_params = array(
+        'x_version'         => '3.1',
+        'x_relay_response'  => 'FALSE',
+        'x_delim_data'      => 'TRUE',
+        'x_echo_data'       => 'TRUE',
+        'x_adc_url'         => 'FALSE',
+        'x_type'            => 'AUTH_CAPTURE',
+        'x_method'          => 'CC',
+        'x_login'           => '',
+        'x_tran_key'        => '',
+        'x_delim_char'      => ',',
+        'x_encap_char'      => '',
+        'md5_hash_salt'    => '',
+    );
+
+    // Array of response names. Used in the results array.
+    protected $_result_fields = Array(
+        'x_response_code',
+        'x_response_subcode',
+        'x_response_reason_code',
+        'x_response_reason_text',
+        'x_auth_code',
+        'x_avs_code',
+        'x_trans_id',
+        'x_invoice_num',
+        'x_description',
+        'x_amount',
+        'x_method',
+        'x_type',
+        'x_cust_id',
+        'x_first_name',
+        'x_last_name',
+        'x_company',
+        'x_address',
+        'x_city',
+        'x_state',
+        'x_zip',
+        'x_country',
+        'x_phone',
+        'x_fax',
+        'x_email',
+        'x_ship_to_first_name',
+        'x_ship_to_last_name',
+        'x_ship_to_company',
+        'x_ship_to_address',
+        'x_ship_to_city',
+        'x_ship_to_state',
+        'x_ship_to_zip',
+        'x_ship_to_country',
+        'x_tax',
+        'x_duty',
+        'x_freight',
+        'x_tax_exempt',
+        'x_po_num',
+        'x_md5_hash',
+        'x_card_code'
+    );
+
+    /**
+     * Constructs a new authentication object.
+     *
+     * @access public
+     *
+     * @param optional array $_params  A hash containing parameters.
+     */
+    public function __construct($params = array())
+    {
+        $app =& App::getInstance();
+
+        if (!function_exists('curl_init')) {
+            trigger_error('AuthorizeNet error: curl not installed.', E_USER_ERROR);
+        }
+
+        // The authorize.net url to post to.
+        $this->post_url = isset($params['post_url']) ? $params['post_url'] : 'https://secure.authorize.net/gateway/transact.dll';
+
+        // Set default parameters.
+        $this->_params = $this->_default_params;
+        $this->setParam($params);
+
+        $this->setParam(array('md5_hash_salt' => $app->getParam('signing_key')));
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+
+    /**
+     * Submit parameters to gateway.
+     *
+     * @access public
+     *
+     * @return mixed      False or x_response_code: false = error, 1 = accepted, 2 = declined, 3 = error
+     */
+    public function process()
+    {
+        $app =& App::getInstance();
+
+        if (empty($this->_params['x_login'])) {
+            $this->_results['x_response_reason_text'] = _("Transaction gateway temporarily not available. Please try again later.");
+            $app->logMsg(sprintf('x_login not specified.', null), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+        if (empty($this->_params['x_card_num'])) {
+            $this->_results['x_response_reason_text'] = _("Transaction gateway temporarily not available. Please try again later.");
+            $app->logMsg(sprintf('x_card_num not specified.', null), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Generate query string from params.
+        $q = '';
+        $delim = '';
+        foreach ($this->_params as $key=>$val) {
+            $q .= $delim . $key . '=' . urlencode($val);
+            $delim = '&';
+        }
+
+        // Setup curl and execute request.
+        $ch = curl_init();
+        curl_setopt($ch, CURLOPT_URL, $this->post_url);
+        curl_setopt($ch, CURLOPT_HEADER, 0);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+        curl_setopt($ch, CURLOPT_POST, 1);
+        curl_setopt($ch, CURLOPT_POSTFIELDS, $q);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
+        $result = curl_exec($ch);
+        $error = curl_error($ch);
+        curl_close($ch);
+
+        if (!$result) {
+            if ('' != $error) {
+                $app->logMsg(sprintf('Curl error: %s', $error), LOG_ERR, __FILE__, __LINE__);
+            }
+            return false;
+        }
+        return $this->_processResult($result);
+    }
+
+
+    /**
+     * Returns the results array. Returns a specific element if $key is provided.
+     *
+     * @access public
+     *
+     * @return array             Returns the results array.
+     */
+    public function getResult($key=null)
+    {
+        if (isset($key)) {
+            if (isset($this->_results[$key])) {
+                return $this->_results[$key];
+            } else {
+                return null;
+            }
+        } else {
+            return $this->_results;
+        }
+    }
+
+    /**
+     * Tests a returned md5 hash value with a locally computed one.
+     *
+     * @access public
+     *
+     * @return bool             True if the hash is valid, false otherwise.
+     */
+    public function validMD5Hash()
+    {
+        return (
+            mb_strtolower($this->getResult('x_md5_hash')) == mb_strtolower(md5(
+                $this->getParam('md5_hash_salt') .
+                $this->getParam('x_login') .
+                $this->getResult('x_trans_id') .
+                $this->getResult('x_amount')
+            ))
+        );
+    }
+
+    /**
+     * Reset all variables. Call before beginning a new transaction.
+     *
+     * @access public
+     */
+    public function reset()
+    {
+        $this->_results = Array();
+        $this->_params = $this->_default_params;
+    }
+
+    /**
+     * Process the result from the curl execution to create an associative array of returned data.
+     *
+     * @access private.
+     *
+     * @param  mixed $result    The result from the curl execution.
+     *
+     * @return integer      Transaction result code.
+     */
+    protected function _processResult($result)
+    {
+        $this->_results = Array();
+
+        $results = explode($this->getParam('x_delim_char'), $result);
+
+        $num = sizeof($this->_result_fields);
+        for ($i=0, $j=0; $i<$num; $i++) {
+            if (isset($this->_result_fields[$i])) {
+                $this->_results[$this->_result_fields[$i]] = $results[$i];
+            } else {
+                $j++;
+                $this->_results["x_custom_$j"] = $results[$i];
+            }
+        }
+        return $this->_results['x_response_code'];
+    }
+}
Index: tags/2.2.3/lib/CSS.inc.php
===================================================================
--- tags/2.2.3/lib/CSS.inc.php	(revision 632)
+++ tags/2.2.3/lib/CSS.inc.php	(revision 632)
@@ -0,0 +1,197 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * CSS.inc.css
+ *
+ * Dynamically outputs cached CSS data.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.2
+ */
+class CSS
+{
+
+    // Include these style sheets.
+    protected $_css_files = array('default' => array());
+
+    // CSS object parameters.
+    protected $_params = array(
+        'character_set' => 'utf-8',
+        'cache_enable' => false,
+        'cache_css' => null, // Same as above; kept for backwards compatibility.
+        'cache_seconds' => 86400,
+        'strip_whitespace' => false,
+        'output_compression' => false,
+    );
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+            // Maintaining backwards compatibility.
+            if (isset($params['cache_css']) && !isset($params['cache_enable'])) {
+                $this->_params['cache_enable'] = $params['cache_css'];
+            }
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Add a file-path to the array of files to include as CSS.
+     *
+     * @access  public
+     * @param   string  $file   Include path to css files.
+     * @param   mixed   $realms   Realm name string or array of realm names.
+     * @return  bool    True on success, false on failure.
+     */
+    public function setFile($file, $realms='')
+    {
+        $app =& App::getInstance();
+
+        if (!is_array($realms)) {
+            $realms = array($realms);
+        }
+
+        if ($fp = fopen($file, 'r', true)) {
+            foreach ($realms as $realm) {
+                $realm = '' == $realm ? 'default' : $realm;
+                $this->_css_files[$realm][] = $file;
+            }
+            fclose($fp);
+            return true;
+        } else {
+            $app->logMsg(sprintf('CSS file non-existent: %s', $file), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Output headers for CSS.
+     *
+     * @access  public
+     *
+     * @return  bool    False if no files have been set.
+     */
+    public function headers($realm='')
+    {
+        $app =& App::getInstance();
+
+        $realm = '' == $realm ? 'default' : $realm;
+
+        if (empty($this->_css_files[$realm])) {
+            $app->logMsg(sprintf('CSS::headers called without specifying any files.', null), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Get time of latest modified file, including this class file.
+        $files_mtime = array();
+        foreach (array_merge($this->_css_files[$realm], array(__FILE__)) as $file) {
+            $files_mtime[] = statIncludePath($file, 'mtime');
+        }
+        sort($files_mtime, SORT_NUMERIC);
+        $latest_mtime = array_pop($files_mtime);
+
+        if ($this->getParam('output_compression') && extension_loaded('zlib')) {
+            ob_start('ob_gzhandler');
+        }
+
+        header(sprintf('Content-Type: text/css; charset=%s', $this->getParam('character_set')));
+        header(sprintf('Last-Modified: %s GMT', gmdate('D, d M Y H:i:s', $latest_mtime), null));
+        if ($this->getParam('cache_enable')) {
+            header(sprintf('Cache-Control: public, max-age=%s', $this->getParam('cache_seconds')));
+            header(sprintf('Expires: %s GMT', gmdate('D, d M Y H:i:s', $latest_mtime + $this->getParam('cache_seconds'))));
+            header('Pragma: public');
+            header('Vary: Accept-Encoding');
+        } else {
+            // Disallow HTTP caching entirely. http://stackoverflow.com/a/2068407
+            header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1.
+            header('Pragma: no-cache'); // HTTP 1.0.
+            header('Expires: 0'); // Proxies.
+        }
+    }
+
+    /**
+     * Include CSS files specified by setFile().
+     *
+     * @access  public
+     *
+     * @return  bool    False if no files have been set.
+     */
+    public function output($realm='')
+    {
+        $app =& App::getInstance();
+
+        $realm = '' == $realm ? 'default' : $realm;
+
+        if (empty($this->_css_files[$realm])) {
+            $app->logMsg(sprintf('CSS::output called without specifying any files.', null), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        foreach ($this->_css_files[$realm] as $file) {
+            if ($this->getParam('strip_whitespace')) {
+                // Strip whitespace and print file.
+                echo preg_replace(
+                    array('!/\*.*?\*/!s', '/[\n\r]+/', '/([;:])\s+/m', '/\s*}[ \t]*/', '/\s*{\s*/', '/[ \t\n\r]*,[ \t\n\r]*/', '/^\s+/'),
+                    array('', "\n", '$1', '}', '{', ',', ''), file_get_contents($file, true)
+                );
+            } else {
+                // Include file as is.
+                include $file;
+            }
+        }
+
+        if ($this->getParam('output_compression') && extension_loaded('zlib')) {
+            ob_end_flush();
+        }
+    }
+}
Index: tags/2.2.3/lib/Cache.inc.php
===================================================================
--- tags/2.2.3/lib/Cache.inc.php	(revision 632)
+++ tags/2.2.3/lib/Cache.inc.php	(revision 632)
@@ -0,0 +1,292 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Cache.inc.php
+ *
+ * Provides an API for storing a limited amount of data
+ * intended to have a short lifetime in a user's session.
+ *
+ * Disable cache per-request by adding '_disable_cache=1' to a GET or POST parameter.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 2.1
+ * @since   2001
+ */
+
+class Cache
+{
+
+    // A place to keep object instances for the singleton pattern.
+    protected static $instances = array();
+
+    // Namespace of this instance of Prefs.
+    protected $_ns;
+
+    // Configuration parameters for this object.
+    protected $_params = array(
+
+        // Type of cache. Currently only 'session' is supported.
+        'type' => 'session',
+
+        // If false nothing will be cached or retrieved. Useful for testing realtime data requests.
+        'enabled' => true,
+
+        // The maximum size in bytes of any one variable.
+        'item_size_limit' => 4194304, // 4 MB
+
+        // The maximum size in bytes before the cache will begin flushing out old items.
+        'stack_size_limit' => 4194304, // 4 MB
+
+        // The minimum items to keep in the cache regardless of item or cache size.
+        'min_items' => 5,
+    );
+
+    /*
+    * Constructor. This is publicly accessible for compatibility with older implementations,
+    * but the preferred method of instantiation is by use of the singleton pattern:
+    *   $cache =& Cache::getInstance('namespace');
+    *   $cache->setParam(array('enabled' => true));
+    *
+    * @access   public
+    * @param    string  $namespace  This object will store data under this realm.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    05 Jun 2006 23:14:21
+    */
+    public function __construct($namespace='')
+    {
+        $app =& App::getInstance();
+
+        $this->_ns = $namespace;
+
+        if (true !== $app->getParam('enable_session')) {
+            // Force disable the cache because there is no session to save to.
+            $app->logMsg('Cache disabled, enable_session != true.', LOG_DEBUG, __FILE__, __LINE__);
+            $this->setParam(array('enabled' => false));
+        } else if (!isset($_SESSION['_cache'][$this->_ns])) {
+            // Otherwise, clear to initialize the session variable.
+            $this->clear();
+        }
+    }
+
+    /**
+     * This method enforces the singleton pattern for this class.
+     *
+     * @return  object  Reference to the global Cache object.
+     * @access  public
+     * @static
+     */
+    public static function &getInstance($namespace='')
+    {
+        if (!array_key_exists($namespace, self::$instances)) {
+            self::$instances[$namespace] = new self($namespace);
+        }
+        return self::$instances[$namespace];
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Stores a new variable in the session cache. The $key should not be numeric
+     * because the array_shift function will reset the key to the next largest
+     * int key. Weird behavior I can't understand. For example $cache["123"] will become $cache[0]
+     *
+     * @param str   $key                An identifier for the cached object.
+     * @param mixed $var                The data to store in the session cache.
+     * @param bool  $allow_oversized    If we have something really big that we still want to cache, setting this to true allows this.
+     * @return bool                     True on success, false otherwise.
+     */
+    public function set($key, $var, $allow_oversized=false)
+    {
+        $app =& App::getInstance();
+
+        if (true !== $this->getParam('enabled') || getFormData('_disable_cache')) {
+            $app->logMsg(sprintf('Cache disabled, not saving data.', null), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (is_numeric($key)) {
+            $app->logMsg(sprintf('Cache::set key value should not be numeric (%s given)', $key), LOG_WARNING, __FILE__, __LINE__);
+        }
+
+        $var = serialize($var);
+        $var_len = mb_strlen($var);
+
+        if ($var_len >= $this->getParam('item_size_limit')) {
+            $app->logMsg(sprintf('Serialized variable (%s bytes) more than item_size_limit (%s bytes).', $var_len, $this->getParam('item_size_limit')), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        if ($allow_oversized && $var_len >= $this->getParam('stack_size_limit')) {
+            $app->logMsg(sprintf('Serialized variable (%s bytes) more than stack_size_limit (%s bytes).', $var_len, $this->getParam('stack_size_limit')), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Remove any value already stored under this key.
+        unset($_SESSION['_cache'][$this->_ns][$key]);
+
+        // Continue to prune the cache if its size is greater than stack_size_limit, but keep at least min_items.
+        while (mb_strlen(serialize($_SESSION['_cache'][$this->_ns])) + $var_len >= $this->getParam('stack_size_limit') && sizeof($_SESSION['_cache'][$this->_ns]) >= $this->getParam('min_items')) {
+            array_shift($_SESSION['_cache'][$this->_ns]);
+        }
+
+        // Save this value under the specified key.
+        $_SESSION['_cache'][$this->_ns][$key] =& $var;
+
+        $app->logMsg(sprintf('Set cache item â%sâ', $key), LOG_DEBUG, __FILE__, __LINE__);
+
+        if ($var_len >= 1024000) {
+            $app->logMsg(sprintf('Successfully cached oversized variable (%s bytes).', $var_len), LOG_DEBUG, __FILE__, __LINE__);
+        }
+
+        return true;
+    }
+
+    /**
+     * Retrieves an object from the session cache and returns it unserialized.
+     * It also moves it to the top of the stack, which makes it such that the
+     * cache flushing mechanism of putCache deletes the oldest referenced items
+     * first.
+     *
+     * @param string $key  The key for the datum to retrieve.
+     * @return mixed          The requested datum, or false on failure.
+     */
+    public function get($key)
+    {
+        $app =& App::getInstance();
+
+        if (true !== $this->getParam('enabled') || getFormData('_disable_cache')) {
+            $app->logMsg(sprintf('Cache disabled, not getting data.', null), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (isset($_SESSION['_cache'][$this->_ns]) && array_key_exists($key, $_SESSION['_cache'][$this->_ns])) {
+            $app->logMsg(sprintf('Retrieving %s from cache.', $key), LOG_DEBUG, __FILE__, __LINE__);
+            // Move the accessed cached datum to the top of the stack. Maybe somebody knows a better way to do this?
+            $tmp =& $_SESSION['_cache'][$this->_ns][$key];
+            unset($_SESSION['_cache'][$this->_ns][$key]);
+            $_SESSION['_cache'][$this->_ns][$key] =& $tmp;
+            // Return the unserialized datum.
+            return unserialize($_SESSION['_cache'][$this->_ns][$key]);
+        } else {
+            $app->logMsg(sprintf('Missing %s from cache.', $key), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Tells you if the object is cached.
+     *
+     * @param string $key  The key of the object to check.
+     * @return bool         True if a value exists for the given key.
+     */
+    public function exists($key)
+    {
+        $app =& App::getInstance();
+
+        if (true !== $this->getParam('enabled') || getFormData('_disable_cache')) {
+            $app->logMsg(sprintf('Cache disabled on exist assertion.', null), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        return (isset($_SESSION['_cache'][$this->_ns]) && array_key_exists($key, $_SESSION['_cache'][$this->_ns]));
+    }
+
+    /**
+     * Removes a cached object.
+     *
+     * @param string $key  The key of the object to check.
+     * @return bool         True if the value existed before being unset.
+     */
+    public function delete($key)
+    {
+        $app =& App::getInstance();
+
+        if (true !== $this->getParam('enabled') || getFormData('_disable_cache')) {
+            $app->logMsg(sprintf('Cache disabled, skipping delete of %s', $key), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (isset($_SESSION['_cache'][$this->_ns]) && array_key_exists($key, $_SESSION['_cache'][$this->_ns])) {
+            $app->logMsg(sprintf('Deleting cache item â%sâ', $key), LOG_DEBUG, __FILE__, __LINE__);
+            unset($_SESSION['_cache'][$this->_ns][$key]);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /*
+    * Delete all existing items from the cache.
+    *
+    * @access   public
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    05 Jun 2006 23:51:34
+    */
+    public function clear()
+    {
+        $app =& App::getInstance();
+
+        $_SESSION['_cache'][$this->_ns] = array();
+        $app->logMsg(sprintf('Cleared %s cache', $this->_ns), LOG_DEBUG, __FILE__, __LINE__);
+    }
+
+// END Cache
+}
+
Index: tags/2.2.3/lib/Captcha.inc.php
===================================================================
--- tags/2.2.3/lib/Captcha.inc.php	(revision 632)
+++ tags/2.2.3/lib/Captcha.inc.php	(revision 632)
@@ -0,0 +1,274 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Captcha.inc.php
+ *
+ * ASCII captcha system.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.0
+-------------------------------------------------------------------------------------
+// Example.
+
+// Instantiate new Captcha. This automatically generates a new 4-digit captcha number.
+$captcha = new Captcha();
+
+<!-- HTML form for captcha -->
+<label for="sc-captcha"><?php echo _("Reverse Turing Test") ?></label>
+<p class="sc-help"><?php echo _("Please type the following number to prove you are a human. This is a measure to prevent spam robots from submitting this form.") ?></p>
+<pre style="font-size: 0.5em;"><?php $captcha->printAsciiNumber() ?></pre>
+<?php $captcha->printForm() ?>
+
+// Finally, test if the captcha was submitted correctly.
+if (!$captcha->valid()) {
+    // Go back, show error.
+}
+
+// Or if you're using the FormValidator class.
+if (!$captcha->valid()) {
+    $fv->addError('sc-captcha', _("The Captcha you entered is invalid. Please try again."));
+}
+
+-------------------------------------------------------------------------------------
+ */
+class Captcha
+{
+
+    public $secret_key = 'some random seed text for the md5';
+    public $random_number;
+    public $ascii_numbers = array(
+        array(
+            '  #####   ',
+            ' ##   ##  ',
+            '##     ## ',
+            '##     ## ',
+            '##     ## ',
+            ' ##   ##  ',
+            '  #####   ',
+        ), array(
+            '   ##   ',
+            ' ####   ',
+            '   ##   ',
+            '   ##   ',
+            '   ##   ',
+            '   ##   ',
+            ' ###### ',
+        ), array(
+            ' #######  ',
+            '##     ## ',
+            '       ## ',
+            ' #######  ',
+            '##        ',
+            '##        ',
+            '######### ',
+        ), array(
+            ' #######  ',
+            '##     ## ',
+            '       ## ',
+            ' #######  ',
+            '       ## ',
+            '##     ## ',
+            ' #######  ',
+        ), array(
+            '##        ',
+            '##    ##  ',
+            '##    ##  ',
+            '##    ##  ',
+            '######### ',
+            '      ##  ',
+            '      ##  ',
+        ), array(
+            '######## ',
+            '##       ',
+            '##       ',
+            '#######  ',
+            '      ## ',
+            '##    ## ',
+            ' ######  ',
+        ), array(
+            ' #######  ',
+            '##     ## ',
+            '##        ',
+            '########  ',
+            '##     ## ',
+            '##     ## ',
+            ' #######  ',
+        ), array(
+            '######## ',
+            '##    ## ',
+            '    ##   ',
+            '   ##    ',
+            '  ##     ',
+            '  ##     ',
+            '  ##     ',
+        ), array(
+            ' #######  ',
+            '##     ## ',
+            '##     ## ',
+            ' #######  ',
+            '##     ## ',
+            '##     ## ',
+            ' #######  ',
+        ), array(
+            ' #######  ',
+            '##     ## ',
+            '##     ## ',
+            ' ######## ',
+            '       ## ',
+            '##     ## ',
+            ' #######  ',
+        )
+    );
+    
+    /**
+     * Constructor. Initialized new random number.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   20 Jan 2006 13:08:22
+     */
+    public function __construct()
+    {
+        $app =& App::getInstance();
+    
+        $this->secret_key = $app->getParam('signing_key');
+        $this->random_number = $this->_getRandomNumber();
+    }
+
+    /**
+     * Print ASCII number.
+     *
+     * @access  public
+     * @param   mixed   $num    Number to convert.
+     * @return  string  ASCII-ized number
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   07 Dec 2005 21:59:25
+     */
+    public function getAsciiNumber($num=null)
+    {
+        $app =& App::getInstance();
+    
+        if (!isset($num)) {
+            $num = $this->random_number;
+        }
+
+        if (preg_match('/[^\d]/', $num)) {
+            $app->logMsg(sprintf('Bad number: %s', $num), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Number must be an array of strings.
+        $num = preg_split('//', strval($num), -1, PREG_SPLIT_NO_EMPTY);
+
+        // All chars must be same height.
+        $output = '';
+        $char_height = sizeof($this->ascii_numbers[0]);
+        for ($i=0; $i<$char_height; $i++) {
+            foreach ($num as $n) {
+                $output .= $this->ascii_numbers[$n][$i];
+            }
+            $output .= "\n";
+        }
+
+        return $output;
+    }
+
+    /**
+     * Prints the captcha ASCII numbers.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   07 Dec 2005 22:09:04
+     */
+    public function printAsciiNumber()
+    {
+        $ascii = $this->getAsciiNumber($this->random_number);
+        ?><pre id="sc-captcha"><?php echo $ascii ?></pre><?php
+    }
+
+    /**
+     * Prints a form to enter captcha, including the required hidden hash form.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   07 Dec 2005 22:09:04
+     */
+    public function printForm()
+    {
+        $hash = $this->_getMD5key($this->random_number);
+        ?>
+        <input name="sc-captcha-input" id="sc-captcha-input" class="sc-small" type="text" />
+        <input name="sc-captcha-hash" id="sc-captcha-hash" type="hidden" value="<?php echo $hash ?>" />
+        <?php
+    }
+
+    /**
+     * Validate submitted number against ascii captcha.
+     * Regenerate md5 hash from submitted captcha number and compare with posted hash.
+     *
+     * @access  public
+     * @return  bool    True if number matches hash or false if EVIL ROBOT.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   07 Dec 2005 22:19:33
+     */
+    public function valid()
+    {
+        $number = getFormData('sc-captcha-input');
+        $hash = getFormData('sc-captcha-hash');
+
+        if ('' == $number . $hash) {
+            return false;
+        }
+
+        return $this->_getMD5key($number) == $hash;
+    }
+
+    /**
+     * Generate random number 3-to-5 digits long.
+     *
+     * @access  private
+     * @return  int         Generated number.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   07 Dec 2005 21:40:25
+     */
+    public function _getRandomNumber()
+    {
+        return mb_substr(strval(rand(10000, 99999)), 0, rand(3, 5));
+    }
+
+    /**
+     * Generate md5 hash of number using secret key.
+     *
+     * @access  private
+     * @param   string  $input  Input text to whack.
+     * @return  string  md5 sum of input + seed
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   07 Dec 2005 21:53:35
+     */
+    public function _getMD5key($input)
+    {
+        return md5($this->secret_key . $input);
+    }
+
+}
+
Index: tags/2.2.3/lib/Cart.inc.php
===================================================================
--- tags/2.2.3/lib/Cart.inc.php	(revision 632)
+++ tags/2.2.3/lib/Cart.inc.php	(revision 632)
@@ -0,0 +1,319 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Cart.inc.php
+ *
+ * Class description goes here...
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.0
+ *
+ * Example of use:
+---------------------------------------------------------------------
+$cart = new Cart();
+
+switch (getFormData('op')) {
+case 'add' :
+    // User clicks an "Add to cart" button.
+    if ($product_id = getFormData('product_id')) {
+        $qid = $db->query("SELECT * FROM product_tbl WHERE product_id = '" . $db->escapeString($product_id) . "'");
+        if ($p = mysql_fetch_assoc($qid)) {
+            $cart->add($product_id, $p['price'], 1, array(
+                'title' => $p['title'],
+                'shipping' => $p['shipping'],
+                'weight' => $p['weight'],
+            ));
+        }
+    }
+    break;
+
+case 'remove' :
+    // User clicks a "Remove item" button.
+    $cart->remove(getFormData('product_id'));
+    break;
+
+case 'qty' :
+    // User changes the quantity of an item.
+    $cart->setQty(getFormData('product_id'), getFormData('qty'));
+    break;
+
+case 'view' :
+default :
+    // User views cart.
+    foreach ($cart->getList() as $item) {
+        printf("Item: %s\nQty: %s\nPrice: %s\n\n", $item['title'], $item['quantity'], $item['price']);
+    }
+    break;
+}
+---------------------------------------------------------------------
+ */
+class Cart
+{
+
+    // Namespace of this instance.
+    protected $_ns;
+
+    // Configuration parameters for this object.
+    protected $_params = array(
+    );
+
+    /**
+     * Cart constructor.
+     */
+    public function __construct($namespace='')
+    {
+        $app =& App::getInstance();
+
+        $this->_ns = $namespace;
+
+        // Initialize.
+        if (!isset($_SESSION['_cart'][$this->_ns])) {
+            $this->clear();
+        }
+    }
+
+    /**
+     * Set the params of this object.
+     *
+     * @param  array $params   Array of param keys and values to set.
+     */
+    public function setParam($params=null)
+    {
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /*
+    * Add an item to the cart. If called twice for one item it will add the quantities together.
+    *
+    * @access   public
+    * @param    string  $item_id    A unique ID for the item
+    * @param    float   $price      Price-per-quantity-unit for the item
+    * @param    float   $quantity   Quantity of items (can be fractional unites?)
+    * @param    array   $specs      An array of additional specifications (title, weight, etc)
+    * @return   float               The new adjusted quantity of this item.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    11 Mar 2008 18:59:37
+    */
+    public function add($item_id, $price, $quantity=1, $specs=array())
+    {
+        $app =& App::getInstance();
+
+        // Include any previously added items in the total quantity.
+        $quantity += isset($_SESSION['_cart'][$this->_ns]['items'][$item_id]) ? $_SESSION['_cart'][$this->_ns]['items'][$item_id]['quantity'] : 0;
+
+        // Add item with adjusted quantity and merge in additional specs.
+        $_SESSION['_cart'][$this->_ns]['items'][$item_id] = array_merge($specs, array(
+            'quantity' => $quantity,
+            'price' => $price,
+            'extended_price' => $quantity * $price,
+        ));
+
+        $app->logMsg(sprintf('Added %s %s to cart (%s)', $quantity, $item_id, truncate(getDump($specs, true), 128, 'end')), LOG_DEBUG, __FILE__, __LINE__);
+
+        return $quantity;
+    }
+
+    /*
+    * Set the absolute quantity value of a specified item in a cart.
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    10 May 2008 16:42:25
+    */
+    public function setQty($item_id, $quantity)
+    {
+        if ($quantity <= 0) {
+            $this->remove($item_id);
+        } else {
+            if (isset($_SESSION['_cart'][$this->_ns]['items'][$item_id])) {
+                $item = $_SESSION['_cart'][$this->_ns]['items'][$item_id];
+                $_SESSION['_cart'][$this->_ns]['items'][$item_id] = array_merge($item, array(
+                    'quantity' => $quantity,
+                    'extended_price' => $quantity * $item['price'],
+                ));
+                return true;
+            } else {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    /*
+    * Set the absolute price value of a specified item in a cart.
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    10 May 2008 16:42:25
+    */
+    public function setPrice($item_id, $price)
+    {
+        if (isset($_SESSION['_cart'][$this->_ns]['items'][$item_id])) {
+            $item = $_SESSION['_cart'][$this->_ns]['items'][$item_id];
+            $_SESSION['_cart'][$this->_ns]['items'][$item_id] = array_merge($item, array(
+                'price' => $price,
+                'extended_price' => $price * $item['quantity'],
+            ));
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /*
+    * Remove an item from the cart.
+    *
+    * @access   public
+    * @param    string  $item_id    Item to remove from cart.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    11 Mar 2008 18:59:48
+    */
+    public function remove($item_id)
+    {
+        $app =& App::getInstance();
+
+        if (isset($_SESSION['_cart'][$this->_ns]['items'][$item_id])) {
+            unset($_SESSION['_cart'][$this->_ns]['items'][$item_id]);
+            $app->logMsg(sprintf('Removed %s from cart', $item_id), LOG_DEBUG, __FILE__, __LINE__);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /*
+    * Return the value matching key for a specified item.
+    *
+    * @access   public
+    * @param    string  $item_id    The ID of the cart item to retrieve.
+    * @param    string  $spec_key   The key of the value to return.
+    * @return   mixed               The value, or false if not found.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    11 Mar 2008 18:59:55
+    */
+    public function get($item_id, $spec_key)
+    {
+        if (isset($_SESSION['_cart'][$this->_ns]['items'][$item_id][$spec_key])) {
+            return $_SESSION['_cart'][$this->_ns]['items'][$item_id][$spec_key];
+        }
+        return false;
+    }
+
+    /*
+    * Return an array of the items in cart.
+    *
+    * @access   public
+    * @return   array   Array of cart items.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    11 Mar 2008 18:59:55
+    */
+    public function getList()
+    {
+        return $_SESSION['_cart'][$this->_ns]['items'];
+    }
+
+    /*
+    * Return the sum of the numeric values stored under the specified key for cart items.
+    * 0 will be returned if key is invalid or not filled with numeric values.
+    * Most commonly, $cart->sum() will be called when checking out to retrieve the total
+    * cost of the items in the cart.
+    *
+    * @access   public
+    * @param    string  $key    The spec key to sum.
+    * @return   float           The total added by all the found values.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    11 Mar 2008 19:00:12
+    */
+    public function sum($key='extended_price')
+    {
+        $sum = 0;
+        switch ($key) {
+        case 'items' :
+            $sum = sizeof($_SESSION['_cart'][$this->_ns]['items']);
+            break;
+
+        case 'extended_price' :
+            foreach ($_SESSION['_cart'][$this->_ns]['items'] as $item_id => $specs) {
+                $sum += isset($specs[$key]) && is_numeric($specs[$key]) ? $specs[$key] : 0;
+            }
+            break;
+
+        default :
+            // Retrieve arbitrary values stored in the cart (shipping, air miles, etc).
+            foreach ($_SESSION['_cart'][$this->_ns]['items'] as $item_id => $specs) {
+                $sum += isset($specs[$key]) && is_numeric($specs[$key]) ? $specs[$key] * $specs['quantity'] : 0;
+            }
+            break;
+        }
+
+        return $sum;
+    }
+
+    /**
+     * Resets the $_SESSION cart array. This should be executed with the same consideration
+     * as $auth->clear(), such as when logging out.
+     */
+    public function clear()
+    {
+        $app =& App::getInstance();
+
+        $_SESSION['_cart'][$this->_ns] = array(
+            'items' => array(),
+        );
+
+        $app->logMsg(sprintf('Cleared %s cart', $this->_ns), LOG_DEBUG, __FILE__, __LINE__);
+    }
+}
Index: tags/2.2.3/lib/Currency.inc.php
===================================================================
--- tags/2.2.3/lib/Currency.inc.php	(revision 632)
+++ tags/2.2.3/lib/Currency.inc.php	(revision 632)
@@ -0,0 +1,268 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Currency.inc.php
+ *
+ * Class to convert currency values.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.5
+ *
+ * Example of use:
+---------------------------------------------------------------------
+$currency = new Currency();
+$currency->setParam(array('cache_dir' => COMMON_BASE . '/tmp/xcache'));
+echo $currency->getRage('eur', 'usd');
+echo $currency->getValue(125.50, 'eur', 'usd');
+---------------------------------------------------------------------
+ */
+
+class Currency
+{
+    // Configuration parameters for this object.
+    protected $_params = array(
+        'cache_result' => true,
+        'cache_dir' => '',
+        'cache_age' => 86400, // 24 hours.
+        'api' => 'ecb', // 'xurrency' or 'ecb'. Add other APIs under the _performAPICall() method.
+        'api_key' => '', // Used only by xurrency API.
+    );
+
+    // Static runtime cache of values.
+    private static $rates = array();
+
+    /**
+     * Cart constructor.
+     */
+    public function __construct($params=array())
+    {
+        $app =& App::getInstance();
+
+        // Set custom parameters.
+        $this->setParam($params);
+
+        // Setup cache directory.
+        if ($this->getParam('cache_result')) {
+            if ('' == $this->getParam('cache_dir')) {
+                // Use a sane default cache directory.
+                $this->setParam(array('cache_dir' => '/tmp/xcache_' . md5(COMMON_BASE)));
+            }
+            if (!is_dir($this->getParam('cache_dir'))) {
+                $app->logMsg(sprintf('Creating cache_dir: %s', $this->getParam('cache_dir')), LOG_INFO, __FILE__, __LINE__);
+                if (!mkdir($this->getParam('cache_dir'))) {
+                    $app->logMsg(sprintf('Could not create cache_dir: %s', $this->getParam('cache_dir')), LOG_WARNING, __FILE__, __LINE__);
+                }
+            }
+        }
+    }
+
+    /**
+     * Set the params of this object.
+     *
+     * @param  array $params   Array of param keys and values to set.
+     */
+    public function setParam($params=null)
+    {
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /*
+    * Return the exchange value between the two given currencies for given amount.
+    *
+    * @access   public
+    * @param    float   $amount Base amount to convert from.
+    * @param    string  $base   3-letter currency code to convert from.
+    * @param    string  $target 3-letter currency code to convert to.
+    * @return   mixed   Float converted currency value, or false on error.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    05 May 2008 23:50:59
+    */
+    public function getValue($amount, $base, $target)
+    {
+        if (false !== $rate = $this->getRate($base, $target)) {
+            return abs($rate * $amount);
+        } else {
+            return false;
+        }
+    }
+
+    /*
+    * Return the currency conversion rate as a ratio.
+    *
+    * @access   public
+    * @param    string  $base   3-letter currency code to convert from.
+    * @param    string  $target 3-letter currency code to convert to.
+    * @return   mixed   Float exchange rate value, or false on error.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    25 May 2011 01:26:24
+    */
+    public function getRate($base, $target)
+    {
+        $app =& App::getInstance();
+
+        // If we've looked-up this rate during this run, return it from the runtime cache.
+        $rate_key = sprintf('%s-to-%s', $base, $target);
+        if (isset(self::$rates[$rate_key])) {
+            $app->logMsg(sprintf('Found %s in runtime cache: %s', $rate_key, self::$rates[$rate_key]), LOG_DEBUG, __FILE__, __LINE__);
+            return self::$rates[$rate_key];
+        }
+
+        $cache_file_path = sprintf('%s/%s-to-%s', $this->getParam('cache_dir'), $base, $target);
+        if (!is_dir(dirname($cache_file_path))) {
+            mkdir(dirname($cache_file_path));
+        }
+        $cache_file_mtime = @filemtime($cache_file_path);
+        if (!$this->getParam('cache_result') || !$cache_file_mtime || $cache_file_mtime < time() - $this->getParam('cache_age')) {
+            // Get fresh data and create cached file if missing or expired.
+            $app->logMsg(sprintf('Getting fresh currency exchange rate: %s-to-%s', $base, $target), LOG_DEBUG, __FILE__, __LINE__);
+            $value = $this->_performAPICall(array(
+                'amount' => '1',
+                'base' => $base,
+                'target' => $target
+            ));
+            if (false === $value || !is_numeric($value)) {
+                // Failed retrieving value. Use cached copy for now.
+                $app->logMsg(sprintf('Failed getting currency exchange rate: %s-to-%s, using cached copy', $base, $target), LOG_NOTICE, __FILE__, __LINE__);
+                if (!$value = @file_get_contents($cache_file_path)) {
+                    $app->logMsg(sprintf('Failed reading cached exchange rate file: %s', $cache_file_path), LOG_ERR, __FILE__, __LINE__);
+                    return false;
+                }
+            } else if ($this->getParam('cache_result') && !filePutContents($cache_file_path, $value)) {
+                $app->logMsg(sprintf('Failed writing to target rate file: %s', $cache_file_path), LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        } else {
+            $app->logMsg(sprintf('Getting cached currency exchange rate: %s-to-%s', $base, $target), LOG_DEBUG, __FILE__, __LINE__);
+            if (!$value = file_get_contents($cache_file_path)) {
+                $app->logMsg(sprintf('Failed reading target rate file: %s', $cache_file_path), LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+        $app->logMsg(sprintf('Found currency exchange rate: %s-to-%s = %s', $base, $target, $value), LOG_DEBUG, __FILE__, __LINE__);
+
+        // Store the value in the runtime cache.
+        self::$rates[$rate_key] = trim($value);
+
+        return trim($value);
+    }
+
+    /**
+     * @param  string
+     * @param  array
+     * @return mixed
+     * @access private
+     */
+    protected function _performAPICall($parameters=null)
+    {
+        $app =& App::getInstance();
+
+        switch ($this->getParam('api')) {
+        case 'xurrency' :
+            $api_url = 'http://xurrency.com/api/%s/%s/%s';
+            if ('' != $this->getParam('api_key')) {
+                $api_url .= '?key=' . $this->getParam('api_key');
+            }
+            $json_response = file_get_contents(sprintf($api_url, $parameters['base'], $parameters['target'], $parameters['amount']));
+            $json = json_decode($json_response);
+            if (null === $json) {
+                $app->logMsg(sprintf('Could not decode JSON response: %s', getDump($json_response)), LOG_WARNING, __FILE__, __LINE__);
+                return false;
+            } else if ($json->status === 'fail') {
+                if ($json->code == 3) {
+                    $app->logMsg(sprintf('Xurrency error LimitReachedException: %s', $json->message), LOG_WARNING, __FILE__, __LINE__);
+                } elseif ($json->code == 2) {
+                    $app->logMsg(sprintf('Xurrency error InvalidCurrencies: %s', $json->message), LOG_WARNING, __FILE__, __LINE__);
+                } elseif ($json->code == 4 || $json->code == 5) {
+                    $app->logMsg(sprintf('Xurrency error InvalidKey: %s', $json->message), LOG_WARNING, __FILE__, __LINE__);
+                } else {
+                    $app->logMsg(sprintf('Xurrency unknown error: %s', $json->message), LOG_WARNING, __FILE__, __LINE__);
+                }
+                return false;
+            } else {
+                return $json->result->value;
+            }
+            break;
+
+        case 'ecb' :
+            // Fetch XML from ECB <http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml>
+            $api_url = 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml';
+            if (false === $sXML = file_get_contents($api_url)) {
+                $app->logMsg(sprintf('Failed to load ECB XML data from: %s', $api_url), LOG_WARNING, __FILE__, __LINE__);
+                return false;
+            }
+            if (false === $oXML = simplexml_load_string($sXML)) {
+                $app->logMsg(sprintf('Failed to decode ECB XML data: %s', truncate($sXML, 200, 'end')), LOG_WARNING, __FILE__, __LINE__);
+                return false;
+            }
+            // Populate Array
+            foreach ($oXML->Cube->Cube->Cube as $oRate) {
+                foreach ($oRate->attributes() as $sKey => $sAttribute) {
+                    if ($sKey == 'currency') {
+                        $sCurrency = strtolower((string) $sAttribute);
+                    } else if ($sKey == 'rate') {
+                        $nRate = (string) $sAttribute;
+                    }
+                }
+                $aCurrencies['eur'][$sCurrency] = $nRate;
+                $aCurrencies[$sCurrency]['eur'] = 1 / $nRate;
+            }
+            // Check if requested rates are available.
+            if (isset($aCurrencies[$parameters['base']][$parameters['target']])) {
+                return (float) $aCurrencies[$parameters['base']][$parameters['target']];
+            } else {
+                $app->logMsg(sprintf('API %s does not have base %s or target %s', $this->getParam('api'), $parameters['base'], $parameters['target']), LOG_ALERT, __FILE__, __LINE__);
+                return false;
+            }
+
+        default :
+            $app->logMsg(sprintf('Unknown currency API: %s', $this->getParam('api')), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+    }
+}
+
+
Index: tags/2.2.3/lib/DB.inc.php
===================================================================
--- tags/2.2.3/lib/DB.inc.php	(revision 632)
+++ tags/2.2.3/lib/DB.inc.php	(revision 632)
@@ -0,0 +1,502 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * DB.inc.php
+ *
+ * Very lightweight DB semi-abstraction layer. Mainly to catch errors with mysql_query, with some goodies.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 2.1
+ */
+
+class DB
+{
+
+    // A place to keep an object instance for the singleton pattern.
+    protected static $instance = null;
+
+    // If $db->connect has successfully opened a db connection.
+    protected $_connected = false;
+
+    // Database handle.
+    public $dbh;
+
+    // Count how many queries run during the whole instance.
+    protected $_query_count = 0;
+
+    // Hash of DB parameters.
+    protected $_params = array();
+
+    // Default parameters.
+    protected $_param_defaults = array(
+
+        // DB passwords should be set as apache environment variables in httpd.conf, readable only by root.
+        'db_server' => 'localhost',
+        'db_name' => null,
+        'db_user' => null,
+        'db_pass' => null,
+
+        // Display all SQL queries. FALSE recommended for production sites.
+        'db_always_debug' => false,
+
+        // Display db errors. FALSE recommended for production sites.
+        'db_debug' => false,
+
+        // Script stops on db error. TRUE recommended for production sites.
+        'db_die_on_failure' => true,
+
+        // Special date settings. These will dynamically changes depending on MySQL version or settings.
+        'zero_date' => '0000-00-00',
+        'infinity_date' => '9999-12-31',
+    );
+
+    // Translate between HTML and MySQL character set names.
+    public $mysql_character_sets = array(
+        'utf-8' => 'utf8',
+        'iso-8859-1' => 'latin1',
+    );
+
+    // Caches.
+    protected static $existing_tables = null;
+    protected static $table_columns = null;
+
+    /**
+     * Constructor.
+     */
+    public function __construct()
+    {
+        // Initialize default parameters.
+        $this->_params = array_merge($this->_params, $this->_param_defaults);
+    }
+
+    /**
+     * This method enforces the singleton pattern for this class.
+     *
+     * @return  object  Reference to the global DB object.
+     * @access  public
+     * @static
+     */
+    public static function &getInstance()
+    {
+        if (self::$instance === null) {
+            self::$instance = new self();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     *
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Connect to database with credentials in params.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Aug 2005 14:02:49
+     */
+    public function connect()
+    {
+        $app =& App::getInstance();
+
+        if (!$this->getParam('db_name') || !$this->getParam('db_user') || !$this->getParam('db_pass')) {
+            $app->logMsg('Database credentials missing.', LOG_EMERG, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (!$this->getParam('db_server')) {
+            // If db_server not specified, assume localhost.
+            $this->setParam(array('db_server' => 'localhost'));
+        }
+
+        // Connect to database. Always create a new link to the server.
+        // Connection errors are suppressed so we can do our own error management below.
+        if ($this->dbh = @mysql_connect($this->getParam('db_server'), $this->getParam('db_user'), $this->getParam('db_pass'), true)) {
+            // Select database
+            mysql_select_db($this->getParam('db_name'), $this->dbh);
+        }
+
+        // Test for connection errors.
+        if (!$this->dbh || mysql_error($this->dbh)) {
+            $mysql_error_msg = $this->dbh ? 'Codebase MySQL error: (' . mysql_errno($this->dbh) . ') ' . mysql_error($this->dbh) : sprintf('Codebase MySQL error: Could not connect to server (db_server=%s, db_name=%s, db_user=%s, db_pass=%s)', $this->getParam('db_server'), $this->getParam('db_name'), $this->getParam('db_user'), ('' == $this->getParam('db_pass') ? 'NO' : 'YES'));
+            $app->logMsg($mysql_error_msg, LOG_EMERG, __FILE__, __LINE__);
+
+            // Print helpful or pretty error?
+            if ($this->getParam('db_debug')) {
+                echo $mysql_error_msg . "\n";
+            }
+
+            // Die if db_die_on_failure = true, or just continue without connection.
+            return $this->_fail();
+        }
+
+        // DB connection success!
+        $this->_connected = true;
+
+        // Tell MySQL what character set we're using. Available only on MySQL versions > 4.01.01.
+        if ('' != $app->getParam('character_set') && isset($this->mysql_character_sets[mb_strtolower($app->getParam('character_set'))])) {
+            $this->query("/*!40101 SET NAMES '" . $this->mysql_character_sets[mb_strtolower($app->getParam('character_set'))] . "' */");
+        } else {
+            $app->logMsg(sprintf('%s is not a known character_set.', $app->getParam('character_set')), LOG_ERR, __FILE__, __LINE__);
+        }
+
+        // Update config for this version of MySQL.
+        if (version_compare(mysql_get_server_info(), '5.7.4', '>=')) {
+            $this->setParam(array('zero_date' => '1000-01-01'));
+        }
+
+        return true;
+    }
+
+    /**
+     * Close db connection.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Aug 2005 14:32:01
+     */
+    public function close()
+    {
+        if (!$this->_connected) {
+            return false;
+        }
+        $this->_connected = false;
+        return mysql_close($this->dbh);
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    03 Jul 2013 14:50:23
+    */
+    public function reconnect()
+    {
+        $this->close();
+        $this->connect();
+    }
+
+    /*
+    * Die only if db_die_on_failure is true. This will be set to false for some cases
+    * when a database is not required for web app functionality.
+    *
+    * @access   public
+    * @param    string  $msg Print $msg when dying.
+    * @return   bool    false If we don't die.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Jan 2007 15:59:00
+    */
+    protected function _fail()
+    {
+        $app =& App::getInstance();
+
+        if ($this->getParam('db_die_on_failure')) {
+            if (!$app->cli) {
+                // For http requests, send a Service Unavailable header.
+                header(' ', true, 503);
+                echo _("This page is temporarily unavailable. Please try again in a few minutes.");
+            }
+            die;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Return the current database handler.
+     *
+     * @access  public
+     * @return  resource Current value of $this->dbh.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   20 Aug 2005 13:50:36
+     */
+    public function getDBH()
+    {
+        if (!$this->_connected) {
+            return false;
+        }
+
+        return $this->dbh;
+    }
+
+    /**
+     * Returns connection status
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Aug 2005 14:58:09
+     */
+    public function isConnected()
+    {
+        return (true === $this->_connected);
+    }
+
+    /**
+     * Returns a properly escaped string using mysql_real_escape_string() with the current connection's charset.
+     *
+     * @access  public
+     * @param   string  $string     Input string to be sent as SQL query.
+     * @return  string              Escaped string from mysql_real_escape_string()
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   06 Mar 2006 16:41:32
+     */
+    public function escapeString($string)
+    {
+        if (!$this->_connected) {
+            return false;
+        }
+
+        return mysql_real_escape_string($string, $this->dbh);
+    }
+
+    /**
+     * A wrapper for mysql_query. Allows us to set the database link_identifier,
+     * to trap errors and ease debugging.
+     *
+     * @param  string  $query   The SQL query to execute
+     * @param  bool    $debug   If true, prints debugging info
+     * @return resource         Query identifier
+     */
+    public function query($query, $debug=false)
+    {
+        $app =& App::getInstance();
+
+        if (!$this->_connected) {
+           return false;
+        }
+
+        $this->_query_count++;
+
+        $debugqry = preg_replace("/\n[\t ]+/", "\n", $query);
+        if ($this->getParam('db_always_debug') || $debug) {
+            echo "<!-- ----------------- Query $this->_query_count ---------------------\n$debugqry\n-->\n";
+        }
+
+        // Ensure we have an active connection.
+        // If we continue on a dead connection we might experience a "MySQL server has gone away" error.
+        // http://dev.mysql.com/doc/refman/5.0/en/gone-away.html
+        if (!mysql_ping($this->dbh)) {
+            $app->logMsg(sprintf('MySQL ping failed; reconnectingâŠ ("%s")', truncate(trim($debugqry), 150)), LOG_DEBUG, __FILE__, __LINE__);
+            $this->reconnect();
+        }
+
+        // Execute!
+        $qid = mysql_query($query, $this->dbh);
+
+        // Error checking.
+        if (!$qid || mysql_error($this->dbh)) {
+            $app->logMsg(sprintf('MySQL error %s: %s in query: %s', mysql_errno($this->dbh), mysql_error($this->dbh), $debugqry), LOG_EMERG, __FILE__, __LINE__);
+            if ($this->getParam('db_debug')) {
+                if (!$app->cli) {
+                    echo '<pre style="padding:2em; background:#ddd; font:9px monaco;">' . wordwrap(mysql_error($this->dbh)) . '<hr>' . htmlspecialchars($debugqry) . '</pre>';
+                }
+            }
+            // Die if db_die_on_failure = true, or just continue without connection
+            return $this->_fail();
+        }
+
+        return $qid;
+    }
+
+    /**
+     * Loads a list of tables in the current database into an array, and returns
+     * true if the requested table is found. Use this function to enable/disable
+     * functionality based upon the current available db tables or to dynamically
+     * create tables if missing.
+     *
+     * @param  string $table                The name of the table to search.
+     * @param  bool   $use_cached_results   Get fresh table info (in case DB changed).
+     * @return bool                         true if given $table exists.
+     */
+    public function tableExists($table, $use_cached_results=true)
+    {
+        $app =& App::getInstance();
+
+        if (!$this->_connected) {
+            return false;
+        }
+
+        if (is_null(self::$existing_tables) || !$use_cached_results) {
+            self::$existing_tables = array();
+            $qid = $this->query("SHOW TABLES");
+            while (list($row) = mysql_fetch_row($qid)) {
+                self::$existing_tables[] = $row;
+            }
+        }
+
+        if (in_array($table, self::$existing_tables)) {
+            return true;
+        } else {
+            $app->logMsg(sprintf('Nonexistent DB table: %s.%s', $this->getParam('db_name'), $table), LOG_INFO, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Tests if the given array of columns exists in the specified table.
+     *
+     * @param  string $table                The name of the table to search.
+     * @param  array  $columns              An array of column names.
+     * @param  bool   $strict               Exact schema match. If TRUE, test if *only* the given columns exist. If FALSE, test if given columns exist (possibly amongst others).
+     * @param  bool   $use_cached_results   Get fresh table info (in case DB changed).
+     * @return bool                         true if column(s) exist.
+     */
+    public function columnExists($table, $columns, $strict=true, $use_cached_results=true)
+    {
+        $app =& App::getInstance();
+
+        if (!$this->_connected) {
+            $app->logMsg(sprintf('No DB connection to run %s', __METHOD__), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Ensure the table exists.
+        if (!$this->tableExists($table, $use_cached_results)) {
+            $app->logMsg(sprintf('Table does not exist: %s', $table), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // For single-value columns.
+        if (!is_array($columns)) {
+            $columns = array($columns);
+        }
+
+        if (!isset(self::$table_columns[$table]) || !$use_cached_results) {
+            // Populate and cache array of current columns for this table.
+            self::$table_columns[$table] = array();
+            $qid = $this->query("DESCRIBE $table");
+            while ($row = mysql_fetch_row($qid)) {
+                self::$table_columns[$table][] = $row[0];
+            }
+        }
+
+        if ($strict) {
+            // Do an exact comparison of table schemas.
+            sort($columns);
+            sort(self::$table_columns[$table]);
+            return self::$table_columns[$table] == $columns;
+        } else {
+            // Only check that the specified columns are available in the table.
+            $match_columns = array_intersect(self::$table_columns[$table], $columns);
+            sort($columns);
+            sort($match_columns);
+            return $match_columns == $columns;
+        }
+    }
+
+    /*
+    * Return the total number of queries executed thus far.
+    *
+    * @access   public
+    * @return   int Number of queries
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    15 Jun 2006 11:46:05
+    */
+    public function numQueries()
+    {
+        return $this->_query_count;
+    }
+
+    /**
+     * Reset cached items.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Aug 2005 22:10:50
+     */
+    public function resetCache()
+    {
+        self::$existing_tables = null;
+        self::$table_columns = null;
+    }
+
+    /**
+     * Returns the values of an ENUM or SET column, returning them as an array.
+     *
+     * @param  string $db_table   database table to lookup
+     * @param  string $db_col     database column to lookup
+     * @param  bool   $sort          Sort the output.
+     * @return array    Array of the set/enum values on success, false on failure.
+     */
+    public function getEnumValues($db_table, $db_col, $sort=false)
+    {
+        $app =& App::getInstance();
+
+        $qid = $this->query("SHOW COLUMNS FROM " . $this->escapeString($db_table) . " LIKE '" . $this->escapeString($db_col) . "'", false);
+
+        $row = mysql_fetch_row($qid);
+        if (preg_match('/^enum|^set/i', $row[1]) && preg_match_all("/'([^']*)'/", $row[1], $matches)) {
+            if ($sort) {
+                natsort($matches[1]);
+            }
+            return $matches[1];
+        } else {
+            $app->logMsg(sprintf('No set or enum fields found in %s.%s', $db_table, $db_col), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+
+} // End.
+
Index: tags/2.2.3/lib/DBSessionHandler.inc.php
===================================================================
--- tags/2.2.3/lib/DBSessionHandler.inc.php	(revision 632)
+++ tags/2.2.3/lib/DBSessionHandler.inc.php	(revision 632)
@@ -0,0 +1,177 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * DBSessionHandler.inc.php
+ *
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 2.1
+ * @since   1999
+ */
+
+class DBSessionHandler
+{
+
+    public $db; // DB object.
+
+    protected $_params = array(
+        'db_table' => 'session_tbl',
+
+        // Automatically create table and verify columns. Better set to false after site launch.
+        // This value is overwritten by the $app->getParam('db_create_tables') setting if it is available.
+        'create_table' => true,
+    );
+
+    /**
+     * Constructor
+     *
+     * @access  public
+     * @param
+     * @return
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   18 Jul 2005 11:02:50
+     */
+    public function __construct($db, $params=array())
+    {
+        $app =& App::getInstance();
+
+        $this->_params = array_merge($this->_params, $params);
+
+        if (!method_exists($db, 'isConnected')) {
+            $app->logMsg(sprintf('Provided object (%s) is not a valid DB object.', get_class($db)), LOG_ERR, __FILE__, __LINE__);
+        } else {
+            if (!$db->isConnected()) {
+                $app->logMsg('Provided DB object is not connected.', LOG_ERR, __FILE__, __LINE__);
+            } else {
+                // OK! We have a valid, connected DB object.
+                $this->db =& $db;
+
+                // Get create tables config from global context.
+                if (!is_null($app->getParam('db_create_tables'))) {
+                    $this->_params['create_table'] = $app->getParam('db_create_tables');
+                }
+
+                // Ensure db table is fit.
+                $this->initDB();
+
+                session_set_save_handler(
+                    array(&$this, 'dbSessionOpen'),
+                    array(&$this, 'dbSessionClose'),
+                    array(&$this, 'dbSessionRead'),
+                    array(&$this, 'dbSessionWrite'),
+                    array(&$this, 'dbSessionDestroy'),
+                    array(&$this, 'dbSessionGarbage')
+                );
+                register_shutdown_function('session_write_close');
+            }
+        }
+    }
+
+    /**
+     * Setup the database table for this class.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   26 Aug 2005 17:09:36
+     */
+    public function initDB($recreate_db=false)
+    {
+        $app =& App::getInstance();
+
+        static $_db_tested = false;
+
+        if ($recreate_db || !$_db_tested && $this->_params['create_table']) {
+            if ($recreate_db) {
+                $this->db->query("DROP TABLE IF EXISTS " . $this->db->escapeString($this->_params['db_table']));
+                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->_params['db_table']), LOG_INFO, __FILE__, __LINE__);
+            }
+            $this->db->query("CREATE TABLE IF NOT EXISTS " . $this->db->escapeString($this->_params['db_table']) . " (
+                session_id char(32) NOT NULL default '',
+                session_data mediumtext NOT NULL,
+                last_access timestamp NOT NULL,
+                PRIMARY KEY (session_id),
+                KEY last_access (last_access)
+            )");
+
+            if (!$this->db->columnExists($this->_params['db_table'], array(
+                'session_id',
+                'session_data',
+                'last_access'
+            ), false, false)) {
+                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->_params['db_table']), LOG_ALERT, __FILE__, __LINE__);
+                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->_params['db_table']), E_USER_ERROR);
+            }
+        }
+        $_db_tested = true;
+    }
+
+    public function dbSessionOpen($save_path, $sess_name)
+    {
+        return true;
+    }
+
+    public function dbSessionClose()
+    {
+        return true;
+    }
+
+    public function dbSessionRead($session_id)
+    {
+        // Select the data belonging to session $session_id from the session table
+        $qid = $this->db->query("SELECT session_data FROM " . $this->db->escapeString($this->_params['db_table']) . " WHERE session_id = '" . $this->db->escapeString($session_id) . "'");
+
+        // Return the session data that was found
+        if (mysql_num_rows($qid) == 1) {
+            $row = mysql_fetch_row($qid);
+            return $row[0];
+        }
+
+        // NOTICE: Output is expected to be an empty string always rather than 'false'.
+        return '';
+    }
+
+    public function dbSessionWrite($session_id, $session_data)
+    {
+        // Write the serialized session data ($session_data) to the session table
+        $this->db->query("REPLACE INTO " . $this->db->escapeString($this->_params['db_table']) . "(session_id, session_data, last_access) VALUES ('" . $this->db->escapeString($session_id) . "', '" . $this->db->escapeString($session_data) . "', null)");
+
+        return true;
+    }
+
+    public function dbSessionDestroy($session_id)
+    {
+        // Delete from the table all data for the session $session_id
+        $this->db->query("DELETE FROM " . $this->db->escapeString($this->_params['db_table']) . " WHERE session_id = '" . $this->db->escapeString($session_id) . "'");
+
+        return true;
+    }
+
+    public function dbSessionGarbage($max_lifetime=72000)
+    {
+        // Delete old values from the session table.
+        $qid = $this->db->query("DELETE FROM " . $this->db->escapeString($this->_params['db_table']) . " WHERE UNIX_TIMESTAMP(last_access) < " . (time() - $max_lifetime));
+
+        return true;
+    }
+}
+
Index: tags/2.2.3/lib/Email.inc.php
===================================================================
--- tags/2.2.3/lib/Email.inc.php	(revision 632)
+++ tags/2.2.3/lib/Email.inc.php	(revision 632)
@@ -0,0 +1,487 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+* Email.inc.php
+*
+* Easy email template usage.
+*
+* @author  Quinn Comendant <quinn@strangecode.com>
+* @version 1.0
+*
+* Example of use:
+---------------------------------------------------------------------
+// Setup email object.
+$email = new Email(array(
+    'to' => array($frm['email'], 'q@lovemachine.local'),
+    'from' => sprintf('"%s" <%s>', addcslashes($app->getParam('site_name'), '"'), $app->getParam('site_email')),
+    'subject' => 'Your account has been activated',
+));
+$email->setTemplate('email_registration_confirm.ihtml');
+// $email->setString('Or you can pass your message body as a string, also with {VARIABLES}.');
+$email->replace(array(
+    'site_name' => $app->getParam('site_name'),
+    'site_url' => $app->getParam('site_url'),
+    'username' => $frm['username'],
+    'password' => $frm['password1'],
+));
+if ($email->send()) {
+    $app->raiseMsg(sprintf(_("A confirmation email has been sent to %s."), $frm['email']), MSG_SUCCESS, __FILE__, __LINE__);
+} else {
+    $app->logMsg(sprintf('Error sending confirmation email to address %s', $frm['email']), LOG_NOTICE, __FILE__, __LINE__);
+}
+---------------------------------------------------------------------
+*/
+
+class Email
+{
+
+    // Default parameters, to be overwritten by setParam() and read with getParam()
+    protected $_params = array(
+        'to' => null,
+        'from' => null,
+        'subject' => null,
+        'headers' => null,
+        'envelope_sender_address' => null, // AKA the bounce-to address. Will default to 'from' if left null.
+        'regex' => null,
+
+        // A single carriage return (\n) should terminate lines for locally injected mail.
+        // A carriage return + line-feed (\r\n) should be used if sending mail directly with SMTP.
+        'crlf' => "\n",
+
+        // RFC 2822 says line length MUST be no more than 998 characters, and SHOULD be no more than 78 characters, excluding the CRLF.
+        // http://mailformat.dan.info/body/linelength.html
+        'wrap' => true,
+        'line_length' => 75,
+
+        'sandbox_mode' => null,
+        'sandbox_to_addr' => null,
+    );
+
+    // String that contains filename of the template used (for logging).
+    protected $_template_filename;
+
+    // String that contains the email body.
+    protected $_template;
+
+    // String that contains the email body after replacements.
+    protected $_template_replaced;
+
+    // Email debug modes.
+    const SANDBOX_MODE_REDIRECT = 1; // Send all mail to 'sandbox_to_addr'
+    const SANDBOX_MODE_STDERR = 2; // Log all mail to stderr
+    const SANDBOX_MODE_LOG = 3; // Log all mail using $app->logMsg(âŠ)
+
+    /**
+     * Constructor.
+     *
+     * @access  public
+     * @param   array   $params     Array of object parameters.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Nov 2005 12:59:41
+     */
+    public function __construct($params=null)
+    {
+        // The regex used in validEmail(). Set here instead of in the default _params above so we can use the concatenation . dot.
+        // This matches a (valid) email address as complex as:
+        //      "Jane & Bob Smith" <bob&smith's/dep=sales!@smith-wick.ca.us > (Sales department)
+        // ...and something as simple as:
+        //      x@x.com
+        $this->setParam(array('regex' => '/^(?:(?:"[^"]*?"\s*|[^,@]*)(<\s*)|(?:"[^"]*?"|[^,@]*)\s+|)'   // Display name
+        . '((?:[^.<>\s@",\[\]]+[^<>\s@",\[\]])*[^.<>\s@",\[\]]+)'       // Local-part
+        . '@'                                                           // @
+        . '((?:(\[)|[A-Z0-9]?)'                                         // Domain, first char
+        . '(?(4)'                                                       // Domain conditional for if first domain char is [
+        . '(?:[0-9]{1,3}\.){3}[0-9]{1,3}\]'                             // TRUE, matches IP address
+        . '|'
+        . '[.-]?(?:[A-Z0-9]+[-.])*(?:[A-Z0-9]+\.)+[A-Z]{2,6}))'         // FALSE, matches domain name
+        . '(?(1)'                                                       // Comment conditional for if initial < exists
+        . '(?:\s*>\s*|>\s+\([^,@]+\)\s*)'                               // TRUE, ensure ending >
+        . '|'
+        . '(?:|\s*|\s+\([^,@]+\)\s*))$/i'));                            // FALSE ensure there is no ending >
+
+        if (isset($params)) {
+            $this->setParam($params);
+        }
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Enforce valid email addresses.
+            if (isset($params['to']) && !$this->validEmail($params['to'])) {
+                $params['to'] = null;
+            }
+            if (isset($params['from']) && !$this->validEmail($params['from'])) {
+                $params['from'] = null;
+            }
+            if (isset($params['envelope_sender_address']) && !$this->validEmail($params['envelope_sender_address'])) {
+                $params['envelope_sender_address'] = null;
+            }
+
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Loads template from file to generate email body.
+     *
+     * @access  public
+     * @param   string  $template   Filename of email template.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Nov 2005 12:56:23
+     */
+    public function setTemplate($template)
+    {
+        $app =& App::getInstance();
+
+        // Load file, using include_path.
+        if (!$this->_template = file_get_contents($template, true)) {
+            $app->logMsg(sprintf('Email template file does not exist: %s', $template), LOG_ERR, __FILE__, __LINE__);
+            $this->_template = null;
+            $this->_template_replaced = null;
+            return false;
+        }
+
+        // Ensure template is UTF-8.
+        $detected_encoding = mb_detect_encoding($this->_template, array('UTF-8', 'ISO-8859-1', 'WINDOWS-1252'), true);
+        if ('UTF-8' != strtoupper($detected_encoding)) {
+            $this->_template = mb_convert_encoding($this->_template, 'UTF-8', $detected_encoding);
+        }
+
+        // This could be a new template, so reset the _template_replaced.
+        $this->_template_replaced = null;
+
+        $this->_template_filename = $template;
+
+        return true;
+    }
+
+    /**
+     * Loads template from string to generate email body.
+     *
+     * @access  public
+     * @param   string  $template   Filename of email template.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Nov 2005 12:56:23
+     */
+    public function setString($string)
+    {
+        $app =& App::getInstance();
+
+        if ('' == trim($string)) {
+            $app->logMsg(sprintf('Empty string provided.', null), LOG_ERR, __FILE__, __LINE__);
+            $this->_template_replaced = null;
+            return false;
+        } else {
+            $this->_template = $string;
+            // This could be a new template, so reset the _template_replaced.
+            $this->_template_replaced = null;
+
+            $this->_template_filename = '(using Email::setString)';
+
+            return true;
+        }
+    }
+
+    /**
+     * Replace variables in template with argument data.
+     *
+     * @access  public
+     * @param   array   $replacements   Array keys are the values to search for, array vales are the replacement values.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Nov 2005 13:08:51
+     */
+    public function replace($replacements)
+    {
+        $app =& App::getInstance();
+
+        // Ensure template exists.
+        if (!isset($this->_template)) {
+            $app->logMsg(sprintf('Cannot replace variables, no template defined.', null), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Ensure replacements argument is an array.
+        if (!is_array($replacements)) {
+            $app->logMsg(sprintf('Cannot replace variables, invalid replacements.', null), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Apply regex pattern to search elements.
+        $search = array_keys($replacements);
+        array_walk($search, create_function('&$v', '$v = "{" . mb_strtoupper($v) . "}";'));
+
+        // Replacement values.
+        $replace = array_values($replacements);
+
+        // Search and replace all values at once.
+        $this->_template_replaced = str_replace($search, $replace, $this->_template);
+    }
+
+    /*
+    * Returns the body of the current email. This can be used to store the message that is being sent.
+    * It will use the original template, or the replaced template if it has been processed.
+    * You can also use this function to do post-processing on the email body before sending it,
+    * like removing extraneous lines:
+    * $email->setString(preg_replace('/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n\n", $email->getBody()));
+    *
+    * @access   public
+    * @return   string  Message body.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    18 Nov 2014 21:15:19
+    */
+    public function getBody()
+    {
+        $app =& App::getInstance();
+
+        $final_body = isset($this->_template_replaced) ? $this->_template_replaced : $this->_template;
+        // Ensure all placeholders have been replaced. Find anything with {...} characters.
+        if (preg_match('/({[^}]+})/', $final_body, $unreplaced_match)) {
+            unset($unreplaced_match[0]);
+            $app->logMsg(sprintf('Cannot get email body. Unreplaced variable %s in template %s', getDump($unreplaced_match)), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+        return $final_body;
+    }
+
+    /**
+     * Send email using PHP's mail() function.
+     *
+     * @access  public
+     * @param   string  $to
+     * @param   string  $from
+     * @param   string  $subject
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Nov 2005 12:56:09
+     */
+    public function send($to=null, $from=null, $subject=null, $headers=null)
+    {
+        $app =& App::getInstance();
+
+        // Use arguments if provided.
+        if (isset($to)) {
+            $this->setParam(array('to' => $to));
+        }
+        if (isset($from)) {
+            $this->setParam(array('from' => $from));
+        }
+        if (isset($subject)) {
+            $this->setParam(array('subject' => $subject));
+        }
+        if (isset($headers)) {
+            $this->setParam(array('headers' => $headers));
+        }
+
+        // Ensure required values exist.
+        if (!isset($this->_params['subject'])) {
+            $app->logMsg('Cannot send email. SUBJECT not defined.', LOG_ERR, __FILE__, __LINE__);
+            return false;
+        } else if (!isset($this->_template)) {
+            $app->logMsg(sprintf('Cannot send email: "%s". Template not set.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        } else if (!isset($this->_params['to'])) {
+            $app->logMsg(sprintf('Cannot send email: "%s". TO not defined.', $this->_params['subject']), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        } else if (!isset($this->_params['from'])) {
+            $app->logMsg(sprintf('Cannot send email: "%s". FROM not defined.', $this->_params['subject']), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Wrap email text body, using _template_replaced if replacements have been used, or just a fresh _template if not.
+        $final_body = isset($this->_template_replaced) ? $this->_template_replaced : $this->_template;
+        if (false !== $this->getParam('wrap')) {
+            $final_body = wordwrap($final_body, $this->getParam('line_length'), $this->getParam('crlf'));
+        }
+
+        // Ensure all placeholders have been replaced. Find anything with {...} characters.
+        if (preg_match('/({[^}]+})/', $final_body, $unreplaced_match)) {
+            unset($unreplaced_match[0]);
+            $app->logMsg(sprintf('Unreplaced variable "%s" in template "%s"', getDump($unreplaced_match), $this->_template_filename), LOG_ERR, __FILE__, __LINE__);
+        }
+
+        // Final "to" header can have multiple addresses if in an array.
+        $final_to = is_array($this->_params['to']) ? join(', ', $this->_params['to']) : $this->_params['to'];
+
+        // From headers are custom headers.
+        $headers = array('From' => $this->_params['from']);
+
+        // Additional headers.
+        if (isset($this->_params['headers']) && is_array($this->_params['headers'])) {
+            $headers = array_merge($this->_params['headers'], $headers);
+        }
+
+        // Process headers.
+        $final_headers = array();
+        foreach ($headers as $key => $val) {
+            // Validate key and values.
+            if (empty($val)) {
+                $app->logMsg(sprintf('Empty email header provided: %s', $key), LOG_DEBUG, __FILE__, __LINE__);
+                continue;
+            }
+            if (empty($key) || !is_string($key) || !is_string($val) || preg_match("/[\n\r]/", $key . $val) || preg_match('/[^\w-]/', $key)) {
+                $app->logMsg(sprintf('Broken email header provided: %s=%s', $key, $val), LOG_WARNING, __FILE__, __LINE__);
+                continue;
+            }
+            // If the envelope_sender_address was given as a header, move it to the correct place.
+            if ('envelope_sender_address' == strtolower($key)) {
+                $this->_params['envelope_sender_address'] = isset($this->_params['envelope_sender_address']) ? $this->_params['envelope_sender_address'] : $val;
+                continue;
+            }
+            // If we're sending in sandbox mode, remove any headers with recipient addresses.
+            if ($this->getParam('sandbox_mode') == self::SANDBOX_MODE_REDIRECT && in_array(strtolower($key), array('to', 'cc', 'bcc')) && mb_strpos($val, '@') !== false) {
+                // Don't carry this into the $final_headers.
+                $app->logMsg(sprintf('Skipping header in sandbox mode: %s=%s', $key, $val), LOG_DEBUG, __FILE__, __LINE__);
+                continue;
+            }
+            $final_headers[] = sprintf('%s: %s', $key, $val);
+        }
+        $final_headers = join($this->getParam('crlf'), $final_headers);
+
+        // This is the address where delivery problems are sent to. We must strip off everything except the local@domain part.
+        if (isset($this->_params['envelope_sender_address'])) {
+            $envelope_sender_address = sprintf('<%s>', trim($this->_params['envelope_sender_address'], '<>'));
+        } else {
+            $envelope_sender_address = preg_replace('/^.*<?([^\s@\[\]<>()]+\@[A-Za-z0-9.-]{1,}\.[A-Za-z]{2,5})>?$/iU', '$1', $this->_params['from']);
+        }
+        if ('' != $envelope_sender_address && $this->validEmail($envelope_sender_address)) {
+            $additional_parameter = sprintf('-f %s', $envelope_sender_address);
+        } else {
+            $additional_parameter = '';
+        }
+
+        // Check for mail header injection attacks.
+        $full_mail_content = join($this->getParam('crlf'), array($final_to, $this->_params['subject'], $final_body));
+        if (preg_match("/(^|[\n\r])(Content-Type|MIME-Version|Content-Transfer-Encoding|Bcc|Cc)\s*:/i", $full_mail_content)) {
+            $app->logMsg(sprintf('Mail header injection attack in content: %s', $full_mail_content), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Enter sandbox mode, if specified.
+        switch ($this->getParam('sandbox_mode')) {
+        case self::SANDBOX_MODE_REDIRECT:
+            if (!$this->getParam('sandbox_to_addr')) {
+                $app->logMsg(sprintf('Email sandbox_mode is SANDBOX_MODE_REDIRECT but sandbox_to_addr is not set.', null), LOG_ERR, __FILE__, __LINE__);
+                break;
+            }
+            $final_to = $this->getParam('sandbox_to_addr');
+            break;
+
+        case self::SANDBOX_MODE_STDERR:
+            file_put_contents('php://stderr', sprintf("Subject: %s\nTo: %s\n%s\n\n%s", $this->getParam('subject'), $final_to, str_replace($this->getParam('crlf'), "\n", $final_headers), $final_body), FILE_APPEND);
+            return true;
+
+        case self::SANDBOX_MODE_LOG:
+            $log_serialize = $app->getParam('log_serialize');
+            $app->setParam(array('log_serialize' => false));
+            $app->logMsg(sprintf("\nSubject: %s\nTo: %s\n%s\n\n%s", $this->getParam('subject'), $final_to, str_replace($this->getParam('crlf'), "\n", $final_headers), $final_body), LOG_DEBUG, __FILE__, __LINE__);
+            $app->setParam(array('log_serialize' => $log_serialize));
+            return true;
+        }
+
+        // Send email without 5th parameter if safemode is enabled.
+        if (ini_get('safe_mode')) {
+            $ret = mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers);
+        } else {
+            $ret = mb_send_mail($final_to, $this->_params['subject'], $final_body, $final_headers, $additional_parameter);
+        }
+
+        // Ensure message was successfully accepted for delivery.
+        if ($ret) {
+            $app->logMsg(sprintf('Email successfully sent to %s', $final_to), LOG_INFO, __FILE__, __LINE__);
+            return true;
+        } else {
+            $app->logMsg(sprintf('Email failure: %s, %s, %s, %s', $final_to, $this->_params['subject'], str_replace("\r\n", '\r\n', $final_headers), $additional_parameter), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Validates an email address based on the recommendations in RFC 3696.
+     * Is more loose than restrictive, to allow the many valid variants of
+     * email addresses while catching the most common mistakes. Checks an array too.
+     * http://www.faqs.org/rfcs/rfc822.html
+     * http://www.faqs.org/rfcs/rfc2822.html
+     * http://www.faqs.org/rfcs/rfc3696.html
+     * http://www.faqs.org/rfcs/rfc1035.html
+     *
+     * @access  public
+     * @param   mixed  $email  Address to check, string or array.
+     * @return  bool    Validity of address.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   30 Nov 2005 22:00:50
+     */
+    public function validEmail($email)
+    {
+        $app =& App::getInstance();
+
+        // If an array, check values recursively.
+        if (is_array($email)) {
+            foreach ($email as $e) {
+                if (!$this->validEmail($e)) {
+                    return false;
+                }
+            }
+            return true;
+        } else {
+            // To be valid email address must match regex and fit within the length constraints.
+            if (preg_match($this->getParam('regex'), $email, $e_parts) && mb_strlen($e_parts[2]) < 64 && mb_strlen($e_parts[3]) < 255) {
+                return true;
+            } else {
+                $app->logMsg(sprintf('Invalid email address: %s', $email), LOG_INFO, __FILE__, __LINE__);
+                return false;
+            }
+        }
+    }
+}
+
Index: tags/2.2.3/lib/FormValidator.inc.php
===================================================================
--- tags/2.2.3/lib/FormValidator.inc.php	(revision 632)
+++ tags/2.2.3/lib/FormValidator.inc.php	(revision 632)
@@ -0,0 +1,698 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * FormValidator.inc.php
+ *
+ * The FormValidator class provides a method for validating input from
+ * http requests and displaying errors.
+ *
+ * @requires  codebase/lib/Validator.inc.php
+ * @author    Quinn Comendant <quinn@strangecode.com>
+ * @version   1.8
+ *
+ * Example of use:
+---------------------------------------------------------------------
+// The object that validates form input.
+require_once 'codebase/lib/FormValidator.inc.php';
+$fv = new FormValidator();
+
+$fv->empty('field_name', sprintf(_("%s cannot be blank."), _("Field name")), MSG_ERR, __FILE__, __LINE__);
+$fv->stringLength('field_name', 0, 255, sprintf(_("%s must be %d-to-%d characters in length."), _("Field name"), 0, 255), MSG_ERR, __FILE__, __LINE__);
+$fv->isInteger('field_name', sprintf(_("%s must be an integer."), _("Field name")), MSG_ERR, __FILE__, __LINE__);
+$fv->checkRegex('field_name', '/^\d{4}$|^$/', true, sprintf(_("%s must be in MMYY format."), _("Field name")), MSG_ERR, __FILE__, __LINE__);
+$fv->numericRange('field_name', 0, 65535, sprintf(_("%s must be a number between %d and %d."), _("Field name"), 0, 65535), MSG_ERR, __FILE__, __LINE__);
+$fv->validatePhone('field_name', MSG_ERR, __FILE__, __LINE__);
+$fv->validateEmail('field_name', MSG_ERR, __FILE__, __LINE__);
+$fv->validateStrDate('field_name', sprintf(_("%s must be a valid date in YYYY-MM-DD format."), _("Field name")), MSG_ERR, __FILE__, __LINE__);
+if (is_null($var)) {
+    $fv->addError('field_name', sprintf(_("%s is invalid."), _("Field name")), MSG_ERR, __FILE__, __LINE__);
+}
+if ($fv->anyErrors()) {
+    // Errors!
+}
+---------------------------------------------------------------------
+ */
+
+// Credit card types are defined in class Validator.
+
+require_once 'codebase/lib/Validator.inc.php';
+
+class FormValidator
+{
+
+    // Class parameters.
+    protected $_params = array(
+        'error' => ' sc-msg-error ',
+        'warning' => ' sc-msg-warning ',
+        'notice' => ' sc-msg-notice ',
+        'success' => ' sc-msg-success ',
+    );
+
+    // Array filling with error messages.
+    public $errors = array();
+
+    /**
+    * FormValidator constructor.
+    *
+    * @access public
+    * @param array $params Configuration parameters for this object.
+    */
+    public function __construct($params=array())
+    {
+        // Set custom parameters.
+        $this->setParam($params);
+    }
+
+    /**
+    * Set (or overwrite existing) parameters by passing an array of new parameters.
+    *
+    * @access public
+    * @param  array    $params     Array of parameters (key => val pairs).
+    */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+    * Return the value of a parameter, if it exists.
+    *
+    * @access public
+    * @param string $param        Which parameter to return.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return mixed               Configured parameter value.
+    */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+    * Return the current list of errors.
+    *
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return array    an array of errors in the following arrangement:
+    *                  keys: the name of the variable with an error
+    *                  vals: the message to display for that error
+    */
+    public function getErrorList()
+    {
+        return $this->errors;
+    }
+
+    /**
+    * Add an error to the errors stack.
+    *
+    * @param   string $form_name   The name of the incoming form variable.
+    * @param   string $msg         The error message for that form.
+    * @param   int    $type        The type of message: MSG_NOTICE,
+    *                              MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
+    * @param   string $file        __FILE__.
+    * @param   string $line        __LINE__.
+    */
+    public function addError($form_name, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        $this->errors[] = array(
+            'name' => $form_name,
+            'message' => $msg,
+            'type' => $type,
+            'file' => $file,
+            'line' => $line
+        );
+    }
+
+    /**
+    * Check whether any errors have been triggered.
+    *
+    * @param  string $form_name the name of the incoming form variable
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool   true if any errors were found, or if found for
+    *                a variable of $form_name, false otherwise
+    */
+    public function anyErrors($form_name=null)
+    {
+        if (isset($form_name)) {
+            foreach ($this->errors as $err) {
+                if ($err['name'] == $form_name) {
+                    return $err['type'];
+                }
+            }
+            return false;
+        } else {
+            return (sizeof($this->errors) > 0);
+        }
+    }
+
+    /**
+    * Reset the error list.
+    */
+    public function resetErrorList()
+    {
+        $this->errors = array();
+    }
+
+    /**
+    * Prints the HTML for displaying error messages.
+    *
+    * @param   string  $above    Additional message to print above error messages (e.g. "Oops!").
+    * @param   string  $below    Additional message to print below error messages (e.g. "Please fix and resubmit").
+    * @param   string  $print_gotohash_js  Print a line of javascript that scrolls the browser window down to view any error messages.
+    * @param   string  $hash     The #hashtag to scroll to.
+    * @access  public
+    * @author  Quinn Comendant <quinn@strangecode.com>
+    * @since   15 Jul 2005 01:39:14
+    */
+    public function printErrorMessages($above='', $below='', $print_gotohash_js=false, $hash='sc-msg-formvalidator')
+    {
+        $app =& App::getInstance();
+        if ($this->anyErrors()) {
+            // data-gotohash="âŠ" is to be used by sites that refuse inline JS (via CSP) but needs to be referenced from an external JS.
+            ?><div class="sc-msg" id="sc-msg-formvalidator" data-gotohash="<?php echo oTxt($hash); ?>"><?php
+            if ('' != $above) {
+                ?><div class="sc-above"><?php echo oTxt($above); ?></div><?php
+            }
+            foreach ($this->getErrorList() as $e) {
+                if ('' != $e['message'] && is_string($e['message'])) {
+                    if (error_reporting() > 0 && $app->getParam('display_errors') && isset($e['file']) && isset($e['line'])) {
+                        echo "\n<!-- [" . $e['file'] . ' : ' . $e['line'] . '] -->';
+                    }
+                    switch ($e['type']) {
+                    case MSG_ERR:
+                        echo '<div data-alert class="sc-msg-error alert-box alert">' . $e['message'] . '<a href="#" class="close">&times;</a></div>';
+                        break;
+
+                    case MSG_WARNING:
+                        echo '<div data-alert class="sc-msg-warning alert-box warning">' . $e['message'] . '<a href="#" class="close">&times;</a></div>';
+                        break;
+
+                    case MSG_SUCCESS:
+                        echo '<div data-alert class="sc-msg-success alert-box alert">' . $e['message'] . '<a href="#" class="close">&times;</a></div>';
+                        break;
+
+                    case MSG_NOTICE:
+                    default:
+                        echo '<div data-alert class="sc-msg-notice alert-box info">' . $e['message'] . '<a href="#" class="close">&times;</a></div>';
+                        break;
+                    }
+                }
+            }
+            if ('' != $below) {
+                ?><div class="sc-below"><?php echo oTxt($below); ?></div><?php
+            }
+            ?></div><?php
+            if ($print_gotohash_js) {
+                ?>
+                <script type="text/javascript">
+                /* <![CDATA[ */
+                window.location.hash = '#<?php echo urlencode($hash); ?>';
+                /* ]]> */
+                </script>
+                <?php
+            }
+        }
+    }
+
+    /**
+    * If this form has an error, print an error marker like "<<".
+    *
+    * @param  string $form_name the name of the incoming form variable
+    * @param  string $marker    A string to print if there is an error. if
+    *                           not provided, use default.
+    */
+    public function err($form_name, $marker=null)
+    {
+        if (false !== ($type = $this->anyErrors($form_name))) {
+            if (isset($marker)) {
+                echo $marker;
+            } else {
+                switch ($type) {
+                case MSG_ERR:
+                default:
+                    echo $this->getParam('error');
+                    break;
+
+                case MSG_WARNING:
+                    echo $this->getParam('warning');
+                    break;
+
+                case MSG_NOTICE:
+                    echo $this->getParam('notice');
+                    break;
+
+                case MSG_SUCCESS:
+                    echo $this->getParam('success');
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+    * Ensure the length of string is non-zero.
+    *
+    * @param  string $form_name the name of the incoming form variable
+    * @param  string $msg       the message to display on error
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool   true if form is not empty, false otherwise.
+    */
+    public function notEmpty($form_name, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        if (Validator::notEmpty(getFormData($form_name), LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $this->addError($form_name, $msg, $type, $file, $line);
+            return false;
+        }
+    }
+
+    /*
+    * We were using the isEmpty method *wrong* for years and should have been using notEmpty because it is more grammatically correct.
+    * Because the only use is to ensure a value is not empty, we're simply going to alias this method to notEmpty().
+    * @since    03 Jun 2006 22:56:46
+    */
+    public function isEmpty($form_name, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        return $this->notEmpty($form_name, $msg, $type, $file, $line);
+    }
+
+    /**
+    * Check whether input is a string.
+    *
+    * @param  string $form_name the name of the incoming form variable
+    * @param  string $msg       the message to display on error
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool   true if form is a string, false otherwise.
+    */
+    public function isString($form_name, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        if (Validator::isString(getFormData($form_name), LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $this->addError($form_name, $msg, $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether input is a number. Allows negative numbers.
+    *
+    * @param  string $form_name the name of the incoming form variable
+    * @param  string $msg       the message to display on error
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool   true if no errors found, false otherwise
+    */
+    public function isNumber($form_name, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        if (Validator::isNumber(getFormData($form_name), LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $this->addError($form_name, $msg, $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * addError if input is NOT an integer. Don't just use is_int() because the
+    * data coming from the user is *really* a string.
+    *
+    * @param  string $form_name the name of the incoming form variable
+    * @param  string $msg       the message to display on error
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool   true if value is an integer
+    */
+    public function isInteger($form_name, $msg='', $negative_ok=false, $type=MSG_ERR, $file=null, $line=null)
+    {
+        if (Validator::isInteger(getFormData($form_name), $negative_ok, LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $this->addError($form_name, $msg, $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether input is a float. Don't just use is_float() because the
+    * data coming from the user is *really* a string. Integers will also
+    * pass this test.
+    *
+    * @param  string $form_name the name of the incoming form variable
+    * @param  string $msg       the message to display on error
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool   true if value is a float
+    */
+    public function isFloat($form_name, $msg='', $negative_ok=false, $type=MSG_ERR, $file=null, $line=null)
+    {
+        if (Validator::isFloat(getFormData($form_name), $negative_ok, LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $this->addError($form_name, $msg, $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether input is a Decimal or Fixed type. Use to check values to be stored in mysql decimal, numeric, num, or fixed types.
+    * The arguments $max and $dec should match M and D of the column definition "DECIMAL(M,D)".
+    * Note: some integers and floats will also pass this test.
+    * https://dev.mysql.com/doc/refman/5.5/en/fixed-point-types.html
+    *
+    * @param  string    $form_name      The name of the incoming form variable
+    * @param  int       $max            Total max number of digits.
+    * @param  int       $dec            Total max number of digits after the decimal place.
+    * @param  string    $msg            The message to display on error
+    * @param  bool      $negative_ok    If the value can be unsigned.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool                      True if value is a decimal, false otherwise.
+    */
+    public function isDecimal($form_name, $max=10, $dec=2, $negative_ok=false, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        if (Validator::isDecimal(getFormData($form_name), $max, $dec, $negative_ok, LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            // Set the example to a sequence of Ns with $max number of digits and a faction part length of $dec.
+            $msg = str_replace('{EX}', sprintf('%s.%s', str_repeat('N', $max - $dec), str_repeat('N', $dec)), $msg);
+            $this->addError($form_name, $msg, $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether input is an array.
+    *
+    * @param  string $form_name the name of the incoming form variable
+    * @param  string $msg       the message to display on error
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool   true if value is an array
+    */
+    public function isArray($form_name, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        if (Validator::isArray(getFormData($form_name), LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $this->addError($form_name, $msg, $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether input matches the specified perl regular expression
+    * pattern.
+    *
+    * @param  string $form_name        The name of the incoming form variable
+    * @param  int    $regex            Perl regex that the string must match
+    * @param  bool   $valid_on_match   Set to true to be valid if match, or false to be valid if the match fails.
+    * @param  string $msg              The message to display on error
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool   true if value passes regex test (or false if $valid_on_match=false)
+    */
+    public function checkRegex($form_name, $regex, $valid_on_match=true, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        if (Validator::checkRegex(getFormData($form_name), $regex, $valid_on_match, LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $this->addError($form_name, $msg, $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Tests if the string length is between specified values. Whitespace excluded for min.
+    *
+    * @param  string $form_name the name of the incoming form variable
+    * @param  int    $min       minimum length of string, inclusive
+    * @param  int    $max       maximum length of string, inclusive
+    * @param  string $msg       the message to display on error
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool   true if string length is within given boundaries
+    */
+    public function stringLength($form_name, $min, $max, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        if (Validator::stringLength(getFormData($form_name), $min, $max, LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $this->addError($form_name, $msg, $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether input is within a valid numeric range.
+    *
+    * @param  string $form_name the name of the incoming form variable
+    * @param  int    $min       minimum value of number, inclusive
+    * @param  int    $max       maximum value of number, inclusive
+    * @param  string $msg       the message to display on error
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool   true if no errors found, false otherwise
+    */
+    public function numericRange($form_name, $min, $max, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        if (Validator::numericRange(getFormData($form_name), $min, $max, LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $this->addError($form_name, $msg, $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Validates an email address based on the recommendations in RFC 3696.
+    * Is more loose than restrictive, to allow the many valid variants of
+    * email addresses while catching the most common mistakes.
+    * http://www.faqs.org/rfcs/rfc822.html
+    * http://www.faqs.org/rfcs/rfc2822.html
+    * http://www.faqs.org/rfcs/rfc3696.html
+    * http://www.faqs.org/rfcs/rfc1035.html
+    *
+    * @access  public
+    * @param   string  $form_name   The name of the incoming form variable.
+    * @param   bool    $strict      Run strict tests (check if the domain exists and has an MX record assigned)
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return  bool                 Validity of address.
+    * @author  Quinn Comendant <quinn@strangecode.com>
+    */
+    public function validateEmail($form_name, $strict=false, $type=MSG_ERR, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+
+        $email = getFormData($form_name);
+
+        if ('' == trim($email)) {
+            // No email address provided, and that's okay.
+            return true;
+        }
+
+        // Validator::validateEmail() returns a value that relates to the Validate::EMAIL_* constants (defined in Validator.inc.php).
+        switch (Validator::validateEmail($email, $strict, LOG_NOTICE, $file, $line)) {
+        case Validator::EMAIL_REGEX_FAIL:
+            // Failed regex match.
+            $this->addError($form_name, sprintf(_("The email address <em>%s</em> is formatted incorrectly."), oTxt($email)), $type, $file, $line);
+            $app->logMsg(sprintf('The email address %s is not valid.', oTxt($email)), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+
+        case Validator::EMAIL_LENGTH_FAIL:
+            // Failed length requirements.
+            $this->addError($form_name, sprintf(_("The email address <em>%s</em> is too long (email addresses must have fewer than 256 characters)."), oTxt($email)), $type, $file, $line);
+            $app->logMsg(sprintf('The email address %s must contain less than 256 characters.', oTxt($email)), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+
+        case Validator::EMAIL_MX_FAIL:
+            // Failed MX record test.
+            $this->addError($form_name, sprintf(_("The email address <em>%s</em> does not have a valid domain name"), oTxt($email)), $type, $file, $line);
+            $app->logMsg(sprintf('The email address %s does not have a valid domain name.', oTxt($email)), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+
+        case Validator::EMAIL_SUCCESS:
+        default :
+            return true;
+        }
+    }
+
+    /**
+    * Check whether input is a valid phone number. Notice: it is now set
+    * to allow characters like - or () or + so people can type in a phone
+    * number that looks like: +1 (530) 555-1212
+    *
+    * @param  string  $form_name the name of the incoming form variable
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool    true if no errors found, false otherwise
+    */
+    public function validatePhone($form_name, $type=MSG_ERR, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+
+        $phone = getFormData($form_name);
+
+        // Validator::validateEmail() returns a value that relates to the Validate::PHONE_* constants (defined in Validator.inc.php).
+        switch (Validator::validatePhone($phone, LOG_NOTICE, $file, $line)) {
+        case Validator::PHONE_REGEX_FAIL:
+            // Failed regex match.
+            $this->addError($form_name, sprintf(_("The phone number <em>%s</em> is not valid."), oTxt($phone)), $type, $file, $line);
+            $app->logMsg(sprintf('The phone number %s is not valid.', oTxt($phone)), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+
+        case Validator::PHONE_LENGTH_FAIL:
+            // Failed length requirements.
+            $this->addError($form_name, sprintf(_("The phone number <em>%s</em> is too long (phone number must have fewer than 25 characters)."), oTxt($phone)), $type, $file, $line);
+            $app->logMsg(sprintf('The phone number %s must contain less than 25 characters.', oTxt($phone)), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+
+        case Validator::PHONE_SUCCESS:
+        default :
+            return true;
+        }
+    }
+
+    /**
+    * Verifies that date can be processed by the strtotime function.
+    *
+    * @param  string  $form_name the name of the incoming form variable
+    * @param  string  $msg       the message to display on error
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool    true if no errors found, false otherwise
+    */
+    public function validateStrDate($form_name, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+
+        if (Validator::validateStrDate(getFormData($form_name, ''), LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $this->addError($form_name, $msg, $type, $file, $line);
+            $app->logMsg(sprintf('The string date %s is not valid.', getFormData($form_name)), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+
+    /**
+    * Verifies credit card number using the Luhn (mod 10) algorithm.
+    * http://en.wikipedia.org/wiki/Luhn_algorithm
+    *
+    * @param  string  $form_name   The name of the incoming form variable.
+    * @param  string  $cc_type     Optional, card type to do specific checks. One of the Validator::CC_TYPE_* constants.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool    true if no errors found, false otherwise
+    */
+    public function validateCCNumber($form_name, $cc_type=null, $type=MSG_ERR, $file=null, $line=null)
+    {
+        $cc_num = getFormData($form_name);
+
+        if (Validator::validateCCNumber($cc_num, $cc_type, LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $this->addError($form_name, sprintf(_("The credit card number you entered is not valid. Please check the number and try again."), $cc_num), $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether a file was selected for uploading. If file is missing, it's an error.
+    *
+    * @param  string $form_name the name of the incoming form variable
+    * @param  string $msg       the message to display on error
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool   true if no errors found, false otherwise
+    */
+    public function fileUploaded($form_name, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        if (Validator::fileUploaded($form_name, LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $this->addError($form_name, $msg, $type, $file, $line);
+            return false;
+        }
+    }
+    /**
+    * Check whether a file was selected for uploading. If file is missing, it's an error.
+    *
+    * @param  string $form_name the name of the incoming form variable
+    * @param  string $msg       the message to display on error
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  const  $file  Filename to log (usually __FILE__)
+    * @param  const  $line  Line number to log (usually __LINE__)
+    * @return bool   true if no errors found, false otherwise
+    */
+    public function fileUploadSize($form_name, $msg='', $type=MSG_ERR, $file=null, $line=null)
+    {
+        if (Validator::fileUploadSize($form_name, LOG_NOTICE, $file, $line)) {
+            return true;
+        } else {
+            $msg = '' == $msg ? sprintf(_("Maximum filesize exceeded. Got %s, but limit is %s."), humanFileSize($_SERVER['CONTENT_LENGTH']), humanFileSize(phpIniGetBytes('upload_max_filesize'))) : $msg;
+            $this->addError($form_name, $msg, $type, $file, $line);
+            return false;
+        }
+    }
+
+} // THE END
+
Index: tags/2.2.3/lib/HTML.inc.php
===================================================================
--- tags/2.2.3/lib/HTML.inc.php	(revision 632)
+++ tags/2.2.3/lib/HTML.inc.php	(revision 632)
@@ -0,0 +1,271 @@
+<?php
+/*
+* The Strangecode Codebase - a general application development framework for PHP
+* For details visit the project site: <http://trac.strangecode.com/>
+* Copyright Â© 2014 Strangecode, LLC
+*
+* This program is free software: you can redistribute it and/or modify
+* it under the terms of the GNU General Public License as published by
+* the Free Software Foundation, either version 3 of the License, or
+* (at your option) any later version.
+*
+* This program is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+* GNU General Public License for more details.
+*
+* You should have received a copy of the GNU General Public License
+* along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/*
+* HTML()
+*
+* Tools for building HTML from PHP data.
+*
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    11 Sep 2014 21:08:06
+*
+* Example of use:
+---------------------------------------------------------------------
+echo HTML::buttons(array(
+    array('name' => 'submit', 'value' => _("Save changes"), 'class' => 'small button', 'accesskey' => 's'),
+    array('name' => 'reset', 'value' => _("Reset"), 'class' => 'small button secondary', 'accesskey' => 'r'),
+    array('name' => 'cancel', 'value' => _("Cancel"), 'class' => 'small button secondary', 'accesskey' => 'c'),
+));
+---------------------------------------------------------------------
+*/
+
+class HTML
+{
+
+    // Browsers add names and ids of form controls as properties to the FORM. This results in the properties of the form being replaced.
+    // Use this list to warn the programmer if he uses an unsafe name.
+    // http://jibbering.com/faq/names/unsafe_names.html
+    static $unsafe_form_control_names = array('accept','acceptCharset','action','addBehavior','addEventListener','addEventSource','addRepetitionBlock','addRepetitionBlockByIndex','all','appendChild','applyElement','ariaBusy','ariaChecked','ariaControls','ariaDescribability','ariaDisabled','ariaExpanded','ariaFlowto','ariaHaspopup','ariaHidden','ariaInvalid','ariaLabelledby','ariaLevel','ariaMultiselect','ariaOwns','ariaPosinset','ariaPressed','ariaReadonly','ariaRequired','ariaSecret','ariaSelected','ariaSetsize','ariaValuemax','ariaValuemin','ariaValuenow','attachEvent','attributes','ATTRIBUTE_NODE','autocomplete','baseURI','behaviorUrns','blockDiraction','blur','canHaveChildren','canHaveHTML','CDATA_SECTION_NODE','checkValidity','childElementCount','childNodes','children','className','clearAttributes','click','clientHeight','clientLeft','clientTop','clientWidth','cloneNode','COMMENT_NODE','compareDocumentPosition','componentFromPoint','constructor','contains','contentEditable','currentStyle','data','detachEvent','dir','dispatchEvent','dispatchFormChange','dispatchFormInput','document','DOCUMENT_FRAGMENT_NODE','DOCUMENT_NODE','DOCUMENT_POSITION_CONTAINED_BY','DOCUMENT_POSITION_CONTAINS','DOCUMENT_POSITION_DISCONNECTED','DOCUMENT_POSITION_FOLLOWING','DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC','DOCUMENT_POSITION_PRECEDING','DOCUMENT_TYPE_NODE','dragDrop','elements','ELEMENT_NODE','encoding','enctype','ENTITY_NODE','ENTITY_REFERENCE_NODE','fireEvent','firstChild','firstElementChild','focus','getAdjacentText','getAttribute','getAttributeNode','getAttributeNodeNS','getAttributeNS','getBoundingClientRect','getClientRects','getElementsByClassName','getElementsByTagName','getElementsByTagNameNS','getExpression','getFeature','getUserData','hasAttribute','hasAttributeNS','hasAttributes','hasChildNodes','hasOwnProperty','hideFocus','id','innerHTML','innerText','insertAdjacentElement','insertAdjacentHTML','insertAdjacentText','insertBefore','isContentEditable','isDefaultNamespace','isDefaultNamespaceURI','isDisabled','isEqualNode','isMultiLine','isPrototypeOf','isSameNode','isSupported','isTextEdit','item','lang','language','lastChild','lastElementChild','length','localName',
+    'lookupPrefix','mergeAttributes','method','moveRepetitionBlock','msBlockProgression','msBoxSizing','name','namedItem','namespaceURI','nextSibling','nodeName','nodeType','nodeValue','normalize','NOTATION_NODE','offsetHeight','offsetWidth','onabort','onactivate','onbeforeactivate','onbeforecopy','onbeforecut','onbeforedeactivate','onbeforeeditfocus','onbeforepaste','onblur','onchage','onclick','onclosecapture','oncontextmenu','oncopy','oncut','ondblclick','ondeactivate','ondrag','ondragend','ondragenter','ondragleave','ondragover','onerror','onfocus','onfocusin','onfocusout','onhelp','oninput','onkeydown','onkeypress','onkeyup','onmousedown','onmouseenter','onmouseleave','onmousemove','onmousemultiwheel','onmouseout','onmouseover','onmouseup','onmousewheel','onmove','onmoveend','onmovestart','onOffBehavior','onpaste','onpropertychange','onreadystatechange','onresize','onresizeend','onresizestart','onscroll','onsearch','onselect','onselectstart','ontimeerror','onunload','outerHTML','outerText','ownerDocument','parentNode','parentTextEdit','prefix','previousElementSibling','previousSibling','PROCESSING_INSTRUCTION_NODE','propertyIsEnumerable','querySelector','querySelectorAll','quotes','releaseCapture','removeAttribute','removeAttributeNode','removeAttributeNS','removeBehavior','removeChild','removeEventListener','removeEventSource','removeExpression','removeNode','removeRepetitionBlock','repeatMax','repeatMin','repeatStart','repetitionBlocks','repetitionIndex','repetitionTemplate','repetitionType','replace','replaceAdjacentText','replaceChild','replaceNode','reset','resetFromData','role','runtimeStyle','schemaTypeInfo','scopeName','scrollByLines','scrollByPages','scrollHeight','scrollIntoView','scrollLeft','scrollTop','scrollWidth','selectNodes','selectSingleNode','setActive','setAttributeNode','setAttributeNodeNS','setAttributeNS','setCapture','setExpression','setIdAttribute','setIdAttributeNode','setIdAttributeNS','setUserData','sourceIndex','spellcheck','style','submit','swapNode','tabIndex','tagName','tagUrn','target','templateElements','text','textContent','TEXT_NODE','title','toLocaleString','toString','uniqueID','unselectable','unwatch','urns','valueOf','watch','window');
+
+    /**
+    * Prints submit buttons based on given array of submit button names and titles. If the array includes an 'href' key, the
+    * button is created using a <a> otherwise an <input type="âŠ" /> is used.
+    *
+    * @access  public
+    * @param   array   $buttons     Array of buttons, the key being the button name, and value being the title of the button.
+    * @return  void
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    12 Sep 2014 00:17:38
+    */
+    static public function printButtons($buttons=array(), $class='button-group')
+    {
+        $app =& App::getInstance();
+
+        if (!isset($buttons[0]) || !is_array($buttons[0])) {
+            $app =& App::getInstance();
+            $app->logMsg(sprintf('Incorrect parameters passed to HTML::buttons(): %s', getDump($buttons)), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+        if (empty($buttons)) {
+            return '';
+        }
+        ?><ul class="<?php echo oTxt($class) ?>"><?php
+        foreach ($buttons as $i => $b) {
+            $defaults = array();
+            $defaults['type'] = isset($b['type']) ? $b['type'] : 'submit';
+            $b = array_merge($defaults, $b);
+            if (isset($b['href'])) {
+                echo '<li><a';
+                foreach (array_diff_key($b, array('value' => null)) as $key => $value) {
+                    printf(' %s="%s"', $key, oTxt($value));
+                }
+                echo '>' . oTxt($b['value']) . '</a></li>';
+            } else if (isset($b['name'])) {
+                if (in_array($b['name'], self::$unsafe_form_control_names)) {
+                    $app->logMsg(sprintf('Unsafe form control name: %s', $b['name']), LOG_NOTICE, __FILE__, __LINE__);
+                }
+                $defaults['id'] = isset($b['id']) ? $b['id'] : sprintf('sc-%s-button', $b['name']);
+                echo '<li><input';
+                foreach ($b as $key => $value) {
+                    printf(' %s="%s"', $key, oTxt($value));
+                }
+                echo ' /></li>';
+            } else {
+                $app->logMsg(sprintf('Button missing name or href: %s', getDump($b)), LOG_ERR, __FILE__, __LINE__);
+                continue;
+            }
+        }
+        ?></ul><?php
+    }
+
+    /*
+    * Return an array of key-value pairs matching a database query for the given parameters. This is useful for
+    * injecting into HTML::printSelectOptions().
+    *
+    * @access   public
+    * @param  string $db_table         database table to lookup
+    * @param  string $key_column       column containing the human-readable titles for the select menu
+    * @param  string $val_column       column containing the computer values for the select menu
+    * @param  string $preselected      the currently selected value of the menu. compared to the $val_column
+    * @param  bool   $first            Optional first item; set true for a blank item, array for item with name and value.
+    * @param  string $extra_clause     SQL exclude clause. Something like "WHERE girls != 'buckteeth'"
+    * @return array                    Array of options suitable to pass into HTML::printSelectOptions().
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    12 Sep 2014 11:43:23
+    */
+    static public function getSelectOptions($db_table, $key_column, $val_column, $preselected, $first=false, $extra_clause='', $sql_format='SELECT %1$s, %2$s FROM %3$s %4$s')
+    {
+        $db =& DB::getInstance();
+
+        // Sometimes preselected comes as a comma list.
+        if (!is_array($preselected)) {
+            $preselected = array($preselected);
+        }
+
+        $options = array();
+        if (true === $first) {
+            // Include a blank first option.
+            $options[] = array(
+                'value' => '',
+                'selected' => in_array('', $preselected),
+                'text' => '',
+            );
+        } else if (is_array($first)) {
+            // When the 'blank' first option needs a specific key->val pair.
+            foreach ($first as $key => $val) {
+                $options[] = array(
+                    'value' => $key,
+                    'selected' => in_array($key, $preselected),
+                    'text' => $val,
+                );
+            }
+        }
+
+        $db =& DB::getInstance();
+        $qid = $db->query(sprintf($sql_format, $key_column, $val_column, $db_table, $extra_clause), false);
+        while ($row = mysql_fetch_assoc($qid)) {
+            $options[] = array(
+                'value' => $row[$val_column],
+                'selected' => in_array($row[$val_column], $preselected),
+                'text' => $row[$key_column],
+            );
+        }
+        return $options;
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param    array   $options    Array of options, with keys: value, selected, text
+    * @param    array   $deselected Array of values to disable in options.
+    * @return   array               Same options, but those with a value matching an element in $deselected will have a 'disabled' element of true.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    24 Jul 2015 01:41:33
+    */
+    static public function disableSelectOptions($options, $deselected)
+    {
+        $app =& App::getInstance();
+        $n = sizeof($options);
+        for ($i=0; $i < $n; $i++) {
+            $app->logMsg(sprintf('Disable check: %s == %s', $options[$i]['value'], getDump($deselected)), LOG_DEBUG, __FILE__, __LINE__);
+            $options[$i]['disabled'] = in_array($options[$i]['value'], $deselected);
+        }
+        return $options;
+    }
+
+    /**
+     * Get an array of option fields for a select form. Works only with enum or set
+     * data types in table columns.
+     *
+     * @param  string $db_table     Database table to lookup
+     * @param  string $db_col       Database column to lookup
+     * @param  string $preselected  The currently selected value of the menu. compared to the $val_column
+     * @param  bool   $first        Optional first item; set true for a blank item, array for item with name and value.
+     * @param  bool   $sort         Sort the output.
+     */
+    static public function getSelectOptionsEnum($db_table, $db_col, $preselected, $first=false, $sort=false)
+    {
+        // Sometimes preselected comes as a comma list.
+        if (!is_array($preselected)) {
+            $preselected = array($preselected);
+        }
+
+        $options = array();
+        if (true === $first) {
+            // Include a blank first option.
+            $options[] = array(
+                'value' => '',
+                'selected' => in_array('', $preselected),
+                'text' => '',
+            );
+        } else if (is_array($first)) {
+            // When the 'blank' first option needs a specific key->val pair.
+            foreach ($first as $key => $val) {
+                $options[] = array(
+                    'value' => $key,
+                    'selected' => in_array($key, $preselected),
+                    'text' => $val,
+                );
+            }
+        }
+
+        $db =& DB::getInstance();
+        $values = $db->getEnumValues($db_table, $db_col, $sort);
+        foreach ($values as $v) {
+            $options[] = array(
+                'value' => $v,
+                'selected' => in_array($v, $preselected),
+                'text' => $v,
+            );
+        }
+
+        return $options;
+    }
+
+    /**
+     * Prints select menu options with the specified array of keys and values.
+     *
+     */
+    static public function printSelectOptions($options)
+    {
+        if (!isset($options) || !is_array($options)) {
+            $app =& App::getInstance();
+            $app->logMsg(sprintf('Incorrect parameters passed to HTML::printSelectOptions(): %s', getDump($options)), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+        if (empty($options)) {
+            return '';
+        }
+
+        foreach ($options as $o) {
+            printf('<option value="%s"%s%s>%s</option>',
+                oTxt($o['value']),
+                (isset($o['selected']) && $o['selected'] ? ' selected' : ''),
+                (isset($o['disabled']) && $o['disabled'] ? ' disabled' : ''),
+                oTxt($o['text'])
+            );
+        }
+    }
+
+    /**
+     * Get a Gravatar URL for a specified email address.
+     *
+     * @param string $email The email address
+     * @param string $size Size in pixels, defaults to 80px [ 1 - 2048 ]
+     * @param string $d Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ]
+     * @param string $r Maximum rating (inclusive) [ g | pg | r | x ]
+     * @return String containing a URL to a gravatar image.
+     * @source http://gravatar.com/site/implement/images/php/
+     */
+    static public function getGravatarURL($email, $size=80, $defset='mm', $rating='g') {
+        return sprintf('https://www.gravatar.com/avatar/%s?s=%s&d=%s&r=%s',
+            md5(strtolower(trim($email))),
+            $size,
+            $defset,
+            $rating
+        );
+    }
+}
Index: tags/2.2.3/lib/Hierarchy.inc.php
===================================================================
--- tags/2.2.3/lib/Hierarchy.inc.php	(revision 632)
+++ tags/2.2.3/lib/Hierarchy.inc.php	(revision 632)
@@ -0,0 +1,850 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Hierarchy.inc.php
+ *
+ * Objective: This class provides the tools to organize pieces of data into a
+ * hierarchy of nodes. Any form of data (article, product, image) can be
+ * represented as a node in this hierarchy. This class does not manipulate the
+ * data, nor is it involved in storing or retrieving the data. In fact it does
+ * not access the tables where data exists and cannot find out info about the
+ * data. You must provide identification of a piece of data (type and ID) to
+ * insert it into the hierarchy. The node hierarchy is completely
+ * separate from data storage and retrieval. You must separately store the data
+ * using whatever logic is specific to the data then also call these functions.
+ * Nodes are not the data. The nodes are mere singularities in virtual space
+ * that represent a piece of data's relationship with another. The hierarchy
+ * is an inverted tree structure. Each node can have virtually infinite
+ * children. Each child can have multiple parents.
+ *
+ * @author    Quinn Comendant <quinn@strangecode.com>
+ * @version   1.1 (07 Apr 2006 20:16:59)
+ */
+
+class Hierarchy
+{
+
+    /**
+     * Array containing object parameters.
+     * @var array $params
+     *
+     *  $params['child_type'] = 'article'
+     *  $params['child_id'] = 23
+     *
+     */
+    public $params = array();
+
+
+    /**
+     * The active node type.
+     * @var string $child_type
+     */
+    public $child_type;
+
+    /**
+     * The active node id.
+     * @var string $child_id
+     */
+    public $child_id;
+
+    /**
+     * Boolean indicating whether or not we've set
+     * the 'active' node type and id.
+     * @var bool $node_init
+     */
+    public $node_init = false;
+
+    /**
+     * Constructor
+     * @param resource  $dbh    A database handler if we are already connected.
+     * @param array     $params A hash containing any additional
+     *                          configuration or connection parameters.
+     */
+    public function __construct($params=array())
+    {
+        $this->params = $params;
+    }
+
+    /**
+     * Defines the default child_type and child_id for this object.
+     * @child_type string   this node's type
+     * @child_id string     this node's id
+     * @return string       the previous node identifier, or the current one
+     *                      if no new ones are specified.
+     */
+    public function currentNode($child_type=null, $child_id=null)
+    {
+        $old_type = isset($this->child_type) ? $this->child_type : $child_type;
+        $old_id = isset($this->child_id) ? $this->child_id : $child_id;
+        if (isset($child_type) && isset($child_id)) {
+            $this->child_type = $child_type;
+            $this->child_id = $child_id;
+            $this->node_init = true;
+        }
+        return $this->toStringID($old_type, $old_id);
+    }
+
+    /**
+     * Takes a node type and id and returns them as a serialized identifier like
+     * article__24 or category__84. If the first argument is an array, and the
+     * second is -1, we loop through the array and serialize the identifiers inside.
+     * @param  mixed    $child_type
+     * @param  string   $child_id
+     * @return mixed    If a single type and id are provided, a single vector identifier is returned,
+     *                  otherwise if an array is provided, an array of identifiers is returned.
+     */
+    public function toStringID($child_type=null, $child_id=null)
+    {
+            $app =& App::getInstance();
+
+            if (!isset($child_type) || !isset($child_id)) {
+            if ($this->node_init) {
+                $child_type =& $this->child_type;
+                $child_id =& $this->child_id;
+            } else {
+                $app->logMsg('toStringID failed. Arguments not specified properly.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+        if (is_array($child_type) && $child_id === -1) {
+            // We take an array in the first argument. It really has nothing
+            // to do with $child_type.
+            if (!isset($child_type[0]['child_type'])) {
+                // It's not a properly configured array.
+                return false;
+            }
+            foreach ($child_type as $node) {
+                $serialized_arr[] = $node['child_type'] . '__' . $node['child_id'];
+            }
+            return $serialized_arr;
+        } else {
+            return $child_type . '__' . $child_id;
+        }
+    }
+
+    /**
+     * Takes a singular node identifier and returns it as components of an array.
+     * @param string    $node
+     * @return mixed    Array of node type and id on success, false on failure.
+     */
+    public function toArrayID(&$node)
+    {
+            $app =& App::getInstance();
+
+            if (preg_match('/^([[:alnum:]]+)__-?([[:digit:]]+)$/', $node, $node_parts)) {
+            return array('node_type' => $node_parts[1], 'node_id' => $node_parts[2]);
+        } else {
+            $app->logMsg('Cannot parse node identifier, not formated correctly.', LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Adds a single node. Tests if it already exists first.
+     * @param string    $child_type
+     * @param string    $child_id
+     * @param string    $parents    A serialized array of serialized parent identifiers
+     * @param string    $relationship_type
+     * @return bool     true on success, false on error.
+     */
+    public function insertNode($parents, $child_type=null, $child_id=null, $relationship_type=null, $title='')
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        if (!isset($child_type) || !isset($child_id)) {
+            if ($this->node_init) {
+                $child_type =& $this->child_type;
+                $child_id =& $this->child_id;
+            } else {
+                $app->logMsg('insertNode failed. Arguments not specified properly.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        // Make sure this is not empty and an array, even if it has only one value.
+        if ('' == $parents) {
+            $app->raiseMsg(sprintf(_("Cannot add node <em>%s %s</em>, no parent was specified."), $child_type, $child_id), MSG_ERR, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Cannot add node %s %s, no parent was specified.', $child_type, $child_id), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        } else if (!is_array($parents)) {
+            $parents = array($parents);
+        }
+
+        // Remove duplicates.
+        $parents = array_unique($parents);
+
+        // Test that this node does not already exist and that the new parents
+        // do exist before we continue.
+        foreach ($parents as $parent_string) {
+            $parent = $this->toArrayID($parent_string);
+            if ($this->nodeExists($child_type, $child_id, $parent['node_type'], $parent['node_id'], $relationship_type)) {
+                $app->raiseMsg(sprintf(_("Cannot add node <em>%s %s</em> to parent <em>%s %s</em>. It already exists there"), $child_type, $child_id, $parent['node_type'], $parent['node_id']), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('Cannot add node %s %s to parent %s %s. It already exists there', $child_type, $child_id, $parent['node_type'], $parent['node_id']), LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+            if (!$this->nodeExists($parent['node_type'], $parent['node_id'])) {
+                $app->raiseMsg(sprintf(_("Cannot add node <em>%s %s</em> to nonexistent parent <em>%s %s</em>."), $child_type, $child_id, $parent['node_type'], $parent['node_id']), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('Cannot add node %s %s to nonexistent parent %s %s.', $child_type, $child_id, $parent['node_type'], $parent['node_id']), LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        // Insert new nodes with the new parents.
+        foreach ($parents as $parent_string) {
+            $parent = $this->toArrayID($parent_string);
+            $db->query("
+                INSERT INTO node_tbl (
+                    parent_type,
+                    parent_id,
+                    child_type,
+                    child_id,
+                    relationship_type,
+                    title
+                ) VALUES (
+                    '" . $db->escapeString($parent['node_type']) . "',
+                    '" . $db->escapeString($parent['node_id']) . "',
+                    '" . $db->escapeString($child_type) . "',
+                    '" . $db->escapeString($child_id) . "',
+                    " . (is_null($relationship_type) ? "NULL" : "'" . $db->escapeString($relationship_type) . "'") . ",
+                    '" . $db->escapeString($title) . "'
+                )
+            ");
+            $app->logMsg(sprintf('insertNode: Added node %s %s with parent %s %s.', $child_type, $child_id, $parent['node_type'], $parent['node_id']), LOG_INFO, __FILE__, __LINE__);
+        }
+        return true;
+    }
+
+    /**
+     * Deletes a node using the following algorithm:
+     * (1) Find each of this node's parents. Place serialized identifiers in array.
+     * (2) Find all children of this node, and move each to all the parents of this node.
+     * (3) Delete this node.
+     *
+     * @param  string    $child_type
+     * @param  string    $child_id
+     *
+     * @return bool      false on error, true otherwise.
+     */
+    public function deleteNode($child_type=null, $child_id=null)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        if (!isset($child_type) || !isset($child_id)) {
+            if ($this->node_init) {
+                $child_type =& $this->child_type;
+                $child_id =& $this->child_id;
+            } else {
+                $app->logMsg('deleteNode failed. Arguments not specified properly.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        // Move children to parents of node being deleted.
+        $new_parents = $this->getParents($child_type, $child_id);
+        $children = $this->getChildren($child_type, $child_id);
+
+        if ($children && $new_parents) {
+            foreach ($children as $child) {
+                $this->moveNode($new_parents, $child['child_type'], $child['child_id']);
+            }
+        }
+
+        $db->query("
+            DELETE FROM node_tbl
+            WHERE child_type = '" . $db->escapeString($child_type) . "'
+            AND child_id = '" . $db->escapeString($child_id) . "'
+        ");
+        $app->logMsg(sprintf('deleteNode: Deleted node %s %s.', $child_type, $child_id), LOG_INFO, __FILE__, __LINE__);
+
+        return true;
+    }
+
+    /**
+     * Moves a node using the following algorithm:
+     * (1) Ensure the newly assigned parents exist.
+     * (2) Delete old nodes with the specified type and id.
+     * (3) Add the node with new parents.
+     *
+     * @param  string    $child_type
+     * @param  string    $child_id
+     * @param  array     $new_parents  An array of serialized node identifiers
+     *                                 that the node will belong to. Example:
+     *                                 $new_parents[0] = 'category__23';
+     * @param  string    $relationship_type
+     *
+     * @return bool      false on error, true otherwise.
+     */
+    public function moveNode($new_parents=null, $child_type=null, $child_id=null, $relationship_type=null, $title='')
+    {
+            $app =& App::getInstance();
+            $db =& DB::getInstance();
+
+            if (!isset($child_type) || !isset($child_id)) {
+            if ($this->node_init) {
+                $child_type =& $this->child_type;
+                $child_id =& $this->child_id;
+            } else {
+                $app->logMsg('moveNode failed. Arguments not specified properly.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        // Make sure this is not empty and an array, even if it has only one value.
+        if (empty($new_parents)) {
+            $app->raiseMsg(sprintf(_("Cannot move node <em>%s %s</em>, no parent was specified."), $child_type, $child_id), MSG_ERR, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Cannot move node %s %s, no parent was specified.', $child_type, $child_id), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        } else if (!is_array($new_parents)) {
+            $new_parents = array($new_parents);
+        }
+
+        // Remove duplicates.
+        $new_parents = array_unique($new_parents);
+
+        // Test that the new parents exist before we delete the old ones.
+        foreach ($new_parents as $parent_string) {
+            $parent = $this->toArrayID($parent_string);
+            if (!$this->nodeExists($parent['node_type'], $parent['node_id'])) {
+                $app->raiseMsg(sprintf(_("Cannot move node <em>%s %s</em> to nonexistent parent <em>%s %s</em>."), $child_type, $child_id, $parent['node_type'], $parent['node_id']), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('Cannot move node %s %s to nonexistent parent %s %s.', $child_type, $child_id, $parent['node_type'], $parent['node_id']), LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+            if ($this->isAncestor($child_type, $child_id, $parent['node_type'], $parent['node_id'])) {
+                $app->raiseMsg(sprintf(_("Cannot move node <em>%s %s</em> to parent <em>%s %s</em> because a node cannot have itself as a parent."), $child_type, $child_id, $parent['node_type'], $parent['node_id']), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('Cannot move node %s %s to parent %s %s because a node cannot have itself as a parent.', $child_type, $child_id, $parent['node_type'], $parent['node_id']), LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        if (empty($title)) {
+            // Select the title of the node we are moving, so we can add it again with the same info.
+            $qid = $db->query("
+                SELECT title FROM node_tbl
+                WHERE child_type = '" . $db->escapeString($child_type) . "'
+                AND child_id = '" . $db->escapeString($child_id) . "'
+                AND relationship_type " . (is_null($relationship_type) ? "IS NULL" : "= '" . $db->escapeString($relationship_type) . "'") . "
+            ");
+            list($title) = mysql_fetch_row($qid);
+        }
+
+        // Delete the nodes with the old parents.
+        $db->query("
+            DELETE FROM node_tbl
+            WHERE child_type = '" . $db->escapeString($child_type) . "'
+            AND child_id = '" . $db->escapeString($child_id) . "'
+            AND relationship_type " . (is_null($relationship_type) ? "IS NULL" : "= '" . $db->escapeString($relationship_type) . "'") . "
+        ");
+        $app->logMsg(sprintf('moveNode: Deleted node %s %s.', $child_type, $child_id), LOG_INFO, __FILE__, __LINE__);
+
+        // Insert new nodes with the new parents.
+        $this->insertNode($new_parents, $child_type, $child_id, $relationship_type, $title);
+
+        return true;
+    }
+
+    /**
+     * Returns an array of all the parents of the current node (just the ones
+     * immediately above this node). You may need to call array_unique if you
+     * don't want duplicate nodes returned.
+     *
+     * @param  string   $child_type
+     * @param  string   $child_id
+     * @return string   The parents as an array of serialized node identifiers.
+     */
+    public function getParents($child_type=null, $child_id=null, $type_constraint=null, $order='')
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        if (!isset($child_type) || !isset($child_id)) {
+            if ($this->node_init) {
+                $child_type =& $this->child_type;
+                $child_id =& $this->child_id;
+            } else {
+                $app->logMsg('getParents failed. Arguments not specified properly.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        $in_clause = '';
+        if (isset($type_constraint)) {
+            if (!is_array($type_constraint)) {
+                $type_constraint = array($type_constraint);
+            }
+            $in_clause = "AND parent_type IN ('" . join("','", array_map(array($db, 'escapeString'), $type_constraint)) . "')";
+        }
+
+        $qid = $db->query("
+            SELECT parent_type, parent_id
+            FROM node_tbl
+            WHERE child_type = '" . $db->escapeString($child_type) . "'
+            AND child_id = '" . $db->escapeString($child_id) . "'
+            $in_clause
+            " . $db->escapeString($order) . "
+        ");
+
+        $parents = array();
+        while ($row = mysql_fetch_assoc($qid)) {
+            $parents[] = $this->toStringID($row['parent_type'], $row['parent_id']);
+        }
+
+        if (sizeof($parents) > 0) {
+            return $parents;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns details of specified node.
+     *
+     * @param  string  $child_type
+     * @param  string  $child_id
+     * @return array   type, id, title, subnode_quantity.
+     */
+    public function getNode($child_type=null, $child_id=null)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        if (!isset($child_type) || !isset($child_id)) {
+            if ($this->node_init) {
+                $child_type =& $this->child_type;
+                $child_id =& $this->child_id;
+            } else {
+                $app->logMsg('getNode failed. Arguments not specified properly.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        $qid = $db->query("
+            SELECT child_type, child_id, title, subnode_quantity
+            FROM node_tbl
+            WHERE child_type = '" . $db->escapeString($child_type) . "'
+            AND child_id = '" . $db->escapeString($child_id) . "'
+        ");
+
+        $children = array();
+        while ($row = mysql_fetch_assoc($qid)) {
+            $children[] = $row;
+        }
+        if (sizeof($children) > 0) {
+            return $children;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns an array of all the children of the current node (just the ones
+     * immediately below this node). You may need to call array_unique if you
+     * don't want duplicate nodes returned.
+     *
+     * @param  string  $child_type
+     * @param  string  $child_id
+     * @param  string  $type_constraint  An array of node types to restrict the search to.
+     *
+     * @return string  The children as an array of serialized node identifiers.
+     */
+    public function getChildren($child_type=null, $child_id=null, $type_constraint=null, $order='')
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        if (!isset($child_type) || !isset($child_id)) {
+            if ($this->node_init) {
+                $child_type =& $this->child_type;
+                $child_id =& $this->child_id;
+            } else {
+                $app->logMsg('getChildren failed. Arguments not specified properly.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        $in_clause = '';
+        if (isset($type_constraint)) {
+            if (!is_array($type_constraint)) {
+                $type_constraint = array($type_constraint);
+            }
+            $in_clause = "AND child_type IN ('" . join("','", array_map(array($db, 'escapeString'), $type_constraint)) . "')";
+        }
+
+        $qid = $db->query("
+            SELECT *
+            FROM node_tbl
+            WHERE parent_type = '" . $db->escapeString($child_type) . "'
+            AND parent_id = '" . $db->escapeString($child_id) . "'
+            $in_clause
+            " . $db->escapeString($order) . "
+        ");
+
+        $children = array();
+        while ($row = mysql_fetch_assoc($qid)) {
+            $children[] = $row;
+        }
+        if (sizeof($children) > 0) {
+            return $children;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Give the number of children a category has. We are talking about the
+     * direct children, on the next level.
+     *
+     * @param string optional $parent The name of the parent from where we begin.
+     * @param string $type_constraint  An array of node types to restrict the search to.
+     *
+     * @return integer
+     */
+    public function getNumberChildren($child_type=null, $child_id=null, $type_constraint=null)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        if (!isset($child_type) || !isset($child_id)) {
+            if ($this->node_init) {
+                $child_type =& $this->child_type;
+                $child_id =& $this->child_id;
+            } else {
+                $app->logMsg('getNumberChildren failed. Arguments not specified properly.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        $in_clause = '';
+        if (isset($type_constraint)) {
+            if (!is_array($type_constraint)) {
+                $type_constraint = array($type_constraint);
+            }
+            $in_clause = "AND child_type IN ('" . join("','", array_map(array($db, 'escapeString'), $type_constraint)) . "')";
+        }
+
+        $qid = $db->query("
+            SELECT COUNT(*)
+            FROM node_tbl
+            WHERE parent_type = '" . $db->escapeString($child_type) . "'
+            AND parent_id = '" . $db->escapeString($child_id) . "'
+            $in_clause
+        ");
+        list($num_children) = mysql_fetch_row($qid);
+
+        return $num_children;
+    }
+
+    /**
+     * Tests of the specified node is a leaf (i.e. it has no children).
+     * @param  string    $child_type
+     * @param  string    $child_id
+     * @return bool      true if a leaf, or false if not or an error
+     */
+    public function isLeaf($child_type=null, $child_id=null)
+    {
+        $app =& App::getInstance();
+
+        if (!isset($child_type) || !isset($child_id)) {
+            if ($this->node_init) {
+                $child_type =& $this->child_type;
+                $child_id =& $this->child_id;
+            } else {
+                $app->logMsg('isLeaf failed. Arguments not specified properly.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        if ($this->getNumberChildren($child_type, $child_id) <= 0) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * This function will tell you if a specified node (the child) is actually
+     * an ancestor to another node that perhaps is considered to be a parent. If
+     * the specified node IS an ancestor of a node made into it's parent, we would
+     * have a circular reference that would cause an infinite loop with any
+     * recursive queries of the hierarchy.
+     * @param  string    $child_type
+     * @param  string    $child_id
+     * @param  string    $considered_parent_type
+     * @param  string    $considered_parent_id
+     * @return bool      true if the child is an ancestor to the considered
+     *                   parent, or false otherwise, or in case of failure.
+     */
+    public function isAncestor($child_type, $child_id, $considered_parent_type, $considered_parent_id)
+    {
+        $family_tree = $this->getAllAncestors($considered_parent_type, $considered_parent_id);
+        $family_tree = $this->toStringID($family_tree, -1);
+
+        if (in_array($this->toStringID($child_type, $child_id), $family_tree)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * This function builds an array of all the parents starting from a
+     * specified node, up to the root of the tree. It will follow paths for
+     * nodes with multiple parents, branching off and filling the array with
+     * ALL ancestors to the specified node. I'm not sure what the order will be
+     * but that probably isn't useful anyways. I use this to prevent circular
+     * references in the hierarchy.
+     * @param  string    $child_type
+     * @param  string    $child_id
+     * @param  bool      $go_linear  ?
+     * @param  int       $_return_flag  An internal value that counts up as
+     *                                  recursion progresses. When the value
+     *                                  drops back to 0, we return the output.
+     * @return array     Array of serialized node identifiers.
+     */
+    public function getAllAncestors($child_type, $child_id, $go_linear=false, $_return_flag=true)
+    {
+        $db =& DB::getInstance();
+
+        static $output = array();
+        static $return_flag;
+
+        $qid = $db->query("
+            SELECT parent_type, parent_id, child_type, child_id, title, subnode_quantity
+            FROM node_tbl
+            WHERE child_type = '" . $db->escapeString($child_type) . "'
+            AND child_id = '" . $db->escapeString($child_id) . "'
+        ");
+        while ($row = mysql_fetch_assoc($qid)) {
+            // Preventing circular references.
+            if ($row['parent_type'] == $child_type && $row['parent_id'] == $child_id) {
+                continue;
+            }
+
+            // Build a linear path to root...no wormholes.
+            if ($enough_already && $go_linear) {
+                continue;
+            }
+            $enough_already = true;
+
+            // To prevent duplicates, only add the new found node
+            // if not already in the array of ancestors.
+            $curr_items = sizeof($output) > 0 ? $this->toStringID($output, -1) : array();
+            if (!in_array($this->toStringID($row['child_type'], $row['child_id']), $curr_items)) {
+                $output[] = $row;
+            }
+
+            $this->getAllAncestors($row['parent_type'], $row['parent_id'], $go_linear, false);
+        }
+        if ($_return_flag) {
+            // We must reset the static $output variable so that it does
+            // not fill up during subsequent function calls.
+            $ret = $output;
+            $output = array();
+            return $ret;
+        }
+    }
+
+    /**
+     * Tests if the specified node exists. If only the child type and id is
+     * specified, check if any node exists like that. If the parent type and id
+     * is also provided, test for an exact node match.
+     * @param  string    $child_type
+     * @param  string    $child_id
+     * @param  string    $parent_type
+     * @param  string    $parent_id
+     * @param  string    $relationship_type
+     * @return bool      true if a leaf, or false if not or an error
+     */
+    public function nodeExists($child_type=null, $child_id=null, $parent_type=null, $parent_id=null, $relationship_type=null)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        if (!isset($child_type) || !isset($child_id)) {
+            if ($this->node_init) {
+                $child_type =& $this->child_type;
+                $child_id =& $this->child_id;
+            } else {
+                $app->logMsg('nodeExists failed. Arguments not specified properly.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        if (isset($parent_type) && isset($parent_id)) {
+            $qid = $db->query("
+                SELECT 1 FROM node_tbl
+                WHERE parent_type = '" . $db->escapeString($parent_type) . "'
+                AND parent_id = '" . $db->escapeString($parent_id) . "'
+                AND child_type = '" . $db->escapeString($child_type) . "'
+                AND child_id = '" . $db->escapeString($child_id) . "'
+                AND relationship_type " . (is_null($relationship_type) ? "IS NULL" : "= '" . $db->escapeString($relationship_type) . "'") . "
+            ");
+        } else {
+            $qid = $db->query("
+                SELECT 1 FROM node_tbl
+                WHERE child_type = '" . $db->escapeString($child_type) . "'
+                AND child_id = '" . $db->escapeString($child_id) . "'
+            ");
+        }
+        if (mysql_num_rows($qid) > 0) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Recursively go through the node tree, starting at a specified node, and
+     * drill down, filling an array with info about the nodes:
+     *
+     * @param  array   $preselected
+     * @param  string  $child_type
+     * @param  string  $child_id
+     * @param  string  $type_constraint  An array of node types to restrict the search to.
+     * @param  bool    $include_curr     Do we include the specified node in the list?
+     * @param  string  $order            SQL to append to the query of the getChildren
+     *                                   call. Ex: 'ORDER BY child_id DESC'
+     * @return array  Details of from the node table of all nodes below the
+     *                specified node: (type, id, title, indent level, selected status)
+     */
+    public function &getNodeList($preselected=null, $child_type=null, $child_id=null, $type_constraint=null, $include_curr=false, $order='', $_indent=0, $_return_flag=true)
+    {
+        $app =& App::getInstance();
+
+        static $output = array();
+        static $is_a_leaf = array();
+
+        if (!isset($child_type) || !isset($child_id)) {
+            if ($this->node_init) {
+                $child_type =& $this->child_type;
+                $child_id =& $this->child_id;
+            } else {
+                $app->logMsg('getNodeList failed. Arguments not specified properly.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        if (!is_array($preselected)) {
+            $preselected = array($preselected);
+        }
+
+        if ($_return_flag && $include_curr) {
+            $my_children = $this->getNode($child_type, $child_id);
+        } else {
+            $my_children = $this->getChildren($child_type, $child_id, $type_constraint, $order);
+        }
+
+        if ($my_children) {
+            $num_children = sizeof($my_children);
+            for ($i=0; $i<$num_children; $i++) {
+
+                // Preventing circular references.
+                if ($my_children[$i]['child_type'] == $child_type && $my_children[$i]['child_id'] == $child_id && !($_return_flag && $include_curr)) {
+                    $app->logMsg(sprintf('Circular reference detected: %s has itself as a parent.', $this->toStringID($my_children[$i]['child_type'], $my_children[$i]['child_id'])), LOG_ERR, __FILE__, __LINE__);
+                    continue;
+                }
+
+                $my_children[$i]['indent'] = $_indent;
+                if (in_array($this->toStringID($my_children[$i]['child_type'], $my_children[$i]['child_id']), $preselected)) {
+                    $my_children[$i]['selected'] = true;
+                }
+                $output[] = $my_children[$i];
+
+                // Test if each node is a string only once. Store the result in the is_a_leaf array statically.
+                if (!isset($is_a_leaf[$this->toStringID($my_children[$i]['child_type'], $my_children[$i]['child_id'])])) {
+                    $is_a_leaf[$this->toStringID($my_children[$i]['child_type'], $my_children[$i]['child_id'])] = $this->isLeaf($my_children[$i]['child_type'], $my_children[$i]['child_id']);
+                }
+                if (!$is_a_leaf[$this->toStringID($my_children[$i]['child_type'], $my_children[$i]['child_id'])]) {
+                    // If this node is not a leaf, we dive into it recursively.
+                    $this->getNodeList($preselected, $my_children[$i]['child_type'], $my_children[$i]['child_id'], $type_constraint, $include_curr, $order, $_indent+1, false);
+                }
+            }
+        }
+
+        if ($_return_flag) {
+            // We must reset the static variables so that they do
+            // not fill up during subsequent function calls.
+            $ret = $output;
+            $output = array();
+            $is_a_leaf = array();
+            return $ret;
+        }
+
+    }
+
+
+    /**
+     * Counts the number of items linked to each parent node
+     * putting the result in the relevant category counters. This function is
+     * called each time a product, article, etc. is inserted, updated, or deleted.
+     *
+     * @param  string  $type_constraint  An array of node types to restrict the search to.
+     */
+    public function rebuildSubnodeQty($type_constraint=null)
+    {
+        $db =& DB::getInstance();
+
+        // Reset all the category counters to zero.
+        $db->query("UPDATE node_tbl SET subnode_quantity = 0");
+
+        // Get all the nodes.
+        $qid = $db->query("SELECT DISTINCT child_type, child_id FROM node_tbl");
+
+        // For each node count the number of children...
+        while (list($child_type, $child_id) = mysql_fetch_row($qid)) {
+            $num_children = $this->getNumberChildren($child_type, $child_id, $type_constraint);
+            // ...and add that number to the node counter for that object and all parents.
+            if ($num_children > 0) {
+                $this->setSubnodeQtyToParents($child_type, $child_id, $num_children);
+            }
+        }
+    }
+
+    /**
+     * Used internally by setSubnodeQty to add the quantity of subnodes to
+     * all parents recursively.
+     */
+    public function setSubnodeQtyToParents($child_type, $child_id, $num_children)
+    {
+        $db =& DB::getInstance();
+
+        $db->query("
+            UPDATE node_tbl
+            SET subnode_quantity = subnode_quantity + '" . $db->escapeString($num_children) . "'
+            WHERE child_type = '" . $db->escapeString($child_type) . "'
+            AND child_id = '" . $db->escapeString($child_id) . "'
+        ",false);
+        $qid = $db->query("
+            SELECT parent_type, parent_id
+            FROM node_tbl
+            WHERE child_type = '" . $db->escapeString($child_type) . "'
+            AND child_id = '" . $db->escapeString($child_id) . "'
+        ",false);
+        while ((list($parent_type, $parent_id) = mysql_fetch_row($qid)) && $parent_id > 0) {
+            $this->setSubnodeQtyToParents($parent_type, $parent_id, $num_children);
+        }
+    }
+
+// THE END
+}
Index: tags/2.2.3/lib/Image.inc.php
===================================================================
--- tags/2.2.3/lib/Image.inc.php	(revision 632)
+++ tags/2.2.3/lib/Image.inc.php	(revision 632)
@@ -0,0 +1,160 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Image.inc.php
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.1
+ * @since   14 Apr 2006 20:07:29
+ */
+class Image
+{
+
+    // Object parameters.
+    protected $_params = array();
+
+    /**
+     * Constructor.
+     *
+     * @param   array    $params  Set parameters for this object.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.1
+     * @since   26 Jan 2005 01:54:50
+     */
+    public function __construct($params=array())
+    {
+        if (!is_array($params)) {
+            trigger_error(sprintf('Parameters not specified properly.', null), E_USER_ERROR);
+        }
+        $defaults = array(
+            // Pattern passed to glob() with $id to match image filenames.
+            'filename_pattern' => '%s*',
+
+            // The path to the image source directory. (Ex: /var/www/htdocs/images)
+            'base_path' => '',
+
+            // The URL to the image directory. (Ex: /images)
+            'base_url' => '',
+
+            // Image to use in the case of a missing image.
+            'default_image_file' => '',
+        );
+        $this->_params = array_merge($defaults, $params);
+    }
+
+    /**
+     * Tests if an image with specified id exists on the file system.
+     *
+     * @access  public
+     * @param   string  $id     Unique image identifier.
+     * @return  bool            Existence of file.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   26 Jan 2005 01:54:50
+      */
+    public function exists($id)
+    {
+        $src = $this->oSrc($id);
+        $filepath = preg_match('!://!', $src) ? $src : getenv('DOCUMENT_ROOT') . $src;
+        // Use exif_imagetype to check not only file existence but that of a valid image.
+        // The return value is the same value that getimagesize() returns in index 2 but exif_imagetype() is much faster.
+        return false != @exif_imagetype($filepath);
+    }
+
+    /**
+     * Returns the value from getimagesize().
+     *
+     * @access  public
+     * @param   string  $id     Unique image identifier.
+     * @param   int     $key    Which element from the array returned by getimagesize:
+     *                   - Index 0 contains the width of the image in pixels.
+     *                   - Index 1 contains the height.
+     *                   - Index 2 is the type of the image.
+     *                   - Index 3 is height="yyy" width="xxx" string.
+     * @return  mixed   return value of getimagesize.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   26 Jan 2005 01:54:50
+     */
+    public function size($id, $key)
+    {
+        $src = $this->oSrc($id);
+        $filepath = preg_match('!://!', $src) ? $src : getenv('DOCUMENT_ROOT') . $src;
+        $img_size = @getimagesize($filepath);
+        return isset($img_size[$key]) ? $img_size[$key] : false;
+    }
+
+    /**
+     * Returns the URL to the source of image specified by id.
+     *
+     * @access  public
+     * @param   string  $id     Unique image identifier.
+     * @return  string  Absolute URL of image.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.1
+     * @since   26 Jan 2005 01:56:35
+     */
+    public function oSrc($id)
+    {
+        $file_name = '';
+        // filename_pattern is a sprintf argument with %s replaced with the image id.
+        if ($file_match = glob(sprintf("%s/{$this->_params['filename_pattern']}", $this->_params['base_path'], $id))) {
+            $file_name = basename(end($file_match));
+        } else if ('' != $this->_params['default_image_file']) {
+            $file_name = $this->_params['default_image_file'];
+        }
+        return sprintf('%s/%s', $this->_params['base_url'], $file_name);
+    }
+
+    /**
+     * Returns an HTML <img> tag with the src set to an image specified by id.
+     * Automatically prints image width and height.
+     *
+     * @access  public
+     * @param   string  $id     Unique image identifier.
+     * @param   string  $alt    Text for image alt attribute.
+     * @param   string  $extra  Additional tag attributes.
+     * @return  HTML image tag.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   26 Jan 2005 01:57:33
+     */
+    public function oImg($id, $alt='', $extra='')
+    {
+        $src = $this->oSrc($id);
+        $filepath = preg_match('!://!', $src) ? $src : getenv('DOCUMENT_ROOT') . $src;
+
+        if (false === ($gis = @getimagesize($filepath)) || preg_match('/width|height/', $extra)) {
+            $image_size = '';
+        } else {
+            $image_size = $gis[3];
+        }
+
+        return sprintf('<img src="%s" %s alt="%s" %s />',
+            $src,
+            $image_size,
+            oTxt($alt),
+            $extra
+        );
+    }
+} // End class
Index: tags/2.2.3/lib/ImageThumb.inc.php
===================================================================
--- tags/2.2.3/lib/ImageThumb.inc.php	(revision 632)
+++ tags/2.2.3/lib/ImageThumb.inc.php	(revision 632)
@@ -0,0 +1,857 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * ImageThumb.inc.php
+ *
+ * @author   Quinn Comendant <quinn@strangecode.com>
+ * @requires Netpbm <http://sourceforge.net/projects/netpbm/> and libjpeg or GD.
+ * @version  2.0
+ */
+
+// Image proprtion options.
+define('IMAGETHUMB_FIT_WIDTH', 1);
+define('IMAGETHUMB_FIT_HEIGHT', 2);
+define('IMAGETHUMB_FIT_LARGER', 3);
+define('IMAGETHUMB_STRETCH', 4);
+define('IMAGETHUMB_NO_SCALE', 5);
+
+// Image resize options.
+define('IMAGETHUMB_METHOD_NETPBM', 6);
+define('IMAGETHUMB_METHOD_GD', 7);
+
+class ImageThumb
+{
+
+    // General object parameters.
+    protected $_params = array(
+        // The location for images to create thumbnails from.
+        'source_dir' => null,
+
+        // Existing files will be overwritten when there is a name conflict?
+        'allow_overwriting' => false,
+
+        // The file permissions of the uploaded files. Remember, files will be owned by the web server user.
+        'dest_file_perms' => 0600,
+
+        // Permissions of auto-created directories. Must be at least 0700 with owner=apache.
+        'dest_dir_perms' => 0700,
+
+        // Require file to have one of the following file name extensions.
+        'valid_file_extensions' => array('jpg', 'jpeg', 'gif', 'png'),
+
+        // Method to use for resizing. (IMAGETHUMB_METHOD_NETPBM or IMAGETHUMB_METHOD_GD)
+        'resize_method' => IMAGETHUMB_METHOD_NETPBM,
+
+        // Netpbm and libjpeg binary locations.
+        'anytopnm_binary' => '/usr/bin/anytopnm',
+        'pnmscale_binary' => '/usr/bin/pnmscale',
+        'cjpeg_binary' => '/usr/bin/cjpeg',
+
+        // Which messages do we pass to raiseMsg? Use one of the MSG_* constants or false to disable.
+        'display_messages' => MSG_ALL,
+    );
+
+    // Default image size specs.
+    protected $_default_image_specs = array(
+        // The destination for an image thumbnail size.
+        // Use initial / to specify absolute paths, leave off to specify a path relative to source_dir (eg: ../thumbs).
+        'dest_dir' => null,
+
+        // Destination file types. (IMG_JPG, IMG_PNG, IMG_GIF, IMG_WBMP)
+        'dest_file_type' => IMG_JPG,
+
+        // Destination file types. ('jpg', 'png', 'gif', 'wbmp')
+        'dest_file_extension' => 'jpg',
+
+        // Type of scaling to perform, and sizes used to calculate max dimensions.
+        'scaling_type' => IMAGETHUMB_FIT_LARGER,
+        'width' => null,
+        'height' => null,
+
+        // Percentage quality of image compression output 0-100.
+        'quality' => 65,
+
+        // Create progressive jpegs?
+        'progressive' => false,
+
+        // If using GD method, apply sharpen filter. Requires PHP > 5.1.
+        'sharpen' => true,
+
+        // Integers between 1-100, useful values are 65-85.
+        'sharpen_value' => 75,
+
+        // If source image is smaller than thumbnail, allow upscaling?
+        'allow_upscaling' => false,
+
+        // If thumb exists and filesize is smaller than this, do not overwrite the thumb.
+        'keep_filesize' => null,
+    );
+
+    // Final specifications for image sizes, set with setSpec().
+    protected $_image_specs = array();
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+
+            // Enforce valid source_dir parameter.
+            if (isset($params['source_dir'])) {
+                $params['source_dir'] = realpath($params['source_dir']);
+                // Source must be directory.
+                if (!is_dir($params['source_dir'])) {
+                    $app->logMsg(sprintf('Attempting to auto-create source directory: %s', $params['source_dir']), LOG_NOTICE, __FILE__, __LINE__);
+                    if (version_compare(PHP_VERSION, '5.0.0', '>=')) {
+                        // Recursive.
+                        mkdir($params['source_dir'], isset($params['dest_dir_perms']) ? $params['dest_dir_perms'] : $this->getParam('dest_dir_perms'), true);
+                    } else {
+                        mkdir($params['source_dir'], isset($params['dest_dir_perms']) ? $params['dest_dir_perms'] : $this->getParam('dest_dir_perms'));
+                    }
+                    if (!is_dir($params['source_dir'])) {
+                        $app->logMsg(sprintf('Source directory invalid: %s', $params['source_dir']), LOG_ERR, __FILE__, __LINE__);
+                        trigger_error(sprintf('Source directory invalid: %s', $params['source_dir']), E_USER_ERROR);
+                    }
+                }
+                // Source must be readable.
+                if (!is_readable($params['source_dir'])) {
+                    $app->logMsg(sprintf('Source directory not readable: %s', $params['source_dir']), LOG_ERR, __FILE__, __LINE__);
+                    trigger_error(sprintf('Source directory not readable: %s', $params['source_dir']), E_USER_ERROR);
+                }
+            }
+
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Set the specification of thumbnails.
+     *
+     * @access  public
+     * @param   array   $spec   The specifications for a size of output image.
+     * @param   int     $index  The position of the specification in the spec array
+     *                          Use to overwrite existing spec array values.
+     */
+    public function setSpec($spec, $index=null)
+    {
+        $app =& App::getInstance();
+
+        // A little sanity checking.
+        if (!isset($spec['dest_dir']) || '' == trim($spec['dest_dir'])) {
+            $app->logMsg('setSpec error: dest_dir not specified.', LOG_ERR, __FILE__, __LINE__);
+        } else {
+            $spec['dest_dir'] = trim($spec['dest_dir']);
+        }
+        if (isset($spec['dest_file_type'])) {
+            switch ($spec['dest_file_type']) {
+            case IMG_JPG :
+                if (imagetypes() & IMG_JPG == 0) {
+                    $app->logMsg(sprintf('IMG_JPG is not supported by this version of PHP GD.', null), LOG_ERR, __FILE__, __LINE__);
+                }
+                $spec['dest_file_extension'] = 'jpg';
+                break;
+            case IMG_PNG :
+                if (imagetypes() & IMG_PNG == 0) {
+                    $app->logMsg(sprintf('IMG_PNG is not supported by this version of PHP GD.', null), LOG_ERR, __FILE__, __LINE__);
+                }
+                $spec['dest_file_extension'] = 'png';
+                break;
+            case IMG_GIF :
+                if (imagetypes() & IMG_GIF == 0) {
+                    $app->logMsg(sprintf('IMG_GIF is not supported by this version of PHP GD.', null), LOG_ERR, __FILE__, __LINE__);
+                }
+                $spec['dest_file_extension'] = 'gif';
+                break;
+            case IMG_WBMP :
+                if (imagetypes() & IMG_WBMP == 0) {
+                    $app->logMsg(sprintf('IMG_WBMP is not supported by this version of PHP GD.', null), LOG_ERR, __FILE__, __LINE__);
+                }
+                $spec['dest_file_extension'] = 'wbmp';
+                break;
+            default :
+                $app->logMsg(sprintf('Invalid dest_file_type: %s', $spec['dest_file_type']), LOG_ERR, __FILE__, __LINE__);
+                break;
+            }
+        }
+        if (IMAGETHUMB_FIT_HEIGHT != $spec['scaling_type'] && (!isset($spec['width']) || !is_numeric($spec['width']))) {
+            $app->logMsg(sprintf('setSpec error: invalid width: %s', $spec['width']), LOG_ERR, __FILE__, __LINE__);
+        }
+        if (IMAGETHUMB_FIT_WIDTH != $spec['scaling_type'] && (!isset($spec['height']) || !is_numeric($spec['height']))) {
+            $app->logMsg(sprintf('setSpec error: invalid height: %s', $spec['height']), LOG_ERR, __FILE__, __LINE__);
+        }
+        if (isset($spec['quality']) && IMG_JPG != $spec['dest_file_type']) {
+            $app->logMsg('The "quality" specification is not used unless IMG_JPG is the dest_file_type.', LOG_NOTICE, __FILE__, __LINE__);
+        }
+        if (isset($spec['progressive']) && IMG_JPG != $spec['dest_file_type']) {
+            $app->logMsg('The "progressive" specification is not used unless IMG_JPG is the dest_file_type.', LOG_NOTICE, __FILE__, __LINE__);
+        }
+
+        // Add to _image_specs array.
+        if (isset($index) && isset($this->_image_specs[$index])) {
+            // Merge with existing spec if index is provided.
+            $final_spec = array_merge($this->_image_specs[$index], $spec);
+            $this->_image_specs[$index] = $final_spec;
+        } else {
+            // Merge with spec defaults.
+            $final_spec = array_merge($this->_default_image_specs, $spec);
+            $this->_image_specs[] = $final_spec;
+        }
+
+        return $final_spec;
+    }
+
+    /*
+    * Retrieve a value of a thumb specification.
+    *
+    * @access   public
+    * @param    string  $key    Key to return. See _default_image_specs above for a list.
+    * @param    int     $index  The index in the spec array of the value to retrieve. The first if not specified.
+    * @return   mixed           Value of requested index.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    08 May 2007 15:26:39
+    */
+    public function getSpec($key, $index=null)
+    {
+        $index = isset($index) ? $index : 0;
+        return $this->_image_specs[$index][$key];
+    }
+
+    /**
+     * Process an entire directory of images.
+     *
+     * @access  public
+     * @return  bool true on success, false on failure.
+     */
+    public function processAll($runtime_specs=null)
+    {
+        $app =& App::getInstance();
+
+        // Ensure we have a source.
+        if ('' == $this->getParam('source_dir')) {
+            $app->logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Get all files in source directory.
+        $dir_handle = opendir($this->getParam('source_dir'));
+        while ($dir_handle && ($file = readdir($dir_handle)) !== false) {
+            // If the file name does not start with a dot (. or .. or .htaccess).
+            if (!preg_match('/^\./', $file) && in_array(mb_strtolower(mb_substr($file, mb_strrpos($file, '.') + 1)), $this->getParam('valid_file_extensions'))) {
+                $files[] = $file;
+            }
+        }
+
+        // Process each found file.
+        if (is_array($files) && !empty($files)) {
+            $return_val = 0;
+            foreach ($files as $file_name) {
+                $return_val += (false === $this->processFile($file_name, $runtime_specs) ? 1 : 0);
+            }
+            $this->_raiseMsg(sprintf(_("Resized %s images."), sizeof($files)), MSG_SUCCESS, __FILE__, __LINE__);
+            $this->_raiseMsg(sprintf(_("Resized %s images."), sizeof($files)), MSG_SUCCESS, __FILE__, __LINE__);
+            return 0 === $return_val;
+        } else {
+            $app->logMsg(sprintf('No source images found in directory: %s', $this->getParam('source_dir')), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Generate thumbnails for the specified file.
+     *
+     * @access  public
+     * @param   string $file_name Name of file with extension.
+     * @param   array $runtime_specs Array of specifications that will override all configured specifications.
+     * @return  bool true on success, false on failure.
+     */
+    public function processFile($file_name, $runtime_specs=null)
+    {
+        $app =& App::getInstance();
+
+        // Source file determined by provided file_name.
+        $source_file = realpath(sprintf('%s/%s', $this->getParam('source_dir'), $file_name));
+
+        // Ensure we have a source.
+        if (sizeof($this->_image_specs) < 1) {
+            if (is_array($runtime_specs)) {
+                $this->setSpec($runtime_specs, 0);
+            } else {
+                $app->logMsg(sprintf('Image specifications not set before processing.'), LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        // Ensure we have a source.
+        if ('' == $this->getParam('source_dir')) {
+            $app->logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Confirm source image exists.
+        if (!file_exists($source_file)) {
+            $this->_raiseMsg(sprintf(_("Image resizing failed: source image <em>%s</em> was not found."), $file_name), MSG_ERR, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Source image not found: %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Confirm source image is readable.
+        if (!is_readable($source_file)) {
+            $this->_raiseMsg(sprintf(_("Image resizing failed: source image <em>%s</em> is not readable."), $file_name), MSG_ERR, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Source image not readable: %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Confirm source image contains data.
+        if (filesize($source_file) <= 0) {
+            $this->_raiseMsg(sprintf(_("Image resizing failed: source image <em>%s</em> is zero bytes."), $file_name), MSG_ERR, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Source image is zero bytes: %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Confirm source image has a valid file extension.
+        if (!$this->_validFileExtension($file_name)) {
+            $this->_raiseMsg(sprintf(_("Image resizing failed: source image <em>%s</em> not a valid type. It must have one of the following file name extensions: %s"), $file_name, join(', ', $this->getParam('valid_file_extensions'))), MSG_ERR, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Image resizing failed: source image not of valid type: %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Ensure destination directories are created. This will only be called once per page load.
+        if (!$this->_createDestDirs()) {
+            $app->logMsg('Image resizing failed: unable to create dest dirs.', LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // To keep this script running even if user tries to stop browser.
+        ignore_user_abort(true);
+        ini_set('max_execution_time', 300);
+        ini_set('max_input_time', 300);
+
+        // This remains zero until something goes wrong.
+        $return_val = 0;
+
+        foreach ($this->_image_specs as $index => $spec) {
+
+            if (is_array($runtime_specs)) {
+                // Override with runtime specs.
+                $spec = $this->setSpec($runtime_specs, $index);
+            }
+
+            // Destination filename uses the extension defined by dest_file_extension.
+            if ('/' == $spec['dest_dir']{0}) {
+                // Absolute path.
+                $dest_file = sprintf('%s/%s.%s', $spec['dest_dir'], mb_substr($file_name, 0, mb_strrpos($file_name, '.')), $spec['dest_file_extension']);
+            } else {
+                // Relative path.
+                $dest_file = sprintf('%s/%s/%s.%s', $this->getParam('source_dir'), $spec['dest_dir'], mb_substr($file_name, 0, mb_strrpos($file_name, '.')), $spec['dest_file_extension']);
+            }
+
+            // Ensure destination directory exists and is writable.
+            if (!is_dir(dirname($dest_file)) || !is_writable(dirname($dest_file))) {
+                $this->_createDestDirs($dest_file);
+                if (!is_dir(dirname($dest_file)) || !is_writable(dirname($dest_file))) {
+                    $app->logMsg(sprintf('Image resizing failed, dest_dir invalid: %s', dirname($dest_file)), LOG_ERR, __FILE__, __LINE__);
+                    $return_val++;
+                    continue;
+                }
+            }
+
+            // Skip existing thumbnails with file size below $spec['keep_filesize'].
+            if (isset($spec['keep_filesize']) && file_exists($dest_file)) {
+                $file_size = filesize($dest_file);
+                if (false !== $file_size && $file_size < $spec['keep_filesize']) {
+                    $app->logMsg(sprintf('Skipping thumbnail %s. File already exists and file size is less than %s bytes.', $spec['dest_dir'] . '/' . $file_name, $spec['keep_filesize']), LOG_INFO, __FILE__, __LINE__);
+                    continue;
+                }
+            }
+
+            // Determine if original file size is smaller than specified thumbnail size. Do not scale-up if $spec['allow_upscaling'] config is set to false.
+            $image_size = getimagesize($source_file);
+            if ($image_size['0'] <= $spec['width'] && $image_size['1'] <= $spec['height'] && !$spec['allow_upscaling']) {
+                $spec['scaling_type'] = IMAGETHUMB_NO_SCALE;
+                $app->logMsg(sprintf('Image %s smaller than specified %s thumbnail size. Keeping original size.', $file_name, basename($spec['dest_dir'])), LOG_INFO, __FILE__, __LINE__);
+            }
+
+            // DO IT! Based on available method.
+            if (IMAGETHUMB_METHOD_NETPBM === $this->getParam('resize_method') && file_exists($this->getParam('anytopnm_binary')) && file_exists($this->getParam('pnmscale_binary')) && file_exists($this->getParam('cjpeg_binary'))) {
+                // Resize using Netpbm binaries.
+                $app->logMsg(sprintf('Resizing with Netpbm: %s', $source_file), LOG_DEBUG, __FILE__, __LINE__);
+                $return_val += $this->_resizeWithNetpbm($source_file, $dest_file, $spec);
+            } else if (IMAGETHUMB_METHOD_GD === $this->getParam('resize_method') && extension_loaded('gd')) {
+                // Resize with GD.
+                $app->logMsg(sprintf('Resizing with GD: %s', $source_file), LOG_DEBUG, __FILE__, __LINE__);
+                $return_val += $this->_resizeWithGD($source_file, $dest_file, $spec);
+            } else {
+                $app->logMsg(sprintf('Image thumbnailing canceled. Neither Netpbm or GD is available.', null), LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+
+        // If > 0, there was a problem thumb-nailing.
+        return 0 === $return_val;
+    }
+
+    /*
+    * Use the Netpbm and libjpg cjpeg tools to generate a rescaled compressed image.
+    * This is the preferred method over GD which has (supposedly) less quality.
+    *
+    * @access   private
+    * @param    string  $source_file    Full path to source image file.
+    * @param    string  $dest_file      Full path to destination image file.
+    * @param    array   $spec           Array of image size specifications.
+    * @return   int                     0 if no error, n > 0 if errors.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    19 May 2006 13:55:46
+    */
+    protected function _resizeWithNetpbm($source_file, $dest_file, $spec)
+    {
+        $app =& App::getInstance();
+
+        // Define pnmscale arguments.
+        switch ($spec['scaling_type']) {
+        case IMAGETHUMB_FIT_WIDTH :
+            $pnmscale_args = sprintf(' -width %s ', escapeshellarg($spec['width']));
+            break;
+        case IMAGETHUMB_FIT_HEIGHT :
+            $pnmscale_args = sprintf(' -height %s ', escapeshellarg($spec['height']));
+            break;
+        case IMAGETHUMB_FIT_LARGER :
+            $pnmscale_args = sprintf(' -xysize %s %s ', escapeshellarg($spec['width']), escapeshellarg($spec['height']));
+            break;
+        case IMAGETHUMB_STRETCH :
+            $pnmscale_args = sprintf(' -width %s -height %s ', escapeshellarg($spec['width']), escapeshellarg($spec['height']));
+            break;
+        case IMAGETHUMB_NO_SCALE :
+        default :
+            $pnmscale_args = ' 1 ';
+            break;
+        }
+
+        // Define cjpeg arguments.
+        $cjpeg_args = sprintf(' -optimize -quality %s ', escapeshellarg($spec['quality']));
+        $cjpeg_args .= (true === $spec['progressive']) ? ' -progressive ' : '';
+
+        // Format the command that creates the thumbnail.
+        $command = sprintf('%s %s | %s %s | %s %s > %s',
+            escapeshellcmd($this->getParam('anytopnm_binary')),
+            escapeshellcmd($source_file),
+            escapeshellcmd($this->getParam('pnmscale_binary')),
+            escapeshellcmd($pnmscale_args),
+            escapeshellcmd($this->getParam('cjpeg_binary')),
+            escapeshellcmd($cjpeg_args),
+            escapeshellcmd($dest_file)
+        );
+        $app->logMsg(sprintf('ImageThumb Netpbm command: %s', $command), LOG_DEBUG, __FILE__, __LINE__);
+
+        // Execute!
+        exec($command, $output, $return_val);
+
+        if (0 === $return_val) {
+            // Success!
+            // Make the thumbnail writable so the user can delete it over ftp without being 'apache'.
+            chmod($dest_file, $this->getParam('dest_file_perms'));
+            $app->logMsg(sprintf('Successfully resized image %s', $spec['dest_dir'] . '/' . basename($dest_file), $return_val), LOG_DEBUG, __FILE__, __LINE__);
+        } else {
+            // An error occurred.
+            $app->logMsg(sprintf('Image %s failed resizing with return value: %s%s', $spec['dest_dir'] . '/' . basename($dest_file), $return_val, empty($output) ? '' : ' (' . truncate(getDump($output, true), 128, 'end') . ')'), LOG_ERR, __FILE__, __LINE__);
+        }
+
+        // Return from the command will be > 0 if there was an error.
+        return $return_val;
+    }
+
+    /*
+    * Use PHP's built-in GD tools to generate a rescaled compressed image.
+    *
+    * @access   private
+    * @param    string  $source_file    Full path to source image file.
+    * @param    string  $dest_file      Full path to destination image file.
+    * @param    array   $spec           Array of image size specifications.
+    * @return   int                     0 if no error, n > 0 if errors.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    19 May 2006 15:46:02
+    */
+    protected function _resizeWithGD($source_file, $dest_file, $spec)
+    {
+        $app =& App::getInstance();
+
+        // Get original file dimensions and type.
+        list($source_image_width, $source_image_height, $source_image_type) = getimagesize($source_file);
+
+        // Define destination image dimensions.
+        switch ($spec['scaling_type']) {
+        case IMAGETHUMB_FIT_WIDTH :
+            $dest_image_width = $spec['width'];
+            $dest_image_height = $source_image_height * ($spec['width'] / $source_image_width);
+            break;
+        case IMAGETHUMB_FIT_HEIGHT :
+            $dest_image_height = $spec['height'];
+            $dest_image_width = $source_image_width * ($spec['height'] / $source_image_height);
+            break;
+        case IMAGETHUMB_FIT_LARGER :
+            if (($source_image_width * ($spec['height'] / $source_image_height)) <= $spec['width']) {
+                // Height is larger.
+                $dest_image_height = $spec['height'];
+                $dest_image_width = $source_image_width * ($spec['height'] / $source_image_height);
+            } else {
+                // Width is larger.
+                $dest_image_width = $spec['width'];
+                $dest_image_height = $source_image_height * ($spec['width'] / $source_image_width);
+            }
+            break;
+        case IMAGETHUMB_STRETCH :
+            $dest_image_width = $spec['width'];
+            $dest_image_height = $spec['height'];
+            break;
+        case IMAGETHUMB_NO_SCALE :
+        default :
+            $dest_image_width = $source_image_width;
+            $dest_image_height = $source_image_height;
+            break;
+        }
+
+        // Create source image data in memory.
+        switch ($source_image_type) {
+        case IMAGETYPE_JPEG : // = 2
+            $source_image_resource = imagecreatefromjpeg($source_file);
+            break;
+        case IMAGETYPE_PNG : // = 3
+            $source_image_resource = imagecreatefrompng($source_file);
+            break;
+        case IMAGETYPE_GIF : // = 1
+            $source_image_resource = imagecreatefromgif($source_file);
+            break;
+        case IMAGETYPE_WBMP : // = 15
+            $source_image_resource = imagecreatefromwbmp($source_file);
+        case IMAGETYPE_SWF : // = 4
+        case IMAGETYPE_PSD : // = 5
+        case IMAGETYPE_BMP : // = 6
+        case IMAGETYPE_TIFF_II : // = 7
+        case IMAGETYPE_TIFF_MM : // = 8
+        case IMAGETYPE_JPC : // = 9
+        case IMAGETYPE_JP2 : // = 10
+        case IMAGETYPE_JPX : // = 11
+        case IMAGETYPE_JB2 : // = 12
+        case IMAGETYPE_SWC : // = 13
+        case IMAGETYPE_IFF : // = 14
+        case IMAGETYPE_XBM : // = 16
+        default :
+            $app->logMsg(sprintf('Source image type %s not supported.', $source_image_type), LOG_WARNING, __FILE__, __LINE__);
+            return 1;
+        }
+        if (!$source_image_resource) {
+            $app->logMsg(sprintf('Error creating %s image in memory from %s', $source_image_type, $source_file), LOG_WARNING, __FILE__, __LINE__);
+            return 1;
+        }
+
+        // Create destination image data in memory.
+        $dest_image_resource = imagecreatetruecolor($dest_image_width, $dest_image_height);
+
+        // Resample!
+        if (!imagecopyresampled($dest_image_resource, $source_image_resource, 0, 0, 0, 0, $dest_image_width, $dest_image_height, $source_image_width, $source_image_height)) {
+            // Always cleanup images from memory.
+            imagedestroy($source_image_resource);
+            imagedestroy($dest_image_resource);
+            $app->logMsg(sprintf('Error resampling image %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
+            return 1;
+        }
+
+        // Sharpen image using a custom filter matrix.
+        if (version_compare(PHP_VERSION, '5.1.0', '>=') && true === $spec['sharpen'] && $spec['sharpen_value'] > 0) {
+            $sharpen_value = round((((48 - 10) / (100 - 1)) * (100 - $spec['sharpen_value'])) + 10); // TODO: WTF is this math?
+            imageconvolution($dest_image_resource, array(array(-1, -1, -1), array(-1, $sharpen_value, -1),array(-1, -1, -1)), ($sharpen_value - 8), 0);
+        }
+
+        // Save image.
+        $return_val = true;
+        switch ($spec['dest_file_type']) {
+        case IMG_JPG :
+            imageinterlace($dest_image_resource, (true == $spec['progressive'] ? 1 : 0));
+            $return_val = imagejpeg($dest_image_resource, $dest_file, $spec['quality']);
+            break;
+        case IMG_PNG :
+            $return_val = imagepng($dest_image_resource, $dest_file);
+            break;
+        case IMG_GIF :
+            $return_val = imagegif($dest_image_resource, $dest_file);
+            break;
+        case IMG_WBMP :
+            $return_val = imagewbmp($dest_image_resource, $dest_file);
+            break;
+        default :
+            $app->logMsg(sprintf('Destination image type %s not supported for image %s.', $spec['dest_file_type'], $dest_file), LOG_WARNING, __FILE__, __LINE__);
+            // Always cleanup images from memory.
+            imagedestroy($source_image_resource);
+            imagedestroy($dest_image_resource);
+            return 1;
+            break;
+        }
+
+        // Always cleanup images from memory.
+        imagedestroy($source_image_resource);
+        imagedestroy($dest_image_resource);
+
+        if ($return_val) {
+            // Success!
+            // Make the thumbnail writable so the user can delete it over ftp without being 'apache'.
+            chmod($dest_file, $this->getParam('dest_file_perms'));
+            $app->logMsg(sprintf('Successfully resized image: %s', $dest_file), LOG_DEBUG, __FILE__, __LINE__);
+            return 0;
+        } else {
+            // An error occurred.
+            $app->logMsg(sprintf('Failed resizing image: %s', $dest_file), LOG_ERR, __FILE__, __LINE__);
+            return 1;
+        }
+    }
+
+    /**
+     * Delete the thumbnails for the specified file name.
+     *
+     * @access  public
+     * @param   string  $file_name  The file name to delete, with extension.
+     * @param   bool    $use_glob   Set true to use glob to find the filename (using $file_name as a pattern)
+     * @return  bool true on success, false on failure.
+     */
+    public function deleteThumbs($file_name, $use_glob=false)
+    {
+        $app =& App::getInstance();
+
+        // Ensure we have a source.
+        if ('' == $this->getParam('source_dir')) {
+            $app->logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        $return_val = 0;
+        foreach ($this->_image_specs as $spec) {
+            // Get base directory depending on if the spec's dest_dir is an absolute path or not.
+            $dest_dir = '/' == $spec['dest_dir']{0} ? $spec['dest_dir'] : sprintf('%s/%s', $this->getParam('source_dir'), $spec['dest_dir']);
+            if ($use_glob) {
+                $dest_file = realpath(sprintf('%s/%s', $dest_dir, $this->_glob($file_name, $dest_dir)));
+            } else {
+                $dest_file = realpath(sprintf('%s/%s.%s', $dest_dir, mb_substr($file_name, 0, mb_strrpos($file_name, '.')), $spec['dest_file_extension']));
+            }
+
+            if (file_exists($dest_file)) {
+                if (!unlink($dest_file)) {
+                    $return_val++;
+                    $app->logMsg(sprintf('Delete thumbs failed: %s', $dest_file), LOG_WARNING, __FILE__, __LINE__);
+                }
+            }
+        }
+        $app->logMsg(sprintf('Thumbnails deleted for file: %s', $dest_file), LOG_DEBUG, __FILE__, __LINE__);
+        return 0 === $return_val;
+    }
+
+    /**
+     * Delete the source image with the specified file name.
+     *
+     * @access  public
+     * @param   string $file_name The file name to delete, with extension.
+     * @return  bool true on success, false on failure.
+     */
+    public function deleteOriginal($file_name)
+    {
+        $app =& App::getInstance();
+
+        // Ensure we have a source.
+        if ('' == $this->getParam('source_dir')) {
+            $app->logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Ensure we have a source.
+        if ('' == $file_name) {
+            $app->logMsg(sprintf('Cannot delete, filename empty.'), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        $source_file = realpath(sprintf('%s/%s', $this->getParam('source_dir'), $file_name));
+        if (is_file($source_file) && unlink($source_file)) {
+            $app->logMsg(sprintf('Original file successfully deleted: %s', $file_name), LOG_INFO, __FILE__, __LINE__);
+            return true;
+        } else {
+            $app->logMsg(sprintf('Delete original failed: %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Returns true if file exists.
+     *
+     * @access  public
+     * @param   string $file_name The file name to test, with extension.
+     * @return  bool true on success, false on failure.
+     */
+    public function exists($file_name)
+    {
+        $app =& App::getInstance();
+
+        // Ensure we have a source.
+        if ('' == $this->getParam('source_dir')) {
+            $app->logMsg(sprintf('Source directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        $source_file = realpath(sprintf('%s/%s', $this->getParam('source_dir'), $file_name));
+        return file_exists($source_file);
+    }
+
+    /**
+     * Tests if extension of $file_name is in the array valid_file_extensions.
+     *
+     * @access  public
+     * @param   string  $file_name  A file name.
+     * @return  bool    True on success, false on failure.
+     */
+    protected function _validFileExtension($file_name)
+    {
+        preg_match('/.*?\.(\w+)$/i', $file_name, $ext);
+        return !empty($ext) && in_array(mb_strtolower($ext[1]), $this->getParam('valid_file_extensions'));
+    }
+
+    /**
+     * Make directory for each specified thumbnail size, if it doesn't exist.
+     *
+     * @access  public
+     * @return  bool true on success, false on failure.
+     */
+    protected function _createDestDirs($filename=null)
+    {
+        $app =& App::getInstance();
+
+        // Keep track of directories we've already created.
+        $dd_hash = md5(isset($filename) ? dirname($filename) : 'none');
+        static $already_checked = array();
+
+        $return_val = 0;
+
+        if (!isset($already_checked[$dd_hash])) {
+            // Ensure we have a source.
+            if ('' == $this->getParam('source_dir')) {
+                $app->logMsg(sprintf('Source directory not set before creating destination directories.'), LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+
+            // Loop through specs and ensure all dirs are created.
+            foreach ($this->_image_specs as $spec) {
+                if (isset($filename)) {
+                    $dest_dir = dirname($filename);
+                } else {
+                    // Get base directory depending on if the spec's dest_dir is an absolute path or not.
+                    $dest_dir = '/' == $spec['dest_dir']{0} ? $spec['dest_dir'] : sprintf('%s/%s', $this->getParam('source_dir'), $spec['dest_dir']);
+                }
+                if (!file_exists($dest_dir)) {
+                    // Recursively create destination directory.
+                    $app->logMsg(sprintf('Creating dest dir: %s', $dest_dir), LOG_DEBUG, __FILE__, __LINE__);
+                    if (!file_exists($dest_dir) && !($ret = mkdir($dest_dir, $this->getParam('dest_dir_perms'), true))) {
+                        $return_val++;
+                        $app->logMsg(sprintf('mkdir failure: %s', $dest_dir), LOG_ERR, __FILE__, __LINE__);
+                    }
+                    if ($ret) {
+                        $app->logMsg(sprintf('mkdir success: %s', $dest_dir), LOG_DEBUG, __FILE__, __LINE__);
+                    }
+                } else {
+                    $app->logMsg(sprintf('Dest dir exists: %s', $dest_dir), LOG_DEBUG, __FILE__, __LINE__);
+                }
+            }
+        }
+
+        $already_checked[$dd_hash] = true;
+
+        // If > 0, there was a problem creating dest dirs.
+        return 0 === $return_val;
+    }
+
+    /**
+     * An alias for $app->raiseMsg that only sends messages configured by display_messages.
+     *
+     * @access public
+     *
+     * @param string $message The text description of the message.
+     * @param int    $type    The type of message: MSG_NOTICE,
+     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
+     * @param string $file    __FILE__.
+     * @param string $line    __LINE__.
+     */
+    protected function _raiseMsg($message, $type, $file, $line)
+    {
+        $app =& App::getInstance();
+
+        if ($this->getParam('display_messages') === true || (is_int($this->getParam('display_messages')) && $this->getParam('display_messages') & $type > 0)) {
+            $app->raiseMsg($message, $type, $file, $line);
+        }
+    }
+
+    /**
+     * Get filename by glob pattern. Searches a directory for an image that matches the
+     * specified glob pattern and returns the filename of the first file found.
+     *
+     * @access  public
+     * @param   string  $pattern    Pattern to match filename.
+     * @param   string  $directory  Pattern to match containing directory.
+     * @return  string filename on success, empty string on failure.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   15 Nov 2005 20:55:22
+     */
+    protected function _glob($pattern, $directory)
+    {
+        $file_list = glob(sprintf('%s/%s', $directory, $pattern));
+        if (isset($file_list[0])) {
+            return basename($file_list[0]);
+        } else {
+            return '';
+        }
+    }
+
+} // End of class.
Index: tags/2.2.3/lib/JS.inc.php
===================================================================
--- tags/2.2.3/lib/JS.inc.php	(revision 632)
+++ tags/2.2.3/lib/JS.inc.php	(revision 632)
@@ -0,0 +1,199 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * JS.inc.php
+ *
+ * Dynamically outputs minified JS data.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.2
+ */
+class JS
+{
+
+    // Include these style sheets.
+    protected $_js_files = array('default' => array());
+
+    // JS object parameters.
+    protected $_params = array(
+        'character_set' => 'utf-8',
+        'cache_enable' => false,
+        'cache_js' => null, // Same as above; kept for backwards compatibility.
+        'cache_seconds' => 86400,
+        'strip_whitespace' => false,
+        'output_compression' => false,
+    );
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+            // Maintaining backwards compatibility.
+            if (isset($params['cache_js']) && !isset($params['cache_enable'])) {
+                $this->_params['cache_enable'] = $params['cache_js'];
+            }
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Add a file-path to the array of files to include as JS.
+     *
+     * @access  public
+     * @param   string  $file   Include path to JS files.
+     * @param   mixed   $realms   Realm name string or array of realm names.
+     * @return  bool    True on success, false on failure.
+     */
+    public function setFile($file, $realms='')
+    {
+        $app =& App::getInstance();
+
+        if (!is_array($realms)) {
+            $realms = array($realms);
+        }
+
+        if ($fp = fopen($file, 'r', true)) {
+            foreach ($realms as $realm) {
+                $realm = '' == $realm ? 'default' : $realm;
+                $this->_js_files[$realm][] = $file;
+            }
+            fclose($fp);
+            return true;
+        } else {
+            $app->logMsg(sprintf('JS file non-existent: %s', $file), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Output headers for JS.
+     *
+     * @access  public
+     *
+     * @return  bool    False if no files have been set.
+     */
+    public function headers($realm='')
+    {
+        $app =& App::getInstance();
+
+        $realm = '' == $realm ? 'default' : $realm;
+
+        if (empty($this->_js_files[$realm])) {
+            $app->logMsg(sprintf('JS::headers called without specifying any files.', null), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Get time of latest modified file, including this class file.
+        $files_mtime = array();
+        foreach (array_merge($this->_js_files[$realm], array(__FILE__)) as $file) {
+            $files_mtime[] = statIncludePath($file, 'mtime');
+        }
+        sort($files_mtime, SORT_NUMERIC);
+        $latest_mtime = array_pop($files_mtime);
+
+        if ($this->getParam('output_compression') && extension_loaded('zlib')) {
+            ob_start('ob_gzhandler');
+        }
+
+        header(sprintf('Content-Type: application/javascript; charset=%s', $this->getParam('character_set')));
+        header(sprintf('Last-Modified: %s GMT', gmdate('D, d M Y H:i:s', $latest_mtime), null));
+        if ($this->getParam('cache_enable')) {
+            header(sprintf('Cache-Control: private, max-age=%s', $this->getParam('cache_seconds')));
+            header(sprintf('Expires: %s GMT', gmdate('D, d M Y H:i:s', $latest_mtime + $this->getParam('cache_seconds'))));
+            header('Pragma: private');
+            header('Vary: Accept-Encoding');
+        } else {
+            // Disallow HTTP caching entirely. http://stackoverflow.com/a/2068407
+            header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1.
+            header('Pragma: no-cache'); // HTTP 1.0.
+            header('Expires: 0'); // Proxies.
+        }
+    }
+
+    /**
+     * Include JS files specified by setFile().
+     *
+     * @access  public
+     *
+     * @return  bool    False if no files have been set.
+     */
+    public function output($realm='')
+    {
+        $app =& App::getInstance();
+
+        $realm = '' == $realm ? 'default' : $realm;
+
+        if (empty($this->_js_files[$realm])) {
+            $app->logMsg(sprintf('JS::output called without specifying any files.', null), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        foreach ($this->_js_files[$realm] as $file) {
+            if ($this->getParam('strip_whitespace')) {
+                // Strip whitespace and print file.
+                echo preg_replace(
+                    array('/(?<=^|;|{)\s*\/\/.*$/m', '/(?<=^|;|{)\s*\/\*.*?\*\//ms', '/[\n\r]+/', '/[ \t]+}[ \t]+/', '/[ \t]+{[ \t]+/', '/\s+=\s+/', '/^[ \t]+/m', '/[ \t]+$/m'),
+                    array('', '', "\n", '}', '{', '=', '', ''), file_get_contents($file, true)
+                );
+            } else {
+                // Include file as is.
+                include $file;
+            }
+            // Make sure there's at least one new-line between files.
+            echo "\n";
+        }
+
+        if ($this->getParam('output_compression') && extension_loaded('zlib')) {
+            ob_end_flush();
+        }
+    }
+}
Index: tags/2.2.3/lib/Lock.inc.php
===================================================================
--- tags/2.2.3/lib/Lock.inc.php	(revision 632)
+++ tags/2.2.3/lib/Lock.inc.php	(revision 632)
@@ -0,0 +1,460 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Lock.inc.php
+ *
+ * The Lock class provides a system for locking abstract DB rows.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 2.1
+ */
+class Lock
+{
+    // A place to keep an object instance for the singleton pattern.
+    protected static $instance = null;
+
+    // Configuration of this object.
+    protected $_params = array(
+        // The time required to pass before a user can forcibly unlock a locked record.
+        'timeout' => 600,
+
+        // The time after which a record will automatically become unlocked.
+        'auto_timeout' => 1800,
+
+        // The URL to the lock script.
+        'error_url' => '/lock.php',
+
+        // The name of the database table to store locks.
+        'db_table' => 'lock_tbl',
+
+        // Automatically create table and verify columns. Better set to false after site launch.
+        // This value is overwritten by the $app->getParam('db_create_tables') setting if it is available.
+        'create_table' => true,
+    );
+
+    // Store lock data from DB.
+    protected $data = array();
+
+    // Auth_SQL object from which to access a current user_id.
+    protected $_auth = null;
+
+    /**
+     * This method enforces the singleton pattern for this class.
+     *
+     * @return  object  Reference to the global Lock object.
+     * @access  public
+     * @static
+     */
+    public static function &getInstance($auth_object=null)
+    {
+        if (self::$instance === null) {
+            self::$instance = new self($auth_object);
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Constructor. Pass an Auth object on which to perform user lookups.
+     *
+     * @param mixed  $auth_object  An Auth_SQL or Auth_FILE object.
+     */
+    public function __construct($auth_object=null)
+    {
+        $app =& App::getInstance();
+
+        if (!is_null($auth_object) || is_null($this->_auth)) {
+            if (!method_exists($auth_object, 'get') || !method_exists($auth_object, 'getUsername')) {
+                trigger_error('Constructor not provided a valid Auth_* object.', E_USER_ERROR);
+            }
+
+            $this->_auth = $auth_object;
+        }
+
+        // Get create tables config from global context.
+        if (!is_null($app->getParam('db_create_tables'))) {
+            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
+        }
+    }
+
+    /**
+     * Setup the database table for this class.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   26 Aug 2005 17:09:36
+     */
+    public function initDB($recreate_db=false)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        static $_db_tested = false;
+
+        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
+            if ($recreate_db) {
+                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
+                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__);
+            }
+            $db->query(sprintf("CREATE TABLE IF NOT EXISTS %s (
+                lock_id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+                record_table varchar(255) NOT NULL default '',
+                record_key varchar(255) NOT NULL default '',
+                record_val varchar(255) NOT NULL default '',
+                title varchar(255) NOT NULL default '',
+                set_by_admin_id smallint(11) NOT NULL default '0',
+                lock_datetime datetime NOT NULL default '%s 00:00:00',
+                KEY record_table (record_table),
+                KEY record_key (record_key),
+                KEY record_val (record_val)
+            )", $db->escapeString($this->getParam('db_table')), $db->getParam('zero_date')));
+
+            if (!$db->columnExists($this->getParam('db_table'), array(
+                'lock_id',
+                'record_table',
+                'record_key',
+                'record_val',
+                'title',
+                'set_by_admin_id',
+                'lock_datetime',
+            ), false, false)) {
+                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
+                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
+            }
+        }
+        $_db_tested = true;
+    }
+
+    /**
+     * Set the params of this object.
+     *
+     * @param  array $params   Array of param keys and values to set.
+     */
+    public function setParam($params=null)
+    {
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Select the lock to manipulate.
+     *
+     * @param mixed  $record_table_or_lock_id  The table containing the record to lock,
+     *                                         or a numeric lock_id.
+     * @param string $record_key  The key column for the record to lock.
+     * @param string $record_val  The value of the key column for the record to lock.
+     * @param string $title       A title to apply to the lock, for display purposes.
+     */
+    public function select($record_table_or_lock_id, $record_key=null, $record_val=null)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Expire old locks.
+        $this->_auto_timeout();
+
+        if (is_numeric($record_table_or_lock_id) && !isset($record_key) && !isset($record_val)) {
+            // Get lock data by lock_id.
+            $qid = $db->query("
+                SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . "
+                WHERE lock_id = '" . $db->escapeString($record_table_or_lock_id) . "'
+            ");
+        } else {
+            // Get lock data by record specs
+            $qid = $db->query("
+                SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . "
+                WHERE record_table = '" . $db->escapeString($record_table_or_lock_id) . "'
+                AND record_key = '" . $db->escapeString($record_key) . "'
+                AND record_val = '" . $db->escapeString($record_val) . "'
+            ");
+        }
+        if ($this->data = mysql_fetch_assoc($qid)) {
+            $app->logMsg(sprintf('Selecting %slocked record: %s %s %s', ($this->data['set_by_admin_id'] == $this->_auth->get('user_id') ? 'self-' : ''), $record_table_or_lock_id, $record_key, $record_val), LOG_DEBUG, __FILE__, __LINE__);
+            // FIXME: What if admin set lock, but public user is current lock user?
+            $this->data['editor'] = $this->_auth->getUsername($this->data['set_by_admin_id']);
+            return true;
+        } else {
+            $app->logMsg(sprintf('No locked record: %s %s %s', $record_table_or_lock_id, $record_key, $record_val), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Returns true if the record we instantiated with is locked.
+     *
+     * @return bool            True if locked.
+     */
+    public function isLocked()
+    {
+        return isset($this->data['lock_id']);
+    }
+
+    /**
+     * Returns the status of who set the lock. Use this to ignore locks set by
+     * the current user.
+     *
+     * @return bool            True if current user set the lock.
+     */
+    public function isMine()
+    {
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        if (isset($this->data['lock_id'])) {
+            $qid = $db->query("SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . " WHERE lock_id = '" . $db->escapeString($this->data['lock_id']) . "'");
+            if ($lock = mysql_fetch_assoc($qid)) {
+                return ($lock['set_by_admin_id'] == $this->_auth->get('user_id'));
+            } else {
+                return false;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Create a new lock for the specified table/key/value.
+     *
+     * @param string $record_table  The table containing the record to lock.
+     * @param string $record_key  The key column for the record to lock.
+     * @param string $record_val  The value of the key column for the record to lock.
+     * @param string $title       A title to apply to the lock, for display purposes.
+     *
+     * @return int            The id for the lock (mysql last insert id).
+     */
+    public function set($record_table, $record_key, $record_val, $title='')
+    {
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Expire old locks.
+        $this->_auto_timeout();
+
+        // Remove previous locks if exist. Is this better than using a REPLACE INTO?
+        $db->query("
+            DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
+            WHERE record_table = '" . $db->escapeString($record_table) . "'
+            AND record_key = '" . $db->escapeString($record_key) . "'
+            AND record_val = '" . $db->escapeString($record_val) . "'
+        ");
+
+        // Set new lock.
+        $db->query("
+            INSERT INTO " . $db->escapeString($this->getParam('db_table')) . " (
+                record_table,
+                record_key,
+                record_val,
+                title,
+                set_by_admin_id,
+                lock_datetime
+            ) VALUES (
+                '" . $db->escapeString($record_table) . "',
+                '" . $db->escapeString($record_key) . "',
+                '" . $db->escapeString($record_val) . "',
+                '" . $db->escapeString($title) . "',
+                '" . $db->escapeString($this->_auth->get('user_id')) . "',
+                NOW()
+            )
+        ");
+        $lock_id = mysql_insert_id($db->getDBH());
+
+        // Must register this locked record as the current.
+        $this->select($lock_id);
+
+        return $lock_id;
+    }
+
+    /**
+     * Unlock the currently selected record.
+     */
+    public function remove()
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Expire old locks.
+        $this->_auto_timeout();
+
+        // Delete a specific lock.
+        $db->query("
+            DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
+            WHERE lock_id = '" . $db->escapeString($this->data['lock_id']) . "'
+        ");
+
+        $app->logMsg(sprintf('Removing lock: %s', $this->data['lock_id']), LOG_DEBUG, __FILE__, __LINE__);
+    }
+
+    /**
+     * Unlock all records, or all records for a specified user.
+     */
+    public function removeAll($user_id=null)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Expire old locks.
+        $this->_auto_timeout();
+
+        if (isset($user_id)) {
+            // Delete specific user's locks.
+            $db->query("DELETE FROM " . $db->escapeString($this->getParam('db_table')) . " WHERE set_by_admin_id = '" . $db->escapeString($user_id) . "'");
+            $app->logMsg(sprintf('Record locks owned by user %s have been deleted', $this->_auth->getUsername($user_id)), LOG_DEBUG, __FILE__, __LINE__);
+        } else {
+            // Delete ALL locks.
+            $db->query("DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "");
+            $app->logMsg(sprintf('All record locks deleted by user %s', $this->_auth->get('username')), LOG_DEBUG, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Deletes all locks that are older than auto_timeout.
+     */
+    public function _auto_timeout()
+    {
+        $db =& DB::getInstance();
+
+        static $_timeout_run = false;
+
+        $this->initDB();
+
+        if (!$_timeout_run) {
+            // Delete all old locks.
+            $db->query("
+                DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
+                WHERE DATE_ADD(lock_datetime, INTERVAL '" . $db->escapeString($this->getParam('auto_timeout')) . "' SECOND) < NOW()
+            ");
+            $_timeout_run = true;
+        }
+    }
+
+    /**
+     * Redirect to record lock error page.
+     */
+    public function dieErrorPage()
+    {
+        $app =& App::getInstance();
+
+        $app->dieURL($this->getParam('error_url'), array('lock_id' => $this->data['lock_id'], 'boomerang' => urlencode(absoluteMe())));
+    }
+
+    /**
+     * Print error page. This method is probably not used anywhere; instead, we're including this via the template codebase/services/templates/lock.ihtml
+     */
+    public function printErrorHTML()
+    {
+        $app =& App::getInstance();
+        ?>
+        <form method="post" action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>">
+            <?php $app->printHiddenSession() ?>
+            <input type="hidden" name="lock_id" value="<?php echo $this->getID(); ?>" />
+            <div id="sc-msg" class="sc-msg">
+                <div class="sc-msg-notice">
+                <?php printf(_("<em>%s</em> is currently being edited by %s (%d minutes elapsed). You cannot modify this database record while it is locked. Please try again later."),
+                    ucfirst($this->getTitle()),
+                    $this->getEditor(),
+                    date('i', $this->getSecondsElapsed() + 60)
+                ); ?>
+                </div>
+                <?php if ($this->getSecondsElapsed() >= $this->getParam('timeout')) { ?>
+                    <div class="sc-msg-notice"><?php printf(_("You can forcibly unlock the record if you believe the editing session has expired. You might want to confirm with %s before doing this."), $this->getEditor()) ?></div>
+                    <div class="sc-msg-notice">
+                        <input type="submit" name="unlock" value="<?php echo _("Unlock"); ?>" />
+                        <input type="submit" name="cancel" value="<?php echo _("Cancel"); ?>" />
+                    </div>
+                <?php } else { ?>
+                    <div class="sc-msg-notice">
+                        <input type="submit" name="cancel" value="<?php echo _("Cancel"); ?>" />
+                    </div>
+                <?php } ?>
+            </div>
+        </form>
+        <?php
+    }
+
+    /**
+     * Return lock_id of locked record.
+     */
+    public function getID()
+    {
+        return $this->data['lock_id'];
+    }
+
+    /**
+     * Return title of locked record.
+     */
+    public function getTitle()
+    {
+        return $this->data['title'];
+    }
+
+    /**
+     * Return administrator username for locked record.
+     */
+    public function getEditor()
+    {
+        return $this->data['editor'];
+    }
+
+    /**
+     * Return total seconds since the record was locked.
+     */
+    public function getSecondsElapsed()
+    {
+        if (isset($this->data['lock_datetime']) && false !== ($lock_timestamp = strtotime($this->data['lock_datetime'])) && $lock_timestamp < time()) {
+            return time() - $lock_timestamp;
+        } else {
+            return 0;
+        }
+    }
+
+
+} // End of class.
Index: tags/2.2.3/lib/Navigation.inc.php
===================================================================
--- tags/2.2.3/lib/Navigation.inc.php	(revision 632)
+++ tags/2.2.3/lib/Navigation.inc.php	(revision 632)
@@ -0,0 +1,431 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Navigation.inc.php
+ *
+ * The Nav class provides a system for working with navigation elements.
+ * It supports storing page titles and URLs for printing breadcrumbs
+ * and titles, as well as setting page params such as hiding the page title on
+ * some pages but not others, and storing vars like the page title itself.
+ *
+ * Note: this class was renamed from "Nav" because of the change in API and to be more descriptive.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 2.0
+ */
+class Navigation
+{
+
+    // Configuration parameters for this object.
+    protected $_params = array(
+        'head_title' => true,
+        'body_title' => true,
+        'title' => true,
+        'path' => true,
+        'breadcrumbs' => true,
+        'chop_breadcrumbs' => 0,
+        'chop_breadcrumb_links' => 1,
+        'path_delimiter' => ' / ',
+        'last_crumb_format' => '%s',
+        'current_page_url' => null, // This should be set at runtime using, e.g., $_SERVER['REQUEST_URI']
+    );
+    public $pages = array();
+
+    /**
+     * Navigation constructor.
+     */
+    public function __construct($params=null)
+    {
+        $app =& App::getInstance();
+
+        // Define current_page_url here because _SERVER, not a static scalar, cannot be defined in the defaults above.
+        // Using PHP_SELF for legacy compatibility, but it might make sense to override this with REQUEST_URI.
+        // This could be overwritten by passed params.
+        $this->_params['current_page_url'] = $_SERVER['PHP_SELF'];
+
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        }
+    }
+
+    /**
+     * Add a page to the internal pages array. Pages must be added sequentially
+     * as they are to be printed. The root page must be added first, and the
+     * current page added last. Vars can be specified for any page, but only vars
+     * from the "current" page will be accessed with Nav::get.
+     *
+     * @access  public
+     * @param   string  $title      The title of the page.
+     * @param   string  $url        The URL to the page. Set to null to use REQUEST_URI.
+     * @param   array   $vars       Additional page variables.
+     */
+    public function add($title, $url=null, $vars=array())
+    {
+        $page = array(
+            'title' => $title,
+            'head_title' => $title,
+            'body_title' => $title,
+            'url' => is_null($url) ? $this->_params['current_page_url'] : $url,
+        );
+        // An "unformed page element" has settings applied (via ->set()) but no page added (via ->add()).
+        if (empty($this->pages) || isset(end($this->pages)['title'])) {
+            // There are no unformed page elements; add a whole new page.
+            $this->pages[] = array_merge($page, $vars);
+        } else {
+            // Append the new page to the unformed page element.
+            $curr_page =& $this->pages[key($this->pages)];
+            $curr_page = array_merge($curr_page, $page, $vars);
+        }
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Unsets all pages.
+     *
+     * @access  public
+     */
+    public function clear()
+    {
+        $this->pages = array();
+    }
+
+    /**
+     * Sets a variable into the current page.
+     *
+     * @access public
+     * @param mixed $key      Which value to set.
+     * @param mixed $val      Value to set variable to.
+     */
+    public function set($key, $val)
+    {
+        // Set params of current page.
+        if (empty($this->pages)) {
+            // If we're setting a value on an empty pages array, we need to add one "unformed" element first.
+            $this->pages[] = array();
+        }
+        end($this->pages);
+        $curr_page =& $this->pages[key($this->pages)];
+        $curr_page[$key] = $val;
+    }
+
+    /**
+     * Returns a specified value from the current page.
+     *
+     * @access public
+     * @param mixed $key      Which value to return.
+     * @param mixed $default  Value to return if key not found in user_data.
+     * @return mixed          Value stored in session.
+     */
+    public function get($key, $default='')
+    {
+        end($this->pages);
+        $curr_page =& $this->pages[key($this->pages)];
+
+        switch ($key) {
+        case 'title' :
+            if ($this->getParam('title') && isset($curr_page['title'])) {
+                return $curr_page['title'];
+            }
+            break;
+
+        case 'head_title' :
+            if ($this->getParam('head_title') && $this->getParam('title') && isset($curr_page['head_title'])) {
+                return $curr_page['head_title'];
+            }
+            break;
+
+        case 'body_title' :
+            if ($this->getParam('body_title') && $this->getParam('title') && isset($curr_page['body_title'])) {
+                return $curr_page['body_title'];
+            }
+            break;
+
+        case 'path' :
+            if ($this->getParam('path')) {
+                return $this->getPath();
+            }
+            break;
+
+        case 'breadcrumbs' :
+            if ($this->getParam('breadcrumbs')) {
+                return $this->getBreadcrumbs();
+            }
+            break;
+
+        default :
+            return isset($curr_page[$key]) ? $curr_page[$key] : $default;
+            break;
+        }
+
+        return $default;
+    }
+
+    /**
+     * Returns the path from root up to the current page as an array.
+     *
+     * @access  public
+     * @param   string   $key   Which value to use in the path (usually head_title or body_title or just title).
+     * @return  mixed           Path (string) or false if path param is not set.
+     */
+    public function getPathArray($key='title')
+    {
+        $path = array();
+        if ($this->getParam('path')) {
+            foreach ($this->pages as $page) {
+                $path[] = strip_tags($page[$key]);
+            }
+        }
+        return $path;
+    }
+
+    /**
+     * Returns the text path from root up to the current page, separated by the
+     * path_delimiter.
+     *
+     * @access  public
+     * @param   string   $key   Which value to use in the path (usually head_title or body_title or just title).
+     * @return  mixed           Path (string) or false if path param is not set.
+     */
+    public function getPath($key='title')
+    {
+
+        $path = $this->getPathArray();
+        return empty($path) ? '' : join(oTxt($this->getParam('path_delimiter'), true), $path);
+    }
+
+    /**
+     * Returns the breadcrumbs from the root page to the current page.
+     * Breadcrumbs are the text path with pages titles linked to that page.
+     *
+     * @access  public
+     * @return  string   Breadcrumbs or empty string if breadcrumbs param not set.
+     */
+    public function getBreadcrumbsArray()
+    {
+        $app =& App::getInstance();
+
+        if ($this->getParam('breadcrumbs')) {
+            $breadcrumbs = array();
+            $crumb_count = sizeof($this->pages);
+            foreach ($this->pages as $page) {
+                if ($crumb_count <= $this->getParam('chop_breadcrumbs')) {
+                    // Stop gathering crumbs.
+                    break;
+                }
+                if ($crumb_count <= 1) {
+                    // The last crumb.
+                    if ('' == trim($page['url']) || $crumb_count <= $this->getParam('chop_breadcrumb_links')) {
+                        // A crumb with no link.
+                        $breadcrumbs[] = array(
+                            'url' => $_SERVER['REQUEST_URI'],
+                            'title' => sprintf($this->getParam('last_crumb_format'), $page['title']),
+                            'class' => 'current'
+                        );
+                    } else if ($crumb_count > $this->getParam('chop_breadcrumb_links')) {
+                        // A normal linked crumb.
+                        $breadcrumbs[] = array(
+                            'url' => $page['url'],
+                            'title' => sprintf($this->getParam('last_crumb_format'), $page['title']),
+                            'class' => '',
+                        );
+                    }
+                } else {
+                    if ('' == trim($page['url'])) {
+                        // A crumb with no link.
+                        $breadcrumbs[] = array(
+                            'url' => false,
+                            'title' => $page['title'],
+                            'class' => 'unavailable',
+                        );
+                    } else {
+                        // A normal linked crumb.
+                        $breadcrumbs[] = array(
+                            'url' => $page['url'],
+                            'title' => $page['title'],
+                            'class' => '',
+                        );
+                    }
+                }
+                $crumb_count--;
+            }
+            return $breadcrumbs;
+        } else {
+            return array();
+        }
+    }
+
+    /**
+     * Returns the breadcrumbs from the root page to the current page.
+     * Breadcrumbs are the text path with pages titles linked to that page.
+     *
+     * @access  public
+     * @return  string   Breadcrumbs or empty string if breadcrumbs param not set.
+     */
+    public function getBreadcrumbs()
+    {
+        $app =& App::getInstance();
+
+        if ($this->getParam('breadcrumbs')) {
+            $breadcrumbs = array();
+            $pathmark = '';
+            $crumb_count = sizeof($this->pages);
+            foreach ($this->pages as $page) {
+                if ($crumb_count <= $this->getParam('chop_breadcrumbs')) {
+                    // Stop gathering crumbs.
+                    break;
+                }
+                if ($crumb_count <= 1) {
+                    // The last crumb.
+                    if ('' == trim($page['url']) || $crumb_count <= $this->getParam('chop_breadcrumb_links')) {
+                        // A crumb with no link.
+                        $breadcrumbs[] =  sprintf($this->getParam('last_crumb_format'), oTxt($page['title'], true));
+                    } else if ($crumb_count > $this->getParam('chop_breadcrumb_links')) {
+                        // A normal linked crumb.
+                        $breadcrumbs[] =  '<a href="' . $page['url'] . '">' . sprintf($this->getParam('last_crumb_format'), oTxt($page['title'], true)) . '</a>';
+                    }
+                } else {
+                    if ('' == trim($page['url'])) {
+                        // A crumb with no link.
+                        $breadcrumbs[] = oTxt($pathmark . $page['title'], true);
+                    } else {
+                        // A normal linked crumb.
+                        $breadcrumbs[] = '<a href="' . $page['url'] . '">' . oTxt($page['title'], true) . '</a>';
+                    }
+                }
+                $pathmark = $this->getParam('path_delimiter');
+                $crumb_count--;
+            }
+            return join(oTxt($pathmark, true), $breadcrumbs);
+        } else {
+            return '';
+        }
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    07 Sep 2014 12:22:19
+    */
+    public function getBreadcrumbsUL()
+    {
+        $out = '';
+        $breadcrumbs = $this->getBreadcrumbsArray();
+        if (!empty($breadcrumbs)) {
+            $out = '<ul class="breadcrumbs">';
+            foreach ($breadcrumbs as $b) {
+                $printclass = '' != $b['class'] ? sprintf(' class="%s"', $b['class']) : '';
+                $out .= sprintf('<li%s><a href="%s">%s</a></li>', $printclass, $b['url'], $b['title']);
+            }
+            $out .= '</ul>';
+        }
+        return $out;
+    }
+
+    /**
+     * Test if the given URI matches the URL of the current page. By default the URI is tested
+     * without concern
+     * One use is to change the returned value for a positive match
+     * so a css class prints for an element representing the current page:
+     *   echo $nav->currentPage('/script.php?op=info', ' class="current"', '', true);
+     * The above will match only if the current page (REQUEST_URI) is also '/script.php?op=info',
+     * and will return the string ' class="current"' if it is.
+     *
+     * @access  public
+     *
+     * @param   mixed   $test_uri       A URI, or an array of URIs, to test against the current page.
+     * @param   mixed   $true_return    The value to return if the current page matches the test URI.
+     * @param   mixed   $false_return   The value to return if the current page does not match the test URI.
+     * @param   bool    $include_query  If set true, include the URI query string in the test.
+     *
+     * @return  mixed   If the test URI matches the current page URI, the value given for $true_return
+     *                  is returned (true by default), otherwise the value given for $false_return is
+     *                  returned (false by default).
+     */
+    public function currentPage($test_uri, $true_return=true, $false_return=false, $include_query=false)
+    {
+        $app =& App::getInstance();
+
+        // If given an array, test each URI recursively returning TRUE on a first match, or FALSE if none match.
+        if (is_array($test_uri)) {
+            foreach ($test_uri as $uri) {
+                if ($this->currentPage($uri, $true_return, $false_return, $include_query)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        $actual_uri = $include_query ? $_SERVER['REQUEST_URI'] : strtok($_SERVER['REQUEST_URI'], '?');
+        $test_uri = $include_query ? $test_uri : strtok($test_uri, '?');
+        if (mb_strtolower($test_uri) == mb_strtolower($actual_uri)) {
+            // $app->logMsg(sprintf('Current page (%s) == test URI (%s)', $actual_uri, $test_uri), LOG_DEBUG, __FILE__, __LINE__);
+            return $true_return;
+        }
+        // $app->logMsg(sprintf('Current page (%s) != test URI (%s)', $actual_uri, $test_uri), LOG_DEBUG, __FILE__, __LINE__);
+        return $false_return;
+    }
+
+}
+// End of class.
Index: tags/2.2.3/lib/PEdit.inc.php
===================================================================
--- tags/2.2.3/lib/PEdit.inc.php	(revision 632)
+++ tags/2.2.3/lib/PEdit.inc.php	(revision 632)
@@ -0,0 +1,744 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PEdit.inc.php
+ *
+ * PEdit provides a mechanism to store text in php variables
+ * which will be printed to the client browser under normal
+ * circumstances, but an authenticated user can 'edit' the document--
+ * data stored in vars will be shown in html form elements to be edited
+ * and saved. Posted data is stored in XML format in a specified data dir.
+ * A copy of the previous version is saved with the unix
+ * timestamp as part of the filename. This allows reverting to previous versions.
+ *
+ * To use, include this file, initialize variables,
+ * and call printing/editing functions where you want data and forms to
+ * show up.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @concept Beau Smith <beau@beausmith.com>
+ * @version 2.0
+ *
+ * Example of use:
+
+ // Initialize PEdit object.
+ require_once 'codebase/lib/PEdit.inc.php';
+ $pedit = new PEdit(array(
+     'data_dir' => COMMON_BASE . '/html/_pedit_data',
+     'authorized' => true,
+ ));
+
+ // Setup content data types.
+ $pedit->set('title');
+ $pedit->set('content', array('type' => 'textarea'));
+
+ // After setting all parameters and data, load the data.
+ $pedit->start();
+
+ // Print content.
+ echo $pedit->get('title');
+ echo $pedit->get('content');
+
+ // Print additional PEdit functionality.
+ $pedit->formBegin();
+ $pedit->printAllForms();
+ $pedit->printVersions();
+ $pedit->formEnd();
+
+ */
+class PEdit
+{
+
+    // PEdit object parameters.
+    protected $_params = array(
+        'data_dir' => '',
+        'character_set' => 'utf-8',
+        'versions_min_qty' => 20,
+        'versions_min_days' => 10,
+    );
+
+    protected $_data = array(); // Array to store loaded data.
+    protected $_data_file = ''; // Full file path to the pedit data file.
+    protected $_authorized = false; // User is authenticated to see extended functions.
+    protected $_data_loaded = false;
+    public $op = '';
+
+    /**
+     * Constructs a new PEdit object. Initializes what file is being operated with
+     * (PHP_SELF) and what that operation is. The two
+     * operations that actually modify data (save, restore) are treated differently
+     * than view operations (versions, view, default). They die redirect so you see
+     * the page you just modified.
+     *
+     * @access public
+     * @param optional array $params  A hash containing connection parameters.
+     */
+    public function __construct($params)
+    {
+        $this->setParam($params);
+
+        if ($this->getParam('authorized') === true) {
+            $this->_authorized = true;
+        }
+
+        // Setup PEAR XML libraries.
+        require_once 'XML/Serializer.php';
+        $this->xml_serializer = new XML_Serializer(array(
+            XML_SERIALIZER_OPTION_INDENT => '',
+            XML_SERIALIZER_OPTION_LINEBREAKS => '',
+            XML_SERIALIZER_OPTION_RETURN_RESULT => true,
+            XML_SERIALIZER_OPTION_TYPEHINTS => true,
+        ));
+        require_once 'XML/Unserializer.php';
+        $this->xml_unserializer = new XML_Unserializer(array(
+            XML_UNSERIALIZER_OPTION_COMPLEXTYPE => 'array',
+        ));
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_WARNING, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /*
+    * Load the pedit data and run automatic functions.
+    *
+    * @access   public
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    12 Apr 2006 12:43:47
+    */
+    public function start($initialize_data_file=false)
+    {
+        $app =& App::getInstance();
+
+        if (!is_dir($this->getParam('data_dir'))) {
+            trigger_error(sprintf('PEdit data directory not found: %s', $this->getParam('data_dir')), E_USER_WARNING);
+        }
+
+        // The location of the data file. (i.e.: "COMMON_DIR/html/_pedit_data/news/index.xml")
+        $this->_data_file = sprintf('%s%s.xml', $this->getParam('data_dir'), $_SERVER['SCRIPT_NAME']);
+
+        // Make certain the evaluated path matches the assumed path (realpath will expand /../../);
+        // if realpath returns FALSE we're not concerned because it means the file doesn't exist (_initializeDataFile() will create it).
+        if (false !== realpath($this->_data_file) && $this->_data_file !== realpath($this->_data_file)) {
+            $app->logMsg(sprintf('PEdit data file not a real path: %s', $this->_data_file), LOG_CRIT, __FILE__, __LINE__);
+            trigger_error(sprintf('PEdit data file not a real path: %s', $this->_data_file), E_USER_ERROR);
+        }
+
+        // op is used throughout the script to determine state.
+        $this->op = getFormData('op');
+
+        // Automatic functions based on state.
+        switch ($this->op) {
+        case 'Save' :
+            if ($this->_writeData()) {
+                $app->dieURL($_SERVER['PHP_SELF']);
+            }
+            break;
+
+        case 'Restore' :
+            if ($this->_restoreVersion(getFormData('version'))) {
+                $app->dieURL($_SERVER['PHP_SELF']);
+            }
+            break;
+
+        case 'View' :
+            $this->_data_file = sprintf('%s%s__%s.xml', $this->getParam('data_dir'), $_SERVER['PHP_SELF'], getFormData('version'));
+            $app->raiseMsg(sprintf(_("This is <em><strong>only a preview</strong></em> of version %s."), getFormData('version')), MSG_NOTICE, __FILE__, __LINE__);
+            break;
+        }
+
+        // Load data.
+        $this->_loadDataFile();
+
+        if ($initialize_data_file === true) {
+            $this->_createVersion();
+            $this->_initializeDataFile();
+        }
+    }
+
+    /**
+     * Stores a variable in the pedit data array with the content name, and type of form.
+     *
+     * @access public
+     *
+     * @param string    $content         The variable containing the text to store.
+     * @param array     $options         Additional options to store with this data.
+     */
+    public function set($name, $options=array())
+    {
+        $app =& App::getInstance();
+
+        $name = preg_replace('/\s/', '_', $name);
+        if (!isset($this->_data[$name])) {
+            $this->_data[$name] = array_merge(array('content' => ''), $options);
+        } else {
+            $app->logMsg(sprintf('Duplicate set data: %s', $name), LOG_NOTICE, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Returns the contents of a data variable. The variable must first be 'set'.
+     *
+     * @access public
+     * @param string $name   The name of the variable to return.
+     * @return string        The trimmed content of the named data.
+     */
+    public function get($name)
+    {
+        $name = preg_replace('/\s/', '_', $name);
+        if ($this->op != 'Edit' && $this->op != 'Versions' && isset($this->_data[$name]['content'])) {
+            return $this->_data[$name]['content'];
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * Prints the beginning <form> HTML tag, as well as hidden input forms.
+     *
+     * @return bool  False if unauthorized or current page is a version.
+     */
+    public function formBegin()
+    {
+        $app =& App::getInstance();
+
+        if (!$this->_authorized || empty($this->_data)) {
+            return false;
+        }
+        ?>
+        <form action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" method="post" id="sc-pedit-form">
+        <input type="hidden" name="filename" value="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" />
+        <input type="hidden" name="file_hash" value="<?php echo $this->_fileHash(); ?>" />
+        <?php
+        $app->printHiddenSession();
+        switch ($this->op) {
+        case 'Edit' :
+            ?>
+            <div class="sc-pedit-buttons">
+                <input type="submit" name="op" value="<?php echo _("Save"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Save"))?>" accesskey="<?php echo mb_substr(_("Save"), 0, 1) ?>" />
+                <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Cancel"))?>" accesskey="<?php echo mb_substr(_("Cancel"), 0, 1) ?>" />
+            </div>
+            <?php
+            break;
+
+        case 'View' :
+            ?>
+            <input type="hidden" name="version" value="<?php echo getFormData('version'); ?>" />
+            <?php
+            break;
+        }
+    }
+
+    /**
+     * Loops through the PEdit data array and prints all the HTML forms corresponding
+     * to all pedit variables, in the order in which they were 'set'.
+     *
+     * @access public
+     */
+    public function printAllForms()
+    {
+        if ($this->_authorized && $this->op == 'Edit' && is_array($this->_data) && $this->_data_loaded) {
+            foreach ($this->_data as $name=>$d) {
+                $this->printForm($name);
+            }
+        }
+    }
+
+    /**
+     * Prints the HTML forms corresponding to pedit variables. Each variable
+     * must first be 'set'.
+     *
+     * @access public
+     * @param string $name      The name of the variable.
+     * @param string $type      Type of form to print. Currently only 'text' and 'textarea' supported.
+     */
+    public function printForm($name, $type='text')
+    {
+        if ($this->_authorized && $this->op == 'Edit' && $this->_data_loaded) {
+            ?>
+            <div class="sc-pedit-item">
+            <?php
+            $type = (isset($this->_data[$name]['type'])) ? $this->_data[$name]['type'] : $type;
+            // Print edit form.
+            switch ($type) {
+            case 'text' :
+            default :
+                ?>
+                <label>
+                <?php echo ucfirst(str_replace('_', ' ', $name)); ?>
+                <input type="text" name="_pedit_data[<?php echo $name; ?>]" id="sc-pedit-field-<?php echo $name; ?>" value="<?php echo oTxt($this->_data[$name]['content']); ?>" class="sc-full" />
+                </label>
+                <?php
+                break;
+
+            case 'textarea' :
+                ?>
+                <label>
+                <?php echo ucfirst(str_replace('_', ' ', $name)); ?>
+                <textarea name="_pedit_data[<?php echo $name; ?>]" id="sc-pedit-field-<?php echo $name; ?>" rows="" cols="" class="sc-full sc-tall"><?php echo oTxt($this->_data[$name]['content']); ?></textarea>
+                </label>
+                <?php
+                break;
+            }
+            ?>
+            </div>
+            <?php
+        }
+    }
+
+    /**
+     * Prints the ending </form> HTML tag, as well as buttons used during
+     * different operations.
+     *
+     * @return bool  False if unauthorized or current page is a version.
+     */
+    public function formEnd()
+    {
+        if (!$this->_authorized || empty($this->_data)) {
+            // Don't show form elements for versioned documents.
+            return false;
+        }
+        switch ($this->op) {
+        case 'Edit' :
+            ?>
+            <div class="sc-pedit-buttons">
+                <input type="submit" name="op" value="<?php echo _("Save"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Save"))?>" accesskey="<?php echo mb_substr(_("Save"), 0, 1) ?>" />
+                <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Cancel"))?>" accesskey="<?php echo mb_substr(_("Cancel"), 0, 1) ?>" />
+            </div>
+            </form>
+            <?php
+            break;
+
+        case 'Versions' :
+            ?>
+            <div class="sc-pedit-buttons">
+                <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Cancel"))?>" accesskey="<?php echo mb_substr(_("Cancel"), 0, 1) ?>" />
+            </div>
+            </form>
+            <?php
+            break;
+
+        case 'View' :
+            ?>
+            <div class="sc-pedit-buttons">
+                <input type="submit" name="op" value="<?php echo _("Restore"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Restore"))?>" accesskey="<?php echo mb_substr(_("Restore"), 0, 1) ?>" />
+                <input type="submit" name="op" value="<?php echo _("Versions"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Versions"))?>" accesskey="<?php echo mb_substr(_("Versions"), 0, 1) ?>" />
+                <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Cancel"))?>" accesskey="<?php echo mb_substr(_("Cancel"), 0, 1) ?>" />
+            </div>
+            </form>
+            <?php
+            break;
+
+        default :
+            ?>
+            <div class="sc-pedit-buttons">
+                <input type="submit" name="op" value="<?php echo _("Edit"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Edit"))?>" accesskey="<?php echo mb_substr(_("Edit"), 0, 1) ?>" />
+                <input type="submit" name="op" value="<?php echo _("Versions"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Versions"))?>" accesskey="<?php echo mb_substr(_("Versions"), 0, 1) ?>" />
+            </div>
+            </form>
+            <?php
+        }
+    }
+
+    /**
+     * Prints an HTML list of versions of current file, with the filesize
+     * and links to view and restore the file.
+     *
+     * @access public
+     */
+    public function printVersions()
+    {
+        $app =& App::getInstance();
+
+        if ($this->_authorized && $this->op == 'Versions') {
+            // Print versions and commands to view/restore.
+            $version_files = $this->_getVersions();
+            ?><h1><?php printf(_("%s saved versions of %s"), sizeof($version_files), basename($_SERVER['PHP_SELF'])); ?></h1><?php
+            if (is_array($version_files) && !empty($version_files)) {
+                ?>
+                <table id="sc-pedit-versions-table">
+                <tr>
+                    <th><?php echo _("Date"); ?></th>
+                    <th><?php echo _("Time"); ?></th>
+                    <th><?php echo _("File size"); ?></th>
+                    <th><?php echo _("Version options"); ?></th>
+                </tr>
+                <?php
+                foreach ($version_files as $v) {
+                    ?>
+                    <tr>
+                        <td><?php echo date($app->getParam('date_format'), $v['unixtime']); ?></td>
+                        <td><?php echo date($app->getParam('time_format'), $v['unixtime']); ?></td>
+                        <td><?php echo humanFileSize($v['filesize']); ?></td>
+                        <td class="sc-nowrap"><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?op=View&version=' . $v['unixtime'] . '&file_hash=' . $this->_fileHash()); ?>"><?php echo _("View"); ?></a> <?php echo _("or"); ?> <a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?op=Restore&version=' . $v['unixtime'] . '&file_hash=' . $this->_fileHash()); ?>"><?php echo _("Restore"); ?></a></td>
+                    </tr>
+                    <?php
+                }
+                ?>
+                </table>
+                <div class="sc-help"><?php printf(_("When there are more than %s versions, those over %s days old are deleted."), $this->getParam('versions_min_qty'), $this->getParam('versions_min_days')); ?></div>
+                <?php
+            }
+        }
+    }
+
+    /*
+    * Returns a secret hash for the current file.
+    *
+    * @access   public
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    12 Apr 2006 10:52:35
+    */
+    protected function _fileHash()
+    {
+        $app =& App::getInstance();
+
+        return md5($app->getParam('signing_key') . $_SERVER['PHP_SELF']);
+    }
+
+    /*
+    * Load the XML data file into $this->_data.
+    *
+    * @access   public
+    * @return   bool    false on error
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    11 Apr 2006 20:36:26
+    */
+    protected function _loadDataFile()
+    {
+        $app =& App::getInstance();
+
+        if (!file_exists($this->_data_file)) {
+            if (!$this->_initializeDataFile()) {
+                $app->logMsg(sprintf('Initializing content file failed: %s', $this->_data_file), LOG_WARNING, __FILE__, __LINE__);
+                return false;
+            }
+        }
+        $xml_file_contents = file_get_contents($this->_data_file);
+        $status = $this->xml_unserializer->unserialize($xml_file_contents, false);
+        if (PEAR::isError($status)) {
+            $app->logMsg(sprintf('XML_Unserialize error: %s', $status->getMessage()), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+        $xml_file_data = $this->xml_unserializer->getUnserializedData();
+
+        // Only load data specified with set(), even though there may be more in the xml file.
+        foreach ($this->_data as $name => $initial_data) {
+            if (isset($xml_file_data[$name])) {
+                $this->_data[$name] = array_merge($initial_data, $xml_file_data[$name]);
+            } else {
+                $this->_data[$name] = $initial_data;
+            }
+        }
+
+        $this->_data_loaded = true;
+        return true;
+    }
+
+    /*
+    * Start a new data file.
+    *
+    * @access   public
+    * @return   The success value of both xml_serializer->serialize() and _filePutContents()
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    11 Apr 2006 20:53:42
+    */
+    protected function _initializeDataFile()
+    {
+        $app =& App::getInstance();
+
+        $app->logMsg(sprintf('Initializing data file: %s', $this->_data_file), LOG_INFO, __FILE__, __LINE__);
+        $xml_file_contents = $this->xml_serializer->serialize($this->_data);
+        return $this->_filePutContents($this->_data_file, $xml_file_contents);
+    }
+
+    /**
+     * Saves the POSTed data by overwriting the pedit variables in the
+     * current file.
+     *
+     * @access private
+     * @return bool  False if unauthorized or on failure. True on success.
+     */
+    protected function _writeData()
+    {
+        $app =& App::getInstance();
+
+        if (!$this->_authorized) {
+            return false;
+        }
+        if ($this->_fileHash() != getFormData('file_hash')) {
+            // Posted data is NOT for this file!
+            $app->logMsg(sprintf('File_hash does not match current file.', null), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Scrub incoming data. Escape tags?
+        $new_data = getFormData('_pedit_data');
+
+        if (is_array($new_data) && !empty($new_data)) {
+            // Make certain a version is created.
+            $this->_deleteOldVersions();
+            if (!$this->_createVersion()) {
+                $app->logMsg(sprintf('Failed creating new version of file.', null), LOG_NOTICE, __FILE__, __LINE__);
+                return false;
+            }
+
+            // Collect posted data that is already specified in _data (by set()).
+            foreach ($new_data as $name => $content) {
+                if (isset($this->_data[$name])) {
+                    $this->_data[$name]['content'] = $content;
+                }
+            }
+
+            if (is_array($this->_data) && !empty($this->_data)) {
+                $xml_file_contents = $this->xml_serializer->serialize($this->_data);
+                return $this->_filePutContents($this->_data_file, $xml_file_contents);
+            }
+        }
+    }
+
+    /*
+    * Writes content to the specified file.
+    *
+    * @access   public
+    * @param    string  $filename   Path to file.
+    * @param    string  $content    Data to write into file.
+    * @return   bool                Success or failure.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    11 Apr 2006 22:48:30
+    */
+    protected function _filePutContents($filename, $content)
+    {
+        $app =& App::getInstance();
+
+        // Ensure requested filename is within the pedit data dir.
+        if (mb_strpos($filename, $this->getParam('data_dir')) === false) {
+            $app->logMsg(sprintf('Failed writing file outside pedit data_dir: %s', $filename), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Recursively create directories.
+        $subdirs = preg_split('!/!', str_replace($this->getParam('data_dir'), '', dirname($filename)), -1, PREG_SPLIT_NO_EMPTY);
+        // Start with the pedit data_dir base.
+        $curr_path = $this->getParam('data_dir');
+        while (!empty($subdirs)) {
+            $curr_path .= '/' . array_shift($subdirs);
+            if (!is_dir($curr_path)) {
+                if (!mkdir($curr_path)) {
+                    $app->logMsg(sprintf('Failed mkdir: %s', $curr_path), LOG_ERR, __FILE__, __LINE__);
+                    return false;
+                }
+            }
+        }
+
+        // Open file for writing and truncate to zero length.
+        if ($fp = fopen($filename, 'w')) {
+            if (flock($fp, LOCK_EX)) {
+                fwrite($fp, $content);
+                flock($fp, LOCK_UN);
+            } else {
+                $app->logMsg(sprintf('Could not lock file for writing: %s', $filename), LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+            fclose($fp);
+            // Success!
+            $app->logMsg(sprintf('Wrote to file: %s', $filename), LOG_DEBUG, __FILE__, __LINE__);
+            return true;
+        } else {
+            $app->logMsg(sprintf('Could not open file for writing: %s', $filename), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Makes a copy of the current file with the unix timestamp appended to the
+     * filename.
+     *
+     * @access private
+     * @return bool  False on failure. True on success.
+     */
+    protected function _createVersion()
+    {
+        $app =& App::getInstance();
+
+        if (!$this->_authorized) {
+            return false;
+        }
+        if ($this->_fileHash() != getFormData('file_hash')) {
+            // Posted data is NOT for this file!
+            $app->logMsg(sprintf('File_hash does not match current file.', null), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Ensure current data file exists.
+        if (!file_exists($this->_data_file)) {
+            $app->logMsg(sprintf('Data file does not yet exist: %s', $this->_data_file), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Do the actual copy. File naming scheme must be consistent!
+        // filename.php.xml becomes filename.php__1124124128.xml
+        $version_file = sprintf('%s__%s.xml', preg_replace('/\.xml$/', '', $this->_data_file), time());
+        if (!copy($this->_data_file, $version_file)) {
+            $app->logMsg(sprintf('Failed copying new version: %s -> %s', $this->_data_file, $version_file), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        return true;
+    }
+
+    /*
+    * Delete all versions older than versions_min_days if there are more than versions_min_qty or 100.
+    *
+    * @access   public
+    * @return bool  False on failure. True on success.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    12 Apr 2006 11:08:11
+    */
+    protected function _deleteOldVersions()
+    {
+        $app =& App::getInstance();
+
+        $version_files = $this->_getVersions();
+        if (is_array($version_files) && sizeof($version_files) > $this->getParam('versions_min_qty')) {
+            // Pop oldest ones off bottom of array.
+            $oldest = array_pop($version_files);
+            // Loop while minimum X qty && minimum X days worth but never more than 100 qty.
+            while ((sizeof($version_files) > $this->getParam('versions_min_qty')
+            && $oldest['unixtime'] < mktime(date('H'), date('i'), date('s'), date('m'), date('d') - $this->getParam('versions_min_days'), date('Y')))
+            || sizeof($version_files) > 100) {
+                $del_file = dirname($this->_data_file) . '/' . $oldest['filename'];
+                if (!unlink($del_file)) {
+                    $app->logMsg(sprintf('Failed deleting version: %s', $del_file), LOG_ERR, __FILE__, __LINE__);
+                }
+                $oldest = array_pop($version_files);
+            }
+        }
+    }
+
+    /**
+     * Returns an array of all archived versions of the current file,
+     * sorted with newest versions at the top of the array.
+     *
+     * @access private
+     * @return array  Array of versions.
+     */
+    protected function _getVersions()
+    {
+        $version_files = array();
+        $dir_handle = opendir(dirname($this->_data_file));
+        $curr_file_preg_pattern = sprintf('/^%s__(\d+).xml$/', preg_quote(basename($_SERVER['PHP_SELF'])));
+        while ($dir_handle && ($version_file = readdir($dir_handle)) !== false) {
+            if (!preg_match('/^\./', $version_file) && !is_dir($version_file) && preg_match($curr_file_preg_pattern, $version_file, $time)) {
+                $version_files[] = array(
+                    'filename' => $version_file,
+                    'unixtime' => $time[1],
+                    'filesize' => filesize(dirname($this->_data_file) . '/' . $version_file)
+                );
+            }
+        }
+
+        if (is_array($version_files) && !empty($version_files)) {
+            array_multisort($version_files, SORT_DESC);
+            return $version_files;
+        } else {
+            return array();
+        }
+    }
+
+    /**
+     * Makes a version backup of the current file, then copies the specified
+     * archived version over the current file.
+     *
+     * @access private
+     * @param string $version    Unix timestamp of archived version to restore.
+     * @return bool  False on failure. True on success.
+     */
+    protected function _restoreVersion($version)
+    {
+        $app =& App::getInstance();
+
+        if (!$this->_authorized) {
+            return false;
+        }
+
+        // The file to restore.
+        $version_file = sprintf('%s__%s.xml', preg_replace('/\.xml$/', '', $this->_data_file), $version);
+
+        // Ensure specified version exists.
+        if (!file_exists($version_file)) {
+            $app->logMsg(sprintf('Cannot restore non-existent file: %s', $version_file), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Make certain a version is created.
+        if (!$this->_createVersion()) {
+            $app->logMsg(sprintf('Failed creating new version of file.', null), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Do the actual copy.
+        if (!copy($version_file, $this->_data_file)) {
+            $app->logMsg(sprintf('Failed copying old version: %s -> %s', $version_file, $this->_data_file), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Success!
+        $app->raiseMsg(sprintf(_("Page has been restored to version %s."), $version), MSG_SUCCESS, __FILE__, __LINE__);
+        return true;
+    }
+
+} // End class.
Index: tags/2.2.3/lib/PageNumbers.inc.php
===================================================================
--- tags/2.2.3/lib/PageNumbers.inc.php	(revision 632)
+++ tags/2.2.3/lib/PageNumbers.inc.php	(revision 632)
@@ -0,0 +1,370 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PageNumbers.inc.php
+ *
+ * The PageNumbers class provides a common abstracted interface to the
+ * multiple pages features. It sets the various numbers needed to display items
+ * on a page, and includes functions for working with these numbers.
+ * You must call set setTotalItems(), setPerPage() and setPageNumber() before calling calculate()
+ * this the other various values will be set automatically. Then you can call printPerPageLinks,
+ * printPageNumbers, etc, and use the various page object properties in your page templates.
+ *
+ * @author    Quinn Comendant <quinn@strangecode.com>
+ * @version   1.61
+ */
+
+require_once dirname(__FILE__) . '/Prefs.inc.php';
+
+class PageNumbers
+{
+
+    public $total_items;       // Total quantity of items.
+    public $total_pages;       // The total number of pages.
+    public $current_page = 1;  // Current page number.
+    public $first_item;        // The counter for the first item on this page (zero index).
+    public $last_item;         // The counter for the last item on this page (zero index).
+    public $max_num_links = 9; // The max number of links to show on page (odd numbers look best).
+    protected $_num_links;        // The number of links to show on page.
+    protected $_per_page = 25;    // Items per page.
+
+    // Flags to ensure all necessary values have been set before calling calculate().
+    public $set_per_page_initialized = false;
+    public $set_page_number_initialized = false;
+    public $set_total_items_initialized = false;
+
+    // These are initialized in the constructor.
+    public $per_page_options;
+    public $left_arrow;
+    public $left_arrow_disabled;
+    public $left_dbl_arrow;
+    public $left_dbl_arrow_disabled;
+    public $right_arrow;
+    public $right_arrow_disabled;
+    public $right_dbl_arrow;
+    public $right_dbl_arrow_disabled;
+
+    /**
+     * PageNumbers constructor. All arguments are depreciated. Use set* functions instead.
+     */
+    public function __construct()
+    {
+        // Default options for the quantity per page links.
+        $this->per_page_options = array(25, 50, 100, 200);
+
+        // Default options for the page number links.
+        $this->left_arrow = _("back");
+        $this->left_arrow_disabled = '<span style="color: #aaaaaa;">' . _("back") . '</span>';
+        $this->left_dbl_arrow = '<strong>&laquo;</strong>';
+        $this->left_dbl_arrow_disabled = '<span style="color: #aaaaaa;"><strong>&laquo;</strong></span>';
+        $this->right_arrow = _("next");
+        $this->right_arrow_disabled = '<span style="color: #aaaaaa;">' . _("next") . '</span>';
+        $this->right_dbl_arrow = '<strong>&raquo;</strong>';
+        $this->right_dbl_arrow_disabled = '<span style="color: #aaaaaa;"><strong>&raquo;</strong></span>';
+
+        // Default url base. This will be set manually after instantiation
+        // in special cases like using a /my/page/# scheme.
+        $this->url_base = $_SERVER['PHP_SELF'] . '?page_number=';
+
+        $this->prefs = new Prefs($_SERVER['PHP_SELF']);
+        $this->prefs->setParam(array('persistent' => false));
+    }
+
+    /**
+     * Set the number of items per page.
+     */
+    public function setPerPage($per_page, $default=25, $save_value=true)
+    {
+        // (1) By provided argument, if valid.
+        // (2) By saved preference, if available.
+        // (3) Set to default value if provided and valid.
+        // (4) Keep as Class default of 25.
+        if (is_numeric($per_page) && $per_page > 0) {
+            $this->_per_page = $per_page;
+            if ($save_value) {
+                $this->prefs->set('items_per_page', $this->_per_page);
+            }
+        } else if ($save_value && $this->prefs->exists('items_per_page')) {
+            $this->_per_page = (int)$this->prefs->get('items_per_page');
+        } else if (is_numeric($default) && $default > 0) {
+            $this->_per_page = $default;
+        }
+        $this->set_per_page_initialized = true;
+    }
+
+    /**
+     * Set the current page number.
+     */
+    public function setPageNumber($page_number, $save_value=true)
+    {
+        // (1) By provided argument, if valid.
+        // (2) By saved preference, if available.
+        // (3) Don't change from what was provided at class instantiation.
+        if (is_numeric($page_number)) {
+            if ($page_number < 1) {
+                // FIXME: How to go back around to the last page? Hmmm. Set to 1 for now.
+                $this->current_page = 1;
+            } else {
+                $this->current_page = $page_number;
+            }
+            if ($save_value) {
+                $this->prefs->set('page_number', $this->current_page);
+            }
+        } else if ($save_value && $this->prefs->exists('page_number')) {
+            $this->current_page = (int)$this->prefs->get('page_number');
+        }
+        $this->set_page_number_initialized = true;
+    }
+
+    /**
+     * Set the total number of items.
+     */
+    public function setTotalItems($total_items)
+    {
+        if (is_numeric($total_items) && $total_items > 0) {
+            $this->total_items = $total_items;
+        } else {
+            $this->total_items = 0;
+        }
+        $this->set_total_items_initialized = true;
+    }
+
+    /**
+     * After $total_items or other options are set, this function calculates
+     * all the other numbers needed. If you set any variables manually,
+     * for example if $page_number comes from
+     * some place other than the GET or POST array, you should call this
+     * function manually, otherwise it happens at object instantiation.
+     *
+     * @access public
+     */
+    public function calculate()
+    {
+        $app =& App::getInstance();
+
+        if (!$this->set_per_page_initialized) {
+            $app->logMsg(sprintf('set_per_page not initialized'), LOG_ERR, __FILE__, __LINE__);
+        }
+        if (!$this->set_page_number_initialized) {
+            $app->logMsg(sprintf('set_page_number not initialized'), LOG_ERR, __FILE__, __LINE__);
+        }
+        if (!$this->set_total_items_initialized) {
+            $app->logMsg(sprintf('set_total_items not initialized'), LOG_ERR, __FILE__, __LINE__);
+        }
+
+        // If the specified page exceeds total pages or is less than 1, set the page to 1.
+        if ($this->_per_page * $this->current_page >= $this->total_items + $this->_per_page || $this->_per_page * $this->current_page < 1) {
+            $this->current_page = 1;
+        }
+
+        // The first item to be shown on this page.
+        $this->first_item = ($this->current_page - 1) * $this->_per_page;
+
+        // The last item to be shown on this page.
+        if ($this->total_items < $this->current_page * $this->_per_page) {
+            $this->last_item = $this->total_items - 1;
+        } else {
+            $this->last_item = $this->current_page * $this->_per_page - 1;
+        }
+
+        // Zeroing. Just in case. Paranoia. Yeah, negative numbers perturb me.
+        if ($this->first_item < 1) {
+            $this->first_item = 0;
+        }
+        if ($this->last_item < 1) {
+            $this->last_item = 0;
+        }
+        if ($this->total_items < 1) {
+            $this->total_items = 0;
+        }
+
+        // The total number of pages.
+        $this->total_pages = ceil($this->total_items / $this->_per_page);
+
+        // Figure out how many page number links to print.
+        if ($this->total_pages >= $this->max_num_links) {
+            $this->_num_links = $this->max_num_links;
+        } else {
+            $this->_num_links = $this->total_pages;
+        }
+    }
+
+    /**
+     * Returns the SQL code to limit query to items that are on current page.
+     */
+    public function getLimitSQL()
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        if (is_numeric($this->first_item) && is_numeric($this->_per_page)) {
+            return ' LIMIT ' . $db->escapeString($this->first_item) . ', ' . $db->escapeString($this->_per_page) . ' ';
+        } else {
+            $app->logMsg(sprintf('Could not find SQL to LIMIT by %s %s.', $this->first_item, $this->_per_page), LOG_WARNING, __FILE__, __LINE__);
+            return '';
+        }
+    }
+
+    /**
+     * Prints links to change the number of items shown per page.
+     *
+     * @access public
+     */
+    public function printPerPageLinks($query_key='per_page')
+    {
+        $app =& App::getInstance();
+
+        $sp = '';
+        for ($i=0; $i<sizeof($this->per_page_options); $i++) {
+            if ($this->_per_page != $this->per_page_options[$i]) {
+                printf('%s<a href="%s">%s</a>',
+                    $sp,
+                    $app->oHREF($_SERVER['PHP_SELF'] . '?' . $query_key . '=' . $this->per_page_options[$i]),
+                    $this->per_page_options[$i]
+                );
+            } else {
+                echo $sp . '<strong>' . $this->per_page_options[$i] . '</strong>';
+            }
+            $sp = '&nbsp;';
+        }
+    }
+
+    /**
+     * Outputs an $app->oHREF compatible url that goes to the page $page_number.
+     * Depends on $this->base_url to build the url onto. This is used in the
+     * page_number.ihtml template.
+     *
+     * @param  int   $page_number    The page number this page will go to.
+     *
+     * @return string                The URL.
+     *
+     * @access public
+     */
+    public function getPageNumURL($page_number, $carry_args=null)
+    {
+        $app =& App::getInstance();
+
+        return $app->oHREF($this->url_base . $page_number, $carry_args);
+    }
+    public function printPageNumURL($page_number, $carry_args=null)
+    {
+        echo $this->getPageNumURL($page_number, $carry_args);
+    }
+
+    /**
+     * Returns an array of page number links.
+     *
+     * @access public
+     */
+    public function getPageNumbersArray($carry_args=null)
+    {
+        $page_numbers = array();
+
+        for ($i = 1; $i <= $this->total_pages; $i++) {
+            $page_numbers[] = array(
+                'number' => $i,
+                'url' => $this->getPageNumURL($i, $carry_args),
+                'current' => ($this->current_page == $i)
+            );
+        }
+
+        return $page_numbers;
+    }
+
+    /**
+     * Returns a string containing the page number links.
+     *
+     * @access public
+     */
+    public function getPageNumbers($carry_args=null)
+    {
+        $page_numbers_string = '';
+
+        if ($this->current_page > $this->total_pages - floor($this->_num_links / 2)) {
+            $high_num = $this->total_pages;
+            $low_num = $high_num - $this->_num_links + 1;
+        } else {
+            $low_num = $this->current_page - floor($this->_num_links / 2);
+            if ($low_num < 1) {
+                $low_num = 1;
+            }
+            $high_num = $low_num + $this->_num_links - 1;
+        }
+
+        if ($this->current_page != 1) {
+            // Print "first" and "previous" page links.
+            if ($this->left_dbl_arrow) {
+                $page_numbers_string .= sprintf('<a href="%s" title="%s">%s</a>&nbsp;', $this->getPageNumURL(1, $carry_args), _("Go to the first page"), $this->left_dbl_arrow);
+            }
+            if ($this->left_arrow) {
+                $page_numbers_string .= sprintf('<a href="%s" title="%s">%s</a>&nbsp;&nbsp;', $this->getPageNumURL($this->current_page - 1, $carry_args), _("Go back one page"), $this->left_arrow);
+            }
+            // Print links to specific page numbers before the current page.
+            for ($i = $low_num; $i < $this->current_page; $i++) {
+                $page_numbers_string .= sprintf('<a href="%s">%s</a>&nbsp;', $this->getPageNumURL($i, $carry_args), $i);
+            }
+        } else {
+            if ($this->left_dbl_arrow) {
+                $page_numbers_string .= $this->left_dbl_arrow_disabled . '&nbsp;';
+            }
+            if ($this->left_arrow) {
+                $page_numbers_string .= $this->left_arrow_disabled . '&nbsp;';
+            }
+        }
+
+        if ($this->_num_links > 0) {
+            // Print the current page number.
+            $page_numbers_string .= sprintf('<strong>%s</strong>&nbsp;', $this->current_page);
+        }
+
+        if ($this->current_page < $this->total_pages) {
+            // Print links to specific page numbers after the current page.
+            for ($i = $this->current_page + 1; $i <= $high_num; $i++) {
+                $page_numbers_string .= sprintf('<a href="%s">%s</a>&nbsp;', $this->getPageNumURL($i, $carry_args), $i);
+            }
+            // Print "last" and "next" page links.
+            if ($this->right_arrow) {
+                $page_numbers_string .= sprintf('<a href="%s" title="%s">%s</a>&nbsp;&nbsp;', $this->getPageNumURL($this->current_page + 1, $carry_args), _("Go forward one page"), $this->right_arrow);
+            }
+            if ($this->right_dbl_arrow) {
+                $page_numbers_string .= sprintf('<a href="%s" title="%s">%s</a>&nbsp;&nbsp;', $this->getPageNumURL($this->total_pages, $carry_args), _("Go to the last page"), $this->right_dbl_arrow);
+            }
+        } else {
+            if ($this->right_arrow_disabled) {
+                $page_numbers_string .= $this->right_arrow_disabled . '&nbsp;';
+            }
+            if ($this->right_dbl_arrow_disabled) {
+                $page_numbers_string .= $this->right_dbl_arrow_disabled;
+            }
+        }
+
+        return $page_numbers_string;
+    }
+
+    public function printPageNumbers($carry_args=null)
+    {
+        echo $this->getPageNumbers($carry_args);
+    }
+
+}
+
Index: tags/2.2.3/lib/PageSequence.inc.php
===================================================================
--- tags/2.2.3/lib/PageSequence.inc.php	(revision 632)
+++ tags/2.2.3/lib/PageSequence.inc.php	(revision 632)
@@ -0,0 +1,384 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PageSequence.inc.php
+ *
+ * The PageSequence class provides an interface to simplify the creation of a multi-step form.
+ *
+ * @requires  This class requires Prefs.inc.php
+ * @author    Quinn Comendant <quinn@strangecode.com>
+ * @version   1.01
+ */
+
+require_once dirname(__FILE__) . '/Prefs.inc.php';
+
+class PageSequence
+{
+    public $current_step_id = 0;
+    public $start_url;
+    public $sequence_title = '';
+    public $seq = '_sequence_';
+    public $idle_timeout;
+
+    /**
+     * Constructor. Sets the title of this sequence and initializes session variables.
+     *
+     * @param  array $params   An associative array of PageSequence parameters.
+     * @access public
+     */
+    public function __construct($params)
+    {
+        if (isset($params['sequence_title'])) {
+            $this->sequence_title = $params['sequence_title'];
+            $this->seq .= $params['sequence_title'];
+        }
+
+        // How long before resetting session? 60 minutes.
+        $this->idle_timeout = isset($params['idle_timeout']) ? $params['idle_timeout'] : 3600;
+
+        // Where is user to be redirected after startOver function call?
+        $this->start_url = isset($params['start_url']) ? $params['start_url'] : $_SERVER['PHP_SELF'];
+
+        // Initialize vars if not set.
+        if (!isset($_SESSION[$this->seq]['steps'])) {
+            $_SESSION[$this->seq]['steps'] = array();
+        }
+        if (!isset($_SESSION[$this->seq]['data'])) {
+            $_SESSION[$this->seq]['data'] = array();
+        }
+        if (!isset($_SESSION[$this->seq]['defaults'])) {
+            $_SESSION[$this->seq]['defaults'] = array();
+        }
+
+        // Manage timeout.
+        $this->_auto_timeout();
+    }
+
+    /**
+     * Create a new step at the end (or specified position) of the $steps array.
+     *
+     * @param  string $step_id      Uniqie identifyer for this step.
+     * @param  array  $params       Values for the creation of the step.
+     *
+     * @return boolean    Success of the addition
+     * @access public
+     */
+    public function addStep($step_id, $params)
+    {
+        // Keys for the steps array cannot be numeric.
+        if (is_numeric($step_id)) {
+            trigger_error('Step ID for function addStep cannot be numeric.', E_USER_WARNING);
+        }
+        // This step ID already exists.
+        if (!is_null($this->getPosition($step_id))) {
+            return false;
+        }
+        $_SESSION[$this->seq]['steps'][] = array(
+            'id'        => $step_id,
+            'title'     => isset($params['title'])     ? $params['title']     : '',
+            'active'    => isset($params['active'])    ? $params['active']    : false,
+            'form_tpl'  => isset($params['form_tpl'])  ? $params['form_tpl']  : '',
+            'disp_tpl'  => isset($params['disp_tpl'])  ? $params['disp_tpl']  : '',
+            'required'  => isset($params['required'])  ? $params['required']  : false,
+            'completed' => isset($params['completed']) ? $params['completed'] : false,
+            'editable'  => isset($params['editable'])  ? $params['editable']  : true,
+        );
+    }
+
+    /**
+     * Set the features of a step. Current step if step_id not specified.
+     *
+     * @param  string $page_id    ID of page. Leave null if modifying current step features.
+     * @param  array  $feature    Array of feature keys and values to set.
+     *
+     * @return bool true on success, false on failure
+     */
+    public function setFeature($step_id=null, $features)
+    {
+        $pos = isset($step_id) ? $this->getPosition($step_id) : $this->getPosition();
+
+        if (isset($features) && is_array($features) && isset($_SESSION[$this->seq]['steps'][$pos])) {
+            $_SESSION[$this->seq]['steps'][$pos] = array_merge($_SESSION[$this->seq]['steps'][$pos], $features);
+        }
+    }
+
+    /**
+     * Returns a specified value from a registered step.
+     *
+     * @access public
+     *
+     * @param string $step_id  Id of step with feature to return.
+     * @param string $key      Which value to return.
+     * @param mixed  $default  Value to return if key not found in user_data.
+     *
+     * @return mixed          Value stored in session.
+     */
+    public function getFeature($step_id, $key, $default='')
+    {
+        $pos = isset($step_id) ? $this->getPosition($step_id) : $this->getPosition();
+        if (isset($_SESSION[$this->seq]['steps'][$pos][$key])) {
+            return $_SESSION[$this->seq]['steps'][$pos][$key];
+        } else {
+            return $default;
+        }
+    }
+
+    /**
+     * Set the current step id. Which step are we on?
+     *
+     * @param  string $step_id      ID or number of current step.
+     * @return string    Actual step ID
+     * @access public
+     */
+    public function setCurrent($step_id)
+    {
+        $app =& App::getInstance();
+
+        if (false !== ($pos = $this->getPosition($step_id))) {
+            // Specified step exists (even if numeric).
+            $this->current_step_id = $this->getID($pos);
+        } else {
+            // Step with specified key does not exist.
+            $app->logMsg(sprintf('Step %s not defined in sequence %s', $step_id, $this->sequence_title), LOG_INFO, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Get the current step id.
+     *
+     * @return int $pos    Actual step position
+     * @access public
+     */
+    public function getID($pos=null)
+    {
+        if (isset($pos)) {
+            return $_SESSION[$this->seq]['steps'][$pos]['id'];
+        } else {
+            return $this->current_step_id;
+        }
+    }
+
+    /**
+     * Get the current step number.
+     *
+     * @param  string $step_id      ID or number of step to convert to position.
+     *
+     * @return string               Actual step number
+     * @access public
+     */
+    public function getPosition($step_id=null)
+    {
+        // Get current step id if step not provided.
+        if (!isset($step_id)) {
+            $step_id = $this->current_step_id;
+        }
+        if (is_numeric($step_id) && isset($_SESSION[$this->seq]['steps'][$step_id])) {
+            // Step ID provided is a number...go directly to key.
+            return $step_id;
+        } else {
+            // Step ID is a string. Loop through searching for match.
+            foreach ($_SESSION[$this->seq]['steps'] as $pos=>$step) {
+                if ($step['id'] == $step_id) {
+                    return $pos;
+                }
+            }
+            // Step not found matching ID.
+            return null;
+        }
+    }
+
+    /**
+     * Returns the ID of the step with required=true and completed=false
+     * and active=true that falls before specified step_id.
+     *
+     * @param  string $curr_step_id ID or number of current step.
+     * @return string               Prerequisite step ID or false if none.
+     * @access public
+     */
+    public function getRequiredID($curr_step_id)
+    {
+        if ('' === $curr_step_id) {
+            return $this->current_step_id;
+        }
+        foreach ($_SESSION[$this->seq]['steps'] as $pos=>$step) {
+            if (is_numeric($curr_step_id) && $pos == $curr_step_id && $step['active']) {
+                return $curr_step_id;
+            } else if ($step['id'] == $curr_step_id && $step['active']) {
+                return $curr_step_id;
+            }
+            if ($step['active'] && $step['required'] && !$step['completed']) {
+                return $step['id'];
+            }
+        }
+        return $curr_step_id;
+    }
+
+    /**
+     * Returns the next step in the steps array or the first active required
+     * uncompleted step.
+     *
+     * @return string  Step identifier of the next step.
+     * @access public
+     */
+    public function getNextID()
+    {
+        // Loop through all steps.
+        foreach ($_SESSION[$this->seq]['steps'] as $pos=>$step) {
+            // If a step is found that is active, required, and not completed, we must do that one.
+            if ($step['active'] && $step['required'] && !$step['completed']) {
+                return $step['id'];
+            }
+            // Otherwise do the first step after the current one that is active.
+            if ($step['active'] && $pos > $this->getPosition()) {
+                return $step['id'];
+            }
+        }
+        return null;
+    }
+
+    /**
+     * To set a set as 'completed'.
+     * @return string  Step identifier of the next step.
+     * @access public
+     */
+    public function complete($step_id=null)
+    {
+        $pos = isset($step_id) ? $this->getPosition($step_id) : $this->getPosition();
+        $_SESSION[$this->seq]['steps'][$pos]['completed'] = true;
+    }
+
+    /**
+     * Prints the a link that returns to the form for a step.
+     *
+     * @param  string $form_type  'form' or 'disp'.
+     * @param  string $step_id    ID of step.
+     * @return string             Filename of template.
+     * @access public
+     */
+    public function printEditLink($step_id=null)
+    {
+        $app =& App::getInstance();
+
+        $pos = isset($step_id) ? $this->getPosition($step_id) : $this->getPosition();
+        if ($_SESSION[$this->seq]['steps'][$pos]['editable']) {
+            printf('[<a href="%s">%s</a>]', $app->oHREF($_SERVER['PHP_SELF'] . '?step=' . $pos . '&boomerang=confirmation'), _("edit"));
+        }
+    }
+
+    /**
+     * Saves given $step_data (usually coming from $_POST) into $_SESSION
+     * array with the $step_id as the key.
+     *
+     * @param  string $step_id   ID of current step.
+     * @param  mixed  $step_data Data to place into session storage.
+     * @return string  Step identifier of the next step.
+     * @access public
+     */
+    public function setDataDefault($data_key, $data_val)
+    {
+        if (!isset($_SESSION[$this->seq]['data'][$data_key])) {
+            $_SESSION[$this->seq]['data'][$data_key] = $data_val;
+            $_SESSION[$this->seq]['defaults'][$data_key] = $data_val;
+        }
+    }
+
+    /**
+     * Returns the value saved in $_SESSION for a specific data key.
+     *
+     * @param  mixed  $data_key  Key of data to return from session data.
+     * @access public
+     */
+    public function getData($data_key=null)
+    {
+        if (!isset($data_key)) {
+            return $_SESSION[$this->seq]['data'];
+        }
+        if (isset($_SESSION[$this->seq]['data'][$data_key])) {
+            return $_SESSION[$this->seq]['data'][$data_key];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Deletes all data saved in $_SESSION.
+     *
+     * @param  mixed  $data_key  Key of data to return from session data.
+     * @access public
+     */
+    public function clearData($data_key=null)
+    {
+        if (isset($data_key)) {
+            // Clear a specific key.
+            $_SESSION[$this->seq]['data'][$key] = array();
+        } else {
+            // Clear the whole thing.
+            $_SESSION[$this->seq]['data'] = array();
+        }
+    }
+
+    /**
+     * Deletes all data that are older than auto_timeout. Set current time if not not expired or not set.
+     */
+    protected function _auto_timeout()
+    {
+        $app =& App::getInstance();
+
+        if (isset($_SESSION[$this->seq]['last_access_time'])
+        && $_SESSION[$this->seq]['last_access_time'] < time() - $this->idle_timeout) {
+            // Session has expired, flush all vars to start over.
+            $this->startOver();
+            $app->dieURL($this->start_url);
+        } else {
+            // Set timer.
+            $_SESSION[$this->seq]['last_access_time'] = time();
+        }
+    }
+
+    /**
+     * Saves given array (usually coming from $_POST) into $_SESSION
+     *
+     * @param  mixed  $step_data  Array of data to merge with session data.
+     * @access public
+     */
+    public function registerData($step_data)
+    {
+        $_SESSION[$this->seq]['data'] = array_merge($_SESSION[$this->seq]['data'], $step_data);
+    }
+
+    /**
+     * Reset all vars.
+     *
+     */
+    public function startOver()
+    {
+        $this->current_step_id = 0;
+        $_SESSION[$this->seq]['steps'] = array();
+        $_SESSION[$this->seq]['data'] = array();
+        $_SESSION[$this->seq]['defaults'] = array();
+        $_SESSION[$this->seq]['last_access_time'] = time();
+    }
+
+} // END CLASS
+
Index: tags/2.2.3/lib/PayPal.inc.php
===================================================================
--- tags/2.2.3/lib/PayPal.inc.php	(revision 632)
+++ tags/2.2.3/lib/PayPal.inc.php	(revision 632)
@@ -0,0 +1,382 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PayPal.inc.php
+ *
+ * The PayPal class provides functions for creating PayPal buttons and for
+ * receiving PayPal's Instant Payment Notification (IPN) service.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.0
+ */
+class PayPal
+{
+
+    // General object parameters.
+    protected $_params = array(
+        'paypal_url' => 'https://www.paypal.com/cgi-bin/webscr',
+        'test_mode' => false,
+    );
+
+    // Options used for specific buttons and links.
+    protected $_default_button_options = array();
+
+    // Array of buttons created by newButton().
+    protected $_buttons = array();
+
+    // Store the response from the last IPN.
+    protected $_ipn_response;
+
+    /**
+     * Constructor.
+     *
+     * @param   bool    $test_mode  Use PayPal sandbox for testing.
+     */
+    public function __construct($test_mode=false)
+    {
+        if ($test_mode) {
+            $this->setParam(array('test_mode' => true));
+            // Use PayPal sandbox when in test mode.
+            $url = 'www.sandbox.paypal.com';
+        } else {
+            $url = 'www.paypal.com';
+        }
+
+        $this->_default_button_options = array(
+            '_global' => array(
+                'business' => null,
+            ),
+            'web_accept' => array(
+                'cmd' => '_xclick',
+                'button_url' => 'https://' . $url . '/cgi-bin/webscr',
+                'link_url' => 'https://' . $url . '/xclick/',
+                'submit_img' => 'https://' . $url . '/en_US/i/btn/x-click-but23.gif',
+                'submit_text' => _("Pay with PayPal"),
+            ),
+            'subscriptions' => array(
+                'cmd' => '_xclick-subscriptions',
+                'button_url' => 'https://' . $url . '/cgi-bin/webscr',
+                'link_url' => 'https://' . $url . '/subscriptions/',
+                'submit_img' => 'https://' . $url . '/en_US/i/btn/x-click-but20.gif',
+                'submit_text' => _("Subscribe with PayPal"),
+            ),
+        );
+    }
+
+    /**
+     * Updates the _default_button_options array with options used for
+     * specific buttons, or all buttons if $type is null.
+     *
+     * @access  public
+     *
+     * @param   mixed   $type       The type of button to set defaults. If null,
+     *                              sets the global button types.
+     * @param   array   $options    Options to set for button.
+     *
+     * @return  bool    True on success, false on failure.
+     */
+    public function setButtonDefaults($type, $options)
+    {
+        $app =& App::getInstance();
+
+        if (!is_array($options) || empty($options)) {
+            $app->logMsg(sprintf('Invalid options: %s', truncate(getDump($options, true), 128, 'end')), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (is_null($type) || '_global' == $type) {
+            $this->_default_button_options['_global'] = array_merge($this->_default_button_options['_global'], $options);
+        } else if (!isset($this->_default_button_options[$type])) {
+            $app->logMsg(sprintf('Invalid button type: %s', $type), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        $this->_default_button_options[$type] = array_merge($this->_default_button_options[$type], $options);
+        return true;
+    }
+
+    /**
+     * Creates a new element in the _buttons array. Uses _default_button_options
+     * merged with provided options.
+     *
+     * @access  public
+     *
+     * @param   string  $type       Type of button to create.
+     * @param   string  $name       Name of button to create.
+     * @param   array   $options    Options of button. Overwrites _default_button_options.
+     *
+     * @return  bool    True on success, false on failure.
+     */
+    public function newButton($type, $name, $options=null)
+    {
+        $app =& App::getInstance();
+
+        if (!isset($this->_default_button_options[$type])) {
+            $app->logMsg(sprintf('Invalid button type: %s', $type), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (!is_array($options) || empty($options)) {
+            $app->logMsg(sprintf('Invalid options: %s', truncate(getDump($options, true), 128, 'end')), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (isset($this->_buttons[$name])) {
+            $app->logMsg(sprintf('Overwriting existing button name: %s', truncate(getDump($this->_buttons[$name], true), 128, 'end')), LOG_DEBUG, __FILE__, __LINE__);
+        }
+
+        $this->_buttons[$name] = array(
+            'type' => $type,
+            'options' => array_merge($this->_default_button_options['_global'], $this->_default_button_options[$type], $options)
+        );
+
+        return true;
+    }
+
+    /**
+     * Returns the URL link for specified button.
+     *
+     * @access  public
+     *
+     * @param   string  $name   Name of button for which to generate link.
+     *
+     * @return  mixed   Link of button, or false on failure.
+     */
+    public function getLink($name)
+    {
+        $app =& App::getInstance();
+
+        if (!isset($this->_buttons[$name])) {
+            $app->logMsg(sprintf('Button does not exist: %s', $name), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        $query_string = '';
+        $delim = '';
+        if (is_array($this->_buttons[$name]['options']) && !empty($this->_buttons[$name]['options'])) {
+            foreach ($this->_buttons[$name]['options'] as $key=>$val) {
+                if (!in_array($key, array('button_url', 'link_url', 'cmd', 'submit_img', 'submit_text'))) {
+                    $query_string .= $delim . $key . '=' . urlencode($val);
+                    $delim = '&';
+                }
+            }
+        }
+
+        // PayPal links do not like urlencoded slashes for some stupid reason.
+        $search = array('/%2F/');
+        $replace = array('/');
+
+        return $this->_buttons[$name]['options']['link_url'] . preg_replace($search, $replace, $query_string);
+    }
+
+    /**
+     * Prints the link returned by getLink().
+     *
+     * @access  public
+     *
+     * @param   string  $name   Name of button for which to generate link.
+     */
+    public function printLink($name)
+    {
+        echo $this->getLink($name);
+    }
+
+    /**
+     * Prints button with specified name.
+     *
+     * @access  public
+     *
+     * @param   string  $name   Name of button to print.
+     */
+    public function printButton($name)
+    {
+        ?>
+        <form action="<?php echo $this->_buttons[$name]['options']['button_url']; ?>" method="post">
+        <?php
+        if (is_array($this->_buttons[$name]['options']) && !empty($this->_buttons[$name]['options'])) {
+            foreach ($this->_buttons[$name]['options'] as $key=>$val) {
+                if (!in_array($key, array('button_url', 'link_url', 'submit_img', 'submit_text'))) {
+                    ?>
+                    <input type="hidden" name="<?php echo $key; ?>" value="<?php echo $val; ?>" />
+                    <?php
+                }
+            }
+        }
+        ?>
+        <input type="image" src="<?php echo $this->_buttons[$name]['options']['submit_img']; ?>" border="0" name="submit" alt="<?php echo $this->_buttons[$name]['options']['submit_text']; ?>" />
+        </form>
+        <?php
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+    
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Tests if the HTTP request is a valid IPN request from PayPal.
+     *
+     * @access  public
+     *
+     * @return  bool    True if valid, false if invalid.
+     */
+    public function incomingIPNRequest()
+    {
+        if ($_SERVER['REQUEST_METHOD'] == 'POST'
+        && $_SERVER['CONTENT_TYPE'] == 'application/x-www-form-urlencoded'
+        && !empty($_POST)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Process incoming IPN.
+     *
+     * @access  public
+     *
+     * @return  bool    True on success, false on failure.
+     */
+    public function processIPN()
+    {
+        $app =& App::getInstance();
+
+        if (getPost('test_ipn') == '1' || $this->getParam('test_mode')) {
+            $app->logMsg(sprintf('Processing PayPal IPN in test mode: %s', truncate(getDump(getFormData(), true), 128, 'end')), LOG_DEBUG, __FILE__, __LINE__);
+            $url = parse_url('https://www.sandbox.paypal.com/cgi-bin/webscr');
+        } else {
+            $app->logMsg(sprintf('Processing PayPal IPN: %s', truncate(getDump(getFormData(), true), 128, 'end')), LOG_DEBUG, __FILE__, __LINE__);
+            $url = parse_url($this->getParam('paypal_url'));
+        }
+
+        // Read POST request and add 'cmd'.
+        $received_data = getPost();
+        $return_data = 'cmd=_notify-validate';
+        foreach ($received_data as $post_key => $post_val) {
+            $return_data .= '&' . $post_key . '=' . urlencode($post_val);
+        }
+
+        // Set the port number based on the scheme.
+        if ($url['scheme'] == "https") {
+            $url['port'] = 443;
+            $ssl = 'ssl://';
+        } else {
+            $url['port'] = 80;
+            $ssl = '';
+        }
+
+        // Open connection to PayPal server.
+        $fp = fsockopen($ssl . $url['host'], $url['port'], $errnum, $errstr, 30);
+
+        if (!$fp) {
+            $app->logMsg(sprintf('Connection to PayPal URL %s failed with error: %s (%s)', $ssl . $url['host'], $errstr, $errnum), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        } else {
+            fputs($fp, "POST {$url['path']} HTTP/1.1\r\n");
+            fputs($fp, "Host: {$url['host']}\r\n");
+            fputs($fp, "Content-type: application/x-www-form-urlencoded\r\n");
+            fputs($fp, "Content-length: " . mb_strlen($return_data) . "\r\n");
+            fputs($fp, "Connection: close\r\n\r\n");
+            fputs($fp, $return_data . "\r\n\r\n");
+
+            // Loop through the response lines from the server.
+            $this->_ipn_response = '';
+            while (!feof($fp)) {
+                $this->_ipn_response .= fgets($fp, 1024);
+            }
+            fclose($fp);
+
+            $app->logMsg(sprintf('IPN response received: %s', $this->_ipn_response), LOG_NOTICE, __FILE__, __LINE__);
+            return true;
+        }
+    }
+
+    /**
+     * Checks the response received from PayPal's IPN upon calling processIPN().
+     *
+     * @access  public
+     *
+     * @return  bool    True if response contains VERIFIED, false otherwise.
+     */
+    public function verifiedIPN()
+    {
+        $app =& App::getInstance();
+
+        if (!isset($this->_ipn_response)) {
+            $app->logMsg(sprintf('Cannot verify IPN, response not received.', null), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (empty($this->_ipn_response)) {
+            $app->logMsg(sprintf('Cannot verify IPN, response empty.', null), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (preg_match('/VERIFIED/', $this->_ipn_response)) {
+            $app->logMsg(sprintf('IPN verified!', null), LOG_DEBUG, __FILE__, __LINE__);
+            return true;
+        } else if (preg_match('/INVALID/', $this->_ipn_response)) {
+            $app->logMsg(sprintf('IPN invalid.', null), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        } else {
+            $app->logMsg(sprintf('IPN unknown.', null), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+
+} // End of class.
+
Index: tags/2.2.3/lib/Prefs.inc.php
===================================================================
--- tags/2.2.3/lib/Prefs.inc.php	(revision 632)
+++ tags/2.2.3/lib/Prefs.inc.php	(revision 632)
@@ -0,0 +1,602 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Prefs.inc.php
+ *
+ * Prefs provides an API for saving arbitrary values in a user's session, in cookies, and in the database.
+ * Prefs can be stored into a database with the optional save() and load() methods.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 3.0
+ * @todo This class could really benefit from being refactored using the factory pattern, with backend storage mechanisms.
+ *
+ * Example of use (database storagetype):
+---------------------------------------------------------------------
+// Load preferences for the user's session.
+require_once 'codebase/lib/Prefs.inc.php';
+$prefs = new Prefs('my-namespace');
+$prefs->setParam(array(
+    'storagetype' => ($auth->isLoggedIn() ? 'database' : 'session'),
+    'user_id' => $auth->get('user_id'),
+));
+$prefs->setDefaults(array(
+    'search_num_results' => 25,
+    'datalog_num_entries' => 25,
+));
+$prefs->load();
+
+// Update preferences. Make sure to validate this input first!
+$prefs->set('search_num_results', getFormData('search_num_results'));
+$prefs->set('datalog_num_entries', getFormData('datalog_num_entries'));
+$prefs->save();
+---------------------------------------------------------------------
+ */
+class Prefs
+{
+
+    // Namespace of this instance of Prefs.
+    protected $_ns;
+
+    // Configuration parameters for this object.
+    protected $_params = array(
+
+        // Store preferences in one of the available storage mechanisms: session, cookie, database
+        // This default should remain set to 'session' for legacy support.
+        'storagetype' => 'session',
+
+        // This parameter is only used for legacy support, superseded by the 'storagetype' setting.
+        // Enable database storage. If this is false, all prefs will live only as long as the session.
+        'persistent' => null,
+
+        // ----------------------------------------------------------
+        // Cookie-type settings.
+
+        // Lifespan of the cookie. If set to an integer, interpreted as a timestamp (0 for 'when user closes browser'), otherwise as a strtotime-compatible value ('tomorrow', etc).
+        'cookie_expire' => '+10 years',
+
+        // The path on the server in which the cookie will be available on.
+        'cookie_path' => null,
+
+        // The domain that the cookie is available to.
+        'cookie_domain' => null,
+
+        // ----------------------------------------------------------
+        // Database-type settings.
+
+        // The current user_id for which to load/save database-backed preferences.
+        'user_id' => null,
+
+        // How long before we force a reload of the persistent prefs data? 300 = every five minutes.
+        'load_timeout' => 300,
+
+        // Name of database table to store prefs.
+        'db_table' => 'pref_tbl',
+
+        // Automatically create table and verify columns. Better set to false after site launch.
+        // This value is overwritten by the $app->getParam('db_create_tables') setting if it is available.
+        'create_table' => true,
+    );
+
+    /**
+     * Prefs constructor.
+     */
+    public function __construct($namespace='', array $params=null)
+    {
+        $app =& App::getInstance();
+
+        $this->_ns = $namespace;
+
+        // Get create tables config from global context.
+        if (!is_null($app->getParam('db_create_tables'))) {
+            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
+        }
+
+        // Optional initial params.
+        $this->setParam($params);
+
+        // Initialized the prefs array.
+        if ('cookie' != $app->getParam('storagetype') && !isset($_SESSION['_prefs'][$this->_ns]['saved'])) {
+            $this->clear();
+        }
+
+        // Run Prefs->save() upon script completion if we're using the database storagetype.
+        // This only works if 'storagetype' is provided as a parameter to the constructor rather than via setParam() later.
+        if ('database' == $this->getParam('storagetype')) {
+            register_shutdown_function(array($this, 'save'));
+        }
+    }
+
+    /**
+     * Setup the database table for this class.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   04 Jun 2006 16:41:42
+     */
+    public function initDB($recreate_db=false)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        static $_db_tested = false;
+
+        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
+            if ($recreate_db) {
+                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
+                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__);
+            }
+            $db->query("CREATE TABLE IF NOT EXISTS " . $db->escapeString($this->getParam('db_table')) . " (
+                user_id VARCHAR(32) NOT NULL DEFAULT '',
+                pref_namespace VARCHAR(32) NOT NULL DEFAULT '',
+                pref_key VARCHAR(64) NOT NULL DEFAULT '',
+                pref_value TEXT,
+                PRIMARY KEY (user_id, pref_namespace, pref_key)
+            )");
+
+            if (!$db->columnExists($this->getParam('db_table'), array(
+                'user_id',
+                'pref_namespace',
+                'pref_key',
+                'pref_value',
+            ), false, false)) {
+                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
+                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
+            }
+        }
+        $_db_tested = true;
+    }
+
+    /**
+     * Set the params of this object.
+     *
+     * @param  array $params   Array of param keys and values to set.
+     */
+    public function setParam(array $params=null)
+    {
+        $app =& App::getInstance();
+
+        // CLI scripts can't use prefs stored in HTTP-based protocols.
+        if ($app->cli
+        && isset($params['storagetype'])
+        && in_array($params['storagetype'], array('cookie', 'session'))) {
+            $app->logMsg(sprintf('Storage type %s not available for CLI', $params['storagetype']), LOG_NOTICE, __FILE__, __LINE__);
+        }
+
+        // Convert the legacy param 'persistent' to 'storagetype=database'.
+        // Old sites would set 'persistent' to true (use database) or false (use sessions).
+        // If it is true, we set storagetype=database here.
+        // If false, we rely on the default, sessions (which is assigned in the params).
+        if (isset($params['persistent']) && $params['persistent'] && !isset($params['storagetype'])) {
+            $params['storagetype'] = 'database';
+        }
+
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Sets the default values for preferences. If a preference is not explicitly
+     * set, the value set here will be used. Can be called multiple times to merge additional
+     * defaults together. This is mostly only useful for the database storagetype, when you have
+     * values you want to use as default, and those are not stored to the database (so the defaults
+     * can be changed later and apply to all users who haven't make s specific setting).
+     * For the cookie storagetype, using setDefaults just sets cookies but only if a cookie with
+     * the same name is not already set.
+     *
+     * @param  array $defaults  Array of key-value pairs
+     */
+    public function setDefaults($defaults)
+    {
+        $app =& App::getInstance();
+
+        if (isset($defaults) && is_array($defaults)) {
+            switch ($this->getParam('storagetype')) {
+            case 'session':
+            case 'database':
+                $_SESSION['_prefs'][$this->_ns]['defaults'] = array_merge($_SESSION['_prefs'][$this->_ns]['defaults'], $defaults);
+                break;
+
+            case 'cookie':
+                foreach ($defaults as $key => $val) {
+                    if (!$this->exists($key)) {
+                        $this->set($key, $val);
+                    }
+                }
+                unset($key, $val);
+                break;
+            }
+        } else {
+            $app->logMsg(sprintf('Wrong data-type passed to Prefs->setDefaults().', null), LOG_NOTICE, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Store a key-value pair.
+     * When using the database storagetype, if the value is different than what is set by setDefaults the value will be scheduled to be saved in the database.
+     *
+     * @param  string $key          The name of the preference to modify.
+     * @param  string $val          The new value for this preference.
+     */
+    public function set($key, $val)
+    {
+        $app =& App::getInstance();
+
+        if (!is_string($key)) {
+            $app->logMsg(sprintf('Key is not a string-compatible type (%s)', getDump($key)), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+        if ('' == trim($key)) {
+            $app->logMsg(sprintf('Key is empty (along with value: %s)', $val), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+        if (!is_scalar($val) && !is_array($val) && !is_object($val)) {
+            $app->logMsg(sprintf('Value is not a compatible data type (%s=%s)', $key, getDump($val)), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        switch ($this->getParam('storagetype')) {
+        // Both session and database prefs are saved in the session (for database, only temporarily until they are saved).
+        case 'session':
+        case 'database':
+            // Set a saved preference if...
+            // - there isn't a default.
+            // - or the new value is different than the default
+            // - or there is a previously existing saved key.
+            if (!(isset($_SESSION['_prefs'][$this->_ns]['defaults']) && array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['defaults']))
+            || $_SESSION['_prefs'][$this->_ns]['defaults'][$key] != $val
+            || (isset($_SESSION['_prefs'][$this->_ns]['saved']) && array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['saved']))) {
+                $_SESSION['_prefs'][$this->_ns]['saved'][$key] = $val;
+                $app->logMsg(sprintf('Setting session preference %s => %s', $key, getDump($val, true)), LOG_DEBUG, __FILE__, __LINE__);
+            } else {
+                $app->logMsg(sprintf('Not setting session preference %s => %s', $key, getDump($val, true)), LOG_DEBUG, __FILE__, __LINE__);
+            }
+            break;
+
+        case 'cookie':
+            $name = $this->_getCookieName($key);
+            $val = json_encode($val);
+            $app->setCookie($name, $val, $this->getParam('cookie_expire'), $this->getParam('cookie_path'), $this->getParam('cookie_domain'));
+            $_COOKIE[$name] = $val;
+            $app->logMsg(sprintf('Setting cookie preference %s => %s', $key, $val), LOG_DEBUG, __FILE__, __LINE__);
+            break;
+        }
+
+    }
+
+    /**
+     * Returns the value of the requested preference. Saved values take precedence, but if none is set
+     * a default value is returned, or if not that, null.
+     *
+     * @param string $key       The name of the preference to retrieve.
+     *
+     * @return string           The value of the preference.
+     */
+    public function get($key)
+    {
+        $app =& App::getInstance();
+
+        switch ($this->getParam('storagetype')) {
+        case 'session':
+        case 'database':
+            if (isset($_SESSION['_prefs'][$this->_ns]['saved']) && array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['saved'])) {
+                $app->logMsg(sprintf('Found %s in saved', $key), LOG_DEBUG, __FILE__, __LINE__);
+                return $_SESSION['_prefs'][$this->_ns]['saved'][$key];
+            } else if (isset($_SESSION['_prefs'][$this->_ns]['defaults']) && array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['defaults'])) {
+                $app->logMsg(sprintf('Found %s in defaults', $key), LOG_DEBUG, __FILE__, __LINE__);
+                return $_SESSION['_prefs'][$this->_ns]['defaults'][$key];
+            } else {
+                $app->logMsg(sprintf('Key not found in prefs cache: %s', $key), LOG_DEBUG, __FILE__, __LINE__);
+            }
+            return null;
+
+        case 'cookie':
+            $name = $this->_getCookieName($key);
+            if ($this->exists($key) && '' != $_COOKIE[$name]) {
+                $val = json_decode($_COOKIE[$name], true);
+                $app->logMsg(sprintf('Found %s in cookie: %s', $key, getDump($val)), LOG_DEBUG, __FILE__, __LINE__);
+                return $val;
+            } else {
+                $app->logMsg(sprintf('Did not find %s in cookie', $key), LOG_DEBUG, __FILE__, __LINE__);
+            }
+            return null;
+        }
+    }
+
+    /**
+     * To see if a preference has been set.
+     *
+     * @param string $key       The name of the preference to check.
+     * @return boolean          True if the preference isset and not empty false otherwise.
+     */
+    public function exists($key)
+    {
+        switch ($this->getParam('storagetype')) {
+        case 'session':
+        case 'database':
+            return (isset($_SESSION['_prefs'][$this->_ns]['saved']) && array_key_exists($key, $_SESSION['_prefs'][$this->_ns]['saved']));
+
+        case 'cookie':
+            $name = $this->_getCookieName($key);
+            return (isset($_COOKIE) && array_key_exists($name, $_COOKIE));
+        }
+
+    }
+
+    /**
+     * Delete an existing preference value. This will also remove the value from the database, once save() is called.
+     *
+     * @param string $key       The name of the preference to delete.
+     */
+    public function delete($key)
+    {
+        $app =& App::getInstance();
+
+        switch ($this->getParam('storagetype')) {
+        case 'session':
+        case 'database':
+            unset($_SESSION['_prefs'][$this->_ns]['saved'][$key]);
+            break;
+
+        case 'cookie':
+            if ($this->exists($key)) {
+                // Just set the existing value to an empty string, which expires in the past.
+                $name = $this->_getCookieName($key);
+                $app->setCookie($name, '', time() - 86400);
+                // Also unset the received cookie value, so it is unavailable.
+                unset($_COOKIE[$name]);
+            }
+            break;
+        }
+
+    }
+
+    /**
+     * Resets all existing values under this namespace. This should be executed with the same consideration as $auth->clear(), such as when logging out.
+     */
+    public function clear($scope='all')
+    {
+        $app =& App::getInstance();
+
+        switch ($scope) {
+        case 'all' :
+            switch ($this->getParam('storagetype')) {
+            case 'session':
+            case 'database':
+                $_SESSION['_prefs'][$this->_ns] = array(
+                    'loaded' => false,
+                    'load_datetime' => '1970-01-01',
+                    'defaults' => array(),
+                    'saved' => array(),
+                );
+                break;
+            case 'cookie':
+                foreach ($_COOKIE as $key => $value) {
+                    // All cookie keys with our internal prefix. Use only the last part as the key.
+                    if (preg_match('/^' . preg_quote(sprintf('_prefs-%s-', $this->_ns)) . '(.+)$/i', $key, $match)) {
+                        $this->delete($match[1]);
+                    }
+                }
+                break;
+            }
+            break;
+
+        case 'defaults' :
+            $_SESSION['_prefs'][$this->_ns]['defaults'] = array();
+            break;
+
+        case 'saved' :
+            $_SESSION['_prefs'][$this->_ns]['saved'] = array();
+            break;
+        }
+
+        $app->logMsg(sprintf('Cleared %s %s prefs', $scope, $this->_ns), LOG_DEBUG, __FILE__, __LINE__);
+    }
+
+    /*
+    * Retrieves all prefs from the database and stores them in the $_SESSION.
+    *
+    * @access   public
+    * @param    bool    $force  Set to always load from database, regardless if _isLoaded() or not.
+    * @return   bool    True if loading succeeded.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    04 Jun 2006 16:56:53
+    */
+    public function load($force=false)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        // Skip this method if not using the db.
+        if ('database' != $this->getParam('storagetype')) {
+            $app->logMsg('Prefs->load() does nothing unless using a database storagetype.', LOG_NOTICE, __FILE__, __LINE__);
+            return true;
+        }
+
+        $this->initDB();
+
+        // Prefs already loaded for this session.
+        if (!$force && $this->_isLoaded()) {
+            return true;
+        }
+
+        // User_id must not be empty.
+        if ('' == $this->getParam('user_id')) {
+            $app->logMsg(sprintf('Cannot save prefs because user_id not set.', null), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Clear existing cache.
+        $this->clear('saved');
+
+        // Retrieve all prefs for this user and namespace.
+        $qid = $db->query("
+            SELECT pref_key, pref_value
+            FROM " . $db->escapeString($this->getParam('db_table')) . "
+            WHERE user_id = '" . $db->escapeString($this->getParam('user_id')) . "'
+            AND pref_namespace = '" . $db->escapeString($this->_ns) . "'
+            LIMIT 10000
+        ");
+        while (list($key, $val) = mysql_fetch_row($qid)) {
+            $_SESSION['_prefs'][$this->_ns]['saved'][$key] = unserialize($val);
+        }
+
+        $app->logMsg(sprintf('Loaded %s prefs from database.', mysql_num_rows($qid)), LOG_DEBUG, __FILE__, __LINE__);
+
+        // Data loaded only once per session.
+        $_SESSION['_prefs'][$this->_ns]['loaded'] = true;
+        $_SESSION['_prefs'][$this->_ns]['load_datetime'] = date('Y-m-d H:i:s');
+
+        return true;
+    }
+
+    /*
+    * Returns true if the prefs had been loaded from the database into the $_SESSION recently.
+    * This function is simply a check so the database isn't access every page load.
+    *
+    * @access   private
+    * @return   bool    True if prefs are loaded.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    04 Jun 2006 17:12:44
+    */
+    protected function _isLoaded()
+    {
+        if ('database' != $this->getParam('storagetype')) {
+            $app->logMsg('Prefs->_isLoaded() does nothing unless using a database storagetype.', LOG_NOTICE, __FILE__, __LINE__);
+            return true;
+        }
+
+        if (isset($_SESSION['_prefs'][$this->_ns]['load_datetime'])
+        && strtotime($_SESSION['_prefs'][$this->_ns]['load_datetime']) > time() - $this->getParam('load_timeout')
+        && isset($_SESSION['_prefs'][$this->_ns]['loaded'])
+        && true === $_SESSION['_prefs'][$this->_ns]['loaded']) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /*
+    * Saves all prefs stored in the $_SESSION into the database.
+    *
+    * @access   public
+    * @return   bool    True if prefs exist and were saved.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    04 Jun 2006 17:19:56
+    */
+    public function save()
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        // Skip this method if not using the db.
+        if ('database' != $this->getParam('storagetype')) {
+            $app->logMsg('Prefs->save() does nothing unless using a database storagetype.', LOG_NOTICE, __FILE__, __LINE__);
+            return true;
+        }
+
+        // User_id must not be empty.
+        if ('' == $this->getParam('user_id')) {
+            $app->logMsg(sprintf('Cannot save prefs because user_id not set.', null), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        $this->initDB();
+
+        if (isset($_SESSION['_prefs'][$this->_ns]['saved']) && is_array($_SESSION['_prefs'][$this->_ns]['saved']) && !empty($_SESSION['_prefs'][$this->_ns]['saved'])) {
+            // Delete old prefs from database.
+            $db->query("
+                DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
+                WHERE user_id = '" . $db->escapeString($this->getParam('user_id')) . "'
+                AND pref_namespace = '" . $db->escapeString($this->_ns) . "'
+            ");
+
+            // Insert new prefs.
+            $insert_values = array();
+            foreach ($_SESSION['_prefs'][$this->_ns]['saved'] as $key => $val) {
+                $insert_values[] = sprintf("('%s', '%s', '%s', '%s')",
+                    $db->escapeString($this->getParam('user_id')),
+                    $db->escapeString($this->_ns),
+                    $db->escapeString($key),
+                    $db->escapeString(serialize($val))
+                );
+            }
+            // TODO: after MySQL 5.0.23 is released this query could benefit from INSERT DELAYED.
+            $db->query("
+                INSERT INTO " . $db->escapeString($this->getParam('db_table')) . "
+                (user_id, pref_namespace, pref_key, pref_value)
+                VALUES " . join(', ', $insert_values) . "
+            ");
+
+            $app->logMsg(sprintf('Saved %s prefs to database.', sizeof($insert_values)), LOG_DEBUG, __FILE__, __LINE__);
+            return true;
+        }
+
+        return false;
+    }
+
+    /*
+    *
+    *
+    * @access   public
+    * @param
+    * @return
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    02 May 2014 18:17:04
+    */
+    protected function _getCookieName($key)
+    {
+        $app =& App::getInstance();
+
+        if (mb_strpos($key, sprintf('_prefs-%s', $this->_ns)) === 0) {
+            $app->logMsg(sprintf('Invalid key name (%s). Leave off "_prefs-%s-" and it should work.', $key, $this->_ns), LOG_NOTICE, __FILE__, __LINE__);
+        }
+        // Use standardized class data names: _ + classname + namespace + variablekey
+        return sprintf('_prefs-%s-%s', $this->_ns, $key);
+    }
+}
+
Index: tags/2.2.3/lib/ScriptTimer.inc.php
===================================================================
--- tags/2.2.3/lib/ScriptTimer.inc.php	(revision 632)
+++ tags/2.2.3/lib/ScriptTimer.inc.php	(revision 632)
@@ -0,0 +1,83 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * ScriptTimer.inc.php
+ */
+class ScriptTimer
+{
+
+    public $time_format = '%.3f';
+    protected $_timing_start_times = array();
+    protected $_timing_stop_times = array();
+    protected $_timing_cumulative_times = array();
+    
+    public function start($name='default')
+    {
+        $this->_timing_start_times[$name] = explode(' ', microtime());
+    }
+    
+    public function stop($name='default')
+    {
+        if (!isset($this->_timing_stop_times[$name]) || !isset($this->_timing_cumulative_times[$name])) {
+            $this->_timing_stop_times[$name] = null;
+            $this->_timing_cumulative_times[$name] = null;
+        }
+        $this->_timing_stop_times[$name] = explode(' ', microtime());
+        $this->_timing_cumulative_times[$name] += $this->getTime($name);
+    }
+    
+    public function getTime($name='default')
+    {
+        if (!isset($this->_timing_start_times[$name])) {
+            return 0;
+        }
+        if (!isset($this->_timing_stop_times[$name])) {
+            $stop_time = explode(' ', microtime());
+        } else {
+            $stop_time = $this->_timing_stop_times[$name];
+        }
+        // Do the big numbers first so the small ones aren't lost.
+        $current = $stop_time[1] - $this->_timing_start_times[$name][1];
+        $current += $stop_time[0] - $this->_timing_start_times[$name][0];
+        return $current;
+    }
+    
+    public function printAll($sort_by_time=false)
+    {
+        $names = array_map('strlen', array_keys($this->_timing_start_times));
+        sort($names);
+        $name_len = end($names);
+        
+        if ($sort_by_time) {
+           arsort($this->_timing_cumulative_times, SORT_NUMERIC);
+        }
+        
+        $this->_timing_cumulative_times['TOTAL'] = array_sum($this->_timing_cumulative_times);
+
+        echo '<pre>';
+        foreach ($this->_timing_cumulative_times as $name => $time) {
+            printf("\n%-{$name_len}s $this->time_format", $name, $time);
+        }
+        echo '</pre>';
+    }
+}
Index: tags/2.2.3/lib/SortOrder.inc.php
===================================================================
--- tags/2.2.3/lib/SortOrder.inc.php	(revision 632)
+++ tags/2.2.3/lib/SortOrder.inc.php	(revision 632)
@@ -0,0 +1,228 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SortOrder.inc.php
+ *
+ * SortOrder can determine how to sort results of a database query for display
+ * on a listing. It can print column headers that will be links to
+ * automatically change the sort and order.
+ *
+ * @requires    This class requires Prefs.inc.php
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.6.2
+ */
+require_once dirname(__FILE__) . '/Prefs.inc.php';
+
+class SortOrder
+{
+
+    protected $_columns;
+    public $sort_by;
+    public $order;
+    public $asc_widget;
+    public $desc_widget;
+    public $default_sort;
+    public $default_order;
+
+    /**
+     * Constructor. Finds the current sort by and order.
+     *
+     * @param string $default_sort   If not found elsewhere, this will be the
+     *                               current sort by.
+     * @param string $default_order  If not found elsewhere, this will be the
+     *                               current order.
+     */
+    public function __construct($default_sort = '', $default_order = '')
+    {
+        $app =& App::getInstance();
+
+        // Setup the HTML for printing ASC/DESC paths.
+        // This should be converted to CSS someday.
+        $images_path = $app->getParam('images_path') ? $app->getParam('images_path') : '/admin/_widgets';
+        $this->asc_widget = sprintf('<img src="%s/sort_ascending.gif" alt="%s" width="11" height="7" border="0" />', $images_path, _("Ascending"));
+        $this->desc_widget = sprintf('<img src="%s/sort_descending.gif" alt="%s" width="11" height="7" border="0" />', $images_path, _("Ascending"));
+
+        // Setup prefs object.
+        $this->prefs = new Prefs($_SERVER['PHP_SELF']);
+        $this->prefs->setParam(array('persistent' => false));
+
+        // Setup defaults.
+        $this->setDefault($default_sort, $default_order);
+        $this->default_sort = $default_sort;
+        $this->default_order = $default_order;
+    }
+
+    /**
+     * Build an array of valid sort SQL for each DB column. This SQL is reference
+     * by the name and 'asc' or 'desc'.
+     *
+     * @param string $name      Reference name for the column this SQL sorts on.
+     * @param string $asc_sql   The sort SQL if $this->order is ascending.
+     * @param string $desc_sql  The sort SQL if $this->order is descending.
+     */
+    public function setColumn($name, $asc_sql, $desc_sql)
+    {
+        $this->_columns[$name] = array(
+            'asc'  => $asc_sql,
+            'desc' => $desc_sql
+        );
+    }
+
+    /**
+     * Set sort and order values. This is how you set new sort values after
+     * already declaring a SortOrder object, but expect values to come from
+     * getFormData.
+     *
+     * @param string $default_sort   If not found elsewhere, this will be the
+     *                               current sort by.
+     * @param string $default_order  If not found elsewhere, this will be the
+     *                               current order.
+     */
+    public function setDefault($default_sort = '', $default_order = '')
+    {
+        // Which column to sort by?
+        // (1) By GET or POST specification, if available.
+        // (2) By saved preference, if available.
+        // (3) By default (provided at class instantiation).
+        $new_sort_by = getFormData('sort');
+        if (!empty($new_sort_by)) {
+            $this->sort_by = $new_sort_by;
+            $this->prefs->set('sort_by', $this->sort_by);
+        } else if ($this->prefs->exists('sort_by')) {
+            $this->sort_by = $this->prefs->get('sort_by');
+        } else {
+            $this->sort_by = $default_sort;
+        }
+
+        // Which sort order to use?
+        // (1) By GET or POST specification, if available.
+        // (2) By saved preference, if available.
+        // (3) By default (provided at class instantiation).
+        $new_order = getFormData('order');
+        if (!empty($new_order)) {
+            $this->order = $new_order;
+            $this->prefs->set('sort_order', $this->order);
+        } else if ($this->prefs->exists('sort_order')) {
+            $this->order = $this->prefs->get('sort_order');
+        } else {
+            $this->order = $default_order;
+        }
+    }
+
+
+    /**
+     * Forcibly set sort and order values. This is how you set new sort values after
+     * already declaring a SortOrder object. This will ignore getFormData values.
+     *
+     * @param string $sort           The sort by name.
+     * @param string $order          The order direction (ASC,
+     *                               for example, for an alphabetical sort)
+     */
+    public function set($sort=null, $order=null, $save_value=true)
+    {
+        // Set new sort value.
+        if (isset($sort)) {
+            $this->sort_by = $sort;
+            if ($save_value) {
+                $this->prefs->set('sort_by', $this->sort_by);
+            }
+        }
+
+        // Set new order value.
+        if (isset($order)) {
+            $this->order = $order;
+            if ($save_value) {
+                $this->prefs->set('sort_order', $this->order);
+            }
+        }
+    }
+
+    /**
+     * Get the current sort and order values.
+     *
+     * @return array    Array with keys: sort and order. These can be fed back into the SortOrder::set() method or defaults.
+     */
+    public function get()
+    {
+        return array(
+            'sort' => $this->prefs->get('sort_by'),
+            'order' => $this->prefs->get('sort_order'),
+        );
+    }
+
+    /**
+     * Returns the SQL code to sort by set column and set order.
+     */
+    public function getSortOrderSQL()
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        if (!isset($this->_columns[mb_strtolower($this->sort_by)])) {
+            $this->sort_by = $this->default_sort;
+            $this->order = $this->default_order;
+        }
+        if (!isset($this->_columns[mb_strtolower($this->sort_by)][mb_strtolower($this->order)])) {
+            $this->order = 'ASC';
+        }
+
+        if (!empty($this->_columns[mb_strtolower($this->sort_by)][mb_strtolower($this->order)])) {
+            return sprintf(' ORDER BY %s ', $this->_columns[mb_strtolower($this->sort_by)][mb_strtolower($this->order)]);
+        } else {
+            $app->logMsg(sprintf('Could not find SQL to sort by %s %s.', $this->sort_by, $this->order), LOG_WARNING, __FILE__, __LINE__);
+            return '';
+        }
+    }
+
+
+    /**
+     * Prints a link for a column header with URL sort determining logic.
+     * Column must be defined first using setColumn().
+     *
+     * @param string $col            The database column to sort by.
+     * @param string $col_name       The human-readable title of the column.
+     * @param string $default_order  The default order for this column (ASC,
+     *                               for example, for an alphabetical sort)
+     */
+    public function printSortHeader($col, $col_name, $default_order='ASC')
+    {
+        $app =& App::getInstance();
+
+        if (isset($this->_columns[$col])) {
+            if ($this->sort_by == $col) {
+                if (mb_strtolower($this->order) == 'desc') {
+                    ?><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?sort=' . $col . '&order=ASC'); ?>" title="<?php echo _("Change to ascending sort order"); ?>" class="sc-sort sc-desc"><?php echo $this->desc_widget; ?></a><?php echo $col_name; ?><?php
+                } else {
+                    ?><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?sort=' . $col . '&order=DESC'); ?>" title="<?php echo _("Change to descending sort order"); ?>" class="sc-sort sc-asc"><?php echo $this->asc_widget; ?></a><?php echo $col_name; ?><?php
+                }
+            } else {
+                ?><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?sort=' . $col . '&order=' . $default_order); ?>" title="<?php echo sprintf(_("Sort by %s"), $col_name); ?>" class="sc-sort"><?php echo $col_name; ?></a><?php
+            }
+        } else {
+            echo $col_name;
+        }
+    }
+
+}
+
Index: tags/2.2.3/lib/SpellCheck.inc.php
===================================================================
--- tags/2.2.3/lib/SpellCheck.inc.php	(revision 632)
+++ tags/2.2.3/lib/SpellCheck.inc.php	(revision 632)
@@ -0,0 +1,455 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * SpellCheck.inc.php
+ *
+ * Interface to PHP's pspell functions.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.1
+ */
+
+/* Implementation example:
+--------------------------------------------------------------------------------
+include_once dirname(__FILE__) . '/_config.inc.php';
+include 'codebase/lib/SpellCheck.inc.php';
+
+// Instantiate with parameters. In this example we'll set the language and the path to the personal wordlist file.
+$spell = new SpellCheck(array(
+    'language' => 'en',
+    'personal_wordlist' => '/tmp/my_custom_dict'
+));
+
+// Just for the heck of it add a new word to persistent personal wordlist file.
+$spell->add('mealworm');
+
+$text_to_check = 'donky rinds taste like mealworm paste';
+
+if (!$spell->checkString($text_to_check)) {
+    $suggestions = $spell->getStringSuggestions($text_to_check);
+    echo 'Spelling errors! Here are suggested alternatives:';
+    print_r($suggestions);
+} else {
+    echo 'No spelling errors';
+}
+
+// Save added words to persistent custom wordlist file.
+$spell->save();
+--------------------------------------------------------------------------------
+*/
+
+class SpellCheck
+{
+
+    protected $_params = array(
+        'language' => 'en',
+        'personal_wordlist' => '', // Text file to save custom words to.
+        'skip_length' => 3, // Words with this many chars or less will not be checked.
+        'mode' => PSPELL_NORMAL, // PSPELL_FAST, PSPELL_NORMAL, or PSPELL_BAD_SPELLERS.
+        'highlight_start' => '<strong style="color:red;">',
+        'highlight_end' => '</strong>',
+    );
+
+    protected $_pspell_cfg_handle;
+    protected $_pspell_handle;
+    protected $_use_personal_wordlist = false;
+    protected $_errors = array();
+
+    /**
+     * Constructor.
+     *
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function __construct($params)
+    {
+        $app =& App::getInstance();
+
+        if (!extension_loaded('pspell')) {
+            trigger_error('Pspell module not installed', E_USER_ERROR);
+        }
+
+        if (!is_array($params) || empty($params)) {
+            trigger_error('SpellCheck parameters not set properly', E_USER_ERROR);
+        }
+
+        $this->setParam($params);
+
+        $this->_pspell_cfg_handle = pspell_config_create($this->getParam('language'));
+
+        pspell_config_ignore($this->_pspell_cfg_handle, $this->getParam('skip_length'));
+        pspell_config_mode($this->_pspell_cfg_handle, $this->getParam('mode'));
+
+        if ('' != $this->getParam('personal_wordlist')) {
+            if (!is_writable(dirname($this->getParam('personal_wordlist'))) || !is_writable($this->getParam('personal_wordlist'))) {
+                $app->logMsg(sprintf('Personal wordlist file not writable: %s', $this->getParam('personal_wordlist')), LOG_WARNING, __FILE__, __LINE__);
+            } else {
+                pspell_config_personal($this->_pspell_cfg_handle, $this->getParam('personal_wordlist'));
+                $this->_use_personal_wordlist = true;
+            }
+        }
+
+        $this->_pspell_handle = pspell_new_config($this->_pspell_cfg_handle);
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Check whether any errors have been triggered.
+     *
+     * @return bool   True if any errors were found, false otherwise.
+     */
+    public function anyErrors()
+    {
+        return (sizeof($this->_errors) > 0);
+    }
+
+    /**
+     * Reset the error list.
+     */
+    public function resetErrorList()
+    {
+        $this->_errors = array();
+    }
+
+    /**
+     * Check one word.
+     *
+     * @access  public
+     * @param   string  $word
+     * @return  bool    True if word is correct.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   09 Jun 2005 18:23:51
+     */
+    public function check($word)
+    {
+        if (pspell_check($this->_pspell_handle, $word)) {
+            return true;
+        } else {
+            $this->_errors[] = $word;
+            return false;
+        }
+    }
+
+    /**
+     * Suggest the correct spelling for one misspelled word.
+     *
+     * @access  public
+     * @param   string  $word
+     * @return  array   Word suggestions.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   09 Jun 2005 18:23:51
+     */
+    public function suggest($word)
+    {
+        return pspell_suggest($this->_pspell_handle, $word);
+    }
+
+    /**
+     * Add a word to a personal list.
+     *
+     * @access  public
+     * @param   string  $word
+     * @return  array   Word suggestions.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   09 Jun 2005 18:23:51
+     */
+    public function add($word)
+    {
+        $app =& App::getInstance();
+
+        if ($this->_use_personal_wordlist) {
+            if (pspell_add_to_personal($this->_pspell_handle, $word)) {
+                $app->logMsg(sprintf('Added "%s" to personal wordlist: %s', $word, $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
+                return true;
+            } else {
+                $app->logMsg(sprintf('Failed adding "%s" to personal wordlist: %s', $word, $this->getParam('personal_wordlist')), LOG_WARNING, __FILE__, __LINE__);
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Save personal list to file.
+     *
+     * @access  public
+     * @param   string  $word
+     * @return  array   Word suggestions.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   09 Jun 2005 18:23:51
+     */
+    public function save()
+    {
+        $app =& App::getInstance();
+
+        if ($this->_use_personal_wordlist) {
+            if (pspell_save_wordlist($this->_pspell_handle)) {
+                $app->logMsg(sprintf('Saved personal wordlist: %s', $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
+                return true;
+            } else {
+                $app->logMsg(sprintf('Failed saving personal wordlist: %s', $this->getParam('personal_wordlist')), LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Returns an array of suggested words for each misspelled word in the given text.
+     * The first word of the returned array is the (possibly) misspelled word.
+     *
+     * @access  public
+     * @param   string  $string String to get suggestions for.
+     * @return  mixed   Array of suggested words or false if none.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   09 Jun 2005 21:29:49
+     */
+    public function getStringSuggestions($string)
+    {
+        $corrections = array();
+        // Split words on punctuation except apostrophes (this regex is used in several places in this class).
+        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
+        $words = preg_split("/((?:^\p{P}+)|(?:\p{P}*\s+\p{P}*)|[\p{Pd}ââ-]+|(?:\p{P}+$))/", $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+        if (is_array($words) && !empty($words)) {
+            // Remove non-word elements.
+            $words = preg_grep('/\w+/', $words);
+            $words = array_map('strip_tags', $words);
+            foreach ($words as $i => $word) {
+                if (!$this->check($word)) {
+                    $corrections[$i] = $this->suggest($word);
+                    // Keep the original spelling as one of the suggestions.
+                    array_unshift($corrections[$i], $word);
+                    array_unique($corrections[$i]);
+                }
+            }
+        }
+        if (is_array($corrections) && !empty($corrections)) {
+            return $corrections;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Checks all words in a given string.
+     *
+     * @access  public
+     * @param   string  $string     String to check.
+     * @return  void
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   09 Jun 2005 22:11:27
+     */
+    public function checkString($string)
+    {
+        $errors = array();
+        // Split words on punctuation except apostrophes (this regex is used in several places in this class).
+        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
+        $words = preg_split("/((?:^\p{P}+)|(?:\p{P}*\s+\p{P}*)|[\p{Pd}ââ-]+|(?:\p{P}+$))/", $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+        if (is_array($words) && !empty($words)) {
+            // Remove non-word elements.
+            $words = preg_grep('/\w+/', $words);
+            $words = array_map('strip_tags', $words);
+            foreach ($words as $i => $word) {
+                if (!$this->check($word)) {
+                    $errors[] = $word;
+                }
+            }
+        }
+        if (empty($errors)) {
+            return true;
+        } else {
+            $this->_errors = $errors + $this->_errors;
+            return false;
+        }
+    }
+
+    /**
+     * Returns a given string with misspelled words highlighted.
+     *
+     * @access  public
+     * @param   string  $string     Text to highlight.
+     * @return  string  Highlighted text.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   09 Jun 2005 21:29:49
+     */
+    public function getStringHighlighted($string, $show_footnote=false)
+    {
+        // Split words on punctuation except apostrophes (this regex is used in several places in this class).
+        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
+        $words = preg_split("/((?:^\p{P}+)|(?:\p{P}*\s+\p{P}*)|[\p{Pd}ââ-]+|(?:\p{P}+$))/", $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+        $cnt = 0;
+        if (is_array($words) && !empty($words)) {
+            $words = preg_grep('/\w+/', $words);
+            $words = array_map('strip_tags', $words);
+            foreach ($words as $i => $word) {
+                if (!$this->check($word)) {
+                    $footnote = $show_footnote ? '<sup style="color:#999;">' . ++$cnt . '</sup>' : '';
+                	$words[$i] = $this->getParam('highlight_start') . $word . $this->getParam('highlight_end') . $footnote;
+                    $string = preg_replace(sprintf('/\b%s\b/', preg_quote($word, '/')), $words[$i], $string);
+                }
+            }
+        }
+        return $string;
+    }
+
+    /**
+     * Prints the HTML for correcting all misspellings found in the text of one $_FORM element.
+     *
+     * @access  public
+     * @param   string  $form_name  Name of the form to check.
+     * @return  void
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   09 Jun 2005 21:29:49
+     */
+    public function printCorrectionForm($form_name)
+    {
+        ?>
+        <input name="<?php echo $form_name ?>" type="hidden" value="<?php echo oTxt(getFormData($form_name)) ?>" />
+        <?php
+
+        $form_words = $this->getStringSuggestions(getFormData($form_name));
+        if (is_array($form_words) && !empty($form_words)) {
+            ?><ol><?php
+            foreach ($form_words as $i => $words) {
+                ?>
+                <li>
+                <label style="color:#999;"><sub style="vertical-align:text-top;"><?php echo $j++; ?></sub></label>
+                <select name="spelling_suggestions[<?php echo $form_name ?>][<?php echo $i ?>]" onchange="document.forms[0].elements['spelling_corrections[<?php echo $form_name ?>][<?php echo $i ?>]'].value = this.value;">
+                <?php $original_word = array_shift($words); ?>
+                <option value="<?php echo $original_word ?>">(<?php echo $original_word ?>)</option>
+                <?php
+
+                foreach ($words as $suggestion) {
+                    ?>
+                    <option value="<?php echo $suggestion ?>"><?php echo $suggestion ?></option>
+                    <?php
+                }
+
+                ?>
+                </select>
+                <input type="text" name="spelling_corrections[<?php echo $form_name ?>][<?php echo $i ?>]" value="<?php echo oTxt($original_word) ?>" size="20">
+                <?php if ($this->_use_personal_wordlist) { ?>
+                <input name="save_to_personal_wordlist[]" type="checkbox" value="<?php echo $i ?>" /><?php echo _("Learn spelling") ?>
+                <?php } ?>
+                </li>
+                <?php
+            }
+            ?></ol><?php
+        }
+    }
+
+    /**
+     * Tests if any form spelling corrections have been submitted.
+     *
+     * @access  public
+     * @return  bool    True if form spelling has been checked.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   09 Jun 2005 23:15:35
+     */
+    public function anyFormCorrections()
+    {
+        return (false !== getFormData('spelling_suggestions', false)) || (false !== getFormData('spelling_corrections', false));
+    }
+
+    /**
+     * Replace the misspelled words in the text of a specified form with the corrections.
+     *
+     * @access  public
+     * @param   string  $form_name      Name of form to apply corrections to.
+     * @return  string  Corrected form text.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   09 Jun 2005 23:18:51
+     */
+    public function applyFormCorrections($form_name)
+    {
+        // Split words on punctuation except apostrophes (this regex is used in several places in this class).
+        // http://stackoverflow.com/questions/790596/split-a-text-into-single-words
+        $form_words = preg_split("/((?:^\p{P}+)|(?:\p{P}*\s+\p{P}*)|[\p{Pd}ââ-]+|(?:\p{P}+$))/", getFormData($form_name), -1, PREG_SPLIT_DELIM_CAPTURE);
+        $suggestions = getFormData('spelling_suggestions');
+        $corrections = getFormData('spelling_corrections');
+
+        $form_words = array_diff($corrections[$form_name], array('')) + $suggestions[$form_name] + $form_words;
+        ksort($form_words);
+
+        if ($this->_use_personal_wordlist) {
+            $save_to_personal_wordlist = getFormData('save_to_personal_wordlist');
+            if (is_array($save_to_personal_wordlist) && !empty($save_to_personal_wordlist)) {
+                foreach ($save_to_personal_wordlist as $cust) {
+                    $this->add($form_words[$cust]);
+                }
+            }
+            $this->save();
+        }
+
+        if (is_array($form_words) && !empty($form_words)) {
+            return join('', $form_words);
+        } else {
+            return getFormData($form_name);
+        }
+    }
+
+} // End.
+
Index: tags/2.2.3/lib/TemplateGlue.inc.php
===================================================================
--- tags/2.2.3/lib/TemplateGlue.inc.php	(revision 632)
+++ tags/2.2.3/lib/TemplateGlue.inc.php	(revision 632)
@@ -0,0 +1,497 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * TemplateGlue.inc.php
+ */
+
+/**
+ * Returns an image tag for image specified in $src.
+ *
+ * @param  string $src     File name of the image, including path and file extension.
+ * @param  string $alt     Alt tag text.
+ * @param  string $extra   Additional attributes.
+ */
+function oImg($src, $alt='', $extra='')
+{
+    $filepath = preg_match('!://!', $src) ? $src : getenv('DOCUMENT_ROOT') . $src;
+
+    if (true === ($gis = @getimagesize($filepath)) && !preg_match('/width|height/', $extra)) {
+        $image_size = $gis[3];
+    } else {
+        $image_size = '';
+    }
+
+    return sprintf('<img src="%s" %s alt="%s" %s />',
+        $src,
+        $image_size,
+        oTxt($alt),
+        $extra
+    );
+}
+
+/**
+ * Prints an image tag for image specified in $src.
+ *
+ * @param  string $src     File name of the image, including path and file extension.
+ * @param  string $alt     Alt tag text.
+ * @param  string $extra   Additional attributes.
+ */
+function printImg($src, $alt='', $extra='')
+{
+    echo oImg($src, $alt, $extra);
+}
+
+/**
+ * Finds the values of an enumeration or set column of a MySQL database, returning them in an array.
+ * Use this to generate a pull-down menu of options or to validate the existence
+ * of options. (Quinn 10 Feb 2001)
+ *
+ * @param  string $db_table   database table to lookup
+ * @param  string $db_col     database column to lookup
+ * @param  bool   $sort          Sort the output.
+ * @return array    Array of the set/enum values on success, false on failure.
+ */
+function getSetEnumFieldValues($db_table, $db_col, $sort=false)
+{
+    $app =& App::getInstance();
+    $db =& DB::getInstance();
+
+    $qid = $db->query("SHOW COLUMNS FROM " . $db->escapeString($db_table) . " LIKE '" . $db->escapeString($db_col) . "'",false);
+
+    $row = mysql_fetch_row($qid);
+    if (preg_match('/^enum|^set/i', $row[1]) && preg_match_all("/'([^']*)'/", $row[1], $enum)) {
+        if ($sort) {
+            natsort($enum[1]);
+        }
+        return $enum[1];
+    } else {
+        $app->logMsg(sprintf('No set or enum fields found.', null), LOG_ERR, __FILE__, __LINE__);
+        return false;
+    }
+}
+
+/**
+ * Prints option fields for a select form. Works only with enum or set
+ * data types in table columns.
+ *
+ * @param  string $db_table     Database table to lookup
+ * @param  string $db_col       Database column to lookup
+ * @param  string $preselected  The currently selected value of the menu. compared to the $val_column
+ * @param  bool   $first        Optional first item; set true for a blank item, array for item with name and value.
+ * @param  bool   $sort         Sort the output.
+ */
+function printSetSelectForm($db_table, $db_col, $preselected, $first=false, $sort=false)
+{
+    $values = getSetEnumFieldValues($db_table, $db_col, $sort);
+    if ($values === false) {
+        ?><option value="">&nbsp;</option>
+        <?php
+        return false;
+    }
+    if (true === $first) {
+        $selected = ($preselected == '') ? ' selected' : '';
+        ?><option value=""<?php echo $selected; ?>>&nbsp;</option>
+        <?php
+    }
+    // When the 'blank' value needs a specific key->val pair.
+    if (is_array($first)) {
+        foreach ($first as $key=>$val) {
+            $selected = ($preselected == $val) ? ' selected="selected"' : '';
+            ?><option value="<?php echo oTxt($key); ?>"<?php echo $selected; ?>><?php echo oTxt($val); ?></option>
+            <?php
+        }
+    }
+    foreach ($values as $v) {
+        $selected = ($v == $preselected) ? ' selected' : '';
+        ?><option value="<?php echo oTxt($v); ?>"<?php echo $selected; ?>><?php echo oTxt($v); ?></option>
+        <?php
+    }
+}
+
+/**
+ * Prints radio buttons from a set/enum column.
+ *
+ * @param  string $db_table     Database table to lookup
+ * @param  string $db_col       Database column to lookup
+ * @param  string $preselected  The currently selected value of the menu. compared to the $val_column
+ * @param  bool   $first        Optional first item; set true for a blank item, array for item with name and value.
+ * @param  bool   $sort         Sort the output.
+ */
+function printEnumRadios($name, $db_table, $db_col, $preselected, $first=false, $sort=false)
+{
+    $values = getSetEnumFieldValues($db_table, $db_col, $sort);
+    if ($values === false) {
+        return false;
+    }
+
+    foreach ($values as $v) {
+        $selected = ($v == $preselected) ? ' checked="checked"' : '';
+        ?><label class="sc-label-secondary"><input type="radio" name="<?php echo oTxt($name) ?>" id="<?php echo oTxt($v); ?>" value="<?php echo oTxt($v); ?>"<?php echo $selected; ?> /> <?php echo oTxt($v); ?></label>
+        <?php
+    }
+}
+
+/**
+ * Prints checkbox fields. Works only with enum or set
+ * data types in table columns.
+ *
+ * @param  string $db_table      database table to lookup
+ * @param  string $db_col        database column to lookup
+ * @param  array  $preselected   array of preselected values (matching the values in $db_col)
+ * @param  int    $columns       number of table columns to print
+ * @param  int    $flag          set to 'allone' for name of input fields to all be the same of a multidimensional array.
+ * @param  bool   $sort          Sort the output.
+ */
+function printSetCheckboxes($db_table, $db_col, $preselected, $columns=1, $flag=null, $sort)
+{
+    ?>
+    <table>
+        <tr>
+    <?php
+    // Sometimes preselected comes as a comma list.
+    if (!is_array($preselected)) {
+        $preselected = explode(',', $preselected);
+    }
+
+    // Checkbox POST data has the primary data in the keys, and 'on' as the values.
+    // Here we assume in all the values of an array are 'on' that we can find the data
+    // in the array keys.
+    $ps_value_count = array_count_values($preselected);
+    if (isset($ps_value_count['on']) && $ps_value_count['on'] == sizeof($preselected)) {
+        $preselected = array_keys($preselected);
+    }
+
+    // Retrieve values of a Set or ENUM database column.
+    $values = getSetEnumFieldValues($db_table, $db_col, $sort);
+
+    // Initialize the HTML table generation vars.
+    $num_cells = sizeof($values) - 1;
+    $num_rows = floor($num_cells / $columns);
+    $cols_lastrow = $num_cells % $columns;
+    $row_cnt = 0;
+    $col_cnt = 0;
+    foreach ($values as $v) {
+        if ($col_cnt == $columns) {
+            // Begin a new row.
+            ?></tr><tr><?php
+            $col_cnt = 0;
+            $row_cnt++;
+        }
+        if ($col_cnt < $cols_lastrow) {
+            $lastrow_add = $col_cnt;
+        } else {
+            $lastrow_add = $cols_lastrow;
+        }
+        $curr = $num_rows * $col_cnt + $lastrow_add + $row_cnt;
+        $col_cnt++;
+
+        // Look for preselected value.
+        if (in_array($v, $preselected)) {
+            $checked = ' checked="checked"';
+        } else {
+            $checked = '';
+        }
+        if ('allone' == $flag) {
+            // Print a cell with multidimensional array checkboxes.
+            $html_name = 'dbcol[' . $db_col . '][' . $v . ']';
+        } else {
+            // Print a cell with basic named checkboxes.
+            $html_name = $db_col . '[' . $v . ']';
+        }
+        ?><td><label class="sc-label-secondary"><input type="checkbox" name="<?php echo oTxt($html_name); ?>" id="<?php echo oTxt($html_name); ?>"<?php echo $checked; ?> /> <?php echo oTxt($v); ?></label></td>
+        <?php
+    }
+    if ($col_cnt < $columns) {
+        // This last cell must expand to fill the last blank cells.
+        ?><td colspan="<?php echo $columns - $col_cnt; ?>">&nbsp;</td><?php
+    }
+    ?>
+        </tr>
+    </table><?php
+}
+
+/**
+ * Prints radio select fields. Works only with enum or set
+ * data types in table columns.
+ *
+ * @param  string $db_table      database table to lookup
+ * @param  string $db_col        database column to lookup
+ * @param  array  $preselected   array of preselected values (matching the values in $db_col)
+ * @param  int    $columns       number of table columns to print
+ * @param  bool   $sort          Sort the output.
+ */
+function printSetRadios($db_table, $db_col, $preselected, $columns=1, $sort=false)
+{
+    ?>
+    <table>
+        <tr>
+    <?php
+    // Sometimes preselected comes as a comma list.
+    if (!is_array($preselected)) {
+        $preselected = explode(',', $preselected);
+    }
+
+    // Checkbox POST data has the primary data in the keys, and 'on' as the values.
+    // Here we assume in all the values of an array are 'on' that we can find the data
+    // in the array keys.
+    $ps_value_count = array_count_values($preselected);
+    if (isset($ps_value_count['on']) && $ps_value_count['on'] == sizeof($preselected)) {
+        $preselected = array_keys($preselected);
+    }
+
+    // Retrieve values of a Set or ENUM database column.
+    $values = getSetEnumFieldValues($db_table, $db_col, $sort);
+
+    // Initialize the HTML table generation vars.
+    $num_cells = sizeof($values) - 1;
+    $num_rows = floor($num_cells / $columns);
+    $cols_lastrow = $num_cells % $columns;
+    $row_cnt = 0;
+    $col_cnt = 0;
+    foreach ($values as $v) {
+        if ($col_cnt == $columns) {
+            // Begin a new row.
+            ?></tr><tr><?php
+            $col_cnt = 0;
+            $row_cnt++;
+        }
+        if ($col_cnt < $cols_lastrow) {
+            $lastrow_add = $col_cnt;
+        } else {
+            $lastrow_add = $cols_lastrow;
+        }
+        $curr = $num_rows * $col_cnt + $lastrow_add + $row_cnt;
+        $col_cnt++;
+
+        // Look for preselected value.
+        if (in_array($v, $preselected)) {
+            $checked = ' checked="checked"';
+        } else {
+            $checked = '';
+        }
+        // Print a cell with basic named checkboxes.
+        ?><td><label class="sc-label-secondary"><input type="radio" name="<?php echo oTxt($db_col); ?>" id="<?php echo oTxt($db_col . '-' . $v); ?>" value="<?php echo oTxt($v); ?>"<?php echo $checked; ?> /> <?php echo oTxt($v); ?></label></td>
+        <?php
+    }
+    if ($col_cnt < $columns) {
+        // This last cell must expand to fill the last blank cells.
+        ?><td colspan="<?php echo $columns - $col_cnt; ?>">&nbsp;</td><?php
+    }
+    ?>
+        </tr>
+    </table><?php
+}
+
+/**
+ * Prints a pulldown menu containing the specified values and keys of a table.
+ *
+ * @param  string $db_table         database table to lookup
+ * @param  string $key_column       column containing the human-readable titles for the select menu
+ * @param  string $val_column       column containing the computer values for the select menu
+ * @param  string $preselected      the currently selected value of the menu. compared to the $val_column
+ * @param  bool   $first            Optional first item; set true for a blank item, array for item with name and value.
+ * @param  string $extra_clause     SQL exclude clause. Something like "WHERE girls != 'buckteeth'"
+ */
+function printSelectForm($db_table, $key_column, $val_column, $preselected, $first=false, $extra_clause='', $sql_format='SELECT %1$s, %2$s FROM %3$s %4$s')
+{
+    $db =& DB::getInstance();
+
+    // Sometimes preselected comes as a comma list.
+    if (!is_array($preselected)) {
+        $preselected = array($preselected);
+    }
+
+    // Print a blank first option.
+    if (true === $first) {
+        $selected = in_array('', $preselected) ? ' selected="selected"' : '';
+        ?><option value=""<?php echo $selected; ?>>&nbsp;</option>
+        <?php
+    }
+
+    // When the 'blank' value needs a specific key->val pair.
+    if (is_array($first)) {
+        foreach ($first as $key=>$val) {
+            $selected = in_array($key, $preselected) ? ' selected="selected"' : '';
+            ?><option value="<?php echo $key; ?>"<?php echo $selected; ?>><?php echo oTxt($val); ?></option>
+            <?php
+        }
+    }
+    // $qid = $db->query("SELECT $key_column, $val_column FROM $db_table $extra_clause",false);
+    $qid = $db->query(sprintf($sql_format, $key_column, $val_column, $db_table, $extra_clause),false);
+    while ($row = mysql_fetch_assoc($qid)) {
+        $selected = in_array($row[$val_column], $preselected) ? ' selected="selected"' : '';
+        ?><option value="<?php echo $row[$val_column]; ?>"<?php echo $selected; ?>><?php echo oTxt($row[$key_column]); ?></option><?php
+    }
+}
+
+/**
+ * Prints checkbox fields. Works only with enum or set
+ * data types in table columns.
+ *
+ * @param  string $db_table         database table to lookup
+ * @param  string $key_column       column containing the human-readable titles for the select menu
+ * @param  string $val_column       column containing the computer values for the select menu
+ * @param  string $preselected      the currently selected value of the menu. compared to the $val_column
+ * @param  int    $columns          number of table columns to print
+ * @param  int    $extra_clause     extra sql to send at end of select statement.
+ * @param  bool   $vert_columns     display checkboxes in vertical orientation first.
+ */
+function printDBCheckboxes($db_table, $key_column, $val_column, $preselected, $columns=1, $extra_clause='', $vert_columns=false)
+{
+    $db =& DB::getInstance();
+
+    // Sometimes preselected comes as a comma list.
+    if (!is_array($preselected)) {
+        $preselected = explode(',', $preselected);
+    }
+
+    // Checkbox POST data has the primary data in the keys, and 'on' as the values.
+    // Here we assume in all the values of an array are 'on' that we can find the data
+    // in the array keys.
+    $ps_value_count = array_count_values($preselected);
+    if (isset($ps_value_count['on']) && $ps_value_count['on'] == sizeof($preselected)) {
+        $preselected = array_keys($preselected);
+    }
+
+    $qid = $db->query("SELECT $key_column, $val_column FROM $db_table $extra_clause",false);
+    while ($row = mysql_fetch_assoc($qid)) {
+        $values[] = $row;
+    }
+
+    // Rearrange array so sort is in vertical columns. FIXME: doesn't work.
+//  if ($vert_columns) {
+//      $per_col = ceil(sizeof($values) / $columns);
+//      $columns = ceil(sizeof($values) / $per_col);
+//      $curr_row = 0;
+//      $curr_col = 0;
+//      $pos = 0;
+//         foreach ($values as $k=>$v) {
+//             $pos = $curr_row * $columns + $curr_col;
+//             if ($curr_row <= $per_col) {
+//                 $curr_row++;
+//             } else {
+//                 $curr_row = 0;
+//                 $curr_col++;
+//             }
+// //             echo '<li>' . $pos . '-' . $v[$key_column];
+//             $new_values[$pos] = $v;
+//         }
+//         $values = $new_values;
+//         ksort($values);
+//  }
+
+    if (empty($values)) {
+        return false;
+    }
+
+    // Initialize the HTML table generation vars.
+    $num_cells = sizeof($values) - 1;
+    $num_rows = floor($num_cells / $columns);
+    $cols_lastrow = $num_cells % $columns;
+    $row_cnt = 0;
+    $col_cnt = 0;
+    ?>
+    <table border="0" cellspacing="0" cellpadding="0">
+        <tr>
+    <?php
+    foreach ($values as $box) {
+        if ($col_cnt == $columns) {
+            // Begin a new row.
+            ?></tr><tr><?php
+            $col_cnt = 0;
+            $row_cnt++;
+        }
+
+        if ($col_cnt < $cols_lastrow) {
+            $lastrow_add = $col_cnt;
+        } else {
+            $lastrow_add = $cols_lastrow;
+        }
+
+        $curr = $num_rows * $col_cnt + $lastrow_add + $row_cnt;
+        $col_cnt++;
+
+        // Look for preselected value.
+        if (in_array($box[$val_column], $preselected)) {
+            $checked = ' checked="checked"';
+        } else {
+            $checked = '';
+        }
+
+        // Print a cell with basic named checkboxes.
+        $html_name = $val_column . '[' . $box[$val_column] . ']';
+        $html_id = $val_column . '-' . $box[$val_column] . '';
+        ?>
+        <td><input type="checkbox" name="<?php echo oTxt($html_name); ?>" id="<?php echo oTxt($html_id); ?>"<?php echo $checked; ?> /></td>
+        <td><label for="<?php echo oTxt($html_id); ?>" class="sc-normal"><?php echo oTxt($box[$key_column]); ?></label></td>
+        <?php
+    }
+
+    if ($col_cnt < $columns) {
+        // This last cell must expand to fill the last blank cells.
+        ?><td colspan="<?php echo $columns - $col_cnt; ?>">&nbsp;</td><?php
+    }
+    ?>
+        </tr>
+    </table><?php
+}
+
+/**
+ * Prints submit buttons based on given array of submit button names and titles.
+ *
+ * @access  public
+ * @param   array   $submit_buttons     Array of buttons, the key being the button name, and value being the title of the button.
+ * @return  void
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.0
+ * @since   15 Jun 2005 13:42:21
+ */
+function printSubmitButtons($submit_buttons)
+{
+    if (is_array($submit_buttons) && !empty($submit_buttons)) {
+        foreach ($submit_buttons as $i => $b) {
+            if (is_array($b)) {
+                $defaults = array();
+                $defaults['type'] = isset($b['type']) ? $b['type'] : 'submit';
+                if (isset($b['href'])) {
+                    echo '<a';
+                    foreach (array_diff_key($b, array('value' => null)) as $key => $value) {
+                        printf(' %s="%s"', $key, oTxt($value));
+                    }
+                    echo '>' . oTxt($b['value']) . '</a>';
+                } else {
+                    $defaults['id'] = isset($b['id']) ? $b['id'] : sprintf('sc-%s-button', $b['name']);
+                    $b = array_merge($defaults, $b);
+                    echo '<input';
+                    foreach ($b as $key => $value) {
+                        printf(' %s="%s"', $key, oTxt($value));
+                    }
+                    echo ' />';
+                }
+            } else {
+                // For backwards compatibility.
+                ?><input type="submit" name="<?php echo oTxt($i) ?>" value="<?php echo oTxt($b); ?>" /><?php
+            }
+        }
+    }
+}
Index: tags/2.2.3/lib/Upload.inc.php
===================================================================
--- tags/2.2.3/lib/Upload.inc.php	(revision 632)
+++ tags/2.2.3/lib/Upload.inc.php	(revision 632)
@@ -0,0 +1,533 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Upload.inc.php
+ *
+ * The Upload class provides an interface to deal with http uploaded files.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @requires App.inc.php
+ * @version 1.4
+ */
+
+// Upload error types.
+define('UPLOAD_USER_ERR_EMPTY_FILE', 100);
+define('UPLOAD_USER_ERR_NOT_UPLOADED_FILE', 101);
+define('UPLOAD_USER_ERR_INVALID_EXTENSION', 102);
+define('UPLOAD_USER_ERR_NOT_UNIQUE', 103);
+define('UPLOAD_USER_ERR_MOVE_FAILED', 104);
+
+class Upload
+{
+
+    // General object parameters.
+    protected $_params = array(
+
+        // Which messages do we pass to raiseMsg?
+        'display_messages' => MSG_ALL,
+
+        // Existing files will be overwritten when there is a name conflict?
+        'allow_overwriting' => false,
+
+        // The filesystem path to the final upload directory.
+        'upload_path' => null,
+
+        // The file permissions of the uploaded files. Remember, files will be owned by the web server user.
+        'dest_file_perms' => 0600,
+
+        // The file permissions of the uploaded files. Remember, files will be owned by the web server user.
+        'dest_dir_perms' => 0700,
+
+        // Require file to have one of the following file name extensions.
+        'valid_file_extensions' => array('jpg', 'jpeg', 'gif', 'png', 'pdf', 'txt', 'text', 'html', 'htm'),
+    );
+
+    // Array of files with errors.
+    public $errors = array();
+
+    // Array of file name extensions and corresponding mime-types.
+    public $mime_extension_map = array( 'ez' => 'application/andrew-inset', 'hqx' => 'application/mac-binhex40', 'cpt' => 'application/mac-compactpro', 'doc' => 'application/msword', 'bin' => 'application/octet-stream', 'class' => 'application/octet-stream', 'dll' => 'application/octet-stream', 'dms' => 'application/octet-stream', 'exe' => 'application/octet-stream', 'lha' => 'application/octet-stream', 'lzh' => 'application/octet-stream', 'so' => 'application/octet-stream', 'oda' => 'application/oda', 'pdf' => 'application/pdf', 'ai' => 'application/postscript', 'eps' => 'application/postscript', 'ps' => 'application/postscript', 'smi' => 'application/smil', 'smil' => 'application/smil', 'mif' => 'application/vnd.mif', 'xls' => 'application/vnd.ms-excel', 'ppt' => 'application/vnd.ms-powerpoint', 'sxc' => 'application/vnd.sun.xml.calc', 'stc' => 'application/vnd.sun.xml.calc.template', 'sxd' => 'application/vnd.sun.xml.draw', 'std' => 'application/vnd.sun.xml.draw.template', 'sxi' => 'application/vnd.sun.xml.impress', 'sti' => 'application/vnd.sun.xml.impress.template', 'sxm' => 'application/vnd.sun.xml.math', 'sxw' => 'application/vnd.sun.xml.writer', 'sxg' => 'application/vnd.sun.xml.writer.global', 'stw' => 'application/vnd.sun.xml.writer.template', 'vsd' => 'application/vnd.visio', 'wbxml' => 'application/vnd.wap.wbxml', 'wmlc' => 'application/vnd.wap.wmlc', 'wmlsc' => 'application/vnd.wap.wmlscriptc', 'bcpio' => 'application/x-bcpio', 'vcd' => 'application/x-cdlink', 'pgn' => 'application/x-chess-pgn', 'Z' => 'application/x-compress', 'cpio' => 'application/x-cpio', 'csh' => 'application/x-csh', 'dcr' => 'application/x-director', 'dir' => 'application/x-director', 'dxr' => 'application/x-director', 'dvi' => 'application/x-dvi', 'spl' => 'application/x-futuresplash', 'gtar' => 'application/x-gtar', 'tgz' => 'application/x-gtar', 'gz' => 'application/x-gzip', 'hdf' => 'application/x-hdf', 'php' => 'application/x-httpd-php', 'php3' => 'application/x-httpd-php3', 'js' => 'application/x-javascript', 'skd' => 'application/x-koan', 'skm' => 'application/x-koan', 'skp' => 'application/x-koan', 'skt' => 'application/x-koan', 'latex' => 'application/x-latex', 'wmd' => 'application/x-ms-wmd', 'wmz' => 'application/x-ms-wmz', 'cdf' => 'application/x-netcdf', 'nc' => 'application/x-netcdf', 'pl' => 'application/x-perl', 'pm' => 'application/x-perl', 'psd' => 'application/x-photoshop', 'sh' => 'application/x-sh', 'shar' => 'application/x-shar', 'swf' => 'application/x-shockwave-flash', 'sit' => 'application/x-stuffit', 'sv4cpio' => 'application/x-sv4cpio', 'sv4crc' => 'application/x-sv4crc', 'tar' => 'application/x-tar', 'tcl' => 'application/x-tcl', 'tex' => 'application/x-tex', 'texi' => 'application/x-texinfo', 'texinfo' => 'application/x-texinfo', 'roff' => 'application/x-troff', 't' => 'application/x-troff', 'tr' => 'application/x-troff', 'man' => 'application/x-troff-man', 'me' => 'application/x-troff-me', 'ms' => 'application/x-troff-ms', 'ustar' => 'application/x-ustar', 'src' => 'application/x-wais-source', 'xht' => 'application/xhtml+xml', 'xhtml' => 'application/xhtml+xml', 'xml' => 'application/xml', 'zip' => 'application/zip', 'au' => 'audio/basic', 'snd' => 'audio/basic', 'kar' => 'audio/midi', 'mid' => 'audio/midi', 'midi' => 'audio/midi', 'mp2' => 'audio/mpeg', 'mp3' => 'audio/mp3', 'mpga' => 'audio/mpeg', 'aif' => 'audio/x-aiff', 'aifc' => 'audio/x-aiff', 'aiff' => 'audio/x-aiff', 'm3u' => 'audio/x-mpegurl', 'wax' => 'audio/x-ms-wax', 'wma' => 'audio/x-ms-wma', 'ram' => 'audio/x-pn-realaudio', 'rm' => 'audio/x-pn-realaudio', 'rpm' => 'audio/x-pn-realaudio-plugin', 'ra' => 'audio/x-realaudio', 'wav' => 'audio/x-wav', 'pdb' => 'chemical/x-pdb', 'xyz' => 'chemical/x-xyz', 'bmp' => 'image/bmp', 'gif' => 'image/gif', 'ief' => 'image/ief', 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'jpg' => 'image/jpeg', 'png' => 'image/png', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'wbmp' => 'image/vnd.wap.wbmp', 'ras' => 'image/x-cmu-raster', 'pnm' => 'image/x-portable-anymap', 'pbm' => 'image/x-portable-bitmap', 'pgm' => 'image/x-portable-graymap', 'ppm' => 'image/x-portable-pixmap', 'rgb' => 'image/x-rgb', 'xbm' => 'image/x-xbitmap', 'xpm' => 'image/x-xpixmap', 'xwd' => 'image/x-xwindowdump', 'iges' => 'model/iges', 'igs' => 'model/iges', 'mesh' => 'model/mesh', 'msh' => 'model/mesh', 'silo' => 'model/mesh', 'vrml' => 'model/vrml', 'wrl' => 'model/vrml', 'ics' => 'text/calendar', 'ifb' => 'text/calendar', 'vcs' => 'text/calendar', 'vfb' => 'text/calendar', 'css' => 'text/css', 'csv' => 'text/csv', 'diff' => 'text/diff', 'patch' => 'text/diff', 'htm' => 'text/html', 'html' => 'text/html', 'shtml' => 'text/html', 'asc' => 'text/plain', 'log' => 'text/plain', 'po' => 'text/plain', 'txt' => 'text/plain', 'rtx' => 'text/richtext', 'rtf' => 'text/rtf', 'sgm' => 'text/sgml', 'sgml' => 'text/sgml', 'tsv' => 'text/tab-separated-values', 'wml' => 'text/vnd.wap.wml', 'wmls' => 'text/vnd.wap.wmlscript', 'etx' => 'text/x-setext', 'vcf' => 'text/x-vcard', 'xsl' => 'text/xml', 'mp4' => 'video/mp4', 'mpe' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mpg' => 'video/mpeg', 'mov' => 'video/quicktime', 'qt' => 'video/quicktime', 'mxu' => 'video/vnd.mpegurl', 'asf' => 'video/x-ms-asf', 'asx' => 'video/x-ms-asf', 'wm' => 'video/x-ms-wm', 'wmv' => 'video/x-ms-wmv', 'wmx' => 'video/x-ms-wmx', 'wvx' => 'video/x-ms-wvx', 'avi' => 'video/x-msvideo', 'movie' => 'video/x-sgi-movie', 'ice' => 'x-conference/x-cooltalk', );
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    public function setParam($params)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params) && is_array($params)) {
+            // Enforce valid upload_path parameter.
+            if (isset($params['upload_path'])) {
+                // Source must be directory.
+                if (!is_dir($params['upload_path'])) {
+                    $app->logMsg(sprintf('Attempting to auto-create upload directory: %s', $params['upload_path']), LOG_NOTICE, __FILE__, __LINE__);
+                    mkdir($params['upload_path'], isset($params['dest_dir_perms']) ? $params['dest_dir_perms'] : $this->getParam('dest_dir_perms'), true);
+                    if (is_dir($params['upload_path'])) {
+                        $app->logMsg(sprintf('Created upload directory: %s', $params['upload_path']), LOG_ERR, __FILE__, __LINE__);
+                    } else {
+                        $app->logMsg(sprintf('Upload directory not found: %s', $params['upload_path']), LOG_ERR, __FILE__, __LINE__);
+                        trigger_error(sprintf('Upload directory not found: %s', $params['upload_path']), E_USER_ERROR);
+                    }
+                }
+                // Source must be writable.
+                if (!is_writable($params['upload_path'])) {
+                    $app->logMsg(sprintf('Upload directory not writable: %s', $params['upload_path']), LOG_ERR, __FILE__, __LINE__);
+                    trigger_error(sprintf('Upload directory not writable: %s', $params['upload_path']), E_USER_ERROR);
+                }
+            }
+
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        } else {
+            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_ERR, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Process uploaded files. Processes files existing within the specified $_FILES['form_name'] array.
+     * It tests for errors, cleans the filename, optionally sets custom file names. It will process
+     * multiple files automatically if the file form element is an array (<input type="file" name="myfiles[]" />).
+     *
+     * @access  public
+     * @param   string  $form_name          The name of the form to process.
+     * @param   string  $custom_file_name   The new name of the file. An array of filenames in the case of multiple files.
+     * @return  mixed   Returns FALSE if a major error occurred preventing any file uploads.
+     *                  Returns an empty array if any minor errors occurred or no files were found.
+     *                  Returns a multidimensional array of filenames, sizes and extensions, if one-or-more files succeeded uploading.
+     *                  Note: this last option presents a problem in the case of when some files uploaded successfully, and some failed.
+     *                        In this case it is necessary to check the Upload::anyErrors method to discover if any did fail.
+     */
+    public function process($form_name, $custom_file_name=null)
+    {
+        $app =& App::getInstance();
+
+        // Ensure we have a upload directory.
+        if (!$this->getParam('upload_path')) {
+            $app->logMsg(sprintf('Upload directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
+            $this->_raiseMsg(_("There was a problem with the file upload. Please try again later."), MSG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Ensure the file form element specified actually exists.
+        if (!isset($_FILES[$form_name])) {
+            $app->logMsg(sprintf('Form element %s does not exist.', $form_name), LOG_ERR, __FILE__, __LINE__);
+            $this->_raiseMsg(_("There was a problem with the file upload. Please try again later."), MSG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (is_array($_FILES[$form_name]['name'])) {
+            $files = $_FILES[$form_name];
+        } else {
+            // Convert variables to single-cell array so it will loop.
+            $files = array(
+                'name'      => array($_FILES[$form_name]['name']),
+                'type'      => array($_FILES[$form_name]['type']),
+                'tmp_name'  => array($_FILES[$form_name]['tmp_name']),
+                'error'     => array($_FILES[$form_name]['error']),
+                'size'      => array($_FILES[$form_name]['size']),
+            );
+        }
+
+        // To keep this script running even if user tries to stop browser.
+        ignore_user_abort(true);
+        ini_set('max_execution_time', 300);
+        ini_set('max_input_time', 300);
+
+        $new_file_names = array();
+
+        $num = sizeof($files['name']);
+        for ($i=0; $i<$num; $i++) {
+            $file_path_name = '';
+
+            if ('' == trim($files['name'][$i])) {
+                // User may not have attached a file.
+                $app->logMsg(sprintf('Skipping file %s with empty name', $i), LOG_DEBUG, __FILE__, __LINE__);
+                continue;
+            }
+
+            // Determine final file name.
+            if ($num == 1) {
+                // Single upload.
+                if (isset($custom_file_name)) {
+                    if (is_array($custom_file_name) && sizeof($custom_file_name) == 1) {
+                        // Is an array, but just one value. Pull it out.
+                        $file_name = current($custom_file_name);
+                        $this->_raiseMsg(sprintf(_("The file %s has been renamed to %s."), $files['name'][$i], $file_name), MSG_NOTICE, __FILE__, __LINE__);
+                        $app->logMsg(sprintf('Using custom file name: %s', $file_name), LOG_DEBUG, __FILE__, __LINE__);
+                    } else if (!is_array($custom_file_name) && '' != $custom_file_name) {
+                        // Valid custom file name.
+                        $file_name = $custom_file_name;
+                        $this->_raiseMsg(sprintf(_("The file %s has been renamed to %s."), $files['name'][$i], $file_name), MSG_NOTICE, __FILE__, __LINE__);
+                        $app->logMsg(sprintf('Using custom file name: %s', $file_name), LOG_DEBUG, __FILE__, __LINE__);
+                    } else {
+                        // Invalid custom file name provided. Use uploaded file name.
+                        $file_name = $files['name'][$i];
+                        $app->logMsg(sprintf('Custom filename invalid! Using uploaded file name: %s', $file_name), LOG_WARNING, __FILE__, __LINE__);
+                    }
+                } else {
+                    // Normal case. Use uploaded file name.
+                    $file_name = $files['name'][$i];
+                    $app->logMsg(sprintf('Using uploaded file name: %s', $file_name), LOG_DEBUG, __FILE__, __LINE__);
+                }
+            } else {
+                // Multiple upload. Final file names must be array.
+                if (isset($custom_file_name) && is_array($custom_file_name) && '' != $custom_file_name[$i]) {
+                    // Valid custom file name.
+                    $file_name = $custom_file_name[$i];
+                    $this->_raiseMsg(sprintf(_("The file %s has been renamed to %s."), $files['name'][$i], $file_name), MSG_NOTICE, __FILE__, __LINE__);
+                    $app->logMsg(sprintf('Using custom file name: %s', $file_name), LOG_DEBUG, __FILE__, __LINE__);
+                } else {
+                    // Invalid custom file name provided. Use uploaded file name.
+                    $file_name = $files['name'][$i];
+                    $app->logMsg(sprintf('Using uploaded file name: %s', $file_name), LOG_DEBUG, __FILE__, __LINE__);
+                }
+            }
+
+            // Check The php upload error messages.
+            if (UPLOAD_ERR_INI_SIZE === $files['error'][$i]) { // Error code 1
+                $this->_raiseMsg(sprintf(_("The file %s failed uploading: it exceeds the maximum allowed upload file size of %s."), $file_name, ini_get('upload_max_filesize')), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('The file %s failed uploading with PHP error UPLOAD_ERR_INI_SIZE (currently %s).', $file_name, ini_get('upload_max_filesize')), LOG_ERR, __FILE__, __LINE__);
+                $this->errors[] = array('filename' => $file_name, 'errortype' => UPLOAD_ERR_INI_SIZE);
+                continue;
+            }
+            if (UPLOAD_ERR_FORM_SIZE === $files['error'][$i]) { // Error code 2
+                $this->_raiseMsg(sprintf(_("The file %s failed uploading: it exceeds the maximum allowed upload file size of %s."), $file_name, $_POST['MAX_FILE_SIZE']), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('The file %s failed uploading with PHP error UPLOAD_ERR_FORM_SIZE (currently %s).', $file_name, $_POST['MAX_FILE_SIZE']), LOG_ERR, __FILE__, __LINE__);
+                $this->errors[] = array('filename' => $file_name, 'errortype' => UPLOAD_ERR_FORM_SIZE);
+                continue;
+            }
+            if (UPLOAD_ERR_PARTIAL === $files['error'][$i]) { // Error code 3
+                $this->_raiseMsg(sprintf(_("The file %s failed uploading: it was only partially uploaded."), $file_name), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('The file %s failed uploading with PHP error UPLOAD_ERR_PARTIAL.', $file_name), LOG_ERR, __FILE__, __LINE__);
+                $this->errors[] = array('filename' => $file_name, 'errortype' => UPLOAD_ERR_PARTIAL);
+                continue;
+            }
+            if (UPLOAD_ERR_NO_FILE === $files['error'][$i]) { // Error code 4
+                $this->_raiseMsg(sprintf(_("The file %s failed uploading: no file was uploaded."), $file_name), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('The file %s failed uploading with PHP error UPLOAD_ERR_NO_FILE.', $file_name), LOG_ERR, __FILE__, __LINE__);
+                $this->errors[] = array('filename' => $file_name, 'errortype' => UPLOAD_ERR_NO_FILE);
+                continue;
+            }
+            if (UPLOAD_ERR_NO_TMP_DIR === $files['error'][$i]) { // Error code 6
+                $this->_raiseMsg(sprintf(_("The file %s failed uploading: temporary upload directory missing."), $file_name), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('The file %s failed uploading with PHP error UPLOAD_ERR_NO_TMP_DIR.', $file_name), LOG_ERR, __FILE__, __LINE__);
+                $this->errors[] = array('filename' => $file_name, 'errortype' => UPLOAD_ERR_NO_TMP_DIR);
+                continue;
+            }
+
+            // Check to be sure it's an uploaded file.
+            if (!is_uploaded_file($files['tmp_name'][$i])) {
+                $this->_raiseMsg(sprintf(_("The file %s failed uploading."), $file_name), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('The file %s failed is_uploaded_file.', $file_name), LOG_ERR, __FILE__, __LINE__);
+                $this->errors[] = array('filename' => $file_name, 'errortype' => UPLOAD_USER_ERR_NOT_UPLOADED_FILE);
+                continue;
+            }
+
+            // Check to be sure the file is not empty.
+            if ($files['size'][$i] <= 0) {
+                $this->_raiseMsg(sprintf(_("The file %s failed uploading: it contains zero bytes."), $file_name), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('The uploaded file %s contains zero bytes.', $file_name), LOG_ERR, __FILE__, __LINE__);
+                $this->errors[] = array('filename' => $file_name, 'errortype' => UPLOAD_USER_ERR_EMPTY_FILE);
+                continue;
+            }
+
+            // Check to be sure the file has a valid file name extension.
+            if (!in_array(mb_strtolower(self::getFilenameExtension($file_name)), $this->getParam('valid_file_extensions'))) {
+                // TODO: Add option to allow any extension to be uploaded.
+                $this->_raiseMsg(sprintf(_("The file %s failed uploading: it is an unrecognized type. Files must have one of the following file name extensions: %s."), $file_name, join(', ', $this->getParam('valid_file_extensions'))), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('The uploaded file %s has an unrecognized file name extension.', $file_name), LOG_WARNING, __FILE__, __LINE__);
+                $this->errors[] = array('filename' => $file_name, 'errortype' => UPLOAD_USER_ERR_INVALID_EXTENSION);
+                continue;
+            }
+
+            // Check to be sure the file has a unique file name.
+            if (!$this->getParam('allow_overwriting') && $this->exists($file_name)) {
+                $this->_raiseMsg(sprintf(_("The file %s failed uploading: a file with that name already exists."), $file_name), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('The uploaded file %s does not have a unique filename.', $file_name), LOG_WARNING, __FILE__, __LINE__);
+                $this->errors[] = array('filename' => $file_name, 'errortype' => UPLOAD_USER_ERR_NOT_UNIQUE);
+                continue;
+            }
+
+            // If the file name has no extension, use the mime-type to choose one.
+            if (!preg_match('/\.[^.]{1,5}$/', $file_name) && function_exists('mime_content_type')) {
+                // TODO: will this run if an extension is filtered by 'valid_file_extensions'?
+                if ($ext = array_search(mime_content_type($files['tmp_name'][$i]), $this->mime_extension_map)) {
+                    $file_name .= ".$ext";
+                }
+            }
+
+            // Clean the file name of bad characters.
+            $file_name = self::cleanFileName($file_name);
+
+            // FINAL path and file name, lowercase extension.
+            $file_extension = mb_strtolower(self::getFilenameExtension($file_name));
+            $file_name = sprintf('%s.%s', mb_substr($file_name, 0, mb_strrpos($file_name, '.')), $file_extension);
+            $file_path_name = sprintf('%s/%s', $this->getParam('upload_path'), $file_name);
+
+            // Move the file to the final place.
+            if (move_uploaded_file($files['tmp_name'][$i], $file_path_name)) {
+                chmod($file_path_name, $this->getParam('dest_file_perms'));
+                $app->logMsg(sprintf('File uploaded: %s', $file_path_name), LOG_INFO, __FILE__, __LINE__);
+                $this->_raiseMsg(sprintf(_("The file %s uploaded successfully."), $file_name), MSG_SUCCESS, __FILE__, __LINE__);
+                if (!isset($custom_file_name) && $files['name'][$i] != $file_name) {
+                    // Notify user if uploaded file name was modified (unless a custom file name will be used anyways).
+                    $this->_raiseMsg(sprintf(_("The file %s was renamed to %s."), $files['name'][$i], $file_name), MSG_NOTICE, __FILE__, __LINE__);
+                }
+                $new_file_names[] = array(
+                    'name' => $file_name,
+                    'mime' => $files['type'][$i],
+                    'size' => filesize($file_path_name),
+                    'extension' => $file_extension,
+                    'original_index' => $i,
+                    'original_name' => $files['name'][$i],
+                );
+            } else {
+                $this->_raiseMsg(sprintf(_("The file %s failed uploading."), $file_name), MSG_ERR, __FILE__, __LINE__);
+                $app->logMsg(sprintf('Moving file failed: %s -> %s', $files['tmp_name'][$i], $file_path_name), LOG_ALERT, __FILE__, __LINE__);
+                $this->errors[] = array('filename' => $file_name, 'errortype' => UPLOAD_USER_ERR_MOVE_FAILED);
+            }
+
+            // Check file extension with browsers interpretation of file type.
+            if (isset($this->mime_extension_map[$file_extension]) && $files['type'][$i] != $this->mime_extension_map[$file_extension]) {
+                $app->logMsg(sprintf('File extension (%s) does not match mime type (%s).', $file_extension, $files['type'][$i]), LOG_DEBUG, __FILE__, __LINE__);
+            }
+        }
+
+        // Return names of files uploaded (or empty array when none processed).
+        return $new_file_names;
+    }
+
+    /**
+     * Remove file within upload path.
+     *
+     * @access  public
+     * @param   string  $file_name  A name of a file.
+     * @param   bool    $use_glob   Set true to use glob to find the filename (using $file_name as a pattern)
+     * @return  bool                Success of operation.
+     */
+    public function deleteFile($file_name, $use_glob=false)
+    {
+        $app =& App::getInstance();
+
+        // Ensure we have a upload directory.
+        if (!$this->getParam('upload_path')) {
+            $app->logMsg(sprintf('Upload directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        $file_path_name = $this->getParam('upload_path') . '/' . ($use_glob ? $this->getFilenameGlob($file_name) : $file_name);
+
+        if (!is_file($file_path_name)) {
+            $app->logMsg(sprintf('Error deleting nonexistent file: %s', $file_path_name), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        } else if (unlink($file_path_name)) {
+            $app->logMsg(sprintf('Deleted file: %s', $file_path_name), LOG_INFO, __FILE__, __LINE__);
+            return true;
+        } else {
+            $this->_raiseMsg(sprintf(_("The file %s could not be deleted."), $file_name), MSG_ERR, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Failed deleting file: %s', $file_path_name), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Renames a file within the upload path.
+     *
+     * @access  public
+     * @param   string  $old_name   The currently existing file name.
+     * @param   string  $new_name   The new name for this file.
+     * @return  bool                Success of operation.
+     */
+    public function moveFile($old_name, $new_name)
+    {
+        $app =& App::getInstance();
+
+        // Ensure we have an upload directory.
+        if (!$this->getParam('upload_path')) {
+            $app->logMsg(sprintf('Upload directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        $old_file_path_name = $this->getParam('upload_path') . '/' . $old_name;
+        $new_file_path_name = $this->getParam('upload_path') . '/' . $new_name;
+        if (file_exists($old_file_path_name)) {
+            if (rename($old_file_path_name, $new_file_path_name)) {
+                $this->_raiseMsg(sprintf(_("The file %s has been renamed to %s."), basename($old_file_path_name), basename($new_file_path_name)), MSG_NOTICE, __FILE__, __LINE__);
+                $app->logMsg(sprintf('File renamed from %s to %s', $old_file_path_name, $new_file_path_name), LOG_DEBUG, __FILE__, __LINE__);
+            } else {
+                $this->_raiseMsg(sprintf(_("Error renaming file to %s"), $new_file_path_name), MSG_WARNING, __FILE__, __LINE__);
+                $app->logMsg(sprintf('Error renaming file to %s', $new_file_path_name), LOG_WARNING, __FILE__, __LINE__);
+                return false;
+            }
+        } else {
+            $this->_raiseMsg(sprintf(_("Couldn't rename nonexistent file %s."), $old_name), MSG_WARNING, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Error renaming nonexistent file: %s', $old_file_path_name), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * Tests if a file exists within the current upload_path.
+     *
+     * @access  public
+     * @param   string  $file_name  A name of a file.
+     * @return  bool                Existence of file.
+     */
+    public function exists($file_name)
+    {
+        $app =& App::getInstance();
+
+        // Ensure we have a upload directory.
+        if (!$this->getParam('upload_path')) {
+            $app->logMsg(sprintf('Upload directory not set before processing.'), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        return file_exists($this->getParam('upload_path') . '/' . $file_name);
+    }
+
+    /**
+     * Get filename by glob pattern. Searches a directory for an image that matches the
+     * specified glob pattern and returns the filename of the first file found.
+     *
+     * @access  public
+     * @param   string  $pattern   Pattern to match filename.
+     * @return  string filename on success, empty string on failure.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   15 Nov 2005 20:55:22
+     */
+    public function getFilenameGlob($pattern)
+    {
+        $file_list = glob(sprintf('%s/%s', $this->getParam('upload_path'), $pattern));
+        if (isset($file_list[0])) {
+            return basename($file_list[0]);
+        } else {
+            return '';
+        }
+    }
+
+    /**
+     * Returns an array of file names that failed uploading.
+     *
+     * @access  public
+     * @return  array   List of file names.
+     */
+    public function getErrors()
+    {
+        return $this->errors;
+    }
+
+    /**
+     * Determines if any errors occurred while calling the Upload::process method.
+     *
+     * @access  public
+     */
+    public function anyErrors()
+    {
+        return sizeof($this->errors) > 0;
+    }
+
+    /**
+     * Returns the extension of a file name, or an empty string if none exists.
+     *
+     * @access  public
+     * @param   string  $file_name  A name of a file, with extension after a dot.
+     * @return  string              The value found after the dot
+     */
+    static public function getFilenameExtension($file_name)
+    {
+        preg_match('/.*?\.(\w+)$/i', trim($file_name), $ext);
+        return isset($ext[1]) ? $ext[1] : '';
+    }
+
+    /**
+     * An alias for $app->raiseMsg that only sends messages configured by display_messages.
+     *
+     * @access public
+     *
+     * @param string $message The text description of the message.
+     * @param int    $type    The type of message: MSG_NOTICE,
+     *                        MSG_SUCCESS, MSG_WARNING, or MSG_ERR.
+     * @param string $file    __FILE__.
+     * @param string $line    __LINE__.
+     */
+    public function _raiseMsg($message, $type, $file, $line)
+    {
+        $app =& App::getInstance();
+
+        if ($this->getParam('display_messages') === true || (is_int($this->getParam('display_messages')) && ($this->getParam('display_messages') & $type) > 0)) {
+            $app->raiseMsg($message, $type, $file, $line);
+        }
+    }
+
+    /**
+     * Remove unsafe characters from filename.
+     * While this function has been moved to Utilities.inc.php, it should remain here for backwards compatibility.
+     *
+     * @access  public
+     * @param   string  $file_name  A name of a file.
+     * @return  string              The same name, but cleaned.
+     */
+    public function cleanFileName($file_name)
+    {
+        $app =& App::getInstance();
+
+        $file_name = preg_replace(array(
+            '/&([a-z]{1,2})(?:acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml|caron);/ui',
+            '/&(?:amp);/ui',
+            '/[&;]+/u',
+            '/[^a-zA-Z0-9()@._=+-]+/u',
+            '/^_+|_+$/u'
+        ), array(
+            '$1',
+            'and',
+            '',
+            '_',
+            ''
+        ), htmlentities($file_name, ENT_NOQUOTES | ENT_IGNORE, $app->getParam('character_set')));
+        return mb_substr($file_name, 0, 250);
+    }
+}
+
Index: tags/2.2.3/lib/Utilities.inc.php
===================================================================
--- tags/2.2.3/lib/Utilities.inc.php	(revision 632)
+++ tags/2.2.3/lib/Utilities.inc.php	(revision 632)
@@ -0,0 +1,1497 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Utilities.inc.php
+ */
+
+
+/**
+ * Print variable dump.
+ *
+ * @param  mixed    $var        The variable to dump.
+ * @param  bool     $display    Print the dump in <pre> tags or hide it in html comments (non-CLI only).
+ * @param  bool     $dump_method   Dump method. See SC_DUMP_* constants.
+ * @param  string   $file       Value of __FILE__.
+ * @param  string   $line       Value of __LINE__
+ */
+define('SC_DUMP_PRINT_R', 0);
+define('SC_DUMP_VAR_DUMP', 1);
+define('SC_DUMP_VAR_EXPORT', 2);
+function dump($var, $display=false, $dump_method=SC_DUMP_PRINT_R, $file='', $line='')
+{
+    $app =& App::getInstance();
+
+    if ($app->cli) {
+        echo "DUMP FROM: $file $line\n";
+    } else {
+        echo $display ? "\n<br />DUMP <strong>$file $line</strong><br /><pre>\n" : "\n<!-- DUMP $file $line\n";
+    }
+
+    switch ($dump_method) {
+    case SC_DUMP_PRINT_R:
+    default:
+        // Print human-readable descriptions of invisible types.
+        if (null === $var) {
+            echo '(null)';
+        } else if (true === $var) {
+            echo '(bool: true)';
+        } else if (false === $var) {
+            echo '(bool: false)';
+        } else if (is_scalar($var) && '' === $var) {
+            echo '(empty string)';
+        } else if (is_scalar($var) && preg_match('/^\s+$/', $var)) {
+            echo '(only white space)';
+        } else {
+            print_r($var);
+        }
+        break;
+
+    case SC_DUMP_VAR_DUMP:
+        var_dump($var);
+        break;
+
+    case SC_DUMP_VAR_EXPORT:
+        var_export($var);
+        break;
+    }
+
+    if ($app->cli) {
+        echo "\n";
+    } else {
+        echo $display ? "\n</pre><br />\n" : "\n-->\n";
+    }
+}
+
+/*
+* Log a PHP variable to javascript console. Relies on getDump(), below.
+*
+* @access   public
+* @param    mixed   $var      The variable to dump.
+* @param    string  $prefix   A short note to print before the output to make identifying output easier.
+* @param    string  $file     The value of __FILE__.
+* @param    string  $line     The value of __LINE__.
+* @return   null
+* @author   Quinn Comendant <quinn@strangecode.com>
+*/
+function jsDump($var, $prefix='jsDump', $file='-', $line='-')
+{
+    if (!empty($var)) {
+        ?>
+        <script type="text/javascript">
+        /* <![CDATA[ */
+        console.log('<?php printf('%s: %s (on line %s of %s)', $prefix, str_replace("'", "\\'", getDump($var, true)), $line, $file); ?>');
+        /* ]]> */
+        </script>
+        <?php
+    }
+}
+
+/*
+* Return a string version of any variable, optionally serialized on one line.
+*
+* @access   public
+* @param    mixed   $var        The variable to dump.
+* @param    bool    $serialize  If true, remove line-endings. Useful for logging variables.
+* @return   string              The dumped variable.
+* @author   Quinn Comendant <quinn@strangecode.com>
+*/
+function getDump($var, $serialize=false)
+{
+    ob_start();
+    print_r($var);
+    $d = ob_get_contents();
+    ob_end_clean();
+    return $serialize ? preg_replace('/\s+/m', ' ', $d) : $d;
+}
+
+/**
+ * Return dump as cleaned text. Useful for dumping data into emails.
+ *
+ * @param  array    $var        Variable to dump.
+ * @param  strong   $indent     A string to prepend indented lines (tab for example).
+ * @return string Dump of var.
+ */
+function fancyDump($var, $indent='')
+{
+    $output = '';
+    if (is_array($var)) {
+        foreach ($var as $k=>$v) {
+            $k = ucfirst(mb_strtolower(str_replace(array('_', '  '), ' ', $k)));
+            if (is_array($v)) {
+                $output .= sprintf("\n%s%s: %s\n", $indent, $k, fancyDump($v, $indent . $indent));
+            } else {
+                $output .= sprintf("%s%s: %s\n", $indent, $k, $v);
+            }
+        }
+    } else {
+        $output .= sprintf("%s%s\n", $indent, $var);
+    }
+    return $output;
+}
+
+/**
+ * @param string|mixed $value A string to UTF8-encode.
+ *
+ * @returns string|mixed The UTF8-encoded string, or the object passed in if
+ *    it wasn't a string.
+ */
+function conditionalUTF8Encode($value)
+{
+  if (is_string($value) && mb_detect_encoding($value, 'UTF-8', true) != 'UTF-8') {
+    return utf8_encode($value);
+  } else {
+    return $value;
+  }
+}
+
+
+/**
+ * Returns text with appropriate html translations (a smart wrapper for htmlspecialchars()).
+ *
+ * @param  string $text             Text to clean.
+ * @param  bool   $preserve_html    If set to true, oTxt will not translate <, >, ", or '
+ *                                  characters into HTML entities. This allows HTML to pass through undisturbed.
+ * @return string                   HTML-safe text.
+ */
+function oTxt($text, $preserve_html=false)
+{
+    $app =& App::getInstance();
+
+    $search = array();
+    $replace = array();
+
+    // Make converted ampersand entities into normal ampersands (they will be done manually later) to retain HTML entities.
+    $search['retain_ampersand']     = '/&amp;/';
+    $replace['retain_ampersand']    = '&';
+
+    if ($preserve_html) {
+        // Convert characters that must remain non-entities for displaying HTML.
+        $search['retain_left_angle']       = '/&lt;/';
+        $replace['retain_left_angle']      = '<';
+
+        $search['retain_right_angle']      = '/&gt;/';
+        $replace['retain_right_angle']     = '>';
+
+        $search['retain_single_quote']     = '/&#039;/';
+        $replace['retain_single_quote']    = "'";
+
+        $search['retain_double_quote']     = '/&quot;/';
+        $replace['retain_double_quote']    = '"';
+    }
+
+    // & becomes &amp;. Exclude any occurrence where the & is followed by a alphanum or unicode character.
+    $search['ampersand']        = '/&(?![\w\d#]{1,10};)/';
+    $replace['ampersand']       = '&amp;';
+
+    return preg_replace($search, $replace, htmlspecialchars($text, ENT_QUOTES, $app->getParam('character_set')));
+}
+
+/**
+ * Returns text with stylistic modifications. Warning: this will break some HTML attributes!
+ * TODO: Allow a string such as this to be passed: <a href="javascript:openPopup('/foo/bar.php')">Click here</a>
+ *
+ * @param  string   $text Text to clean.
+ * @return string         Cleaned text.
+ */
+function fancyTxt($text)
+{
+    $search = array();
+    $replace = array();
+
+    // "double quoted text"  becomes  &ldquo;double quoted text&rdquo;
+    $search['double_quotes']    = '/(^|[^\w=])(?:"|&quot;|&#34;|&#x22;|&ldquo;)([^"]+?)(?:"|&quot;|&#34;|&#x22;|&rdquo;)([^\w]|$)/ms'; // " is the same as &quot; and &#34; and &#x22;
+    $replace['double_quotes']   = '$1&ldquo;$2&rdquo;$3';
+
+    // text's apostrophes  become  text&rsquo;s apostrophes
+    $search['apostrophe']       = '/(\w)(?:\'|&#39;|&#039;)(\w)/ms';
+    $replace['apostrophe']      = '$1&rsquo;$2';
+
+    // 'single quoted text'  becomes  &lsquo;single quoted text&rsquo;
+    $search['single_quotes']    = '/(^|[^\w=])(?:\'|&#39;|&lsquo;)([^\']+?)(?:\'|&#39;|&rsquo;)([^\w]|$)/ms';
+    $replace['single_quotes']   = '$1&lsquo;$2&rsquo;$3';
+
+    // plural posessives' apostrophes become posessives&rsquo;
+    $search['apostrophes']      = '/(s)(?:\'|&#39;|&#039;)(\s)/ms';
+    $replace['apostrophes']     = '$1&rsquo;$2';
+
+    // em--dashes  become em&mdash;dashes
+    $search['em_dash']          = '/(\s*[^!<-])--([^>-]\s*)/';
+    $replace['em_dash']         = '$1&mdash;$2';
+
+    return preg_replace($search, $replace, $text);
+}
+
+/*
+* Finds all URLs in text and hyperlinks them.
+*
+* @access   public
+* @param    string  $text   Text to search for URLs.
+* @param    bool    $strict True to only include URLs starting with a scheme (http:// ftp:// im://), or false to include URLs starting with 'www.'.
+* @param    mixed   $length Number of characters to truncate URL, or NULL to disable truncating.
+* @param    string  $delim  Delimiter to append, indicate truncation.
+* @return   string          Same input text, but URLs hyperlinked.
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  2.0
+* @since    22 Mar 2015 23:29:04
+*/
+function hyperlinkTxt($text, $strict=false, $length=null, $delim='âŠ')
+{
+    // A list of schemes we allow at the beginning of a URL.
+    $schemes = 'mailto:|tel:|skype:|callto:|facetime:|bitcoin:|geo:|magnet:\?|sip:|sms:|xmpp:|view-source:(?:https?://)?|[\w-]{2,}://';
+
+    // Capture the full URL into the first match and only the first X characters into the second match.
+    // This will match URLs not preceeded by " ' or = (URLs inside an attribute) or ` (Markdown quoted) or double-scheme (http://http://www.asdf.com)
+    // Valid URL characters: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=
+    $regex = '@
+        \b                              # Start with a word-boundary.
+        (?<!"|\'|=|>|`|[\w-]{2}://)     # Negative look-behind to exclude URLs already in <a> tag, Markdown quoted, or double SCHEME://
+        (                               # Begin match 1
+            (                           # Begin match 2
+                (?:%s)                  # URL starts with known scheme or www. if strict = false
+                [^\s/$.?#]+             # Any domain-valid characters
+                [^\s"`<>]{1,%s}         # Match 2 is limited to a maximum of LENGTH valid URL characters
+            )
+            [^\s"`<>]*                  # Match 1 continues with any further valid URL characters
+            ([^\P{Any}\sâŠ<>Â«Â»"ââ%s])    # Final character not a space or common end-of-sentence punctuation (.,:;?!, etc). Using double negation set, see http://stackoverflow.com/a/4786560/277303
+        )
+        @Suxi
+    ';
+    $regex = sprintf($regex,
+        ($strict ? $schemes : $schemes . '|www\.'), // Strict=false adds "www." to the list of allowed start-of-URL.
+        ($length ? $length : ''),
+        ($strict ? '' : '?!.,:;)\'-') // Strict=false excludes some "URL-valid" characters from the last character of URL. (Hyphen must remain last character in this class.)
+    );
+
+    // Use a callback function to decide when to append the delim.
+    // Also encode special chars with oTxt().
+    return preg_replace_callback($regex, function ($m) use ($length, $delim) {
+        $url = $m[1];
+        $truncated_url = $m[2] . $m[3];
+        $absolute_url = preg_replace('!^www\.!', 'http://www.', $url);
+        if (is_null($length) || $url == $truncated_url) {
+            // If not truncating, or URL was not truncated.
+            // Remove http schemas, and any single trailing / to make the display URL.
+            $display_url = preg_replace(['!^https?://!', '!^([^/]+)/$!'], ['', '$1'], $url);
+            return sprintf('<a href="%s">%s</a>', oTxt($absolute_url), $display_url);
+        } else {
+            // Truncated URL.
+            // Remove http schemas, and any single trailing / to make the display URL.
+            $display_url = preg_replace(['!^https?://!', '!^([^/]+)/$!'], ['', '$1'], trim($truncated_url));
+            return sprintf('<a href="%s">%s%s</a>', oTxt($absolute_url), $display_url, $delim);
+        }
+    }, $text);
+}
+
+/**
+ * Applies a class to search terms to highlight them ala google results.
+ *
+ * @param  string   $text   Input text to search.
+ * @param  string   $search String of word(s) that will be highlighted.
+ * @param  string   $class  CSS class to apply.
+ * @return string           Text with searched words wrapped in <span>.
+ */
+function highlightWords($text, $search, $class='sc-highlightwords')
+{
+    $words = preg_split('/[^\w]/', $search, -1, PREG_SPLIT_NO_EMPTY);
+
+    $search = array();
+    $replace = array();
+
+    foreach ($words as $w) {
+        if ('' != trim($w)) {
+            $search[] = '/\b(' . preg_quote($w) . ')\b/i';
+            $replace[] = '<span class="' . $class . '">$1</span>';
+        }
+    }
+
+    return empty($replace) ? $text : preg_replace($search, $replace, $text);
+}
+
+/**
+ * Generates a hexadecimal html color based on provided word.
+ *
+ * @access public
+ * @param  string $text  A string for which to convert to color.
+ * @return string  A hexadecimal html color.
+ */
+function getTextColor($text, $method=1, $n=0.87)
+{
+    $hash = md5($text);
+    $rgb = array(
+        mb_substr($hash, 0, 1),
+        mb_substr($hash, 1, 1),
+        mb_substr($hash, 2, 1),
+        mb_substr($hash, 3, 1),
+        mb_substr($hash, 4, 1),
+        mb_substr($hash, 5, 1),
+    );
+
+    switch ($method) {
+    case 1 :
+    default :
+        // Reduce all hex values slightly to avoid all white.
+        array_walk($rgb, create_function('&$v', "\$v = dechex(round(hexdec(\$v) * $n));"));
+        break;
+    case 2 :
+        foreach ($rgb as $i => $v) {
+            if (hexdec($v) > hexdec('c')) {
+                $rgb[$i] = dechex(hexdec('f') - hexdec($v));
+            }
+        }
+        break;
+    }
+
+    return join('', $rgb);
+}
+
+/**
+ * Encodes a string into unicode values 128-255.
+ * Useful for hiding an email address from spambots.
+ *
+ * @access  public
+ * @param   string   $text   A line of text to encode.
+ * @return  string   Encoded text.
+ */
+function encodeAscii($text)
+{
+    $output = '';
+    $num = mb_strlen($text);
+    for ($i=0; $i<$num; $i++) {
+        $output .= sprintf('&#%03s', ord($text{$i}));
+    }
+    return $output;
+}
+
+/**
+ * Encodes an email into a "user at domain dot com" format.
+ *
+ * @access  public
+ * @param   string   $email   An email to encode.
+ * @param   string   $at      Replaces the @.
+ * @param   string   $dot     Replaces the ..
+ * @return  string   Encoded email.
+ */
+function encodeEmail($email, $at=' at ', $dot=' dot ')
+{
+    $search = array('/@/', '/\./');
+    $replace = array($at, $dot);
+    return preg_replace($search, $replace, $email);
+}
+
+/**
+ * Truncates "a really long string" into a string of specified length
+ * at the beginning: "âŠlong string"
+ * at the middle: "a reaâŠstring"
+ * or at the end: "a reallyâŠ".
+ *
+ * The regular expressions below first match and replace the string to the specified length and position,
+ * and secondly they remove any whitespace from around the delimiter (to avoid "this âŠ " from happening).
+ *
+ * @access  public
+ * @param   string  $str    Input string
+ * @param   int     $len    Maximum string length.
+ * @param   string  $where  Where to cut the string. One of: 'start', 'middle', or 'end'.
+ * @return  string          Truncated output string.
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @since   29 Mar 2006 13:48:49
+ */
+function truncate($str, $len=50, $where='end', $delim='âŠ')
+{
+    $dlen = mb_strlen($delim);
+    if ($len <= $dlen || mb_strlen($str) <= $dlen) {
+        return substr($str, 0, $len);
+    }
+    $part1 = floor(($len - $dlen) / 2);
+    $part2 = ceil(($len - $dlen) / 2);
+
+    if ($len > ini_get('pcre.backtrack_limit')) {
+        $app =& App::getInstance();
+        $app->logMsg(sprintf('Asked to truncate string len of %s > pcre.backtrack_limit of %s', $len, ini_get('pcre.backtrack_limit')), LOG_DEBUG, __FILE__, __LINE__);
+        ini_set('pcre.backtrack_limit', $len);
+    }
+
+    switch ($where) {
+    case 'start' :
+        return preg_replace(array(sprintf('/^.{%s,}(.{%s})$/su', $dlen + 1, $part1 + $part2), sprintf('/\s*%s{%s,}\s*/su', preg_quote($delim), $dlen)), array($delim . '$1', $delim), $str);
+
+    case 'middle' :
+        return preg_replace(array(sprintf('/^(.{%s}).{%s,}(.{%s})$/su', $part1, $dlen + 1, $part2), sprintf('/\s*%s{%s,}\s*/su', preg_quote($delim), $dlen)), array('$1' . $delim . '$2', $delim), $str);
+
+    case 'end' :
+    default :
+        return preg_replace(array(sprintf('/^(.{%s}).{%s,}$/su', $part1 + $part2, $dlen + 1), sprintf('/\s*%s{%s,}\s*/su', preg_quote($delim), $dlen)), array('$1' . $delim, $delim), $str);
+    }
+}
+
+/*
+* A substitution for the missing mb_ucfirst function.
+*
+* @access   public
+* @param    string  $string The string
+* @return   string          String with upper-cased first character.
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    06 Dec 2008 17:04:01
+*/
+if (!function_exists('mb_ucfirst')) {
+    function mb_ucfirst($string)
+    {
+        return mb_strtoupper(mb_substr($string, 0, 1)) . mb_substr($string, 1, mb_strlen($string));
+    }
+}
+
+/*
+* A substitution for the missing mb_strtr function.
+*
+* @access   public
+* @param    string  $string The string
+* @param    string  $from   String of characters to translate from
+* @param    string  $to     String of characters to translate to
+* @return   string          String with translated characters.
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    20 Jan 2013 12:33:26
+*/
+if (!function_exists('mb_strtr')) {
+    function mb_strtr($string, $from, $to)
+    {
+        return str_replace(mb_split('.', $from), mb_split('.', $to), $string);
+    }
+}
+
+/*
+* A substitution for the missing mb_str_pad function.
+*
+* @access   public
+* @param    string  $input      The string that receives padding.
+* @param    string  $pad_length Total length of resultant string.
+* @param    string  $pad_string The string to use for padding
+* @param    string  $pad_type   Flags STR_PAD_RIGHT or STR_PAD_LEFT or STR_PAD_BOTH
+* @return   string          String with translated characters.
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    20 Jan 2013 12:33:26
+*/
+if (!function_exists('mb_str_pad')) {
+    function mb_str_pad($input, $pad_length, $pad_string=' ', $pad_type=STR_PAD_RIGHT) {
+        $diff = strlen($input) - mb_strlen($input);
+        return str_pad($input, $pad_length + $diff, $pad_string, $pad_type);
+    }
+}
+
+/*
+* Converts a string into a URL-safe slug, removing spaces and non word characters.
+*
+* @access   public
+* @param    string  $str    String to convert.
+* @return   string          URL-safe slug.
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    18 Aug 2014 12:54:29
+*/
+function URLSlug($str)
+{
+    $slug = preg_replace(array('/\W+/u', '/^-+|-+$/'), array('-', ''), $str);
+    $slug = strtolower($slug);
+    return $slug;
+}
+
+/**
+ * Return a human readable disk space measurement. Input value measured in bytes.
+ *
+ * @param       int    $size        Size in bytes.
+ * @param       int    $unit        The maximum unit
+ * @param       int    $format      The return string format
+ * @author      Aidan Lister <aidan@php.net>
+ * @author      Quinn Comendant <quinn@strangecode.com>
+ * @version     1.2.0
+ */
+function humanFileSize($size, $format='%01.2f %s', $max_unit=null, $multiplier=1024)
+{
+    // Units
+    $units = array('B', 'KB', 'MB', 'GB', 'TB');
+    $ii = count($units) - 1;
+
+    // Max unit
+    $max_unit = array_search((string) $max_unit, $units);
+    if ($max_unit === null || $max_unit === false) {
+        $max_unit = $ii;
+    }
+
+    // Loop
+    $i = 0;
+    while ($max_unit != $i && $size >= $multiplier && $i < $ii) {
+        $size /= $multiplier;
+        $i++;
+    }
+
+    return sprintf($format, $size, $units[$i]);
+}
+
+/*
+* Returns a human readable amount of time for the given amount of seconds.
+*
+* 45 seconds
+* 12 minutes
+* 3.5 hours
+* 2 days
+* 1 week
+* 4 months
+*
+* Months are calculated using the real number of days in a year: 365.2422 / 12.
+*
+* @access   public
+* @param    int $seconds Seconds of time.
+* @param    string $max_unit Key value from the $units array.
+* @param    string $format Sprintf formatting string.
+* @return   string Value of units elapsed.
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    23 Jun 2006 12:15:19
+*/
+function humanTime($seconds, $max_unit=null, $format='%01.1f')
+{
+    // Units: array of seconds in the unit, singular and plural unit names.
+    $units = array(
+        'second' => array(1, _("second"), _("seconds")),
+        'minute' => array(60, _("minute"), _("minutes")),
+        'hour' => array(3600, _("hour"), _("hours")),
+        'day' => array(86400, _("day"), _("days")),
+        'week' => array(604800, _("week"), _("weeks")),
+        'month' => array(2629743.84, _("month"), _("months")),
+        'year' => array(31556926.08, _("year"), _("years")),
+        'decade' => array(315569260.8, _("decade"), _("decades")),
+        'century' => array(3155692608, _("century"), _("centuries")),
+    );
+
+    // Max unit to calculate.
+    $max_unit = isset($units[$max_unit]) ? $max_unit : 'year';
+
+    $final_time = $seconds;
+    $final_unit = 'second';
+    foreach ($units as $k => $v) {
+        if ($seconds >= $v[0]) {
+            $final_time = $seconds / $v[0];
+            $final_unit = $k;
+        }
+        if ($max_unit == $final_unit) {
+            break;
+        }
+    }
+    $final_time = sprintf($format, $final_time);
+    return sprintf('%s %s', $final_time, (1 == $final_time ? $units[$final_unit][1] : $units[$final_unit][2]));
+}
+
+/**
+ * Removes non-latin characters from file name, using htmlentities to convert known weirdos into regular squares.
+ *
+ * @access  public
+ * @param   string  $file_name  A name of a file.
+ * @return  string              The same name, but cleaned.
+ */
+function cleanFileName($file_name)
+{
+    $app =& App::getInstance();
+
+    $file_name = preg_replace(array(
+        '/&([a-z]{1,2})(?:acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml|caron);/ui',
+        '/&(?:amp);/ui',
+        '/[&;]+/u',
+        '/[^a-zA-Z0-9()@._=+-]+/u',
+        '/^_+|_+$/u'
+    ), array(
+        '$1',
+        'and',
+        '',
+        '_',
+        ''
+    ), htmlentities($file_name, ENT_NOQUOTES | ENT_IGNORE, $app->getParam('character_set')));
+    return mb_substr($file_name, 0, 250);
+}
+
+/**
+ * Returns the extension of a file name, or an empty string if none exists.
+ *
+ * @access  public
+ * @param   string  $file_name  A name of a file, with extension after a dot.
+ * @return  string              The value found after the dot
+ */
+function getFilenameExtension($file_name)
+{
+    preg_match('/.*?\.(\w+)$/i', trim($file_name), $ext);
+    return isset($ext[1]) ? $ext[1] : '';
+}
+
+/*
+* Convert a php.ini value (8M, 512K, etc), into integer value of bytes.
+*
+* @access   public
+* @param    string  $val    Value from php config, e.g., upload_max_filesize.
+* @return   int             Value converted to bytes as an integer.
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    20 Aug 2014 14:32:41
+*/
+function phpIniGetBytes($val)
+{
+    $val = trim(ini_get($val));
+    if ($val != '') {
+        $last = strtolower($val{strlen($val) - 1});
+    } else {
+        $last = '';
+    }
+    switch ($last) {
+        // The 'G' modifier is available since PHP 5.1.0
+        case 'g':
+            $val *= 1024;
+        case 'm':
+            $val *= 1024;
+        case 'k':
+            $val *= 1024;
+    }
+
+    return (int)$val;
+}
+
+/**
+ * Tests the existence of a file anywhere in the include path.
+ * Replaced by stream_resolve_include_path() in PHP 5 >= 5.3.2
+ *
+ * @param   string  $file   File in include path.
+ * @return  mixed           False if file not found, the path of the file if it is found.
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @since   03 Dec 2005 14:23:26
+ */
+function fileExistsIncludePath($file)
+{
+    $app =& App::getInstance();
+
+    foreach (explode(PATH_SEPARATOR, get_include_path()) as $path) {
+        $fullpath = $path . DIRECTORY_SEPARATOR . $file;
+        if (file_exists($fullpath)) {
+            $app->logMsg(sprintf('Found file "%s" at path: %s', $file, $fullpath), LOG_DEBUG, __FILE__, __LINE__);
+            return $fullpath;
+        } else {
+            $app->logMsg(sprintf('File "%s" not found in include_path: %s', $file, get_include_path()), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+    }
+}
+
+/**
+ * Returns stats of a file from the include path.
+ *
+ * @param   string  $file   File in include path.
+ * @param   mixed   $stat   Which statistic to return (or null to return all).
+ * @return  mixed           Value of requested key from fstat(), or false on error.
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @since   03 Dec 2005 14:23:26
+ */
+function statIncludePath($file, $stat=null)
+{
+    // Open file pointer read-only using include path.
+    if ($fp = fopen($file, 'r', true)) {
+        // File opened successfully, get stats.
+        $stats = fstat($fp);
+        fclose($fp);
+        // Return specified stats.
+        return is_null($stat) ? $stats : $stats[$stat];
+    } else {
+        return false;
+    }
+}
+
+/*
+* Writes content to the specified file. This function emulates the functionality of file_put_contents from PHP 5.
+* It makes an exclusive lock on the file while writing.
+*
+* @access   public
+* @param    string  $filename   Path to file.
+* @param    string  $content    Data to write into file.
+* @return   bool                Success or failure.
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @since    11 Apr 2006 22:48:30
+*/
+function filePutContents($filename, $content)
+{
+    $app =& App::getInstance();
+
+    // Open file for writing and truncate to zero length.
+    if ($fp = fopen($filename, 'w')) {
+        if (flock($fp, LOCK_EX)) {
+            if (!fwrite($fp, $content, mb_strlen($content))) {
+                $app->logMsg(sprintf('Failed writing to file: %s', $filename), LOG_ERR, __FILE__, __LINE__);
+                fclose($fp);
+                return false;
+            }
+            flock($fp, LOCK_UN);
+        } else {
+            $app->logMsg(sprintf('Could not lock file for writing: %s', $filename), LOG_ERR, __FILE__, __LINE__);
+            fclose($fp);
+            return false;
+        }
+        fclose($fp);
+        // Success!
+        $app->logMsg(sprintf('Wrote to file: %s', $filename), LOG_DEBUG, __FILE__, __LINE__);
+        return true;
+    } else {
+        $app->logMsg(sprintf('Could not open file for writing: %s', $filename), LOG_ERR, __FILE__, __LINE__);
+        return false;
+    }
+}
+
+/**
+ * If $var is net set or null, set it to $default. Otherwise leave it alone.
+ * Returns the final value of $var. Use to find a default value of one is not available.
+ *
+ * @param  mixed $var       The variable that is being set.
+ * @param  mixed $default   What to set it to if $val is not currently set.
+ * @return mixed            The resulting value of $var.
+ */
+function setDefault(&$var, $default='')
+{
+    if (!isset($var)) {
+        $var = $default;
+    }
+    return $var;
+}
+
+/**
+ * Like preg_quote() except for arrays, it takes an array of strings and puts
+ * a backslash in front of every character that is part of the regular
+ * expression syntax.
+ *
+ * @param  array $array    input array
+ * @param  array $delim    optional character that will also be escaped.
+ * @return array    an array with the same values as $array1 but shuffled
+ */
+function pregQuoteArray($array, $delim='/')
+{
+    if (!empty($array)) {
+        if (is_array($array)) {
+            foreach ($array as $key=>$val) {
+                $quoted_array[$key] = preg_quote($val, $delim);
+            }
+            return $quoted_array;
+        } else {
+            return preg_quote($array, $delim);
+        }
+    }
+}
+
+/**
+ * Converts a PHP Array into encoded URL arguments and return them as an array.
+ *
+ * @param  mixed $data        An array to transverse recursively, or a string
+ *                            to use directly to create url arguments.
+ * @param  string $prefix     The name of the first dimension of the array.
+ *                            If not specified, the first keys of the array will be used.
+ * @return array              URL with array elements as URL key=value arguments.
+ */
+function urlEncodeArray($data, $prefix='', $_return=true)
+{
+    // Data is stored in static variable.
+    static $args = array();
+
+    if (is_array($data)) {
+        foreach ($data as $key => $val) {
+            // If the prefix is empty, use the $key as the name of the first dimension of the "array".
+            // ...otherwise, append the key as a new dimension of the "array".
+            $new_prefix = ('' == $prefix) ? urlencode($key) : $prefix . '[' . urlencode($key) . ']';
+            // Enter recursion.
+            urlEncodeArray($val, $new_prefix, false);
+        }
+    } else {
+        // We've come to the last dimension of the array, save the "array" and its value.
+        $args[$prefix] = urlencode($data);
+    }
+
+    if ($_return) {
+        // This is not a recursive execution. All recursion is complete.
+        // Reset static var and return the result.
+        $ret = $args;
+        $args = array();
+        return is_array($ret) ? $ret : array();
+    }
+}
+
+/**
+ * Converts a PHP Array into encoded URL arguments and return them in a string.
+ *
+ * Todo: probably update to use the built-in http_build_query().
+ *
+ * @param  mixed $data        An array to transverse recursively, or a string
+ *                            to use directly to create url arguments.
+ * @param  string $prefix     The name of the first dimension of the array.
+ *                            If not specified, the first keys of the array will be used.
+ * @return string url         A string ready to append to a url.
+ */
+function urlEncodeArrayToString($data, $prefix='')
+{
+
+    $array_args = urlEncodeArray($data, $prefix);
+    $url_args = '';
+    $delim = '';
+    foreach ($array_args as $key=>$val) {
+        $url_args .= $delim . $key . '=' . $val;
+        $delim = ini_get('arg_separator.output');
+    }
+    return $url_args;
+}
+
+/**
+ * Fills an array with the result from a multiple ereg search.
+ * Courtesy of Bruno - rbronosky@mac.com - 10-May-2001
+ *
+ * @param  mixed $pattern   regular expression needle
+ * @param  mixed $string   haystack
+ * @return array    populated with each found result
+ */
+function eregAll($pattern, $string)
+{
+    do {
+        if (!mb_ereg($pattern, $string, $temp)) {
+             continue;
+        }
+        $string = str_replace($temp[0], '', $string);
+        $results[] = $temp;
+    } while (mb_ereg($pattern, $string, $temp));
+    return $results;
+}
+
+/**
+ * Prints the word "checked" if a variable is set, and optionally matches
+ * the desired value, otherwise prints nothing,
+ * used for printing the word "checked" in a checkbox form input.
+ *
+ * @param  mixed $var     the variable to compare
+ * @param  mixed $value   optional, what to compare with if a specific value is required.
+ */
+function frmChecked($var, $value=null)
+{
+    if (func_num_args() == 1 && $var) {
+        // 'Checked' if var is true.
+        echo ' checked="checked" ';
+    } else if (func_num_args() == 2 && $var == $value) {
+        // 'Checked' if var and value match.
+        echo ' checked="checked" ';
+    } else if (func_num_args() == 2 && is_array($var)) {
+        // 'Checked' if the value is in the key or the value of an array.
+        if (isset($var[$value])) {
+            echo ' checked="checked" ';
+        } else if (in_array($value, $var)) {
+            echo ' checked="checked" ';
+        }
+    }
+}
+
+/**
+ * prints the word "selected" if a variable is set, and optionally matches
+ * the desired value, otherwise prints nothing,
+ * otherwise prints nothing, used for printing the word "checked" in a
+ * select form input
+ *
+ * @param  mixed $var     the variable to compare
+ * @param  mixed $value   optional, what to compare with if a specific value is required.
+ */
+function frmSelected($var, $value=null)
+{
+    if (func_num_args() == 1 && $var) {
+        // 'selected' if var is true.
+        echo ' selected="selected" ';
+    } else if (func_num_args() == 2 && $var == $value) {
+        // 'selected' if var and value match.
+        echo ' selected="selected" ';
+    } else if (func_num_args() == 2 && is_array($var)) {
+        // 'selected' if the value is in the key or the value of an array.
+        if (isset($var[$value])) {
+            echo ' selected="selected" ';
+        } else if (in_array($value, $var)) {
+            echo ' selected="selected" ';
+        }
+    }
+}
+
+/**
+ * Adds slashes to values of an array and converts the array to a comma
+ * delimited list. If value provided is a string return the string
+ * escaped.  This is useful for putting values coming in from posted
+ * checkboxes into a SET column of a database.
+ *
+ *
+ * @param  array $in      Array to convert.
+ * @return string         Comma list of array values.
+ */
+function escapedList($in, $separator="', '")
+{
+    require_once dirname(__FILE__) . '/DB.inc.php';
+    $db =& DB::getInstance();
+
+    if (is_array($in) && !empty($in)) {
+        return join($separator, array_map(array($db, 'escapeString'), $in));
+    } else {
+        return $db->escapeString($in);
+    }
+}
+
+/**
+ * Converts a human string date into a SQL-safe date.  Dates nearing
+ * infinity use the date 2038-01-01 so conversion to unix time format
+ * remain within valid range.
+ *
+ * @param  array $date     String date to convert.
+ * @param  array $format   Date format to pass to date(). Default produces MySQL datetime: YYYY-MM-DD hh:mm:ss
+ * @return string          SQL-safe date.
+ */
+function strToSQLDate($date, $format='Y-m-d H:i:s')
+{
+    require_once dirname(__FILE__) . '/DB.inc.php';
+    $db =& DB::getInstance();
+
+    if ($db->isConnected() && mb_strpos($db->getParam('zero_date'), '-') !== false) {
+        // Mysql version >= 5.7.4 stopped allowing a "zero" date of 0000-00-00.
+        // https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_zero_date
+        $zero_date_parts = explode('-', $db->getParam('zero_date'));
+        $zero_y = $zero_date_parts[0];
+        $zero_m = $zero_date_parts[1];
+        $zero_d = $zero_date_parts[2];
+    } else {
+        $zero_y = '0000';
+        $zero_m = '00';
+        $zero_d = '00';
+    }
+    // Translate the human string date into SQL-safe date format.
+    if (empty($date) || mb_strpos($date, sprintf('%s-%s-%s', $zero_y, $zero_m, $zero_d)) !== false || strtotime($date) === -1 || strtotime($date) === false || strtotime($date) === null) {
+        // Return a string of zero time, formatted the same as $format.
+        return strtr($format, array(
+            'Y' => $zero_y,
+            'm' => $zero_m,
+            'd' => $zero_d,
+            'H' => '00',
+            'i' => '00',
+            's' => '00',
+        ));
+    } else {
+        return date($format, strtotime($date));
+    }
+}
+
+/**
+ * If magic_quotes_gpc is in use, run stripslashes() on $var. If $var is an
+ * array, stripslashes is run on each value, recursively, and the stripped
+ * array is returned.
+ *
+ * @param  mixed $var   The string or array to un-quote, if necessary.
+ * @return mixed        $var, minus any magic quotes.
+ */
+function dispelMagicQuotes($var, $always=false)
+{
+    static $magic_quotes_gpc;
+
+    if (!isset($magic_quotes_gpc)) {
+        $magic_quotes_gpc = get_magic_quotes_gpc();
+    }
+
+    if ($always || $magic_quotes_gpc) {
+        if (!is_array($var)) {
+            $var = stripslashes($var);
+        } else {
+            foreach ($var as $key=>$val) {
+                if (is_array($val)) {
+                    $var[$key] = dispelMagicQuotes($val, $always);
+                } else {
+                    $var[$key] = stripslashes($val);
+                }
+            }
+        }
+    }
+    return $var;
+}
+
+/**
+ * Get a form variable from GET or POST data, stripped of magic
+ * quotes if necessary.
+ *
+ * @param string $var (optional) The name of the form variable to look for.
+ * @param string $default (optional) The value to return if the
+ *                                   variable is not there.
+ * @return mixed      A cleaned GET or POST if no $var specified.
+ * @return string     A cleaned form $var if found, or $default.
+ */
+function getFormData($var=null, $default=null)
+{
+    $app =& App::getInstance();
+
+    if ('POST' == getenv('REQUEST_METHOD') && is_null($var)) {
+        return dispelMagicQuotes($_POST, $app->getParam('always_dispel_magicquotes'));
+    } else if ('GET' == getenv('REQUEST_METHOD') && is_null($var)) {
+        return dispelMagicQuotes($_GET, $app->getParam('always_dispel_magicquotes'));
+    }
+    if (isset($_POST[$var])) {
+        return dispelMagicQuotes($_POST[$var], $app->getParam('always_dispel_magicquotes'));
+    } else if (isset($_GET[$var])) {
+        return dispelMagicQuotes($_GET[$var], $app->getParam('always_dispel_magicquotes'));
+    } else {
+        return $default;
+    }
+}
+
+function getPost($var=null, $default=null)
+{
+    $app =& App::getInstance();
+
+    if (is_null($var)) {
+        return dispelMagicQuotes($_POST, $app->getParam('always_dispel_magicquotes'));
+    }
+    if (isset($_POST[$var])) {
+        return dispelMagicQuotes($_POST[$var], $app->getParam('always_dispel_magicquotes'));
+    } else {
+        return $default;
+    }
+}
+
+function getGet($var=null, $default=null)
+{
+    $app =& App::getInstance();
+    if (is_null($var)) {
+        return dispelMagicQuotes($_GET, $app->getParam('always_dispel_magicquotes'));
+    }
+    if (isset($_GET[$var])) {
+        return dispelMagicQuotes($_GET[$var], $app->getParam('always_dispel_magicquotes'));
+    } else {
+        return $default;
+    }
+}
+
+/*
+* Sets a $_GET or $_POST variable.
+*
+* @access   public
+* @param    string  $key    The key of the request array to set.
+* @param    mixed   $val    The value to save in the request array.
+* @return   void
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    01 Nov 2009 12:25:29
+*/
+function putFormData($key, $val)
+{
+    switch (strtoupper(getenv('REQUEST_METHOD'))) {
+    case 'POST':
+        $_POST[$key] = $val;
+        break;
+
+    case 'GET':
+        $_GET[$key] = $val;
+        break;
+    }
+}
+
+/*
+* Generates a base-65-encoded sha512 hash of $string truncated to $length.
+*
+* @access   public
+* @param    string  $string Input string to hash.
+* @param    int     $length Length of output hash string.
+* @return   string          String of hash.
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    03 Apr 2016 19:48:49
+*/
+function hash64($string, $length=18)
+{
+    return mb_substr(preg_replace('/[^\w]/', '', base64_encode(hash('sha512', $string, true))), 0, $length);
+}
+
+/**
+ * Signs a value using md5 and a simple text key. In order for this
+ * function to be useful (i.e. secure) the salt must be kept secret, which
+ * means keeping it as safe as database credentials. Putting it into an
+ * environment variable set in httpd.conf is a good place.
+ *
+ * @access  public
+ * @param   string  $val    The string to sign.
+ * @param   string  $salt   (Optional) A text key to use for computing the signature.
+ * @param   string  $length (Optional) The length of the added signature. Longer signatures are safer. Must match the length passed to verifySignature() for the signatures to match.
+ * @return  string  The original value with a signature appended.
+ */
+function addSignature($val, $salt=null, $length=18)
+{
+    $app =& App::getInstance();
+
+    if ('' == trim($val)) {
+        $app->logMsg(sprintf('Cannot add signature to an empty string.', null), LOG_INFO, __FILE__, __LINE__);
+        return '';
+    }
+
+    if (!isset($salt)) {
+        $salt = $app->getParam('signing_key');
+    }
+
+    switch ($app->getParam('signing_method')) {
+    case 'sha512+base64':
+        return $val . '-' . mb_substr(preg_replace('/[^\w]/', '', base64_encode(hash('sha512', $val . $salt, true))), 0, $length);
+
+    case 'md5':
+    default:
+        return $val . '-' . mb_strtolower(mb_substr(md5($salt . md5($val . $salt)), 0, $length));
+    }
+}
+
+/**
+ * Strips off the signature appended by addSignature().
+ *
+ * @access  public
+ * @param   string  $signed_val     The string to sign.
+ * @return  string  The original value with a signature removed.
+ */
+function removeSignature($signed_val)
+{
+    if (empty($signed_val) || mb_strpos($signed_val, '-') === false) {
+        return '';
+    }
+    return mb_substr($signed_val, 0, mb_strrpos($signed_val, '-'));
+}
+
+/**
+ * Verifies a signature appended to a value by addSignature().
+ *
+ * @access  public
+ * @param   string  $signed_val A value with appended signature.
+ * @param   string  $salt       (Optional) A text key to use for computing the signature.
+ * @param   string  $length (Optional) The length of the added signature.
+ * @return  bool    True if the signature matches the var.
+ */
+function verifySignature($signed_val, $salt=null, $length=18)
+{
+    // Strip the value from the signed value.
+    $val = removeSignature($signed_val);
+    // If the signed value matches the original signed value we consider the value safe.
+    if ('' != $signed_val && $signed_val == addSignature($val, $salt, $length)) {
+        // Signature verified.
+        return true;
+    } else {
+        $app =& App::getInstance();
+        $app->logMsg(sprintf('Failed signature (%s should be %s)', $signed_val, addSignature($val, $salt, $length)), LOG_DEBUG, __FILE__, __LINE__);
+        return false;
+    }
+}
+
+/**
+ * Sends empty output to the browser and flushes the php buffer so the client
+ * will see data before the page is finished processing.
+ */
+function flushBuffer()
+{
+    echo str_repeat('          ', 205);
+    flush();
+}
+
+/**
+ * Adds email address to mailman mailing list. Requires /etc/sudoers entry for apache to sudo execute add_members.
+ * Don't forget to allow php_admin_value open_basedir access to "/var/mailman/bin".
+ *
+ * @access  public
+ * @param   string  $email     Email address to add.
+ * @param   string  $list      Name of list to add to.
+ * @param   bool    $send_welcome_message   True to send welcome message to subscriber.
+ * @return  bool    True on success, false on failure.
+ */
+function mailmanAddMember($email, $list, $send_welcome_message=false)
+{
+    $app =& App::getInstance();
+
+    $add_members = '/usr/lib/mailman/bin/add_members';
+    if (@is_executable($add_members)) {
+        $welcome_msg = $send_welcome_message ? 'y' : 'n';
+        exec(sprintf("/bin/echo '%s' | /usr/bin/sudo %s -r - --welcome-msg=%s --admin-notify=n '%s'", escapeshellarg($email), escapeshellarg($add_members), $welcome_msg, escapeshellarg($list)), $stdout, $return_code);
+        if (0 == $return_code) {
+            $app->logMsg(sprintf('Mailman add member success for list: %s, user: %s', $list, $email), LOG_INFO, __FILE__, __LINE__);
+            return true;
+        } else {
+            $app->logMsg(sprintf('Mailman add member failed for list: %s, user: %s, with message: %s', $list, $email, getDump($stdout)), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+    } else {
+        $app->logMsg(sprintf('Mailman add member program not executable: %s', $add_members), LOG_WARNING, __FILE__, __LINE__);
+        return false;
+    }
+}
+
+/**
+ * Removes email address from mailman mailing list. Requires /etc/sudoers entry for apache to sudo execute add_members.
+ * Don't forget to allow php_admin_value open_basedir access to "/var/mailman/bin".
+ *
+ * @access  public
+ * @param   string  $email     Email address to add.
+ * @param   string  $list      Name of list to add to.
+ * @param   bool    $send_user_ack   True to send goodbye message to subscriber.
+ * @return  bool    True on success, false on failure.
+ */
+function mailmanRemoveMember($email, $list, $send_user_ack=false)
+{
+    $app =& App::getInstance();
+
+    $remove_members = '/usr/lib/mailman/bin/remove_members';
+    if (@is_executable($remove_members)) {
+        $userack = $send_user_ack ? '' : '--nouserack';
+        exec(sprintf("/usr/bin/sudo %s %s --noadminack '%s' '%s'", escapeshellarg($remove_members), $userack, escapeshellarg($list), escapeshellarg($email)), $stdout, $return_code);
+        if (0 == $return_code) {
+            $app->logMsg(sprintf('Mailman remove member success for list: %s, user: %s', $list, $email), LOG_INFO, __FILE__, __LINE__);
+            return true;
+        } else {
+            $app->logMsg(sprintf('Mailman remove member failed for list: %s, user: %s, with message: %s', $list, $email, getDump($stdout)), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+    } else {
+        // $app->logMsg(sprintf('Mailman remove member program not executable: %s', $remove_members), LOG_WARNING, __FILE__, __LINE__);
+        return false;
+    }
+}
+
+/*
+* Returns the remote IP address, taking into consideration proxy servers.
+*
+* If strict checking is enabled, we will only trust REMOTE_ADDR or an HTTP header
+* value if REMOTE_ADDR is a trusted proxy (configured as an array in $cfg['trusted_proxies']).
+*
+* @access   public
+* @param    bool $dolookup            Resolve to IP to a hostname?
+* @param    bool $trust_all_proxies   Should we trust any IP address set in HTTP_* variables? Set to FALSE for secure usage.
+* @return   mixed Canonicalized IP address (or a corresponding hostname if $dolookup is true), or false if no IP was found.
+* @author   Alix Axel <http://stackoverflow.com/a/2031935/277303>
+* @author   Corey Ballou <http://blackbe.lt/advanced-method-to-obtain-the-client-ip-in-php/>
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    12 Sep 2014 19:07:46
+*/
+function getRemoteAddr($dolookup=false, $trust_all_proxies=true)
+{
+    global $cfg;
+
+    if (!isset($_SERVER['REMOTE_ADDR'])) {
+        // In some cases this won't be set, e.g., CLI scripts.
+        return null;
+    }
+
+    // Use an HTTP header value only if $trust_all_proxies is true or when REMOTE_ADDR is in our $cfg['trusted_proxies'] array.
+    // $cfg['trusted_proxies'] is an array of proxy server addresses we expect to see in REMOTE_ADDR.
+    if ($trust_all_proxies || isset($cfg['trusted_proxies']) && is_array($cfg['trusted_proxies']) && in_array($_SERVER['REMOTE_ADDR'], $cfg['trusted_proxies'], true)) {
+        // Then it's probably safe to use an IP address value set in an HTTP header.
+        // Loop through possible IP address headers.
+        foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED') as $key) {
+            // Loop through and if
+            if (array_key_exists($key, $_SERVER)) {
+                foreach (explode(',', $_SERVER[$key]) as $addr) {
+                    // Strip non-address data to avoid "PHP Warning:  inet_pton(): Unrecognized address for=189.211.197.173 in ./Utilities.inc.php on line 1293"
+                    $addr = preg_replace('/[^=]=/', '', $addr);
+                    $addr = canonicalIPAddr(trim($addr));
+                    if (false !== filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
+                        return $dolookup && '' != $addr ? gethostbyaddr($addr) : $addr;
+                    }
+                }
+            }
+        }
+    }
+
+    $addr = canonicalIPAddr(trim($_SERVER['REMOTE_ADDR']));
+    return $dolookup && $addr ? gethostbyaddr($addr) : $addr;
+}
+
+/*
+* Converts an ipv4 IP address in hexadecimal form into canonical form (i.e., it removes the prefix).
+*
+* @access   public
+* @param    string  $addr   IP address.
+* @return   string          Canonical IP address.
+* @author   Sander Steffann <http://stackoverflow.com/a/12436099/277303>
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    15 Sep 2012
+*/
+function canonicalIPAddr($addr)
+{
+    // Known prefix
+    $v4mapped_prefix_bin = pack('H*', '00000000000000000000ffff');
+
+    // Parse
+    $addr_bin = inet_pton($addr);
+
+    // Check prefix
+    if (substr($addr_bin, 0, strlen($v4mapped_prefix_bin)) == $v4mapped_prefix_bin) {
+        // Strip prefix
+        $addr_bin = substr($addr_bin, strlen($v4mapped_prefix_bin));
+    }
+
+    // Convert back to printable address in canonical form
+    return inet_ntop($addr_bin);
+}
+
+/**
+ * Tests whether a given IP address can be found in an array of IP address networks.
+ * Elements of networks array can be single IP addresses or an IP address range in CIDR notation
+ * See: http://en.wikipedia.org/wiki/Classless_inter-domain_routing
+ *
+ * @access  public
+ * @param   string  IP address to search for.
+ * @param   array   Array of networks to search within.
+ * @return  mixed   Returns the network that matched on success, false on failure.
+ */
+function ipInRange($addr, $networks)
+{
+    if (!is_array($networks)) {
+        $networks = array($networks);
+    }
+
+    $addr_binary = sprintf('%032b', ip2long($addr));
+    foreach ($networks as $network) {
+        if (preg_match('![\d\.]{7,15}/\d{1,2}!', $network)) {
+            // IP is in CIDR notation.
+            list($cidr_ip, $cidr_bitmask) = explode('/', $network);
+            $cidr_ip_binary = sprintf('%032b', ip2long($cidr_ip));
+            if (mb_substr($addr_binary, 0, $cidr_bitmask) === mb_substr($cidr_ip_binary, 0, $cidr_bitmask)) {
+               // IP address is within the specified IP range.
+               return $network;
+            }
+        } else {
+            if ($addr === $network) {
+               // IP address exactly matches.
+               return $network;
+            }
+        }
+    }
+
+    return false;
+}
+
+/**
+ * If the given $url is on the same web site, return true. This can be used to
+ * prevent from sending sensitive info in a get query (like the SID) to another
+ * domain.
+ *
+ * @param  string $url    the URI to test.
+ * @return bool True if given $url is our domain or has no domain (is a relative url), false if it's another.
+ */
+function isMyDomain($url)
+{
+    static $urls = array();
+
+    if (!isset($urls[$url])) {
+        if (!preg_match('|https?://[\w.]+/|', $url)) {
+            // If we can't find a domain we assume the URL is local (i.e. "/my/url/path/" or "../img/file.jpg").
+            $urls[$url] = true;
+        } else {
+            $urls[$url] = preg_match('|https?://[\w.]*' . preg_quote(getenv('HTTP_HOST'), '|') . '|i', $url);
+        }
+    }
+    return $urls[$url];
+}
+
+/**
+ * Takes a URL and returns it without the query or anchor portion
+ *
+ * @param  string $url   any kind of URI
+ * @return string        the URI with ? or # and everything after removed
+ */
+function stripQuery($url)
+{
+    return preg_replace('/[?#].*$/', '', $url);
+}
+
+/**
+ * Returns a fully qualified URL to the current script, including the query.
+ *
+ * @return string    a full url to the current script
+ */
+function absoluteMe()
+{
+    return sprintf('%s://%s%s', (getenv('HTTPS') ? 'https' : 'http'), getenv('HTTP_HOST'), getenv('REQUEST_URI'));
+}
+
+/**
+ * Compares the current url with the referring url.
+ *
+ * @param  bool $exclude_query  Remove the query string first before comparing.
+ * @return bool                 True if the current URL is the same as the referring URL, false otherwise.
+ */
+function refererIsMe($exclude_query=false)
+{
+    $current_url = absoluteMe();
+    $referrer_url = getenv('HTTP_REFERER');
+
+    // If one of the hostnames is an IP address, compare only the path of both.
+    if (preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', parse_url($current_url, PHP_URL_HOST)) || preg_match('/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', parse_url($referrer_url, PHP_URL_HOST))) {
+        $current_url = preg_replace('@^https?://[^/]+@', '', $current_url);
+        $referrer_url = preg_replace('@^https?://[^/]+@', '', $referrer_url);
+    }
+
+    if ($exclude_query) {
+        return (stripQuery($current_url) == stripQuery($referrer_url));
+    } else {
+        $app =& App::getInstance();
+        $app->logMsg(sprintf('refererIsMe comparison: %s == %s', $current_url, $referrer_url), LOG_DEBUG, __FILE__, __LINE__);
+        return ($current_url == $referrer_url);
+    }
+}
+
+/*
+* Returns true if the given URL resolves to a resource with a HTTP 2xx or 3xx header response.
+* The download will abort if it retrieves >= 10KB of data to avoid downloading large files.
+* We couldn't use CURLOPT_NOBODY (a HEAD request) because some services don't behave without a GET request (ahem, BBC).
+* This function may not be very portable, if the server doesn't support CURLOPT_PROGRESSFUNCTION.
+*
+* @access   public
+* @param    string  $url    URL to a file.
+* @return   bool            True if the resource exists, false otherwise.
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  2.0
+* @since    02 May 2015 15:10:09
+*/
+function httpExists($url)
+{
+    $ch = curl_init($url);
+    curl_setopt($ch, CURLOPT_TIMEOUT, 5);
+    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
+    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
+    curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36");
+    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // Don't pass through data to the browser.
+    curl_setopt($ch, CURLOPT_BUFFERSIZE, 128); // Frequent progress function calls.
+    curl_setopt($ch, CURLOPT_NOPROGRESS, false); // Required to use CURLOPT_PROGRESSFUNCTION.
+    // Function arguments for CURLOPT_PROGRESSFUNCTION changed with php 5.5.0.
+    if (version_compare(PHP_VERSION, '5.5.0', '>=')) {
+        curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($ch, $dltot, $dlcur, $ultot, $ulcur){
+            // Return a non-zero value to abort the transfer. In which case, the transfer will set a CURLE_ABORTED_BY_CALLBACK error
+            // 10KB should be enough to catch a few 302 redirect headers and get to the actual content.
+            return ($dlcur > 10*1024) ? 1 : 0;
+        });
+    } else {
+        curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($dltot, $dlcur, $ultot, $ulcur){
+            // Return a non-zero value to abort the transfer. In which case, the transfer will set a CURLE_ABORTED_BY_CALLBACK error
+            // 10KB should be enough to catch a few 302 redirect headers and get to the actual content.
+            return ($dlcur > 10*1024) ? 1 : 0;
+        });
+    }
+    curl_exec($ch);
+    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+    return preg_match('/^[23]\d\d$/', $http_code);
+}
Index: tags/2.2.3/lib/Validator.inc.php
===================================================================
--- tags/2.2.3/lib/Validator.inc.php	(revision 632)
+++ tags/2.2.3/lib/Validator.inc.php	(revision 632)
@@ -0,0 +1,581 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Validator.inc.php
+ *
+ * The Validator class provides a methods for validating input against different criteria.
+ * All functions return true if the input passes the test.
+ *
+ * @author    Quinn Comendant <quinn@strangecode.com>
+ * @version   1.0
+ */
+
+class Validator
+{
+
+    // Known credit card types.
+    const CC_TYPE_VISA = 1;
+    const CC_TYPE_MASTERCARD = 2;
+    const CC_TYPE_AMEX = 3;
+    const CC_TYPE_DISCOVER = 4;
+    const CC_TYPE_DINERS = 5;
+    const CC_TYPE_JCB = 6;
+
+    // Validator::validateEmail() return types.
+    const EMAIL_SUCCESS = 0;
+    const EMAIL_REGEX_FAIL = 1;
+    const EMAIL_LENGTH_FAIL = 2;
+    const EMAIL_MX_FAIL = 3;
+
+    // Validator::validatePhone() return types.
+    const PHONE_SUCCESS = 0;
+    const PHONE_REGEX_FAIL = 1;
+    const PHONE_LENGTH_FAIL = 2;
+
+    /**
+    * Check if a value is not empty (the opposite of isEmpty()).
+    *
+    * @param  string $val The input data to validate.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool   true if form is not empty, false otherwise.
+    */
+    static public function notEmpty($val, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        if (is_array($val)) {
+            if (!empty($val)) {
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            if ('' != trim((string)$val)) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    /*
+    * We were using the isEmpty method *wrong* for years and should have been using notEmpty because it is more grammatically correct.
+    * Because the only use is to ensure a value is not empty, we're simply going to alias this method to notEmpty().
+    *
+    * @param  string $val   The input data to validate.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool   true if form is empty, false otherwise.
+    */
+    static public function isEmpty($val, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        return !self::notEmpty($val, $type, $file, $line);
+    }
+
+    /**
+    * Check whether input is a string.
+    *
+    * @param  string $val The input data to validate.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool   true if form is a string, false otherwise.
+    */
+    static public function isString($val, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        if ('' == trim((string)$val) || is_string($val)) {
+            return true;
+        } else {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether input is a number. Allows negative numbers.
+    *
+    * @param  string $val The input data to validate.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool   True if no errors found, false otherwise.
+    */
+    static public function isNumber($val, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        if ('' == trim((string)$val) || is_numeric($val)) {
+            return true;
+        } else {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * addError if input is NOT an integer. Don't just use is_int() because the
+    * data coming from the user is *really* a string.
+    *
+    * @param  string $val The input data to validate.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool   true if value is an integer
+    */
+    static public function isInteger($val, $negative_ok=false, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        $pattern = $negative_ok ? '/^-?[[:digit:]]+$/' : '/^[[:digit:]]+$/';
+        if ('' == trim((string)$val) || (is_numeric($val) && preg_match($pattern, $val))) {
+            return true;
+        } else {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether input is a float. Don't just use is_float() because the
+    * data coming from the user is *really* a string. Integers will also
+    * pass this test.
+    *
+    * @param  string $val The input data to validate.
+    * @param  bool $negative_ok  If the value can be unsigned.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool   true if value is a float
+    */
+    static public function isFloat($val, $negative_ok=false, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        $pattern = $negative_ok ? '/^-?[[:digit:]]*(?:\.?[[:digit:]]+)$/' : '/^[[:digit:]]*(?:\.?[[:digit:]]+)$/';
+        if ('' == trim((string)$val) || (is_numeric($val) && preg_match($pattern, $val))) {
+            return true;
+        } else {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether input is a Decimal or Fixed type. Check values to be stored in mysql decimal, numeric, num, or fixed types.
+    * Note: some integers and floats will also pass this test.
+    * https://dev.mysql.com/doc/refman/5.5/en/fixed-point-types.html
+    *
+    * @param  string $val The input data to validate.
+    * @param  bool $negative_ok  If the value can be unsigned.
+    * @param  int  $max    Total max number of digits (for mysql max is 65).
+    * @param  int  $dec    Total max number of digits after the decimal place (for mysql max is 30).
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool   true if value is a float
+    */
+    static public function isDecimal($val, $max=10, $dec=2, $negative_ok=false, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        if ('' == trim((string)$val)) {
+            return true;
+        }
+        if (!$negative_ok && is_numeric($val) && $val < 0) {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        }
+        // Get the length of the part after any decimal point, or zero.
+        $num_parts = explode('.', $val);
+        $dec_count = sizeof($num_parts) <= 1 ? 0 : mb_strlen(end($num_parts));
+        // Must be numeric, total digits <= $max, dec digits <= $dec.
+        if (is_numeric($val) && mb_strlen(str_replace(['-', '.'], '', $val)) <= $max && $dec_count <= $dec) {
+            return true;
+        } else {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether input is an array.
+    *
+    * @param  string $val The input data to validate.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool   true if value is a float
+    */
+    static public function isArray($val, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        if ((is_string($val) && '' == trim((string)$val)) || is_array($val)) {
+            return true;
+        } else {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether input matches the specified perl regular expression
+    * pattern.
+    *
+    * @param  string $val The input data to validate.
+    * @param  int    $regex            PREG that the string must match
+    * @param  bool   $valid_on_match   Set to true to be valid if match, or false to be valid if the match fails.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool   true if value passes regex test
+    */
+    static public function checkRegex($val, $regex, $valid_on_match=true, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        if ($valid_on_match ? preg_match($regex, $val) : !preg_match($regex, $val)) {
+            return true;
+        } else {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Tests if the string length is between specified values. Whitespace excluded for min.
+    *
+    * @param  string $val The input data to validate.
+    * @param  int    $min       minimum length of string, inclusive
+    * @param  int    $max       maximum length of string, inclusive
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool   true if string length is within given boundaries
+    */
+    static public function stringLength($val, $min, $max, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        if (mb_strlen((string)$val) >= $min && mb_strlen((string)$val) <= $max) {
+            return true;
+        } else {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether input is within a valid numeric range.
+    *
+    * @param  string $val The input data to validate.
+    * @param  int    $min       minimum value of number, inclusive
+    * @param  int    $max       maximum value of number, inclusive
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool   True if no errors found, false otherwise.
+    */
+    static public function numericRange($val, $min, $max, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        if ('' == trim((string)$val) || (is_numeric($val) && $val >= $min && $val <= $max)) {
+            return true;
+        } else {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Validates an email address based on the recommendations in RFC 3696.
+    * Is more loose than restrictive, to allow the many valid variants of
+    * email addresses while catching the most common mistakes.
+    * http://www.faqs.org/rfcs/rfc822.html
+    * http://www.faqs.org/rfcs/rfc2822.html
+    * http://www.faqs.org/rfcs/rfc3696.html
+    * http://www.faqs.org/rfcs/rfc1035.html
+    *
+    * @access  public
+    * @param   string   $val    The input data to validate..
+    * @param   bool     $strict Run strict tests (check if the domain exists and has an MX record assigned)
+    * @param   const    $type  A LOG_* constant (see App->logMsg())
+    * @param   string   $file  Filename to log (usually __FILE__)
+    * @param   int      $line  Line number to log (usually __LINE__)
+    * @return  const           One of the constant values: Validate::EMAIL_SUCCESS|Validate::EMAIL_REGEX_FAIL|Validate::EMAIL_LENGTH_FAIL|Validate::EMAIL_MX_FAIL
+    * @author  Quinn Comendant <quinn@strangecode.com>
+    */
+    static public function validateEmail($val, $strict=false, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        require_once 'codebase/lib/Email.inc.php';
+        $e = new Email();
+
+        // Test email address format.
+        if (!preg_match($e->getParam('regex'), $val, $e_parts)) {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return self::EMAIL_REGEX_FAIL;
+        }
+
+        // We have a match! Here are the captured subpatterns, on which further tests are run.
+        // The part before the @.
+        $local = $e_parts[2];
+
+        // The part after the @.
+        // If domain is an IP [XXX.XXX.XXX.XXX] strip off the brackets.
+        $domain = $e_parts[3]{0} == '[' ? mb_substr($e_parts[3], 1, -1) : $e_parts[3];
+
+        // Test length.
+        if (mb_strlen($local) > 64 || mb_strlen($domain) > 191) {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return self::EMAIL_LENGTH_FAIL;
+        }
+
+        if ($strict) {
+            // Strict tests.
+            if (ip2long($domain) === false && function_exists('checkdnsrr') && !checkdnsrr($domain . '.', 'MX') && gethostbyname($domain) == $domain) {
+                // Check domain exists: It's a domain if ip2long fails; checkdnsrr ensures a MX record exists; gethostbyname() ensures the domain exists.
+                $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+                return self::EMAIL_MX_FAIL;
+            }
+        }
+
+        return self::EMAIL_SUCCESS;
+    }
+
+    /**
+    * Check whether input is a valid phone number. Notice: it is now set
+    * to allow characters like - or () or + so people can type in a phone
+    * number that looks like: +1 (530) 555-1212
+    *
+    * @param  string  $form_name the name of the incoming form variable
+    *
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool    true if no errors found, false otherwise
+    */
+    static public function validatePhone($val, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        if (!self::checkRegex($val, '/^[0-9 +().-]*$/', true, $type, $file, $line)) {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return self::PHONE_REGEX_FAIL;
+        }
+        if (!self::stringLength($val, 0, 25, $type, $file, $line)) {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return self::PHONE_LENGTH_FAIL;
+        }
+        return self::PHONE_SUCCESS;
+    }
+
+    /**
+    * Verifies that date can be processed by the strtotime function.
+    * Empty strings are considered valid. Other values are tested on their return value from strtotime(). Null values will fail.
+    *
+    * @param  string  $val The input data to validate.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool    True if no errors found, false otherwise.
+    */
+    static public function validateStrDate($val, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        if (is_string($val) && '' === trim($val)) {
+            // Don't be too bothered about empty strings.
+            return true;
+        }
+
+        $timestamp = strtotime($val);
+        if (!$timestamp || $timestamp < 1) {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /*
+    * Checks if value is a "zero" SQL DATE, DATETIME, or TIMESTAMP value (or simply empty).
+    *
+    * @access   public
+    * @param    string  $val    String to check.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return   bool            True if value is an empty date.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    19 May 2015 09:57:27
+    */
+    static public function isEmptyDate($val, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+
+        if (empty($val) || '0000-00-00 00:00:00' == $val || '1000-01-01 00:00:00' == $val || '0000-00-00' == $val || '1000-01-01' == $val || '00:00:00' == $val) {
+            return true;
+        }
+        $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+        return false;
+    }
+
+    /**
+    * Verifies credit card number using the Luhn (mod 10) algorithm.
+    * http://en.wikipedia.org/wiki/Luhn_algorithm
+    *
+    * @param  string  $val   The input data to validate..
+    * @param  string  $cc_num      Card number to verify.
+    * @param  string  $cc_type     Optional, card type to do specific checks.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool    True if no errors found, false otherwise.
+    */
+    static public function validateCCNumber($val, $cc_type=null, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        // Get rid of any non-digits
+        $cc_num = preg_replace('/[^\d]/', '', $val);
+
+        // Perform card-specific checks, if applicable
+        switch ($cc_type) {
+        case self::CC_TYPE_VISA :
+            $regex = '/^4\d{15}$|^4\d{12}$/';
+            break;
+        case self::CC_TYPE_MASTERCARD :
+            $regex = '/^5[1-5]\d{14}$/';
+            break;
+        case self::CC_TYPE_AMEX :
+            $regex = '/^3[47]\d{13}$/';
+            break;
+        case self::CC_TYPE_DISCOVER :
+            $regex = '/^6011\d{12}$/';
+            break;
+        case self::CC_TYPE_DINERS :
+            $regex = '/^30[0-5]\d{11}$|^3[68]\d{12}$/';
+            break;
+        case self::CC_TYPE_JCB :
+            $regex = '/^3\d{15}$|^2131|1800\d{11}$/';
+            break;
+        default :
+            $regex = '/\d{13,}/';
+            break;
+        }
+
+        if ('' != $regex && !preg_match($regex, $cc_num)) {
+            // Invalid format.
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        }
+
+        // The Luhn formula works right to left, so reverse the number.
+        $cc_num = strrev($cc_num);
+
+        $luhn_total = 0;
+
+        $num = mb_strlen($cc_num);
+        for ($i=0; $i<$num; $i++) {
+            // Get each digit.
+            $digit = mb_substr($cc_num, $i, 1);
+
+            //  If it's an odd digit, double it.
+            if ($i / 2 != floor($i / 2)) {
+                $digit *= 2;
+            }
+
+            //  If the result is two digits, add them.
+            if (mb_strlen($digit) == 2) {
+                $digit = mb_substr($digit, 0, 1) + mb_substr($digit, 1, 1);
+            }
+
+            //  Add the current digit to the $luhn_total.
+            $luhn_total += $digit;
+        }
+
+        // If the Total is evenly divisible by 10, it's cool!
+        if ($luhn_total % 10 == 0) {
+            return true;
+        } else {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($val)), $type, $file, $line);
+            return false;
+        }
+    }
+
+    /**
+    * Check whether a file was selected for uploading. If file is missing, it's an error.
+    *
+    * @param  string $form_name The input data to validate.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return bool   True if no errors found, false otherwise.
+    */
+    static public function fileUploaded($form_name, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        if (!isset($_FILES[$form_name]['name']) || empty($_FILES[$form_name]['name'])) {
+            $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, 'no _FILES'), $type, $file, $line);
+            return false;
+        }
+
+        if (is_array($_FILES[$form_name]['name'])) {
+            foreach($_FILES[$form_name]['name'] as $f) {
+                if ('' == $f) {
+                    $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($_FILES)), $type, $file, $line);
+                    return false;
+                }
+            }
+        } else {
+            if ('' == $_FILES[$form_name]['name']) {
+                $app->logMsg(sprintf('%s (line %s) failed: %s', __METHOD__, __LINE__, getDump($_FILES)), $type, $file, $line);
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /*
+    * Check if the amount of content sent by the browser exceeds the upload_max_filesize value configured in php.ini.
+    * http://stackoverflow.com/a/24202363
+    *
+    * @access   public
+    * @param    string $form_name The input data to validate.
+    * @param  const  $type  A LOG_* constant (see App->logMsg())
+    * @param  string $file  Filename to log (usually __FILE__)
+    * @param  int    $line  Line number to log (usually __LINE__)
+    * @return   bool   True if no errors found, false otherwise.
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @version  1.0
+    * @since    20 Aug 2014 14:44:23
+    */
+    static public function fileUploadSize($form_name, $type=LOG_DEBUG, $file=null, $line=null)
+    {
+        $app =& App::getInstance();
+        $upload_max_filesize = phpIniGetBytes('upload_max_filesize');
+        if (isset($_SERVER['CONTENT_LENGTH']) && 0 != $upload_max_filesize && $_SERVER['CONTENT_LENGTH'] > $upload_max_filesize) {
+            $app->logMsg(sprintf('%s (line %s) failed: filesize %s exceeds limit of %s', __METHOD__, __LINE__, $_SERVER['CONTENT_LENGTH'], $upload_max_filesize), $type, $file, $line);
+            return false;
+        }
+        return true;
+    }
+
+} // THE END
+
Index: tags/2.2.3/lib/Version.inc.php
===================================================================
--- tags/2.2.3/lib/Version.inc.php	(revision 632)
+++ tags/2.2.3/lib/Version.inc.php	(revision 632)
@@ -0,0 +1,542 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Version.inc.php
+ *
+ * The Version class provides a system for saving, reviewing, and
+ * restoring versions of a record of any DB table. All the data in the record is
+ * serialized, compressed, and saved in a blob in the version_tbl. Restoring a
+ * version simply does a REPLACE INTO of the data. It is very simple, and works
+ * with multiple database tables, but the drawback is that relationships for
+ * a record cannot be retained. For example, an article from an article_tbl can
+ * be saved, but not categories associated to the record in a category_article_tbl.
+ * The restored article will simple retain the relationships that the previous
+ * current article had.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 2.1
+ */
+class Version
+{
+
+    // A place to keep an object instance for the singleton pattern.
+    protected static $instance = null;
+
+    // Configuration of this object.
+    protected $_params = array(
+        'max_qty' => 100, // Never have more than this many versions of each record.
+        'min_qty' => 25, // Keep at least this many versions of each record.
+        'min_days' => 7, // Keep ALL versions within this many days, even if MORE than min_qty.
+        'db_table' => 'version_tbl',
+
+        // Automatically create table and verify columns. Better set to false after site launch.
+        // This value is overwritten by the $app->getParam('db_create_tables') setting if it is available.
+        'create_table' => true,
+
+        // If true, makes an exact comparison of saved vs. live table schemas. If false, just checks that the saved columns are available.
+        'db_schema_strict' => true,
+
+        // Serialization method.
+        // Legacy installations will have been using 'phpserialize' but these should migrate to use 'json' to avoid PHP object injection https://www.owasp.org/index.php/PHP_Object_Injection
+        'serialization_method' => 'phpserialize', // Or 'json'
+    );
+
+    // Auth_SQL object from which to access a current user_id.
+    protected $_auth;
+
+    /**
+     * This method enforces the singleton pattern for this class.
+     *
+     * @return  object  Reference to the global Lock object.
+     * @access  public
+     * @static
+     */
+    public static function &getInstance($auth_object=null)
+    {
+        if (self::$instance === null) {
+            self::$instance = new self($auth_object);
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Constructor. Pass an Auth object on which to perform user lookups.
+     *
+     * @param mixed  $auth_object  An Auth_SQL object.
+     */
+    public function __construct($auth_object=null)
+    {
+        $app =& App::getInstance();
+
+        if (!is_null($auth_object) || is_null($this->_auth)) {
+            if (!method_exists($auth_object, 'get') || !method_exists($auth_object, 'getUsername')) {
+                trigger_error('Constructor not provided a valid Auth_* object.', E_USER_ERROR);
+            }
+
+            $this->_auth = $auth_object;
+        }
+
+        // Get create tables config from global context.
+        if (!is_null($app->getParam('db_create_tables'))) {
+            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
+        }
+    }
+
+    /**
+     * Setup the database table for this class.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   26 Aug 2005 17:09:36
+     */
+    public function initDB($recreate_db=false)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        static $_db_tested = false;
+
+        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
+            if ($recreate_db) {
+                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
+                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__);
+            }
+            $db->query(sprintf("CREATE TABLE IF NOT EXISTS %s (
+                version_id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
+                record_table VARCHAR(255) NOT NULL DEFAULT '',
+                record_key VARCHAR(255) NOT NULL DEFAULT '',
+                record_val VARCHAR(255) NOT NULL DEFAULT '',
+                version_data MEDIUMBLOB NOT NULL,
+                version_title VARCHAR(255) NOT NULL DEFAULT '',
+                version_number SMALLINT(11) UNSIGNED NOT NULL DEFAULT '0',
+                version_notes VARCHAR(255) NOT NULL DEFAULT '',
+                saved_by_user_id SMALLINT(11) NOT NULL DEFAULT '0',
+                version_datetime DATETIME NOT NULL DEFAULT '%s 00:00:00',
+                KEY record_table (record_table),
+                KEY record_key (record_key),
+                KEY record_val (record_val)
+            )", $db->escapeString($this->getParam('db_table')), $db->getParam('zero_date')));
+
+            if (!$db->columnExists($this->getParam('db_table'), array(
+                'version_id',
+                'record_table',
+                'record_key',
+                'record_val',
+                'version_data',
+                'version_title',
+                'version_number',
+                'version_notes',
+                'saved_by_user_id',
+                'version_datetime',
+            ), false, false)) {
+                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
+                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
+            }
+        }
+        $_db_tested = true;
+    }
+
+    /**
+     * Set the params of this object.
+     *
+     * @param  array $params   Array of param keys and values to set.
+     */
+    public function setParam($params=null)
+    {
+        $app =& App::getInstance();
+
+        if (isset($params['serialization_method']) && !in_array($params['serialization_method'], ['phpserialize', 'json'])) {
+            trigger_error(sprintf('Invalid serialization_method: %s', $params['serialization_method']), E_USER_ERROR);
+        }
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        }
+    }
+
+    /**
+     * Return the value of a parameter, if it exists.
+     *
+     * @access public
+     * @param string $param        Which parameter to return.
+     * @return mixed               Configured parameter value.
+     */
+    public function getParam($param)
+    {
+        $app =& App::getInstance();
+
+        if (array_key_exists($param, $this->_params)) {
+            return $this->_params[$param];
+        } else {
+            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
+            return null;
+        }
+    }
+
+    /**
+     * Saves a version of the current record into the version table.
+     *
+     * @param string $record_table  The table containing the record.
+     * @param string $record_key    The key column for the record.
+     * @param string $record_val    The value of the key column for the record.
+     * @param string $title         The title of this record. Only used for human presentation.
+     *
+     * @return int                  The id for the version (mysql last insert id).
+     */
+    public function create($record_table, $record_key, $record_val, $title='', $notes='')
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Get current record.
+        if (!$record = $this->getCurrent($record_table, $record_key, $record_val)) {
+            $app->logMsg(sprintf('Could not create %s version, record not found: %s, %s, %s.', $title, $record_table, $record_key, $record_val), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Get previous version_number.
+        $qid = $db->query("
+            SELECT MAX(version_number) FROM " . $db->escapeString($this->getParam('db_table')) . "
+            WHERE record_table = '" . $db->escapeString($record_table) . "'
+            AND record_key = '" . $db->escapeString($record_key) . "'
+            AND record_val = '" . $db->escapeString($record_val) . "'
+        ");
+        list($last_version_number) = mysql_fetch_row($qid);
+
+        // Clean-up old versions.
+        $this->deleteOld($record_table, $record_key, $record_val);
+
+        // Serialize the DB record.
+        switch ($this->getParam('serialization_method')) {
+        case 'phpserialize':
+            $data = gzcompress(serialize($record), 9);
+            break;
+
+        case 'json':
+            $data = gzcompress(json_encode($record), 9);
+            break;
+        }
+
+        // Save as new version.
+        // TODO: after MySQL 5.0.23 is released this query could benefit from INSERT DELAYED.
+        $db->query("
+            INSERT INTO " . $db->escapeString($this->getParam('db_table')) . " (
+                record_table,
+                record_key,
+                record_val,
+                version_data,
+                version_title,
+                version_number,
+                version_notes,
+                saved_by_user_id,
+                version_datetime
+            ) VALUES (
+                '" . $db->escapeString($record_table) . "',
+                '" . $db->escapeString($record_key) . "',
+                '" . $db->escapeString($record_val) . "',
+                '" . $db->escapeString($data) . "',
+                '" . $db->escapeString($title) . "',
+                '" . $db->escapeString($last_version_number + 1) . "',
+                '" . $db->escapeString($notes) . "',
+                '" . $db->escapeString($this->_auth->get('user_id')) . "',
+                NOW()
+            )
+        ");
+
+        return mysql_insert_id($db->getDBH());
+    }
+
+    /**
+     * Copy a version back into it's original table.
+     *
+     * @param string $version_id    The id of the version to restore.
+     *
+     * @return int                  The id for the version (mysql last insert id).
+     */
+    public function restore($version_id)
+    {
+        $app =& App::getInstance();
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Get version data.
+        $qid = $db->query("
+            SELECT *
+            FROM " . $db->escapeString($this->getParam('db_table')) . "
+            WHERE version_id = '" . $db->escapeString($version_id) . "'
+        ");
+        if (!$record = mysql_fetch_assoc($qid)) {
+            $app->raiseMsg(sprintf(_("Version %s%s not found."), $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), MSG_WARNING, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Version %s%s not found.', $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Unserialize the DB record.
+        switch ($this->getParam('serialization_method')) {
+        case 'phpserialize':
+            $data = unserialize(gzuncompress($record['version_data']));
+            break;
+
+        case 'json':
+            $data = json_decode(gzuncompress($record['version_data']), true);
+            break;
+        }
+
+        // Ensure saved db columns match current table schema.
+        if (!$db->columnExists($record['record_table'], array_keys($data), $this->getParam('db_schema_strict'))) {
+            $app->raiseMsg(sprintf(_("Version %s%s is not compatible with the current database table."), $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), MSG_ERR, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Version %s%s restoration failed, DB schema does not match for table %s.', $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')'), $record['record_table']), LOG_ALERT, __FILE__, __LINE__);
+            return false;
+        }
+
+        // SQLize the keys of the specified versioned record.
+        $replace_keys = join(",\n", array_map(array($db, 'escapeString'), array_keys($data)));
+
+        // SQLize the keys of the values of the specified versioned record. (These are more complex because we need to account for SQL null values.)
+        $replace_values = '';
+        $comma = '';
+        foreach ($data as $v) {
+            $replace_values .= is_null($v) ? "$comma\nNULL" : "$comma\n'" . $db->escapeString($v) . "'";
+            $comma = ',';
+        }
+
+        // Disable foreign_key_checks to prevent ON DELETE triggers or restrictions.
+        $db->query("SET SESSION foreign_key_checks = 0");
+        // Replace current record with specified versioned record. Consider converting this SQL to use INSERT âŠ ON DUPLICATE KEY UPDATE âŠ
+        $db->query("
+        REPLACE INTO " . $record['record_table'] . " (
+                $replace_keys
+            ) VALUES (
+                $replace_values
+            );
+        ");
+        // Re-enable foreign_key_checks.
+        $db->query("SET SESSION foreign_key_checks = 1");
+
+        return $record;
+    }
+
+    /**
+     * Version garbage collection. Deletes versions older than min_days
+     * when quantity of versions exceeds min_qty. If quantity
+     * exceeds 100 within min_days, the oldest are deleted to bring the
+     * quantity back down to min_qty.
+     *
+     * @param string $record_table  The table containing the record.
+     * @param string $record_key    The key column for the record.
+     * @param string $record_val    The value of the key column for the record.
+     *
+     * @return mixed                Array of versions, or false if none.
+     */
+    public function deleteOld($record_table, $record_key, $record_val)
+    {
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Get total number of versions for this record.
+        $qid = $db->query("
+            SELECT COUNT(*) FROM " . $db->escapeString($this->getParam('db_table')) . "
+            WHERE record_table = '" . $db->escapeString($record_table) . "'
+            AND record_key = '" . $db->escapeString($record_key) . "'
+            AND record_val = '" . $db->escapeString($record_val) . "'
+        ");
+        list($v_count) = mysql_fetch_row($qid);
+
+        if ($v_count > $this->getParam('min_qty')) {
+            if ($v_count > $this->getParam('max_qty')) {
+                // To prevent a record bomb, limit max number of versions to max_qty.
+                // First query for oldest records, selecting enough to bring total number down to min_qty.
+                $qid = $db->query("
+                    SELECT version_id
+                    FROM " . $db->escapeString($this->getParam('db_table')) . "
+                    WHERE record_table = '" . $db->escapeString($record_table) . "'
+                    AND record_key = '" . $db->escapeString($record_key) . "'
+                    AND record_val = '" . $db->escapeString($record_val) . "'
+                    ORDER BY version_datetime ASC
+                    LIMIT " . $db->escapeString($v_count - $this->getParam('min_qty')) . "
+                ");
+                $old_versions = array();
+                while (list($old_id) = mysql_fetch_row($qid)) {
+                    $old_versions[] = $old_id;
+                }
+                $db->query("
+                    DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
+                    WHERE version_id IN ('" . join("','", $old_versions) . "')
+                ");
+            } else {
+                // Delete versions older than min_days, while still keeping min_qty.
+                $qid = $db->query("
+                    SELECT version_id
+                    FROM " . $db->escapeString($this->getParam('db_table')) . "
+                    WHERE record_table = '" . $db->escapeString($record_table) . "'
+                    AND record_key = '" . $db->escapeString($record_key) . "'
+                    AND record_val = '" . $db->escapeString($record_val) . "'
+                    AND DATE_ADD(version_datetime, INTERVAL '" . $this->getParam('min_days') . "' DAY) < NOW()
+                    ORDER BY version_datetime ASC
+                    LIMIT " . ($v_count - $this->getParam('min_qty')) . "
+                ");
+                $old_versions = array();
+                while (list($old_id) = mysql_fetch_row($qid)) {
+                    $old_versions[] = $old_id;
+                }
+                if (sizeof($old_versions) > 0) {
+                    $db->query("
+                        DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
+                        WHERE version_id IN ('" . join("','", $old_versions) . "')
+                    ");
+                }
+            }
+        }
+    }
+
+    /**
+     * Get a list of versions of specified record.
+     *
+     * @param string $record_table  The table containing the record.
+     * @param string $record_key    The key column for the record.
+     * @param string $record_val    The value of the key column for the record.
+     *
+     * @return mixed                Array of versions, or false if none.
+     */
+    public function getList($record_table, $record_key, $record_val)
+    {
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Get versions of this record.
+        $qid = $db->query("
+            SELECT
+                version_id,
+                saved_by_user_id,
+                version_datetime,
+                version_title,
+                version_number,
+                version_notes
+            FROM " . $db->escapeString($this->getParam('db_table')) . "
+            WHERE record_table = '" . $db->escapeString($record_table) . "'
+            AND record_key = '" . $db->escapeString($record_key) . "'
+            AND record_val = '" . $db->escapeString($record_val) . "'
+            ORDER BY version_datetime DESC
+        ");
+        $versions = array();
+        while ($row = mysql_fetch_assoc($qid)) {
+            // Get admin usernames.
+            $row['editor'] = $this->_auth->getUsername($row['saved_by_user_id']);
+            $versions[] = $row;
+        }
+        return $versions;
+    }
+
+    /**
+     * Get the version record for a specified version id.
+     *
+     * @param string $version_id    The id of the version to restore.
+     *
+     * @return mixed                Array of data saved in version, or false if none.
+     */
+    public function getVerson($version_id)
+    {
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Get version data.
+        $qid = $db->query("
+            SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . "
+            WHERE version_id = '" . $db->escapeString($version_id) . "'
+        ");
+        return mysql_fetch_assoc($qid);
+    }
+
+    /**
+     * Get the data stored for a specified version id.
+     *
+     * @param string $version_id    The id of the version to restore.
+     *
+     * @return mixed                Array of data saved in version, or false if none.
+     */
+    public function getData($version_id)
+    {
+        $db =& DB::getInstance();
+
+        $this->initDB();
+
+        // Get version data.
+        $qid = $db->query("
+            SELECT *
+            FROM " . $db->escapeString($this->getParam('db_table')) . "
+            WHERE version_id = '" . $db->escapeString($version_id) . "'
+        ");
+        $record = mysql_fetch_assoc($qid);
+        if (isset($record['version_data'])) {
+            // Unserialize the DB record.
+            switch ($this->getParam('serialization_method')) {
+            case 'phpserialize':
+                return unserialize(gzuncompress($record['version_data']));
+
+            case 'json':
+                return json_decode(gzuncompress($record['version_data']));
+            }
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Get the current record data from the original table.
+     *
+     * @param string $version_id    The id of the version to restore.
+     *
+     * @return mixed                Array of data saved in version, or false if none.
+     */
+    public function getCurrent($record_table, $record_key, $record_val)
+    {
+        $db =& DB::getInstance();
+        $app =& App::getInstance();
+
+        $this->initDB();
+
+        if (!$record_table || !$record_key || !$record_val) {
+            $app->logMsg(sprintf('Invalid current version args: %s, %s, %s.', $record_table, $record_key, $record_val), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        $qid = $db->query("
+            SELECT * FROM " . $db->escapeString($record_table) . "
+            WHERE " . $db->escapeString($record_key) . " = '" . $db->escapeString($record_val) . "'
+        ");
+        if ($record = mysql_fetch_assoc($qid)) {
+            return $record;
+        } else {
+            return false;
+        }
+    }
+
+
+} // End of class.
Index: tags/2.2.3/services/admins.php
===================================================================
--- tags/2.2.3/services/admins.php	(revision 632)
+++ tags/2.2.3/services/admins.php	(revision 632)
@@ -0,0 +1,555 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * admins.php
+ */
+
+// require_once dirname(__FILE__) . '/_config.inc.php';
+
+$app->sslOn();
+$auth->requireLogin();
+
+require_once 'codebase/lib/PageNumbers.inc.php';
+require_once 'codebase/lib/Cache.inc.php';
+require_once 'codebase/lib/FormValidator.inc.php';
+require_once 'codebase/lib/SortOrder.inc.php';
+require_once 'codebase/lib/HTML.inc.php';
+require_once 'codebase/lib/Prefs.inc.php';
+require_once 'codebase/lib/Lock.inc.php';
+require_once 'codebase/lib/Version.inc.php';
+
+
+/********************************************************************
+* CONFIG
+********************************************************************/
+
+// Titles and navigation header.
+$nav->add(_("Administrators"), null);
+$nav->set('id', 'admins');
+
+// The object to validate form input.
+$fv = new FormValidator();
+
+// Configure the prefs object.
+$tmp_prefs = new Prefs('admins');
+$tmp_prefs->setParam(array('persistent' => false));
+
+// Configure the cache object.
+$cache =& Cache::getInstance('admins');
+$cache->setParam(array('enabled' => false)); // Better leave disabled; the list gets out of sync with the db otherwise, somehow.
+
+// Instantiate a sorting object with the default sort and order. Add SQL for each column.
+$so = new SortOrder('admin_id', 'DESC');
+$so->setColumn('admin_id', $auth->getParam('db_primary_key') . ' ASC', $auth->getParam('db_primary_key') . ' DESC');
+$so->setColumn('username', $auth->getParam('db_username_column') . ' ASC', $auth->getParam('db_username_column') . ' DESC');
+$so->setColumn('userpass', $auth->getParam('db_table') . '.userpass ASC', $auth->getParam('db_table') . '.userpass DESC');
+$so->setColumn('first_name', $auth->getParam('db_table') . '.first_name ASC', $auth->getParam('db_table') . '.first_name DESC');
+$so->setColumn('last_name', $auth->getParam('db_table') . '.last_name ASC', $auth->getParam('db_table') . '.last_name DESC');
+$so->setColumn('email', $auth->getParam('db_table') . '.email ASC', $auth->getParam('db_table') . '.email DESC');
+$so->setColumn('seconds_online', $auth->getParam('db_table') . '.seconds_online ASC', $auth->getParam('db_table') . '.seconds_online DESC');
+$so->setColumn('last_login_datetime', $auth->getParam('db_table') . '.last_login_datetime ASC', $auth->getParam('db_table') . '.last_login_datetime DESC');
+$so->setColumn('last_access_datetime', $auth->getParam('db_table') . '.last_access_datetime ASC', $auth->getParam('db_table') . '.last_access_datetime DESC');
+$so->setColumn('last_login_ip', $auth->getParam('db_table') . '.last_login_ip ASC', $auth->getParam('db_table') . '.last_login_ip DESC');
+$so->setColumn('added_by_user_id', $auth->getParam('db_table') . '.added_by_user_id ASC', $auth->getParam('db_table') . '.added_by_user_id DESC');
+$so->setColumn('modified_by_user_id', $auth->getParam('db_table') . '.modified_by_user_id ASC', $auth->getParam('db_table') . '.modified_by_user_id DESC');
+$so->setColumn('added_datetime', $auth->getParam('db_table') . '.added_datetime ASC', $auth->getParam('db_table') . '.added_datetime DESC');
+$so->setColumn('modified_datetime', $auth->getParam('db_table') . '.modified_datetime ASC', $auth->getParam('db_table') . '.modified_datetime DESC');
+
+// Instantiate page numbers. Total items are set and calculation is done in the getRecordList function.
+$page = new PageNumbers();
+$page->setPerPage(getFormData('per_page'), 50);
+$page->setPageNumber(getFormData('page_number'));
+
+// Query parameters to retain only locally.
+$locally_carried_queries = array(
+    'search_query',
+);
+
+/********************************************************************
+* MAIN
+********************************************************************/
+
+// We may want to use the add/edit interface from another script, so this
+// allows us to remember which page we came from so we can go back there.
+if (getFormData('boomerang', false) && isset($_SERVER['HTTP_REFERER'])) {
+    $app->setBoomerangURL($_SERVER['HTTP_REFERER'], 'admins');
+}
+
+if (getFormData('break_list_cache', false)) {
+    // Remove any stale cached list data.
+    $cache->delete('list');
+}
+
+// What action to take.
+switch (getFormData('op')) {
+
+case 'add' :
+    // Initialize variables for the form template.
+    $frm =& addRecordForm();
+    $nav->add(_("Add Administrator"));
+    $main_template = 'admin_form.ihtml';
+    break;
+
+case 'edit' :
+    // Initialize variables for the form template.
+    $frm =& editRecordForm(getFormData('admin_id'));
+    $nav->add(_("Edit Administrator"));
+    $main_template = 'admin_form.ihtml';
+    break;
+
+case 'del' :
+    deleteRecord(getFormData('admin_id'));
+    if ($app->validBoomerangURL('admins')) {
+        // Display boomerang page.
+        $app->dieBoomerangURL('admins', $locally_carried_queries);
+    }
+    // Display default page.
+    $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
+    break;
+
+case 'insert' :
+    if (getFormdata('cancel', false)) {
+        if ($app->validBoomerangURL('admins')) {
+            // Display boomerang page.
+            $app->dieBoomerangURL('admins', $locally_carried_queries);
+        }
+        // Display default page.
+        $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
+    }
+    validateInput();
+    if ($fv->anyErrors()) {
+        $frm =& addRecordForm();
+        $frm = array_merge($frm, getFormData());
+        $nav->add(_("Add Administrator"));
+        $main_template = 'admin_form.ihtml';
+    } else {
+        $admin_id = insertRecord(getFormData());
+        if (getFormdata('repeat', false)) {
+            // Display function again.
+            $app->dieURL($_SERVER['PHP_SELF'] . '?op=add', $locally_carried_queries);
+        } else if ($app->validBoomerangURL('admins')) {
+            // Display boomerang page.
+            $app->dieBoomerangURL('admins', $locally_carried_queries);
+        }
+        // Display default page.
+        $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
+    }
+    break;
+
+case 'update' :
+    if (getFormdata('reset', false)) {
+        $app->raiseMsg(_("Saved values have been reloaded."), MSG_NOTICE, __FILE__, __LINE__);
+        $app->dieURL($_SERVER['PHP_SELF'] . '?op=edit&admin_id=' . getFormData('admin_id'), $locally_carried_queries);
+    }
+    if (getFormdata('cancel', false)) {
+        // Remove lock
+        $lock->select($auth->getParam('db_table'), $auth->getParam('db_primary_key'), getFormData('admin_id'));
+        $lock->remove();
+        if ($app->validBoomerangURL('admins')) {
+            // Display boomerang page.
+            $app->dieBoomerangURL('admins', $locally_carried_queries);
+        }
+        // Display default page.
+        $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
+    }
+    validateInput();
+    if ($fv->anyErrors()) {
+        $frm =& editRecordForm(getFormData('admin_id'));
+        $frm = array_merge($frm, getFormData());
+        $nav->add(_("Edit Administrator"));
+        $main_template = 'admin_form.ihtml';
+    } else {
+        updateRecord(getFormData());
+        if (getFormdata('repeat', false)) {
+            // Display edit function with next available ID.
+            $qid = $db->query("SELECT " . $auth->getParam('db_primary_key') . " FROM " . $auth->getParam('db_table') . " WHERE " . $auth->getParam('db_primary_key') . " > '" . $db->escapeString(getFormData('admin_id')) . "' ORDER BY " . $auth->getParam('db_primary_key') . " ASC LIMIT 1");
+            if (list($next_id) = mysql_fetch_row($qid)) {
+                $app->dieURL($_SERVER['PHP_SELF'] . '?op=edit&admin_id=' . $next_id, $locally_carried_queries);
+            } else {
+                $app->raiseMsg(_("Cannot edit next, the end of the list was reached"), MSG_NOTICE, __FILE__, __LINE__);
+            }
+        } else if ($app->validBoomerangURL('admins')) {
+            // Display boomerang page.
+            $app->dieBoomerangURL('admins', $locally_carried_queries);
+        }
+        // Display default page.
+        $app->dieURL($_SERVER['PHP_SELF'], $locally_carried_queries);
+    }
+    break;
+
+default :
+    $list =& getRecordList();
+    $main_template = 'admin_list.ihtml';
+    break;
+}
+
+/******************************************************************************
+ * TEMPLATE INITIALIZATION
+ *****************************************************************************/
+
+include 'header.ihtml';
+$app->carryQuery($locally_carried_queries);
+include 'codebase/services/templates/' . $main_template;
+include 'footer.ihtml';
+
+/********************************************************************
+* FUNCTIONS
+********************************************************************/
+
+
+function validateInput()
+{
+    global $fv, $auth;
+
+    // If the username was changed during edit, verify.
+    if (((getFormData('username') != getFormData('old_username')) && 'update' == getFormData('op'))
+    || 'insert' == getFormData('op')) {
+        if ($auth->usernameExists(getFormData('username'))) {
+            $fv->addError('username', sprintf(_("The username %s already exists. Please choose another."), getFormData('username')));
+        }
+    }
+
+    $fv->numericRange('admin_id', 0, 32767, _("<strong>Admin id</strong> must be a valid number between 0 and 32767."));
+
+    $fv->isEmpty('username', _("<strong>Username</strong> cannot be blank."));
+    $fv->stringLength('username', 0, 255, _("<strong>Username</strong> must contain less than 256 characters."));
+
+    $fv->isEmpty('userpass', _("<strong>Passwords</strong> cannot be blank."));
+    $fv->stringLength('userpass', 8, 36, _("<strong>Passwords</strong> must be between 8 and 36 characters long."));
+
+    $fv->stringLength('first_name', 0, 255, _("<strong>First name</strong> must contain less than 256 characters."));
+
+    $fv->stringLength('last_name', 0, 255, _("<strong>Last name</strong> must contain less than 256 characters."));
+
+    $fv->isEmpty('email', _("<strong>Email</strong> cannot be blank."));
+    $fv->validateEmail('email');
+}
+
+function &addRecordForm()
+{
+    // Set default values for the reset of the fields.
+    $frm = array(
+        'admin_id' => '',
+        'old_username' => '',
+        'username' => '',
+        'userpass' => '',
+        'first_name' => '',
+        'last_name' => '',
+        'email' => '',
+        'seconds_online' => '0',
+        'last_login_datetime' => '',
+        'last_access_datetime' => '',
+        'last_login_ip' => '0.0.0.0',
+        'added_by_user_id' => '',
+        'modified_by_user_id' => '',
+        'added_datetime' => '',
+        'modified_datetime' => '',
+        'new_op' => 'insert',
+        'submit_buttons' => array(
+            array('name' => 'submit', 'value' => _("Add Administrator"), 'class' => 'small button', 'accesskey' => 's'),
+            array('name' => 'repeat', 'value' => _("Add &amp; repeat"), 'class' => 'small button secondary', 'accesskey' => 'r'),
+            array('name' => 'cancel', 'value' => _("Cancel"), 'class' => 'small button secondary', 'accesskey' => 'c'),
+        ),
+    );
+
+    return $frm;
+}
+
+function &editRecordForm($id)
+{
+    global $auth;
+    global $lock;
+    global $locally_carried_queries;
+    $app =& App::getInstance();
+    $db =& DB::getInstance();
+
+    $lock->select($auth->getParam('db_table'), $auth->getParam('db_primary_key'), $id);
+    if ($lock->isLocked() && !$lock->isMine()) {
+        $lock->dieErrorPage();
+    }
+
+    // Get the information for the form.
+    $qid = $db->query("
+        SELECT *,
+        " . $auth->getParam('db_primary_key') . " AS admin_id
+        FROM " . $auth->getParam('db_table') . "
+        WHERE " . $auth->getParam('db_primary_key') . " = '" . $db->escapeString($id) . "'
+    ");
+    if (!$frm = mysql_fetch_assoc($qid)) {
+        $app->logMsg('Could not find record with admin_id: ' . $id, LOG_WARNING, __FILE__, __LINE__);
+        $app->raiseMsg(sprintf(_("The requested record %s could not be found."), $id), MSG_ERR, __FILE__, __LINE__);
+        $app->dieBoomerangURL('admins', $locally_carried_queries);
+    }
+
+    // Lock this record.
+    $lock->set($auth->getParam('db_table'), $auth->getParam('db_primary_key'), $id, $frm['username']);
+
+    // Set misc values for the form.
+    $frm = array_merge(array(
+        'admin_id' => '',
+        'old_username' => $frm['username'],
+        'username' => '',
+//         'userpass' => '****************',
+        'first_name' => '',
+        'last_name' => '',
+        'email' => '',
+        'seconds_online' => '0',
+        'last_login_datetime' => '',
+        'last_access_datetime' => '',
+        'last_login_ip' => '0.0.0.0',
+        'added_by_user_id' => '',
+        'modified_by_user_id' => '',
+        'added_datetime' => '',
+        'modified_datetime' => '',
+        'new_op' => 'update',
+        'old_username' => $frm['username'],
+        'submit_buttons' => array(
+            array('name' => 'submit', 'value' => _("Save changes"), 'class' => 'small button', 'accesskey' => 's'),
+            array('name' => 'repeat', 'value' => _("Save & edit next"), 'class' => 'small button secondary', 'accesskey' => 'e'),
+            array('name' => 'reset', 'value' => _("Reset"), 'class' => 'small button secondary', 'accesskey' => 'r'),
+            array('name' => 'cancel', 'value' => _("Cancel"), 'class' => 'small button secondary', 'accesskey' => 'c'),
+        ),
+    ), $frm, array('userpass' => 'â¢â¢â¢â¢â¢â¢â¢â¢â¢â¢â¢â¢â¢â¢â¢'));
+
+    return $frm;
+}
+
+function deleteRecord($id)
+{
+    global $auth;
+    global $lock;
+    global $cache;
+    global $locally_carried_queries;
+    $app =& App::getInstance();
+    $db =& DB::getInstance();
+
+    $lock->select($auth->getParam('db_table'), $auth->getParam('db_primary_key'), $id);
+    if ($lock->isLocked() && !$lock->isMine()) {
+        $lock->dieErrorPage();
+    }
+
+    // Remove any stale cached list data.
+    $cache->delete('list');
+
+    // Get the information for this object.
+    $qid = $db->query("
+        SELECT " . $auth->getParam('db_username_column') . "
+        FROM " . $auth->getParam('db_table') . "
+        WHERE " . $auth->getParam('db_primary_key') . " = '" . $db->escapeString($id) . "'
+    ");
+    if (! list($name) = mysql_fetch_row($qid)) {
+        $app->logMsg('Could not find record with admin_id: ' . $id, LOG_WARNING, __FILE__, __LINE__);
+        $app->raiseMsg(sprintf(_("The requested record %s could not be found."), $id), MSG_ERR, __FILE__, __LINE__);
+        $app->dieBoomerangURL('admins', $locally_carried_queries);
+    }
+
+    // Get the information for this object.
+    $qid = $db->query("SELECT COUNT(*) from " . $auth->getParam('db_table') . "");
+    list($num_admins) = mysql_fetch_row($qid);
+    if ($num_admins <= 1) {
+        // There must always be at least one admnistrator!
+        $app->raiseMsg(_("You cannot delete the only user in the database. There must be at least one to log in and create other users."), MSG_NOTICE, __FILE__, __LINE__);
+    } else if ($auth->get('user_id') == $id) {
+        // Do not delete yourself!
+        $app->raiseMsg(_("You cannot delete yourself."), MSG_NOTICE, __FILE__, __LINE__);
+    } else {
+        // Delete the record.
+        $db->query("DELETE FROM " . $auth->getParam('db_table') . " WHERE " . $auth->getParam('db_primary_key') . " = '" . $db->escapeString($id) . "'");
+        $app->raiseMsg(sprintf(_("The admin <em>%s</em> has been deleted."), $name), MSG_SUCCESS, __FILE__, __LINE__);
+    }
+
+    // Unlock record.
+    $lock->remove();
+}
+
+function insertRecord($frm)
+{
+    global $auth;
+    global $cache;
+    $app =& App::getInstance();
+    $db =& DB::getInstance();
+
+    // Remove any stale cached list data.
+    $cache->delete('list');
+
+    // Insert record data.
+    $db->query("
+        INSERT INTO " . $auth->getParam('db_table') . " (
+            " . $auth->getParam('db_username_column') . ",
+            first_name,
+            last_name,
+            email,
+            added_by_user_id,
+            added_datetime
+        ) VALUES (
+            '" . $db->escapeString($frm['username']) . "',
+            '" . $db->escapeString($frm['first_name']) . "',
+            '" . $db->escapeString($frm['last_name']) . "',
+            '" . $db->escapeString($frm['email']) . "',
+            '" . $db->escapeString($auth->get('user_id')) . "',
+            NOW()
+        )
+    ");
+    $last_insert_id = mysql_insert_id($db->getDBH());
+
+    // Set admin password.
+    $auth->setPassword($last_insert_id, $frm['userpass']);
+
+    // Create version.
+    $version = Version::getInstance($auth);
+    $version->create($auth->getParam('db_table'), $auth->getParam('db_primary_key'), $last_insert_id, $frm['username']);
+
+    $app->raiseMsg(sprintf(_("The user <em>%s</em> has been added."), $frm['username']), MSG_SUCCESS, __FILE__, __LINE__);
+
+    return $last_insert_id;
+}
+
+function updateRecord($frm)
+{
+    global $auth;
+    global $lock;
+    global $cache;
+    $app =& App::getInstance();
+    $db =& DB::getInstance();
+
+    $lock->select($auth->getParam('db_table'), $auth->getParam('db_primary_key'), $frm['admin_id']);
+    if ($lock->isLocked() && !$lock->isMine()) {
+        $lock->dieErrorPage();
+    }
+
+    // Remove any stale cached list data.
+    $cache->delete('list');
+
+    // If the userpass is left blank or with the filler **** characters, we don't want to update it.
+    if (!empty($frm['userpass']) && !preg_match('/[\*]{4,}/', $frm['userpass'])) {
+        // Set user password.
+        $auth->setPassword($frm['admin_id'], $frm['userpass']);
+    }
+
+    // Update record data.
+    $db->query("
+        UPDATE " . $auth->getParam('db_table') . " SET
+            " . $auth->getParam('db_username_column') . " = '" . $db->escapeString($frm['username']) . "',
+            first_name = '" . $db->escapeString($frm['first_name']) . "',
+            last_name = '" . $db->escapeString($frm['last_name']) . "',
+            email = '" . $db->escapeString($frm['email']) . "',
+            modified_by_user_id = '" . $db->escapeString($auth->get('user_id')) . "',
+            modified_datetime = NOW()
+        WHERE " . $auth->getParam('db_primary_key') . " = '" . $db->escapeString($frm['admin_id']) . "'
+    ");
+
+    // Create version.
+    $version = Version::getInstance($auth);
+    $version->create($auth->getParam('db_table'), $auth->getParam('db_primary_key'), $frm['admin_id'], $frm['username']);
+
+    $app->raiseMsg(sprintf(_("The user <em>%s</em> has been updated."), $frm['username']), MSG_SUCCESS, __FILE__, __LINE__);
+
+    // Unlock record.
+    $lock->remove();
+}
+
+function &getRecordList()
+{
+    global $page;
+    global $so;
+    global $tmp_prefs;
+    global $cache;
+    global $auth;
+    $db =& DB::getInstance();
+
+    $where_clause = '';
+
+    // Build search query if available.
+    if (getFormData('search_query', false)) {
+        $qry_words = preg_split('/[^\w]/', getFormData('search_query'));
+        for ($i=0; $i<sizeof($qry_words); $i++) {
+            $where_clause .= (empty($where_clause) ? 'WHERE' : 'AND') . "
+                (
+                    " . $auth->getParam('db_table') . "." . $auth->getParam('db_username_column') . " LIKE '%" . $db->escapeString($qry_words[$i]) . "%'
+                    OR " . $auth->getParam('db_table') . ".first_name LIKE '%" . $db->escapeString($qry_words[$i]) . "%'
+                    OR " . $auth->getParam('db_table') . ".last_name LIKE '%" . $db->escapeString($qry_words[$i]) . "%'
+                    OR " . $auth->getParam('db_table') . ".email LIKE '%" . $db->escapeString($qry_words[$i]) . "%'
+                )
+            ";
+        }
+    }
+
+    // Count the total number of records so we can do something about the page numbers.
+    $qid = $db->query("
+        SELECT COUNT(*)
+        FROM " . $auth->getParam('db_table') . "
+        $where_clause
+    ");
+    list($num_results) = mysql_fetch_row($qid);
+
+    // Set page numbers now we know (needed for next step).
+    $page->setTotalItems($num_results);
+    $page->calculate();
+
+    // Final SQL, with sort and page limiters.
+    $sql = "
+        SELECT
+            " . $auth->getParam('db_table') . ".*,
+            " . $auth->getParam('db_table') . "." . $auth->getParam('db_primary_key') . " AS admin_id,
+            a1." . $auth->getParam('db_username_column') . " AS added_admin_username,
+            a2." . $auth->getParam('db_username_column') . " AS modified_admin_username
+        FROM " . $auth->getParam('db_table') . "
+        LEFT JOIN " . $auth->getParam('db_table') . " a1 ON (" . $auth->getParam('db_table') . ".added_by_user_id = a1." . $auth->getParam('db_primary_key') . ")
+        LEFT JOIN " . $auth->getParam('db_table') . " a2 ON (" . $auth->getParam('db_table') . ".modified_by_user_id = a2." . $auth->getParam('db_primary_key') . ")
+        $where_clause
+        " . $so->getSortOrderSQL() . "
+        " . $page->getLimitSQL() . "
+    ";
+
+    // Use a cash hash to determine if the result-set has changed.
+    // A unique key for this query, with the total_items in case db records
+    // were added since the last cache. This identifies a unique set of
+    // cached data, but we must refer to the list that is cached by a more
+    // generic name. so that we can flush the cache (if records updated)
+    // without knowing the hash.
+    $cache_hash = md5($sql . '|' . $page->total_items);
+    if ($tmp_prefs->get('cache_hash') != $cache_hash) {
+        $cache->delete('list');
+        $tmp_prefs->set('cache_hash', $cache_hash);
+    }
+
+    // First try to return from the cache.
+    if ($cache->exists('list')) {
+        $list = $cache->get('list');
+        return $list;
+    }
+
+    // The list was not cached, so issue the real query.
+    $qid = $db->query($sql);
+    while ($row = mysql_fetch_assoc($qid)) {
+        $list[] = $row;
+    }
+
+    // Save this list into the cache.
+    if (isset($list) && !empty($list)) {
+        $cache->set('list', $list);
+    }
+
+    return $list;
+}
+
Index: tags/2.2.3/services/css.php
===================================================================
--- tags/2.2.3/services/css.php	(revision 632)
+++ tags/2.2.3/services/css.php	(revision 632)
@@ -0,0 +1,34 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * css.php
+ *
+ * This file works with the CSS.inc.php class to print a "normal" css file and
+ * is to be included as any css file would be. It should not be necessary to
+ * ever modify this file.
+ */
+if (is_a($css, 'CSS')) {
+    $css->headers(getFormData('app'));
+    $css->output(getFormData('app'));
+}
+
Index: tags/2.2.3/services/js.php
===================================================================
--- tags/2.2.3/services/js.php	(revision 632)
+++ tags/2.2.3/services/js.php	(revision 632)
@@ -0,0 +1,34 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * js.php
+ *
+ * This file works with the JS class to print a minified JS file, to be
+ * included as any JS file would be. It should not be necessary to
+ * ever modify this file.
+ */
+if (is_a($js, 'JS')) {
+    $js->headers(getFormData('app'));
+    $js->output(getFormData('app'));
+}
+
Index: tags/2.2.3/services/lock.php
===================================================================
--- tags/2.2.3/services/lock.php	(revision 632)
+++ tags/2.2.3/services/lock.php	(revision 632)
@@ -0,0 +1,78 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * lock.php
+ */
+
+// Redefine include_path including the codebase/services but allow local templates override global ones.
+ini_set('include_path', join(PATH_SEPARATOR, array(
+    get_include_path(),
+    dirname(__FILE__) . '/templates'
+)));
+
+require_once 'codebase/lib/Lock.inc.php';
+require_once 'codebase/lib/HTML.inc.php';
+
+$app->sslOn();
+$auth->requireLogin();
+
+if (getFormData('boomerang', false)) {
+    // We remember which page we came from so we can go back there.
+    $boom_url = mb_strpos(getFormData('boomerang'), '/') !== false ? getFormData('boomerang') : $_SERVER['HTTP_REFERER'];
+    $app->setBoomerangURL($boom_url, 'lock');
+    if (isset($_SERVER['HTTP_REFERER'])) {
+        $app->setBoomerangURL($_SERVER['HTTP_REFERER'], 'cancel-lock');
+    }
+}
+
+if (!isset($lock) || !is_a($lock, 'Lock')) {
+    $lock =& Lock::getInstance($auth);
+}
+$lock->select(getFormData('lock_id'));
+
+if (!$lock->isLocked()) {
+    $app->logMsg('Lock not found with lock_id: ' . getFormData('lock_id') . ', from referrer: ' . $_SERVER['HTTP_REFERER'], LOG_WARNING, __FILE__, __LINE__);
+    $app->dieBoomerangURL('lock');
+}
+
+// What action to take.
+if (getFormData('unlock', false)) {
+    $lock->remove();
+    $app->raiseMsg(sprintf(_("The record <em>%s</em> has been unlocked, and can now be modified."), $lock->getTitle('title')), MSG_NOTICE, __FILE__, __LINE__);
+    $app->dieBoomerangURL('lock');
+} else if (getFormData('cancel', false)) {
+    // Since the boomerang URL will go back to the locked record, and the record was not unlocked, we must delete the boomerang URL otherwise we'll come back here.
+    $app->deleteBoomerangURL('lock');
+    $app->dieBoomerangURL('cancel-lock');
+}
+
+// Titles and navigation header.
+$nav->add(sprintf(_("Locked record: <em>%s</em>"), $lock->getTitle()));
+$nav->set('id', 'lock');
+
+// Templates.
+include 'header.ihtml';
+// $lock->printErrorHTML();
+include 'lock.ihtml';
+include 'footer.ihtml';
+
Index: tags/2.2.3/services/login.php
===================================================================
--- tags/2.2.3/services/login.php	(revision 632)
+++ tags/2.2.3/services/login.php	(revision 632)
@@ -0,0 +1,75 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * login.php
+ */
+
+// Redefine include_path including the codebase/services but allow local templates override global ones.
+ini_set('include_path', join(PATH_SEPARATOR, array(
+    get_include_path(),
+    dirname(__FILE__) . '/templates'
+)));
+
+// We may want to use the add/edit interface from another script, so this
+// allows us to remember which page we came from so we can go back there.
+if (getFormData('boomerang', false) && isset($_SERVER['HTTP_REFERER'])) {
+    $app->setBoomerangURL($_SERVER['HTTP_REFERER'], 'login');
+}
+$app->sslOn();
+
+// Cookie-based storage preferences.
+require_once 'codebase/lib/Prefs.inc.php';
+$login_prefs = new Prefs('login');
+$login_prefs->setParam(array('storagetype' => 'cookie'));
+
+$frm['username'] = getFormdata('username', $login_prefs->get('username'));
+$frm['password'] = getFormdata('password');
+$frm['remember_me'] = ('' != $login_prefs->get('username')) ? '1' : '';
+
+if (getFormdata('username', false)) {
+    // Form has been submitted, check if the user login information is correct.
+
+    if ($auth->login($frm['username'], $frm['password'])) {
+        if (getFormData('remember_me')) {
+            $login_prefs->set('username', getFormData('username'));
+        } else {
+            $login_prefs->set('username', '');
+        }
+        $app->raiseMsg(_("You are now logged in."), MSG_SUCCESS, __FILE__, __LINE__);
+        $app->logMsg(sprintf('User %s successfully logged-in.', $frm['username']), LOG_INFO, __FILE__, __LINE__);
+        $app->dieBoomerangURL('login');
+    } else {
+        $app->raiseMsg(_("Login failed, please try again."), MSG_NOTICE, __FILE__, __LINE__);
+        $app->logMsg(sprintf('User %s failed login (encrypted password: %s)', $frm['username'], $auth->encryptPassword($frm['password'])), LOG_NOTICE, __FILE__, __LINE__);
+    }
+}
+
+// Titles and navigation header.
+$nav->add(_("Login"));
+$nav->set('id', 'login');
+
+// Templates.
+include 'header.ihtml';
+include 'login_form.ihtml';
+include 'footer.ihtml';
+
Index: tags/2.2.3/services/logout.php
===================================================================
--- tags/2.2.3/services/logout.php	(revision 632)
+++ tags/2.2.3/services/logout.php	(revision 632)
@@ -0,0 +1,53 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * logout.php
+ */
+
+if (isset($prefs) && method_exists($prefs, 'clear')) {
+    $prefs->clear();
+}
+if (isset($tmp_prefs) && method_exists($tmp_prefs, 'clear')) {
+    $tmp_prefs->clear();
+}
+if (isset($cache) && method_exists($cache, 'clear')) {
+    $cache->clear();
+}
+if (isset($cart) && method_exists($cart, 'clear')) {
+    $cart->clear();
+}
+if (isset($auth) && method_exists($auth, 'get')) {
+    if (true === $app->getParam('enable_db')) {
+        // Delete the current user's record locks.
+        require_once 'codebase/lib/Lock.inc.php';
+        $lock =& Lock::getInstance($auth);
+        $lock->removeAll($auth->get('user_id'));
+    }
+}
+if (isset($auth) && method_exists($auth, 'clear')) {
+    $auth->clear();
+}
+
+$app->raiseMsg(sprintf(_("Your session has been logged-out."), null), MSG_SUCCESS, __FILE__, __LINE__);
+$app->dieURL($app->getParam('redirect_home_url'));
+
Index: tags/2.2.3/services/logs.php
===================================================================
--- tags/2.2.3/services/logs.php	(revision 632)
+++ tags/2.2.3/services/logs.php	(revision 632)
@@ -0,0 +1,299 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * logs.php
+ */
+
+// Redefine include_path including the codebase/services but allow local templates override global ones.
+ini_set('include_path', join(PATH_SEPARATOR, array(
+    get_include_path(),
+    dirname(__FILE__) . '/templates'
+)));
+
+$app->sslOn();
+$auth->requireLogin();
+
+require_once 'codebase/lib/PageNumbers.inc.php';
+require_once 'codebase/lib/SortOrder.inc.php';
+require_once 'codebase/lib/TemplateGlue.inc.php';
+require_once 'codebase/lib/Prefs.inc.php';
+require_once 'codebase/lib/Upload.inc.php';
+
+
+/******************************************************************************
+ * CODE CONFIG
+ *****************************************************************************/
+
+// Files with these extensions will be displayed at the top of the log list.
+$valid_file_extensions = array('', 'txt', 'log');
+
+// Files that shouldn't be deleted (preg search expressions).
+$no_delete_files = '/^php_error_log$|^access_log$|^error_log$|^ssl_request_log$/';
+
+// Files that shouldn't be cleared (preg search expressions).
+$no_clear_files = '/__|^access_log$|^error_log$|^ssl_request_log$/';
+
+// Files that shouldn't be archived (preg search expressions).
+$no_archive_files = '/__|^access_log$|^error_log$|^ssl_request_log$/';
+
+// Files that shouldn't be downloaded (preg search expressions).
+$no_download_files = '/^$/';
+
+// Configure the prefs object.
+$tmp_prefs = new Prefs('admin_logs');
+$tmp_prefs->setParam(array('persistent' => false));
+$tmp_prefs->setDefaults(array(
+    'log_file' => $app->getParam('log_filename')
+));
+if (getFormData('log', false)) {
+    $tmp_prefs->set('log_file', getFormData('log'));
+}
+
+// Titles and navigation header.
+$nav->add(sprintf(_("Viewing <em>%s</em>"), $tmp_prefs->get('log_file')), '/admin/logs.php');
+$nav->set('id', 'logs');
+
+/********************************************************************
+* MAIN
+********************************************************************/
+
+// Allow realtime file stats.
+clearstatcache();
+
+// What action to take.
+switch (getFormData('op')) {
+case 'delete' :
+    deleteLog($tmp_prefs->get('log_file'));
+    $tmp_prefs->set('log_file', $app->getParam('log_filename'));
+    if ($app->validBoomerangURL('app_log')) {
+        // Display boomerang page.
+        $app->dieBoomerangURL('app_log');
+    }
+    // Display default page.
+    $app->dieURL($_SERVER['PHP_SELF']);
+    break;
+
+case 'clear' :
+    clearLog($tmp_prefs->get('log_file'));
+    if ($app->validBoomerangURL('app_log')) {
+        // Display boomerang page.
+        $app->dieBoomerangURL('app_log');
+    }
+    // Display default page.
+    $app->dieURL($_SERVER['PHP_SELF']);
+    break;
+
+case 'archive' :
+    if (archiveLog($tmp_prefs->get('log_file'))) {
+        // Now flush current log.
+        $app->dieURL($_SERVER['PHP_SELF'] . '?op=clear');
+    }
+    if ($app->validBoomerangURL('app_log')) {
+        // Display boomerang page.
+        $app->dieBoomerangURL('app_log');
+    }
+    // Display default page.
+    $app->dieURL($_SERVER['PHP_SELF']);
+    break;
+
+// case 'output' :
+//     $main_template = 'output';
+//     break;
+
+case 'download' :
+    header('Content-Type: application/octet-stream');
+    header(sprintf('Content-Disposition: attachment; filename=%s.txt', $tmp_prefs->get('log_file')));
+    printLog($tmp_prefs->get('log_file'));
+    die;
+    break;
+
+default :
+    $list =& getLog($tmp_prefs->get('log_file'), getFormData('search_query'));
+    $main_template = 'log_list.ihtml';
+    break;
+}
+
+
+/******************************************************************************
+ * TEMPLATE INITIALIZATION
+ *****************************************************************************/
+
+$logs = &getLogList();
+
+// Instantiate page numbers. Total items are set and calculation is done in the getRecordList function.
+$page = new PageNumbers();
+$page->setPerPage(getFormData('per_page'), 500);
+$page->setPageNumber(getFormData('page_number'));
+$page->setTotalItems(sizeof($list));
+$page->per_page_options = array(100, 250, 500, 600, 800, 1000, 2000, 5000, 10000);
+$page->calculate();
+
+include 'header.ihtml';
+if ('output' == $main_template) {
+    printLog($tmp_prefs->get('log_file'));
+} else {
+    include $main_template;
+}
+include 'footer.ihtml';
+
+
+/********************************************************************
+* FUNCTIONS
+********************************************************************/
+
+function deleteLog($log_file)
+{
+    $app =& App::getInstance();
+
+    if (!file_exists($app->getParam('log_directory') . '/' . $log_file)) {
+        $app->raiseMsg(sprintf(_("Log file %s does not exist."), $log_file), MSG_NOTICE, __FILE__, __LINE__);
+        $app->logMsg(sprintf('Cannot delete nonexistent log file %s', $app->getParam('log_directory') . '/' . $log_file), LOG_INFO, __FILE__, __LINE__);
+        return false;
+    }
+
+    if (!is_writable($app->getParam('log_directory') . '/' . $log_file) && !is_writable($app->getParam('log_directory'))) {
+        $app->raiseMsg(sprintf(_("Log file %s could not be deleted."), $log_file), MSG_NOTICE, __FILE__, __LINE__);
+        $app->logMsg(sprintf('Cannot delete log file %s, not writable.', $app->getParam('log_directory') . '/' . $log_file), LOG_INFO, __FILE__, __LINE__);
+        return false;
+    }
+
+    if (unlink($app->getParam('log_directory') . '/' . $log_file)) {
+        $app->raiseMsg(sprintf(_("Log file %s has been deleted."), $log_file), MSG_NOTICE, __FILE__, __LINE__);
+        $app->logMsg(sprintf('Log file %s has been deleted', $log_file), LOG_INFO, __FILE__, __LINE__);
+        return true;
+    } else {
+        $app->raiseMsg(sprintf(_("Log file %s could not be deleted."), $log_file), MSG_WARNING, __FILE__, __LINE__);
+        $app->logMsg(sprintf('unlink failed on log file %s', $app->getParam('log_directory') . '/' . $log_file), LOG_WARNING, __FILE__, __LINE__);
+        return false;
+    }
+}
+
+function clearLog($log_file)
+{
+    $app =& App::getInstance();
+
+    if (!$fp = fopen($app->getParam('log_directory') . '/' . $log_file, 'r+')) {
+        $app->raiseMsg(sprintf(_("Log file %s could not be opened."), $log_file), MSG_NOTICE, __FILE__, __LINE__);
+        $app->logMsg(sprintf('fopen failed on log file %s', $app->getParam('log_directory') . '/' . $log_file), LOG_INFO, __FILE__, __LINE__);
+        return false;
+    }
+
+    flock($fp, LOCK_EX);
+    $ftruncate_return = ftruncate($fp, 0);
+    flock($fp, LOCK_UN);
+    fclose($fp);
+    if (!$ftruncate_return) {
+        $app->raiseMsg(sprintf(_("Log file %s could not be cleared."), $log_file), MSG_WARNING, __FILE__, __LINE__);
+        $app->logMsg(sprintf('ftruncate failed on log file %s', $app->getParam('log_directory') . '/' . $log_file), LOG_WARNING, __FILE__, __LINE__);
+        return false;
+    } else {
+        $app->raiseMsg(sprintf(_("Log file %s has been cleared."), $log_file), MSG_NOTICE, __FILE__, __LINE__);
+        $app->logMsg(sprintf('Log file %s has been cleared', $log_file), LOG_INFO, __FILE__, __LINE__);
+        return true;
+    }
+}
+
+function archiveLog($log_file)
+{
+    $app =& App::getInstance();
+
+    $old_file_name = $log_file;
+    $new_file_name = $log_file . '__' . date('Y-m-d');
+    If (!is_writable($app->getParam('log_directory') . '')) {
+        $app->raiseMsg(sprintf('Cannot archive log, log directory not writable: %s', $app->getParam('log_directory')), MSG_WARNING, __FILE__, __LINE__);
+        $app->logMsg(sprintf('Cannot archive log, log directory not writable: %s', $app->getParam('log_directory')), LOG_WARNING, __FILE__, __LINE__);
+        return false;
+    }
+    If (!copy($app->getParam('log_directory') . '/' . $old_file_name, $app->getParam('log_directory') . '/' . $new_file_name)) {
+        $app->raiseMsg(sprintf(_("Cannot archive log, copying old log file failed."), null), MSG_WARNING, __FILE__, __LINE__);
+        $app->logMsg(sprintf('Cannot archive log, copying old log file failed.', null), LOG_WARNING, __FILE__, __LINE__);
+        return false;
+    }
+
+    $app->raiseMsg(sprintf(_("Log file %s has been archived to %s."), $old_file_name, $new_file_name), MSG_NOTICE, __FILE__, __LINE__);
+    $app->logMsg(sprintf('Log file %s has been archived to %s.', $old_file_name, $new_file_name), LOG_NOTICE, __FILE__, __LINE__);
+    return true;
+}
+
+function printLog($log_file)
+{
+    $app =& App::getInstance();
+
+    if (!is_file($app->getParam('log_directory') . '/' . $log_file)) {
+        $app->raiseMsg(sprintf(_("Log file %s not found."), $log_file), MSG_WARNING, __FILE__, __LINE__);
+        $app->logMsg(sprintf('Log file %s not found.', $app->getParam('log_directory') . '/' . $log_file), LOG_WARNING, __FILE__, __LINE__);
+        return false;
+    }
+
+    readfile($app->getParam('log_directory') . '/' . $log_file);
+}
+
+function &getLog($log_file, $search_query='')
+{
+    $app =& App::getInstance();
+
+    $log = array();
+
+    if (!is_file($app->getParam('log_directory') . '/' . $log_file)) {
+        $app->raiseMsg(sprintf(_("Log file %s not found."), $log_file), MSG_WARNING, __FILE__, __LINE__);
+        $app->logMsg(sprintf('Log file %s not found.', $app->getParam('log_directory') . '/' . $log_file), LOG_WARNING, __FILE__, __LINE__);
+        return $log;
+    }
+    $log = file($app->getParam('log_directory') . '/' . $log_file);
+
+    if ('' != trim($search_query)) {
+        if (getFormData('search_grep')) {
+            $log = preg_grep('/' . str_replace('/', '\/', $search_query) . '/', $log);
+        } else {
+            $log = preg_grep('/' . preg_quote($search_query, '/') . '/i', $log);
+        }
+    }
+    $log = array_values($log);
+    return $log;
+}
+
+function &getLogList()
+{
+    global $valid_file_extensions;
+    $app =& App::getInstance();
+
+    // Get a list of all files in the log directory.
+    $dir_handle = opendir($app->getParam('log_directory'));
+    $list = array();
+    while ($dir_handle && ($file = readdir($dir_handle)) !== false) {
+        if (!preg_match('/^\./', $file) && is_file($app->getParam('log_directory') . '/' . $file) && in_array(mb_strtolower(Upload::getFilenameExtension($file)), $valid_file_extensions)) {
+            $list[] = array(
+                'filename' => $file,
+                'filesize' => filesize($app->getParam('log_directory') . '/' . $file),
+                'modified' => filemtime($app->getParam('log_directory') . '/' . $file),
+            );
+        }
+    }
+    if (is_array($list) && !empty($list)) {
+        sort($list);
+    }
+    return $list;
+}
+
+
+
Index: tags/2.2.3/services/password.php
===================================================================
--- tags/2.2.3/services/password.php	(revision 632)
+++ tags/2.2.3/services/password.php	(revision 632)
@@ -0,0 +1,96 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * password.php
+ */
+
+// Redefine include_path including the codebase/services but allow local templates override global ones.
+ini_set('include_path', join(PATH_SEPARATOR, array(
+    get_include_path(),
+    dirname(__FILE__) . '/templates'
+)));
+
+$auth->requireLogin();
+
+require_once 'codebase/lib/FormValidator.inc.php';
+require_once 'codebase/lib/HTML.inc.php';
+
+/******************************************************************************
+ * CODE CONFIG
+ *****************************************************************************/
+
+// Titles and navigation header.
+$nav->add(sprintf(_("Change password for <em>%s</em>"), $auth->get('username')));
+$nav->set('id', 'password');
+
+// The object to validate form input from the user.
+$fv = new FormValidator();
+
+/********************************************************************
+* MAIN
+********************************************************************/
+
+if (getFormData('boomerang', false) && isset($_SERVER['HTTP_REFERER'])) {
+    // We remember which page we came from so we can go back there.
+    $app->setBoomerangURL($_SERVER['HTTP_REFERER'], 'admin_password');
+}
+$app->sslOn();
+
+switch (getFormData('op')) {
+case 'update_password' :
+
+    // Get the form variables.
+    $frm = getFormData();
+
+    // Validate the posted data.
+    if ($fv->notEmpty('oldpassword', _("You did not specify the <strong>old password</strong>."))) {
+        $fv->checkRegex('oldpassword', '/^\S{0,128}$/i', true, _("The <strong>old password</strong> specified is not valid."));
+    }
+    if ($fv->notEmpty('newpassword', _("You did not specify the <strong>new password</strong>."))) {
+        if ($fv->checkRegex('newpassword', '/^\S{8,128}$/i', true, _("The <strong>new password</strong> specified is not valid. A password must be eight or more characters."))) {
+            if ($fv->notEmpty('newpassword2', _("You need to type the <strong>new password</strong> twice.")) && $frm['newpassword'] != $frm['newpassword2']) {
+                $fv->addError('newpassword', _("The <strong>new passwords</strong> do not match."));
+                $fv->addError('newpassword2');
+            }
+        }
+    }
+
+    if (!$fv->anyErrors() && false === $auth->authenticate($auth->get('username'), $frm['oldpassword'])) {
+        $fv->addError('oldpassword', _("Your <strong>old password</strong> failed authentication."));
+        $app->logMsg(sprintf('Password change failed for %s, using (md5ed) password: %s', $auth->get('username'), md5($frm['oldpassword'])), LOG_NOTICE, __FILE__, __LINE__);
+    }
+
+    if (!$fv->anyErrors()) {
+        $auth->setPassword(null, $frm['newpassword']);
+        $app->logMsg(sprintf('Password change successful for %s', $auth->get('username')), LOG_INFO, __FILE__, __LINE__);
+        $app->raiseMsg(sprintf(_("Password change successful for %s"), $auth->get('username')), MSG_SUCCESS, __FILE__, __LINE__);
+        $app->dieBoomerangURL('admin_password');
+    }
+    break;
+}
+
+// Templates.
+include 'header.ihtml';
+include 'password.ihtml';
+include 'footer.ihtml';
+
Index: tags/2.2.3/services/phpinfo.php
===================================================================
--- tags/2.2.3/services/phpinfo.php	(revision 632)
+++ tags/2.2.3/services/phpinfo.php	(revision 632)
@@ -0,0 +1,39 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+* Authenticated phpinfo() page including environment variables.
+*
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+*/
+// require_once dirname(__FILE__) . '/_config.inc.php';
+$auth->requireLogin();
+phpinfo();
+?><h1>$_SESSION dump</h1><?php
+dump($_SESSION,1);
+?><h1>$_GET dump</h1><?php
+dump($_GET,1);
+?><h1>$_POST dump</h1><?php
+dump($_POST,1);
+?><h1>$_SERVER dump</h1><?php
+dump($_SERVER,1);
Index: tags/2.2.3/services/reset_password.php
===================================================================
--- tags/2.2.3/services/reset_password.php	(revision 632)
+++ tags/2.2.3/services/reset_password.php	(revision 632)
@@ -0,0 +1,97 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+* reset_password.php
+*
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+* @since    30 Jun 2006 01:39:32
+*/
+
+
+/********************************************************************
+* CONFIG
+********************************************************************/
+
+// Redefine include_path including the codebase/services but allow local templates override global ones.
+ini_set('include_path', join(PATH_SEPARATOR, array(
+    get_include_path(),
+    dirname(__FILE__) . '/templates'
+)));
+
+// The object to validate form input from the user.
+require_once 'codebase/lib/FormValidator.inc.php';
+$fv = new FormValidator();
+
+require_once 'codebase/lib/HTML.inc.php';
+
+/********************************************************************
+* MAIN
+********************************************************************/
+
+// If boomerang is set remember which page we came from so we can go back there.
+if (getFormData('boomerang', false) && isset($_SERVER['HTTP_REFERER'])) {
+    $app->setBoomerangURL($_SERVER['HTTP_REFERER'], 'reset_password');
+}
+
+$app->sslOn();
+
+switch (getFormData('op')) {
+case 'reset' :
+    $fv->notEmpty('username', sprintf(_("You must enter your %s."), _("Username")));
+    if (!$fv->anyErrors()) {
+        // Get the user id for this username, if it exists.
+        $qid = $db->query("
+            SELECT " . $db->escapeString($auth->getParam('db_primary_key')) . "
+            FROM " . $db->escapeString($auth->getParam('db_table')) . "
+            WHERE " . $db->escapeString($auth->getParam('db_username_column')) . " = '" . $db->escapeString(getFormData('username')) . "'
+        ");
+        if ((list($user_id) = mysql_fetch_row($qid)) && $auth->resetPassword($user_id, 'This was requested on the "I forgot my password" page.')) {
+            $app->raiseMsg(sprintf(_("Your password has been reset. Your new password has been sent to you in an email."), null), MSG_SUCCESS, __FILE__, __LINE__);
+            $app->dieURL($auth->getParam('login_url'));
+        } else {
+            $app->raiseMsg(sprintf(_("There was a problem resetting the password for <em>%s</em>. Please contact us if you need assistance."), getFormData('username')), MSG_WARNING, __FILE__, __LINE__);
+            $app->logMsg(sprintf('Password reset for %s failed.', getFormData('username')), LOG_NOTICE, __FILE__, __LINE__);
+            $frm = array('username' => getFormData('username'));
+        }
+    }
+    break;
+
+case 'form' :
+default :
+    $frm = array('username' => '');
+    break;
+}
+
+
+/********************************************************************
+* OUTPUT
+********************************************************************/
+
+$nav->add(_("Reset Password"));
+$nav->set('id', 'reset_password');
+
+include 'header.ihtml';
+include 'reset_password.ihtml';
+include 'footer.ihtml';
+
Index: tags/2.2.3/services/templates/admin_form.ihtml
===================================================================
--- tags/2.2.3/services/templates/admin_form.ihtml	(revision 632)
+++ tags/2.2.3/services/templates/admin_form.ihtml	(revision 632)
@@ -0,0 +1,45 @@
+<header class="row">
+    <div class="large-12 columns">
+        <h1><?php echo $nav->get('title'); ?></h1>
+        <?php $fv->printErrorMessages(); ?>
+    </div>
+</header>
+<main role="main">
+    <form method="post" action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" class="sc-form">
+        <?php $app->printHiddenSession(); ?>
+        <input type="hidden" name="op" value="<?php echo $frm['new_op']; ?>" />
+        <input type="hidden" name="admin_id" value="<?php echo $frm['admin_id']; ?>" />
+        <input type="hidden" name="old_username" value="<?php echo $frm['old_username']; ?>" />
+        <div class="row">
+            <div class="medium-5 large-3 columns">
+                <label for="first_name" class="<?php $fv->err('first_name'); ?>"><?php echo _("First name"); ?></label>
+                <input type="text" class="sc-small" size="50" name="first_name" value="<?php echo oTxt($frm['first_name']); ?>" />
+            </div>
+            <div class="medium-5 large-3 columns end">
+                <label for="last_name" class="<?php $fv->err('last_name'); ?>"><?php echo _("Last name"); ?></label>
+                <input type="text" class="sc-small" size="50" name="last_name" value="<?php echo oTxt($frm['last_name']); ?>" />
+            </div>
+        </div>
+        <div class="row">
+            <div class="medium-10 large-6 columns">
+                <label for="email" class="<?php $fv->err('email'); ?>"><?php echo _("Email address"); ?></label>
+                <input type="text" class="sc-medium" size="50" name="email" value="<?php echo oTxt($frm['email']); ?>" />
+            </div>
+        </div>
+        <div class="row">
+            <div class="medium-5 large-3 columns">
+                <label for="username" class="<?php $fv->err('username'); ?>"><?php echo _("Username"); ?></label>
+                <input type="text" class="sc-small" size="50" name="username" value="<?php echo oTxt($frm['username']); ?>" />
+            </div>
+            <div class="medium-5 large-3 columns end">
+                <label for="userpass" class="<?php $fv->err('userpass'); ?>"><?php echo _("Password"); ?></label>
+                <input type="password" class="sc-small" size="50" name="userpass" value="<?php echo oTxt($frm['userpass']); ?>" />
+            </div>
+        </div>
+        <div class="row">
+            <div class="large-6 columns">
+                <?php HTML::printButtons($frm['submit_buttons']); ?>
+            </div>
+        </div>
+    </form>
+</main>
Index: tags/2.2.3/services/templates/admin_list.ihtml
===================================================================
--- tags/2.2.3/services/templates/admin_list.ihtml	(revision 632)
+++ tags/2.2.3/services/templates/admin_list.ihtml	(revision 632)
@@ -0,0 +1,79 @@
+<div class="sticky">
+    <nav class="top-bar list-bar" data-topbar role="navigation" data-options="sticky_on: large">
+        <ul class="title-area">
+            <li class="name"><h1><a href="#"><?php echo $nav->get('title'); ?></a></h1></li>
+            <li class="toggle-topbar"><a href="#"><span><?php echo _("Show commands"); ?></span></a></li>
+        </ul>
+        <section class="top-bar-section">
+            <ul class="left">
+                <li class="has-form"><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?op=add'); ?>" class="button"><i class="fa fa-plus"></i> <?php echo _("Add Administrator"); ?></a></li>
+            </ul>
+            <ul class="right">
+                <li class="has-form">
+                    <form action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" method="get">
+                        <div class="row collapse">
+                            <?php $app->printHiddenSession(false); ?>
+                            <div class="small-8 columns">
+                                <input type="text" class="" size="20" name="search_query" value="<?php echo getFormData('search_query'); ?>" placeholder="<?php echo _("SearchâŠ"); ?>" title="<?php echo oTxt(_("Fields searched: question text.")); ?>" />
+                            </div>
+                            <div class="small-4 columns">
+                                <input type="submit" class="button expand" value="<?php echo _("Search"); ?>" />
+                            </div>
+                        </div>
+                    </form>
+                </li>
+                <?php if ('' != getFormData('search_query')): ?>
+                    <li class="has-form"><a href="<?php echo $app->ohref($_SERVER['PHP_SELF'], false); ?>" class="button expand"><?php echo _("Show all"); ?></a></li>
+                <?php endif ?>
+            </ul>
+        </section>
+    </nav>
+</div>
+<table>
+    <thead>
+        <tr>
+            <th></th>
+            <th><?php echo $so->printSortHeader('admin_tbl.admin_id', _("ID"), 'ASC'); ?></th>
+            <th><?php echo $so->printSortHeader('admin_tbl.username', _("Username"), 'ASC'); ?></th>
+            <th><?php echo $so->printSortHeader('admin_tbl.first_name', _("First"), 'ASC'); ?> <?php echo $so->printSortHeader('admin_tbl.last_name', _("Last name"), 'ASC'); ?></th>
+            <th><?php echo $so->printSortHeader('admin_tbl.seconds_online', _("Min online"), 'ASC'); ?></th>
+            <th><?php echo $so->printSortHeader('admin_tbl.last_login_datetime', _("Last login"), 'ASC'); ?></th>
+            <th><?php echo $so->printSortHeader('admin_tbl.last_login_ip', _("Remote host"), 'ASC'); ?></th>
+            <th><?php echo $so->printSortHeader('admin_tbl.added_datetime', _("Added"), 'ASC'); ?></th>
+            <th><?php echo $so->printSortHeader('admin_tbl.added_by_user_id', _("by"), 'ASC'); ?></th>
+            <th><?php echo $so->printSortHeader('admin_tbl.modified_datetime', _("Modified"), 'ASC'); ?></th>
+            <th><?php echo $so->printSortHeader('admin_tbl.modified_by_user_id', _("by"), 'ASC'); ?></th>
+        </tr>
+    </thead>
+    <tbody>
+        <?php for ($i = 0; $i <= $page->last_item - $page->first_item && $page->total_items > 0; $i++) { ?>
+        <tr>
+            <td>
+                <a href="#" data-dropdown="actions-for-id-<?php echo oTxt($list[$i]['admin_id']); ?>" aria-controls="actions-for-id-<?php echo oTxt($list[$i]['admin_id']); ?>" aria-expanded="false" class="sc-action dropdown" data-options="is_hover:false"><i class="fa fa-cog fa-lg"></i></a>
+                <ul id="actions-for-id-<?php echo oTxt($list[$i]['admin_id']); ?>" data-dropdown-content class="f-dropdown sc-action" aria-hidden="true" tabindex="-1">
+                    <li><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?op=edit&admin_id=' . $list[$i]['admin_id']); ?>"><i class="fa fa-pencil fa-fw"></i> <?php printf(_("Edit <em>%s</em>"), oTxt(truncate($list[$i]['username'], 30))) ?></a></li>
+                    <li><a href="<?php echo $app->oHREF('/versions.php?record_table=admin_tbl&record_key=admin_id&boomerang=true&record_val=' . $list[$i]['admin_id']); ?>"><i class="fa fa-files-o fa-fw"></i> <?php printf(_("Versions of %s"), oTxt(truncate($list[$i]['username'], 30))) ?></a></li>
+                    <li><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . "?op=del&admin_id=" . $list[$i]['admin_id']); ?>"><i class="fa fa-trash-o fa-fw"></i> <?php printf(_("Delete <em>%s</em>"), oTxt(truncate($list[$i]['username'], 30))) ?></a></li>
+                </ul>
+            </td>
+            <td class="sc-nowrap"><?php echo oTxt($list[$i]['admin_id'], true); ?></td>
+            <td class="sc-nowrap"><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?op=edit&admin_id=' . $list[$i]['admin_id']); ?>"><?php echo oTxt($list[$i]['username'], true); ?></a></td>
+            <td class="sc-nowrap"><?php echo oTxt($list[$i]['first_name'], true); ?> <?php echo oTxt($list[$i]['last_name'], true); ?></td>
+            <td class="sc-nowrap"><?php echo round($list[$i]['seconds_online'] / 60); ?></td>
+            <td class="sc-nowrap"><?php
+                if ($auth->isLoggedIn($list[$i]['admin_id'])) {
+                    ?><strong><?php echo date($app->getParam('date_format'), strtotime($list[$i]['last_login_datetime'])) ?></strong><?php
+                } else {
+                    echo Validator::validateStrDate($list[$i]['last_login_datetime']) ? date($app->getParam('date_format'), strtotime($list[$i]['last_login_datetime'])) : '';
+                }
+            ?></td>
+            <td class="sc-nowrap"><?php echo gethostbyaddr($list[$i]['last_login_ip']); ?></td>
+            <td class="sc-nowrap"><?php echo Validator::validateStrDate($list[$i]['added_datetime']) ? date($app->getParam('date_format'), strtotime($list[$i]['added_datetime'])) : ''; ?></td>
+            <td class="sc-nowrap"><?php echo oTxt($list[$i]['added_admin_username'], true); ?></td>
+            <td class="sc-nowrap"><?php echo Validator::validateStrDate($list[$i]['modified_datetime']) ? date($app->getParam('date_format'), strtotime($list[$i]['modified_datetime'])) : ''; ?></td>
+            <td class="sc-nowrap"><?php echo oTxt($list[$i]['modified_admin_username'], true); ?></td>
+        </tr>
+        <?php } ?>
+    </tbody>
+</table>
+<?php include 'list_info.ihtml'; ?>
Index: tags/2.2.3/services/templates/email_reset_password.txt
===================================================================
--- tags/2.2.3/services/templates/email_reset_password.txt	(revision 632)
+++ tags/2.2.3/services/templates/email_reset_password.txt	(revision 632)
@@ -0,0 +1,12 @@
+Hello {NAME},
+
+Your password at {SITE_NAME} has been reset. {REASON}Your new login credentials are:
+
+  USERNAME: {USERNAME}
+  PASSWORD: {PASSWORD}
+
+Any questions? Send us an email at <{SITE_EMAIL}>.
+
+{SITE_NAME}
+{SITE_URL}/
+
Index: tags/2.2.3/services/templates/list_info.ihtml
===================================================================
--- tags/2.2.3/services/templates/list_info.ihtml	(revision 632)
+++ tags/2.2.3/services/templates/list_info.ihtml	(revision 632)
@@ -0,0 +1,11 @@
+<div class="row">
+    <div class="large-12 text-center columns">
+        <ul class="inline-list">
+            <li><?php printf(_("Listing <strong>%s-%s</strong> of <strong>%s</strong> results on <strong>%s</strong> %s"), (0==$page->total_items ? 0 : $page->first_item+1), (0==$page->total_items ? 0 : $page->last_item+1), $page->total_items, $page->total_pages, (1==$page->total_pages ? _("page") : _("pages"))); ?></li>
+            <?php if ($page->total_pages > 1) { ?>
+                <li class=""><?php $page->printPageNumbers() ?></li>
+                <li><?php echo _("Per page"); ?>:&nbsp;<?php $page->printPerPageLinks() ?></li>
+            <?php } ?>
+        </ul>
+    </div>
+</div>
Index: tags/2.2.3/services/templates/lock.ihtml
===================================================================
--- tags/2.2.3/services/templates/lock.ihtml	(revision 632)
+++ tags/2.2.3/services/templates/lock.ihtml	(revision 632)
@@ -0,0 +1,55 @@
+<?php
+if (!is_a($lock, 'Lock')) {
+    // If the Lock object hasn't been instantiated, don't execute this template.
+    return null;
+}
+?>
+<header class="row">
+    <div class="large-12 columns">
+        <h1><?php echo $nav->get('title'); ?></h2>
+    </div>
+</header>
+<main role="main">
+    <form method="post" action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>">
+        <?php $app->printHiddenSession() ?>
+        <input type="hidden" name="lock_id" value="<?php echo $lock->getID(); ?>" />
+        <div id="sc-msg" class="sc-msg row">
+            <div class="large-12 columns">
+                <p class="sc-msg-notice">
+                <?php printf(_("<em>%s</em> is currently being edited by %s (%d minutes elapsed). You cannot modify this database record while it is locked. Please try again later."),
+                    ucfirst($lock->getTitle()),
+                    $lock->getEditor(),
+                    date('i', $lock->getSecondsElapsed() + 60)
+                ); ?>
+                </p>
+            </div>
+        </div>
+        <?php if ($lock->getSecondsElapsed() >= $lock->getParam('timeout')) { ?>
+            <div id="sc-msg" class="sc-msg row">
+                <div class="large-12 columns">
+                    <p class="sc-msg-notice"><?php printf(_("You can forcibly unlock the record if you believe the editing session has expired. You might want to confirm with %s before doing this."), $lock->getEditor()) ?></p>
+                </div>
+            </div>
+            <div class="row sc-submit-buttons">
+                <div class="large-12 columns">
+                    <?php
+                    HTML::printButtons(array(
+                        array('name' => 'unlock', 'value' => _("Unlock"), 'class' => 'small button alert', 'accesskey' => 'u'),
+                        array('name' => 'cancel', 'value' => _("Cancel"), 'class' => 'small button secondary', 'accesskey' => 'c'),
+                    ));
+                    ?>
+                </div>
+            </div>
+        <?php } else { ?>
+            <div class="row sc-submit-buttons">
+                <div class="large-12 columns">
+                    <?php
+                    HTML::printButtons(array(
+                        array('name' => 'cancel', 'value' => _("Cancel"), 'class' => 'small button secondary', 'accesskey' => 'c'),
+                    ));
+                    ?>
+                </div>
+            </div>
+        <?php } ?>
+    </form>
+</main>
Index: tags/2.2.3/services/templates/log_list.ihtml
===================================================================
--- tags/2.2.3/services/templates/log_list.ihtml	(revision 632)
+++ tags/2.2.3/services/templates/log_list.ihtml	(revision 632)
@@ -0,0 +1,87 @@
+<div class="sticky">
+    <nav class="top-bar list-bar" data-topbar role="navigation" data-options="sticky_on: large">
+        <ul class="title-area">
+            <li class="name"><h1><a href="#"><?php echo $nav->get('title'); ?></a></h1></li>
+            <li class="toggle-topbar"><a href="#"><span><?php echo _("Show commands"); ?></span></a></li>
+        </ul>
+        <section class="top-bar-section">
+            <ul class="left">
+                <li class="has-form">
+                    <button data-dropdown="loglist" aria-controls="loglist" aria-expanded="false" class="button"><?php echo _("Select log"); ?> <i class="fa fa-caret-down"></i></button>
+                </li>
+            </ul>
+            <ul class="right">
+                <li class="has-form">
+                    <form action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" method="get">
+                        <div class="row collapse">
+                            <?php $app->printHiddenSession(false); ?>
+                            <div class="small-5 columns">
+                                <input type="text" class="" size="20" name="search_query" value="<?php echo getFormData('search_query'); ?>" placeholder="<?php echo _("SearchâŠ"); ?>" title="<?php echo oTxt(_("Fields searched: question text.")); ?>" />
+                            </div>
+                            <div class="small-4 columns">
+                                <input type="checkbox" id="search_grep" name="search_grep"<?php frmChecked(getFormData('search_grep')) ?> /><label for="search_grep"><?php echo _("Regex"); ?></label>
+                            </div>
+                            <div class="small-3 columns">
+                                <input type="submit" class="button expand" value="<?php echo _("Search"); ?>" />
+                            </div>
+                        </div>
+                    </form>
+                </li>
+                <?php if ('' != getFormData('search_query')): ?>
+                    <li class="has-form"><a href="<?php echo $app->ohref($_SERVER['PHP_SELF'], false); ?>" class="button expand"><?php echo _("Show all"); ?></a></li>
+                <?php endif ?>
+            </ul>
+        </section>
+    </nav>
+    <?php if (is_array($logs) && !empty($logs)) { ?>
+    <div id="loglist" data-dropdown-content class="f-dropdown content large" aria-hidden="true" tabindex="-1">
+        <table>
+            <?php
+            foreach ($logs as $l) {
+                ?>
+                <tr>
+                    <td>
+                        <?php if ($l['filename'] == $tmp_prefs->get('log_file')) { ?>
+                            <strong class="sc-padright"><?php echo sprintf(_("%s"), $l['filename']); ?></strong>
+                        <?php } else { ?>
+                            <a class="sc-padright" href="<?php echo $app->oHREF('?log=' . $l['filename']); ?>"><strong><?php echo sprintf(_("%s"), $l['filename']); ?></strong></a>
+                        <?php } ?>
+                    </td>
+                    <td>
+                        <em class="sc-padright"><?php echo sprintf(_("%01.1f KB, last modified %s"), ($l['filesize']/1024), date($app->getParam('date_format'), $l['modified'])); ?></em>
+                    </td>
+                    <td>
+                        <?php if (!preg_match($no_download_files, $l['filename'])) { ?><a class="sc-padright" href="<?php echo $app->oHREF('?op=download&log=' . $l['filename']); ?>"><?php echo _("Download"); ?></a><?php } ?>
+                        <?php if (!preg_match($no_archive_files, $l['filename'])) { ?><a class="sc-padright" href="<?php echo $app->oHREF('?op=archive&log=' . $l['filename']); ?>"><?php echo _("Archive"); ?></a><?php } ?>
+                        <?php if (!preg_match($no_clear_files, $l['filename'])) { ?><a class="sc-padright" href="<?php echo $app->oHREF('?op=clear&log=' . $l['filename']); ?>" onclick="javascript:return confirm('<?php printf(_("Are you sure you want to clear the log file %s? This will empty this file of its contents. This action is not reversible."), $l['filename'])?>')"><?php echo _("Clear"); ?></a><?php } ?>
+                        <?php if (!preg_match($no_delete_files, $l['filename'])) { ?><a href="<?php echo $app->oHREF('?op=delete&log=' . $l['filename']); ?>" onclick="javascript:return confirm('<?php printf(_("Are you sure you want to delete the log file %s? This will remove this log file permanently. This action is not reversible."), $l['filename'])?>')"><?php echo _("Delete"); ?></a><?php } ?>
+                    </td>
+                </tr><?php
+            }
+            ?>
+        </table>
+    </div>
+    <?php } ?>
+</div>
+
+<pre>
+<?php
+for ($i = $page->first_item; $i <= $page->last_item && $page->total_items; $i++) {
+    echo $list[$i];
+}
+?>
+</pre>
+<?php include 'list_info.ihtml'; ?>
+
+<style>
+td { padding: .1rem .5rem; }
+pre {
+    font-family: monospace;
+    font-size: .75rem;
+    line-height: 1.2;
+    padding: 15px;
+}
+.right .has-form .columns {
+    padding: 0 .25rem;
+}
+</style>
Index: tags/2.2.3/services/templates/login_form.ihtml
===================================================================
--- tags/2.2.3/services/templates/login_form.ihtml	(revision 632)
+++ tags/2.2.3/services/templates/login_form.ihtml	(revision 632)
@@ -0,0 +1,41 @@
+<?php
+$autocomplete = $auth->getParam('login_form_allow_autocomplete') ? '' : ' autocomplete="off"';
+?>
+<form method="post" action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" class="sc-form"<?php echo $autocomplete; ?>>
+<?php $app->printHiddenSession() ?>
+<table>
+    <tr class="sc-form-row">
+        <td class="sc-right"><label for="username"><?php echo _("Username"); ?></label></td>
+        <td>
+            <input type="text" class="sc-small" size="20" name="username" value="<?php echo oTxt($frm['username']); ?>" />
+        </td>
+    </tr>
+    <tr class="sc-form-row">
+        <td class="sc-right"><label for="password"><?php echo _("Password"); ?></label></td>
+        <td>
+            <input type="password" class="sc-small" size="20" name="password" value=""<?php echo $autocomplete; ?> />
+        </td>
+    </tr>
+    <tr class="sc-form-row">
+        <td class="sc-right">&nbsp;</td>
+        <td>
+            <label class="sc-label-secondary"><input type="checkbox" name="remember_me" id="remember_me" value="1"<?php frmChecked($frm['remember_me']) ?> /> <?php echo _("Remember my username"); ?></label>
+        </td>
+    </tr>
+    <tr>
+        <td>&nbsp;</td>
+        <td><input type="submit" value="<?php echo _("Login"); ?>" /></td>
+    </tr>
+</table>
+</form>
+<script type="text/javascript" charset="utf-8">
+/* <![CDATA[ */
+$(document).ready(function() {
+    if ($('input[name="username"]').val() != '') {
+        $('input[name="password"]').focus();
+    } else {
+        $('input[name="username"]').focus();
+    }
+});
+/* ]]> */
+</script>
Index: tags/2.2.3/services/templates/password.ihtml
===================================================================
--- tags/2.2.3/services/templates/password.ihtml	(revision 632)
+++ tags/2.2.3/services/templates/password.ihtml	(revision 632)
@@ -0,0 +1,42 @@
+<header class="row">
+    <div class="large-12 columns">
+        <h1><?php echo $nav->get('title'); ?></h1>
+        <?php $fv->printErrorMessages(); ?>
+    </div>
+</header>
+<main role="main">
+    <form method="post" action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" class="sc-form">
+        <?php $app->printHiddenSession() ?>
+        <input type="hidden" name="op" value="update_password" />
+        <div class="row">
+            <div class="large-6 columns">
+                <label for="oldpassword" class="<?php $fv->err('oldpassword'); ?>"><?php echo _("Old password"); ?></label>
+                <input type="password" class="sc-medium" size="50" name="oldpassword" />
+            </div>
+        </div>
+        <div class="row">
+            <div class="large-6 columns">
+                <label for="newpassword" class="<?php $fv->err('newpassword'); ?><?php $fv->err('newpassword2'); ?>"><?php echo _("New password (twice)"); ?></label>
+                <input type="password" class="sc-medium" size="50" name="newpassword" />
+            </div>
+        </div>
+        <div class="row">
+            <div class="large-6 columns">
+                <input type="password" class="sc-medium" size="50" name="newpassword2" />
+                <div class="sc-help"><?php printf(_("See the <a href=\"%s\">Strangecode password guide</a> for tips on choosing a strong password."), 'http://www.strangecode.com/support/passwords'); ?></div>
+            </div>
+        </div>
+        <div class="row">
+            <div class="large-6 columns">
+                <?php
+                HTML::printButtons(array(array(
+                    'name' => 'submit',
+                    'value' => _("Change password"),
+                    'class' => 'small button',
+                    'accesskey' => 's'
+                )));
+                ?>
+            </div>
+        </div>
+    </form>
+</main>
Index: tags/2.2.3/services/templates/reset_password.ihtml
===================================================================
--- tags/2.2.3/services/templates/reset_password.ihtml	(revision 632)
+++ tags/2.2.3/services/templates/reset_password.ihtml	(revision 632)
@@ -0,0 +1,31 @@
+<header class="row">
+    <div class="large-12 columns">
+        <h1><?php echo $nav->get('title'); ?></h1>
+        <p><?php echo _("If you have forgotten your password enter your username here. A new password will be emailed to you."); ?></p>
+        <?php $fv->printErrorMessages(); ?>
+    </div>
+</header>
+<main role="main">
+    <form method="post" action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" class="sc-form">
+        <?php $app->printHiddenSession(); ?>
+        <input type="hidden" name="op" value="reset" />
+        <div class="row<?php $fv->err('username'); ?>">
+            <div class="large-6 columns">
+                <label for="username"><?php echo _("Username"); ?></label>
+                <input type="text" name="username" id="username" class="sc-medium" value="<?php echo oTxt($frm['username']); ?>" />
+            </div>
+        </div>
+        <div class="row sc-submit-buttons">
+            <div class="large-6 columns">
+                <?php
+                HTML::printButtons(array(array(
+                    'name' => 'submit',
+                    'value' => _("Submit"),
+                    'class' => 'small button',
+                    'accesskey' => 's'
+                )));
+                ?>
+            </div>
+        </div>
+    </form>
+</main>
Index: tags/2.2.3/services/templates/versions_diff.ihtml
===================================================================
--- tags/2.2.3/services/templates/versions_diff.ihtml	(revision 632)
+++ tags/2.2.3/services/templates/versions_diff.ihtml	(revision 632)
@@ -0,0 +1,54 @@
+<header class="row">
+    <div class="large-12 columns">
+        <h1><?php echo $nav->get('title'); ?></h1>
+    </div>
+</header>
+<main role="main">
+    <div class="row">
+        <div class="large-12 columns">
+            <?php
+            if (is_array($data) && !empty($data)) {
+                ?>
+                <table class="list">
+                    <thead>
+                        <tr>
+                            <th class="sc-padleft"><?php echo _("Field"); ?></th>
+                            <th class="sc-padleft"><?php echo _("Saved version"); ?></th>
+                            <th class="sc-padleft"><?php echo _("Current record"); ?></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php
+                        foreach ($current as $k=>$v_c) {
+                            if (isset($data[$k])) {
+                                $v_d =  $data[$k];
+                                $td_style = '';
+                            } else {
+                                $v_d = '(Unavailable)';
+                                $td_style = 'color: #e00;';
+                            }
+                            $tr_style = $v_d != $v_c ? 'font-weight:bold;' : '';
+                            ?>
+                            <tr style="<?php echo $tr_style; ?>">
+                                <td class="sc-nowrap"><label><?php echo oTxt($k); ?></label></td>
+                                <td style="<?php echo $td_style; ?>"><?php echo oTxt($v_d); ?></td>
+                                <td style="<?php echo $td_style; ?>"><?php echo oTxt($v_c); ?></td>
+                            </tr>
+                        <?php } ?>
+                    </tbody>
+                </table>
+                <p class="sc-help"><?php echo _("Emboldened lines indicate something changed between the saved version and the current record."); ?></p>
+            <?php
+            }
+
+            // Buttons.
+            $action_links = array();
+            if (!getFormData('current', false)) {
+                $action_links[] = array('href' => $app->oHREF('?op=restore', array('version_id', 'version_title')), 'value' => _("Restore this saved version"), 'class' => 'small button alert', 'accesskey' => 'r');
+            }
+            $action_links[] = array('href' => $app->ohref('?op=cancel'), 'value' => _("Cancel"), 'class' => 'small button secondary', 'accesskey' => 'c');
+            HTML::printButtons($action_links);
+            ?>
+        </div>
+    </div>
+</main>
Index: tags/2.2.3/services/templates/versions_list.ihtml
===================================================================
--- tags/2.2.3/services/templates/versions_list.ihtml	(revision 632)
+++ tags/2.2.3/services/templates/versions_list.ihtml	(revision 632)
@@ -0,0 +1,60 @@
+<header class="row">
+    <div class="large-12 columns">
+        <h1><?php echo $nav->get('title'); ?></h1>
+    </div>
+</header>
+<main role="main">
+    <form method="post" action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" class="sc-form">
+        <?php $app->printHiddenSession() ?>
+        <div class="row">
+            <div class="large-12 columns">
+                <table>
+                    <thead>
+                        <tr>
+                            <th></th>
+                            <th><?php echo _("ID"); ?></th>
+                            <th><?php echo _("Date"); ?></th>
+                            <th><?php echo _("By"); ?></th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <?php
+                        $first = true;
+                        foreach ($versions as $v) { ?>
+                        <tr>
+                            <td>
+                                <a href="#" data-dropdown="actions-for-id-<?php echo oTxt($v['version_id']); ?>" aria-controls="actions-for-id-<?php echo oTxt($v['version_id']); ?>" aria-expanded="false" class="sc-action dropdown" data-options="is_hover:false"><i class="fa fa-cog fa-lg"></i></a>
+                                <ul id="actions-for-id-<?php echo oTxt($v['version_id']); ?>" data-dropdown-content class="f-dropdown sc-action" aria-hidden="true" tabindex="-1">
+                                    <li><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?op=view&version_id=' . $v['version_id']); ?>"><i class="fa fa-eye fa-fw"></i> <?php printf(_("View version <em>%s</em>"), oTxt(truncate($v['version_id'], 30))) ?></a></li>
+                                    <?php if (!$first) { ?>
+                                        <li><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?op=diff&version_id=' . $v['version_id']); ?>"><i class="fa fa-columns fa-fw"></i> <?php printf(_("View differences from version %s"), oTxt(truncate($v['version_id'], 30))) ?></a></li>
+                                        <li><a href="<?php echo $app->oHREF($_SERVER['PHP_SELF'] . '?op=restore&version_id=' . $v['version_id']); ?>"><i class="fa fa-history fa-fw"></i> <?php printf(_("Restore version <em>%s</em>"), oTxt(truncate($v['version_id'], 30))) ?></a></li>
+                                    <?php } ?>
+                                </ul>
+                            </td>
+                            <td class="sc-padleft sc-nowrap"><?php echo $v['version_id']; ?></td>
+                            <td class="sc-padleft sc-nowrap"><?php
+                                echo date(sprintf('%s %s', $app->getParam('date_format'), $app->getParam('time_format')), strtotime($v['version_datetime']));
+                                if ($first) {
+                                    ?> <span><em>(<?php echo _("The current record"); ?>)</em></span><?php
+                                }
+                                ?></td>
+                            <td class="sc-padleft sc-nowrap"><?php echo $v['editor']; ?></td>
+                        </tr>
+                        <?php
+                        $first = false;
+                        }
+                        ?>
+                    </tbody>
+                </table>
+                <div class="sc-help"><?php printf(_("When there are more than %s versions, those over %s days old are deleted."), $version->getParam('min_qty'), $version->getParam('min_days')); ?></div>
+                <?php
+                // Buttons.
+                $action_links = array();
+                $action_links[] = array('href' => $app->ohref('?op=cancel'), 'value' => _("Cancel"), 'class' => 'small button secondary', 'accesskey' => 'c');
+                HTML::printButtons($action_links);
+                ?>
+            </div>
+        </div>
+    </form>
+</main>
Index: tags/2.2.3/services/templates/versions_view.ihtml
===================================================================
--- tags/2.2.3/services/templates/versions_view.ihtml	(revision 632)
+++ tags/2.2.3/services/templates/versions_view.ihtml	(revision 632)
@@ -0,0 +1,37 @@
+<header class="row">
+    <div class="large-12 columns">
+        <h1><?php echo $nav->get('title'); ?></h1>
+    </div>
+</header>
+<main role="main">
+    <div class="row">
+        <div class="large-12 columns">
+            <?php
+            if (is_array($data) && !empty($data)) {
+                ?>
+                <table class="list">
+                    <tbody>
+                        <?php
+                        foreach ($data as $k => $v_d) {
+                            ?>
+                            <tr>
+                                <td class="sc-nowrap"><label><?php echo oTxt($k); ?></label></td>
+                                <td><pre><?php echo oTxt($v_d); ?></pre></td>
+                            </tr>
+                        <?php } ?>
+                    </tbody>
+                </table>
+            <?php
+            }
+
+            // Buttons.
+            $action_links = array();
+            if (!getFormData('current', false)) {
+                $action_links[] = array('href' => $app->oHREF('?op=restore', array('version_id', 'version_title')), 'value' => _("Restore this saved version"), 'class' => 'small button alert', 'accesskey' => 'r');
+            }
+            $action_links[] = array('href' => $app->ohref('?op=cancel'), 'value' => _("Cancel"), 'class' => 'small button secondary', 'accesskey' => 'c');
+            HTML::printButtons($action_links);
+            ?>
+        </div>
+    </div>
+</main>
Index: tags/2.2.3/services/versions.php
===================================================================
--- tags/2.2.3/services/versions.php	(revision 632)
+++ tags/2.2.3/services/versions.php	(revision 632)
@@ -0,0 +1,148 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * versions.php
+ */
+
+// require_once dirname(__FILE__) . '/_config.inc.php';
+
+$app->sslOn();
+$auth->requireLogin();
+
+require_once 'codebase/lib/Version.inc.php';
+require_once 'codebase/lib/Lock.inc.php';
+require_once 'codebase/lib/HTML.inc.php';
+
+/******************************************************************************
+ * CODE CONFIG
+ *****************************************************************************/
+
+// Since we're using the singleton pattern we can instantiate a Version object earlier with custom parameters.
+$version =& Version::getInstance($auth);
+$version->setParam(array(
+    // If true, makes an exact comparison of saved vs. live table schemas. If false, just checks that the saved columns are available.
+    'db_schema_strict' => false,
+));
+
+// Query arguments to retain their values between page requests.
+$app->carryQuery('record_table');
+$app->carryQuery('record_key');
+$app->carryQuery('record_val');
+$app->carryQuery('version_title');
+
+// Titles and navigation header.
+$nav->add(_("Versions"), null);
+$nav->set('id', 'versions');
+
+/********************************************************************
+* MAIN
+********************************************************************/
+
+// Request arguments.
+$version_id = getFormData('version_id');
+$record_table = getFormData('record_table');
+$record_key = getFormData('record_key');
+$record_val = getFormData('record_val');
+
+if ('' == $version_id && ('' == $record_table || '' == $record_key || '' == $record_val)) {
+    $app->raiseMsg(_("Record not specified for versioning."), MSG_WARNING, __FILE__, __LINE__);
+    $app->logMsg('Record not specified for versioning.', LOG_WARNING, __FILE__, __LINE__);
+    $app->dieBoomerangURL();
+}
+
+if (getFormData('boomerang', false) && isset($_SERVER['HTTP_REFERER'])) {
+    // We remember which page we came from so we can go back there.
+    $app->setBoomerangURL($_SERVER['HTTP_REFERER'], 'versions');
+}
+
+// What action to take.
+switch (getFormData('op')) {
+
+case 'cancel' :
+    if ($app->validBoomerangURL('versions')) {
+        // Display boomerang page.
+        $app->dieBoomerangURL('versions');
+    }
+    // Display default page.
+    $app->dieURL(false, false);
+    break;
+
+case 'view' :
+    $data = $version->getData($version_id);
+    $versionrecord = $version->getVerson($version_id);
+    $nav->add(sprintf(_("%s <small>Version %s (%s)</small>"), $versionrecord['version_title'], $version_id, date(sprintf('%s %s', $app->getParam('date_format'), $app->getParam('time_format')), strtotime($versionrecord['version_datetime']))));
+    $main_template = 'versions_view.ihtml';
+    break;
+
+case 'diff' :
+    $data = $version->getData($version_id);
+    $versionrecord = $version->getVerson($version_id);
+    $current = $version->getCurrent($record_table, $record_key, $record_val);
+    if (serialize($data) == serialize($current)) {
+        $app->raiseMsg(sprintf(_("Version %s is identical to the current record"), $version_id), MSG_NOTICE, __FILE__, __LINE__);
+    }
+    $nav->add(sprintf(_("%s <small>Version %s (%s)</small>"), $versionrecord['version_title'], $version_id, date(sprintf('%s %s', $app->getParam('date_format'), $app->getParam('time_format')), strtotime($versionrecord['version_datetime']))));
+    $main_template = 'versions_diff.ihtml';
+    break;
+
+case 'restore' :
+    if (!isset($lock) || !is_a($lock, 'Lock')) {
+        $lock =& Lock::getInstance($auth);
+    }
+    $lock->select($record_table, $record_key, $record_val);
+    if ($lock->isLocked() && !$lock->isMine()) {
+        $lock->dieErrorPage();
+    }
+
+    if ($v = $version->restore($version_id)) {
+        // Create version of this restored record as the "current" version.
+        $version->create($record_table, $record_key, $record_val, $v['version_title']);
+        $app->raiseMsg(sprintf(_("The record <em>%s</em> has been replaced with <em>%s</em> version <em>%s</em> from <em>%s</em>."), getFormData('version_title'), $v['version_title'], $version_id, $v['version_datetime']), MSG_SUCCESS, __FILE__, __LINE__);
+        $app->dieBoomerangURL('versions', array('break_list_cache'=>'true', false));
+    } else {
+        $app->raiseMsg(_("Version restoration failed."), MSG_ERR, __FILE__, __LINE__);
+        $app->dieURL($_SERVER['PHP_SELF']);
+    }
+    break;
+
+default :
+    $versions = $version->getList($record_table, $record_key, $record_val);
+    if (is_array($versions) && !empty($versions)) {
+        $_POST['version_title'] = $versions[0]['version_title'];
+        $nav->add(sprintf(_("%s versions of <em>%s</em>"), sizeof($versions), $versions[0]['version_title']));
+        $main_template = 'versions_list.ihtml';
+    } else {
+        $app->raiseMsg(sprintf(_("No saved versions available"), null), MSG_NOTICE, __FILE__, __LINE__);
+        $app->dieBoomerangURL('versions');
+    }
+}
+
+
+/******************************************************************************
+ * TEMPLATE INITIALIZATION
+ *****************************************************************************/
+
+include 'header.ihtml';
+include 'codebase/services/templates/' . $main_template;
+include 'footer.ihtml';
+
Index: tags/2.2.3/tests/AppTest.php
===================================================================
--- tags/2.2.3/tests/AppTest.php	(revision 632)
+++ tags/2.2.3/tests/AppTest.php	(revision 632)
@@ -0,0 +1,216 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/App.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class AppTest extends PHPUnit_Framework_TestCase {
+
+    var $App;
+
+    static $shared_session;
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        $this->App =& $app;
+        $this->App->cli = false;
+        $_SESSION = AppTest::$shared_session;
+    }
+
+    function tearDown()
+    {
+        unset($this->App);
+        AppTest::$shared_session = $_SESSION;
+    }
+
+    function test_getinstance()
+    {
+        $thisapp =& App::getInstance();
+        $this->assertTrue(serialize($thisapp) === serialize($this->App), 'Objects do not match across instantiations.');
+    }
+
+    function test_setParam()
+    {
+        $this->App->setParam(array(
+            'test_config_value' => 1234
+        ));
+        $this->assertTrue(1234 === $this->App->getParam('test_config_value'));
+    }
+
+    function test_getParam()
+    {
+        //$this->App->setParam('test_config_value2', 'okay');
+        $this->App->setParam(array(
+            'test_config_value2' => 'okay'
+        ));
+        $result = $this->App->getParam('test_config_value2');
+        $this->assertEquals('okay', $result);
+    }
+
+    function test_start()
+    {
+        unset($_SESSION);
+        $this->App->stop();
+        $this->App->start();
+
+        $this->assertEquals(ini_get('error_reporting'), E_ALL, 'Error reporting not set to E_ALL.');
+        $this->assertTrue($this->App->db->dbh && is_resource($this->App->db->dbh), 'DB handler not a resource');
+        $this->assertTrue(isset($_SESSION), '$_SESSION is not set.');
+        $_SESSION['sess_test_value'] = 'okay';
+        $this->assertEquals($_SESSION['sess_test_value'], 'okay', '$_SESSION not storing values.');
+        $this->assertTrue(session_name() == 'StrangecodeTestSession', 'Session_name not set correctly.');
+    }
+
+    function test_stop()
+    {
+    }
+
+    function test_dbquery()
+    {
+        $db =& DB::getInstance();
+
+        $qid = $db->query("SELECT 2 + 2");
+        list($result) = mysql_fetch_row($qid);
+        $this->assertEquals('4', $result);
+    }
+
+    function test_raisemsg()
+    {
+        $app =& App::getInstance();
+        $expected = 'My message';
+        $app->raiseMsg($expected, MSG_NOTICE, __FILE__, __LINE__);
+        $msg = current($_SESSION['_app']['testapp']['messages']);
+        $this->assertEquals($expected, $msg['message']);
+    }
+
+    function test_printraisedmessages()
+    {
+        $app =& App::getInstance();
+        ob_start();
+        $this->test_raisemsg();  //had to add this line for phpunit ver. 3.7 ///
+        $app->printraisedmessages();
+        $result = ob_get_clean();
+        $this->assertContains('My message', $result, 'Raised message not found in output.');
+    }
+
+    function test_logmsg()
+    {
+        $app =& App::getInstance();
+        $file = $this->App->getParam('log_directory') . '/' . $this->App->getParam('log_filename');
+        $app->logMsg('Test log message', LOG_DEBUG, __FILE__, __LINE__);
+        if ($result = file($file)) {
+            $result = end($result);
+        } else {
+            $result = '';
+        }
+        $this->assertContains('Test log message', $result, 'Test message not recorded in log: ' . $file);
+    }
+
+    function test_ohref()
+    {
+        $app =& App::getInstance();
+        $_GET['arg1'] = 'A';
+        $result = $app->ohref('/some/url.php', array('arg1'), true);
+        $this->assertContains(session_name(), $result, 'SSID not found in URL.');
+        $this->assertContains('arg1=A', $result, 'Argument not passed through.');
+    }
+
+    function test_printhiddensession()
+    {
+        $app =& App::getInstance();
+        $app->setParam(array('session_use_trans_sid' => true));
+        ob_start();
+        $app->printhiddensession();
+        $result = ob_get_clean();
+        $this->assertContains(session_name(), $result);
+    }
+
+//     function test_dieurl()
+//     {
+//         $app =& App::getInstance();
+//         $app->dieURL('/die/to/this/url.php');
+//     }
+//
+//     function test_dieboomerangurl()
+//     {
+//         $result = $this->App->dieboomerangurl(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function test_setboomerangurl()
+//     {
+//         $result = $this->App->setboomerangurl(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function test_getboomerangurl()
+//     {
+//         $result = $this->App->getboomerangurl(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function test_deleteboomerangurl()
+//     {
+//         $result = $this->App->deleteboomerangurl(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function test_validboomerangurl()
+//     {
+//         $result = $this->App->validboomerangurl(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function test_sslon()
+//     {
+//         $result = $this->App->sslon(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function test_ssloff()
+//     {
+//         $result = $this->App->ssloff(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+
+}
+// Running the test.
+/*
+$suite = new PHPUnit_TestSuite('AppTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+ */
Index: tags/2.2.3/tests/AuthFileTest.php
===================================================================
--- tags/2.2.3/tests/AuthFileTest.php	(revision 632)
+++ tags/2.2.3/tests/AuthFileTest.php	(revision 632)
@@ -0,0 +1,116 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/AuthFile.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+//
+class AuthFileTest extends PHPUnit_Framework_TestCase {
+
+    var $AuthFile;
+    
+    static $shared_session;
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Auth_File.inc.php';
+        $this->AuthFile = new Auth_File('test_auth');
+        $this->AuthFile->setParam(array('htpasswd_file' => dirname(__FILE__) . '/_test_htpasswd'));
+        $_SESSION = AuthFileTest::$shared_session;
+    }
+
+    function tearDown()
+    {
+        unset($this->AuthFile);
+        AuthFileTest::$shared_session = $_SESSION;
+    }
+
+    function test_authenticate()
+    {
+        $true = $this->AuthFile->authenticate('testuser', 'testpass');
+        $this->assertTrue($true, 'testuser not authorized with good password.');
+
+        $false = $this->AuthFile->authenticate('testuser', 'wrongpass');
+        $this->assertFalse($false, 'testuser not blocked with bad password.');
+    }
+
+    function test_login()
+    {
+        $result = $this->AuthFile->login('testuser', 'testpass');
+        $this->assertTrue($result, 'testuser login failed.');
+        $this->assertTrue($_SESSION['_auth_file']['test_auth']['authenticated'], 'testuser authentication not found in session.');
+    }
+
+    function test_clear()
+    {
+        $result = $this->AuthFile->login('testuser', 'testpass');
+        $this->AuthFile->clear();
+        $this->assertFalse($_SESSION['_auth_file']['test_auth']['authenticated'], 'testuser authentication not false in session.');
+    }
+
+    function test_isloggedin()
+    {
+        $this->AuthFile->login('testuser', 'testpass');
+        $true = $this->AuthFile->isloggedin();
+        $this->assertTrue($true, 'testuser not logged in but should be.');
+
+        $this->AuthFile->clear();
+        $false = $this->AuthFile->isloggedin();
+        $this->assertFalse($false, 'testuser is logged in but shouldn\'t be.');
+    }
+
+//     function test_isadmin()
+//     {
+//         $this->AuthFile->login('testuser', 'testpass');
+//         $true = $this->AuthFile->isadmin();
+//         $this->assertTrue($true);
+//     }
+//
+//     function test_requireadminlogin()
+//     {
+//         $result = $this->AuthFile->requireadminlogin(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function test__encrypt()
+//     {
+//         $result = $this->AuthFile->_encrypt(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function test__salt()
+//     {
+//         $result = $this->AuthFile->_salt(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+
+}
Index: tags/2.2.3/tests/AuthSQLTest.php
===================================================================
--- tags/2.2.3/tests/AuthSQLTest.php	(revision 632)
+++ tags/2.2.3/tests/AuthSQLTest.php	(revision 632)
@@ -0,0 +1,286 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/Auth_SQL.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+class AuthSQLTest extends PHPUnit_Framework_TestCase {
+
+    var $Auth_SQL;
+
+    static $shared_session;
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Auth_SQL.inc.php';
+        $this->Auth_SQL = new Auth_SQL('testauth');
+        $this->Auth_SQL->setParam(array(
+            'db_table'          => 'test_user_tbl',
+            'db_primary_key'    => 'user_id',
+            'db_login_table'    => 'test_login_tbl',
+            'login_url'         => '/login.php',
+            'blocking'          => true,
+            'encryption_type' => Auth_SQL::ENCRYPT_MD5_HARDENED,
+        ));
+
+        // Use fresh user table.
+        $this->Auth_SQL->initDB(true);
+
+        // Insert test data.
+        $db =& DB::getInstance();
+        $db->query("
+            INSERT INTO test_user_tbl (
+                username,
+                userpass,
+                first_name,
+                last_name,
+                email
+            ) VALUES (
+                'testuser',
+                '" . $this->Auth_SQL->encryptPassword('testpass') . "',
+                'John',
+                'Doe',
+                'root@localhost'
+            )
+        ");
+        $_SESSION = AuthSQLTest::$shared_session;
+
+        // Sessions require client IP addr.
+        $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+    }
+
+    function tearDown()
+    {
+        $db =& DB::getInstance();
+
+        unset($this->Auth_SQL);
+        $db->query("DROP TABLE IF EXISTS test_user_tbl");
+        $db->query("DROP TABLE IF EXISTS test_login_tbl");
+        AuthSQLTest::$shared_session = $_SESSION;
+    }
+
+    function test_set()
+    {
+        $this->Auth_SQL->set('testuserkey', 'testuserval');
+        $this->assertEquals('testuserval', $_SESSION['_auth_sql']['testauth']['user_data']['testuserkey']);
+    }
+
+    function test_get()
+    {
+        $_SESSION['_auth_sql']['testauth']['user_data']['testuserkey'] = 'testuserval';
+        $val = $this->Auth_SQL->get('testuserkey');
+        $this->assertEquals('testuserval', $val);
+    }
+
+    function test_setparam()
+    {
+        $this->Auth_SQL->setParam(array(
+            'login_url'         => 'testloginurl.php'
+        ));
+        $this->assertEquals('testloginurl.php', $this->Auth_SQL->getParam('login_url'));
+    }
+
+    function test_getparam()
+    {
+        //$this->Auth_SQL->_params['login_url'] = 'testloginurl.php';
+        $this->Auth_SQL->setParam(array(
+            'login_url'         => 'testloginurl.php'
+        ));
+        $param = $this->Auth_SQL->getParam('login_url');
+        $this->assertEquals('testloginurl.php', $param);
+    }
+
+    function test_clear()
+    {
+        $login = $this->Auth_SQL->login('testuser', 'testpass');
+        $this->assertTrue($login, 'User login failed, but should have succeeded.');
+        $before_logged_in = $this->Auth_SQL->isloggedin();
+        $this->assertTrue($before_logged_in, 'User is not logged in, but should be.');
+        $this->Auth_SQL->clear();
+        $after_logged_in = $this->Auth_SQL->isloggedin();
+        $this->assertFalse($after_logged_in, 'User is still logged in but should not be.');
+    }
+
+    function test_authenticate()
+    {
+        $true = $this->Auth_SQL->authenticate('testuser', 'testpass');
+        $this->assertInternalType('array', $true, 'User login failed, but should have succeeded.');
+
+        // Testing wrong password.
+        $false = $this->Auth_SQL->authenticate('testuser', 'wrongpass');
+
+        $this->assertfalse($false, 'User login succeeded, but should have failed.');
+    }
+
+    function test_login_and_isLoggedIn()
+    {
+        $login = $this->Auth_SQL->login('testuser', 'testpass');
+        $this->assertTrue($login, '1. User login failed, but should have succeeded.');
+        $before_logged_in = $this->Auth_SQL->isloggedin();
+        $this->assertTrue($before_logged_in, '2. User is not logged in, but should be.');
+        $this->Auth_SQL->clear();
+        $after_logged_in = $this->Auth_SQL->isloggedin();
+        $this->assertFalse($after_logged_in, '3. User is still logged in but should not be.');
+
+        // Testing wrong password.
+        $login2 = $this->Auth_SQL->login('testuser', 'wrongpass');
+        $this->assertFalse($login2, '4. User login succeeded, but should have failed.');
+        $before_logged_in2 = $this->Auth_SQL->isloggedin();
+        $this->assertFalse($before_logged_in2, '5. User is logged in, but should not be.');
+        $this->Auth_SQL->clear();
+        $after_logged_in2 = $this->Auth_SQL->isloggedin();
+        $this->assertFalse($after_logged_in2, '6. Wrong user is still logged in but should not be.');
+    }
+
+    function test_requirelogin()
+    {
+//         $this->Auth_SQL->requirelogin('Login is required!');
+    }
+
+    function test_blockaccount()
+    {
+        $db =& DB::getInstance();
+
+        $this->Auth_SQL->login('testuser', 'testpass');
+        $this->Auth_SQL->blockaccount(null, 'blocktestuser');
+        $qid = $db->query("
+            SELECT blocked_reason
+            FROM test_user_tbl
+        ");
+        list($reason) = mysql_fetch_row($qid);
+        $this->assertEquals('blocktestuser', $reason, "Block not found in DB record.");
+    }
+
+    function test_unblockaccount()
+    {
+        $db =& DB::getInstance();
+
+        $db->query("
+            UPDATE test_user_tbl SET blocked_reason = 'blocktestuser'
+        ");
+        $this->Auth_SQL->unblockaccount();
+
+        $qid = $db->query("
+            SELECT blocked_reason
+            FROM test_user_tbl
+        ");
+        list($reason) = mysql_fetch_row($qid);
+        $this->assertTrue('' == $reason, "Block not removed from DB record.");
+    }
+
+    function test_usernameexists()
+    {
+        $result = $this->Auth_SQL->usernameexists('testuser');
+        $this->assertTrue($result);
+    }
+
+    function test_getusername()
+    {
+        $result = $this->Auth_SQL->getusername(1);
+        $this->assertEquals('testuser', $result);
+    }
+
+    function test_generatepassword()
+    {
+        $result = $this->Auth_SQL->generatepassword(10);
+        $this->assertEquals(14, strlen($result));
+    }
+
+    function test_encryptpassword()
+    {
+        $result = $this->Auth_SQL->encryptpassword('123', null, Auth_SQL::ENCRYPT_MD5);
+        $this->assertEquals('202cb962ac59075b964b07152d234b70', $result);
+        $this->assertTrue($this->Auth_SQL->verifyPassword('123', $result, Auth_SQL::ENCRYPT_MD5));
+
+        $result = $this->Auth_SQL->encryptpassword('123', null, Auth_SQL::ENCRYPT_MD5_HARDENED);
+        $this->assertEquals('1f0f8d357a96eb97f24371ebf53dcaf6', $result);
+        $this->assertTrue($this->Auth_SQL->verifyPassword('123', $result, Auth_SQL::ENCRYPT_MD5_HARDENED));
+
+        $result = $this->Auth_SQL->encryptpassword('123', null, Auth_SQL::ENCRYPT_SHA1);
+        $this->assertEquals('40bd001563085fc35165329ea1ff5c5ecbdbbeef', $result);
+        $this->assertTrue($this->Auth_SQL->verifyPassword('123', $result, Auth_SQL::ENCRYPT_SHA1));
+
+        $result = $this->Auth_SQL->encryptpassword('123', null, Auth_SQL::ENCRYPT_SHA1_HARDENED);
+        $this->assertEquals('1d086fcae3dd941e0f1371148502d03e96ab536f', $result);
+        $this->assertTrue($this->Auth_SQL->verifyPassword('123', $result, Auth_SQL::ENCRYPT_SHA1_HARDENED));
+
+        $result = $this->Auth_SQL->encryptpassword('123', null, Auth_SQL::ENCRYPT_PLAINTEXT);
+        $this->assertEquals('123', $result);
+        $this->assertTrue($this->Auth_SQL->verifyPassword('123', $result, Auth_SQL::ENCRYPT_PLAINTEXT));
+
+        $result = $this->Auth_SQL->encryptpassword('123', 'saltstring', Auth_SQL::ENCRYPT_CRYPT);
+        $this->assertTrue($this->Auth_SQL->verifyPassword('123', $result, Auth_SQL::ENCRYPT_CRYPT));
+
+        if (function_exists('password_hash')) {
+            // Only available in PHP >= 5.5
+            $result = $this->Auth_SQL->encryptpassword('123', 'saltstring', Auth_SQL::ENCRYPT_PASSWORD_BCRYPT);
+            $this->assertTrue($this->Auth_SQL->verifyPassword('123', $result, Auth_SQL::ENCRYPT_PASSWORD_BCRYPT));
+
+            $result = $this->Auth_SQL->encryptpassword('123', 'saltstring', Auth_SQL::ENCRYPT_PASSWORD_DEFAULT);
+            $this->assertTrue($this->Auth_SQL->verifyPassword('123', $result, Auth_SQL::ENCRYPT_PASSWORD_DEFAULT));
+        }
+    }
+
+    function test_setpassword()
+    {
+        $db =& DB::getInstance();
+
+        $this->Auth_SQL->setParam(array('hash_type' => Auth_SQL::ENCRYPT_SHA1_HARDENED));
+        $this->Auth_SQL->setpassword(null, '123');
+        $qid = $db->query("
+            SELECT userpass
+            FROM test_user_tbl
+        ");
+        list($pass) = mysql_fetch_row($qid);
+        $this->assertEquals('1d086fcae3dd941e0f1371148502d03e96ab536f', $pass);
+    }
+
+    function test_resetpassword()
+    {
+        $result = $this->Auth_SQL->resetpassword(1, 'Because this is a test.');
+        $this->assertInternalType('array', $result);
+
+    }
+
+//     function test_inclearancezone()
+//     {
+//         $result = $this->Auth_SQL->inclearancezone(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function test_requireaccessclearance()
+//     {
+//         $result = $this->Auth_SQL->requireaccessclearance(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+
+}
+
Index: tags/2.2.3/tests/Auth_FileTest.php
===================================================================
--- tags/2.2.3/tests/Auth_FileTest.php	(revision 632)
+++ tags/2.2.3/tests/Auth_FileTest.php	(revision 632)
@@ -0,0 +1,121 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/Auth_File.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+require_once 'PHPUnit.php';
+class Auth_FileTest extends PHPUnit_TestCase {
+
+    var $Auth_File;
+
+    function Auth_FileTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Auth_File.inc.php';
+        $this->Auth_File =& new Auth_File('test_auth');
+        $this->Auth_File->setParam(array('htpasswd_file' => dirname(__FILE__) . '/_test_htpasswd'));
+    }
+
+    function tearDown()
+    {
+        unset($this->Auth_File);
+    }
+
+    function test_authenticate()
+    {
+        $true = $this->Auth_File->authenticate('testuser', 'testpass');
+        $this->assertTrue($true, 'testuser not authorized with good password.');
+
+        $false = $this->Auth_File->authenticate('testuser', 'wrongpass');
+        $this->assertFalse($false, 'testuser not blocked with bad password.');
+    }
+
+    function test_login()
+    {
+        $result = $this->Auth_File->login('testuser', 'testpass');
+        $this->assertTrue($result, 'testuser login failed.');
+        $this->assertTrue($_SESSION['_auth_file'][$this->Auth_File->_ns]['authenticated'], 'testuser authentication not found in session.');
+    }
+
+    function test_clear()
+    {
+        $result = $this->Auth_File->login('testuser', 'testpass');
+        $this->Auth_File->clear();
+        $this->assertFalse($_SESSION['_auth_file'][$this->Auth_File->_ns]['authenticated'], 'testuser authentication not false in session.');
+    }
+
+    function test_isloggedin()
+    {
+        $this->Auth_File->login('testuser', 'testpass');
+        $true = $this->Auth_File->isloggedin();
+        $this->assertTrue($true, 'testuser not logged in but should be.');
+
+        $this->Auth_File->clear();
+        $false = $this->Auth_File->isloggedin();
+        $this->assertFalse($false, 'testuser is logged in but shouldn\'t be.');
+    }
+
+//     function test_isadmin()
+//     {
+//         $this->Auth_File->login('testuser', 'testpass');
+//         $true = $this->Auth_File->isadmin();
+//         $this->assertTrue($true);
+//     }
+//
+//     function test_requireadminlogin()
+//     {
+//         $result = $this->Auth_File->requireadminlogin(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function test__encrypt()
+//     {
+//         $result = $this->Auth_File->_encrypt(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function test__salt()
+//     {
+//         $result = $this->Auth_File->_salt(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('Auth_FileTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
Index: tags/2.2.3/tests/AuthorizeNetTest.php
===================================================================
--- tags/2.2.3/tests/AuthorizeNetTest.php	(revision 632)
+++ tags/2.2.3/tests/AuthorizeNetTest.php	(revision 632)
@@ -0,0 +1,119 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/AuthorizeNet.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class AuthorizeNetTest extends PHPUnit_Framework_TestCase {
+
+    var $AuthorizeNet;
+    
+    static $shared_session;
+    
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/AuthorizeNet.inc.php';
+
+        $this->AuthorizeNet = new AuthorizeNet();
+        $this->AuthorizeNet->setParam(array(
+            'x_login' => 'myaccount',
+            'x_test_request' => 'true',
+            'x_first_name' => 'john',
+            'x_last_name' => 'doe',
+            'x_amount' => '1.20',
+            'x_card_num' => '4111111111111111',
+            'x_card_code' => '123',
+            'x_exp_date' => '042008',
+            'x_invoice_num' => '100',
+            'x_address' => '10 rue levouvÃ©',
+            'x_city' => 'somecity',
+            'x_state' => 'ca',
+            'x_zip' => '75010',
+        ));
+        $_SESSION = AuthorizeNetTest::$shared_session;
+    }
+
+    function tearDown()
+    {
+        unset($this->AuthorizeNet);
+        AuthorizeNetTest::$shared_session = $_SESSION;
+    }
+
+    function test_setparam()
+    {
+        $this->AuthorizeNet->setparam(array('x_Login' => 'myaccount2'));
+        $this->assertEquals('myaccount2', $this->AuthorizeNet->getParam('x_Login'));
+    }
+
+    function test_getparam()
+    {
+        $result = $this->AuthorizeNet->getparam('x_card_num');
+        $expected = '4111111111111111';
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_process()
+    {
+        $result_code = $this->AuthorizeNet->process();
+
+        $this->assertTrue(false != $result_code, 'Processing did not return valid response. Do you have net access?');
+        $this->assertEquals('3', $result_code);
+    }
+
+    function test_getresult()
+    {
+        $result_code = $this->AuthorizeNet->process();
+        $result_array = $this->AuthorizeNet->getResult();
+        $this->assertTrue(false != $result_code, 'Processing did not return valid response. Do you have net access?');
+        $this->assertTrue(isset($result_array['x_response_reason_code']) && '13' == $result_array['x_response_reason_code'], 'Server resonse did not include x_response.');
+    }
+
+//     function test_validmd5hash()
+//     {
+//         $this->AuthorizeNet->process();
+//         $result = $this->AuthorizeNet->validmd5hash();
+//         $this->assertTrue($result, 'Invalid md5 hash returned from server.');
+//     }
+
+//     function test_reset()
+//     {
+//         $result = $this->AuthorizeNet->reset(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function test__processresult()
+//     {
+//         $result = $this->AuthorizeNet->_processresult(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+
+}
Index: tags/2.2.3/tests/CSSTest.php
===================================================================
--- tags/2.2.3/tests/CSSTest.php	(revision 632)
+++ tags/2.2.3/tests/CSSTest.php	(revision 632)
@@ -0,0 +1,82 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/CSS.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class CSSTest extends PHPUnit_Framework_TestCase {
+
+    var $CSS;
+    var $test_css_file;
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        $this->test_css_file = dirname(__FILE__) . '/../css/codebase.inc.css';
+        require_once '../lib/CSS.inc.php';
+        $this->CSS = new CSS();
+        $this->CSS->setFile($this->test_css_file);
+    }
+
+    function tearDown()
+    {
+        unset($this->CSS);
+    }
+
+    function test_setparam()
+    {
+        $this->CSS->setparam(array('asdf' => 123));
+    }
+
+    function test_getparam()
+    {
+        $this->CSS->getparam('asdf');
+    }
+
+    function test_setfile()
+    {
+        $this->CSS->setfile(dirname(__FILE__) . '/../css/codebase.inc.css');
+    }
+
+    function test_headers()
+    {
+        ob_start();
+        $this->CSS->headers();
+        $result = ob_get_clean();
+    }
+
+    function test_output()
+    {
+        ob_start();
+        $this->CSS->output();
+        $result = ob_get_clean();
+        $this->assertEquals(file_get_contents($this->test_css_file), $result);
+    }
+
+}
Index: tags/2.2.3/tests/CacheTest.php
===================================================================
--- tags/2.2.3/tests/CacheTest.php	(revision 632)
+++ tags/2.2.3/tests/CacheTest.php	(revision 632)
@@ -0,0 +1,106 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/Cache.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class CacheTest extends PHPUnit_Framework_TestCase {
+
+    var $Cache;
+
+    function CacheTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Cache.inc.php';
+        $this->Cache =& new Cache(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->Cache);
+    }
+
+    function test_getinstance()
+    {
+        $result = $this->Cache->getinstance(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setparam()
+    {
+        $result = $this->Cache->setparam(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getparam()
+    {
+        $result = $this->Cache->getparam(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_putcache()
+    {
+        $result = $this->Cache->putcache(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getcache()
+    {
+        $result = $this->Cache->getcache(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_iscached()
+    {
+        $result = $this->Cache->iscached(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_breakcache()
+    {
+        $result = $this->Cache->breakcache(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/DBSessionHandlerTest.php
===================================================================
--- tags/2.2.3/tests/DBSessionHandlerTest.php	(revision 632)
+++ tags/2.2.3/tests/DBSessionHandlerTest.php	(revision 632)
@@ -0,0 +1,86 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/DBSessionHandler.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class DBSessionHandlerTest extends PHPUnit_Framework_TestCase {
+
+    var $DBSessionHandler;
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/DBSessionHandler.inc.php';
+        session_write_close();
+        $this->DBSessionHandler = new DBSessionHandler($app->db, array(
+            'db_table' => 'session_tbl',
+            'create_table' => true,
+        ));
+    }
+
+    function tearDown()
+    {
+        unset($this->DBSessionHandler);
+    }
+
+    function test_DBsessionopen()
+    {
+        $this->DBSessionHandler->DBsessionopen('', '');
+    }
+
+    function test_DBsessionclose()
+    {
+        $this->DBSessionHandler->DBsessionclose();
+    }
+
+    function test_DBsessionread()
+    {
+        $this->DBSessionHandler->DBsessionread(session_id());
+    }
+
+    function test_DBsessionwrite()
+    {
+        if (!isset($_SESSION)) {
+            $_SESSION = array();
+        }
+        $this->DBSessionHandler->DBsessionwrite(session_id(), serialize($_SESSION));
+    }
+
+    function test_DBsessiondestroy()
+    {
+        $this->DBSessionHandler->DBsessiondestroy(session_id());
+    }
+
+    function test_DBsessiongarbage()
+    {
+        $this->DBSessionHandler->DBsessiongarbage(0);
+    }
+
+}
Index: tags/2.2.3/tests/EmailTest.php
===================================================================
--- tags/2.2.3/tests/EmailTest.php	(revision 632)
+++ tags/2.2.3/tests/EmailTest.php	(revision 632)
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/Email.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-12-01
+ */
+
+class EmailTest extends PHPUnit_Framework_TestCase {
+
+    var $Email;
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Email.inc.php';
+        $this->Email = new Email();
+    }
+
+    function tearDown()
+    {
+        unset($this->Email);
+    }
+
+//     function testsetparam()
+//     {
+//         $result   = $this->Email->setparam(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function testgetparam()
+//     {
+//         $result   = $this->Email->getparam(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function testsettemplate()
+//     {
+//         $result   = $this->Email->settemplate(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function testsetstring()
+//     {
+//         $result   = $this->Email->setstring(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+//     function testreplace()
+//     {
+//         $result   = $this->Email->replace(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $this->assertEquals($expected, $result);
+//     }
+//
+    function testsend()
+    {
+
+        $this->Email = new Email(array(
+            'to' => 'Rob Recipient <to@example.com>',
+            'from' => 'Sam Sender <from@example.com>',
+            'subject' => 'EmailTest',
+            'headers' => array(
+                'CC' => 'This header removed in REDIRECT sandbox mode <remove-me@example.com>',
+                'X-Hello' => 'Hi there',
+            ),
+            'sandbox_mode' => Email::SANDBOX_MODE_REDIRECT,
+            'sandbox_to_addr' => 'quinn@strangecode.com',
+        ));
+        $this->Email->setString('This is a {TEST}');
+        $this->Email->replace(array(
+            'test' => 'â you guessed it â a test!'
+        ));
+        $result   = $this->Email->send();
+        $expected = true;
+        $this->assertEquals($expected, $result);
+    }
+
+    function testvalidemail()
+    {
+        $testpool = file('_email_test_addresses.txt');
+
+        $gc = 0;
+        $bc = 0;
+        $good = array();
+        $bad = array();
+        if (is_array($testpool) && !empty($testpool)) {
+            foreach ($testpool as $a) {
+                if ($this->Email->validEmail($a)) {
+                    $good[] = trim($a);
+                    $gc++;
+                } else {
+                    $bad[] = trim($a);
+                    $bc++;
+                }
+            }
+        }
+        $this->assertEquals(45, $gc, 'Unexpected number of valid emails found.');
+        $this->assertEquals(45, $bc, 'Unexpected number of invalid emails found.');
+        // echo "Good addresses: $gc\n";
+        // echo "Bad addresses: $bc\n";
+        // echo join("\n", $bad);
+        // echo "\n";
+    }
+
+}
Index: tags/2.2.3/tests/FormValidatorTest.php
===================================================================
--- tags/2.2.3/tests/FormValidatorTest.php	(revision 632)
+++ tags/2.2.3/tests/FormValidatorTest.php	(revision 632)
@@ -0,0 +1,290 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/FormValidator.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class FormValidatorTest extends PHPUnit_Framework_TestCase {
+
+    var $FormValidator;
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/FormValidator.inc.php';
+        $this->FormValidator = new FormValidator();
+    }
+
+    function tearDown()
+    {
+        unset($this->FormValidator);
+    }
+
+    function test_geterrorlist()
+    {
+        $this->FormValidator->addError('some_field', _("This is an error."), MSG_ERR, __FILE__, __LINE__);
+        $err_list = $this->FormValidator->geterrorlist();
+        $this->assertTrue($err_list[0]['message'] === _("This is an error."));
+    }
+
+    function test_adderror()
+    {
+        $this->FormValidator->addError('some_field', _("This is an error."), MSG_ERR, __FILE__, __LINE__);
+        $this->assertTrue($this->FormValidator->errors[0]['message'] === _("This is an error."));
+    }
+
+    function test_anyerrors()
+    {
+        $result = $this->FormValidator->anyerrors();
+        $this->assertFalse($result);
+        $this->FormValidator->addError('some_field', _("This is an error."), MSG_ERR, __FILE__, __LINE__);
+        $result = $this->FormValidator->anyerrors();
+        $this->assertTrue($result);
+    }
+
+    function test_reseterrorlist()
+    {
+        $this->FormValidator->addError('some_field', _("This is an error."), MSG_ERR, __FILE__, __LINE__);
+        $this->FormValidator->reseterrorlist();
+        $this->assertFalse($this->FormValidator->anyerrors());
+    }
+
+    function test_printerrormessages()
+    {
+        $this->FormValidator->addError('some_field', _("This is an error."), MSG_ERR, __FILE__, __LINE__);
+        ob_start();
+        $this->FormValidator->printerrormessages();
+        $result = ob_get_clean();
+        $this->assertContains('This is an error', $result);
+    }
+
+    function test_err()
+    {
+        $this->FormValidator->addError('some_field', _("This is an error."), MSG_ERR, __FILE__, __LINE__);
+        ob_start();
+        $this->FormValidator->err('some_field', 'printthis');
+        $result = ob_get_clean();
+        $this->assertContains('printthis', $result);
+    }
+
+    function test_notempty()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = 'a non empty string';
+        $this->FormValidator->isempty('some_field', _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = '';
+        $this->FormValidator->isempty('some_field', _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_isempty()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = 'a non empty string';
+        $this->FormValidator->isempty('some_field', _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = '';
+        $this->FormValidator->isempty('some_field', _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_isstring()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = 'this is a string';
+        $this->FormValidator->isstring('some_field', _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = 12.3248; // not a string.
+        $this->FormValidator->isstring('some_field', _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_isnumber()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = '1234.453';
+        $this->FormValidator->isnumber('some_field', _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = 'not a number';
+        $this->FormValidator->isnumber('some_field', _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_isinteger()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = '1234';
+        $this->FormValidator->isinteger('some_field', _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = '1234.1';
+        $this->FormValidator->isinteger('some_field', _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_isfloat()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = '123.1234';
+        $this->FormValidator->isfloat('some_field', _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = 'some falsity';
+        $this->FormValidator->isfloat('some_field', _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_isarray()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = array('asdf', 123);
+        $this->FormValidator->isarray('some_field', _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = 'some falsity';
+        $this->FormValidator->isarray('some_field', _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_checkregex()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = '1234abcd';
+        $this->FormValidator->checkregex('some_field', '/\d{4}[a-d]{4}/', true, _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This should not generate any error.
+        $_POST['some_field'] = 'no digits here';
+        $this->FormValidator->checkregex('some_field', '/\d/', false, _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = 'oops, a d1git';
+        $this->FormValidator->checkregex('some_field', '/\d/', false, _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_stringlength()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = 'some truth';
+        $this->FormValidator->stringLength('some_field', 0, 255, _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = 'some falsity';
+        $this->FormValidator->stringLength('some_field', 0, 4, _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_numericrange()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = '12';
+        $this->FormValidator->numericrange('some_field', 3, 22, _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = '12';
+        $this->FormValidator->numericrange('some_field', 300, 2200, _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_validateemail()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = 'Quinn the Kook <quinn@strangecode.com>';
+        $this->FormValidator->validateemail('some_field', _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+                
+        // This one is an error!
+        $_POST['some_field'] = 'quinn@kook.com.';
+        $this->FormValidator->validateemail('some_field', _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_validatephone()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = '+1 (530) 555-1212';
+        $this->FormValidator->validatephone('some_field', _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = 'd321/654*9875 ';
+        $this->FormValidator->validatephone('some_field', _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_validatestrdate()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = 'next tuesday';
+        $this->FormValidator->validatestrdate('some_field', _("Error message"));
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = 'in a galaxy far far away';
+        $this->FormValidator->validatestrdate('some_field', _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_validateccnumber()
+    {
+        // This should not generate any error.
+        $_POST['some_field'] = '2323-2005-7766-3554';
+        $this->FormValidator->validateccnumber('some_field', null);
+        $this->assertFalse($this->FormValidator->anyerrors());
+        
+        // This one is an error!
+        $_POST['some_field'] = '1234 1234 1234 1234';
+        $this->FormValidator->validateccnumber('some_field', null);
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+    function test_fileuploaded()
+    {
+        // This one is an error!
+        $this->FormValidator->fileUploaded('this-file-does-not-exist', _("Error message"));
+        $this->assertTrue($this->FormValidator->anyerrors());
+    }
+
+}
Index: tags/2.2.3/tests/Google_APITest.php
===================================================================
--- tags/2.2.3/tests/Google_APITest.php	(revision 632)
+++ tags/2.2.3/tests/Google_APITest.php	(revision 632)
@@ -0,0 +1,92 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/GoogleAPI.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class GoogleAPITest extends PHPUnit_Framework_TestCase {
+
+    var $GoogleAPI;
+
+    function GoogleAPITest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/GoogleAPI.inc.php';
+        $this->GoogleAPI =& new GoogleAPI(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->GoogleAPI);
+    }
+
+    function test_getcachedpage()
+    {
+        $result = $this->GoogleAPI->getcachedpage(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getspellingsuggestion()
+    {
+        $result = $this->GoogleAPI->getspellingsuggestion(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_search()
+    {
+        $result = $this->GoogleAPI->search(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test__performapicall()
+    {
+        $result = $this->GoogleAPI->_performapicall(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getfault()
+    {
+        $result = $this->GoogleAPI->getfault(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/HierarchyTest.php
===================================================================
--- tags/2.2.3/tests/HierarchyTest.php	(revision 632)
+++ tags/2.2.3/tests/HierarchyTest.php	(revision 632)
@@ -0,0 +1,176 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/Hierarchy.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class HierarchyTest extends PHPUnit_Framework_TestCase {
+
+    var $Hierarchy;
+
+    function HierarchyTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Hierarchy.inc.php';
+        $this->Hierarchy =& new Hierarchy(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->Hierarchy);
+    }
+
+    function test_currentnode()
+    {
+        $result = $this->Hierarchy->currentnode(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_tostringid()
+    {
+        $result = $this->Hierarchy->tostringid(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_toarrayid()
+    {
+        $result = $this->Hierarchy->toarrayid(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_insertnode()
+    {
+        $result = $this->Hierarchy->insertnode(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_deletenode()
+    {
+        $result = $this->Hierarchy->deletenode(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_movenode()
+    {
+        $result = $this->Hierarchy->movenode(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getparents()
+    {
+        $result = $this->Hierarchy->getparents(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getnode()
+    {
+        $result = $this->Hierarchy->getnode(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getchildren()
+    {
+        $result = $this->Hierarchy->getchildren(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getnumberchildren()
+    {
+        $result = $this->Hierarchy->getnumberchildren(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_isleaf()
+    {
+        $result = $this->Hierarchy->isleaf(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_isancestor()
+    {
+        $result = $this->Hierarchy->isancestor(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getallancestors()
+    {
+        $result = $this->Hierarchy->getallancestors(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_nodeexists()
+    {
+        $result = $this->Hierarchy->nodeexists(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getnodelist()
+    {
+        $result = $this->Hierarchy->getnodelist(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_rebuildsubnodeqty()
+    {
+        $result = $this->Hierarchy->rebuildsubnodeqty(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setsubnodeqtytoparents()
+    {
+        $result = $this->Hierarchy->setsubnodeqtytoparents(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/ImageThumbTest.php
===================================================================
--- tags/2.2.3/tests/ImageThumbTest.php	(revision 632)
+++ tags/2.2.3/tests/ImageThumbTest.php	(revision 632)
@@ -0,0 +1,120 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/ImageThumb.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class ImageThumbTest extends PHPUnit_Framework_TestCase {
+
+    var $ImageThumb;
+
+    function ImageThumbTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/ImageThumb.inc.php';
+        $this->ImageThumb =& new ImageThumb(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->ImageThumb);
+    }
+
+    function test_setsourcedirectory()
+    {
+        $result = $this->ImageThumb->setsourcedirectory(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setspec()
+    {
+        $result = $this->ImageThumb->setspec(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_createdestdirs()
+    {
+        $result = $this->ImageThumb->createdestdirs(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_processfile()
+    {
+        $result = $this->ImageThumb->processfile(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_processall()
+    {
+        $result = $this->ImageThumb->processall(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_deletethumbs()
+    {
+        $result = $this->ImageThumb->deletethumbs(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_deleteoriginal()
+    {
+        $result = $this->ImageThumb->deleteoriginal(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_exists()
+    {
+        $result = $this->ImageThumb->exists(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_validfileextension()
+    {
+        $result = $this->ImageThumb->validfileextension(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/LockTest.php
===================================================================
--- tags/2.2.3/tests/LockTest.php	(revision 632)
+++ tags/2.2.3/tests/LockTest.php	(revision 632)
@@ -0,0 +1,212 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/Lock.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class LockTest extends PHPUnit_Framework_TestCase {
+
+    var $Lock;
+    var $Auth_SQL;
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Lock.inc.php';
+        require_once '../lib/Auth_SQL.inc.php';
+
+        $this->Auth_SQL = new Auth_SQL('test');
+        $this->Auth_SQL->setParam(array(
+            'db_table'          => 'test_user_tbl',
+            'db_primary_key'    => 'user_id',
+            'db_login_table'    => 'test_login_tbl',
+            'login_url'         => '/login.php',
+            'blocking'          => true
+        ));
+
+        // Use fresh user table.
+        $this->Auth_SQL->initDB(true);
+
+        // Insert test data.
+        $db =& DB::getInstance();
+        $db->query("
+            INSERT INTO test_user_tbl (
+                username,
+                userpass,
+                first_name,
+                last_name,
+                email
+            ) VALUES (
+                'testuser',
+                '" . $this->Auth_SQL->encryptPassword('testpass') . "',
+                'John',
+                'Doe',
+                'john@example.com'
+            )
+        ");
+
+        // Sessions require client IP addr.
+        $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+
+        $this->Auth_SQL->login('testuser', 'testpass');
+
+        if (!$this->Auth_SQL->isLoggedIn()) {
+            trigger_error("User login failed...tests canceled.", E_USER_ERROR);
+        }
+
+        $this->Lock =& Lock::getInstance($this->Auth_SQL);
+        $this->Lock->setParam(array('db_table' => 'test_lock_tbl'));
+
+        // Use fresh lock table.
+        $this->Lock->initDB(true);
+    }
+
+    function tearDown()
+    {
+        $db =& DB::getInstance();
+
+        unset($this->Lock);
+        unset($this->Auth_SQL);
+        $db->query("DROP TABLE IF EXISTS test_user_tbl");
+        $db->query("DROP TABLE IF EXISTS test_login_tbl");
+        $db->query("DROP TABLE IF EXISTS test_lock_tbl");
+    }
+
+    function test_getinstance()
+    {
+        $result = Lock::getinstance($this->Auth_SQL);
+        $this->assertTrue($result && is_a($result, 'Lock'));
+    }
+
+    function test_setparam()
+    {
+        $this->Lock->setparam(array('var'=>'val'));
+    }
+
+    function test_getparam()
+    {
+        $this->Lock->getparam('var');
+    }
+
+    function test_select()
+    {
+        $this->Lock->set('test_user_tbl', 'user_id', '1');
+        $result = $this->Lock->select('test_user_tbl', 'user_id', '1');
+        $this->assertTrue($result);
+    }
+
+    function test_islocked()
+    {
+        $this->Lock->select('test_user_tbl', 'user_id', '1');
+
+        $this->Lock->set('test_user_tbl', 'user_id', '1');
+        $result = $this->Lock->islocked();
+        $this->assertTrue($result, 'Lock was not set.');
+
+        $this->Lock->remove();
+        $this->Lock->select('test_user_tbl', 'user_id', '1');
+        $result = $this->Lock->islocked();
+        $this->assertFalse($result, 'Lock was not removed.');
+    }
+
+    function test_ismine()
+    {
+        $this->Lock->set('test_user_tbl', 'user_id', '1');
+        $this->Lock->select('test_user_tbl', 'user_id', '1');
+        $result = $this->Lock->ismine();
+        $this->assertTrue($result);
+    }
+
+    function test_set()
+    {
+        $this->Lock->select('test_user_tbl', 'user_id', '1');
+        $this->Lock->remove();
+        $this->Lock->set('test_user_tbl', 'user_id', '1');
+        $this->Lock->select('test_user_tbl', 'user_id', '1');
+        $result = $this->Lock->islocked();
+        $this->assertTrue($result);
+    }
+
+    function test_remove()
+    {
+        $this->Lock->select('test_user_tbl', 'user_id', '1');
+        $this->Lock->set('test_user_tbl', 'user_id', '1');
+        $this->Lock->remove();
+        $this->Lock->select('test_user_tbl', 'user_id', '1');
+        $result = $this->Lock->islocked();
+        $this->assertFalse($result);
+    }
+
+    function test_removeall()
+    {
+        $this->Lock->select('test_user_tbl', 'user_id', '1');
+        $this->Lock->set('test_user_tbl', 'user_id', '1');
+        $this->Lock->removeall();
+        $this->Lock->select('test_user_tbl', 'user_id', '1');
+        $result = $this->Lock->islocked();
+        $this->assertFalse($result);
+    }
+
+    function test__auto_timeout()
+    {
+        $this->Lock->_auto_timeout();
+    }
+
+    function test_getid()
+    {
+        $this->Lock->select('test_user_tbl', 'user_id', '1');
+        $this->Lock->set('test_user_tbl', 'user_id', '1');
+        $result = $this->Lock->getid();
+        $this->assertTrue('' != $result);
+        $this->assertTrue(is_numeric($result));
+    }
+
+    function test_gettitle()
+    {
+        $this->Lock->select('test_user_tbl', 'user_id', '1');
+        $this->Lock->set('test_user_tbl', 'user_id', '1', 'userlock');
+        $result = $this->Lock->gettitle();
+        $this->assertEquals('userlock', $result);
+    }
+
+    function test_geteditor()
+    {
+        $this->Lock->select('test_user_tbl', 'user_id', '1');
+        $this->Lock->set('test_user_tbl', 'user_id', '1', 'userlock');
+        $result = $this->Lock->geteditor();
+        $this->assertEquals('testuser', $result);
+    }
+
+    function test_getsecondselapsed()
+    {
+        $result = $this->Lock->getsecondselapsed();
+        $this->assertInternalType('integer', $result);
+    }
+
+}
Index: tags/2.2.3/tests/MCVETest.php
===================================================================
--- tags/2.2.3/tests/MCVETest.php	(revision 632)
+++ tags/2.2.3/tests/MCVETest.php	(revision 632)
@@ -0,0 +1,92 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/MCVE.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class MCVETest extends PHPUnit_Framework_TestCase {
+
+    var $MCVE;
+
+    function MCVETest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/MCVE.inc.php';
+        $this->MCVE =& new MCVE(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->MCVE);
+    }
+
+    function test__connect()
+    {
+        $result = $this->MCVE->_connect(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_begintrans()
+    {
+        $result = $this->MCVE->begintrans(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_transparam()
+    {
+        $result = $this->MCVE->transparam(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_sendtrans()
+    {
+        $result = $this->MCVE->sendtrans(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_disconnect()
+    {
+        $result = $this->MCVE->disconnect(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/NavTest.php
===================================================================
--- tags/2.2.3/tests/NavTest.php	(revision 632)
+++ tags/2.2.3/tests/NavTest.php	(revision 632)
@@ -0,0 +1,134 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/Navigation.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class NavigationTest extends PHPUnit_Framework_TestCase {
+
+    var $Navigation;
+
+    function NavigationTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Navigation.inc.php';
+        $this->Navigation =& new Navigation(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->Navigation);
+    }
+
+    function test_addpage()
+    {
+        $result = $this->Navigation->addpage(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setfeature()
+    {
+        $result = $this->Navigation->setfeature(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_clearpath()
+    {
+        $result = $this->Navigation->clearpath(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getfeature()
+    {
+        $result = $this->Navigation->getfeature(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_gettitle()
+    {
+        $result = $this->Navigation->gettitle(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printtitle()
+    {
+        $result = $this->Navigation->printtitle(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getpath()
+    {
+        $result = $this->Navigation->getpath(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printpath()
+    {
+        $result = $this->Navigation->printpath(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getbreadcrumbs()
+    {
+        $result = $this->Navigation->getbreadcrumbs(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printbreadcrumbs()
+    {
+        $result = $this->Navigation->printbreadcrumbs(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_currentpage()
+    {
+        $result = $this->Navigation->currentpage(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/PEditTest.php
===================================================================
--- tags/2.2.3/tests/PEditTest.php	(revision 632)
+++ tags/2.2.3/tests/PEditTest.php	(revision 632)
@@ -0,0 +1,155 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/PEdit.inc.php
+ *
+ * T\he method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class PEditTest extends PHPUnit_Framework_TestCase {
+
+    var $PEdit;
+
+    function PEditTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/PEdit.inc.php';
+        $this->PEdit =& new PEdit(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->PEdit);
+    }
+
+    function test_set()
+    {
+        $result = $this->PEdit->set(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setcheckbox()
+    {
+        $result = $this->PEdit->setcheckbox(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_displaymode()
+    {
+        $result = $this->PEdit->displaymode(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printversions()
+    {
+        $result = $this->PEdit->printversions(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getcontent()
+    {
+        $result = $this->PEdit->getcontent(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printcontent()
+    {
+        $result = $this->PEdit->printcontent(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printform()
+    {
+        $result = $this->PEdit->printform(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printallforms()
+    {
+        $result = $this->PEdit->printallforms(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_formbegin()
+    {
+        $result = $this->PEdit->formbegin(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_formend()
+    {
+        $result = $this->PEdit->formend(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test__writedata()
+    {
+        $result = $this->PEdit->_writedata(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test__createversion()
+    {
+        $result = $this->PEdit->_createversion(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test__getversions()
+    {
+        $result = $this->PEdit->_getversions(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test__restoreversion()
+    {
+        $result = $this->PEdit->_restoreversion(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/PageNumbersTest.php
===================================================================
--- tags/2.2.3/tests/PageNumbersTest.php	(revision 632)
+++ tags/2.2.3/tests/PageNumbersTest.php	(revision 632)
@@ -0,0 +1,134 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/PageNumbers.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class PageNumbersTest extends PHPUnit_Framework_TestCase {
+
+    var $PageNumbers;
+
+    function PageNumbersTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/PageNumbers.inc.php';
+        $this->PageNumbers =& new PageNumbers(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->PageNumbers);
+    }
+
+    function test_setperpage()
+    {
+        $result = $this->PageNumbers->setperpage(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setpagenumber()
+    {
+        $result = $this->PageNumbers->setpagenumber(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_settotalitems()
+    {
+        $result = $this->PageNumbers->settotalitems(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_calculate()
+    {
+        $result = $this->PageNumbers->calculate(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getlimitsql()
+    {
+        $result = $this->PageNumbers->getlimitsql(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printperpagelinks()
+    {
+        $result = $this->PageNumbers->printperpagelinks(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getpagenumurl()
+    {
+        $result = $this->PageNumbers->getpagenumurl(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printpagenumurl()
+    {
+        $result = $this->PageNumbers->printpagenumurl(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getpagenumbersarray()
+    {
+        $result = $this->PageNumbers->getpagenumbersarray(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getpagenumbers()
+    {
+        $result = $this->PageNumbers->getpagenumbers(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printpagenumbers()
+    {
+        $result = $this->PageNumbers->printpagenumbers(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/PageSequenceTest.php
===================================================================
--- tags/2.2.3/tests/PageSequenceTest.php	(revision 632)
+++ tags/2.2.3/tests/PageSequenceTest.php	(revision 632)
@@ -0,0 +1,183 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/PageSequence.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class PageSequenceTest extends PHPUnit_Framework_TestCase {
+
+    var $PageSequence;
+
+    function PageSequenceTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/PageSequence.inc.php';
+        $this->PageSequence =& new PageSequence(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->PageSequence);
+    }
+
+    function test_addstep()
+    {
+        $result = $this->PageSequence->addstep(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setfeature()
+    {
+        $result = $this->PageSequence->setfeature(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getfeature()
+    {
+        $result = $this->PageSequence->getfeature(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setcurrent()
+    {
+        $result = $this->PageSequence->setcurrent(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getid()
+    {
+        $result = $this->PageSequence->getid(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getposition()
+    {
+        $result = $this->PageSequence->getposition(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getrequiredid()
+    {
+        $result = $this->PageSequence->getrequiredid(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getnextid()
+    {
+        $result = $this->PageSequence->getnextid(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_complete()
+    {
+        $result = $this->PageSequence->complete(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printeditlink()
+    {
+        $result = $this->PageSequence->printeditlink(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setdatadefault()
+    {
+        $result = $this->PageSequence->setdatadefault(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getdata()
+    {
+        $result = $this->PageSequence->getdata(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_cleardata()
+    {
+        $result = $this->PageSequence->cleardata(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test__auto_timeout()
+    {
+        $result = $this->PageSequence->_auto_timeout(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_registerdata()
+    {
+        $result = $this->PageSequence->registerdata(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_startover()
+    {
+        $result = $this->PageSequence->startover(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_savedata()
+    {
+        $result = $this->PageSequence->savedata(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_loaddata()
+    {
+        $result = $this->PageSequence->loaddata(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/PayPalTest.php
===================================================================
--- tags/2.2.3/tests/PayPalTest.php	(revision 632)
+++ tags/2.2.3/tests/PayPalTest.php	(revision 632)
@@ -0,0 +1,133 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/PayPal.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class PayPalTest extends PHPUnit_Framework_TestCase {
+
+    var $PayPal;
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/PayPal.inc.php';
+        $this->PayPal = new PayPal();
+
+        // Defaults for purchasing classified postings.
+        $this->PayPal->setButtonDefaults('web_accept', array(
+            'business' => 'paypal@example.com',
+            'item_name' => 'Test Button',
+            'no_shipping' => '1',
+            'no_note' => '1',
+            'currency_code' => 'USD',
+        ));
+    }
+
+    function tearDown()
+    {
+        unset($this->PayPal);
+    }
+
+    function test_setbuttondefaults()
+    {
+        $result = $this->PayPal->setButtonDefaults('web_accept', array(
+            'item_name' => 'Test Button',
+            'no_shipping' => '1',
+            'no_note' => '1',
+            'currency_code' => 'USD',
+        ));
+        $this->assertTrue($result);
+    }
+
+    function test_newbutton()
+    {
+        $result = $this->PayPal->newButton('web_accept', 'testitem', array(
+            'item_name' => 'Test Item',
+            'amount' => 1.23,
+            'custom' => 'customdata'
+        ));
+        $this->assertTrue($result);
+    }
+
+    function test_getlink()
+    {
+        $this->PayPal->newButton('web_accept', 'testitem', array(
+            'item_name' => 'Test Item',
+            'amount' => 1.23,
+            'custom' => 'customdata'
+        ));
+        $result = $this->PayPal->getlink('testitem');
+        //$this->assertTrue(preg_match('/customdata/', $result));
+        $this->assertSame(1, preg_match('/customdata/', $result));
+    }
+
+    function test_printbutton()
+    {
+        $this->PayPal->newButton('web_accept', 'testitem', array(
+            'item_name' => 'Test Item',
+            'amount' => 1.23,
+            'custom' => 'customdata'
+        ));
+        ob_start();
+        $this->PayPal->printbutton('testitem');
+        $result = ob_get_clean();
+        //$this->assertTrue(preg_match('/customdata/', $result));
+        $this->assertSame(1, preg_match('/customdata/', $result));
+        
+    }
+
+    function test_setparam()
+    {
+        $this->PayPal->setparam(array('business' => 'business@example.com'));
+    }
+
+    function test_getparam()
+    {
+        $this->PayPal->setparam(array('business' => 'business@example.com'));
+        $result = $this->PayPal->getparam('business');
+        $this->assertEquals('business@example.com', $result);
+    }
+
+//     function test_incomingipnrequest()
+//     {
+//         $result = $this->PayPal->incomingipnrequest();
+//     }
+//
+//     function test_processipn()
+//     {
+//         $result = $this->PayPal->processipn();
+//     }
+//
+//     function test_verifiedipn()
+//     {
+//         $result = $this->PayPal->verifiedipn();
+//     }
+
+}
Index: tags/2.2.3/tests/PrefsTest.php
===================================================================
--- tags/2.2.3/tests/PrefsTest.php	(revision 632)
+++ tags/2.2.3/tests/PrefsTest.php	(revision 632)
@@ -0,0 +1,115 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/Prefs.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class PrefsTest extends PHPUnit_Framework_TestCase {
+
+    var $Prefs;
+
+    function PrefsTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Prefs.inc.php';
+        $prefs = new Prefs('test', array(
+            'storagetype' => 'session',
+        ));
+    }
+
+    function tearDown()
+    {
+        unset($this->Prefs);
+    }
+
+    function test_setdefault()
+    {
+        $result = $this->Prefs->setdefault(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setvalue()
+    {
+        $result = $this->Prefs->setvalue(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getue()
+    {
+        $result = $this->Prefs->getue(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_exists()
+    {
+        $result = $this->Prefs->exists(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_clearvalue()
+    {
+        $result = $this->Prefs->clearvalue(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_retrieve()
+    {
+        $result = $this->Prefs->retrieve(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_store()
+    {
+        $result = $this->Prefs->store(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_cleanup()
+    {
+        $result = $this->Prefs->cleanup(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/ScriptTimerTest.php
===================================================================
--- tags/2.2.3/tests/ScriptTimerTest.php	(revision 632)
+++ tags/2.2.3/tests/ScriptTimerTest.php	(revision 632)
@@ -0,0 +1,78 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/ScriptTimer.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class ScriptTimerTest extends PHPUnit_Framework_TestCase {
+
+    var $ScriptTimer;
+
+    function ScriptTimerTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/ScriptTimer.inc.php';
+        $this->ScriptTimer =& new ScriptTimer(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->ScriptTimer);
+    }
+
+    function test_start()
+    {
+        $result = $this->ScriptTimer->start(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_stop()
+    {
+        $result = $this->ScriptTimer->stop(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_gettime()
+    {
+        $result = $this->ScriptTimer->gettime(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/SortOrderTest.php
===================================================================
--- tags/2.2.3/tests/SortOrderTest.php	(revision 632)
+++ tags/2.2.3/tests/SortOrderTest.php	(revision 632)
@@ -0,0 +1,92 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/SortOrder.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class SortOrderTest extends PHPUnit_Framework_TestCase {
+
+    var $SortOrder;
+
+    function SortOrderTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/SortOrder.inc.php';
+        $this->SortOrder =& new SortOrder(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->SortOrder);
+    }
+
+    function test_setcolumn()
+    {
+        $result = $this->SortOrder->setcolumn(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setdefault()
+    {
+        $result = $this->SortOrder->setdefault(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_set()
+    {
+        $result = $this->SortOrder->set(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getsortordersql()
+    {
+        $result = $this->SortOrder->getsortordersql(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printsortheader()
+    {
+        $result = $this->SortOrder->printsortheader(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/SpellCheckTest.php
===================================================================
--- tags/2.2.3/tests/SpellCheckTest.php	(revision 632)
+++ tags/2.2.3/tests/SpellCheckTest.php	(revision 632)
@@ -0,0 +1,155 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/SpellCheck.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class SpellCheckTest extends PHPUnit_Framework_TestCase {
+
+    var $SpellCheck;
+
+    function SpellCheckTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/SpellCheck.inc.php';
+        $this->SpellCheck =& new SpellCheck(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->SpellCheck);
+    }
+
+    function test_setparam()
+    {
+        $result = $this->SpellCheck->setparam(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getparam()
+    {
+        $result = $this->SpellCheck->getparam(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_anyerrors()
+    {
+        $result = $this->SpellCheck->anyerrors(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_reseterrorlist()
+    {
+        $result = $this->SpellCheck->reseterrorlist(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_check()
+    {
+        $result = $this->SpellCheck->check(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_suggest()
+    {
+        $result = $this->SpellCheck->suggest(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_add()
+    {
+        $result = $this->SpellCheck->add(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_save()
+    {
+        $result = $this->SpellCheck->save(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getstringsuggestions()
+    {
+        $result = $this->SpellCheck->getstringsuggestions(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_checkstring()
+    {
+        $result = $this->SpellCheck->checkstring(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getstringhighlighted()
+    {
+        $result = $this->SpellCheck->getstringhighlighted(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printcorrectionform()
+    {
+        $result = $this->SpellCheck->printcorrectionform(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_anyformcorrections()
+    {
+        $result = $this->SpellCheck->anyformcorrections(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_applyformcorrections()
+    {
+        $result = $this->SpellCheck->applyformcorrections(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/TemplateGlueTest.php
===================================================================
--- tags/2.2.3/tests/TemplateGlueTest.php	(revision 632)
+++ tags/2.2.3/tests/TemplateGlueTest.php	(revision 632)
@@ -0,0 +1,57 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/TemplateGlue.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class TemplateGlueTest extends PHPUnit_Framework_TestCase {
+
+    var $TemplateGlue;
+
+    function TemplateGlueTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/TemplateGlue.inc.php';
+        $this->TemplateGlue =& new TemplateGlue(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->TemplateGlue);
+    }
+
+}
Index: tags/2.2.3/tests/UploadTest.php
===================================================================
--- tags/2.2.3/tests/UploadTest.php	(revision 632)
+++ tags/2.2.3/tests/UploadTest.php	(revision 632)
@@ -0,0 +1,127 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/Upload.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class UploadTest extends PHPUnit_Framework_TestCase {
+
+    var $Upload;
+
+    function UploadTest($name)
+    {
+        $this->PHPUnit_Framework_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Upload.inc.php';
+        $this->Upload =& new Upload(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->Upload);
+    }
+
+    function test_setparam()
+    {
+        $result = $this->Upload->setparam(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getparam()
+    {
+        $result = $this->Upload->getparam(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setuploadpath()
+    {
+        $result = $this->Upload->setuploadpath(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_process()
+    {
+        $result = $this->Upload->process(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_deletefile()
+    {
+        $result = $this->Upload->deletefile(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_movefile()
+    {
+        $result = $this->Upload->movefile(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_exists()
+    {
+        $result = $this->Upload->exists(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_geterrors()
+    {
+        $result = $this->Upload->geterrors(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_cleanfilename()
+    {
+        $result = $this->Upload->cleanfilename(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getfilenameextension()
+    {
+        $result = $this->Upload::getfilenameextension(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
Index: tags/2.2.3/tests/UtilitiesTest.php
===================================================================
--- tags/2.2.3/tests/UtilitiesTest.php	(revision 632)
+++ tags/2.2.3/tests/UtilitiesTest.php	(revision 632)
@@ -0,0 +1,257 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/Utilities.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class UtilitiesTest extends PHPUnit_Framework_TestCase {
+
+    function setUp()
+    {
+        define('_CLI', true);
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Utilities.inc.php';
+        // $this->Utilities =& new Utilities(PARAM);
+    }
+
+    function tearDown()
+    {
+        $app =& App::getInstance();
+        $app->stop();
+    }
+
+    function test_hyperlinkTxt()
+    {
+        $urls = [
+            ' http://www.asdf.com/ ' => ' <a href="http://www.asdf.com/">www.asdf.com</a> ',
+            ' ws://www.asdf.com/ ' => ' <a href="ws://www.asdf.com/">ws://www.asdf.com/</a> ',
+            ' sftp://user@host.tld:22/path/x ' => ' <a href="sftp://user@host.tld:22/path/x">sftp://user@host.tld:22/path/x</a> ',
+            ' www.asdf.com/ ' => ' <a href="http://www.asdf.com/">www.asdf.com</a> ',
+            ' http://asdf/ ' => ' <a href="http://asdf/">asdf</a> ',
+            ' www.asdf.com/? ' => ' <a href="http://www.asdf.com/">www.asdf.com</a>? ',
+            ' www.asdf.com/?x=y+z ' => ' <a href="http://www.asdf.com/?x=y+z">www.asdf.com/?x=y+z</a> ',
+            ' www.asdf.com/?x=y+ ' => ' <a href="http://www.asdf.com/?x=y+">www.asdf.com/?x=y+</a> ',
+            ' www.asdf.com/?x= ' => ' <a href="http://www.asdf.com/?x=">www.asdf.com/?x=</a> ',
+            ' www.asdf.com/?x ' => ' <a href="http://www.asdf.com/?x">www.asdf.com/?x</a> ',
+            ' www.asdf.com/?â€ïž=ð¥ ' => ' <a href="http://www.asdf.com/?â€ïž=ð¥">www.asdf.com/?â€ïž=ð¥</a> ',
+            ' www.asdf.com? ' => ' <a href="http://www.asdf.com">www.asdf.com</a>? ',
+            ' www.asdf.com! ' => ' <a href="http://www.asdf.com">www.asdf.com</a>! ',
+            ' www.asdf.com. ' => ' <a href="http://www.asdf.com">www.asdf.com</a>. ',
+            ' `www.asdf.com` ' => ' `www.asdf.com` ',
+            ' "www.asdf.com" ' => ' "www.asdf.com" ',
+            ' <www.asdf.com> ' => ' <<a href="http://www.asdf.com">www.asdf.com</a>> ',
+            ' <http://www.asdf.com> ' => ' <<a href="http://www.asdf.com">www.asdf.com</a>> ',
+            ' (http://www.asdf.com) ' => ' (<a href="http://www.asdf.com">www.asdf.com</a>) ',
+            ' (URL: http://www.asdf.com#1) ' => ' (URL: <a href="http://www.asdf.com#1">www.asdf.com#1</a>) ',
+            ' <a href="http://www.example.com/">Click Here</a> ' => ' <a href="http://www.example.com/">Click Here</a> ',
+            ' <a href="http://www.example.com/">http://www.example.com/</a> ' => ' <a href="http://www.example.com/">http://www.example.com/</a> ',
+            ' <a href=http://www.example.com/>http://www.example.com/</a> ' => ' <a href=http://www.example.com/>http://www.example.com/</a> ',
+            ' <a href=\'http://www.example.com/\' >http://www.example.com/</a> ' => ' <a href=\'http://www.example.com/\' >http://www.example.com/</a> ',
+            ' http://foo.com/blah_blah ' => ' <a href="http://foo.com/blah_blah">foo.com/blah_blah</a> ',
+            ' http://foo.com/blah_blah/ ' => ' <a href="http://foo.com/blah_blah/">foo.com/blah_blah/</a> ',
+            ' http://foo.com/blah_blah_(wikipedia) ' => ' <a href="http://foo.com/blah_blah_(wikipedia">foo.com/blah_blah_(wikipedia</a>) ',
+            ' http://foo.com/blah_blah_(wikipedia)_(again) ' => ' <a href="http://foo.com/blah_blah_(wikipedia)_(again">foo.com/blah_blah_(wikipedia)_(again</a>) ',
+            ' http://www.example.com/wpstyle/?p=364 ' => ' <a href="http://www.example.com/wpstyle/?p=364">www.example.com/wpstyle/?p=364</a> ',
+            ' https://www.example.com/foo/?bar=baz&inga=42&quux ' => ' <a href="https://www.example.com/foo/?bar=baz&amp;inga=42&amp;quux">www.example.com/foo/?bar=baz&inga=42&quux</a> ',
+            ' http://âªdf.ws/123 ' => ' <a href="http://âªdf.ws/123">âªdf.ws/123</a> ',
+            ' http://userid:password@example.com:8080 ' => ' <a href="http://userid:password@example.com:8080">userid:password@example.com:8080</a> ',
+            ' http://userid:password@example.com:8080/ ' => ' <a href="http://userid:password@example.com:8080/">userid:password@example.com:8080</a> ',
+            ' http://userid@example.com ' => ' <a href="http://userid@example.com">userid@example.com</a> ',
+            ' http://userid@example.com/ ' => ' <a href="http://userid@example.com/">userid@example.com</a> ',
+            ' http://userid@example.com:8080 ' => ' <a href="http://userid@example.com:8080">userid@example.com:8080</a> ',
+            ' http://userid@example.com:8080/ ' => ' <a href="http://userid@example.com:8080/">userid@example.com:8080</a> ',
+            ' http://userid:password@example.com ' => ' <a href="http://userid:password@example.com">userid:password@example.com</a> ',
+            ' http://userid:password@example.com/ ' => ' <a href="http://userid:password@example.com/">userid:password@example.com</a> ',
+            ' http://142.42.1.1/ ' => ' <a href="http://142.42.1.1/">142.42.1.1</a> ',
+            ' http://142.42.1.1:8080/ ' => ' <a href="http://142.42.1.1:8080/">142.42.1.1:8080</a> ',
+            ' http://â¡.ws/äš¹ ' => ' <a href="http://â¡.ws/äš¹">â¡.ws/äš¹</a> ',
+            ' http://â.ws ' => ' <a href="http://â.ws">â.ws</a> ',
+            ' http://â.ws/ ' => ' <a href="http://â.ws/">â.ws</a> ',
+            ' http://foo.com/blah_(wikipedia)#cite-1 ' => ' <a href="http://foo.com/blah_(wikipedia)#cite-1">foo.com/blah_(wikipedia)#cite-1</a> ',
+            ' http://foo.com/blah_(wikipedia)_blah#cite-1 ' => ' <a href="http://foo.com/blah_(wikipedia)_blah#cite-1">foo.com/blah_(wikipedia)_blah#cite-1</a> ',
+            ' http://foo.com/unicode_(âª)_in_parens ' => ' <a href="http://foo.com/unicode_(âª)_in_parens">foo.com/unicode_(âª)_in_parens</a> ',
+            ' http://foo.com/(something)?after=parens ' => ' <a href="http://foo.com/(something)?after=parens">foo.com/(something)?after=parens</a> ',
+            ' http://âº.damowmow.com/ ' => ' <a href="http://âº.damowmow.com/">âº.damowmow.com</a> ',
+            ' http://code.google.com/events/#&product=browser ' => ' <a href="http://code.google.com/events/#&amp;product=browser">code.google.com/events/#&product=browser</a> ',
+            ' http://j.mp ' => ' <a href="http://j.mp">j.mp</a> ',
+            ' ftp://foo.bar/baz ' => ' <a href="ftp://foo.bar/baz">ftp://foo.bar/baz</a> ',
+            ' http://foo.bar/?q=Test%20URL-encoded%20stuff ' => ' <a href="http://foo.bar/?q=Test%20URL-encoded%20stuff">foo.bar/?q=Test%20URL-encoded%20stuff</a> ',
+            ' http://Ù
+Ø«Ø§Ù.Ø¥Ø®ØªØšØ§Ø± ' => ' <a href="http://Ù
+Ø«Ø§Ù.Ø¥Ø®ØªØšØ§Ø±">Ù
+Ø«Ø§Ù.Ø¥Ø®ØªØšØ§Ø±</a> ',
+            ' http://äŸå­.æµè¯ ' => ' <a href="http://äŸå­.æµè¯">äŸå­.æµè¯</a> ',
+            ' http://à€à€Šà€Ÿà€¹à€°à€£.à€ªà€°à¥à€à¥à€·à€Ÿ ' => ' <a href="http://à€à€Šà€Ÿà€¹à€°à€£.à€ªà€°à¥à€à¥à€·à€Ÿ">à€à€Šà€Ÿà€¹à€°à€£.à€ªà€°à¥à€à¥à€·à€Ÿ</a> ',
+            ' http://-.~_!$&\'()*+,;=:%40:80%2f::::::@example.com ' => ' <a href="http://-.~_!$&amp;&#039;()*+,;=:%40:80%2f::::::@example.com">-.~_!$&\'()*+,;=:%40:80%2f::::::@example.com</a> ',
+            ' http://1337.net ' => ' <a href="http://1337.net">1337.net</a> ',
+            ' http://a.b-c.de ' => ' <a href="http://a.b-c.de">a.b-c.de</a> ',
+            ' http://223.255.255.254 ' => ' <a href="http://223.255.255.254">223.255.255.254</a> ',
+            ' http://x and.co OK?' => ' http://x and.co OK?',
+            ' http://foo.bar?q=Spaces should be encoded ' => ' <a href="http://foo.bar?q=Spaces">foo.bar?q=Spaces</a> should be encoded ',
+            ' https://github.com/search?utf8=â&q=gif ' => ' <a href="https://github.com/search?utf8=â&amp;q=gif">github.com/search?utf8=â&q=gif</a> ',
+            ' https://github.com/search?q=gif&utf8=* ' => ' <a href="https://github.com/search?q=gif&amp;utf8=*">github.com/search?q=gif&utf8=*</a> ',
+            ' https://github.com/search?q=gif&utf8=â ' => ' <a href="https://github.com/search?q=gif&amp;utf8=â">github.com/search?q=gif&utf8=â</a> ',
+
+            // These should fail.
+            ' http://x and.co OK?' => ' http://x and.co OK?',
+            ' www. ' => ' www. ',
+            ' www.x ' => ' www.x ',
+            ' http: ' => ' http: ',
+            ' http:// ' => ' http:// ',
+            ' http:x ' => ' http:x ',
+            ' http://x ' => ' http://x ',
+            ' http://x/ ' => ' http://x/ ',
+            ' http:// ?.x.com/ ' => ' http:// ?.x.com/ ',
+            ' http://?.com/ ' => ' http://?.com/ ',
+            ' http://.?.com/ ' => ' http://.?.com/ ',
+
+            // These are allowed to fuckup due to limitations in our ability to be omnipotent.
+            ' http:// www.x.com/ ' => ' http:// <a href="http://www.x.com/">www.x.com</a> ',
+            ' <a href=http://www.example.com/ > http://www.example.com/</a> ' => ' <a href=http://www.example.com/ > <a href="http://www.example.com/">www.example.com</a></a> ',
+
+            '<a href="http://www.example.com">Lorem ipsum</a> <a href="sftp://user@host.tld/path">dolor sit amet</a> sftp://user@host.tld/path, [http://www.example.com/ consetetur] sadipscing elitr, (sed diam) www.example.com! Nonumy http://x.com! Invidunt mailto:asdf@asdf.com ut <a href="mailto:asdf@asdf.com">asdf@asdf.com</a> \'\'\'et dolore\'\'\' magna. <!-- comment -->  Aliquyam:erat, sed://diam //voluptua//. At vero.eos.et {{{ for (var i = Things.length - 1; i >= 0; i--) { Things[i]; } }}} `http://x.com` justo duo doloreshttp://www.not.com!' => '<a href="http://www.example.com">Lorem ipsum</a> <a href="sftp://user@host.tld/path">dolor sit amet</a> <a href="sftp://user@host.tld/path">sftp://user@host.tld/path</a>, [<a href="http://www.example.com/">www.example.com</a> consetetur] sadipscing elitr, (sed diam) <a href="http://www.example.com">www.example.com</a>! Nonumy <a href="http://x.com">x.com</a>! Invidunt <a href="mailto:asdf@asdf.com">mailto:asdf@asdf.com</a> ut <a href="mailto:asdf@asdf.com">asdf@asdf.com</a> \'\'\'et dolore\'\'\' magna. <!-- comment -->  Aliquyam:erat, <a href="sed://diam">sed://diam</a> //voluptua//. At vero.eos.et {{{ for (var i = Things.length - 1; i >= 0; i--) { Things[i]; } }}} `http://x.com` justo duo <a href="doloreshttp://www.not.com">doloreshttp://www.not.com</a>!',
+        ];
+
+        $urls_strict = [
+            ' http://www.asdf.com/ ' => ' <a href="http://www.asdf.com/">www.asdf.com</a> ',
+            ' ws://www.asdf.com/ ' => ' <a href="ws://www.asdf.com/">ws://www.asdf.com/</a> ',
+            ' sftp://user@host.tld:22/path/x ' => ' <a href="sftp://user@host.tld:22/path/x">sftp://user@host.tld:22/path/x</a> ',
+            ' sip:beer ' => ' <a href="sip:beer">sip:beer</a> ',
+            ' http://asdf/ ' => ' <a href="http://asdf/">asdf</a> ',
+            ' http://www.asdf.com/? ' => ' <a href="http://www.asdf.com/?">www.asdf.com/?</a> ',
+            ' http://www.asdf.com/?x=y+z ' => ' <a href="http://www.asdf.com/?x=y+z">www.asdf.com/?x=y+z</a> ',
+            ' http://www.asdf.com/?x=y+ ' => ' <a href="http://www.asdf.com/?x=y+">www.asdf.com/?x=y+</a> ',
+            ' http://www.asdf.com/?x= ' => ' <a href="http://www.asdf.com/?x=">www.asdf.com/?x=</a> ',
+            ' http://www.asdf.com/?x ' => ' <a href="http://www.asdf.com/?x">www.asdf.com/?x</a> ',
+            ' https://www.asdf.com/?â€ïž=ð¥ ' => ' <a href="https://www.asdf.com/?â€ïž=ð¥">www.asdf.com/?â€ïž=ð¥</a> ',
+            ' www.asdf.com? ' => ' www.asdf.com? ',
+            ' www.asdf.com/ ' => ' www.asdf.com/ ',
+            ' www.asdf.com/? ' => ' www.asdf.com/? ',
+            ' www.asdf.com/?x=y+z ' => ' www.asdf.com/?x=y+z ',
+            ' www.asdf.com/?x=y+ ' => ' www.asdf.com/?x=y+ ',
+            ' www.asdf.com/?x= ' => ' www.asdf.com/?x= ',
+            ' www.asdf.com/?x ' => ' www.asdf.com/?x ',
+            ' www.asdf.com/?â€ïž=ð¥ ' => ' www.asdf.com/?â€ïž=ð¥ ',
+            ' www.asdf.com! ' => ' www.asdf.com! ',
+            ' www.asdf.com. ' => ' www.asdf.com. ',
+            ' `www.asdf.com` ' => ' `www.asdf.com` ',
+            ' "www.asdf.com" ' => ' "www.asdf.com" ',
+            ' <www.asdf.com> ' => ' <www.asdf.com> ',
+            ' `http://www.asdf.com` ' => ' `http://www.asdf.com` ',
+            ' "http://www.asdf.com" ' => ' "http://www.asdf.com" ',
+            ' <http://www.asdf.com> ' => ' <<a href="http://www.asdf.com">www.asdf.com</a>> ',
+            ' (http://www.asdf.com) ' => ' (<a href="http://www.asdf.com)">www.asdf.com)</a> ',
+            ' (URL: http://www.asdf.com#1) ' => ' (URL: <a href="http://www.asdf.com#1)">www.asdf.com#1)</a> ',
+            ' <a href="http://www.example.com/">Click Here</a> ' => ' <a href="http://www.example.com/">Click Here</a> ',
+            ' <a href="http://www.example.com/">http://www.example.com/</a> ' => ' <a href="http://www.example.com/">http://www.example.com/</a> ',
+            ' <a href=http://www.example.com/>http://www.example.com/</a> ' => ' <a href=http://www.example.com/>http://www.example.com/</a> ',
+            ' <a href=\'http://www.example.com/\' >http://www.example.com/</a> ' => ' <a href=\'http://www.example.com/\' >http://www.example.com/</a> ',
+            ' http://foo.com/blah_blah ' => ' <a href="http://foo.com/blah_blah">foo.com/blah_blah</a> ',
+            ' http://foo.com/blah_blah/ ' => ' <a href="http://foo.com/blah_blah/">foo.com/blah_blah/</a> ',
+            ' http://foo.com/blah_blah_(wikipedia) ' => ' <a href="http://foo.com/blah_blah_(wikipedia)">foo.com/blah_blah_(wikipedia)</a> ',
+            ' http://foo.com/blah_blah_(wikipedia)_(again) ' => ' <a href="http://foo.com/blah_blah_(wikipedia)_(again)">foo.com/blah_blah_(wikipedia)_(again)</a> ',
+            ' http://www.example.com/wpstyle/?p=364 ' => ' <a href="http://www.example.com/wpstyle/?p=364">www.example.com/wpstyle/?p=364</a> ',
+            ' https://www.example.com/foo/?bar=baz&inga=42&quux ' => ' <a href="https://www.example.com/foo/?bar=baz&amp;inga=42&amp;quux">www.example.com/foo/?bar=baz&inga=42&quux</a> ',
+            ' http://âªdf.ws/123 ' => ' <a href="http://âªdf.ws/123">âªdf.ws/123</a> ',
+            ' http://userid:password@example.com:8080 ' => ' <a href="http://userid:password@example.com:8080">userid:password@example.com:8080</a> ',
+            ' http://userid:password@example.com:8080/ ' => ' <a href="http://userid:password@example.com:8080/">userid:password@example.com:8080</a> ',
+            ' http://userid@example.com ' => ' <a href="http://userid@example.com">userid@example.com</a> ',
+            ' http://userid@example.com/ ' => ' <a href="http://userid@example.com/">userid@example.com</a> ',
+            ' http://userid@example.com:8080 ' => ' <a href="http://userid@example.com:8080">userid@example.com:8080</a> ',
+            ' http://userid@example.com:8080/ ' => ' <a href="http://userid@example.com:8080/">userid@example.com:8080</a> ',
+            ' http://userid:password@example.com ' => ' <a href="http://userid:password@example.com">userid:password@example.com</a> ',
+            ' http://userid:password@example.com/ ' => ' <a href="http://userid:password@example.com/">userid:password@example.com</a> ',
+            ' http://142.42.1.1/ ' => ' <a href="http://142.42.1.1/">142.42.1.1</a> ',
+            ' http://142.42.1.1:8080/ ' => ' <a href="http://142.42.1.1:8080/">142.42.1.1:8080</a> ',
+            ' http://â¡.ws/äš¹ ' => ' <a href="http://â¡.ws/äš¹">â¡.ws/äš¹</a> ',
+            ' http://â.ws ' => ' <a href="http://â.ws">â.ws</a> ',
+            ' http://â.ws/ ' => ' <a href="http://â.ws/">â.ws</a> ',
+            ' http://foo.com/blah_(wikipedia)#cite-1 ' => ' <a href="http://foo.com/blah_(wikipedia)#cite-1">foo.com/blah_(wikipedia)#cite-1</a> ',
+            ' http://foo.com/blah_(wikipedia)_blah#cite-1 ' => ' <a href="http://foo.com/blah_(wikipedia)_blah#cite-1">foo.com/blah_(wikipedia)_blah#cite-1</a> ',
+            ' http://foo.com/unicode_(âª)_in_parens ' => ' <a href="http://foo.com/unicode_(âª)_in_parens">foo.com/unicode_(âª)_in_parens</a> ',
+            ' http://foo.com/(something)?after=parens ' => ' <a href="http://foo.com/(something)?after=parens">foo.com/(something)?after=parens</a> ',
+            ' http://âº.damowmow.com/ ' => ' <a href="http://âº.damowmow.com/">âº.damowmow.com</a> ',
+            ' http://code.google.com/events/#&product=browser ' => ' <a href="http://code.google.com/events/#&amp;product=browser">code.google.com/events/#&product=browser</a> ',
+            ' http://j.mp ' => ' <a href="http://j.mp">j.mp</a> ',
+            ' ftp://foo.bar/baz ' => ' <a href="ftp://foo.bar/baz">ftp://foo.bar/baz</a> ',
+            ' http://foo.bar/?q=Test%20URL-encoded%20stuff ' => ' <a href="http://foo.bar/?q=Test%20URL-encoded%20stuff">foo.bar/?q=Test%20URL-encoded%20stuff</a> ',
+            ' http://Ù
+Ø«Ø§Ù.Ø¥Ø®ØªØšØ§Ø± ' => ' <a href="http://Ù
+Ø«Ø§Ù.Ø¥Ø®ØªØšØ§Ø±">Ù
+Ø«Ø§Ù.Ø¥Ø®ØªØšØ§Ø±</a> ',
+            ' http://äŸå­.æµè¯ ' => ' <a href="http://äŸå­.æµè¯">äŸå­.æµè¯</a> ',
+            ' http://à€à€Šà€Ÿà€¹à€°à€£.à€ªà€°à¥à€à¥à€·à€Ÿ ' => ' <a href="http://à€à€Šà€Ÿà€¹à€°à€£.à€ªà€°à¥à€à¥à€·à€Ÿ">à€à€Šà€Ÿà€¹à€°à€£.à€ªà€°à¥à€à¥à€·à€Ÿ</a> ',
+            ' http://-.~_!$&\'()*+,;=:%40:80%2f::::::@example.com ' => ' <a href="http://-.~_!$&amp;&#039;()*+,;=:%40:80%2f::::::@example.com">-.~_!$&\'()*+,;=:%40:80%2f::::::@example.com</a> ',
+            ' http://1337.net ' => ' <a href="http://1337.net">1337.net</a> ',
+            ' http://a.b-c.de ' => ' <a href="http://a.b-c.de">a.b-c.de</a> ',
+            ' http://223.255.255.254 ' => ' <a href="http://223.255.255.254">223.255.255.254</a> ',
+            ' http://foo.bar?q=Spaces should be encoded ' => ' <a href="http://foo.bar?q=Spaces">foo.bar?q=Spaces</a> should be encoded ',
+            ' https://github.com/search?utf8=â&q=gif ' => ' <a href="https://github.com/search?utf8=â&amp;q=gif">github.com/search?utf8=â&q=gif</a> ',
+            ' https://github.com/search?q=gif&utf8=* ' => ' <a href="https://github.com/search?q=gif&amp;utf8=*">github.com/search?q=gif&utf8=*</a> ',
+            ' https://github.com/search?q=gif&utf8=â ' => ' <a href="https://github.com/search?q=gif&amp;utf8=â">github.com/search?q=gif&utf8=â</a> ',
+
+            // Unusual schemes.
+            ' view-source:https://trac.strangecode.com/x/ticket/2#comment:4 ' => ' <a href="view-source:https://trac.strangecode.com/x/ticket/2#comment:4">view-source:https://trac.strangecode.com/x/ticket/2#comment:4</a> ',
+            ' mailto:asdf@asdf.com ' => ' <a href="mailto:asdf@asdf.com">mailto:asdf@asdf.com</a> ',
+            ' facetime://+19995551234 ' => ' <a href="facetime://+19995551234">facetime://+19995551234</a> ',
+            ' tel:+15552001010 ' => ' <a href="tel:+15552001010">tel:+15552001010</a> ',
+            ' magnet:?xt=urn:ed2k:354B15E68FB8F36D7CD88FF94116CDC1&xt=urn:tree:tiger:7N5OAMRNGMSSEUE3ORHOKWN4WWIQ5X4EBOOTLJY&xt=urn:btih:QHQXPYWMACKDWKP47RRVIV7VOURXFE5Q&xl=10826029&dn=mediawiki-1.15.1.tar.gz&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&as=http%3A%2F%2Fdownload.wikimedia.org%2Fmediawiki%2F1.15%2Fmediawiki-1.15.1.tar.gz&xs=http%3A%2F%2Fcache.example.org%2FXRX2PEFXOOEJFRVUCX6HMZMKS5TWG4K5&xs=dchub://example.org ' => ' <a href="magnet:?xt=urn:ed2k:354B15E68FB8F36D7CD88FF94116CDC1&amp;xt=urn:tree:tiger:7N5OAMRNGMSSEUE3ORHOKWN4WWIQ5X4EBOOTLJY&amp;xt=urn:btih:QHQXPYWMACKDWKP47RRVIV7VOURXFE5Q&amp;xl=10826029&amp;dn=mediawiki-1.15.1.tar.gz&amp;tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&amp;as=http%3A%2F%2Fdownload.wikimedia.org%2Fmediawiki%2F1.15%2Fmediawiki-1.15.1.tar.gz&amp;xs=http%3A%2F%2Fcache.example.org%2FXRX2PEFXOOEJFRVUCX6HMZMKS5TWG4K5&amp;xs=dchub://example.org">magnet:?xt=urn:ed2k:354B15E68FB8F36D7CD88FF94116CDC1&xt=urn:tree:tiger:7N5OAMRNGMSSEUE3ORHOKWN4WWIQ5X4EBOOTLJY&xt=urn:btih:QHQXPYWMACKDWKP47RRVIV7VOURXFE5Q&xl=10826029&dn=mediawiki-1.15.1.tar.gz&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80%2Fannounce&as=http%3A%2F%2Fdownload.wikimedia.org%2Fmediawiki%2F1.15%2Fmediawiki-1.15.1.tar.gz&xs=http%3A%2F%2Fcache.example.org%2FXRX2PEFXOOEJFRVUCX6HMZMKS5TWG4K5&xs=dchub://example.org</a> ',
+            ' chrome-extension://71f05c488a96b441ef679b09267ea46c/index.html ' => ' <a href="chrome-extension://71f05c488a96b441ef679b09267ea46c/index.html">chrome-extension://71f05c488a96b441ef679b09267ea46c/index.html</a> ',
+
+            // These should fail.
+            ' http://x and.co OK?' => ' http://x and.co OK?',
+            ' www. ' => ' www. ',
+            ' www.x ' => ' www.x ',
+            ' http: ' => ' http: ',
+            ' http:// ' => ' http:// ',
+            ' http:x ' => ' http:x ',
+            ' http://x ' => ' http://x ',
+            ' http://x/ ' => ' http://x/ ',
+            ' http:// www.x.com/ ' => ' http:// www.x.com/ ',
+            ' http://?.com/ ' => ' http://?.com/ ',
+            ' http://.?.com/ ' => ' http://.?.com/ ',
+
+            // These are allowed to fuckup due to limitations in our ability to be omnipotent.
+            ' <a href=http://www.example.com/ > http://www.example.com/</a> ' => ' <a href=http://www.example.com/ > <a href="http://www.example.com/">www.example.com</a></a> ',
+            ' dummyhttp://www.asdf.com/ ' => ' <a href="dummyhttp://www.asdf.com/">dummyhttp://www.asdf.com/</a> ',
+
+            '<a href="http://www.example.com">Lorem ipsum</a> <a href="sftp://user@host.tld/path">dolor sit amet</a> sftp://user@host.tld/path, [http://www.example.com/ consetetur] sadipscing elitr, (sed diam) www.example.com! Nonumy http://x.com! Invidunt mailto:asdf@asdf.com ut <a href="mailto:asdf@asdf.com">asdf@asdf.com</a> \'\'\'et dolore\'\'\' magna. <!-- comment -->  Aliquyam:erat, sed://diam //voluptua//. At vero.eos.et {{{ for (var i = Things.length - 1; i >= 0; i--) { Things[i]; } }}} `http://x.com` justo duo doloreshttp://www.not.com!' => '<a href="http://www.example.com">Lorem ipsum</a> <a href="sftp://user@host.tld/path">dolor sit amet</a> <a href="sftp://user@host.tld/path,">sftp://user@host.tld/path,</a> [<a href="http://www.example.com/">www.example.com</a> consetetur] sadipscing elitr, (sed diam) www.example.com! Nonumy <a href="http://x.com!">x.com!</a> Invidunt <a href="mailto:asdf@asdf.com">mailto:asdf@asdf.com</a> ut <a href="mailto:asdf@asdf.com">asdf@asdf.com</a> \'\'\'et dolore\'\'\' magna. <!-- comment -->  Aliquyam:erat, <a href="sed://diam">sed://diam</a> //voluptua//. At vero.eos.et {{{ for (var i = Things.length - 1; i >= 0; i--) { Things[i]; } }}} `http://x.com` justo duo <a href="doloreshttp://www.not.com!">doloreshttp://www.not.com!</a>',
+        ];
+        foreach ($urls as $input => $expected) {
+            $result = hyperlinkTxt($input, false);
+            $this->assertEquals($expected, $result, sprintf('Failed with input: %s', $input));
+        }
+        foreach ($urls_strict as $input => $expected) {
+            // printf("'%s' => '%s',\n", $input, hyperlinkTxt($input, true));
+            $result = hyperlinkTxt($input, true);
+            $this->assertEquals($expected, $result, sprintf('Failed with input: %s', $input));
+        }
+    }
+
+}
Index: tags/2.2.3/tests/VersionTest.php
===================================================================
--- tags/2.2.3/tests/VersionTest.php	(revision 632)
+++ tags/2.2.3/tests/VersionTest.php	(revision 632)
@@ -0,0 +1,186 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * PHPUnit test case for codebase/lib/Version.inc.php
+ *
+ * The method skeletons below need to be filled in with
+ * real data so that the tests will run correctly. Replace
+ * all EXPECTED_VAL and PARAM strings with real data.
+ *
+ * Created with PHPUnit_Skeleton on 2005-08-09
+ */
+
+class VersionTest extends PHPUnit_Framework_TestCase {
+
+    var $Version;
+    var $Auth_SQL;
+
+    function setUp()
+    {   
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Version.inc.php';
+
+        require_once '../lib/Auth_SQL.inc.php';
+        $this->Auth_SQL = new Auth_SQL('testauth');
+        $this->Auth_SQL->setParam(array(
+            'db_table'          => 'test_user_tbl',
+            'db_primary_key'    => 'user_id',
+            'db_login_table'    => 'test_login_tbl',
+            'login_url'         => '/login.php',
+            'blocking'          => true
+        ));
+
+        // Use fresh user table.
+        $this->Auth_SQL->initDB(true);
+
+        // Insert test data.
+        $db =& DB::getInstance();
+        $db->query("
+            INSERT INTO test_user_tbl (
+                username,
+                userpass,
+                first_name,
+                last_name,
+                email
+            ) VALUES (
+                'testuser',
+                md5('testpass'),
+                'John',
+                'Doe',
+                'john@example.com'
+            )
+        ");
+
+        $this->Auth_SQL->login('testuser', 'testpass');
+
+        $this->Version =& Version::getInstance($this->Auth_SQL);
+        $this->Version->setParam(array('db_table' => 'test_version_tbl'));
+
+        // Use fresh version table.
+        $this->Version->initDB(true);
+    }
+
+    function tearDown()
+    {
+        $db =& DB::getInstance();
+    
+        unset($this->Version);
+        unset($this->Auth_SQL);
+        $db->query("DROP TABLE IF EXISTS test_user_tbl");
+        $db->query("DROP TABLE IF EXISTS test_login_tbl");
+        $db->query("DROP TABLE IF EXISTS test_version_tbl");
+    }
+
+    function test_getinstance()
+    {
+        $result = Version::getinstance($this->Auth_SQL);
+        $this->assertTrue($result && is_a($result, 'Version'));
+    }
+
+    function test_setparam()
+    {
+        $this->Version->setparam(array('var'=>'val'));
+    }
+
+    function test_getparam()
+    {
+        $this->Version->getparam('var');
+    }
+
+    function test_create()
+    {
+        $result = $this->Version->create('test_user_tbl', 'user_id', '1', 'Test User', 'First version of user');
+        $this->assertSame(1, $result);
+    }
+
+    function test_restore()
+    {
+        $current = $this->Version->getCurrent('test_user_tbl', 'user_id', '1');
+        $version_id = $this->Version->create('test_user_tbl', 'user_id', '1', 'Test User', 'First version of user');
+        $versioned = $this->Version->getData($version_id);
+        $result = $this->Version->restore($version_id);
+        $restored = $this->Version->getCurrent('test_user_tbl', 'user_id', '1');
+        $this->assertTrue(is_array($result), 'Restore did not return array.');
+        $this->assertTrue($current === $versioned && $versioned === $restored, 'Restored version does not match original.');
+    }
+
+    function test_deleteold()
+    {
+        // Creat 3 test versions.
+        $this->Version->create('test_user_tbl', 'user_id', '1', 'Test 1', '1 version of user');
+        $this->Version->create('test_user_tbl', 'user_id', '1', 'Test 2', '2 version of user');
+        $this->Version->create('test_user_tbl', 'user_id', '1', 'Test 3', '3 version of user');
+
+        $a = $this->Version->getList('test_user_tbl', 'user_id', '1');
+
+        $this->Version->deleteold('test_user_tbl', 'user_id', '1');
+        $b = $this->Version->getList('test_user_tbl', 'user_id', '1');
+
+        $this->Version->setparam(array('max_qty'=>2));
+        $this->Version->setparam(array('min_qty'=>1));
+        $this->Version->setparam(array('min_days'=>0));
+        $this->Version->deleteold('test_user_tbl', 'user_id', '1');
+        $c = $this->Version->getList('test_user_tbl', 'user_id', '1');
+
+        $this->Version->setparam(array('max_qty'=>0));
+        $this->Version->setparam(array('min_qty'=>0));
+        $this->Version->setparam(array('min_days'=>0));
+        $this->Version->deleteold('test_user_tbl', 'user_id', '1');
+        $d = $this->Version->getList('test_user_tbl', 'user_id', '1');
+
+        $this->assertTrue(sizeof($a) == 3, 'A is wrong');
+        $this->assertTrue(sizeof($b) == 3, 'B is wrong');
+        $this->assertTrue(sizeof($c) == 1, 'C is wrong');
+        $this->assertTrue(sizeof($d) == 0, 'D is wrong');
+    }
+
+    function test_getlist()
+    {
+        $this->Version->create('test_user_tbl', 'user_id', '1', 'Test 1', '1 version of user');
+        $this->Version->create('test_user_tbl', 'user_id', '1', 'Test 2', '2 version of user');
+        $this->Version->create('test_user_tbl', 'user_id', '1', 'Test 3', '3 version of user');
+        $result = $this->Version->getList('test_user_tbl', 'user_id', '1');
+        $this->assertTrue(is_array($result) && !empty($result));
+    }
+
+    function test_getverson()
+    {
+        $version_id = $this->Version->create('test_user_tbl', 'user_id', '1', 'Test User', 'First version of user');
+        $result = $this->Version->getverson($version_id);
+        $this->assertTrue(is_array($result) && !empty($result));
+    }
+
+    function test_getdata()
+    {
+        $version_id = $this->Version->create('test_user_tbl', 'user_id', '1', 'Test User', 'First version of user');
+        $result = $this->Version->getdata($version_id);
+        $this->assertTrue(is_array($result) && !empty($result));
+    }
+
+    function test_getcurrent()
+    {
+        $result = $this->Version->getCurrent('test_user_tbl', 'user_id', '1');
+        $this->assertTrue(is_array($result) && !empty($result));
+    }
+
+}
Index: tags/2.2.3/tests/_config.inc.php
===================================================================
--- tags/2.2.3/tests/_config.inc.php	(revision 632)
+++ tags/2.2.3/tests/_config.inc.php	(revision 632)
@@ -0,0 +1,62 @@
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ *
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ *
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+* Configuration for unit tests.
+*
+* @author   Quinn Comendant <quinn@strangecode.com>
+* @version  1.0
+*/
+require_once '../lib/App.inc.php';
+require_once '../lib/DB.inc.php';
+
+$app =& App::getInstance('testapp');
+
+$app->setParam(array(
+    'site_name' => 'Test App',
+    'site_email' => 'hello@example.com',
+    'redirect_home_url' => '/',
+    'enable_session' => true,
+    'enable_db_session_handler' => true,
+    'session_use_cookies' => true,
+    'session_name' => 'StrangecodeTestSession',
+    'enable_db' => true,
+    'db_create_tables' => true,
+    'db_always_debug' => false,
+    'db_debug' => true,
+    'db_die_on_failure' => true,
+    'db_server' => getenv('DB_SERVER'),
+    'db_name' => getenv('DB_NAME'),
+    'db_user' => getenv('DB_USER'),
+    'db_pass' => getenv('DB_PASS'),
+    'display_errors' => true,
+    'db_always_debug' => false, // TRUE = display all SQL queries.
+    'db_debug' => true,
+    'db_die_on_failure' => true,
+    'log_directory' => '/tmp',
+    'log_filename' => 'codebase_test_log',
+    'log_file_priority' => LOG_DEBUG,
+    'log_screen_priority' => LOG_WARNING,
+    'error_reporting' => E_ALL,
+));
+
+$app->start();
Index: tags/2.2.3/tests/_email_test_addresses.txt
===================================================================
--- tags/2.2.3/tests/_email_test_addresses.txt	(revision 632)
+++ tags/2.2.3/tests/_email_test_addresses.txt	(revision 632)
@@ -0,0 +1,90 @@
+ asdf@domain.com
+Bob Smith asdf@domain.com
+Bob Smith <asdf@domain.com>
+"Bob Smith" asdf@domain.com
+"Bob Smith"a sldkf &D*&F^*&^# @#*$ @#* asdf@domain.com
+ "white space" <asdf@domain.com>
+ whitespace@domain.com
+asdf@domain.com (hello donkey)
+<asdf@domain.com>
+asdf@domain.com
+asdf@domain4.com
+asdf@[011.0.0.100]
+asdf@[1.0.0.0]
+asdf&asdf@domain.com
+Fred\ Bloggs@example.com
+Joe.\\Blow@example.com
+user+mailbox@example.com
+customer/department=shipping@example.com
+$A12345@example.com
+!def!xyz%abc@example.com
+_somename@example.com
+cthornhill_94%1@yahoo.com
+    "bob!#$%&'*+-/=?^_`.{|}~rice@domain.com",
+mÃŒllÃ©r@domain.com
+sarah.adams.2008@femba.ucla.edu
+dig@whap.heck.suddenly.willy.sam.digity.okay.com.org
+kathryn.p.o\'fee@gsk.com
+stephen.j.o\'quinn@gsk.com
+Wallace.v.cunneen@gsk.com
+myrada.a.carpenter@gsk.com
+caro=los.mccumber@tap.com
+$%^%JidO_kdoe*&&2@lkd.vfg
+asdf@[011.00.100]
+asdf@[011.1234.0.100]
+asdf@[011.0.234.0.100]
+asdf@011.0.0.100]
+asdf@[011.0.0.100
+asdf@011.0.0.100
+asdf@domain4.co-mm
+asdf@4domain.com
+asdf@-domain.com
+asdf@dom_ain.com
+asdf@domain-.com
+asdf@-.com
+asdf@asdf--asdf.com
+asdf@asdfas.asdf--asdf.asdfs.com
+myrada.a.carpenter@g.sk..com
+< asdf@domain.com>
+<asdf@domain.com >
+Mr. Asdf < asdf@domain.com>
+Mr. Asdf <asdf@domain.com >
+asdf@domain.com>
+<asdf@domain.com
+asdf@domain.com (hello donkey
+"Abc@def"@example.com
+"Fred Bloggs"@example.com
+Abc\@def@example.com
+myrada.a.carpenter@gsk.museums
+"asdf"@domain.com
+asdf[asdf@asdf.com
+asdf\'"+)*#%@^!asdf@asdf.com
+Barbara.gonzalez@
+bnutt-spectrumsales.net
+catherine.algra.2008.anderson.ucla.edu
+cheryl.a.mcguirk
+david.zipse@cgu,edu
+davidzipse@cgu,edu
+deanna.t.lapierre@@wellsfargo.com
+freda@freda\'s.com
+get1@pge,com
+john.lamb.ngc.com
+jslinger@saturnoftroy
+kellywatts@saturnof
+kent.plummer@Solvay
+lakshmi.thirumalai.2007@anderson.
+nraymon@hoaghospital
+.bob@domain.com
+bobrice..@domain.com
+bob..rice@domain.com
+usernameistoolongusernameistoolongusernameistoolongusernameistoolongusernameis@domain.com
+domainistoolong@domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.domainistoolong.com
+<ken@mail.mpundzu.com>
+<lisah@specialtycare4women.com>
+<jknolle@learningchange.com>
+<cd@monstro.com>
+<spoofing@monstro.com>
+lanebecker@gmail.com
+<fabio@monstro.com>
+neville@dynamiccity.org
+<mtbcsc-spamsubmit@jayallen.org>
Index: tags/2.2.3/tests/_skel.php
===================================================================
--- tags/2.2.3/tests/_skel.php	(revision 632)
+++ tags/2.2.3/tests/_skel.php	(revision 632)
@@ -0,0 +1,79 @@
+#!/usr/bin/env php
+<?php
+/**
+ * The Strangecode Codebase - a general application development framework for PHP
+ * For details visit the project site: <http://trac.strangecode.com/codebase/>
+ * Copyright 2001-2012 Strangecode, LLC
+ * 
+ * This file is part of The Strangecode Codebase.
+ *
+ * The Strangecode Codebase is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 3 of the License, or (at your option)
+ * any later version.
+ * 
+ * The Strangecode Codebase is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ * 
+ * You should have received a copy of the GNU General Public License along with
+ * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+* Creates new test case file from specified class file.
+*
+* @access  public
+* @param   Only argument is path to file containing class.
+* @author  Quinn Comendant <quinn@strangecode.com>
+* @since   01 Dec 2005 13:56:09
+*/
+require_once 'PHPUnit/Skeleton.php';
+
+$dir = dirname(__FILE__) . '/../lib';
+
+if ($_SERVER['argc'] == 2 && file_exists($dir . '/' . $_SERVER['argv'][1])) {
+    $file = $_SERVER['argv'][1];
+    echo 'Proccessing ' . realpath($dir . '/' . $file) . "\n";
+} else {
+    die('First argument or file not found: ' . $_SERVER['argv'][1] . "\n");
+}
+
+preg_match('/([^\.]+)\..*$/i', $file, $match);
+$classname = $match[1];
+
+$ps = new PHPUnit_Skeleton($classname, '../lib/' . $file);
+
+// Generate the test class.
+// Default settings will not include any parent class methods, but
+// will include private methods.
+$ps->createTestClass();
+
+// Write the new test class to file.
+// By default, code to run the test will be included.
+$ps->writeTestClass();
+
+
+// $dir_handle = opendir($dir);
+// while ($dir_handle && ($file = readdir($dir_handle)) !== false) {
+//     if (!preg_match('/^\./', $file) && is_file($dir . '/' . $file)) {
+//         echo 'Proccessing ' . realpath($dir . '/' . $file) . "\n";
+//
+//         preg_match('/([^\.]+)\..*$/i', $file, $match);
+//         $classname = $match[1];
+//
+//         $ps = new PHPUnit_Skeleton($classname, '../lib/' . $file);
+//
+//         // Generate the test class.
+//         // Default settings will not include any parent class methods, but
+//         // will include private methods.
+//         $ps->createTestClass();
+//
+//         // Write the new test class to file.
+//         // By default, code to run the test will be included.
+//         $ps->writeTestClass();
+//
+//     }
+// }
+
Index: tags/2.2.3/tests/_test_htpasswd
===================================================================
--- tags/2.2.3/tests/_test_htpasswd	(revision 632)
+++ tags/2.2.3/tests/_test_htpasswd	(revision 632)
@@ -0,0 +1,1 @@
+testuser:nOZSYqRQlsv1o
Index: tags/2.2.3/tests/bootstrap.php
===================================================================
--- tags/2.2.3/tests/bootstrap.php	(revision 632)
+++ tags/2.2.3/tests/bootstrap.php	(revision 632)
@@ -0,0 +1,5 @@
+<?php
+// session_start();
+// //set_include_path(get_include_path() . PATH_SEPARATOR . '../..');
+// ini_set('include_path',get_include_path().PATH_SEPARATOR.'/opt'.PATH_SEPARATOR.'..');
+// //die(get_include_path());
Index: tags/2.2.3/tests/phpunit.xml
===================================================================
--- tags/2.2.3/tests/phpunit.xml	(revision 632)
+++ tags/2.2.3/tests/phpunit.xml	(revision 632)
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/5.7/phpunit.xsd"
+backupGlobals="false"
+beStrictAboutCoversAnnotation="true"
+beStrictAboutOutputDuringTests="true"
+beStrictAboutTestsThatDoNotTestAnything="true"
+beStrictAboutTodoAnnotatedTests="true"
+convertErrorsToExceptions="false"
+convertNoticesToExceptions="false"
+convertWarningsToExceptions="false"
+bootstrap="bootstrap.php"
+strict="true"
+colors="true"
+verbose="true"
+debug="false"
+tap="true"
+stopOnError="true"
+stopOnFailure="true"
+stopOnIncomplete="true"
+stopOnSkipped="true"
+>
+    <includePath>.</includePath>
+    <includePath>/opt</includePath>
+    <testsuite>
+        <file>AppTest.php</file>
+        <file>AuthFileTest.php</file>
+        <!-- <file>AuthorizeNetTest.php</file> -->
+        <file>AuthSQLTest.php</file>
+        <file>CSSTest.php</file>
+        <file>DBSessionHandlerTest.php</file>
+        <file>EmailTest.php</file>
+        <file>FormValidatorTest.php</file>
+        <file>LockTest.php</file>
+        <file>PayPalTest.php</file>
+        <file>VersionTest.php</file>
+        <file>UtilitiesTest.php</file>
+    </testsuite>
+
+    <filter>
+        <whitelist processUncoveredFilesFromWhitelist="true">
+            <directory suffix=".php">src</directory>
+        </whitelist>
+    </filter>
+</phpunit>
Index: tags/2.2.3/tests/run_tests.sh
===================================================================
--- tags/2.2.3/tests/run_tests.sh	(revision 632)
+++ tags/2.2.3/tests/run_tests.sh	(revision 632)
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+# This script sets-up the test environment and runs all tests.
+# You'll want to define your local mysql credentials for a test
+# database as environment variables, e.g., in ~/.bash_profile:
+#  export DB_NAME="test"
+#  export DB_USER="test"
+#  export DB_PASS="..."
+
+function err {
+    MSG="$1";
+    IFS='';
+    echo -e $MSG 1>&2
+    exit 1;
+}
+
+# Be in the directory with all the tests.
+cd `dirname $0`;
+
+# Get required ENV variables.
+if [[ -z "$DB_USER$DB_PASS$DB_NAME" ]]; then
+    echo "MySQL test DB credential environment variables are missing.\nSet these in ~/.bash_profile to avoid seeing these prompts each time.";
+fi
+for E in DB_SERVER DB_NAME DB_USER DB_PASS; do
+    while [[ -z ${!E} ]]; do
+        read -p "$E: " $E;
+    done
+    export $E;
+done
+
+# Create database.
+mysql -e "CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\`" || err "Failed to create database '${DB_NAME}";
+
+# Go!
+echo "Running the tests!";
+echo "You'll want to 'tail -f /tmp/codebase_test_log' and watch for errors.";
+
+# Config options go in phpunit.xml
+# phpunit --tap | grep -v '^ok '
+../vendor/phpunit/phpunit/composer/bin/phpunit --stderr || err "\nSomething went wrong (code $?). If there is no output above, check the php_error_log";
+
