Index: branches/2.0singleton/bin/extract_gettext.pl
===================================================================
--- branches/2.0singleton/bin/extract_gettext.pl	(revision 122)
+++ branches/2.0singleton/bin/extract_gettext.pl	(revision 122)
@@ -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: branches/2.0singleton/bin/file_importer.php
===================================================================
--- branches/2.0singleton/bin/file_importer.php	(revision 122)
+++ branches/2.0singleton/bin/file_importer.php	(revision 122)
@@ -0,0 +1,98 @@
+#!/usr/local/bin/php -q
+<?php
+/**
+ * file_importer.cli.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+require realpath(dirname(__FILE__) . '/..') . '/config/cli_config.inc.php';
+
+// Test arguments.
+if ($_SERVER['argc'] == 2 && is_dir($_SERVER['argv'][1])) {
+    $file_dir = preg_replace('/\/$/', '', $_SERVER['argv'][1]);
+} else {
+    die("You must specify the directory of files to import as the only argument.\n");
+}
+
+// Get array of all files.
+$files = getFilesRecursive($file_dir, 1);
+
+// Loop through files.
+// dump($files);
+$file_count = 0;
+if (is_array($files) && !empty($files)) {
+    foreach ($files as $file) {
+        if ($fp = fopen($file, 'r')) {
+            echo ++$file_count . "\n";
+            $file_text = join('', file($file));
+            fclose($fp);
+
+            // Do something with file contents.
+            preg_match('/BALANCE:\s*\$([\.\d]+)/', $file_text, $amt);
+            $file_date = date('Y-m-d', strtotime(preg_replace('|[^_]*_|', '', basename($file))));
+//             DB::query("
+//                 INSERT INTO invoice_tbl (
+//                     client_id,
+//                     invoice_type,
+//                     invoice_date,
+//                     invoice_amount,
+//                     invoice_status,
+//                     payment_type,
+//                     invoice_text,
+//                     email_sent_datetime,
+//                     added_datetime
+//                 ) VALUES (
+//                     '" . DB::escapeString(0) . "',
+//                     '" . DB::escapeString('hosting') . "',
+//                     '" . DB::escapeString($file_date) . "',
+//                     '" . DB::escapeString($amt[1]) . "',
+//                     '" . DB::escapeString('Paid') . "',
+//                     '" . DB::escapeString('') . "',
+//                     '" . DB::escapeString($file_text) . "',
+//                     '" . DB::escapeString($file_date) . "',
+//                     NOW()
+//                 )
+//             ");
+
+        } else {
+            App::logMsg('Could not open file: ' . $file, LOG_INFO, __FILE__, __LINE__);
+        }
+    }
+    App::logMsg('Proccessing complete: ' . $file_count . ' files total.', LOG_INFO, __FILE__, __LINE__);
+} else {
+    App::logMsg('No files available in that directory.', LOG_INFO, __FILE__, __LINE__);
+}
+
+
+
+
+/**
+ * Find all files in directories recursivly with a specified file extension.
+ *
+ * @param  string $dir               the full path to the directory to scan
+ * @param  array  $valid_extensions  valid extensions
+ * @param  int    $recurs_lev        how many levels deep to scan. Set this
+ *                                   to a low number to prevent infinite loops
+ *
+ * @return array   multi-dimentional array of found files on success
+ */
+function getFilesRecursive($dir, $recurs_lev=0, $return=true)
+{
+    static $output;
+
+    $dir_handle = opendir($dir);
+    while ($dir_handle && ($file = readdir($dir_handle)) !== false) {
+        if (!preg_match('/^\./', $file)) {
+            if (is_dir($dir . '/' . $file) && $recurs_lev > 0) {
+                getFilesRecursive($dir . '/' . $file, $valid_extensions, $recurs_lev - 1);
+            } else if (!is_dir($dir . '/' . $file)) {
+                $output[] = $dir . '/' . $file;
+            }
+        }
+    }
+    if ($return) {
+        return $output;
+    }
+}
+
+?>
Index: branches/2.0singleton/bin/module_maker/_config.inc.php
===================================================================
--- branches/2.0singleton/bin/module_maker/_config.inc.php	(revision 122)
+++ branches/2.0singleton/bin/module_maker/_config.inc.php	(revision 122)
@@ -0,0 +1,50 @@
+<?php
+/* module_maker/_config.inc.php
+ * code by strangecode :: www.strangecode.com :: this document contains copyrighted information. */
+
+// Determine common site directory.
+$common_base = realpath($_SERVER['argv'][1]);
+
+// Test argument.
+if ($_SERVER['argc'] > 1 && '' != $common_base && is_dir($common_base)) {
+    // First arg is path to current site. Realpath removes trailing /s
+    define('COMMON_BASE', $common_base);
+} else {
+    die("Error: First argument must be the directory path to an existing site (ex: /home/sc/www.strangecode.com).\n");
+}
+
+// Make sure necessary files exist.
+if (!file_exists(COMMON_BASE . '/global/db_auth.inc.php')) {
+    die("Error: First argument directory must contain the global/db_auth.inc.php file with valid MySQL credentials.\n");
+}
+
+// Set include path.
+ini_set('include_path', get_include_path()
+    . PATH_SEPARATOR . COMMON_BASE
+);
+
+// Include core libraries.
+require_once 'codebase/lib/App.inc.php';
+require_once 'codebase/lib/Utilities.inc.php';
+require_once 'codebase/lib/Auth_SQL.inc.php';
+
+$app =& App::getInstance('module_maker');
+
+$app->setParam(array(
+    'site_name' => 'Module Maker',
+    'site_email' => 'codebase@strangecode.com',
+    'enable_session' => false,
+    'enable_db' => true,
+    'db_always_debug' => false,
+    'db_debug' => true,
+    'db_die_on_failure' => true,
+    'display_errors' => true,
+    'error_reporting' => E_ALL,
+    'log_screen_priority' => LOG_DEBUG,
+));
+
+require_once 'global/db_auth.inc.php';
+
+$app->start();
+
+?>
Index: branches/2.0singleton/bin/module_maker/form_template.cli.php
===================================================================
--- branches/2.0singleton/bin/module_maker/form_template.cli.php	(revision 122)
+++ branches/2.0singleton/bin/module_maker/form_template.cli.php	(revision 122)
@@ -0,0 +1,212 @@
+#!/usr/local/bin/php -q
+<?php
+/**
+ * form_template.cli.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+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
+
+<label for="$field"<\x3fphp \$fv->err('$field') \x3f>><\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>
+    <span class="commanditem commandtext"><a href="<\x3fphp echo App::oHREF('<##>.php?op=add&boomerang=true'); \x3f>" onClick="javascript:return confirm('<\x3fphp echo _("Notice: You are about to leave this form page and any changes you may have made without saving will be lost."); \x3f>')"><\x3fphp echo _("Add <##>") \x3f></a></span>
+    <\x3fphp if ('' != \$frm['<##>_id']) { \x3f>
+    <span class="commanditem commandtext"><a href="<\x3fphp echo App::oHREF('<##>.php?op=edit&boomerang=true&<##>_id=' . \$frm['<##>_id']); \x3f>" onClick="javascript:return confirm('<\x3fphp echo _("Notice: You are about to leave this form page and any changes you may have made without saving will be lost."); \x3f>')"><\x3fphp echo sprintf(_("Edit <##> <em>%s</em>"), \$frm['<##>']) \x3f></a></span>
+    <\x3fphp } \x3f>
+E_O_F;
+            continue;
+        }
+
+        // File upload.
+        if (preg_match('/file|image/i', $field)) {
+            $output[$field] = <<<E_O_F
+
+<label for="$field"<\x3fphp \$fv->err('$field') \x3f>><\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(_("File to upload must have one of the following file-name extensions: %s."), join(', ', \$upload->getParam('valid_file_extensions'))) \x3f></div>
+E_O_F;
+            continue;
+        }
+
+        // Password field.
+        if (preg_match('/pass/i', $field)) {
+            $output[$field] = <<<E_O_F
+
+<label for="$field"<\x3fphp \$fv->err('$field') \x3f>><\x3fphp echo _("$title"); \x3f></label>
+<input type="password" class="sc-medium" size="50" name="$field" id="$field" value="<\x3fphp echo oTxt(\$frm['$field']); \x3f>" />
+E_O_F;
+            continue;
+        }
+
+        switch ($type) {
+
+        // Select menu (or radio buttons)
+        case 'enum' :
+            $output[$field] = <<<E_O_F
+
+<label for="$field"<\x3fphp \$fv->err('$field') \x3f>><\x3fphp echo _("$title"); \x3f></label>
+<select name="$field" id="$field"><\x3fphp printSetSelectForm('$db_tbl', '$field', \$frm['$field'], true); \x3f></select>
+E_O_F;
+            break;
+
+        // Set checkboxes
+        case 'set' :
+            $output[$field] = <<<E_O_F
+
+<label for="$field"<\x3fphp \$fv->err('$field') \x3f>><\x3fphp echo _("$title"); \x3f></label>
+<\x3fphp printSetCheckboxes('$db_tbl', '$field', \$frm['$field'], 1) \x3f>
+E_O_F;
+            break;
+
+        // Single checkbox
+        case 'toggle' :
+            $output[$field] = <<<E_O_F
+
+<label for="$field"<\x3fphp \$fv->err('$field') \x3f>><\x3fphp echo _("$title"); \x3f></label>
+<input type="checkbox" name="$field" id="$field" <\x3fphp frmChecked(!empty(\$frm['$field'])) \x3f> /><label class="left"><\x3fphp echo _("Check this box to $title"); \x3f></label>
+E_O_F;
+            break;
+
+        // Textarea small
+        case 'text' :
+        case 'tinyblob' :
+        case 'blob' :
+            $output[$field] = <<<E_O_F
+
+<label for="$field"<\x3fphp \$fv->err('$field') \x3f>><\x3fphp echo _("$title"); \x3f></label>
+<textarea class="sc-large sc-short" cols="75" rows="4" name="$field" id="$field"><\x3fphp echo oTxt(\$frm['$field']); \x3f></textarea>
+E_O_F;
+            break;
+
+        // Textarea big
+        case 'mediumtext' :
+        case 'longtext' :
+        case 'mediumblob' :
+        case 'longblob' :
+            $output[$field] = <<<E_O_F
+
+<label for="$field"<\x3fphp \$fv->err('$field') \x3f>><\x3fphp echo _("$title"); \x3f></label>
+<textarea class="sc-large sc-tall" cols="75" rows="4" name="$field" id="$field"><\x3fphp echo oTxt(\$frm['$field']); \x3f></textarea>
+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
+
+<label for="$field"<\x3fphp \$fv->err('$field') \x3f>><\x3fphp echo _("$title"); \x3f></label>
+<input type="text" class="sc-tiny" size="12" name="$field" id="$field" value="<\x3fphp echo oTxt(\$frm['$field']); \x3f>" />
+E_O_F;
+            break;
+
+        // Date
+        case 'date' :
+        case 'datetime' :
+        case 'timestamp' :
+        case 'time' :
+        case 'year' :
+        default :
+            $output[$field] = <<<E_O_F
+
+<label for="$field"<\x3fphp \$fv->err('$field') \x3f>><\x3fphp echo _("$title"); \x3f></label>
+<input type="text" class="sc-medium" size="25" name="$field" id="$field" value="<\x3fphp echo oTxt(\$frm['$field']); \x3f>" />
+E_O_F;
+            break;
+
+        // Text
+        case 'char' :
+        case 'varchar' :
+        case 'tinytext' :
+        case 'tinyblob' :
+        default :
+            $output[$field] = <<<E_O_F
+
+<label for="$field"<\x3fphp \$fv->err('$field') \x3f>><\x3fphp echo _("$title"); \x3f></label>
+<input type="text" class="sc-medium" size="50" name="$field" id="$field" value="<\x3fphp echo oTxt(\$frm['$field']); \x3f>" />
+E_O_F;
+            break;
+        }
+    }
+} else {
+    die(basename($_SERVER['argv'][0]) . " Warning: $db_tbl does not have any columns.\n");
+}
+
+
+echo join("\n", $output);
+
+?>
Index: branches/2.0singleton/bin/module_maker/list_template.cli.php
===================================================================
--- branches/2.0singleton/bin/module_maker/list_template.cli.php	(revision 122)
+++ branches/2.0singleton/bin/module_maker/list_template.cli.php	(revision 122)
@@ -0,0 +1,178 @@
+#!/usr/local/bin/php -q
+<?php
+/**
+ * list_template.cli.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+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]\n", basename($_SERVER['argv'][0])));
+}
+
+// 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];
+        $title = ucfirst(str_replace('_', ' ', $field));
+        $type = preg_replace('/^(\w+).*$/', '\\1', $col[1]);
+
+        // 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 strlen(\$list[\$i]['$field'])<50 \x3f oTxt(\$list[\$i]['$field'], true) : oTxt(trim(substr(\$list[\$i]['$field'], 0, 50)) . '...'); \x3f>";
+        } else if (preg_match('/.*(begin|start).*date.*/i', $field)) {
+            $listrows[] = "<\x3fphp echo '0000-00-00' == \$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 '0000-00-00 00:00:00' == \$list[\$i]['$field'] ? '' : date(App::getParam('date_format'), strtotime(\$list[\$i]['$field'])); \x3f>";
+        } else if (preg_match('/date/i', $type)) {
+            $listrows[] = "<\x3fphp echo '0000-00-00' == \$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_admin_username']); \x3f>";
+        } else if (preg_match('/(modified_by_user_id)/i', $field)) {
+            $listrows[] = "<\x3fphp echo oTxt(\$list[\$i]['modified_admin_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 id="commandbox">
+<form action="<\x3fphp echo \$_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 \$_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 ($field == $primary_key) {
+            echo "        <th><\x3fphp echo \$so->printSortHeader('$db_tbl.$field', _(\"ID\"), 'ASC'); \x3f></th>\n";
+        } else {
+            echo "        <th><\x3fphp echo \$so->printSortHeader('$db_tbl.$field', _(\"$title\"), 'ASC'); \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/_widgets/edit.gif" alt="Edit" width="14" height="18" 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/_widgets/subcategory.gif" alt="" width="18" height="14" 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/_widgets/delete.gif" alt="Delete" width="16" height="17" 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: branches/2.0singleton/bin/module_maker/module.cli.php
===================================================================
--- branches/2.0singleton/bin/module_maker/module.cli.php	(revision 122)
+++ branches/2.0singleton/bin/module_maker/module.cli.php	(revision 122)
@@ -0,0 +1,568 @@
+#!/usr/local/bin/php -q
+<?php
+/**
+ * module.cli.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+include_once dirname(__FILE__) . '/_config.inc.php';
+
+$op = null;
+$valid_ops = array('clean', 'var', 'test');
+
+// Test for a single argument.
+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: invalid arguments. Try like this:
+
+    " . basename($_SERVER['argv'][0]) . " site_directory name_singular name_plural [clean]
+
+The following files will be generated by this script:
+<site_directory>/admin/nameplural.php
+<site_directory>/admin/_templates/namesingular_list.ihtml
+<site_directory>/admin/_templates/namesingular_form.ihtml
+<site_directory>/html/nameplural.php
+<site_directory>/html/_templates/namesingular_list.ihtml
+<site_directory>/html/_templates/namesingular.ihtml
+
+");
+}
+
+// Test for operation.
+if (isset($_SERVER['argv'][4])) {
+    // Optional fourth arg is op.
+    $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;
+default :
+
+}
+
+
+/******************************************************************************
+ * 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 = 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';
+
+// Databaes 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 'Runing Module Maker. Using database: ' . $app->getParam('db_name') . "\n";
+
+
+/******************************************************************************
+ * PREPROCESSING
+ *****************************************************************************/
+
+// Ensure skel files exist.
+if (
+    !file_exists("$skel_dir/admin.php") ||
+    !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 skeleton source files missing. Please check directory: $skel_dir.\n");
+}
+
+// Ensure essential directories exist.
+if (!is_dir("$admin_dir")) {
+    die(basename($_SERVER['argv'][0]) . " Error: $admin_dir directory not found.\n");
+}
+if (!is_dir("$admin_tpl_dir")) {
+    die(basename($_SERVER['argv'][0]) . " Error: $admin_tpl_dir directory not found.\n");
+}
+if (!is_dir("$public_dir")) {
+    die(basename($_SERVER['argv'][0]) . " Error: $public_dir directory not found.\n");
+}
+if (!is_dir("$public_tpl_dir")) {
+    die(basename($_SERVER['argv'][0]) . " Error: $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 (!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];
+
+        // 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['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 \$_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 \$_SERVER['PHP_SELF']; \x3f>\" class=\"sc-form\">\n<input type=\"hidden\" name=\"MAX_FILE_SIZE\" value=\"<##>\" />";
+
+    // 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->addPage(_("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
+ *****************************************************************************/
+
+// (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);
+    }
+}
+
+// (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: branches/2.0singleton/bin/module_maker/skel/adm_form.ihtml
===================================================================
--- branches/2.0singleton/bin/module_maker/skel/adm_form.ihtml	(revision 122)
+++ branches/2.0singleton/bin/module_maker/skel/adm_form.ihtml	(revision 122)
@@ -0,0 +1,13 @@
+<?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%']; ?>" />
+
+<?php printSubmitButtons($frm['submit_buttons']); ?>
+
+%ADM_FORM_TABLE_ROWS%
+
+<?php printSubmitButtons($frm['submit_buttons']); ?>
+</form>
Index: branches/2.0singleton/bin/module_maker/skel/adm_list.ihtml
===================================================================
--- branches/2.0singleton/bin/module_maker/skel/adm_list.ihtml	(revision 122)
+++ branches/2.0singleton/bin/module_maker/skel/adm_list.ihtml	(revision 122)
@@ -0,0 +1,44 @@
+
+<?php $fv->printErrorMessages(); ?>
+
+<div id="commandbox">
+<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="get">
+<?php App::printHiddenSession(false); ?>
+    <span class="sc-nowrap commandtext"><a href="<?php echo App::oHREF($_SERVER['PHP_SELF'] . '?op=add'); ?>"><?php echo _("Add %ITEM_TITLE%"); ?></a></span>
+    <br />
+
+    <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_<##>">
+        <?php // printSelectForm('<##>_tbl', "CONCAT(<##>_id, '&mdash;', city, '&mdash;', title)", '<##>_id', getFormData('filter_<##>'), array('Any <##>'), 'ORDER BY <##> ASC'); ?>
+    </select>
+    <input type="submit" value="<?php echo _("Search"); ?>" />
+</form>
+</div>
+
+<form action="<?php echo $_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="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/_widgets/edit.gif" alt="Edit" width="14" height="18" border="0"></a> &nbsp;</td>
+        <td class="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/_widgets/subcategory.gif" alt="" width="18" height="14" border="0" /></a> &nbsp;</td>
+%ADM_LIST_ROWS%
+        <td class="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/_widgets/delete.gif" alt="Delete" width="16" height="17" 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: branches/2.0singleton/bin/module_maker/skel/admin.php
===================================================================
--- branches/2.0singleton/bin/module_maker/skel/admin.php	(revision 122)
+++ branches/2.0singleton/bin/module_maker/skel/admin.php	(revision 122)
@@ -0,0 +1,431 @@
+<?php
+/**
+ * %ADMIN_SCRIPT%
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ *
+ * Generated by module_maker.cli.php on %DATE%
+ */
+
+require_once dirname(__FILE__) . '/_config.inc.php';
+
+$auth->requireLogin();
+// $auth->requireAccessClearance(ZONE_ADMIN_%NAME_UPPER%);
+App::sslOn();
+
+require_once 'codebase/lib/PageNumbers.inc.php';
+require_once 'codebase/lib/SessionCache.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/RecordLock.inc.php';
+require_once 'codebase/lib/RecordVersion.inc.php';
+%ADMIN_UPLOAD_INCLUDE%
+
+/******************************************************************************
+ * CONFIG
+ *****************************************************************************/
+
+// Titles and navigation header.
+$nav->addPage(_("%TITLE%"), $_SERVER['PHP_SELF']);
+
+// The object to validate form input.
+$fv = new FormValidator();
+
+%SORT_ORDER%
+
+// Instantiate page numbers. Total items are set and calculation is done in the getRecordList function.
+$page = new PageNumbers();
+$page->setPerPage(getFormData('per_page'), 100);
+$page->setPageNumber(getFormData('page_number'));
+
+// Search limiters retain their values between page requests.
+App::carryQuery('search_query');
+App::carryQuery('filter_<##>');
+%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)) {
+    // Break the cache because we are changing the list data.
+    SessionCache::breakCache($_SERVER['PHP_SELF']);
+}
+
+// What action to take.
+switch (getFormData('op')) {
+
+case 'add' :
+//     $auth->requireAccessClearance(ZONE_ADMIN_%NAME_UPPER%_FUNC_ADD);
+    // Initialize variables for the form template.
+    $frm =& addRecordForm();
+    $nav->addPage(_("Add %ITEM_TITLE%"));
+    $main_template = '%ADMIN_FORM_TEMPLATE%';
+    break;
+
+case 'edit' :
+//     $auth->requireAccessClearance(ZONE_ADMIN_%NAME_UPPER%_FUNC_EDIT);
+    // Initialize variables for the form template.
+    $frm =& editRecordForm(getFormData('%PRIMARY_KEY%'));
+    $nav->addPage(_("Edit %ITEM_TITLE%"));
+    $main_template = '%ADMIN_FORM_TEMPLATE%';
+    break;
+
+case 'del' :
+//     $auth->requireAccessClearance(ZONE_ADMIN_%NAME_UPPER%_FUNC_DELETE);
+    deleteRecord(getFormData('%PRIMARY_KEY%'));%ADMIN_UPLOAD_DEL%
+    if (App::validBoomerangURL('%NAME_PLURAL%')) {
+        // Display boomerang page.
+        App::dieBoomerangURL('%NAME_PLURAL%');
+    }
+    // Display default page.
+    App::dieURL($_SERVER['PHP_SELF']);
+    break;
+
+case 'insert' :
+//     $auth->requireAccessClearance(ZONE_ADMIN_%NAME_UPPER%_FUNC_ADD);
+    if (getFormdata('cancel', false)) {
+        if (App::validBoomerangURL('%NAME_PLURAL%')) {
+            // Display boomerang page.
+            App::dieBoomerangURL('%NAME_PLURAL%');
+        }
+        // Display default page.
+        App::dieURL($_SERVER['PHP_SELF']);
+    }
+    validateInput();
+    if ($fv->anyErrors()) {
+        $frm =& addRecordForm();
+        $frm = array_merge($frm, getFormData());
+        $nav->addPage(_("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');
+        } else if (App::validBoomerangURL('%NAME_PLURAL%')) {
+            // Display boomerang page.
+            App::dieBoomerangURL('%NAME_PLURAL%');
+        }
+        // Display default page.
+        App::dieURL($_SERVER['PHP_SELF']);
+    }
+    break;
+
+case 'update' :
+//     $auth->requireAccessClearance(ZONE_ADMIN_%NAME_UPPER%_FUNC_EDIT);
+    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%'));
+    }
+    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%');
+        }
+        // Display default page.
+        App::dieURL($_SERVER['PHP_SELF']);
+    }
+    validateInput();
+    if ($fv->anyErrors()) {
+        $frm =& editRecordForm(getFormData('%PRIMARY_KEY%'));
+        $frm = array_merge($frm, getFormData());
+        $nav->addPage(_("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);
+            } 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%');
+        }
+        // Display default page.
+        App::dieURL($_SERVER['PHP_SELF']);
+    }
+    break;
+
+case _("Save rank") :
+//     $auth->requireAccessClearance(ZONE_ADMIN_PROGRAMS_FUNC_REORDER);
+    updateRank(getFormData('rank'));
+    App::dieURL($_SERVER['PHP_SELF']);
+    break;
+
+default :
+//     $auth->requireAccessClearance(ZONE_ADMIN_%NAME_UPPER%_FUNC_LIST, _("Permission to view %NAME_PLURAL% list denied."));
+    $list =& getRecordList();
+    $main_template = '%ADMIN_LIST_TEMPLATE%';
+    break;
+}
+
+/******************************************************************************
+ * TEMPLATE INITIALIZATION
+ *****************************************************************************/
+
+include 'header.ihtml';
+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(
+            'submit' => _("Add %ITEM_TITLE%"),
+            'repeat' => _("Add &amp; repeat"),
+            'cancel' => _("Cancel"),
+        ),
+    );
+
+    return $frm;
+}
+
+function &editRecordForm($id)
+{
+    global $lock;
+
+    $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();
+    }
+
+    // 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(
+            'submit' => _("Save changes"),
+            'repeat' => _("Save &amp; edit next"),
+            'reset' => _("Reset"),
+            'cancel' => _("Cancel"),
+        ),
+    ), $frm);
+
+    return $frm;
+}
+
+function deleteRecord($id)
+{
+    global $lock;
+
+    $lock->select('%DB_TBL%', '%PRIMARY_KEY%', $id);
+    if ($lock->isLocked() && !$lock->isMine()) {
+        $lock->dieErrorPage();
+    }
+
+    // Break the cache because we are changing the list data.
+    SessionCache::breakCache($_SERVER['PHP_SELF']);
+
+    // 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();
+    }
+
+    // Delete the record.
+    DB::query("DELETE FROM %DB_TBL% WHERE %PRIMARY_KEY% = '" . DB::escapeString($id) . "'");
+
+    App::raiseMsg(sprintf(_("The %ITEM_TITLE% <strong>%s</strong> has been deleted."), $name), MSG_SUCCESS, __FILE__, __LINE__);
+
+    // Unlock record.
+    $lock->remove();
+}
+
+function insertRecord($frm)
+{
+    global $auth;
+
+    // Break the cache because we are changing the list data.
+    SessionCache::breakCache($_SERVER['PHP_SELF']);
+
+%INSERT%
+    $last_insert_id = mysql_insert_id(DB::getDBH());
+
+    // Create version.
+    $version = RecordVersion::getInstance($GLOBALS['auth']);
+    $version->create('%DB_TBL%', '%PRIMARY_KEY%', $last_insert_id, $frm['<##>']);
+
+    App::raiseMsg(sprintf(_("The %ITEM_TITLE% <strong>%s</strong> has been added."), $frm['<##>']), MSG_SUCCESS, __FILE__, __LINE__);
+
+    return $last_insert_id;
+}
+
+function updateRecord($frm)
+{
+    global $auth, $lock;
+
+    $lock->select('%DB_TBL%', '%PRIMARY_KEY%', $frm['%PRIMARY_KEY%']);
+    if ($lock->isLocked() && !$lock->isMine()) {
+        $lock->dieErrorPage();
+    }
+
+    // Break the cache because we are changing the list data.
+    SessionCache::breakCache($_SERVER['PHP_SELF']);
+
+%UPDATE%
+
+    // Create version.
+    $version = RecordVersion::getInstance($GLOBALS['auth']);
+    $version->create('%DB_TBL%', '%PRIMARY_KEY%', $frm['%PRIMARY_KEY%'], $frm['<##>']);
+
+    App::raiseMsg(sprintf(_("The %ITEM_TITLE% <strong>%s</strong> has been updated."), $frm['<##>']), MSG_SUCCESS, __FILE__, __LINE__);
+
+    // Unlock record.
+    $lock->remove();
+}
+
+function &getRecordList()
+{
+    global $page;
+    global $so;
+
+    $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_admin_username,
+            a2.username AS modified_admin_username
+        FROM %DB_TBL%
+        LEFT JOIN admin_tbl a1 ON (%DB_TBL%.added_by_user_id = a1.admin_id)
+        LEFT JOIN admin_tbl a2 ON (%DB_TBL%.modified_by_user_id = a2.admin_id)
+        $where_clause
+        " . $so->getSortOrderSQL() . "
+        " . $page->getLimitSQL() . "
+    ";
+
+    // 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 (Prefs::getValue('cache_hash', $_SERVER['PHP_SELF']) != $cache_hash) {
+        SessionCache::breakCache($_SERVER['PHP_SELF']);
+        Prefs::setValue('cache_hash', $cache_hash, $_SERVER['PHP_SELF']);
+    }
+
+    if (SessionCache::isCached($_SERVER['PHP_SELF'])) {
+        // Get the cached results.
+        $list = SessionCache::getCache($_SERVER['PHP_SELF']);
+    } else {
+        // If the list is not already cached, query now.
+        $qid = DB::query($sql);
+        // Fill an array with the items for this page.
+        while ($row = mysql_fetch_assoc($qid)) {
+            $list[] = $row;
+        }
+
+        if (isset($list) && !empty($list)) {
+            // Cache the results.
+            SessionCache::putCache($list, $_SERVER['PHP_SELF']);
+        }
+    }
+
+    return $list;
+}
+
+function updateRank($ranks)
+{
+    if (!is_array($ranks)) {
+        App::logMsg('Saving rank failed, data posted is not an array: ' . $ranks, LOG_ERR, __FILE__, __LINE__);
+        return false;
+    }
+
+    // Break the cache because we are changing the list data.
+    SessionCache::breakCache($_SERVER['PHP_SELF']);
+
+    // 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: branches/2.0singleton/bin/module_maker/skel/public.ihtml
===================================================================
--- branches/2.0singleton/bin/module_maker/skel/public.ihtml	(revision 122)
+++ branches/2.0singleton/bin/module_maker/skel/public.ihtml	(revision 122)
@@ -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: branches/2.0singleton/bin/module_maker/skel/public.php
===================================================================
--- branches/2.0singleton/bin/module_maker/skel/public.php	(revision 122)
+++ branches/2.0singleton/bin/module_maker/skel/public.php	(revision 122)
@@ -0,0 +1,125 @@
+<?php
+/**
+ * %PUBLIC_SCRIPT%
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ *
+ * 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->addPage(_("%TITLE%"), $_SERVER['PHP_SELF']);
+$nav->setFeature(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 = '0000-00-00')
+        <##>AND (expire_date > CURDATE() OR expire_date = '0000-00-00')
+    ");
+    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->addPage($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: branches/2.0singleton/bin/module_maker/skel/public_list.ihtml
===================================================================
--- branches/2.0singleton/bin/module_maker/skel/public_list.ihtml	(revision 122)
+++ branches/2.0singleton/bin/module_maker/skel/public_list.ihtml	(revision 122)
@@ -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 (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 (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: branches/2.0singleton/bin/module_maker/sql.cli.php
===================================================================
--- branches/2.0singleton/bin/module_maker/sql.cli.php	(revision 122)
+++ branches/2.0singleton/bin/module_maker/sql.cli.php	(revision 122)
@@ -0,0 +1,197 @@
+#!/usr/local/bin/php -q
+<?php
+/**
+ * sql.cli.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+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]\n", basename($_SERVER['argv'][0])));
+}
+
+// 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) {
+            // Toggle types.
+            $c[$field] = "'\" . DB::escapeString(\$auth->getVal('user_id')) . \"'";
+        } else if ('added_datetime' == $field || 'modified_datetime' == $field) {
+            // DB record insertion datetime.
+            $c[$field] = "NOW()";
+        } 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: branches/2.0singleton/bin/module_maker/validation.cli.php
===================================================================
--- branches/2.0singleton/bin/module_maker/validation.cli.php	(revision 122)
+++ branches/2.0singleton/bin/module_maker/validation.cli.php	(revision 122)
@@ -0,0 +1,287 @@
+#!/usr/local/bin/php -q
+<?php
+/**
+ * validation.cli.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+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]);
+        $is_primary_key = ('PRI' == $col[3]);
+        $unsigned = preg_match('/\s*unsigned\s*$/i', $col[1]);
+
+        if (in_array($field, $exclude)) {
+            continue;
+        }
+
+        // ----------- isEmpty ------------
+        $o[] = "\$fv->isEmpty('$field', _(\"<strong>$title</strong> cannot be blank.\"));";
+
+        // ----------- 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 = 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 'tinyint' :
+        case 'bit' :
+        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';
+            $min = -3.40282E+38;
+            $max = 3.40282E+38;
+            break;
+
+        case 'double' :
+        case 'double precision' :
+        case 'real' :
+        case 'decimal' :
+        case 'dec' :
+        case 'numeric' :
+            $len_type = 'num';
+            $min = -1.79769E+308;
+            $max = 1.79769E+308;
+            break;
+
+        default :
+            $len_type = null;
+            break;
+        }
+        if ($max_length > 0 && $len_type == 'setenum') {
+            $o[] = "\$fv->stringLength('$field', 0, $max_length, _(\"<strong>$title</strong> has an invalid selection.\"));";
+        }
+        if ($max_length > 0 && $len_type == 'string') {
+            $o[] = "\$fv->stringLength('$field', 0, $max_length, _(\"<strong>$title</strong> must contain less than " . ($max_length+1) . " characters.\"));";
+        }
+        if ($len_type == 'num') {
+            $o[] = "\$fv->numericRange('$field', $min, $max, _(\"<strong>$title</strong> must be a valid number between $min and $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', _(\"<strong>$title</strong> must be an integer.\")$negative_ok);";
+            break;
+
+        case 'float' :
+        case 'float' :
+        case 'double' :
+        case 'double' :
+        case 'real' :
+        case 'decimal' :
+        case 'dec' :
+        case 'numeric' :
+            $negative_ok = $unsigned ? '' : ', true';
+            $o[] = "\$fv->isFloat('$field', _(\"<strong>$title</strong> must be a valid number.\")$negative_ok);";
+            break;
+
+        case 'date' :
+        case 'datetime' :
+            $o[] = "\$fv->validateStrDate('$field', _(\"<strong>$title</strong> must be a valid date in YYYY-MM-DD format.\"));";
+            $o[] = "\$fv->checkRegex('$field', '/^\d{4}-\d{2}-\d{2}$/', true, _(\"<strong>$title</strong> must be a valid date in YYYY-MM-DD format.\"));";
+            break;
+
+        case 'timestamp' :
+            $o[] = "\$fv->checkRegex('$field', '/^\d{14}$/', true, _(\"<strong>$title</strong> must be a valid mysql timestamp in YYYYMMDDhhmmss format.\"));";
+            break;
+
+        case 'time' :
+            $o[] = "\$fv->checkRegex('$field', '/^\d{1,3}:\d{2}:\d{2}$/', true, _(\"<strong>$title</strong> must be a valid time in hh:mm:ss format.\"));";
+            break;
+
+        case 'year' :
+            $o[] = "\$fv->checkRegex('$field', '/^\d{4}$/', true, _(\"<strong>$title</strong> must be a valid year in YYYY format.\"));";
+            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);
+
+echo "function validateInput()
+{
+    global \$fv;
+
+    ";
+echo join("\n    ", $o);
+echo "
+}";
+?>
Index: branches/2.0singleton/bin/pedit_permissions.sh
===================================================================
--- branches/2.0singleton/bin/pedit_permissions.sh	(revision 122)
+++ branches/2.0singleton/bin/pedit_permissions.sh	(revision 122)
@@ -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: branches/2.0singleton/css/admin.inc.css
===================================================================
--- branches/2.0singleton/css/admin.inc.css	(revision 122)
+++ branches/2.0singleton/css/admin.inc.css	(revision 122)
@@ -0,0 +1,292 @@
+/**
+ * admin.css 
+ * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
+ *
+ * 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; }
+
+/*_____________________ 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,
+.commandtext { font-size: 70%; }
+
+/*_____________________ HEADER ____________________*/
+#userinfo {
+    width: 98%;
+    margin: 10px auto 2px;
+    background-color: #fff;
+    color: #333;
+    text-align: right;
+}
+
+#userinfo p {
+    margin: 0;
+    padding: 0;
+    font-size: 60%;
+}
+
+#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: 120%;
+    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 gray;
+    margin-left: 0em;
+    padding: 1px 0 2px 6px;
+    font: bold 60% Verdana, sans-serif;
+}
+
+#leftnav #navlist li a {
+    text-decoration: none;
+    padding: 1px;
+}
+
+#leftnav #navlist li.label {
+    color: #999;
+	padding-top: 10px;
+	font-size: 75%;
+	font-weight: bold;
+}
+
+#leftnav #navlist li a:link, #navlist li a:visited {
+    color: #336;
+}
+
+#leftnav #navlist li a:hover, #navlist li a:active {
+    color: #C00;
+}
+
+#leftnav a.addlink {
+    margin-left: 0.5em;
+}
+
+/*_____________________ CONTENT ____________________*/
+#content {
+    margin-left: 9em;
+    border-left: 1px solid gray;
+    padding: 0.75em 0.5em;
+    background-color: #FFF;
+}
+
+#content h2 {
+    font-size: 100%;
+    margin: 0 0 .5em 0;
+}
+
+#content h3 {
+    font-size: 85%;
+    margin: 0 0 .5em 0;
+}
+
+#content h4 {
+    font-size: 70%;
+    margin: 0 0 .5em 0;
+}
+
+#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-right: 6px;
+	font-size: 70%;
+	background-color: #CCC;
+	border-bottom: 1px solid #000;
+}
+
+table.list td {
+	font-size: 70%;
+	padding: 2px;
+	border-bottom: 1px dotted #000;
+}
+
+/* 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
+{
+	padding: 5px;
+	margin: 0 0 .5em 0;
+	border: black 1px dotted;
+	background-color: #DDD;
+	color: inherit;
+}
+
+.commanditem
+{
+	margin: 0 10px 0 0;
+	white-space: nowrap;
+}
+
+.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: 60%;
+    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: branches/2.0singleton/css/codebase.inc.css
===================================================================
--- branches/2.0singleton/css/codebase.inc.css	(revision 122)
+++ branches/2.0singleton/css/codebase.inc.css	(revision 122)
@@ -0,0 +1,50 @@
+/**
+ * codebase.css 
+ * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
+ *
+ * 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.
+ */
+
+/*_____________________ SC Messaging ____________________*/
+.sc-msg { font-family: verdana,geneva,arial,sans-serif; }
+
+.sc-msg-warning,
+.sc-msg-error,
+.sc-msg-success,
+.sc-msg-notice {
+	margin: 0 0 6px 0;
+	padding: 5px;
+	background: #fff;
+	color: #f00;
+	border: 1px solid #f00;
+}
+.sc-msg-success { color: #090; border-color: #090; }
+.sc-msg-notice { color: #C60; border-color: #C60; }
+
+/*_____________________ PEdit Forms ____________________*/
+
+.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;
+}
Index: branches/2.0singleton/css/utilities.inc.css
===================================================================
--- branches/2.0singleton/css/utilities.inc.css	(revision 122)
+++ branches/2.0singleton/css/utilities.inc.css	(revision 122)
@@ -0,0 +1,47 @@
+/**
+ * utilities.css 
+ * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
+ *
+ * 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-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       { margin: 0 0.5em 0 0; }
+
+input.sc-tiny, textarea.sc-tiny, select.sc-tiny          { width: 4.75em; }
+input.sc-small, textarea.sc-small, select.sc-small       { width: 10em; }
+input.sc-medium, textarea.sc-medium, select.sc-medium    { width: 20.5em; }
+input.sc-large, textarea.sc-large, select.sc-large       { width: 41.5em; }
+input.sc-full, textarea.sc-full, select.sc-full          { width: 100%; }
+select.sc-small                                    { width: 10em; }
+select.sc-medium                                   { width: 20.5em; }
+
+textarea.sc-twolines, select.sc-twolines              { height: 3em; }
+textarea.sc-short, select.sc-short                    { height: 8em; }
+textarea.sc-tall, select.sc-tall                      { height: 25em; }
+textarea.sc-fullscreen, select.sc-fullscreen          { height: 40em; }
+
+/*_____________________ UTILITIES ____________________*/
+.sc-nowrap { white-space: nowrap; }
+.sc-clearboth { clear: both; }
+.sc-center { text-align: center; }
+.sc-right { text-align: right; }
+.sc-padright { padding: 0 10px 0 0; }
+.sc-padleft { padding: 0 0 0 10px; }
+.sc-help { color: #999; font-weight: normal; }
+.sc-monospaced { font-family: monaco, courier; font-weight: normal; }
+
+/* .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: branches/2.0singleton/docs/coding_standards.txt
===================================================================
--- branches/2.0singleton/docs/coding_standards.txt	(revision 122)
+++ branches/2.0singleton/docs/coding_standards.txt	(revision 122)
@@ -0,0 +1,367 @@
+Strangecode coding standards
+29 Aug 2005 20:12:49
+
+
+======================================================================
+Preable
+======================================================================
+
+We're following the PEAR coding standards, with minor modifications.
+
+This is 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.ihtml         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, usually outputs TEXT, not HTML.
+
+dbname.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
+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 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.
+
+Recommend using studlyCaps for function names, to distinguish from php
+internal functions which standard on under_score_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);'.
+
+
+======================================================================
+String concatination
+======================================================================
+
+Always include a space before and after the concatonation 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 processesor is bypassed and content goes
+directly to output:
+
+    <?php
+    if (something) {
+        // Here comes the HTML...
+        ?>
+        <div align="right" class="sc-tiny">
+        [&nbsp;<a href="<?php echo App::oHREF('contact.php') ?>">Contact us</a>&nbsp;]
+        </div>
+        <?php
+    } else if (somethingelse) {
+        ?><h1>Just a little 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
+
+    <?php
+    /**
+     * Description of function.
+     *
+     * @access  public
+     * @param   string  $db_table   Database table.
+     * @param   string  $db_column  Database column containing set or enum datatype.
+     * @return  array               The set/enum values or false on failure.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @version 1.0
+     * @since   24 Feb 2004 19:34:04
+     */
+    function getSetEnumFieldValues()
+    {
+        $qid = DB::query("SHOW COLUMNS FROM $db_table LIKE '$db_col'",false);
+
+        $row = mysql_fetch_row($qid);
+        if (preg_match('/^enum|^set/i', $row[1]) && preg_match_all("/'([^']*)'/", $row[1], $match)) {
+            return $match[1];
+        } else {
+            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 <?php ?> shorthand. Even
+use <?php echo $name ?> instead of <?php echo $name; ?>.
+
+
+======================================================================
+php.ini settings
+======================================================================
+
+All code should work with register_globals = Off. This means using
+$_GET, $_POST, $_COOKIE, $_SESSION,
+$_SERVER, and $_ENV to access all 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.
+
+
+======================================================================
+XHTML 1.0 Compliance
+======================================================================
+
+All HTML should be valid XHTML 1.0 verfied 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 aways 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 prefered over using aliases of table names (a.username, l.location_address).
+
+Table names should be singular (user_tbl instead of users_tbl).
Index: branches/2.0singleton/docs/example_config.inc.php
===================================================================
--- branches/2.0singleton/docs/example_config.inc.php	(revision 122)
+++ branches/2.0singleton/docs/example_config.inc.php	(revision 122)
@@ -0,0 +1,100 @@
+<?php
+/**
+ * _config.inc.php lives in the document root of the site/application. It is the beginning of everything.
+ *
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information.
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.2
+ * @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 ifferent 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 tempates.
+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/Auth_SQL.inc.php';
+require_once 'codebase/lib/App.inc.php';
+
+// Primary application class.
+$app =& App::getInstance('admin');
+$app->setParam(array(
+    'site_name' => 'WWW Admin',
+    'site_email' => 'hello@example.com',
+    'redirect_home_url' => '/admin/',
+
+    'date_format' => 'd M Y',
+    'sql_date_format' => '%e %b %Y',
+    'sql_time_format' => '%k:%i',
+    'character_set' => 'utf-8',
+
+    'enable_session' => true,
+    'enable_db_session_handler' => false,
+    'session_use_cookies' => true,
+
+    '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,
+
+    'log_directory' => COMMON_BASE . '/log',
+    'log_filename' => 'admin_log',
+    'log_file_priority' => LOG_DEBUG,
+    'log_email_priority' => false,
+    'log_sms_priority' => false,
+    'log_screen_priority' => false,
+    'log_to_email_address' => 'log@example.com',
+    'log_to_sms_address' => 'sms@example.com',
+));
+// 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 must be included after App.
+require_once 'global/db_auth.inc.php';
+
+// Most configuration parameters must be set before starting the App.
+$app->start();
+
+// User authentication.
+$auth = new Auth_SQL('admin');
+$auth->setParam(array(
+    'db_table'          => 'admin_tbl',
+    'db_primary_key'    => 'admin_id',
+    'login_url'         => '/admin/login.php'
+));
+
+// Set CSS files.
+require_once 'codebase/lib/CSS.inc.php';
+$css = new CSS();
+$css->setParam(array('cache_css' => true));
+$css->setFile('codebase/css/codebase.inc.css');
+$css->setFile('codebase/css/utilities.inc.css');
+$css->setFile('codebase/css/admin.inc.css');
+
+// Nav class for titles, breadcrumbs, and page features.
+require_once 'codebase/lib/Nav.inc.php';
+$nav = new Nav();
+$nav->path_delimiter = ' / ';
+$nav->last_crumb_format = '<b>%s</b>';
+
+?>
Index: branches/2.0singleton/docs/example_script_template.php
===================================================================
--- branches/2.0singleton/docs/example_script_template.php	(revision 122)
+++ branches/2.0singleton/docs/example_script_template.php	(revision 122)
@@ -0,0 +1,41 @@
+<?php
+/**
+ * <##>
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information.
+ * @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: branches/2.0singleton/docs/example_virtualhost_directive.txt
===================================================================
--- branches/2.0singleton/docs/example_virtualhost_directive.txt	(revision 122)
+++ branches/2.0singleton/docs/example_virtualhost_directive.txt	(revision 122)
@@ -0,0 +1,17 @@
+<VirtualHost 216.110.45.174:80>
+    ServerName www.example.com
+    ServerAlias *.example.com example.com
+    SetEnv USER user
+    SetEnv DB_NAME carl
+    SetEnv DB_USER user_asdf
+    SetEnv DB_PASS pass_asdf
+    SetEnv SIGNING_KEY wekgbb
+    DocumentRoot /home/user/www.example.com/html
+    Alias /logs /home/user/www.example.com/log/webalizer
+    Alias /admin /home/user/www.example.com/admin
+    CustomLog /home/user/www.example.com/log/access_log combined
+    ErrorLog /home/user/www.example.com/log/error_log
+    <Directory "/home/user/www.example.com">
+        php_admin_value open_basedir "/home/user/www.example.com:/usr/local/lib/php:/tmp"
+    </Directory>
+</VirtualHost>
Index: branches/2.0singleton/docs/file_layout.txt
===================================================================
--- branches/2.0singleton/docs/file_layout.txt	(revision 122)
+++ branches/2.0singleton/docs/file_layout.txt	(revision 122)
@@ -0,0 +1,113 @@
+Updated: 26 Aug 2005 14:37:42
+
+======================================================================================
+Codebase can be located anywhere, as long as it is in the application's include path.
+======================================================================================
+
+codebase/
+    bin/
+        (generic command-line scripts and applications)
+        file_importer.php
+        install.sh
+        pedit_permissions.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
+    docs/
+        codebase_script_template.php
+        coding_standards.txt
+        example_virtualhost_directive.txt
+        file_layout.txt
+        software_licence.txt
+        todo.txt
+    lib/
+        App.inc.php (functions dealing with 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)
+        CSS.inc.php
+        DB.inc.php (database abstraction layer.)
+        DBSessionHandler.inc.php (database session handler.)
+        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)
+        ImageThumb.inc.php (automated image thumbnailing routines.)
+        MCVE.inc.php
+        Nav.inc.php (navigation element management class))
+        NodeHeirarchy.php (class for manipulation of node heirarchies.)
+        PageNumbers.inc.php (class for managing and printing various elements of page numbers and pagenation)
+        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-permenent storage of values)
+        RecordLock.inc.php (db record locking system)
+        RecordVersion.inc.php (db record versioning system)
+        ScriptTimer.inc.php (timer for scripts)
+        SessionCache.inc.php (class for accessing a cache in a users session, stores any variables for quick retreival)
+        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.)
+    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
+        templates
+        versions.php
+
+
+======================================================================================
+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)
+    tei_db.mysql (db schema)
+
+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: branches/2.0singleton/docs/gpl.txt
===================================================================
--- branches/2.0singleton/docs/gpl.txt	(revision 122)
+++ branches/2.0singleton/docs/gpl.txt	(revision 122)
@@ -0,0 +1,345 @@
+            GNU GENERAL PUBLIC LICENSE
+               Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  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
+this service 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 make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  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.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+
+            GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+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
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the 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 a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE 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.
+
+             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
+convey 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 2 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, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision 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, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This 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 Library General
+Public License instead of this License.
Index: branches/2.0singleton/docs/revision_history.txt
===================================================================
--- branches/2.0singleton/docs/revision_history.txt	(revision 122)
+++ branches/2.0singleton/docs/revision_history.txt	(revision 122)
@@ -0,0 +1,77 @@
+﻿--------------------------------------------------------------------------------
+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 only dependency for other classes. It includes Utilities.inc.php silently.
+    - Update libs requiring external files, such as html templates and email templates, classes now self-contained.
+
+Things with modified interfaces:
+    - App
+    - Auth_SQL
+    - DB
+    - DBSessionHandler
+    - RecordLock
+    - RecordVersion
+    - SessionCache
+    - 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, RecordVersion, RecordLock, 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: branches/2.0singleton/lib/App.inc.php
===================================================================
--- branches/2.0singleton/lib/App.inc.php	(revision 122)
+++ branches/2.0singleton/lib/App.inc.php	(revision 122)
@@ -0,0 +1,1117 @@
+<?php
+/**
+ * App.inc.php
+ * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
+ *
+ * Primary application framework class.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 2.0
+ */
+
+// 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 {
+
+    // Name of this application.
+    var $app = '_app_';
+
+    // If App::start has run successfully.
+    var $running = false;
+
+    // Instance of database object.
+    var $db;
+
+    // Array of query arguments will be carried persistently between requests.
+    var $_carry_queries = array();
+
+    // Hash of global application parameters.
+    var $_params = array();
+
+    // Default parameters.
+    var $_param_defaults = array(
+
+        // Public name and email address for this application.
+        'site_name' => null,
+        'site_email' => null,
+        'site_url' => '', // URL automatically determined by _SERVER['HTTP_HOST'] if not set here.
+
+        // The location the user will go if the system doesn't knew where else to send them.
+        'redirect_home_url' => '/',
+
+        // SSL URL used when redirecting with App::sslOn().
+        'ssl_domain' => null,
+        'ssl_enabled' => false,
+
+        // 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' => 'Strangecode',
+        'session_use_cookies' => true,
+
+        // Use database?
+        'enable_db' => false,
+
+        // Use db-based sessions?
+        'enable_db_session_handler' => false,
+
+        // 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,
+
+        // 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 set this to 0 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.
+        '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_error_log',
+
+        // 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' => false,
+        'log_email_priority' => false,
+        'log_sms_priority' => false,
+        'log_screen_priority' => false,
+
+        // Email address to receive log event emails.
+        'log_to_email_address' => null,
+
+        // SMS Email address to receive log event SMS messages.
+        'log_to_sms_address' => null,
+
+        // A key for calculating simple cryptographic signatures. Set using as an environment variables in the httpd.conf with 'SetEnv SIGNING_KEY <key>'.
+        'signing_key' => 'aae6abd6209d82a691a9f96384a7634a',
+    );
+
+    /**
+     * This method enforces the singleton pattern for this class. Only one application is running at a time.
+     *
+     * @return  object  Reference to the global SessionCache object.
+     * @access  public
+     * @static
+     */
+    function &getInstance($app=null)
+    {
+        static $instance = null;
+
+        if ($instance === null) {
+            $instance = new App($app);
+        }
+
+        return $instance;
+    }
+
+    /**
+     * Constructor.
+     */
+    function App($app=null)
+    {
+        if (isset($app)) {
+            $this->app .= $app;
+        }
+
+        // Initialize default parameters.
+        $this->_params = array_merge($this->_params, $this->_param_defaults);
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $param     Array of parameters (key => val pairs).
+     */
+    function setParam($param=null)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (isset($param) && is_array($param)) {
+            // Merge new parameters with old overriding only those passed.
+            $_this->_params = array_merge($_this->_params, $param);
+        }
+    }
+
+    /**
+     * 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.
+     */
+    function &getParam($param=null)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if ($param === null) {
+            return $_this->_params;
+        } else if (isset($_this->_params[$param])) {
+            return $_this->_params[$param];
+        } else {
+            trigger_error(sprintf('Parameter is not set: %s', $param), E_USER_NOTICE);
+            return null;
+        }
+    }
+
+    /**
+     * Begin running this application.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   15 Jul 2005 00:32:21
+     */
+    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'));
+        }
+
+
+        /**
+         * 1. Start Database.
+         */
+
+        if (true === $this->getParam('enable_db')) {
+
+            // DB connection parameters taken from environment variables in the httpd.conf file, readable only by root.
+            if (!empty($_SERVER['DB_SERVER'])) {
+                $this->setParam(array('db_server' => $_SERVER['DB_SERVER']));
+            }
+            if (!empty($_SERVER['DB_NAME'])) {
+                $this->setParam(array('db_name' => $_SERVER['DB_NAME']));
+            }
+            if (!empty($_SERVER['DB_USER'])) {
+                $this->setParam(array('db_user' => $_SERVER['DB_USER']));
+            }
+            if (!empty($_SERVER['DB_PASS'])) {
+                $this->setParam(array('db_pass' => $_SERVER['DB_PASS']));
+            }
+
+            // The only instance of the DB object.
+            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.
+         */
+
+        // Skip session for some user agents.
+        if (preg_match('/Atomz|ApacheBench|Wget/i', getenv('HTTP_USER_AGENT'))) {
+            $this->setParam(array('enable_session' => false));
+        }
+
+        if (true === $this->getParam('enable_session')) {
+
+            // Set the session ID to one provided in GET/POST. This is necessary for linking
+            // between domains and keeping the same session.
+            if ($ses = getFormData($this->getParam('session_name'), false)) {
+                session_id($ses);
+            }
+
+            if (true === $this->getParam('enable_db_session_handler') && true === $this->getParam('enable_db')) {
+                // Database session handling.
+                require_once dirname(__FILE__) . '/DBSessionHandler.inc.php';
+                $db_save_handler = new DBSessionHandler($this->db, array(
+                    'db_table' => 'session_tbl',
+                    'create_table' => $this->getParam('db_create_tables'),
+                ));
+            }
+
+            // Session parameters.
+            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');
+            session_name($this->getParam('session_name'));
+
+            // Start the session.
+            session_start();
+
+            if (!isset($_SESSION[$this->app])) {
+                // Access session data using: $_SESSION['...'].
+                // Initialize here _after_ session has started.
+                $_SESSION[$this->app] = array(
+                    'messages' => array(),
+                    'boomerang' => array('url'),
+                );
+            }
+        }
+
+
+        /**
+         * 3. Misc setup.
+         */
+
+        // Script URI will be 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', ('on' == getenv('HTTPS') ? 'https' : 'http'), 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.
+        header('Content-type: text/html; charset=' . $this->getParam('character_set'));
+
+        $this->running = true;
+    }
+
+    /**
+     * Stop running this application.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   17 Jul 2005 17:20:18
+     */
+    function stop()
+    {
+        session_write_close();
+        restore_include_path();
+        $this->running = false;
+        if (true === $this->getParam('enable_db')) {
+            $this->db->close();
+        }
+    }
+
+
+    /**
+     * 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__.
+     */
+    function raiseMsg($message, $type=MSG_NOTICE, $file=null, $line=null)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        $message = trim($message);
+
+        if (!$_this->running || '' == $message) {
+            $_this->logMsg(sprintf('Canceled method call %s, application not running or message is an empty string.', __FUNCTION__), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Save message in session under unique key to avoid duplicate messages.
+        $msg_id = md5($type . $message . $file . $line);
+        $_SESSION[$_this->app]['messages'][$msg_id] = array(
+            'type'    => $type,
+            'message' => $message,
+            'file'    => $file,
+            'line'    => $line,
+            'count'   => (isset($_SESSION[$_this->app]['messages'][$msg_id]['count']) ? (1 + $_SESSION[$_this->app]['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_DEBUG, __FILE__, __LINE__);
+        }
+    }
+    
+    /**
+     * Returns an array of the raised messages.
+     *
+     * @access  public
+     * @return  array   List of messags in FIFO order.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   21 Dec 2005 13:09:20
+     */
+    function getRaisedMessages()
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (!$_this->running) {
+            $_this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+        
+        $output = array();
+        while (isset($_SESSION[$_this->app]['messages']) && $message = array_shift($_SESSION[$_this->app]['messages'])) {
+            $output[] = $message;
+        }
+        return $output;
+    }
+    
+    /**
+     * Resets the message list.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   21 Dec 2005 13:21:54
+     */
+    function clearRaisedMessages()
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (!$_this->running) {
+            $_this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+        
+        $_SESSION[$_this->app]['messages'] = array();
+    }
+
+    /**
+     * Prints the HTML for displaying raised messages.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   15 Jul 2005 01:39:14
+     */
+    function printRaisedMessages()
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (!$_this->running) {
+            $_this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        while (isset($_SESSION[$_this->app]['messages']) && $message = array_shift($_SESSION[$_this->app]['messages'])) {
+            ?><div class="sc-msg"><?php
+            if (error_reporting() > 0 && $_this->getParam('display_errors')) {
+                echo "\n<!-- [" . $message['file'] . ' : ' . $message['line'] . '] -->';
+            }
+            switch ($message['type']) {
+            case MSG_ERR:
+                echo '<div class="sc-msg-error">' . $message['message'] . '</div>';
+                break;
+
+            case MSG_WARNING:
+                echo '<div class="sc-msg-warning">' . $message['message'] . '</div>';
+                break;
+
+            case MSG_SUCCESS:
+                echo '<div class="sc-msg-success">' . $message['message'] . '</div>';
+                break;
+
+            case MSG_NOTICE:
+            default:
+                echo '<div class="sc-msg-notice">' . $message['message'] . '</div>';
+                break;
+
+            }
+            ?></div><?php
+        }
+    }
+
+    /**
+     * Logs messages to defined channels: file, email, sms, and screen. Repeated messages are
+     * not repeated but printed once with count.
+     *
+     * @access public
+     * @param string $message   The text description of the message.
+     * @param int    $priority  The type of message priority (in descending order):
+     *                          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
+     * @param string $file      The file where the log event occurs.
+     * @param string $line      The line of the file where the log event occurs.
+     */
+    function logMsg($message, $priority=LOG_INFO, $file=null, $line=null)
+    {
+        static $previous_events = array();
+
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        // 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;
+        }
+
+        // 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);
+        }
+
+        // Make sure to log in the system's locale.
+        $locale = setlocale(LC_TIME, 0);
+        setlocale(LC_TIME, 'C');
+
+        // Strip HTML tags except any with more than 7 characters because that's probably not a HTML tag, e.g. <email@address.com>.
+        preg_match_all('/(<[^>\s]{7,})[^>]*>/', $message, $strip_tags_allow);
+        $message = strip_tags(preg_replace('/\s+/', ' ', $message), (!empty($strip_tags_allow[1]) ? join('> ', $strip_tags_allow[1]) . '>' : null));
+
+        // 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 (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;
+        }
+        
+        // Data to be stored for a log event.
+        $event = array(
+            'date'      => date('Y-m-d H:i:s'),
+            'remote ip' => getRemoteAddr(),
+            'pid'       => (substr(PHP_OS, 0, 3) != 'WIN' ? posix_getpid() : ''),
+            'type'      => $_this->logPriorityToString($priority),
+            'file:line' => "$file : $line",
+            'url'       => (isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : ''),
+            'message'   => $message
+        );
+
+        // FILE ACTION
+        if ($_this->getParam('log_file_priority') && $priority <= $_this->getParam('log_file_priority')) {
+            $event_str = '[' . join('] [', $event) . ']';
+            error_log($event_str . "\n", 3, $_this->getParam('log_directory') . '/' . $_this->getParam('log_filename'));
+        }
+
+        // EMAIL ACTION
+        if ($_this->getParam('log_email_priority') && $priority <= $_this->getParam('log_email_priority')) {
+            $subject = sprintf('[%s %s] %s', getenv('HTTP_HOST'), $event['type'], $message);
+            $email_msg = sprintf("A %s log event occured on %s\n\n", $event['type'], getenv('HTTP_HOST'));
+            $headers = "From: codebase@strangecode.com";
+            foreach ($event as $k=>$v) {
+                $email_msg .= sprintf("%-11s%s\n", $k, $v);
+            }
+            mail($_this->getParam('log_to_email_address'), $subject, $email_msg, $headers, '-f codebase@strangecode.com');
+        }
+
+        // SMS ACTION
+        if ($_this->getParam('log_sms_priority') && $priority <= $_this->getParam('log_sms_priority')) {
+            $subject = sprintf('[%s %s]', getenv('HTTP_HOST'), $priority);
+            $sms_msg = sprintf('%s [%s:%s]', $event['message'], basename($file), $line);
+            $headers = "From: codebase@strangecode.com";
+            mail($_this->getParam('log_to_sms_address'), $subject, $sms_msg, $headers, '-f codebase@strangecode.com');
+        }
+
+        // SCREEN ACTION
+        if ($_this->getParam('log_screen_priority') && $priority <= $_this->getParam('log_screen_priority')) {
+            echo "[{$event['date']}] [{$event['type']}] [{$event['file:line']}] [{$event['message']}]\n";
+        }
+
+        // Restore original locale.
+        setlocale(LC_TIME, $locale);
+    }
+
+    /**
+     * Returns the string representation of a LOG_* integer constant.
+     *
+     * @param int  $priority  The LOG_* integer constant.
+     *
+     * @return                The string representation of $priority.
+     */
+    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 {
+            return false;
+        }
+    }
+
+    /**
+     * Sets 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   string  $query_key  The key of the query argument to save.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   14 Nov 2005 19:24:52
+     */
+    function carryQuery($query_key)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        // If not already set, and there is a non-empty value provided in the request...
+        if (!isset($_this->_carry_queries[$query_key]) && getFormData($query_key, false)) {
+            // Copy the value of the specified query argument into the _carry_queries array.
+            $_this->_carry_queries[$query_key] = getFormData($query_key);
+        }
+    }
+
+    /**
+     * 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, the session id will be propogated 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.
+     *
+     * @return string url with attached queries and, if not using cookies, the session id
+     */
+    function url($url, $carry_args=null, $always_include_sid=false)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (!$_this->running) {
+            $_this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        // 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) && !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 = 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) {
+                // 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');
+                }
+            }
+        }
+
+        // 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 propogation 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('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 .= $q . $delim . session_name() . '=' . session_id();
+            return $url;
+        } else {
+            $url .= $q;
+            return $url;
+        }
+    }
+
+    /**
+     * Returns a HTML-friendly URL processed with App::url and & replaced with &amp;
+     *
+     * @access  public
+     * @param   string  $url    Input URL to parse.
+     * @return  string          URL with App::url() and htmlentities() applied.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   09 Dec 2005 17:58:45
+     */
+    function oHREF($url, $carry_args=null, $always_include_sid=false)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        $url = $_this->url($url, $carry_args, $always_include_sid);
+
+        // Replace any & not followed by an html or unicode entity with it's &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.
+     */
+    function printHiddenSession($carry_args=null)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (!$_this->running) {
+            $_this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_DEBUG, __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) && !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 POST 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) {
+                echo '<input type="hidden" name="' . $key . '" value="' . $val . '" />';
+            }
+        }
+
+        // Include the SID if cookies are disabled.
+        if (!isset($_COOKIE[session_name()]) && !ini_get('session.use_trans_sid')) {
+            echo '<input type="hidden" name="' . session_name() . '" value="' . session_id() . '" />';
+        }
+    }
+
+    /**
+     * 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.
+     */
+    function dieURL($url, $carry_args=null, $always_include_sid=false)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (!$_this->running) {
+            $_this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_DEBUG, __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 = 'on' == 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);
+
+        header(sprintf('Location: %s', $url));
+        $_this->logMsg(sprintf('dieURL: %s', $url), LOG_DEBUG, __FILE__, __LINE__);
+
+        // End this 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::oHREF()).
+    * @param    string  $default_url    A default URL if there is not a valid specified boomerang URL.
+    * @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
+    */
+    function dieBoomerangURL($id=null, $carry_args=null, $default_url=null)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (!$_this->running) {
+            $_this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_DEBUG, __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[$_this->app]['boomerang']['url'][$id])) {
+                $url = $_SESSION[$_this->app]['boomerang']['url'][$id];
+                $_this->logMsg(sprintf('dieBoomerangURL(%s) found: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+            } else {
+                $url = end($_SESSION[$_this->app]['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()) {
+            // Ensure that the redirecting page is not also the referrer.
+            $url = getenv('HTTP_REFERER');
+            $_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) not found, using redirect_home_url: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+        }
+
+        // A redirection will never happen immediately twice.
+        // Set the time so ensure this doesn't happen.
+        $_SESSION[$_this->app]['boomerang']['time'] = time();
+        $_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?
+     */
+    function setBoomerangURL($url=null, $id=null)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (!$_this->running) {
+            $_this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+        // A redirection will never happen immediately after setting the boomerangURL.
+        // Set the time so ensure this doesn't happen. See App::validBoomerangURL for more.
+
+        if ('' != $url && is_string($url)) {
+            // Delete any boomerang request keys in the query string.
+            $url = preg_replace('/boomerang=[\w]+/', '', $url);
+
+            if (isset($_SESSION[$_this->app]['boomerang']['url']) && is_array($_SESSION[$_this->app]['boomerang']['url']) && !empty($_SESSION[$_this->app]['boomerang']['url'])) {
+                // If the URL currently exists in the boomerang array, delete.
+                while ($existing_key = array_search($url, $_SESSION[$_this->app]['boomerang']['url'])) {
+                    unset($_SESSION[$_this->app]['boomerang']['url'][$existing_key]);
+                }
+            }
+
+            if (isset($id)) {
+                $_SESSION[$_this->app]['boomerang']['url'][$id] = $url;
+            } else {
+                $_SESSION[$_this->app]['boomerang']['url'][] = $url;
+            }
+            $_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.
+     *
+     * @param string  $id     An identification tag for this url.
+     */
+    function getBoomerangURL($id=null)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (!$_this->running) {
+            $_this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (isset($id)) {
+            if (isset($_SESSION[$_this->app]['boomerang']['url'][$id])) {
+                return $_SESSION[$_this->app]['boomerang']['url'][$id];
+            } else {
+                return '';
+            }
+        } else if (is_array($_SESSION[$_this->app]['boomerang']['url'])) {
+            return end($_SESSION[$_this->app]['boomerang']['url']);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Delete the URL set for the specified $id.
+     *
+     * @param string  $id     An identification tag for this url.
+     */
+    function deleteBoomerangURL($id=null)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (!$_this->running) {
+            $_this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        $_this->logMsg(sprintf('deleteBoomerangURL(%s): %s', $id, $_this->getBoomerangURL($id)), LOG_DEBUG, __FILE__, __LINE__);
+
+        if (isset($id) && isset($_SESSION[$_this->app]['boomerang']['url'][$id])) {
+            unset($_SESSION[$_this->app]['boomerang']['url'][$id]);
+        } else if (is_array($_SESSION[$_this->app]['boomerang']['url'])) {
+            array_pop($_SESSION[$_this->app]['boomerang']['url']);
+        }
+    }
+
+    /**
+     * 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.
+     */
+    function validBoomerangURL($id=null, $use_nonspecificboomerang=false)
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (!$_this->running) {
+            $_this->logMsg(sprintf('Canceled method call %s, application not running.', __FUNCTION__), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (!isset($_SESSION[$_this->app]['boomerang']['url'])) {
+            $_this->logMsg(sprintf('validBoomerangURL(%s) no boomerang URL set.', $id), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Time is the timestamp of a boomerangURL redirection, or setting of a boomerangURL.
+        // a boomerang redirection will always occur at least several seconds after the last boomerang redirect
+        // or a boomerang being set.
+        $boomerang_time = isset($_SESSION[$_this->app]['boomerang']['time']) ? $_SESSION[$_this->app]['boomerang']['time'] : 0;
+
+        $url = '';
+        if (isset($id) && isset($_SESSION[$_this->app]['boomerang']['url'][$id])) {
+            $url = $_SESSION[$_this->app]['boomerang']['url'][$id];
+        } else if (!isset($id) || $use_nonspecificboomerang) {
+            // Use non specific boomerang if available.
+            $url = end($_SESSION[$_this->app]['boomerang']['url']);
+        }
+
+        $_this->logMsg(sprintf('validBoomerangURL(%s) testing: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+
+        if ('' == $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;
+        }
+        if ($boomerang_time >= (time() - 2)) {
+            // Last boomerang direction was more than 2 seconds ago.
+            $_this->logMsg(sprintf('validBoomerangURL(%s) not valid, boomerang_time too short: %s', $id, time() - $boomerang_time), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        $_this->logMsg(sprintf('validBoomerangURL(%s) is valid: %s', $id, $url), LOG_DEBUG, __FILE__, __LINE__);
+        return true;
+    }
+
+    /**
+     * Force the user to connect via https (port 443) by redirecting them to
+     * the same page but with https.
+     */
+    function sslOn()
+    {
+        if (!isset($_this) || !is_a($_this, 'App') && !is_subclass_of($_this, 'App')) {
+            $_this =& App::getInstance();
+        }
+
+        if (function_exists('apache_get_modules')) {
+            $modules = apache_get_modules();
+        } else {
+            // It's safe to assume we have mod_ssl if we can't determine otherwise.
+            $modules = array('mod_ssl');
+        }
+
+        if ('' == getenv('HTTPS') && $_this->getParam('ssl_enabled') && in_array('mod_ssl', $modules)) {
+            $_this->raiseMsg(sprintf(_("Secure SSL connection made to %s"), $_this->getParam('ssl_domain')), MSG_NOTICE, __FILE__, __LINE__);
+            // Always append session because some browsers do not send cookie when crossing to SSL URL.
+            $_this->dieURL('https://' . $_this->getParam('ssl_domain') . getenv('REQUEST_URI'), null, true);
+        }
+    }
+
+
+    /**
+     * to enforce the user to connect via http (port 80) by redirecting them to
+     * a http version of the current url.
+     */
+    function sslOff()
+    {
+        if ('' != getenv('HTTPS')) {
+            $this->dieURL('http://' . getenv('HTTP_HOST') . getenv('REQUEST_URI'), null, true);
+        }
+    }
+
+
+} // End.
+
+?>
Index: branches/2.0singleton/lib/Auth_File.inc.php
===================================================================
--- branches/2.0singleton/lib/Auth_File.inc.php	(revision 122)
+++ branches/2.0singleton/lib/Auth_File.inc.php	(revision 122)
@@ -0,0 +1,366 @@
+<?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'
+// ));
+
+// Available encryption types for class Auth_SQL.
+define('AUTH_ENCRYPT_MD5', 'md5');
+define('AUTH_ENCRYPT_CRYPT', 'crypt');
+define('AUTH_ENCRYPT_SHA1', 'sha1');
+define('AUTH_ENCRYPT_PLAINTEXT', 'plaintext');
+
+class Auth_File {
+
+    var $_auth = '';
+    var $_sess = '_auth_';
+    var $_params = array();
+    var $_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 AUTH_ENCRYPT_* types specified above.
+        'encryption_type' => AUTH_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.
+    var $_users = array();
+
+    /**
+     * Constructs a new htpasswd authentication object.
+     *
+     * @access public
+     *
+     * @param optional array $params  A hash containing parameters.
+     */
+    function Auth_File($auth_name=null)
+    {
+        if (isset($auth_name)) {
+            $this->_auth = $auth_name;
+            $this->_sess .= $auth_name;
+        }
+
+        // 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
+     */
+    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.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            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
+     */
+    function clearAuth()
+    {
+        $_SESSION[$this->_sess] = array('authenticated' => false);
+    }
+
+
+    /**
+     * 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.
+     */
+    function setVal($key, $val)
+    {
+        if (!isset($_SESSION[$this->_sess]['user_data'])) {
+            $_SESSION[$this->_sess]['user_data'] = array();
+        }
+        $_SESSION[$this->_sess]['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.
+     */
+    function getVal($key, $default='')
+    {
+        if (isset($_SESSION[$this->_sess][$key])) {
+            return $_SESSION[$this->_sess][$key];
+        } else if (isset($_SESSION[$this->_sess]['user_data'][$key])) {
+            return $_SESSION[$this->_sess]['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.
+     */
+    function authenticate($username, $password)
+    {
+        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.
+     */
+    function login($username, $password)
+    {
+        $username = strtolower(trim($username));
+
+        $this->clearAuth();
+
+        if (!$this->authenticate($username, $password)) {
+            // No login: failed authentication!
+            return false;
+        }
+        
+        $_SESSION[$this->_sess] = 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
+     */
+    function isLoggedIn()
+    {
+        // 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[$this->_sess]['username'], $trusted_net), LOG_DEBUG, __FILE__, __LINE__);
+        } else if (preg_match('/proxy.aol.com$/i', getRemoteAddr(true))) {
+            $user_in_trusted_network = true;
+            App::logMsg(sprintf('User %s accessing from trusted network proxy.aol.com', $_SESSION[$this->_sess]['username']), 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[$this->_sess])
+            && true === $_SESSION[$this->_sess]['authenticated']
+            && !empty($_SESSION[$this->_sess]['username'])
+            && strtotime($_SESSION[$this->_sess]['login_datetime']) > time() - $this->_params['login_timeout']
+            && strtotime($_SESSION[$this->_sess]['last_access_datetime']) > time() - $this->_params['idle_timeout']
+            && ($_SESSION[$this->_sess]['remote_ip'] == getRemoteAddr() || $user_in_trusted_network)
+        ) {
+            // User is authenticated!
+            $_SESSION[$this->_sess]['last_access_datetime'] = date('Y-m-d H:i:s');
+            return true;
+        } else if (isset($_SESSION[$this->_sess]) && true === $_SESSION[$this->_sess]['authenticated']) {
+            if (strtotime($_SESSION[$this->_sess]['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[$this->_sess]['username'])) {
+                $expire_reasons[] = 'username not found';
+            }
+            if (strtotime($_SESSION[$this->_sess]['login_datetime']) <= time() - $this->_params['login_timeout']) {
+                $expire_reasons[] = 'login_timeout expired';
+            }
+            if (strtotime($_SESSION[$this->_sess]['last_access_datetime']) <= time() - $this->_params['idle_timeout']) {
+                $expire_reasons[] = 'idle_timeout expired';
+            }
+            if ($_SESSION[$this->_sess]['remote_ip'] != getRemoteAddr() && !$user_in_trusted_network) {
+                $expire_reasons[] = sprintf('remote_ip not matched (%s != %s)', $_SESSION[$this->_sess]['remote_ip'], getRemoteAddr());
+            }
+            App::logMsg(sprintf('User %s session expired: %s', $_SESSION[$this->_sess]['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
+     */
+    function requireLogin($message='', $type=MSG_NOTICE, $file=null, $line=null)
+    {
+        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']);
+        }
+    }
+    
+    /*
+    * 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
+    */
+    function _loadHTPasswdFile()
+    {
+        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.
+     */
+    function _encrypt($password, $encrypted_password=null)
+    {
+        switch ($this->_params['encryption_type']) {
+        case AUTH_ENCRYPT_PLAINTEXT :
+            return $password;
+            break;
+
+        case AUTH_ENCRYPT_SHA1 :
+            return sha1($password);
+            break;
+
+        case AUTH_ENCRYPT_MD5 :
+            return md5($password);
+            break;
+
+        case AUTH_ENCRYPT_CRYPT :
+        default :
+            return crypt($password, $encrypted_password);
+            break;
+        }
+    }
+
+} // end class
+?>
Index: branches/2.0singleton/lib/Auth_SQL.inc.php
===================================================================
--- branches/2.0singleton/lib/Auth_SQL.inc.php	(revision 122)
+++ branches/2.0singleton/lib/Auth_SQL.inc.php	(revision 122)
@@ -0,0 +1,887 @@
+<?php
+/**
+ * The Auth_SQL:: class provides a SQL implementation for authentication.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 2.0
+ */
+
+// Available encryption types for class Auth_SQL.
+define('AUTH_ENCRYPT_MD5', 'md5');
+define('AUTH_ENCRYPT_CRYPT', 'crypt');
+define('AUTH_ENCRYPT_SHA1', 'sha1');
+define('AUTH_ENCRYPT_PLAINTEXT', 'plaintext');
+
+require_once dirname(__FILE__) . '/Email.inc.php';
+
+class Auth_SQL {
+
+    var $_auth = '';
+    var $_sess = '_auth_';
+    var $_authentication_tested;
+    var $_params = array();
+
+    // Default param values.
+    var $_default_params = array(
+
+        // Automatically create table and verify columns. Better set to false after site launch.
+        '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 encryption to use for passwords stored in the db_table. Use one of the AUTH_ENCRYPT_* types specified above.
+        'encryption_type' => AUTH_ENCRYPT_MD5,
+
+        // 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,
+
+        // 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 cheatsheet at bottom).
+        // Any integar 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(),
+
+        // 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,
+    );
+
+    /**
+     * Constructs a new authentication object.
+     *
+     * @access public
+     * @param optional array $params  A hash containing parameters.
+     */
+    function Auth_SQL($auth_name=null)
+    {
+        if (isset($auth_name)) {
+            $this->_auth = $auth_name;
+            $this->_sess .= $auth_name;
+        }
+
+        // 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')));
+        }
+    }
+
+    /**
+     * Setup the database tables for this class.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   26 Aug 2005 17:09:36
+     */
+    function initDB($recreate_db=false)
+    {
+        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_DEBUG, __FILE__, __LINE__);
+            }
+
+            // The minimal columns for a table compatable with the Auth_SQL class.
+            DB::query("CREATE TABLE IF NOT EXISTS " . $this->getParam('db_table') . " (
+                " . $this->getParam('db_primary_key') . " smallint(11) NOT NULL auto_increment,
+                " . $this->getParam('db_username_column') . " varchar(255) NOT NULL default '',
+                userpass varchar(255) NOT NULL default '',
+                first_name varchar(255) NOT NULL default '',
+                last_name varchar(255) NOT NULL default '',
+                email varchar(255) NOT NULL default '',
+                user_type enum('public', 'editor', 'admin', 'root') default NULL,
+                login_abuse_exempt enum('true') default NULL,
+                blocked enum('true') default NULL,
+                blocked_reason varchar(255) NOT NULL default '',
+                abuse_warning_level tinyint(4) NOT NULL default '0',
+                seconds_online int(11) NOT NULL default '0',
+                last_login_datetime datetime NOT NULL default '0000-00-00 00:00:00',
+                last_access_datetime datetime NOT NULL default '0000-00-00 00:00:00',
+                last_login_ip varchar(255) NOT NULL default '0.0.0.0',
+                added_by_user_id smallint(11) default NULL,
+                modified_by_user_id smallint(11) default NULL,
+                added_datetime datetime NOT NULL default '0000-00-00 00:00:00',
+                modified_datetime datetime NOT NULL default '0000-00-00 00:00:00',
+                PRIMARY KEY (" . $this->getParam('db_primary_key') . "),
+                KEY " . $this->getParam('db_username_column') . " (" . $this->getParam('db_username_column') . "),
+                KEY userpass (userpass),
+                KEY email (email)
+            )");
+
+            if (!DB::columnExists($this->getParam('db_table'), array(
+                $this->getParam('db_primary_key'),
+                $this->getParam('db_username_column'),
+                'userpass',
+                'first_name',
+                'last_name',
+                'email',
+                'user_type',
+                '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_DEBUG, __FILE__, __LINE__);
+                }
+                DB::query("CREATE TABLE IF NOT EXISTS " . $this->getParam('db_login_table') . " (
+                    " . $this->getParam('db_primary_key') . " smallint(11) NOT NULL default '0',
+                    login_datetime datetime NOT NULL default '0000-00-00 00:00:00',
+                    remote_ip_binary char(32) NOT NULL default '',
+                    KEY " . $this->getParam('db_primary_key') . " (" . $this->getParam('db_primary_key') . "),
+                    KEY login_datetime (login_datetime),
+                    KEY remote_ip_binary (remote_ip_binary)
+                )");
+
+                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
+     */
+    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.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            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
+     */
+    function clearAuth()
+    {
+        $this->initDB();
+
+        DB::query("
+            UPDATE " . $this->_params['db_table'] . " SET
+            seconds_online = seconds_online + (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)),
+            last_login_datetime = '0000-00-00 00:00:00'
+            WHERE " . $this->_params['db_primary_key'] . " = '" . $this->getVal('user_id') . "'
+        ");
+        $_SESSION[$this->_sess] = array('authenticated' => false);
+    }
+
+    /**
+     * 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.
+     */
+    function setVal($key, $val)
+    {
+        if (!isset($_SESSION[$this->_sess]['user_data'])) {
+            $_SESSION[$this->_sess]['user_data'] = array();
+        }
+        $_SESSION[$this->_sess]['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.
+     */
+    function getVal($key, $default='')
+    {
+        if (isset($_SESSION[$this->_sess][$key])) {
+            return $_SESSION[$this->_sess][$key];
+        } else if (isset($_SESSION[$this->_sess]['user_data'][$key])) {
+            return $_SESSION[$this->_sess]['user_data'][$key];
+        } else {
+            return $default;
+        }
+    }
+
+    /**
+     * Find out if a set of login credentials are valid.
+     *
+     * @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.
+     */
+    function authenticate($username, $password)
+    {
+        $this->initDB();
+
+        // Query DB for user matching credentials.
+        // FIXME: Cannot compare crypt style passwords this way.
+        $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) . "'
+            AND BINARY userpass = '" . DB::escapeString($this->encryptPassword($password)) . "'
+        ");
+
+        // Return user data if found.
+        if ($user_data = mysql_fetch_assoc($qid)) {
+            App::logMsg(sprintf('Authentication successful for %s %s (%s)', $this->_auth, $user_data['user_id'], $username), LOG_INFO, __FILE__, __LINE__);
+            return $user_data;
+        } else {
+            App::logMsg(sprintf('Authentication failed for %s %s (encrypted attempted password: %s)', $this->_auth, $username, $this->encryptPassword($password)), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    /**
+     * If user authenticated, register login into session.
+     *
+     * @access private
+     * @param string $username     The username to check.
+     * @param string $password     The password to compare to username.
+     * @return boolean  Whether or not the credentials are valid.
+     */
+    function login($username, $password)
+    {
+        $this->initDB();
+
+        $this->clearAuth();
+
+        if (!$user_data = $this->authenticate($username, $password)) {
+            // No login: failed authentication!
+            return false;
+        }
+
+        // Register authenticated session.
+        $_SESSION[$this->_sess] = array(
+            'authenticated'         => true,
+            'user_id'               => $user_data['user_id'],
+            'auth_name'             => $this->_auth,
+            'username'              => $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($username, $this->_params['login_abuse_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 (!empty($user_data['blocked'])) {
+
+                App::logMsg(sprintf('%s %s (%s) login failed due to blocked account: %s', ucfirst($this->_auth), $this->getVal('user_id'), $this->getVal('username'), $this->getVal('blocked_reason')), LOG_NOTICE, __FILE__, __LINE__);
+
+                switch ($user_data['blocked_reason']) {
+                    case 'account abuse' :
+                        App::raiseMsg(sprintf(_("This account has been blocked due to possible account abuse. Please contact us to reactivate."), null), MSG_WARNING, __FILE__, __LINE__);
+                        break;
+                    default :
+                        App::raiseMsg(sprintf(_("This account is currently not active. %s"), $user_data['blocked_reason']), MSG_WARNING, __FILE__, __LINE__);
+                        break;
+                }
+
+                // No login: user is blocked!
+                $this->clearAuth();
+                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.
+        **/
+        if ($this->getParam('abuse_detection') && !$this->getVal('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->getVal('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->getVal('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 login information 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 %s %s (%s) from IP %s', $this->_auth, $this->getVal('user_id'), $this->getVal('username'), $this->getVal('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 %s %s (%s) from IP %s', $this->_auth, $this->getVal('user_id'), $this->getVal('username'), $this->getVal('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->getVal('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->getVal('user_id') . "'");
+                // No login: reset password because of account abuse!
+                $this->clearAuth();
+                return false;
+            }
+
+            // Update the login counter table with this login access. Convert IP to binary.
+            DB::query("
+                INSERT INTO " . $this->_params['db_login_table'] . " (
+                    " . $this->_params['db_primary_key'] . ",
+                    login_datetime,
+                    remote_ip_binary
+                ) VALUES (
+                    '" . $this->getVal('user_id') . "',
+                    '" . $this->getVal('login_datetime') . "',
+                    '" . sprintf('%032b', ip2long($this->getVal('remote_ip'))) . "'
+                )
+            ");
+        }
+
+        // Update user table with this login.
+        DB::query("
+            UPDATE " . $this->_params['db_table'] . " SET
+                last_login_datetime = '" . $this->getVal('login_datetime') . "',
+                last_access_datetime = '" . $this->getVal('login_datetime') . "',
+                last_login_ip = '" . $this->getVal('remote_ip') . "'
+            WHERE " . $this->_params['db_primary_key'] . " = '" . $this->getVal('user_id') . "'
+        ");
+
+        // 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 (aol users excluded).
+     *
+     * @access public
+     */
+    function isLoggedIn($user_id=null)
+    {
+        $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 DATE_ADD(last_login_datetime, INTERVAL '" . $this->_params['login_timeout'] . "' SECOND) > NOW()
+                AND DATE_ADD(last_access_datetime, INTERVAL '" . $this->_params['idle_timeout'] . "' SECOND) > NOW()
+            ");
+            return (mysql_num_rows($qid) > 0);
+        }
+
+        // User login test need only be run once per script execution. We cache the result in the session.
+        if ($this->_authentication_tested && isset($_SESSION[$this->_sess]['authenticated'])) {
+            return $_SESSION[$this->_sess]['authenticated'];
+        }
+
+        // Tesing 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.
+        if ($trusted_net = ipInRange(getRemoteAddr(), $this->_params['trusted_networks'])) {
+            $user_in_trusted_network = true;
+            App::logMsg(sprintf('%s%s accessing from trusted network %s',
+                ucfirst($this->_auth),
+                ($this->getVal('user_id') ? ' ' . $this->getVal('user_id') . ' (' .  $this->getVal('username') . ')' : ''),
+                $trusted_net
+            ), LOG_DEBUG, __FILE__, __LINE__);
+        } else if (preg_match('/proxy.aol.com$/i', getRemoteAddr(true))) {
+            $user_in_trusted_network = true;
+            App::logMsg(sprintf('%s%s accessing from trusted network proxy.aol.com',
+                ucfirst($this->_auth),
+                ($this->getVal('user_id') ? ' ' . $this->getVal('user_id') . ' (' .  $this->getVal('username') . ')' : '')
+            ), 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[$this->_sess])
+            && true === $_SESSION[$this->_sess]['authenticated']
+            && !empty($_SESSION[$this->_sess]['username'])
+            && strtotime($_SESSION[$this->_sess]['login_datetime']) > time() - $this->_params['login_timeout']
+            && strtotime($_SESSION[$this->_sess]['last_access_datetime']) > time() - $this->_params['idle_timeout']
+            && ($_SESSION[$this->_sess]['remote_ip'] == getRemoteAddr() || $user_in_trusted_network)
+        ) {
+            // User is authenticated!
+            $_SESSION[$this->_sess]['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 + (UNIX_TIMESTAMP() - UNIX_TIMESTAMP(last_access_datetime)) + 1,
+                last_access_datetime = '" . $this->getVal('last_access_datetime') . "'
+                WHERE " . $this->_params['db_primary_key'] . " = '" . $this->getVal('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('User update failed. Record not found for %s %s (%s).', $this->_auth, $this->getVal('user_id'), $this->getVal('username')), LOG_NOTICE, __FILE__, __LINE__);
+            }
+        } else if (isset($_SESSION[$this->_sess]) && true === $_SESSION[$this->_sess]['authenticated']) {
+            // User is authenticated, but login has expired.
+            if (strtotime($_SESSION[$this->_sess]['last_access_datetime']) > time() - 43200) {
+                // Only raise message if last session is less than 12 hours old.
+                App::raiseMsg(sprintf(_("Your %s session has closed. You need to log-in again."), strtolower($this->_auth)), MSG_NOTICE, __FILE__, __LINE__);
+            }
+
+            // Log the reason for login expiration.
+            $expire_reasons = array();
+            if (empty($_SESSION[$this->_sess]['username'])) {
+                $expire_reasons[] = 'username not found';
+            }
+            if (strtotime($_SESSION[$this->_sess]['login_datetime']) <= time() - $this->_params['login_timeout']) {
+                $expire_reasons[] = 'login_timeout expired';
+            }
+            if (strtotime($_SESSION[$this->_sess]['last_access_datetime']) <= time() - $this->_params['idle_timeout']) {
+                $expire_reasons[] = 'idle_timeout expired';
+            }
+            if ($_SESSION[$this->_sess]['remote_ip'] != getRemoteAddr() && !$user_in_trusted_network) {
+                $expire_reasons[] = sprintf('remote_ip not matched (%s != %s)', $_SESSION[$this->_sess]['remote_ip'], getRemoteAddr());
+            }
+            App::logMsg(sprintf('%s %s (%s) session expired: %s', ucfirst($this->_auth), $this->getVal('user_id'), $this->getVal('username'), join(', ', $expire_reasons)), LOG_INFO, __FILE__, __LINE__);
+        }
+
+        // User is not authenticated.
+        $this->clearAuth();
+        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
+     */
+    function requireLogin($message='', $type=MSG_NOTICE, $file=null, $line=null)
+    {
+        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']);
+        }
+    }
+
+    /**
+     * 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.
+     */
+    function blockAccount($user_id=null, $reason='')
+    {
+        $this->initDB();
+
+        if ($this->getParam('blocking')) {
+            if (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->getVal('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) . "'
+            ");
+        }
+    }
+
+    /**
+     * Unblocks a user in the db_table, and clears any blocked_reason.
+     */
+    function unblockAccount($user_id=null)
+    {
+        $this->initDB();
+
+        if ($this->getParam('blocking')) {
+            // Get user_id if specified.
+            $user_id = isset($user_id) ? $user_id : $this->getVal('user_id');
+            DB::query("
+                UPDATE " . $this->_params['db_table'] . " SET
+                blocked = '',
+                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.
+     */
+    function usernameExists($username)
+    {
+        $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.
+     */
+    function getUsername($user_id)
+    {
+        $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;
+        }
+    }
+
+    /**
+     * Returns a randomly generated password based on $pattern. The pattern is any
+     * sequence of 'x', 'V', 'C', 'v', 'c', or 'd' and if it is something like 'cvccv' this
+     * function will generate a pronouncable password. Recommend using more complex
+     * patterns, at minimum the US State Department standard: cvcddcvc.
+     *
+     * - x    a random upper or lower alpha character or digit
+     * - C    a random upper or lower consanant
+     * - V    a random upper or lower vowel
+     * - c    a random lowercase consanant
+     * - v    a random lowercase vowel
+     * - d    a random digit
+     *
+     * @param  string $pattern  a sequence of character types, above.
+     * @return string           a password
+     */
+    function generatePassword($pattern='CvccvCdd')
+    {
+        mt_srand((double) microtime() * 10000000);
+        $str = '';
+        for ($i=0; $i<strlen($pattern); $i++) {
+            $x = substr('bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZaeiouyAEIOUY0123456789', (mt_rand() % 60), 1);
+            $c = substr('bcdfghjklmnprstvwxz', (mt_rand() % 19), 1);
+            $C = substr('bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZ', (mt_rand() % 38), 1);
+            $v = substr('aeiouy', (mt_rand() % 6), 1);
+            $V = substr('aeiouyAEIOUY', (mt_rand() % 12), 1);
+            $d = substr('0123456789', (mt_rand() % 10), 1);
+            $str .= $$pattern{$i};
+        }
+        return $str;
+    }
+
+    /**
+     *
+     */
+    function encryptPassword($password)
+    {
+        switch ($this->_params['encryption_type']) {
+        case AUTH_ENCRYPT_PLAINTEXT :
+            return $password;
+            break;
+
+        case AUTH_ENCRYPT_CRYPT :
+            return crypt($password);
+            break;
+
+        case AUTH_ENCRYPT_SHA1 :
+            return sha1($password);
+            break;
+
+        case AUTH_ENCRYPT_MD5 :
+        default :
+            return md5($password);
+            break;
+        }
+    }
+
+    /**
+     *
+     */
+    function setPassword($user_id=null, $password)
+    {
+        $this->initDB();
+
+        // Get user_id if specified.
+        $user_id = isset($user_id) ? $user_id : $this->getVal('user_id');
+
+        // Issue the password change query.
+        DB::query("
+            UPDATE " . $this->_params['db_table'] . "
+            SET userpass = '" . DB::escapeString($this->encryptPassword($password)) . "'
+            WHERE " . $this->_params['db_primary_key'] . " = '" . DB::escapeString($user_id) . "'
+        ");
+        
+        if (mysql_affected_rows(DB::getDBH()) != 1) {
+            App::logMsg(sprintf('setPassword failed to update password for user %s', $user_id), LOG_NOTICE, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * 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.
+     */
+    function resetPassword($user_id=null, $reason='')
+    {
+        $this->initDB();
+
+        // Get user_id if specified.
+        $user_id = isset($user_id) ? $user_id : $this->getVal('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. %s %s not found.', ucfirst($this->_auth), $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 %s %s (%s).', $this->_auth, $user_data[$this->_params['db_primary_key']], $user_data[$this->_params['db_username_column']]), LOG_NOTICE, __FILE__, __LINE__);
+        } else {
+            // Body for email.
+            $email_body = <<<EOF
+Hello {NAME},
+
+Your password at {SITE_NAME} has been reset. {REASON}
+Your new login information is:
+
+USERNAME: {USERNAME}
+PASSWORD: {PASSWORD}
+
+If you have any questions or concerns please reply to this email or visit the website below.
+
+Thank you,
+{SITE_NAME}
+{SITE_URL}/
+
+EOF;
+            $email = new Email(array(
+                'to' => $user_data['email'],
+                'from' => sprintf('%s <%s>', App::getParam('site_name'), App::getParam('site_email')),
+                'subject' => sprintf('%s password change', App::getParam('site_name'))
+            ));
+            $email->setString($email_body);
+            $email->replace(array(
+                'site_name' => App::getParam('site_name'),
+                'site_url' => App::getParam('site_url'),
+                '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' => $reason,
+            ));
+            $email->send();
+        }
+
+        return array(
+            'username' => $user_data[$this->_params['db_username_column']],
+            'userpass' => $password
+        );
+    }
+
+    /**
+     * If the current user has access to the specified $security_zone, return true.
+     * If the optional $priv is supplied, test that against the zone.
+     *
+     * @param  constant $security_zone   string of comma delimited priviliges for the zone
+     * @param  string   $priv            a privilege that might be found in a zone
+     * @return bool     true if user is a member of security zone, false otherwise
+     */
+    function inClearanceZone($security_zone, $priv='')
+    {
+        return true;
+        $zone_members = preg_split('/,\s*/', $security_zone);
+        $priv = empty($priv) ? $this->getVal('priv') : $priv;
+
+        // If the current user's privilege level is NOT in that array or if the
+        // user has no privilege, return false. Otherwise the user is clear.
+        if (!in_array($priv, $zone_members) || empty($priv)) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * This function tests a list of arguments $security_zone against the priv that the current user has.
+     * If the user doesn't have one of the supplied privs, die.
+     *
+     * @param  constant $security_zone   string of comma delimited priviliges for the zone
+     */
+    function requireAccessClearance($security_zone, $message='')
+    {
+        return true;
+        $zone_members = preg_split('/,\s*/', $security_zone);
+
+        /* If the current user's privilege level is NOT in that array or if the
+         * user has no privilege, DIE with a message. */
+        if (!in_array($this->getVal('priv'), $zone_members) || !$this->getVal('priv')) {
+            $message = empty($message) ? _("You have insufficient privileges to view that page.") : $message;
+            App::raiseMsg($message, MSG_NOTICE, __FILE__, __LINE__);
+            App::dieBoomerangURL();
+        }
+    }
+
+} // end class
+
+// CIDR cheat-sheet
+//
+// Netmask              Netmask (binary)                 CIDR     Notes
+// _____________________________________________________________________________
+// 255.255.255.255  11111111.11111111.11111111.11111111  /32  Host (single addr)
+// 255.255.255.254  11111111.11111111.11111111.11111110  /31  Unusable
+// 255.255.255.252  11111111.11111111.11111111.11111100  /30    2  useable
+// 255.255.255.248  11111111.11111111.11111111.11111000  /29    6  useable
+// 255.255.255.240  11111111.11111111.11111111.11110000  /28   14  useable
+// 255.255.255.224  11111111.11111111.11111111.11100000  /27   30  useable
+// 255.255.255.192  11111111.11111111.11111111.11000000  /26   62  useable
+// 255.255.255.128  11111111.11111111.11111111.10000000  /25  126  useable
+// 255.255.255.0    11111111.11111111.11111111.00000000  /24 "Class C" 254 useable
+//
+// 255.255.254.0    11111111.11111111.11111110.00000000  /23    2  Class C's
+// 255.255.252.0    11111111.11111111.11111100.00000000  /22    4  Class C's
+// 255.255.248.0    11111111.11111111.11111000.00000000  /21    8  Class C's
+// 255.255.240.0    11111111.11111111.11110000.00000000  /20   16  Class C's
+// 255.255.224.0    11111111.11111111.11100000.00000000  /19   32  Class C's
+// 255.255.192.0    11111111.11111111.11000000.00000000  /18   64  Class C's
+// 255.255.128.0    11111111.11111111.10000000.00000000  /17  128  Class C's
+// 255.255.0.0      11111111.11111111.00000000.00000000  /16  "Class B"
+//
+// 255.254.0.0      11111111.11111110.00000000.00000000  /15    2  Class B's
+// 255.252.0.0      11111111.11111100.00000000.00000000  /14    4  Class B's
+// 255.248.0.0      11111111.11111000.00000000.00000000  /13    8  Class B's
+// 255.240.0.0      11111111.11110000.00000000.00000000  /12   16  Class B's
+// 255.224.0.0      11111111.11100000.00000000.00000000  /11   32  Class B's
+// 255.192.0.0      11111111.11000000.00000000.00000000  /10   64  Class B's
+// 255.128.0.0      11111111.10000000.00000000.00000000  /9   128  Class B's
+// 255.0.0.0        11111111.00000000.00000000.00000000  /8   "Class A"
+//
+// 254.0.0.0        11111110.00000000.00000000.00000000  /7
+// 252.0.0.0        11111100.00000000.00000000.00000000  /6
+// 248.0.0.0        11111000.00000000.00000000.00000000  /5
+// 240.0.0.0        11110000.00000000.00000000.00000000  /4
+// 224.0.0.0        11100000.00000000.00000000.00000000  /3
+// 192.0.0.0        11000000.00000000.00000000.00000000  /2
+// 128.0.0.0        10000000.00000000.00000000.00000000  /1
+// 0.0.0.0          00000000.00000000.00000000.00000000  /0   IP space
+?>
Index: branches/2.0singleton/lib/AuthorizeNet.inc.php
===================================================================
--- branches/2.0singleton/lib/AuthorizeNet.inc.php	(revision 122)
+++ branches/2.0singleton/lib/AuthorizeNet.inc.php	(revision 122)
@@ -0,0 +1,280 @@
+<?php
+// 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";
+// }
+
+/**
+ * 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
+ */
+
+require_once dirname(__FILE__) . '/Utilities.inc.php';
+
+class AuthorizeNet
+{
+    var $post_url = ''; // The URL to post data to.
+    var $md5_hash_value = ','; // A custom value for the response delimination character.
+    var $_results = array();
+    var $_params = array();
+    var $_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'      => '',
+    );
+
+    // Array of response names. Used in the results array.
+    var $_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.
+     */
+    function AuthorizeNet($params = array())
+    {
+        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';
+
+        // A custom value for the response delimination character.
+        $this->md5_hash_value = isset($params['md5_hash_value']) ? $params['md5_hash_value'] : '';
+
+        // Set default parameters.
+        $this->_params = $this->_default_params;
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    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);
+        } 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.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            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
+     */
+    function process()
+    {
+        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_ERROR, __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_ERROR, __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);
+        curl_close($ch);
+
+        if (!$result) {
+            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.
+     */
+    function getResult($key=null)
+    {
+        if (isset($key)) {
+            return $this->_results[$key];
+        } else {
+            return $this->_results;
+        }
+    }
+
+    /**
+     * Tests a returned md5 hash value with a locally computated one.
+     *
+     * @access public
+     *
+     * @return bool             True if the hash is valid, false otherwise.
+     */
+    function validMD5Hash()
+    {
+        return (
+            strtolower($this->getResult('x_md5_hash')) == strtolower(md5(
+                $this->md5_hash_value .
+                $this->getParam('x_login') .
+                $this->getResult('x_trans_id') .
+                $this->getResult('x_amount')
+            ))
+        );
+    }
+
+    /**
+     * Reset all variables. Call before beginning a new transaction.
+     *
+     * @access 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.
+     */
+    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: branches/2.0singleton/lib/CSS.inc.php
===================================================================
--- branches/2.0singleton/lib/CSS.inc.php	(revision 122)
+++ branches/2.0singleton/lib/CSS.inc.php	(revision 122)
@@ -0,0 +1,139 @@
+<?php
+/**
+ * CSS.inc.css
+ * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
+ *
+ * Dynamically outputs cached CSS data.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.2
+ */
+class CSS {
+
+    // Include these style sheets.
+    var $_css_files = array('default' => array());
+
+    // CSS object parameters.
+    var $_params = array(
+        'cache_css' => true,
+        'character_set' => 'utf-8',
+    );
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    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);
+        } 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.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            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   $apps   App name string or array of app names.
+     * @return  bool    True on success, false on failure.
+     */
+    function setFile($file, $apps='')
+    {
+        if (!is_array($apps)) {
+            $apps = array($apps);
+        }
+
+        if ($fp = fopen($file, 'r', true)) {
+            foreach ($apps as $app) {
+                $app = '' == $app ? 'default' : $app;
+                $this->_css_files[$app][] = $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.
+     */
+    function headers($app='')
+    {
+        $app = '' == $app ? 'default' : $app;
+
+        if (empty($this->_css_files[$app])) {
+            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[$app], array(__FILE__)) as $file) {
+            $files_mtime[] = statIncludePath($file, 'mtime');
+        }
+        sort($files_mtime, SORT_NUMERIC);
+        $latest_mtime = array_pop($files_mtime);
+
+        if ($this->_params['cache_css']) {
+            header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $latest_mtime) . ' GMT');
+            header('Cache-Control: public, max-age=86400');
+        } else {
+            header('Expires: -1');
+            header('Pragma: no-cache');
+            header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
+        }
+        header('Content-Type: text/css; charset=' . $this->_params['character_set']);
+    }
+
+    /**
+     * Include CSS files specified by setFile().
+     *
+     * @access  public
+     *
+     * @return  bool    False if no files have been set.
+     */
+    function output($app='')
+    {
+        $app = '' == $app ? 'default' : $app;
+
+        if (empty($this->_css_files[$app])) {
+            App::logMsg(sprintf('CSS::output called without specifying any files.', null), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        foreach ($this->_css_files[$app] as $file) {
+            include $file;
+        }
+    }
+
+}
+?>
Index: branches/2.0singleton/lib/Captcha.inc.php
===================================================================
--- branches/2.0singleton/lib/Captcha.inc.php	(revision 122)
+++ branches/2.0singleton/lib/Captcha.inc.php	(revision 122)
@@ -0,0 +1,239 @@
+<?php
+/**
+ * Captcha.inc.php
+ * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
+ *
+ * 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() ?>
+-------------------------------------------------------------------------------------
+ */
+class Captcha {
+
+    var $secret_key = 'some random seed text for the md5';
+    var $random_number;
+    var $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
+     */
+    function Captcha()
+    {
+        $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
+     */
+    function getAsciiNumber($num=null)
+    {
+        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
+     */
+    function printAsciiNumber()
+    {
+        $ascii = $this->getAsciiNumber($this->random_number);
+        ?><pre name="sc-captcha" 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
+     */
+    function printForm()
+    {
+        $hash = $this->_getMD5key($this->random_number);
+        ?>
+        <input name="sc-captcha-input" id="sc-captcha-input" 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
+     */
+    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
+     */
+    function _getRandomNumber()
+    {
+        return 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
+     */
+    function _getMD5key($input)
+    {
+        return md5($this->secret_key . $input);
+    }
+
+}
+
+?>
Index: branches/2.0singleton/lib/DB.inc.php
===================================================================
--- branches/2.0singleton/lib/DB.inc.php	(revision 122)
+++ branches/2.0singleton/lib/DB.inc.php	(revision 122)
@@ -0,0 +1,394 @@
+<?php
+/**
+ * DB.inc.php
+ * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
+ *
+ * DB abstraction layer.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.0.1
+ */
+
+class DB {
+
+    // If DB::connect has successfully opened a db connection.
+    var $_connected = false;
+
+    // Database handler.
+    var $dbh;
+
+    // Hash of DB parameters.
+    var $_params = array();
+
+    // Default parameters.
+    var $_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,
+
+        // 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.
+    );
+
+    // Translate between HTML and MySQL character set names.
+    var $mysql_character_sets = array(
+        'utf-8' => 'utf8',
+        'iso-8859-1' => 'latin1',
+    );
+
+    // Caches.
+    var $existing_tables;
+    var $table_columns;
+
+    /**
+     * This method enforces the singleton pattern for this class.
+     *
+     * @return  object  Reference to the global SessionCache object.
+     * @access  public
+     * @static
+     */
+    function &getInstance()
+    {
+        static $instance = null;
+
+        if ($instance === null) {
+            $instance = new DB();
+        }
+
+        return $instance;
+    }
+
+    /**
+     * Constructor.
+     */
+    function DB()
+    {
+        // Initialize default params.
+        if (isset($params) && is_array($params)) {
+            // Merge new parameters with old overriding only those passed.
+            $this->_params = array_merge($this->_params, $params);
+        }
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     *
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    function setParam($params)
+    {
+        if (!isset($_this) || !is_a($_this, 'DB') && !is_subclass_of($_this, 'DB')) {
+            $_this =& DB::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.
+     *
+     * @access  public
+     *
+     * @param   string  $param      The key of the parameter to return.
+     *
+     * @return  mixed               Parameter value.
+     */
+    function getParam($param)
+    {
+        if (!isset($_this) || !is_a($_this, 'DB') && !is_subclass_of($_this, 'DB')) {
+            $_this =& DB::getInstance();
+        }
+
+        if (isset($_this->_params[$param])) {
+            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
+     */
+    function connect()
+    {
+        if (!isset($_this) || !is_a($_this, 'DB') && !is_subclass_of($_this, 'DB')) {
+            $_this =& DB::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;
+        }
+
+        // Connect to database. Always create a new link to the server.
+        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) : 'Codebase MySQL error: Could not connect to server.';
+            App::logMsg($mysql_error_msg, LOG_EMERG, __FILE__, __LINE__);
+
+            // Print helpful or pretty error?
+            if ($_this->getParam('db_debug')) {
+                echo $mysql_error_msg . "\n";
+            } else {
+                echo _("This page is temporarily unavailable. It should be back up in a few minutes.");
+            }
+
+            // Die or continue without connection?
+            if ($_this->getParam('db_die_on_failure')) {
+                echo "\n\n<!-- Script execution stopped out of embarrassment. -->";
+                die;
+            } else {
+                return false;
+            }
+        }
+
+        // DB connection success!
+        $_this->_connected = true;
+
+        // Tell MySQL what character set we're useing. Available only on MySQL verions > 4.01.01.
+        $_this->query("/*!40101 SET NAMES '" . $_this->mysql_character_sets[strtolower(App::getParam('character_set'))] . "' */");
+
+        return true;
+    }
+
+    /**
+     * Close db connection.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Aug 2005 14:32:01
+     */
+    function close()
+    {
+        if (!isset($_this) || !is_a($_this, 'DB') && !is_subclass_of($_this, 'DB')) {
+            $_this =& DB::getInstance();
+        }
+
+        if (!$_this->_connected) {
+            return false;
+        }
+
+        mysql_close($_this->dbh);
+    }
+
+    /**
+     * 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
+     */
+    function getDBH()
+    {
+        if (!isset($_this) || !is_a($_this, 'DB') && !is_subclass_of($_this, 'DB')) {
+            $_this =& DB::getInstance();
+        }
+
+        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
+     */
+    function isConnected()
+    {
+        return $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
+     */
+    function escapeString($string)
+    {
+        if (!isset($_this) || !is_a($_this, 'DB') && !is_subclass_of($_this, 'DB')) {
+            $_this =& DB::getInstance();
+        }
+        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
+     */
+    function query($query, $debug=false)
+    {
+        static $_query_count = 0;
+
+        if (!isset($_this) || !is_a($_this, 'DB') && !is_subclass_of($_this, 'DB')) {
+            $_this =& DB::getInstance();
+        }
+
+        if (!$_this->_connected) {
+           return false;
+        }
+
+        $_query_count++;
+        $debugqry = preg_replace("/\n[\t ]+/", "\n", $query);
+        if ($_this->getParam('db_always_debug') || $debug) {
+            echo "<!-- ----------------- Query $_query_count ---------------------\n$debugqry\n-->\n";
+        }
+
+        // Execute!
+        $qid = mysql_query($query, $_this->dbh);
+
+        // Error checking.
+        if (!$qid || mysql_error($_this->dbh)) {
+            if ($_this->getParam('db_debug')) {
+                echo '<pre style="padding:2em; background:#ddd; font:9px monaco;">' . wordwrap(mysql_error($_this->dbh)) . '<hr>' . htmlspecialchars($debugqry) . '</pre>';
+            } else {
+                echo _("This page is temporarily unavailable. It should be back up in a few minutes.");
+            }
+            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_die_on_failure')) {
+                echo "\n\n<!-- Script execution stopped out of embarrassment. -->";
+                die;
+            }
+        }
+
+        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
+     * funtionality 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   $strict   Get fresh table info (in case DB changed).
+     * @return bool    true if given $table exists.
+     */
+    function tableExists($table, $use_cached_results=true)
+    {
+        if (!isset($_this) || !is_a($_this, 'DB') && !is_subclass_of($_this, 'DB')) {
+            $_this =& DB::getInstance();
+        }
+
+        if (!$_this->_connected) {
+            return false;
+        }
+
+        if (!isset($_this->existing_tables) || !$use_cached_results) {
+            $_this->existing_tables = array();
+            $qid = $_this->query("SHOW TABLES");
+            while (list($row) = mysql_fetch_row($qid)) {
+                $_this->existing_tables[] = $row;
+            }
+        }
+        if (in_array($table, $_this->existing_tables)) {
+            return true;
+        } else {
+            App::logMsg(sprintf('nonexistent DB table: %s.%s', $_this->getParam('db_name'), $table), LOG_ALERT, __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, or are additional fields in the table okay?
+     * @param  bool   $strict   Get fresh table info (in case DB changed).
+     * @return bool    true if given $table exists.
+     */
+    function columnExists($table, $columns, $strict=true, $use_cached_results=true)
+    {
+        if (!isset($_this) || !is_a($_this, 'DB') && !is_subclass_of($_this, 'DB')) {
+            $_this =& DB::getInstance();
+        }
+
+        if (!$_this->_connected) {
+            return false;
+        }
+
+        // Ensure the table exists.
+        if (!$_this->tableExists($table, $use_cached_results)) {
+            return false;
+        }
+
+        // For single-value columns.
+        if (!is_array($columns)) {
+            $columns = array($columns);
+        }
+
+        if (!isset($_this->table_columns[$table]) || !$use_cached_results) {
+            // Populate and cache array of current columns for this table.
+            $_this->table_columns[$table] = array();
+            $qid = $_this->query("DESCRIBE $table");
+            while ($row = mysql_fetch_row($qid)) {
+                $_this->table_columns[$table][] = $row[0];
+            }
+        }
+
+        if ($strict) {
+            // Do an exact comparison of table schemas.
+            sort($columns);
+            sort($_this->table_columns[$table]);
+            return $_this->table_columns[$table] == $columns;
+        } else {
+            // Only check that the specified columns are available in the table.
+            $match_columns = array_intersect($_this->table_columns[$table], $columns);
+            sort($columns);
+            sort($match_columns);
+            return $match_columns == $columns;
+        }
+    }
+
+    /**
+     * Reset cached items.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Aug 2005 22:10:50
+     */
+    function resetCache()
+    {
+        $this->existing_tables = null;
+        $this->table_columns = null;
+    }
+
+} // End.
+
+?>
Index: branches/2.0singleton/lib/DBSessionHandler.inc.php
===================================================================
--- branches/2.0singleton/lib/DBSessionHandler.inc.php	(revision 122)
+++ branches/2.0singleton/lib/DBSessionHandler.inc.php	(revision 122)
@@ -0,0 +1,171 @@
+<?php
+/**
+ * DBSessionHandler.inc.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information.
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.3
+ * @since   1999
+ */
+
+class DBSessionHandler {
+
+    var $db; // DB object.
+
+    var $_params = array(
+        'db_server' => 'localhost',
+        'db_name' => '',
+        'db_user' => '',
+        'db_pass' => '',
+        'db_table' => 'session_tbl',
+        'create_table' => true, // Automatically create table and verify columns. Better set to false after site launch.
+    );
+
+    /**
+     * Constructor
+     *
+     * @access  public
+     * @param
+     * @return
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   18 Jul 2005 11:02:50
+     */
+    function DBSessionHandler($db=null, $params=array())
+    {
+        $this->_params = array_merge($this->_params, $params);
+
+        if (isset($db)) {
+            if (is_a($db, 'DB')) {
+                if ($db->isConnected()) {
+                    // Use existing db connection.
+                    $this->db =& $db;
+                } else {
+                    App::logMsg(sprintf('Provided DB object is not connected. %s', mysql_error($db->dbh)), LOG_ERR, __FILE__, __LINE__);
+                }
+            } else {
+                App::logMsg(sprintf('Provided DB object is not valid. %s', gettype($db)), LOG_ERR, __FILE__, __LINE__);
+            }
+        } else {
+            // Create our own new db connection.
+            require_once dirname(__FILE__) . '/DB.inc.php';
+
+            $this->db =& new DB();
+            $this->db->setParam(array(
+                'db_server' => $this->_params['db_server'],
+                'db_name' => $this->_params['db_name'],
+                'db_user' => $this->_params['db_user'],
+                'db_pass' => $this->_params['db_pass'],
+                'db_always_debug' => $this->_params['db_always_debug'],
+                'db_debug' => $this->_params['db_debug'],
+                'db_die_on_failure' => $this->_params['db_die_on_failure'],
+            ));
+
+            // Connect to database.
+            $this->db->connect();
+        }
+
+        if (!isset($this) || !is_a($this->db, 'DB') || !$this->db->isConnected()) {
+            trigger_error('Invalid DB object or unable to connect to database.', E_USER_ERROR);
+        }
+
+        // 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();
+
+        ini_set('session.save_handler', 'user');
+        session_set_save_handler(
+            array(&$this, 'dbSessionOpen'),
+            array(&$this, 'dbSessionClose'),
+            array(&$this, 'dbSessionRead'),
+            array(&$this, 'dbSessionWrite'),
+            array(&$this, 'dbSessionDestroy'),
+            array(&$this, 'dbSessionGarbage')
+        );
+    }
+
+    /**
+     * Setup the database table for this class.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   26 Aug 2005 17:09:36
+     */
+    function initDB($recreate_db=false)
+    {
+        static $_db_tested = false;
+
+        if ($recreate_db || !$_db_tested && $this->_params['create_table']) {
+            if ($recreate_db) {
+                $this->db->query("DROP TABLE IF EXISTS " . $this->_params['db_table']);
+                App::logMsg(sprintf('Dropping and recreating table %s.', $this->_params['db_table']), LOG_DEBUG, __FILE__, __LINE__);
+            }
+            $this->db->query("CREATE TABLE IF NOT EXISTS " . $this->_params['db_table'] . " (
+                session_id char(32) NOT NULL default '',
+                session_data mediumtext NOT NULL,
+                last_access timestamp(14) 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'))) {
+                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;
+    }
+
+    function dbSessionOpen($save_path, $sess_name)
+    {
+        return true;
+    }
+
+    function dbSessionClose()
+    {
+        return true;
+    }
+
+    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->_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 '';
+    }
+
+    function dbSessionWrite($session_id, $session_data)
+    {
+        // Write the serialized session data ($session_data) to the session table
+        $this->db->query("REPLACE INTO " . $this->_params['db_table'] . "(session_id, session_data, last_access) VALUES ('" . $this->db->escapeString($session_id) . "', '" . $this->db->escapeString($session_data) . "', null)");
+
+        return true;
+    }
+
+    function dbSessionDestroy($session_id)
+    {
+        // Delete from the table all data for the session $session_id
+        $this->db->query("DELETE FROM " . $this->_params['db_table'] . " WHERE session_id = '" . $this->db->escapeString($session_id) . "'");
+
+        return true;
+    }
+
+    function dbSessionGarbage($max_lifetime=4000)
+    {
+        // Delete old values from the session table
+        $qid = $this->db->query("DELETE FROM " . $this->_params['db_table'] . " WHERE UNIX_TIMESTAMP(last_access) < " . (time() - $max_lifetime));
+
+        return true;
+    }
+}
+
+?>
Index: branches/2.0singleton/lib/Email.inc.php
===================================================================
--- branches/2.0singleton/lib/Email.inc.php	(revision 122)
+++ branches/2.0singleton/lib/Email.inc.php	(revision 122)
@@ -0,0 +1,327 @@
+<?php
+/**
+ * Email.inc.php
+ * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
+ *
+ * Easy email template usage.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.0
+-------------------------------------------------------------------------------------
+// Example.
+$email = new Email(array(
+    'to' => array($frm['email'], 'q@lovemachine.local'),
+    'from' => sprintf('%s <%s>', 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()
+    var $_params = array(
+        'to' => null,
+        'from' => null,
+        'subject' => null,
+        'headers' => null,
+        'regex' => null
+    );
+
+    // String that contains the email body.
+    var $_template;
+
+    // String that contains the email body after replacements.
+    var $_template_replaced;
+
+    /**
+     * Constructor.
+     *
+     * @access  public
+     * @param   array   $params     Array of object parameters.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   28 Nov 2005 12:59:41
+     */
+    function Email($params=null)
+    {
+        // The regex used in validEmail(). Set here instead of in the default _params above so we can use the concatination . dot.
+        // This matches an email address as complex as:
+        //      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+|[^,@]*(<)|)'   // 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*)'                                  // 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).
+     */
+    function setParam($params)
+    {
+        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;
+            }
+
+            // 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.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            return $this->_params[$param];
+        } else {
+            App::logMsg(sprintf('Parameter is not set: %s', $param), LOG_NOTICE, __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
+     */
+    function setTemplate($template)
+    {
+        // 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;
+        }
+        // This could be a new template, so reset the _template_replaced.
+        $this->_template_replaced = null;
+        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
+     */
+    function setString($string)
+    {
+        // Load file, using include_path.
+        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;
+            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
+     */
+    function replace($replacements)
+    {
+        // 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 = "/{" . preg_quote($v) . "}/i";'));
+
+        // Replacement values.
+        $replace = array_values($replacements);
+
+        // Search and replace all values at once.
+        $this->_template_replaced = preg_replace($search, $replace, $this->_template);
+    }
+
+    /**
+     * 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
+     */
+    function send($to=null, $from=null, $subject=null, $headers=null)
+    {
+        // 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(sprintf('Cannot send email to %s. SUBJECT not defined.', $this->_params['to']), 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 = wordwrap(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)) {
+            App::logMsg(sprintf('Cannot send email. Variables left unreplaced in template: %s', (isset($unreplaced_match[1]) ? $unreplaced_match[1] : '')), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // 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) {
+            $final_headers[] = sprintf('%s: %s', $key, $val);
+        }
+        $final_headers = join("\r\n", $final_headers);
+
+        // This is the address where delivery problems are sent to. We must strip off everything except the local@domain part.
+        $envelope_sender_header = sprintf('-f %s', preg_replace('/^.*<?([^\s@\[\]<>()]+\@[A-Za-z0-9.-]{1,}\.[A-Za-z]{2,5})>?$/iU', '$1', $this->_params['from']));
+
+        // Check for mail header injection attacks.
+        $full_mail_content = join("\n", array($final_to, $this->_params['subject'], $final_body, $final_headers, $envelope_sender_header));
+        if (preg_match("/(Content-Type:|MIME-Version:|Content-Transfer-Encoding:|[\n\r]Bcc:|[\n\r]Cc:)/i", $full_mail_content)) {
+            App::logMsg(sprintf('Mail header injection attack in content: %s', $full_mail_content), LOG_WARNING, __FILE__, __LINE__);
+            sleep(3);
+            return false;
+        }
+
+        // Ensure message was successfully accepted for delivery.
+        if (mail($final_to, $this->_params['subject'], $final_body, $final_headers, $envelope_sender_header)) {
+            App::logMsg(sprintf('Email successfully sent to %s', $final_to), LOG_DEBUG, __FILE__, __LINE__);
+            return true;
+        } else {
+            App::logMsg(sprintf('Email failure with parameters: %s, %s, %s, %s', $final_to, $this->_params['subject'], str_replace("\r\n", '\r\n', $final_headers), $envelope_sender_header), LOG_NOTICE, __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
+     */
+    function validEmail($email)
+    {
+        // 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 lenth constraints.
+            if (preg_match($this->getParam('regex'), $email, $e_parts) && strlen($e_parts[2]) < 64 && strlen($e_parts[3]) < 255) {
+                return true;
+            } else {
+                App::logMsg(sprintf('Invalid email: %s', $email), LOG_INFO, __FILE__, __LINE__);
+                return false;
+            }
+        }
+    }
+}
+
+?>
Index: branches/2.0singleton/lib/FormValidator.inc.php
===================================================================
--- branches/2.0singleton/lib/FormValidator.inc.php	(revision 122)
+++ branches/2.0singleton/lib/FormValidator.inc.php	(revision 122)
@@ -0,0 +1,577 @@
+<?php
+/**
+ * FormValidator.inc.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ *
+ * The FormValidator:: class provides a method for validating input from
+ * http requests and displaying errors.
+ *
+ * @author    Quinn Comendant <quinn@strangecode.com>
+ * @version   1.8
+ *
+ * Examples of use:
+ *
+ * require_once 'codebase/lib/FormValidator.inc.php';
+ * $fv = new FormValidator();
+ *
+ * $fv->isEmpty('location_name', _("<strong>Location name</strong> cannot be blank."));
+ * $fv->checkRegex('cc_exp', '/^\d{4}$|^$/', true, _("CC exp date must be in MMYY format."));
+ * $fv->isInteger('client_id', _("<strong>Client id</strong> must be an integer."));
+ * $fv->numericRange('client_id', -32768, 32767, _("<strong>Client id</strong> must be a number between -32768 and 32767."));
+ * $fv->stringLength('zip', 0, 255, _("<strong>Zip</strong> must contain less than 256 characters."));
+ * $fv->validateEmail('invoice_email');
+ * $fv->validatePhone('phone1');
+ */
+class FormValidator
+{
+
+    // Array filling with error messages.
+    var $errors = array();
+    
+    // Default error marker;
+    var $marker = ' class="sc-msg-error"';
+
+    /**
+     * Return the current list of errors.
+     *
+     * @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
+     */
+    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__.
+     */
+    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
+     *
+     * @return bool   true if any errors were found, or if found for
+     *                a variable of $form_name, false otherwise
+     */
+    function anyErrors($form_name=null)
+    {
+        if (isset($form_name)) {
+            foreach ($this->errors as $err) {
+                if ($err['name'] == $form_name) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        return (sizeof($this->errors) > 0);
+    }
+
+    /**
+     * Reset the error list.
+     */
+    function resetErrorList()
+    {
+        $this->errors = array();
+    }
+
+    /**
+     * Prints the HTML for displaying error messages.
+     *
+     * @access  public
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     * @since   15 Jul 2005 01:39:14
+     */
+    function printErrorMessages()
+    {
+        if ($this->anyErrors()) {
+            ?><div class="sc-msg"><?php
+            $errors = $this->getErrorList();
+            foreach ($errors as $err) {
+                if (!empty($err['message']) && is_string($err['message'])) {
+                    if (error_reporting() > 0 && isset($err['file']) && isset($err['line'])) {
+                        echo "\n<!-- [" . $err['file'] . ' : ' . $err['line'] . '] -->';
+                    }
+                    switch ($err['type']) {
+                    case MSG_ERR:
+                        echo '<div class="sc-msg-error">' . $err['message'] . '</div>';
+                        break;
+
+                    case MSG_WARNING:
+                        echo '<div class="sc-msg-warning">' . $err['message'] . '</div>';
+                        break;
+
+                    case MSG_SUCCESS:
+                        echo '<div class="sc-msg-success">' . $err['message'] . '</div>';
+                        break;
+
+                    case MSG_NOTICE:
+                    default:
+                        echo '<div class="sc-msg-notice">' . $err['message'] . '</div>';
+                        break;
+                    }
+                }
+            }
+            ?></div><?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.
+     */
+    function err($form_name, $marker=null)
+    {
+        if ($this->anyErrors($form_name)) {
+            if (isset($marker)) {
+                echo $marker;
+            } else {
+                echo $this->marker;
+            }
+        }
+    }
+
+    /**
+     * Check whether input has a value. To be used when a value must be empty
+     * under certain circumstances.
+     *
+     * @param  string $form_name the name of the incoming form variable
+     * @param  string $msg       the message to display on error
+     *
+     * @return bool   true if form is not empty, false otherwise.
+     */
+    function notEmpty($form_name, $msg='')
+    {
+
+        $val = trim(getFormData($form_name));
+        if ($val != '') {
+            $this->addError($form_name, $msg);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Check whether input is blank.
+     *
+     * @param  string $form_name the name of the incoming form variable
+     * @param  string $msg       the message to display on error
+     *
+     * @return bool   true if form is empty, false otherwise.
+     */
+    function isEmpty($form_name, $msg='')
+    {
+
+        $val = trim(getFormData($form_name));
+        if ($val == '') {
+            $this->addError($form_name, $msg);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 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
+     *
+     * @return bool   true if form is a string, false otherwise.
+     */
+    function isString($form_name, $msg='')
+    {
+        $val = getFormData($form_name);
+        if (!is_string($val) && $val != '') {
+            $this->addError($form_name, $msg);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 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
+     *
+     * @return bool   true if no errors found, false otherwise
+     */
+    function isNumber($form_name, $msg='')
+    {
+        $val = getFormData($form_name);
+        if (!is_numeric($val) && $val != '') {
+            $this->addError($form_name, $msg);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 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
+     *
+     * @return bool   true if value is an integer
+     */
+    function isInteger($form_name, $msg='', $negative_ok=false)
+    {
+        $val = getFormData($form_name);
+        $pattern = $negative_ok ? '/^-?[[:digit:]]+$/' : '/^[[:digit:]]+$/';
+        if ((!is_numeric($val) || !preg_match($pattern, $val)) && $val != '') {
+            $this->addError($form_name, $msg);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 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
+     *
+     * @return bool   true if value is a float
+     */
+    function isFloat($form_name, $msg='', $negative_ok=false)
+    {
+        $val = getFormData($form_name);
+        $pattern = $negative_ok ? '/^-?[[:digit:]]*(?:\.?[[:digit:]]+)$/' : '/^[[:digit:]]*(?:\.?[[:digit:]]+)$/';
+        if ((!is_numeric($val) || !preg_match($pattern, $val)) && $val != '') {
+            $this->addError($form_name, $msg);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 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
+     *
+     * @return bool   true if value is a float
+     */
+    function isArray($form_name, $msg='')
+    {
+        $val = getFormData($form_name);
+        if (!is_array($val) && !empty($val)) {
+            $this->addError($form_name, $msg);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 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   $not       set to false to be valid if match, or true
+     *                           to be valid on no match
+     * @param  string $msg       the message to display on error
+     *
+     * @return bool   true if value passes regex test
+     */
+    function checkRegex($form_name, $regex, $not, $msg='')
+    {
+        $val = getFormData($form_name);
+        if ($not) {
+            if (!preg_match($regex, $val))  {
+                $this->addError($form_name, $msg);
+                return false;
+            } else {
+                return true;
+            }
+        } else {
+            if (preg_match($regex, $val))  {
+                $this->addError($form_name, $msg);
+                return false;
+            } else {
+                return true;
+            }
+        }
+    }
+
+    /**
+     * 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
+     *
+     * @return bool   true if string length is within given boundaries
+     */
+    function stringLength($form_name, $min, $max, $msg='')
+    {
+        $val = getFormData($form_name);
+
+        if (strlen(trim($val)) < $min || strlen($val) > $max) {
+            $this->addError($form_name, $msg);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * 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
+     *
+     * @return bool   true if no errors found, false otherwise
+     */
+    function numericRange($form_name, $min, $max, $msg='')
+    {
+        $val = getFormData($form_name);
+        if ($val != '' && is_numeric($val)) {
+            if ($val < $min || $val > $max) {
+                $this->addError($form_name, $msg);
+                return false;
+            }
+            return true;
+        } else {
+            // Not a number!
+            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.
+     * @return  bool    Validity of address.
+     * @author  Quinn Comendant <quinn@strangecode.com>
+     */
+    function validateEmail($form_name)
+    {
+        $email = getFormData($form_name);
+        if ('' == trim($email)) {
+            return false;
+        }
+
+        require_once 'codebase/lib/Email.inc.php';
+        $e = new Email();
+
+        // Test email address format.
+        if (!preg_match($e->getParam('regex'), $email, $e_parts)) {
+            $this->addError($form_name, sprintf(_("<strong>%s</strong> is not a valid email address."), oTxt($email)));
+            App::logMsg(sprintf('The email address %s is not valid.', getFormData($form_name)), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        // We have a match! Here are the captured subpatterns, on which further tests are run.
+        $local = $e_parts[2];
+        // If domain is an IP [XXX.XXX.XXX.XXX] strip off the brackets.
+        $domain = $e_parts[3]{0} == '[' ? substr($e_parts[3], 1, -1) : $e_parts[3];
+
+        // Test length.
+        if (strlen($local) > 64 || strlen($domain) > 191) {
+            $this->addError($form_name, sprintf(_("<strong>Email address</strong> must contain less than 256 characters."), oTxt($email)));
+            App::logMsg(sprintf('The email address %s must contain less than 256 characters.', getFormData($form_name)), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Check domain exists: It's a domain if ip2long fails; Checkdnsrr ensures a MX record exists; Gethostbyname() ensures the domain exists.
+        // Compare ip2long twice for php4 backwards compat.
+        if ((ip2long($domain) == '-1' || ip2long($domain) === false) && function_exists('checkdnsrr') && !checkdnsrr($domain . '.', 'MX') && gethostbyname($domain) == $domain) {
+            $this->addError($form_name, sprintf(_("<strong>%s</strong> is not a valid email domain name"), oTxt($domain)));
+            App::logMsg(sprintf('The email address %s contains an invalid email domain name (%s).', getFormData($form_name), $domain), LOG_INFO, __FILE__, __LINE__);
+            return false;
+        }
+
+        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) 624-4410
+     *
+     * @param  string  $form_name the name of the incoming form variable
+     *
+     * @return bool    true if no errors found, false otherwise
+     */
+    function validatePhone($form_name)
+    {
+        $phone = getFormData($form_name);
+
+        return $this->checkRegex($form_name, '/^[0-9 +().-]*$/', true, sprintf(_("The phone number <strong>%s</strong> is not valid."), $phone))
+        && $this->stringLength($form_name, 0, 25, sprintf(_("The phone number <strong>%s</strong> is too long"), $phone));
+    }
+
+    /**
+     * 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
+     *
+     * @return bool    true if no errors found, false otherwise
+     */
+    function validateStrDate($form_name, $msg='')
+    {
+        if (($timestamp = strtotime(getFormData($form_name, '0'))) === -1) {
+            $this->addError($form_name, $msg);
+            App::logMsg(sprintf('The string date %s is not valid.', getFormData($form_name)), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+
+    /**
+     * Verifies credit card number.
+     *
+     * @param  string  $form_name   The name of the incoming form variable.
+     * @param  string  $cc_num      Card number to verify.
+     * @param  string  $cc_type     Optional, card type to do specific checks.
+     *
+     * @return bool    true if no errors found, false otherwise
+     */
+    function validateCCNumber($form_name, $cc_num=null, $cc_type=null)
+    {
+        if (!isset($cc_num)) {
+            $cc_num = getFormData($form_name);
+        }
+
+        if ('' == $cc_num) {
+            return false;
+        }
+
+        // Innocent until proven guilty
+        $card_is_valid = true;
+
+        // Get rid of any non-digits
+        $cc_num = preg_replace('/[^\d]/', '', $cc_num);
+
+        // Perform card-specific checks, if applicable
+        switch (strtolower($cc_type)) {
+            case 'visa' :
+                $card_is_valid = preg_match('/^4\d{15}$|^4\d{12}$/', $cc_num);
+                break;
+            case 'mastercard' :
+            case 'mc' :
+                $card_is_valid = preg_match('/^5[1-5]\d{14}$/', $cc_num);
+                break;
+            case 'american_express' :
+            case 'american_ex' :
+            case 'americanexpress' :
+            case 'americanex' :
+            case 'am_ex' :
+            case 'amex' :
+            case 'ae' :
+                $card_is_valid = preg_match('/^3[47]\d{13}$/', $cc_num);
+                break;
+            case 'discover' :
+                $card_is_valid = preg_match('/^6011\d{12}$/', $cc_num);
+                break;
+            case 'diners_club' :
+            case 'dinersclub' :
+            case 'diners' :
+            case 'diner' :
+            case 'dc' :
+                $card_is_valid = preg_match('/^30[0-5]\d{11}$|^3[68]\d{12}$/', $cc_num);
+                break;
+            case 'jcb' :
+                $card_is_valid = preg_match('/^3\d{15}$|^2131|1800\d{11}$/', $cc_num);
+                break;
+        }
+
+        // The Luhn formula works right to left, so reverse the number.
+        $cc_num = strrev($cc_num);
+
+        $luhn_total = 0;
+
+        $num = strlen($cc_num);
+        for ($i=0; $i<$num; $i++) {
+            // Get each digit.
+            $digit = 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 (strlen($digit) == 2) {
+                $digit = substr($digit,0,1) + substr($digit,1,1);
+            }
+
+            //  Add the current digit to the $luhn_total.
+            $luhn_total += $digit;
+        }
+
+        // If it passed (or bypassed) the card-specific check and the Total is evenly divisible by 10, it's cool!
+        if ($card_is_valid && $luhn_total % 10 == 0) {
+            return true;
+        } else {
+            $this->addError($form_name, _("The <strong>credit card number</strong> you entered is not valid."));
+            return false;
+        }
+    }
+
+    /**
+     * Check whether uploaded file is valid.
+     *
+     * @param  string $form_name the name of the incoming form variable
+     * @param  string $msg       the message to display on error
+     *
+     * @return bool   true if no errors found, false otherwise
+     */
+    function validateFile($form_name, $msg='')
+    {
+        if (isset($_FILES[$form_name]['tmp_name']) && '' == trim($_FILES[$form_name]['tmp_name'])) {
+            $this->addError($form_name, $msg);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+} // THE END
+
+?>
Index: branches/2.0singleton/lib/Google_API.inc.php
===================================================================
--- branches/2.0singleton/lib/Google_API.inc.php	(revision 122)
+++ branches/2.0singleton/lib/Google_API.inc.php	(revision 122)
@@ -0,0 +1,167 @@
+<?php
+/**
+ * Interface to the Google API using SOAP/Client.php.
+ *
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information.
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.1
+ */
+
+// Example of use:
+//
+// require_once 'Google_API.php';
+// $google = new Google_API('your license key');
+// $result = $google->search(
+//   array(
+//     'query' => 'sebastian bergmann'
+//   )
+// );
+// if (false !== $result) {
+//   print_r($result);
+// } else {
+//   echo 'Query failed.';
+// }
+
+require_once 'SOAP/Client.php';
+
+
+/**
+* PHP Interface to the Google API
+*
+* @author  Sebastian Bergmann <sb@sebastian-bergmann.de>
+* @access  public
+*/
+class Google_API
+{
+
+    /**
+     * @var    string
+     * @access private
+     */
+    var $_licenseKey = '';
+
+    /**
+     * @var    object
+     * @access private
+     */
+    var $_soapClient = NULL;
+
+    /**
+     * Constructor.
+     *
+     * @param  string
+     * @access public
+     */
+    function GoogleAPI($licenseKey)
+    {
+        $this->_licenseKey = $licenseKey;
+        $this->_soapClient = new SOAP_Client(
+          'http://api.google.com/search/beta2'
+        );
+    }
+
+    /**
+     * Retrieves a page by URL from the Google Cache.
+     *
+     * @param  string
+     * @return mixed
+     * @access public
+     */
+    function getCachedPage($url)
+    {
+        $result = $this->_performAPICall(
+            'doGetCachedPage',
+            array(
+                'key' => $this->_licenseKey,
+                'url' => $url
+            )
+        );
+        if ($result) {
+            $result = base64_decode($result);
+        }
+        return $result;
+    }
+
+    /**
+     * Retrieves a spelling suggestion for a phrase.
+     *
+     * @param  string
+     * @return mixed
+     * @access public
+     */
+    function getSpellingSuggestion($phrase)
+    {
+        return $this->_performAPICall(
+            'doSpellingSuggestion',
+            array(
+                'key'    => $this->_licenseKey,
+                'phrase' => $phrase
+            )
+        );
+    }
+
+    /**
+     * Performs a web search.
+     *
+     * @param  array
+     * @return mixed
+     * @access public
+     */
+    function search($parameters = array())
+    {
+        if (!isset($parameters['query'])) {
+            return false;
+        }
+        return $this->_performAPICall(
+            'doGoogleSearch',
+            array(
+                'key'         => $this->_licenseKey,
+                'q'           => $parameters['query'],
+                'start'       => isset($parameters['start'])      ? $parameters['start']      : 0,
+                'maxResults'  => isset($parameters['maxResults']) ? $parameters['maxResults'] : 10,
+                'filter'      => isset($parameters['filter'])     ? $parameters['filter']     : false,
+                'restrict'    => isset($parameters['restrict'])   ? $parameters['restrict']   : '',
+                'safeSearch'  => isset($parameters['safeSearch']) ? $parameters['safeSearch'] : false,
+                'lr'          => isset($parameters['lr'])         ? $parameters['lr']         : '',
+                'ie'          => isset($parameters['ie'])         ? $parameters['ie']         : '',
+                'oe'          => isset($parameters['oe'])         ? $parameters['oe']         : ''
+            )
+        );
+    }
+
+    /**
+     * @param  string
+     * @param  array
+     * @return mixed
+     * @access private
+     */
+    function _performAPICall($apiCall, $parameters)
+    {
+        $result = $this->_soapClient->call(
+            $apiCall,
+            $parameters,
+            'urn:GoogleSearch'
+        );
+        if (!PEAR::isError($result)) {
+            return $result;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * getFault
+     *
+     * returns a simple native php array containing the fault data
+     *
+     * @return array
+     * @access public
+     */
+    function getFault()
+    {
+        return $this->_soapClient->__getfault();
+    }
+}
+?>
+
+
Index: branches/2.0singleton/lib/Hierarchy.inc.php
===================================================================
--- branches/2.0singleton/lib/Hierarchy.inc.php	(revision 122)
+++ branches/2.0singleton/lib/Hierarchy.inc.php	(revision 122)
@@ -0,0 +1,798 @@
+<?php
+/**
+ * Hierarchy.inc.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+/**
+ * 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 retreival. You must separatly 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
+     *
+     */
+    var $params = array();
+
+
+    /**
+     * The active node type.
+     * @var string $child_type
+     */
+    var $child_type;
+
+    /**
+     * The active node id.
+     * @var string $child_id
+     */
+    var $child_id;
+
+    /**
+     * Boolean indicating whether or not we've set
+     * the 'active' node type and id.
+     * @var bool $node_init
+     */
+    var $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.
+     */
+    function Hierarchy($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.
+     */
+    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.
+     */
+    function toStringID($child_type=null, $child_id=null)
+    {
+        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 singlar 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.
+     */
+    function toArrayID(&$node)
+    {
+        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 sucess, false on error.
+     */
+    function insertNode($parents, $child_type=null, $child_id=null, $relationship_type=null, $title='')
+    {
+        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 <strong>%s %s</strong>, no parent was specified."), $child_type, $child_id), MSG_ERR, __FILE__, __LINE__);
+            App::logMsg(sprintf(_("Cannot add node <strong>%s %s</strong>, 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 <strong>%s %s</strong> to parent <strong>%s %s</strong>. It already exists there"), $child_type, $child_id, $parent['node_type'], $parent['node_id']), MSG_ERR, __FILE__, __LINE__);
+                App::logMsg(sprintf(_("Cannot add node <strong>%s %s</strong> to parent <strong>%s %s</strong>. 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 <strong>%s %s</strong> to nonexistent parent <strong>%s %s</strong>."), $child_type, $child_id, $parent['node_type'], $parent['node_id']), MSG_ERR, __FILE__, __LINE__);
+                App::logMsg(sprintf(_("Cannot add node <strong>%s %s</strong> to nonexistent parent <strong>%s %s</strong>."), $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_DEBUG, __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.
+     */
+    function deleteNode($child_type=null, $child_id=null)
+    {
+        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_DEBUG, __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.
+     */
+    function moveNode($new_parents=null, $child_type=null, $child_id=null, $relationship_type=null, $title='')
+    {
+        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 <strong>%s %s</strong>, no parent was specified."), $child_type, $child_id), MSG_ERR, __FILE__, __LINE__);
+            App::logMsg(sprintf(_("Cannot move node <strong>%s %s</strong>, 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 <strong>%s %s</strong> to nonexistent parent <strong>%s %s</strong>."), $child_type, $child_id, $parent['node_type'], $parent['node_id']), MSG_ERR, __FILE__, __LINE__);
+                App::logMsg(sprintf(_("Cannot move node <strong>%s %s</strong> to nonexistent parent <strong>%s %s</strong>."), $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 <strong>%s %s</strong> to parent <strong>%s %s</strong> 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 <strong>%s %s</strong> to parent <strong>%s %s</strong> 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_DEBUG, __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
+     * immediatly 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.
+     */
+    function getParents($child_type=null, $child_id=null, $type_constraint=null, $order='')
+    {
+        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.
+     */
+    function getNode($child_type=null, $child_id=null)
+    {
+        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
+     * immediatly 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.
+     */
+    function getChildren($child_type=null, $child_id=null, $type_constraint=null, $order='')
+    {
+        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
+     */
+    function getNumberChildren($child_type=null, $child_id=null, $type_constraint=null)
+    {
+        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
+     */
+    function isLeaf($child_type=null, $child_id=null)
+    {
+        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.
+     */
+    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 progesses. When the value
+     *                                  drops back to 0, we return the output.
+     * @return array     Array of serialized node identifiers.
+     */
+    function getAllAncestors($child_type, $child_id, $go_linear=false, $_return_flag=true)
+    {
+        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
+     */
+    function nodeExists($child_type=null, $child_id=null, $parent_type=null, $parent_id=null, $relationship_type=null)
+    {
+        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)
+     */
+    function &getNodeList($preselected=null, $child_type=null, $child_id=null, $type_constraint=null, $include_curr=false, $order='', $_indent=0, $_return_flag=true)
+    {
+        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 recursivly.
+                    $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.
+     */
+    function rebuildSubnodeQty($type_constraint=null)
+    {
+        // 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 recursivly.
+     */
+    function setSubnodeQtyToParents($child_type, $child_id, $num_children)
+    {
+        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: branches/2.0singleton/lib/Image.inc.php
===================================================================
--- branches/2.0singleton/lib/Image.inc.php	(revision 122)
+++ branches/2.0singleton/lib/Image.inc.php	(revision 122)
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Image.inc.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information.
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.1
+ * @since   14 Apr 2006 20:07:29
+ */
+class Image {
+
+    // Object parameters.
+    var $_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
+     */
+    function Image($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
+      */
+    function exists($id)
+    {
+        $src = $this->oSrc($id);
+        $filepath = preg_match('!://!', $src) ? $src : getenv('DOCUMENT_ROOT') . $src;
+        // Use exif_imagetype to check not only file existance but that of a valid image.
+        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
+     */
+    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
+     */
+    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
+     */
+    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: branches/2.0singleton/lib/ImageThumb.inc.php
===================================================================
--- branches/2.0singleton/lib/ImageThumb.inc.php	(revision 122)
+++ branches/2.0singleton/lib/ImageThumb.inc.php	(revision 122)
@@ -0,0 +1,687 @@
+<?php
+/**
+ * ImageThumb.inc.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ *
+ * @author   Quinn Comendant <quinn@strangecode.com>
+ * @requires Netpbm <http://sourceforge.net/projects/netpbm/> and libjpeg or GD.
+ * @version  2.0
+ */
+
+// Image resize 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);
+define('IMAGETHUMB_METHOD_NETPBM', 6);
+define('IMAGETHUMB_METHOD_GD', 7);
+
+class ImageThumb {
+    
+    // General object parameters.
+    var $_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 autocreated directories. Must be at least 0700 with owner=apache.
+        'dest_dir_perms' => 0777,
+
+        // Require file to have one of the following file name extentions.
+        '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.
+    var $default_image_specs = array(
+        // The destination for an image thumbnail size. 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_extention' => 'jpg',
+        
+        // Type of scaling to perform, and sizes used to calculate max dimentions.
+        '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().
+    var $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).
+     */
+    function setParam($params)
+    {
+        if (isset($params) && is_array($params)) {
+
+            // Enforce valid upload_path parameter.
+            if (isset($params['source_dir'])) {
+                $params['source_dir'] = realpath($params['source_dir']);
+                // Must be directory.
+                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);
+                }
+                // 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.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            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.
+     */
+    function setSpec($spec, $index=null)
+    {
+        // A little sanity checking.
+        if (!isset($spec['dest_dir']) || '' == $spec['dest_dir']) {
+            App::logMsg('setSpec error: dest_dir not specified.', LOG_ERR, __FILE__, __LINE__);
+        }
+        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_extention'] = '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_extention'] = '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_extention'] = '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_extention'] = 'wbmp';
+                break;
+            default :
+                App::logMsg(sprintf('Invalid dest_file_type: %s', $spec['dest_file_type']), LOG_ERR, __FILE__, __LINE__);
+                break;
+            }
+        }
+        if (!isset($spec['width']) || !is_int($spec['width'])) {
+            App::logMsg('setSpec error: width not specified.', LOG_ERR, __FILE__, __LINE__);
+        }
+        if (!isset($spec['height']) || !is_int($spec['height'])) {
+            App::logMsg('setSpec error: height not specified.', 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_INFO, __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_INFO, __FILE__, __LINE__);
+        }
+        
+        if (isset($index) && isset($this->image_specs[$index])) {
+            // Merge with previous.
+            $final_spec = array_merge($this->image_specs[$index], $spec);
+            $this->image_specs[$index] = $final_spec;
+        } else {
+            // Merge with defaults.
+            $final_spec = array_merge($this->default_image_specs, $spec);            
+            $this->image_specs[] = $final_spec;
+        }
+        
+        return $final_spec;
+    }
+
+    /**
+     * Process an entire directory of images.
+     *
+     * @access  public
+     * @return  bool true on success, false on failure.
+     */
+    function processAll($runtime_specs=null)
+    {
+        // 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(strtolower(substr($file, 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 += $this->processFile($file_name, $runtime_specs);
+            }
+            $this->_raiseMsg(sprintf(_("Resized %s images."), sizeof($files)), MSG_SUCCESS, __FILE__, __LINE__);
+            return 0 === $return_val;
+        } else {
+            App::logMsg(sprintf('No images found to thumbnail 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 extention.
+     * @param   array $runtime_specs Array of specifications that will override all configured specifications.
+     * @return  bool true on success, false on failure.
+     */
+    function processFile($file_name, $runtime_specs=null)
+    {
+        // Source file determinted 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 %s was not found."), $file_name), MSG_ERR, __FILE__, __LINE__);
+            App::logMsg(sprintf('Source image not found: %s', $source_file), LOG_ALERT, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Confirm source image is readable.
+        if (!is_readable($source_file)) {
+            $this->_raiseMsg(sprintf(_("Image resizing failed: source image %s is not readable."), $file_name), MSG_ERR, __FILE__, __LINE__);
+            App::logMsg(sprintf('Source image not readable: %s', $source_file), LOG_ALERT, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Confirm source image contains data.
+        if (filesize($source_file) <= 0) {
+            $this->_raiseMsg(sprintf(_("Image resizing failed: source image %s is zero bytes."), $file_name), MSG_ERR, __FILE__, __LINE__);
+            App::logMsg(sprintf('Source image is zero bytes: %s', $source_file), LOG_ALERT, __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 %s 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_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Ensure destination directories are created. This will only be called once per page load.
+        $this->_createDestDirs();
+        
+        // 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 extention defined by dest_file_extention.
+            $dest_file = realpath(sprintf('%s/%s/%s.%s', $this->getParam('source_dir'), $spec['dest_dir'], substr($file_name, 0, strrpos($file_name, '.')), $spec['dest_file_extention']));
+
+            // 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_DEBUG, __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, $spec['dest_dir']), LOG_DEBUG, __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...', null), 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...', null), LOG_DEBUG, __FILE__, __LINE__);
+                $return_val += $this->_resizeWithGD($source_file, $dest_file, $spec);
+            } else {
+                App::logMsg(sprintf('Image thumbnailing failed. Neither Netpbm or GD is available.', null), LOG_DEBUG, __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
+    */
+    function _resizeWithNetpbm($source_file, $dest_file, $spec)
+    {
+        // 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/%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),
+            escapeshellcmd($file_name)
+        );
+        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) ? '' : ' (' . getDump($output) . ')'), 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
+    */
+    function _resizeWithGD($source_file, $dest_file, $spec)
+    {
+        // Get original file dimensions and type.
+        list($source_image_width, $source_image_height, $source_image_type) = getimagesize($source_file);
+
+        // Define destination image dimentions.
+        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 :
+            $source_image_resource = imagecreatefromjpeg($source_file);
+            break;
+        case IMAGETYPE_PNG :
+            $source_image_resource = imagecreatefrompng($source_file);
+            break;
+        case IMAGETYPE_GIF :
+            $source_image_resource = imagecreatefromgif($source_file);
+            break;
+        case IMAGETYPE_WBMP :
+            $source_image_resource = imagecreatefromwbmp($source_file);
+        default :
+            App::logMsg(sprintf('Source image type %s not supported.', $source_image_type), LOG_WARNING, __FILE__, __LINE__);
+            return 1;
+            break;
+        }
+        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)) {
+            App::logMsg(sprintf('Error resampling image %s', $source_file), LOG_WARNING, __FILE__, __LINE__);
+            return 1;
+        }
+        
+        // Sharpen image using a custom filter matrix.
+        if (phpversion() > '5.1' && true === $spec['sharpen'] && $spec['sharpen_value'] > 0) {
+            $sharpen_value = round((((48 - 10) / (100 - 1)) * (100 - $spec['sharpen_value'])) + 10);
+            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__);
+            return 1;
+            break;
+        }
+
+        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('Image %s failed resizing.', $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 extention.
+     * @return  bool true on success, false on failure.
+     */
+    function deleteThumbs($file_name)
+    {
+        // 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) {
+            $dest_file = realpath(sprintf('%s/%s/%s.%s', $this->getParam('source_dir'), $spec['dest_dir'], substr($file_name, 0, strrpos($file_name, '.')), $spec['dest_file_extention']));
+            if (file_exists($dest_file)) {
+                if (!unlink($dest_file)) {
+                    $return_val++;
+                    App::logMsg(sprintf(_("Delete thumbs failed: %s"), $dest_file), LOG_WARNING, __FILE__, __LINE__);
+                }
+            }
+        }
+        $this->_raiseMsg(sprintf(_("The thumbnails for file %s have been deleted."), $file_name), MSG_SUCCESS, __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 extention.
+     * @return  bool true on success, false on failure.
+     */
+    function deleteOriginal($file_name)
+    {
+        // 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));
+        if (!unlink($source_file)) {
+            App::logMsg(sprintf(_("Delete original failed: %s"), $source_file), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+        $this->_raiseMsg(sprintf(_("The original file %s has been deleted."), $file_name), MSG_SUCCESS, __FILE__, __LINE__);
+        return true;
+    }
+
+    /**
+     * Returns true if file exists.
+     *
+     * @access  public
+     * @param   string $file_name The file name to test, with extention.
+     * @return  bool true on success, false on failure.
+     */
+    function exists($file_name)
+    {
+        // 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 extention 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.
+     */
+    function _validFileExtension($file_name)
+    {
+        preg_match('/.*?\.(\w+)$/i', $file_name, $ext);
+        return !empty($ext) && in_array(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.
+     */
+    function _createDestDirs()
+    {
+        static $already_checked = false;
+
+        if (!$already_checked) {
+            // 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.
+            $return_val = 0;
+            foreach ($this->image_specs as $spec) {
+                if (!file_exists($this->getParam('source_dir') . '/' . $spec['dest_dir'])) {
+                    if (!mkdir($this->getParam('source_dir') . '/' . $spec['dest_dir'], $this->getParam('dest_dir_perms'))) {
+                        $return_val++;
+                        App::logMsg(sprintf('mkdir failure: %s', $this->getParam('source_dir') . '/' . $spec['dest_dir']), LOG_ERR, __FILE__, __LINE__);
+                    }
+                }
+            }
+
+            // If > 0, there was a problem creating dest dirs.
+            return 0 === $return_val;
+        }
+
+        $already_checked = true;
+    }
+
+    /**
+     * 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__.
+     */
+    function _raiseMsg($message, $type, $file, $line)
+    {
+        if ($this->getParam('display_messages') === true || (is_int($this->getParam('display_messages')) && $this->getParam('display_messages') & $type > 0)) {
+            App::raiseMsg($message, $type, $file, $line);
+        }
+    }
+
+} // End of class.
+?>
Index: branches/2.0singleton/lib/MCVE.inc.php
===================================================================
--- branches/2.0singleton/lib/MCVE.inc.php	(revision 122)
+++ branches/2.0singleton/lib/MCVE.inc.php	(revision 122)
@@ -0,0 +1,179 @@
+<?php
+/**
+ * The MCVE:: class provides functions for communicating with a MCVE server.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.0
+ */
+class MCVE {
+
+    var $username;
+    var $password;
+    var $host = 'localhost';
+    var $ip_port = 8333;
+    var $ssl_port = 8444;
+    var $ca_bundle_file = '/usr/share/ssl/certs/ca-bundle.crt';
+    var $connect_method = 'ip';
+    var $timeout = 10;
+    var $blocking = 1;
+    var $connected = false;
+    var $conn;
+
+    function MCVE($username, $password)
+    {
+        // Ensure PHP was compiled with the MCVE functions.
+        if (!extension_loaded('mcve')) {
+            trigger_error('MCVE class instantiation failed: MCVE extension not available.', E_USER_ERROR);
+        }
+        if ('' == $username || '' == $password) {
+            App::logMsg(sprintf('Empty username or password provided.', null), LOG_ERR, __FILE__, __LINE__);
+        }
+        $this->username = $username;
+        $this->password = $password;
+    }
+
+    function _connect()
+    {
+        if ($this->connected) {
+            return true;
+        }
+
+        // Initialize SSL structures and definitions.
+        MCVE_InitEngine($ca_bundle_file);
+
+        // Allocate Connection Structures
+        $this->conn = MCVE_InitConn();
+
+        // Set Connection Method and Locations
+        switch ($this->connect_method) {
+        case 'ip' :
+            if (!MCVE_SetIP($this->conn, $this->host, $this->ip_port)) {
+                App::logMsg('Could not set method to IP.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+            break;
+        case 'ssl' :
+            if (!MCVE_SetSSL($this->conn, $this->host, $this->ssl_port)) {
+                App::logMsg('Could not set method to IP.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+            break;
+        case 'dropfile' :
+            if (!MCVE_SetDropFile($this->conn, '/var/mcve/trans')) {
+                App::logMsg('Could not set method to IP.', LOG_ERR, __FILE__, __LINE__);
+                return false;
+            }
+            break;
+        default :
+            App::logMsg('Connection method not defined.', LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Put connection into non-blocking mode, meaning that the client must
+        // loop waiting for the transaction to complete. You should always specify
+        // this function with your blocking preference as the default is currently
+        // non-blocking, but future versions of php_mcve will default to blocking.
+        if (!MCVE_SetBlocking($this->conn, $this->blocking)) {
+            App::logMsg('Could not set blocking mode.', LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Maximum of 30s per transaction allowed. Timeout occurs on server-end, not client-end
+        if (!MCVE_SetTimeout($this->conn, $this->timeout)) {
+            App::logMsg('Could not set timeout.', LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Connect to MCVE, if there's an error, print the exact reason for connection failure
+        if (!MCVE_Connect($this->conn)) {
+            $error = MCVE_ConnectionError($this->conn);
+            App::logMsg("Connection failed: $error. Are you sure the MCVE engine is running?", LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        $this->connected = true;
+        return true;
+    }
+
+    function beginTrans()
+    {
+        $this->_connect();
+        $tid = MCVE_TransNew($this->conn); // Allocate memory for sending a transaction.
+        $this->transParam($tid, MC_USERNAME, $this->username);
+        $this->transParam($tid, MC_PASSWORD, $this->password);
+        return $tid;
+    }
+
+    function transParam($tid, $key, $var1, $var2=null)
+    {
+        if (!isset($var2)) {
+            return MCVE_TransParam($this->conn, $tid, $key, $var1);
+        } else {
+            return MCVE_TransParam($this->conn, $tid, $key, $var1, $var2);
+        }
+
+    }
+
+    function sendTrans($tid, $type='', $hide_msg=false)
+    {
+        // Finalize structuring of  to MCVE and ready it to be sent
+        if (!MCVE_TransSend($this->conn, $tid)) {
+            App::logMsg('Transaction improperly structured, possibly not enough info.', LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+
+        // Perform actual communication with MCVE engine.
+        // If blocking method is used, loop until transaction is complete
+        if ($this->blocking != 1) {
+            while (MCVE_CheckStatus($this->conn, $tid) != MCVE_DONE) {
+                MCVE_Monitor($this->conn); // Perform communication on connection
+                MCVE_uwait(10000); // Microsecond thread-safe sleep timer. Better than usleep()
+            }
+        }
+
+        $ret_status = MCVE_ReturnStatus($this->conn, $tid);
+        $ret_code = MCVE_ReturnCode($this->conn, $tid);
+        $ret_text = mcve_text_code($ret_code);
+        $verbiage = MCVE_TransactionText($this->conn, $tid);
+
+        // Check to see if transaction was successful or not using a strict success/fail function
+        if ($ret_status == MCVE_FAIL) {
+            App::raiseMsg(sprintf('MCVE %s failure: %s %s', $type, $ret_text, ('' == $verbiage ? '' : '(' . trim($verbiage) . ')')), MSG_WARNING, __FILE__, __LINE__);
+            return false;
+        } else if ($ret_status == MCVE_SUCCESS) {
+            if (!$hide_msg) {
+                App::raiseMsg(sprintf(_("MCVE %s success: %s %s"), $type, $ret_text, ('' == $verbiage ? '' : '(' . trim($verbiage) . ')')), MSG_SUCCESS, __FILE__, __LINE__);
+            }
+            App::logMsg(sprintf(_("MCVE success details. Auth: %s; Batch: %s; Item: %s; TTID: %s; AVS: %s; CV: %s."),
+                MCVE_TransactionAuth($this->conn, $tid),
+                MCVE_TransactionBatch($this->conn, $tid),
+                MCVE_TransactionItem($this->conn, $tid),
+                MCVE_TransactionID($this->conn, $tid),
+                mcve_text_avs(MCVE_TransactionAVS($this->conn, $tid)),
+                mcve_text_cv(MCVE_TransactionCV($this->conn, $tid))
+            ), LOG_DEBUG, __FILE__, __LINE__);
+            return true;
+        } else {
+            App::logMsg(sprintf('Transaction failed. Unknown return code: %s', $ret_status), LOG_ERR, __FILE__, __LINE__);
+            return false;
+        }
+    }
+
+    function disconnect($tid)
+    {
+        if ($this->connected) {
+            // Optional, clean up memory by transaction ... This is done automatically by
+            // MCVE_DestroyConn though
+            MCVE_DeleteTrans($this->conn, $tid);
+
+            // Clean up and close MCVE.
+            MCVE_DestroyConn($this->conn);
+            MCVE_DestroyEngine();
+
+            $this->connected = false;
+        }
+    }
+}
+// End of class.
+
+?>
Index: branches/2.0singleton/lib/Nav.inc.php
===================================================================
--- branches/2.0singleton/lib/Nav.inc.php	(revision 122)
+++ branches/2.0singleton/lib/Nav.inc.php	(revision 122)
@@ -0,0 +1,296 @@
+<?php
+/**
+ * The Nav:: class provides a system for working with navigation elements.
+ * Currently it supports storing page titles and URLs for printing breadcrumbs
+ * and titles, as well as setting page features such as hiding the page title on
+ * some pages but not others.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.0
+ */
+class Nav {
+
+    var $pages = array();
+    var $path_delimiter = ' / ';
+    var $features = array();
+    var $default_features = array(
+        'title' => true,
+        'path' => true,
+        'breadcrumbs' => true,
+        'chop_breadcrumbs' => 0,
+        'chop_breadcrumb_links' => 1,
+    );
+    var $last_crumb_format = '%s';
+
+    /**
+     * Constructor. Set default features to apply to all added pages.
+     */
+    function Nav($default_features=null)
+    {
+        if (isset($default_features) && is_array($default_features)) {
+            $this->default_features = array_merge($this->default_features, $default_features);
+        }
+        $this->features = $this->default_features;
+    }
+
+/******************************************************************************
+ * INPUT
+ *****************************************************************************/
+
+    /**
+     * 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. Features can be specified for a page, but currently
+     * only the features for the current page can be set. Future versions of this
+     * class may have the ability to set features for each page specifically.
+     *
+     * @access  public
+     *
+     * @param   string  $title      The title of the page.
+     * @param   string  $url        The URL to the page. Leave blank (or null) if
+     *                              page is to not be linked.
+     * @param   array   $features   Set the features of the current page.
+     */
+    function addPage($title, $url=null, $features=null)
+    {
+        $this->pages[] = array(
+            'title'     => $title,
+            'url'       => $url,
+            'features'  => (isset($features) && is_array($features)) ? array_merge($this->default_features, $features) : $this->default_features
+        );
+    }
+
+    /**
+     * Set the features of the current page. Future versions of this class
+     * may have the ability to set features for a specific page. In that case
+     * some form of ID will need to be specified for each page.
+     *
+     * @param  array $feature   Array of feature keys and value to set.
+     *
+     * @return bool true on success, false on failure
+     */
+    function setFeature($features=null, $page_id=null)
+    {
+        $page_id = $this->_calculatePageID($page_id);
+
+        if (isset($features) && is_array($features) && isset($this->pages[sizeof($this->pages)-1]['features']) && is_array($this->pages[sizeof($this->pages)-1]['features'])) {
+            // Set features for specified page.
+            $this->pages[sizeof($this->pages)-1]['features'] = array_merge($this->pages[sizeof($this->pages)-1]['features'], $features);
+            // TODO: Store "current page" features.
+//             $this->pages[$page_id]['features'] = array_merge($this->pages[$page_id]['features'], $features);
+        }
+    }
+
+    /**
+     * Unsets all page variables and resets features to the default set.
+     *
+     * @access  public
+     */
+    function clearPath()
+    {
+        $this->pages = array();
+        $this->features = $this->default_features;
+    }
+
+/******************************************************************************
+ * OUTPUT
+ *****************************************************************************/
+
+    /**
+     * Get the value of a feature for specified page_id or current page if page_id not specified (future use).
+     *
+     * @param  string $feature   Name of feature value to retreive.
+     * @param  mixed $page_id    Future use: ID of page.
+     *
+     * @return bool true on success, false on failure
+     */
+    function getFeature($feature, $page_id=null, $default=null)
+    {
+        $page_id = $this->_calculatePageID($page_id);
+
+        if (isset($this->pages[$page_id]['features'][$feature])) {
+            switch ($feature) {
+            case 'breadcrumbs' :
+                // No breadcrumbs if displayed quantity of crumbs is less than 1.
+                return $this->pages[$page_id]['features'][$feature] && ((sizeof($this->pages) - $this->getFeature('chop_breadcrumbs')) > 0);
+                break;
+            default :
+                return $this->pages[$page_id]['features'][$feature];
+            }
+        } else {
+            return $default;
+        }
+    }
+
+
+    /**
+     * Returns the title of current page.
+     *
+     * @access  public
+     *
+     * @return  mixed  Title of page (string) or false if title feature not set.
+     */
+    function getTitle($page_id=null)
+    {
+        $page_id = $this->_calculatePageID($page_id);
+
+        if ($this->getFeature('title', $page_id)) {
+            return oTxt($this->pages[$page_id]['title'], true);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Prints the title of page returned by getTitle().
+     *
+     * @access  public
+     */
+    function printTitle($page_id=null)
+    {
+        echo $this->getTitle($page_id);
+    }
+
+    /**
+     * Returns the text path from root up to the current page, seperated by the
+     * path_delimeter.
+     *
+     * @access  public
+     *
+     * @return  mixed   Path (string) or false if path feature is not set.
+     */
+    function getPath($page_id=null)
+    {
+        $page_id = $this->_calculatePageID($page_id);
+
+        if ($this->getFeature('path', $page_id)) {
+            $path = '';
+            $pathmark = '';
+            foreach ($this->pages as $curr_id => $page) {
+                $path .= oTxt($pathmark . strip_tags($page['title']), true);
+                $pathmark = $this->path_delimiter;
+                if ($curr_id === $page_id) {
+                    // Reached requested page.
+                    return $path;
+                }
+            }
+            return $path;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Prints the path returned by getPath().
+     *
+     * @access  public
+     */
+    function printPath($page_id=null)
+    {
+        echo $this->getPath($page_id);
+    }
+
+    /**
+     * 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  mixed   Breadcrumbs (string) or false if breadcrumbs feature not set.
+     */
+    function getBreadcrumbs($page_id=null)
+    {
+        $page_id = $this->_calculatePageID($page_id);
+
+        if ($this->getFeature('breadcrumbs')) {
+            $breadcrumbs = '';
+            $pathmark = '';
+            $crumb_count = sizeof($this->pages);
+            foreach ($this->pages as $curr_id => $page) {
+                if ($crumb_count <= $this->getFeature('chop_breadcrumbs')) {
+                    // Stop gathering crumbs.
+                    return $breadcrumbs;
+                }
+                if ($crumb_count <= 1) {
+                    // The last crumb.
+                    if (empty($page['url']) || $crumb_count <= $this->getFeature('chop_breadcrumb_links')) {
+                        // A crumb with no link.
+                        $breadcrumbs .= oTxt($pathmark, true) . sprintf($this->last_crumb_format, oTxt($page['title'], true));
+                    } else if ($crumb_count > $this->getFeature('chop_breadcrumb_links')) {
+                        // A normal linked crumb.
+                        $breadcrumbs .= oTxt($pathmark, true) . '<a href="' . App::oHREF($page['url']) . '">' . sprintf($this->last_crumb_format, oTxt($page['title'], true)) . '</a>';
+                    }
+                } else {
+                    if (empty($page['url'])) {
+                        // A crumb with no link.
+                        $breadcrumbs .= oTxt($pathmark . $page['title'], true);
+                    } else {
+                        // A normal linked crumb.
+                        $breadcrumbs .= oTxt($pathmark, true) . '<a href="' . App::oHREF($page['url']) . '">' . oTxt($page['title'], true) . '</a>';
+                    }
+                }
+                $pathmark = $this->path_delimiter;
+                $crumb_count--;
+
+                if ($curr_id === $page_id) {
+                    // Reached requested page.
+                    return $breadcrumbs;
+                }
+            }
+            return $breadcrumbs;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Prints the breadcrumbs returned by getBreadcrumbs().
+     *
+     * @access  public
+     */
+    function printBreadcrumbs($page_id=null)
+    {
+        echo $this->getBreadcrumbs($page_id);
+    }
+
+    /**
+     * Returns a string if the queried page is the current page. One use is to print
+     * CSS tags if the current page matches a link, such as:
+     * $nav->currentPage('mypage.php', ' id="current"');
+     *
+     * @access  public
+     *
+     * @param   mixed   $page   The URI of the page to query, with PREG express markup, if needed.
+     * @param   mixed   $return The value to return if the current page matches the page queried.
+     *
+     * @return  mixed   The value set for $return, TRUE by default.
+     */
+    function currentPage($page_uri, $return=true)
+    {
+        $page_uri = str_replace('/', '\/', $page_uri);
+        if (preg_match('/^' . $page_uri . '/i', $_SERVER['PHP_SELF'])) {
+            return $return;
+        }
+    }
+
+    /**
+     * Returns the ID of the current page, or the adjusted ID for a given page ID.
+     *
+     * @access  private
+     * @return  string  The value of the current page id.
+     */
+    function _calculatePageID($page_id=null)
+    {
+        if (!isset($page_id)) {
+            return sizeof($this->pages) - 1;
+        } else if ($page_id < 0 && is_numeric($page_id)) {
+            return sizeof($this->pages) + intval($page_id);
+        } else {
+            return $page_id;
+        }
+    }
+
+}
+// End of class.
+
+?>
Index: branches/2.0singleton/lib/PEdit.inc.php
===================================================================
--- branches/2.0singleton/lib/PEdit.inc.php	(revision 122)
+++ branches/2.0singleton/lib/PEdit.inc.php	(revision 122)
@@ -0,0 +1,671 @@
+<?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 editied
+ * 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. Below is an 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();
+
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @concept Beau Smith <beau@beausmith.com>
+ * @version 2.0
+ */
+class PEdit {
+
+    // PEdit object parameters.
+    var $_params = array(
+        'data_dir' => '',
+        'character_set' => 'utf-8',
+        'versions_min_qty' => 20,
+        'versions_min_days' => 10,
+    );
+
+    var $_data = array(); // Array to store loaded data.
+    var $_data_file = ''; // Full file path to the pedit data file.
+    var $_authorized = false; // User is authenticated to see extended functions.
+    var $_data_loaded = false;
+    var $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.
+     */
+    function PEdit($params)
+    {
+        $this->setParam($params);
+        
+        if ($this->getParam('authorized') === true) {
+            $this->_authorized = true;
+        }
+        
+        // Setup PEAR XML libraries.
+        require_once 'XML/Unserializer.php';
+        $this->xml_unserializer =& new XML_Unserializer(array(
+            XML_UNSERIALIZER_OPTION_COMPLEXTYPE => 'array',
+        ));
+        require_once 'XML/Serializer.php';
+        $this->xml_serializer =& new XML_Serializer(array(
+            XML_SERIALIZER_OPTION_INDENT        => '    ',
+            XML_SERIALIZER_OPTION_RETURN_RESULT => true
+        ));
+    }
+    
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    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);
+        } 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.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            return $this->_params[$param];
+        } else {
+            App::logMsg(sprintf('Parameter is not set: %s', $param), LOG_NOTICE, __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
+    */
+    function start($initialize_data_file=false)
+    {
+        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['PHP_SELF']);
+
+        // 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.
+     */
+    function set($name, $options=array())
+    {
+        $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.
+     */
+    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.
+     */
+    function formBegin()
+    {
+        if (!$this->_authorized || empty($this->_data)) {
+            return false;
+        }
+        ?>        
+        <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post" id="sc-pedit-form">
+        <input type="hidden" name="filename" value="<?php echo $_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 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 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
+     */
+    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.
+     */
+    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)); ?></label>
+                <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" />
+                <?php
+                break;
+            case 'textarea' :
+                ?>
+                <label><?php echo ucfirst(str_replace('_', ' ', $name)); ?></label>
+                <textarea name="_pedit_data[<?php echo $name; ?>]" id="sc-pedit-field-<?php echo $name; ?>" rows="" cols="" class="full sc-tall"><?php echo oTxt($this->_data[$name]['content']); ?></textarea>
+                <?php
+                break;
+            }
+            ?>
+            </div>
+            <?php
+        }
+    }
+
+    /**
+     * Prints the endig </form> HTML tag, as well as buttons used during
+     * different operations.
+     *
+     * @return bool  False if unauthorized or current page is a version.
+     */
+    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 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 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 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 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 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 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 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 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
+     */
+    function printVersions()
+    {
+        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 secreat hash for the current file.
+    *
+    * @access   public
+    * @author   Quinn Comendant <quinn@strangecode.com>
+    * @since    12 Apr 2006 10:52:35
+    */
+    function _fileHash()
+    {
+        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
+    */
+    function _loadDataFile()
+    {
+        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
+    */
+    function _initializeDataFile()
+    {
+        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.
+     */
+    function _writeData()
+    {
+        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
+    */
+    function _filePutContents($filename, $content)
+    {
+        // Ensure requested filename is within the pedit data dir.
+        if (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, strlen($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.
+     */
+    function _createVersion()
+    {
+        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
+    */
+    function _deleteOldVersions()
+    {
+        $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.
+     */
+    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.
+     */
+    function _restoreVersion($version)
+    {
+        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-existant 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: branches/2.0singleton/lib/PageNumbers.inc.php
===================================================================
--- branches/2.0singleton/lib/PageNumbers.inc.php	(revision 122)
+++ branches/2.0singleton/lib/PageNumbers.inc.php	(revision 122)
@@ -0,0 +1,338 @@
+<?php
+/**
+ * PageNumbers.inc.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ *
+ * 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 {
+
+    var $total_items;       // Total quantity of items.
+    var $total_pages;       // The total number of pages.
+    var $current_page = 1;  // Current page number.
+    var $first_item;        // The counter for the first item on this page (zero index).
+    var $last_item;         // The counter for the last item on this page (zero index).
+    var $max_num_links = 9; // The max number of links to show on page (odd numbers look best).
+    var $_num_links;        // The number of links to show on page.
+    var $_per_page = 25;    // Items per page.
+
+    // Flags to ensure all necessary values have been set before calling calculate().
+    var $set_per_page_initialized = false;
+    var $set_page_number_initialized = false;
+    var $set_total_items_initialized = false;
+
+    // These are initialized in the constructor.
+    var $per_page_options;
+    var $left_arrow;
+    var $left_arrow_disabled;
+    var $left_dbl_arrow;
+    var $left_dbl_arrow_disabled;
+    var $right_arrow;
+    var $right_arrow_disabled;
+    var $right_dbl_arrow;
+    var $right_dbl_arrow_disabled;
+
+    /**
+     * PageNumbers constructor. All arguments are depreciated. Use set* functions instead.
+     */
+    function PageNumbers()
+    {
+        // 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=';
+    }
+
+    /**
+     * Set the number of items per page.
+     */
+    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) {
+                Prefs::setValue('items_per_page', $this->_per_page, $_SERVER['PHP_SELF']);
+            }
+        } else if ($save_value && Prefs::exists('items_per_page', $_SERVER['PHP_SELF'])) {
+            $this->_per_page = (int)Prefs::getValue('items_per_page', $_SERVER['PHP_SELF']);
+        } else if (is_numeric($default) && $default > 0) {
+            $this->_per_page = $default;
+        }
+        $this->set_per_page_initialized = true;
+    }
+
+    /**
+     * Set the current page number.
+     */
+    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) {
+                Prefs::setValue('page_number', $this->current_page, $_SERVER['PHP_SELF']);
+            }
+        } else if ($save_value && Prefs::exists('page_number', $_SERVER['PHP_SELF'])) {
+            $this->current_page = (int)Prefs::getValue('page_number', $_SERVER['PHP_SELF']);
+        }
+        $this->set_page_number_initialized = true;
+    }
+
+    /**
+     * Set the total number of items.
+     */
+    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
+     */
+    function calculate()
+    {
+        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 exceedes 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.
+     */
+    function getLimitSQL()
+    {
+        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
+     */
+    function printPerPageLinks($query_key='per_page')
+    {
+        $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
+     */
+    function getPageNumURL($page_number, $carry_args=null)
+    {
+        return App::oHREF($this->url_base . $page_number, $carry_args);
+    }
+    function printPageNumURL($page_number, $carry_args=null)
+    {
+        echo $this->getPageNumURL($page_number, $carry_args);
+    }
+
+    /**
+     * Returns an array of page number links.
+     *
+     * @access 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
+     */
+    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;
+    }
+
+    function printPageNumbers($carry_args=null)
+    {
+        echo $this->getPageNumbers($carry_args);
+    }
+
+}
+
+?>
Index: branches/2.0singleton/lib/PageSequence.inc.php
===================================================================
--- branches/2.0singleton/lib/PageSequence.inc.php	(revision 122)
+++ branches/2.0singleton/lib/PageSequence.inc.php	(revision 122)
@@ -0,0 +1,382 @@
+<?php
+/**
+ * PageSequence.inc.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ *
+ * 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
+{
+    var $current_step_id = 0;
+    var $start_url;
+    var $sequence_title = '';
+    var $seq = '_sequence_';
+    var $idle_timeout;
+
+    /**
+     * Constructor. Sets the title of this sequence and initializes session variables.
+     *
+     * @param  array $params   An associative array of PageSequence parameters.
+     * @access public
+     */
+    function PageSequence($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
+     */
+    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
+     */
+    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.
+     */
+    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
+     */
+    function setCurrent($step_id)
+    {
+        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 poisition
+     * @access 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
+     */
+    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
+     */
+    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 identifyer of the next step.
+     * @access 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 identifyer of the next step.
+     * @access 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
+     */
+    function printEditLink($step_id=null)
+    {
+        $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 identifyer of the next step.
+     * @access 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
+     */
+    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
+     */
+    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();
+        }
+    }
+
+    /**
+     * Delete's all data that are older than auto_timeout. Set current time if not not expired or not set.
+     */
+    function _auto_timeout()
+    {
+        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
+     */
+    function registerData($step_data)
+    {
+        $_SESSION[$this->seq]['data'] = array_merge($_SESSION[$this->seq]['data'], $step_data);
+    }
+
+    /**
+     * Reset all vars.
+     *
+     */
+    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();
+    }
+
+    /**
+     * Template function to be extended with custom SQL code.
+     *
+     * @return int  Unique DB identifyer for saved record.
+     * @access public
+     */
+    function saveData()
+    {
+        return false;
+    }
+
+    /**
+     * Template function to be extended with custom SQL code.
+     *
+     * @return mixed  Data stored in DB.
+     * @access public
+     */
+    function loadData()
+    {
+        return false;
+    }
+
+
+} // END CLASS
+
+?>
Index: branches/2.0singleton/lib/PayPal.inc.php
===================================================================
--- branches/2.0singleton/lib/PayPal.inc.php	(revision 122)
+++ branches/2.0singleton/lib/PayPal.inc.php	(revision 122)
@@ -0,0 +1,345 @@
+<?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.
+    var $_params = array(
+        'paypal_url' => 'https://www.paypal.com/cgi-bin/webscr',
+        'test_mode' => false,
+    );
+
+    // Options used for specific buttons and links.
+    var $_default_button_options = array();
+
+    // Array of buttons created by newButton().
+    var $_buttons = array();
+
+    // Store the response from the last IPN.
+    var $_ipn_response;
+
+    /**
+     * Constructor.
+     *
+     * @param   bool    $test_mode  Use PayPal sandbox for testing.
+     */
+    function PayPal($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.
+     */
+    function setButtonDefaults($type, $options)
+    {
+        if (!is_array($options) || empty($options)) {
+            App::logMsg(sprintf('Invalid options: %s', getDump($options)), 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.
+     */
+    function newButton($type, $name, $options=null)
+    {
+        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', getDump($options)), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (isset($this->_buttons[$name])) {
+            App::logMsg(sprintf('Overwriting existing button name: %s', getDump($this->_buttons[$name])), 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.
+     */
+    function getLink($name)
+    {
+        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.
+     */
+    function printLink($name)
+    {
+        echo $this->getLink($name);
+    }
+
+    /**
+     * Prints button with specified name.
+     *
+     * @access  public
+     *
+     * @param   string  $name   Name of button to print.
+     */
+    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).
+     */
+    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);
+        } 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.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            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.
+     */
+    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.
+     */
+    function processIPN()
+    {
+        if (getPost('test_ipn') == '1' || $this->getParam('test_mode')) {
+            App::logMsg(sprintf('Processing PayPal IPN in test mode: %s', getDump(getFormData())), LOG_DEBUG, __FILE__, __LINE__);
+            $url = parse_url('https://www.sandbox.paypal.com/cgi-bin/webscr');
+        } else {
+            App::logMsg(sprintf('Processing PayPal IPN: %s', getDump(getFormData())), 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: " . 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.
+     */
+    function verifiedIPN()
+    {
+        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: branches/2.0singleton/lib/Prefs.inc.php
===================================================================
--- branches/2.0singleton/lib/Prefs.inc.php	(revision 122)
+++ branches/2.0singleton/lib/Prefs.inc.php	(revision 122)
@@ -0,0 +1,246 @@
+<?php
+/**
+ * Prefs:: provides an API for saving arbitrary values in a user's session.
+ * Database save routines to come.
+ *
+ * @inspiration  Horde 2.0's Prefs class. This one is much simpler, but
+ *               the API for the methods that exist are sort of like Horde's
+ *               in case we want to be more like it in the future.
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.1
+ */
+class Prefs {
+
+    /**
+     * Where these preferences can be used. To differentiate between preferences
+     * for a specific script, application, or global.
+     * @var string $scope
+     */
+    var $scope = 'global';
+
+    /**
+     * Indicator whether we save in the database or not.
+     * @var boolean $_perpetual
+     */
+    var $_perpetual = false;
+
+    /**
+     * Hash containing connection parameters.
+     * @var array $params
+     */
+    var $params = array();
+
+
+    /**
+     * Prefs constructor.
+     */
+    function Prefs($dbh=false, $params=array())
+    {
+        $this->params = $params;
+        $_perpetual = false; // Until database routines are completed.
+    }
+
+    /**
+     * Sets the default value of a preference. The pref will be set only if
+     * is not set already.
+     *
+     * @param  string $pref      The name of the preference to modify.
+     * @param  string $val       The new value for this preference.
+     * @param  string $scope     The scope for this preference.
+     *
+     * @return boolean  True if the value was successfully set.
+     */
+    function setDefault($pref, $val, $scope=null)
+    {
+        if (!isset($scope)) {
+            $scope =& $this->scope;
+        }
+
+        // No empty values allowed.
+        if ('' == $pref || '' == $val || '' == $scope) {
+            return false;
+        }
+
+        // Initialized the prefs array.
+        if (!isset($_SESSION['_prefs'])) {
+            $_SESSION['_prefs'] = array();
+        }
+
+        // In case boolean or null values are passed as a string.
+        if ($val == 'true') {
+            $val = true;
+        } else if ($val == 'false') {
+            $val = false;
+        } else if ($val == 'null') {
+            $val = null;
+        }
+
+        // Set it only if not set already.
+        if (!isset($_SESSION['_prefs'][$scope][$pref])) {
+            $_SESSION['_prefs'][$scope][$pref] = $val;
+            return true;
+        }
+    }
+
+    /**
+     * Sets the given preferences to the specific value,
+     *
+     * @param  string $pref      The name of the preference to modify.
+     * @param  string $val       The new value for this preference.
+     * @param  string $scope     The scope for this preference.
+     *
+     * @return boolean  True if the value was successfully set.
+     */
+    function setValue($pref, $val, $scope=null)
+    {
+        if (!isset($scope)) {
+            $scope =& $this->scope;
+        }
+
+        // No empty values allowed.
+        if ('' == $pref || '' == $val || '' == $scope) {
+            return false;
+        }
+
+        // Initialized the prefs array.
+        if (!isset($_SESSION['_prefs'])) {
+            $_SESSION['_prefs'] = array();
+        }
+
+        // In case boolean or null values are passed as a string.
+        if ($val == 'true') {
+            $val = true;
+        } else if ($val == 'false') {
+            $val = false;
+        } else if ($val == 'null') {
+            $val = null;
+        }
+
+        $_SESSION['_prefs'][$scope][$pref] = $val;
+        return true;
+    }
+
+    /**
+     * Returns the value of the requested preference.
+     *
+     * @param string $pref      The name of the preference to retrieve.
+     * @param string $scope     The scope for this preference.
+     *
+     * @return string           The value of the preference.
+     */
+    function getValue($pref, $scope=null)
+    {
+        if (!isset($scope)) {
+            $scope =& $this->scope;
+        }
+
+        return (isset($_SESSION['_prefs'][$scope][$pref])) ? $_SESSION['_prefs'][$scope][$pref] : null;
+    }
+
+    /**
+     * To see if a preference has been set.
+     *
+     * @param string $pref      The name of the preference to check.
+     * @param string $scope     The scope for this preference.
+     *
+     * @return boolean          True if the preference isset and not empty
+     *                          false otherwise.
+     */
+    function exists($pref, $scope=null)
+    {
+        if (!isset($scope)) {
+            $scope =& $this->scope;
+        }
+
+        return isset($_SESSION['_prefs'][$scope][$pref]);
+    }
+
+    /**
+     * Clear a set preference value.
+     *
+     * @param string $pref      The name of the preference to check.
+     * @param string $scope     The scope for this preference.
+     */
+    function clearValue($pref, $scope=null)
+    {
+        if (!isset($scope)) {
+            $scope =& $this->scope;
+        }
+
+        if (isset($_SESSION['_prefs'][$scope][$pref])) {
+            unset($_SESSION['_prefs'][$scope][$pref]);
+        }
+    }
+
+    /**
+     * Retrieves the requested set of preferences from the user's database
+     * entry.
+     *
+     * @param optional array $prefs  An array listing the preferences to
+     *                     retrieve. If not specified, retrieve all of the
+     *                     preferences listed in the $prefs hash.
+     *
+     * @return mixed       True on success or a PEAR_Error object on failure.
+     */
+    function retrieve()
+    {
+        // Check that we don't have the preferences loaded yet.
+        if ($_SESSION['_prefs']['loaded'] == true || !$_perpetual) {
+            return true;
+        }
+
+        // FIXME: Database query goes here....
+        return false;
+
+        $_SESSION['_prefs']['loaded'] = true;
+    }
+
+    /**
+     * Stores preferences to SQL server.
+     *
+     * @param array $prefs (optional) An array listing the preferences to be
+     *                     stored.  If not specified, store all of the
+     *                     preferences listed in the $prefs hash.
+     *
+     * @return mixed       True on success or a PEAR_Error object on failure.
+     */
+    function store($prefs=array())
+    {
+        // If we are not storing prefs in perpetually.
+        if (!$_perpetual) {
+            return true;
+        }
+
+        /*
+         * If a list of preferences to store hasn't been provided in
+         * $prefs, assume all preferences are desired.
+         */
+        if (empty($prefs)) {
+            $prefs =& $_SESSION['_prefs'];
+        } else if ($_SESSION['_prefs']['loaded'] == true) {
+            $prefs = array_merge($_SESSION['_prefs'], $prefs);
+        }
+
+        if (!is_array($prefs) || empty($prefs)) {
+            App::raiseMsg(_("No preferences are available."), MSG_ERR, __FILE__, __LINE__);
+            App::dieBoomerangURL();
+        }
+
+        // FIXME: Database query goes here....
+        return false;
+
+    }
+
+    /**
+     * Perform cleanup operations.
+     */
+    function cleanup()
+    {
+        $_SESSION['_prefs'] = array();
+    }
+}
+
+
+
+
+?>
Index: branches/2.0singleton/lib/RecordLock.inc.php
===================================================================
--- branches/2.0singleton/lib/RecordLock.inc.php	(revision 122)
+++ branches/2.0singleton/lib/RecordLock.inc.php	(revision 122)
@@ -0,0 +1,396 @@
+<?php
+/**
+ * The RecordLock:: class provides a system for locking abstract DB rows.
+ *
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 2.0
+ */
+class RecordLock {
+
+    // Configuration of this object.
+    var $_params = array(
+        'timeout' => 600,
+        'auto_timeout' => 1800,
+        'error_url' => '/lock.php',
+        'db_table' => 'lock_tbl',
+        'create_table' => true, // Automatically create table and verify columns. Better set to false after site launch.
+    );
+
+    // Store lock data from DB.
+    var $data = array();
+
+    // Auth_SQL object from which to access a current user_id.
+    var $_auth;
+
+    /**
+     * This method enforces the singleton pattern for this class.
+     *
+     * @return  object  Reference to the global RecordLock object.
+     * @access  public
+     * @static
+     */
+    function &getInstance($auth_object)
+    {
+        static $instances = array();
+
+        if (!isset($instances[$auth_object->getVal('auth_name')])) {
+            $instances[$auth_object->getVal('auth_name')] = new RecordLock($auth_object);
+        }
+
+        return $instances[$auth_object->getVal('auth_name')];
+    }
+
+    /**
+     * Constructor. Pass an Auth object on which to perform user lookups.
+     *
+     * @param mixed  $auth_object  An Auth_SQL object.
+     */
+    function RecordLock($auth_object)
+    {
+        if (!is_a($auth_object, 'Auth_SQL')) {
+            trigger_error('Constructor not provided a valid Auth_SQL 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
+     */
+    function initDB($recreate_db=false)
+    {
+        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_DEBUG, __FILE__, __LINE__);
+            }
+            DB::query("CREATE TABLE IF NOT EXISTS " . $this->getParam('db_table') . " (
+                lock_id int NOT NULL 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 '0000-00-00 00:00:00',
+                PRIMARY KEY (lock_id),
+                KEY record_table (record_table),
+                KEY record_key (record_key),
+                KEY record_val (record_val)
+            )");
+
+            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.
+     */
+    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 param setting.
+     *
+     * @access  public
+     * @param   string  $params Which param to return.
+     * @return  mixed   Configured param value.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            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.
+     */
+    function select($record_table_or_lock_id, $record_key=null, $record_val=null)
+    {
+        $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 " . $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 " . $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->getVal('user_id') ? 'self-' : ''), $record_table_or_lock_id, $record_key, $record_val), LOG_DEBUG, __FILE__, __LINE__);
+            /// FIX ME: 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.
+     */
+    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.
+     */
+    function isMine()
+    {
+        $this->initDB();
+
+        if (isset($this->data['lock_id'])) {
+            $qid = DB::query("SELECT * FROM " . $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->getVal('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).
+     */
+    function set($record_table, $record_key, $record_val, $title='')
+    {
+        $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 " . $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 " . $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->getVal('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.
+     */
+    function remove()
+    {
+        $this->initDB();
+
+        // Expire old locks.
+        $this->_auto_timeout();
+
+        // Delete a specific lock.
+        DB::query("
+            DELETE FROM " . $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.
+     */
+    function removeAll($user_id=null)
+    {
+        $this->initDB();
+
+        // Expire old locks.
+        $this->_auto_timeout();
+
+        if (isset($user_id)) {
+            // Delete specific user's locks.
+            DB::query("DELETE FROM " . $this->getParam('db_table') . " WHERE set_by_admin_id = '" . DB::escapeString($user_id) . "'");
+            App::logMsg(sprintf('Record locks owned by %s %s have been deleted', $this->_auth->getVal('auth_name'), $this->_auth->getUsername($user_id)), LOG_DEBUG, __FILE__, __LINE__);
+        } else {
+            // Delete ALL locks.
+            DB::query("DELETE FROM " . $this->getParam('db_table') . "");
+            App::logMsg(sprintf('All record locks deleted by %s %s', $this->_auth->getVal('auth_name'), $this->_auth->getVal('username')), LOG_DEBUG, __FILE__, __LINE__);
+        }
+    }
+
+    /**
+     * Delete's all locks that are older than auto_timeout.
+     */
+    function _auto_timeout()
+    {
+        static $_timeout_run = false;
+
+        $this->initDB();
+
+        if (!$_timeout_run) {
+            // Delete all old locks.
+            DB::query("
+                DELETE FROM " . $this->getParam('db_table') . "
+                WHERE DATE_ADD(lock_datetime, INTERVAL '" . $this->getParam('auto_timeout') . "' SECOND) < NOW()
+            ");
+            $_timeout_run = true;
+        }
+    }
+
+    /**
+     * Redirect to record lock error page.
+     */
+    function dieErrorPage()
+    {
+        App::dieURL(sprintf('%s?lock_id=%s&boomerang=%s', $this->getParam('error_url'), $this->data['lock_id'], urlencode(absoluteMe())));
+    }
+
+    /**
+     * Print error page.
+     */
+    function printErrorHTML()
+    {
+        ?>
+        <form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
+        <?php App::printHiddenSession() ?>
+        <input type="hidden" name="lock_id" value="<?php echo $this->getID(); ?>" />
+
+        <p><?php
+        printf(_("The record <strong>%s</strong> is currently being edited by <strong>%s</strong> (%d minutes elapsed). You cannot modify the record while it is locked by another user."),
+            $this->getTitle(),
+            $this->getEditor(),
+            date('i', $this->getSecondsElapsed() + 60)
+        );
+        ?></p>
+
+        <?php if ($this->getSecondsElapsed() > $this->getParam('timeout')) { ?>
+        <p><?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()) ?></p>
+        <input type="submit" name="unlock" value="<?php echo _("Unlock"); ?>" />
+        <?php } ?>
+
+        <input type="submit" name="cancel" value="<?php echo _("Cancel"); ?>" />
+        </form>
+        <?php
+    }
+
+    /**
+     * Return lock_id of locked record.
+     */
+    function getID()
+    {
+        return $this->data['lock_id'];
+    }
+
+    /**
+     * Return title of locked record.
+     */
+    function getTitle()
+    {
+        return $this->data['title'];
+    }
+
+    /**
+     * Return administrator username for locked record.
+     */
+    function getEditor()
+    {
+        return $this->data['editor'];
+    }
+
+    /**
+     * Return total seconds since the record was locked.
+     */
+    function getSecondsElapsed()
+    {
+        if (isset($this->data['lock_datetime']) && $this->data['lock_datetime'] < time()) {
+            return time() - strtotime($this->data['lock_datetime']);
+        } else {
+            return 0;
+        }
+    }
+
+
+} // End of class.
+?>
Index: branches/2.0singleton/lib/RecordVersion.inc.php
===================================================================
--- branches/2.0singleton/lib/RecordVersion.inc.php	(revision 122)
+++ branches/2.0singleton/lib/RecordVersion.inc.php	(revision 122)
@@ -0,0 +1,417 @@
+<?php
+/**
+ * The RecordVersion:: 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 RecordVersion {
+
+    // Configuration of this object.
+    var $_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',
+        'create_table' => true, // Automatically create table and verify columns. Better set to false after site launch.
+        'db_schema_strict' => true, // If true, makes an exact comparison of saved vs. live table schemas. If false, just checks that the saved columns are available.
+    );
+
+    // Auth_SQL object from which to access a current user_id.
+    var $_auth;
+
+    /**
+     * This method enforces the singleton pattern for this class.
+     *
+     * @return  object  Reference to the global RecordVersion object.
+     * @access  public
+     * @static
+     */
+    function &getInstance($auth_object)
+    {
+        static $instances = array();
+
+        if (!isset($instances[$auth_object->getVal('auth_name')])) {
+            $instances[$auth_object->getVal('auth_name')] = new RecordVersion($auth_object);
+        }
+
+        return $instances[$auth_object->getVal('auth_name')];
+    }
+
+    /**
+     * Constructor. Pass an Auth object on which to perform user lookups.
+     *
+     * @param mixed  $auth_object  An Auth_SQL object.
+     */
+    function RecordVersion($auth_object)
+    {
+        if (!is_a($auth_object, 'Auth_SQL')) {
+            trigger_error('Constructor not provided a valid Auth_SQL 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
+     */
+    function initDB($recreate_db=false)
+    {
+        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_DEBUG, __FILE__, __LINE__);
+            }
+            DB::query("CREATE TABLE IF NOT EXISTS " . $this->getParam('db_table') . " (
+                version_id int NOT NULL 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_notes varchar(255) NOT NULL default '',
+                saved_by_admin_id smallint(11) NOT NULL default '0',
+                version_datetime datetime NOT NULL default '0000-00-00 00:00:00',
+                PRIMARY KEY (version_id),
+                KEY record_table (record_table),
+                KEY record_key (record_key),
+                KEY record_val (record_val)
+            )");
+
+            if (!DB::columnExists($this->getParam('db_table'), array(
+                'version_id',
+                'record_table',
+                'record_key',
+                'record_val',
+                'version_data',
+                'version_title',
+                'version_notes',
+                'saved_by_admin_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.
+     */
+    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 param setting.
+     *
+     * @access  public
+     * @param   string  $params Which param to return.
+     * @return  mixed   Configured param value.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            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).
+     */
+    function create($record_table, $record_key, $record_val, $title='', $notes='')
+    {
+        $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;
+        }
+
+        // Clean-up old versions.
+        $this->deleteOld($record_table, $record_key, $record_val);
+
+        // Save as new version.
+        DB::query("
+            INSERT INTO " . $this->getParam('db_table') . " (
+                record_table,
+                record_key,
+                record_val,
+                version_data,
+                version_title,
+                version_notes,
+                saved_by_admin_id,
+                version_datetime
+            ) VALUES (
+                '" . DB::escapeString($record_table) . "',
+                '" . DB::escapeString($record_key) . "',
+                '" . DB::escapeString($record_val) . "',
+                '" . DB::escapeString(gzcompress(serialize($record), 9)) . "',
+                '" . DB::escapeString($title) . "',
+                '" . DB::escapeString($notes) . "',
+                '" . DB::escapeString($this->_auth->getVal('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).
+     */
+    function restore($version_id)
+    {
+        $this->initDB();
+
+        // Get version data.
+        $qid = DB::query("
+            SELECT * FROM " . $this->getParam('db_table') . "
+            WHERE version_id = '" . DB::escapeString($version_id) . "'
+        ");
+        if (!$record = mysql_fetch_assoc($qid)) {
+            App::raiseMsg(sprintf(_("Version ID %s%s not found."), $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), MSG_WARNING, __FILE__, __LINE__);
+            App::logMsg(sprintf(_("Version ID %s%s not found."), $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+        $data = unserialize(gzuncompress($record['version_data']));
+
+        // 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 ID %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 ID %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 = ',';
+        }
+
+        // Replace current record with specified versioned record.
+        DB::query("
+            REPLACE INTO " . $record['record_table'] . " (
+                $replace_keys
+            ) VALUES (
+                $replace_values
+            )
+        ");
+
+        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.
+     */
+    function deleteOld($record_table, $record_key, $record_val)
+    {
+        $this->initDB();
+
+        // Get total number of versions for this record.
+        $qid = DB::query("
+            SELECT COUNT(*) FROM " . $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 " . $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 " . ($v_count - $this->getParam('min_qty')) . "
+                ");
+                while (list($old_id) = mysql_fetch_row($qid)) {
+                    $old_versions[] = $old_id;
+                }
+                DB::query("
+                    DELETE FROM " . $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 " . $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')) . "
+                ");
+                while (list($old_id) = mysql_fetch_row($qid)) {
+                    $old_versions[] = $old_id;
+                }
+                if (sizeof($old_versions) > 0) {
+                    DB::query("
+                        DELETE FROM " . $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.
+     */
+    function getList($record_table, $record_key, $record_val)
+    {
+        $this->initDB();
+
+        // Get versions of this record.
+        $qid = DB::query("
+            SELECT version_id, saved_by_admin_id, version_datetime, version_title
+            FROM " . $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->getVal('auth_type') . ' ' . $this->_auth->getUsername($row['saved_by_admin_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.
+     */
+    function getVerson($version_id)
+    {
+        $this->initDB();
+
+        // Get version data.
+        $qid = DB::query("
+            SELECT * FROM " . $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.
+     */
+    function getData($version_id)
+    {
+        $this->initDB();
+
+        // Get version data.
+        $qid = DB::query("
+            SELECT * FROM " . $this->getParam('db_table') . "
+            WHERE version_id = '" . DB::escapeString($version_id) . "'
+        ");
+        $record = mysql_fetch_assoc($qid);
+        if (isset($record['version_data'])) {
+            return unserialize(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.
+     */
+    function getCurrent($record_table, $record_key, $record_val)
+    {
+        $this->initDB();
+
+        $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: branches/2.0singleton/lib/ScriptTimer.inc.php
===================================================================
--- branches/2.0singleton/lib/ScriptTimer.inc.php	(revision 122)
+++ branches/2.0singleton/lib/ScriptTimer.inc.php	(revision 122)
@@ -0,0 +1,61 @@
+<?php
+/**
+ * ScriptTimer.inc.php 
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+ 
+class ScriptTimer {
+
+    var $time_format = '%.10s';
+    var $_timing_start_times = array();
+    var $_timing_stop_times = array();
+    var $_timing_cumulative_times = array();
+    
+	function start($name='default')
+	{
+        $this->_timing_start_times[$name] = explode(' ', microtime());
+	}
+	
+	function stop($name='default')
+	{
+        $this->_timing_stop_times[$name] = explode(' ', microtime());
+        $this->_timing_cumulative_times[$name] += $this->getTime($name);
+	}
+	
+	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;
+	}
+	
+	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: branches/2.0singleton/lib/SessionCache.inc.php
===================================================================
--- branches/2.0singleton/lib/SessionCache.inc.php	(revision 122)
+++ branches/2.0singleton/lib/SessionCache.inc.php	(revision 122)
@@ -0,0 +1,211 @@
+<?php
+/**
+ * SessionCache.inc.php
+ * Provides an API for storing a limited amount of data
+ * intended to have a short lifetime in a user's session.
+ *
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information.
+ * @author  Quinn Comendant <quinn@strangecode.com>
+ * @version 1.2
+ * @since   2001
+ */
+class SessionCache
+{
+    var $_params = array(
+        'enabled' => true,
+        'soft_limit' => 204800,
+        'hard_limit' => 4194304,
+        'min_items' => 3,
+    );
+
+    /**
+     * This method enforces the singleton pattern for this class.
+     *
+     * @return  object  Reference to the global SessionCache object.
+     * @access  public
+     * @static
+     */
+    function &getInstance() {
+        static $instance = null;
+
+        if ($instance === null) {
+            $instance = new SessionCache();
+        }
+
+        return $instance;
+    }
+
+    /**
+     * Set (or overwrite existing) parameters by passing an array of new parameters.
+     *
+     * @access public
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    function setParam($params)
+    {
+        if (!isset($_this) || !is_a($_this, 'SessionCache') && !is_subclass_of($_this, 'SessionCache')) {
+            $_this =& SessionCache::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.
+     */
+    function getParam($param)
+    {
+        if (!isset($_this) || !is_a($_this, 'SessionCache') && !is_subclass_of($_this, 'SessionCache')) {
+            $_this =& SessionCache::getInstance();
+        }
+
+        if (isset($_this->_params[$param])) {
+            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 $var_id is is md5'ed
+     * because if a variable id is a very large integer, the array_shift function
+     * will reset the key to the next largest int key. Weird behaviour I can't
+     * understand. $session_cache[32341234123] will become $session_cache[0]
+     * for example. Usage warning: if the variable is too big to fit, or is
+     * old and discarded, you must provide alternative ways of accessing the data.
+     *
+     * @param mixed $var          The var to store in the session cache.
+     * @param str   $var_id       An identifyer for the cached object.
+     * @param bool  $force_it_in  If we have something really big that we
+     *                            still want to cache, setting this true
+     *                            allows this.
+     *
+     * @return string        The $var_id, or false if the object was too big to cache.
+     */
+    function putCache($var, $var_id, $force_it_in=false)
+    {
+        if (!isset($_this) || !is_a($_this, 'SessionCache') && !is_subclass_of($_this, 'SessionCache')) {
+            $_this =& SessionCache::getInstance();
+        }
+
+        if (!$_this->getParam('enabled')) {
+            App::logMsg(sprintf('SessionCache not enabled, not saving data.', null), LOG_DEBUG, __FILE__, __LINE__);
+            return false;
+        }
+
+        $var_id = md5($var_id);
+        $serialized_var = serialize($var);
+        $serialized_var_len = strlen($serialized_var);
+
+        if ($serialized_var_len >= $_this->getParam('soft_limit') && !$force_it_in) {
+            App::logMsg(sprintf('Serialized variable (%s bytes) more than soft_limit (%s bytes).', $serialized_var_len, $_this->getParam('soft_limit')), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        if ($serialized_var_len >= $_this->getParam('hard_limit')) {
+            App::logMsg(sprintf('Serialized variable (%s bytes) more than hard_limit (%s bytes).', $serialized_var_len, $_this->getParam('hard_limit')), LOG_NOTICE, __FILE__, __LINE__);
+            return false;
+        }
+
+        if (!isset($_SESSION['_session_cache'])) {
+            $_SESSION['_session_cache'] = array();
+        } else {
+            unset($_SESSION['_session_cache'][$var_id]);
+            // Continue to prune the cache if it's length is too long for the new variable to fit, but keep at least MIN_ITEMS at least.
+            while (strlen(serialize($_SESSION['_session_cache'])) + $serialized_var_len >= $_this->getParam('soft_limit')
+            && sizeof($_SESSION['_session_cache']) >= $_this->getParam('min_items')) {
+                array_shift($_SESSION['_session_cache']);
+            }
+        }
+        $_SESSION['_session_cache'][$var_id] =& $serialized_var;
+
+        if ($serialized_var_len >= 1024000) {
+            App::logMsg(sprintf('Successfully cached oversized variable (%s bytes).', $serialized_var_len), LOG_DEBUG, __FILE__, __LINE__);
+        }
+
+        return $var_id;
+    }
+
+    /**
+     * Retrives 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 $var_id  The identifyer for the datum to retrieve.
+     *
+     * @return mixed          The requested datum, or false on failure.
+     */
+    function getCache($var_id)
+    {
+        if (!isset($_this) || !is_a($_this, 'SessionCache') && !is_subclass_of($_this, 'SessionCache')) {
+            $_this =& SessionCache::getInstance();
+        }
+
+        if (!$_this->getParam('enabled')) {
+            return false;
+        }
+
+        $var_id = md5($var_id);
+        if (isset($_SESSION['_session_cache'][$var_id])) {
+            // Move the accessed cached datum to the top of the stack. Maybe somebody knows a better way to do this?
+            $tmp =& $_SESSION['_session_cache'][$var_id];
+            unset($_SESSION['_session_cache'][$var_id]);
+            $_SESSION['_session_cache'][$var_id] =& $tmp;
+            // Return the unserialized datum.
+            return unserialize($_SESSION['_session_cache'][$var_id]);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Tells you if the object is cached.
+     *
+     * @param string $var_id  The ID of the object to check.
+     *
+     * @return bool           The return from isset().
+     */
+    function isCached($var_id)
+    {
+        if (!isset($_this) || !is_a($_this, 'SessionCache') && !is_subclass_of($_this, 'SessionCache')) {
+            $_this =& SessionCache::getInstance();
+        }
+
+        if (!$_this->getParam('enabled')) {
+            return false;
+        }
+
+        $var_id = md5($var_id);
+        return isset($_SESSION['_session_cache'][$var_id]);
+    }
+
+    /**
+     * Tells you if the object is cached.
+     *
+     * @param string $var_id  The ID of the object to check.
+     *
+     * @return bool           The return from isset().
+     */
+    function breakCache($var_id)
+    {
+        $var_id = md5($var_id);
+        if (isset($_SESSION['_session_cache'][$var_id])) {
+            unset($_SESSION['_session_cache'][$var_id]);
+        }
+    }
+
+// END SessionCache
+}
+
+?>
Index: branches/2.0singleton/lib/SortOrder.inc.php
===================================================================
--- branches/2.0singleton/lib/SortOrder.inc.php	(revision 122)
+++ branches/2.0singleton/lib/SortOrder.inc.php	(revision 122)
@@ -0,0 +1,170 @@
+<?php
+/**
+ * SortOrder.inc.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ *
+ * SortOrder can determing 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 {
+
+    var $_columns;
+    var $sort_by;
+    var $order;
+    var $asc_widget = '<img src="/admin/_widgets/sort_ascending.gif" alt="[ASC]" width="11" height="7" border="0" />';
+    var $desc_widget = '<img src="/admin/_widgets/sort_descending.gif" alt="[DESC]" width="11" height="7" border="0" />';
+    var $default_sort;
+    var $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.
+     */
+    function SortOrder($default_sort = '', $default_order = '')
+    {
+        $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.
+     */
+    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.
+     */
+    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;
+            Prefs::setValue('sort_by', $this->sort_by, $_SERVER['PHP_SELF']);
+        } else if (Prefs::exists('sort_by', $_SERVER['PHP_SELF'])) {
+            $this->sort_by = Prefs::getValue('sort_by', $_SERVER['PHP_SELF']);
+        } 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 instanciation).
+        $new_order = getFormData('order');
+        if (!empty($new_order)) {
+            $this->order = $new_order;
+            Prefs::setValue('sort_order', $this->order, $_SERVER['PHP_SELF']);
+        } else if (Prefs::exists('sort_order', $_SERVER['PHP_SELF'])) {
+            $this->order = Prefs::getValue('sort_order', $_SERVER['PHP_SELF']);
+        } 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)
+     */
+    function set($sort = null, $order = null)
+    {
+        // Set new sort value.
+        if (isset($sort)) {
+            $this->sort_by = $sort;
+            Prefs::setValue('sort_by', $this->sort_by, $_SERVER['PHP_SELF']);
+        }
+
+        // Set new order value.
+        if (isset($order)) {
+            $this->order = $order;
+            Prefs::setValue('sort_order', $this->order, $_SERVER['PHP_SELF']);
+        }
+    }
+
+
+    /**
+     * Returns the SQL code to sort by set column and set order.
+     */
+    function getSortOrderSQL()
+    {
+        if (!isset($this->_columns[strtolower($this->sort_by)])) {
+            $this->sort_by = $this->default_sort;
+            $this->order = $this->default_order;
+        }
+        if (!isset($this->_columns[strtolower($this->sort_by)][strtolower($this->order)])) {
+            $this->order = 'ASC';
+        }
+
+        if (!empty($this->_columns[strtolower($this->sort_by)][strtolower($this->order)])) {
+            return sprintf(' ORDER BY %s ', DB::escapeString($this->_columns[strtolower($this->sort_by)][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.
+     *
+     * @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)
+     */
+    function printSortHeader($col, $col_name, $default_order='ASC')
+    {
+        if ($this->sort_by == $col) {
+            if (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"); ?>"><?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"); ?>"><?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); ?>"><?php echo $col_name; ?></a><?php
+        }
+    }
+
+}
+
+?>
Index: branches/2.0singleton/lib/SpellCheck.inc.php
===================================================================
--- branches/2.0singleton/lib/SpellCheck.inc.php	(revision 122)
+++ branches/2.0singleton/lib/SpellCheck.inc.php	(revision 122)
@@ -0,0 +1,411 @@
+<?php
+/**
+ * SpellCheck.inc.php
+ * code by strangecode :: www.strangecode.com :: this document contains copyrighted information
+ *
+ * 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 {
+
+    var $_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>',
+    );
+
+    var $_pspell_cfg_handle;
+    var $_pspell_handle;
+    var $_use_personal_wordlist = false;
+    var $_errors = array();
+
+    /**
+     * Constructor.
+     *
+     * @param  array    $params     Array of parameters (key => val pairs).
+     */
+    function SpellCheck($params)
+    {
+        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;
+                App::logMsg(sprintf('Using personal wordlist: %s', $this->getParam('personal_wordlist')), LOG_DEBUG, __FILE__, __LINE__);
+            }
+        }
+
+        $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).
+     */
+    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);
+        } 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.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            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.
+     */
+    function anyErrors()
+    {
+        return (sizeof($this->_errors) > 0);
+    }
+
+    /**
+     * Reset the error list.
+     */
+    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
+     */
+    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
+     */
+    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
+     */
+    function add($word)
+    {
+        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
+     */
+    function save()
+    {
+        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 mispelled 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
+     */
+    function getStringSuggestions($string)
+    {
+        $corrections = array();
+        $words = preg_split('/([\W]+?)/', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+        // Remove non-word elements.
+        $words = preg_grep('/\w+/', $words);
+
+        if (is_array($words) && !empty($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
+     */
+    function checkString($string)
+    {
+        $errors = array();
+        $words = preg_split('/([\W]+?)/', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+        // Remove non-word elements.
+        $check_words = preg_grep('/\w+/', $words);
+
+        if (is_array($check_words) && !empty($check_words)) {
+            foreach ($check_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
+     */
+    function getStringHighlighted($string, $show_footnote=false)
+    {
+        $words = preg_split('/([\W]+?)/', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+        $check_words = preg_grep('/\w+/', $words);
+        $cnt = 0;
+        if (is_array($check_words) && !empty($check_words)) {
+            foreach ($check_words as $i => $word) {
+                if (!$this->check($word)) {
+                    $footnote = $show_footnote ? '<sup>' . ++$cnt . '</sup>' : '';
+                    $words[$i] = $this->getParam('highlight_start') . $word . $this->getParam('highlight_end') . $footnote;
+                }
+            }
+        }
+        return join('', $words);
+    }
+
+    /**
+     * Prints the HTML for correcting all mispellings 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
+     */
+    function printCorrectionForm($form_name)
+    {
+        ?>
+        <input name="<?php echo $form_name ?>" type="hidden" value="<?php echo 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>
+                <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 $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
+     */
+    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
+     */
+    function applyFormCorrections($form_name)
+    {
+        $form_words = preg_split('/([\W]+?)/', 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: branches/2.0singleton/lib/TemplateGlue.inc.php
===================================================================
--- branches/2.0singleton/lib/TemplateGlue.inc.php	(revision 122)
+++ branches/2.0singleton/lib/TemplateGlue.inc.php	(revision 122)
@@ -0,0 +1,451 @@
+<?php
+/**
+ * TemplateGlue.inc.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+/**
+ * Print URL to download file with BBEdit/Interarchy. "USER" must be a pre-set
+ * environment variable. Files must reside within and be relative to env "DOCUMENT_ROOT".
+ *
+ * @access  public
+ * @param   mixed   $file   File path relative to DOCUMENT_ROOT (i.e. '/_templates/index.ihtml').
+ * @return  string  <a> with file basename as text.
+ */
+function bbftp($file)
+{
+    return sprintf('<a href="bbftp://%s@%s:%s%s" title="%s" style="text-decoration: none; border-bottom: blue 1px dotted;">%s</a>', $_SERVER['USER'], $_SERVER['HTTP_HOST'], $_SERVER['DOCUMENT_ROOT'], $file, $file, basename($file));
+}
+
+/**
+ * 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 existance
+ * of options. (Quinn 10 Feb 2001)
+ *
+ * @param  string $db_table   database table to lookup
+ * @param  string $db_col     database column to lookup
+ * @return array    Array of the set/enum values on success, false on failure.
+ */
+function getSetEnumFieldValues($db_table, $db_col)
+{
+    $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)) {
+        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
+ */
+function printSetSelectForm($db_table, $db_col, $preselected, $blank=false)
+{
+    $values = getSetEnumFieldValues($db_table, $db_col);
+    if ($values === false) {
+        ?><option value="">&nbsp;</option>
+        <?php
+        return false;
+    }
+    if (true === $blank) {
+        $selected = ($preselected == '') ? ' selected' : '';
+        ?><option value=""<?php echo $selected; ?>>&nbsp;</option>
+        <?php
+    }
+    // When the 'blank' value needs a specific key->val pair.
+    if (is_array($blank)) {
+        foreach ($blank as $key=>$val) {
+            $selected = ($preselected == $val) ? ' selected="selected"' : '';
+            ?><option value="<?php echo $key; ?>"<?php echo $selected; ?>><?php echo oTxt($val); ?></option>
+            <?php
+        }
+    }
+    foreach ($values as $v) {
+        $selected = ($v == $preselected) ? ' selected' : '';
+        ?><option value="<?php echo $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
+ */
+function printEnumRadios($name, $db_table, $db_col, $preselected, $blank=false)
+{
+    $values = getSetEnumFieldValues($db_table, $db_col);
+    if ($values === false) {
+        return false;
+    }
+
+    foreach ($values as $v) {
+        $selected = ($v == $preselected) ? ' selected' : '';
+        ?><input type="radio" name="<?php echo $name ?>" id="<?php echo $v; ?>" value="<?php echo $v; ?>"<?php echo $selected; ?> /><label for="<?php echo $v; ?>"><?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 multidimentional array.
+ */
+function printSetCheckboxes($db_table, $db_col, $preselected, $columns=1, $flag=null)
+{
+    ?>
+    <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);
+    }
+
+    // Retreive values of a Set or ENUM database column.
+    $values = getSetEnumFieldValues($db_table, $db_col);
+
+    // 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 multidimentioal array checkboxes.
+            $html_name = 'dbcol[' . $db_col . '][' . $v . ']';
+        } else {
+            // Print a cell with basic named checkboxes.
+            $html_name = $db_col . '[' . $v . ']';
+        }
+        ?><td><input type="checkbox" name="<?php echo $html_name ?>" id="<?php echo $html_name ?>"<?php echo $checked; ?> /><label for="<?php echo $html_name ?>"><?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  int    $flag          set to 'allone' for name of input fields to all be the same of a multidimentional array.
+ */
+function printSetRadios($db_table, $db_col, $preselected, $columns=1, $flag=null)
+{
+    ?>
+    <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);
+    }
+
+    // Retreive values of a Set or ENUM database column.
+    $values = getSetEnumFieldValues($db_table, $db_col);
+
+    // 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><input type="radio" name="<?php echo $db_col; ?>" id="<?php echo $db_col . '-' . $v; ?>" value="<?php echo $v ?>"<?php echo $checked; ?> /><label class="sc-label-secondary" for="<?php echo $db_col . '-' . $v ?>"><?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   $blank            leave one blank at the top?
+ * @param  string $extra_clause     SQL exclude cluase. Something like "WHERE girls != 'buckteeth'"
+ */
+function printSelectForm($db_table, $key_column, $val_column, $preselected, $blank=false, $extra_clause='')
+{
+    // Sometimes preselected comes as a comma list.
+    if (!is_array($preselected)) {
+        $preselected = array($preselected);
+    }
+
+    // Print a blank first option.
+    if (true === $blank) {
+        $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($blank)) {
+        foreach ($blank as $key=>$val) {
+            $selected = in_array($val, $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);
+    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)
+{
+    // 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 ($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] . ']';
+        ?>
+        <td><input type="checkbox" name="<?php echo $html_name ?>" id="<?php echo $html_name ?>"<?php echo $checked; ?> /></td>
+        <td><label for="<?php echo $html_name ?>" class="normal"><?php echo oTxt($box[$key_column]); ?></label></td>
+        <?php /// FIXME: class=normal?
+    }
+
+    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 $button_name => $button_text) {
+            ?><input type="submit" name="<?php echo $button_name ?>" value="<?php echo oTxt($button_text); ?>" /><?php
+        }
+    }
+}
+
+?>
Index: branches/2.0singleton/lib/Upload.inc.php
===================================================================
--- branches/2.0singleton/lib/Upload.inc.php	(revision 122)
+++ branches/2.0singleton/lib/Upload.inc.php	(revision 122)
@@ -0,0 +1,473 @@
+<?php
+/**
+ * Upload.inc.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ *
+ * 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.
+    var $_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 extentions.
+        'valid_file_extensions' => array('jpg', 'jpeg', 'gif', 'png', 'pdf', 'txt', 'text', 'html', 'htm'),
+    );
+
+    // Array of files with errors.
+    var $errors = array();
+
+    // Array of file name extensions and corresponding mime-types.
+    var $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/mpeg', '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/mp4v-es', '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).
+     */
+    function setParam($params)
+    {
+        if (isset($params) && is_array($params)) {
+
+            // Enforce valid upload_path parameter.
+            if (isset($params['upload_path'])) {
+                $params['upload_path'] = realpath($params['upload_path']);
+                // 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'], $this->getParam('dest_dir_perms'));
+                    if (!is_dir($params['upload_path'])) {
+                        App::logMsg(sprintf('Upload directory invalid: %s', $params['upload_path']), LOG_ERR, __FILE__, __LINE__);
+                        trigger_error(sprintf('Upload directory invalid: %s', $params['upload_path']), E_USER_ERROR);
+                    }
+                }
+                // 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.
+     */
+    function getParam($param)
+    {
+        if (isset($this->_params[$param])) {
+            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 filename in the case of multiple files.
+     * @return  mixed   Returns FALSE if a major error occured preventing any file uploads.
+     *                  Returns an empty array if any minor errors occured or no files were found.
+     *                  Returns a multidimentional array of filenames, sizes and extentions, 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.
+     */
+    function process($form_name, $custom_file_name=null)
+    {
+        // 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.
+                continue;
+            }
+
+            // Determine final file name.
+            if ($num == 1) {
+                // Single upload.
+                if (isset($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('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) && is_array($custom_file_name[$i]) && '' != $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]) {
+                $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 %s UPLOAD_ERR_INI_SIZE (currently %s)."), $files['error'][$i], $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]) {
+                $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 %s UPLOAD_ERR_FORM_SIZE (currently %s)."), $files['error'][$i], $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]) {
+                $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 %s UPLOAD_ERR_PARTIAL."), $files['error'][$i], $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]) {
+                $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 %s UPLOAD_ERR_NO_FILE."), $files['error'][$i], $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]) {
+                $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 %s UPLOAD_ERR_NO_TMP_DIR."), $files['error'][$i], $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(strtolower($this->getFilenameExtension($file_name)), $this->getParam('valid_file_extensions'))) {
+                $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 doesn't 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')) {
+                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 = $this->cleanFileName($file_name);
+
+            // FINAL path and file name, lowercase extension.
+            $file_extension = strtolower($this->getFilenameExtension($file_name));
+            $file_name = sprintf('%s.%s', substr($file_name, 0, 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,
+                );
+            } 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_NOTICE, __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.
+     * @return  bool                Success of operation.
+     */
+    function deleteFile($file_name)
+    {
+        // 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') . '/' . $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__);
+        } 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.
+     */
+    function moveFile($old_name, $new_name)
+    {
+        // 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.
+     */
+    function exists($file_name)
+    {
+        // 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
+     */
+    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.
+     */
+    function getErrors()
+    {
+        return $this->errors;
+    }
+
+    /**
+     * Determintes if any errors occured while calling the Upload::process method.
+     *
+     * @access  public
+     */
+    function anyErrors()
+    {
+        return sizeof($this->errors) > 0;
+    }
+
+    /**
+     * Removes unsafe characters from file name.
+     *
+     * @access  public
+     * @param   string  $file_name  A name of a file.
+     * @return  string              The same name, but cleaned.
+     */
+    function cleanFileName($file_name)
+    {
+        $bad  = 'Ã¡Ã©Ã­Ã³ÃºÃ ÃšÃ¬Ã²Ã¹Ã€Ã«Ã¯Ã¶ÃŒÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃÃ¢ÃªÃ®ÃŽÃ»ÃÃÃÃÃÃ±Ã§Ã@';
+        $good = 'aeiouaeiouaeiouAEIOUAEIOUAEIOUaeiouAEIOUncCa';
+        $file_name = trim($file_name);
+        $file_name = strtr($file_name, $bad, $good);
+        $file_name = preg_replace('/[^-\w.,~_=+()]/i', '_', $file_name);
+        $file_name = substr($file_name, 0, 250);
+        return $file_name;
+    }
+
+
+    /**
+     * Returns the extention of a file name, or an empty string if non exists.
+     *
+     * @access  public
+     * @param   string  $file_name  A name of a file, with extention 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] : '';
+    }
+
+    /**
+     * 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__.
+     */
+    function _raiseMsg($message, $type, $file, $line)
+    {
+        if ($this->getParam('display_messages') === true || (is_int($this->getParam('display_messages')) && $this->getParam('display_messages') & $type > 0)) {
+            App::raiseMsg($message, $type, $file, $line);
+        }
+    }
+}
+
+?>
Index: branches/2.0singleton/lib/Utilities.inc.php
===================================================================
--- branches/2.0singleton/lib/Utilities.inc.php	(revision 122)
+++ branches/2.0singleton/lib/Utilities.inc.php	(revision 122)
@@ -0,0 +1,906 @@
+<?php
+/**
+ * Utilities.inc.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+
+/**
+ * Print variable dump.
+ *
+ * @param  mixed $var      Variable to dump.
+ * @param  bool  $display   Hide the dump in HTML comments?
+ * @param  bool  $var_dump Use var_dump instead of print_r.
+ */
+function dump($var, $display=false, $var_dump=false)
+{
+    echo $display ? "\n<br /><pre>\n" : "\n\n\n<!--\n";
+    if ($var_dump) {
+        var_dump($var);
+    } else {
+        print_r($var);
+    }
+    echo $display ?  "\n</pre><br />\n" : "\n-->\n\n\n";
+}
+
+/**
+ * Return dump as variable.
+ *
+ * @param  mixed $var      Variable to dump.
+ * @return string Dump of var.
+ */
+function getDump($var)
+{
+    ob_start();
+    print_r($var);
+    $d = ob_get_contents();
+    ob_end_clean();
+    return $d;
+}
+
+/**
+ * Return dump as cleaned text. Useful for dumping data into emails.
+ *
+ * @param  mixed $var      Variable to dump.
+ * @return string Dump of var.
+ */
+function fancyDump($var, $indent='')
+{
+    $output = '';
+    if (is_array($var)) {
+        foreach ($var as $k=>$v) {
+            $k = ucfirst(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;
+}
+
+/**
+ * Returns text with appropriate html translations.
+ *
+ * @param  string $txt              Text to clean.
+ * @param  bool   $preserve_html    If set to true, oTxt will not translage <, >, ", or '
+ *                                  characters into HTML entities. This allows HTML to pass
+ *                                  through unmunged.
+ * @return string                   Cleaned text.
+ */
+function oTxt($txt, $preserve_html=false)
+{
+    $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 occurance where the & is followed by a alphanum or unicode caracter.
+    $search['ampersand']        = '/&(?![\w\d#]{1,10};)/';
+    $replace['ampersand']       = '&amp;';
+
+    return preg_replace($search, $replace, htmlentities($txt, ENT_QUOTES, App::getParam('character_set')));
+}
+
+/**
+ * Returns text with stylistic modifications. Warning: this will break some HTML attibutes!
+ * TODO: Allow a string such as this to be passted: <a href="javascript:openPopup('/foo/bar.php')">Click here</a>
+ *
+ * @param  string   $txt  Text to clean.
+ * @return string         Cleaned text.
+ */
+function fancyTxt($txt)
+{
+    $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, $txt);
+}
+
+
+/**
+ * Generates a hexadecibal 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)
+{
+    $r = substr(md5($text), 0, 1);
+    $g = substr(md5($text), 1, 1);
+    $b = substr(md5($text), 2, 1);
+
+    switch ($method) {
+    case 2 :
+        if (hexdec($r) > hexdec('c')) {
+            $r = dechex(hexdec('f') - hexdec($r));
+        }
+        if (hexdec($g) > hexdec('c')) {
+            $g = dechex(hexdec('f') - hexdec($g));
+        }
+        if (hexdec($b) > hexdec('c')) {
+            $b = dechex(hexdec('f') - hexdec($b));
+        }
+        break;
+
+    case 1 :
+    default :
+        $r = dechex(round(hexdec($r) * .8));
+        $g = dechex(round(hexdec($g) * .8));
+        $b = dechex(round(hexdec($b) * .6));
+        break;
+    }
+
+    return $r . $r . $g . $g . $b . $b;
+}
+
+/**
+ * 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)
+{
+    $ouput = '';
+    $num = 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);
+}
+
+/**
+ * Turns "a really long string" into "a rea...string"
+ *
+ * @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, $where='middle')
+{
+    $part1 = floor(($len - 3) / 2);
+    $part2 = ceil(($len - 3) / 2);
+    switch ($where) {
+    case 'start' :
+        return preg_replace(array(sprintf('/^.{4,}(.{%s})$/', $part1 + $part2), '/\s*\.{3,}\s*/'), array('...$1', '...'), $str);
+        break;
+    default :
+    case 'middle' :
+        return preg_replace(array(sprintf('/^(.{%s}).{4,}(.{%s})$/', $part1, $part2), '/\s*\.{3,}\s*/'), array('$1...$2', '...'), $str);
+        break;    
+    case 'end' :
+        return preg_replace(array(sprintf('/^(.{%s}).{4,}$/', $part1 + $part2), '/\s*\.{3,}\s*/'), array('$1...', '...'), $str);
+        break;    
+    }
+}
+
+/**
+ * Return a human readable filesize.
+ *
+ * @param       int    $size        Size
+ * @param       int    $unit        The maximum unit
+ * @param       int    $format      The return string format
+ * @author      Aidan Lister <aidan@php.net>
+ * @version     1.1.0
+ */
+function humanFileSize($size, $unit=null, $format='%01.2f %s')
+{
+    // Units
+    $units = array('B', 'KB', 'MB', 'GB', 'TB');
+    $ii = count($units) - 1;
+
+    // Max unit
+    $unit = array_search((string) $unit, $units);
+    if ($unit === null || $unit === false) {
+        $unit = $ii;
+    }
+
+    // Loop
+    $i = 0;
+    while ($unit != $i && $size >= 1024 && $i < $ii) {
+        $size /= 1024;
+        $i++;
+    }
+
+    return sprintf($format, $size, $units[$i]);
+}
+
+/**
+ * Returns stats of a file from the include path.
+ *
+ * @param   string  $file   File in include path.
+ * @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)
+{
+    // Open file pointer read-only using include path.
+    if ($fp = fopen($file, 'r', true)) {
+        // File opend successfully, get stats.
+        $stats = fstat($fp);
+        fclose($fp);
+        // Return specified stats.
+        return $stats[$stat];
+    } else {
+        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 avilable.
+ *
+ * @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 excaped.
+ * @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 recursivly, 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;
+
+    if (is_array($data)) {
+        foreach ($data as $key => $val) {
+            // If the prefix is empty, use the $key as the name of the first dimention of the "array".
+            // ...otherwise, append the key as a new dimention 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 dimention 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.
+ *
+ * @param  mixed $data        An array to transverse recursivly, or a string
+ *                            to use directly to create url arguments.
+ * @param  string $prefix     The name of the first dimention 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 arrray with the result from a multiple ereg search.
+ * Curtesy of Bruno - rbronosky@mac.com - 10-May-2001
+ * Blame him for the funky do...while loop.
+ *
+ * @param  mixed $pattern   regular expression needle
+ * @param  mixed $string   haystack
+ * @return array    populated with each found result
+ */
+function eregAll($pattern, $string)
+{
+    do {
+        if (!ereg($pattern, $string, $temp)) {
+             continue;
+        }
+        $string = str_replace($temp[0], '', $string);
+        $results[] = $temp;
+    } while (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)
+{
+    if (is_array($in) && !empty($in)) {
+        return "'" . join("', '", 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: 0000-00-00 00:00:00.
+ * @return string          SQL-safe date.
+ */
+function strToSQLDate($date, $format='Y-m-d H:i:s')
+{
+    // Translate the human string date into SQL-safe date format.
+    if (empty($date) || '0000-00-00' == $date || strtotime($date) === -1) {
+        $sql_date = '0000-00-00';
+    } else {
+        $sql_date = date($format, strtotime($date));
+    }
+
+    return $sql_date;
+}
+
+/**
+ * If magic_quotes_gpc is in use, run stripslashes() on $var. If $var is an
+ * array, stripslashes is run on each value, recursivly, 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)
+{
+    static $magic_quotes_gpc;
+
+    if (!isset($magic_quotes_gpc)) {
+        $magic_quotes_gpc = get_magic_quotes_gpc();
+    }
+
+    if ($magic_quotes_gpc) {
+        if (!is_array($var)) {
+            $var = stripslashes($var);
+        } else {
+            foreach ($var as $key=>$val) {
+                if (is_array($val)) {
+                    $var[$key] = dispelMagicQuotes($val);
+                } 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)
+{
+    if ('POST' == getenv('REQUEST_METHOD') && is_null($var)) {
+        return dispelMagicQuotes($_POST);
+    } else if ('GET' == getenv('REQUEST_METHOD') && is_null($var)) {
+        return dispelMagicQuotes($_GET);
+    }
+    if (isset($_POST[$var])) {
+        return dispelMagicQuotes($_POST[$var]);
+    } else if (isset($_GET[$var])) {
+        return dispelMagicQuotes($_GET[$var]);
+    } else {
+        return $default;
+    }
+}
+function getPost($var=null, $default=null)
+{
+    if (is_null($var)) {
+        return dispelMagicQuotes($_POST);
+    }
+    if (isset($_POST[$var])) {
+        return dispelMagicQuotes($_POST[$var]);
+    } else {
+        return $default;
+    }
+}
+function getGet($var=null, $default=null)
+{
+    if (is_null($var)) {
+        return dispelMagicQuotes($_GET);
+    }
+    if (isset($_GET[$var])) {
+        return dispelMagicQuotes($_GET[$var]);
+    } else {
+        return $default;
+    }
+}
+
+/**
+ * Signs a value using md5 and a simple text key. In order for this
+ * function to be useful (i.e. secure) the key 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  $seed_key   (Optional) A text key to use for computing the signature.
+ * @return  string  The original value with a signature appended.
+ */
+function addSignature($val, $seed_key=null)
+{
+    if ('' == $val) {
+        App::logMsg(sprintf('Adding signature to empty string.', null), LOG_NOTICE, __FILE__, __LINE__);
+    }
+
+    if (!isset($seed_key)) {
+        $seed_key = App::getParam('signing_key');
+    }
+
+    return $val . '-' . substr(md5($val . $seed_key), 0, 18);
+}
+
+/**
+ * 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)
+{
+    return substr($signed_val, 0, strrpos($signed_val, '-'));
+}
+
+
+/**
+ * Verifies a signature appened to a value by addSignature().
+ *
+ * @access  public
+ * @param   string  $signed_val A value with appended signature.
+ * @param   string  $seed_key       (Optional) A text key to use for computing the signature.
+ * @return  bool    True if the signature matches the var.
+ */
+function verifySignature($signed_val, $seed_key=null)
+{
+    // 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 == addSignature($val, $seed_key)) {
+        // Signature verified.
+        return true;
+    } else {
+        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)
+{
+   $add_members = '/var/mailman/bin/add_members';
+    if (is_executable($add_members) && is_readable($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, $stdout), LOG_INFO, __FILE__, __LINE__);
+            return true;
+        } else {
+            App::logMsg(sprintf('Mailman add member failed for list: %s, user: %s, with message: %s', $list, $email, $stdout), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+    } else {
+        App::logMsg(sprintf('Mailman add member program not executable: %s', $add_members), LOG_ALERT, __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)
+{
+    $remove_members = '/var/mailman/bin/remove_members';
+    if (is_executable($remove_members) && is_readable($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, $stdout), LOG_INFO, __FILE__, __LINE__);
+            return true;
+        } else {
+            App::logMsg(sprintf('Mailman remove member failed for list: %s, user: %s, with message: %s', $list, $email, $stdout), LOG_WARNING, __FILE__, __LINE__);
+            return false;
+        }
+    } else {
+        App::logMsg(sprintf('Mailman remove member program not executable: %s', $remove_members), LOG_ALERT, __FILE__, __LINE__);
+        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 this 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('|\w{1,}\.\w{2,5}/|', $url)) {
+            // If we can't find a domain we assume the URL is relative.
+            $urls[$url] = true;
+        } else {
+            $urls[$url] = preg_match('/' . preg_quote(getenv('HTTP_HOST')) . '/', $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 the remote IP address, taking into consideration proxy servers.
+ *
+ * @param  bool $dolookup   If true we resolve to IP to a host name,
+ *                          if false we don't.
+ * @return string    IP address if $dolookup is false or no arg
+ *                   Hostname if $dolookup is true
+ */
+function getRemoteAddr($dolookup=false)
+{
+    $ip = getenv('HTTP_CLIENT_IP');
+    if (empty($ip) || $ip == 'unknown' || $ip == 'localhost' || $ip == '127.0.0.1') {
+        $ip = getenv('HTTP_X_FORWARDED_FOR');
+        if (empty($ip) || $ip == 'unknown' || $ip == 'localhost' || $ip == '127.0.0.1') {
+            $ip = getenv('REMOTE_ADDR');
+        }
+    }
+    return $dolookup && '' != $ip ? gethostbyaddr($ip) : $ip;
+}
+
+/**
+ * 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($ip, $networks)
+{
+    if (!is_array($networks)) {
+        $networks = array($networks);
+    }
+
+    $ip_binary = sprintf('%032b', ip2long($ip));
+    foreach ($networks as $network) {
+        if (preg_match('![\d\.]{7,15}/\d{1,2}!', $network)) {
+            // IP is in CIDR notation.
+            list($cidr_ip, $cidr_bitmask) = split('/', $network);
+            $cidr_ip_binary = sprintf('%032b', ip2long($cidr_ip));
+            if (substr($ip_binary, 0, $cidr_bitmask) === substr($cidr_ip_binary, 0, $cidr_bitmask)) {
+               // IP address is within the specified IP range.
+               return $network;
+            }
+        } else {
+            if ($ip === $network) {
+               // IP address exactly matches.
+               return $network;
+            }
+        }
+    }
+
+    return false;
+}
+
+/**
+ * Returns a fully qualified URL to the current script, including the query.
+ *
+ * @return string    a full url to the current script
+ */
+function absoluteMe()
+{
+    $protocol = ('on' == getenv('HTTPS')) ? 'https://' : 'http://';
+    return $protocol . getenv('HTTP_HOST') . getenv('REQUEST_URI');
+}
+
+/**
+ * Compares the current url with the referring url.
+ *
+ * @param  string  $compary_query  Include the query string in the comparison.
+ * @return bool    true if the current script (or specified valid_referer)
+ *                 is the referrer. false otherwise.
+ */
+function refererIsMe($exclude_query=false)
+{
+    if ($exclude_query) {
+        return (stripQuery(absoluteMe()) == stripQuery(getenv('HTTP_REFERER')));
+    } else {
+        return (absoluteMe() == getenv('HTTP_REFERER'));
+    }
+}
+
+/**
+ * Stub functions used when installation does not have
+ * GNU gettext extension installed
+ */
+if (!extension_loaded('gettext')) {
+    /**
+    * Translates text
+    *
+    * @access public
+    * @param string $text the text to be translated
+    * @return string translated text
+    */
+    function gettext($text) {
+        return $text;
+    }
+
+    /**
+    * Translates text
+    *
+    * @access public
+    * @param string $text the text to be translated
+    * @return string translated text
+    */
+    function _($text) {
+        return $text;
+    }
+
+    /**
+    * Translates text by domain
+    *
+    * @access public
+    * @param string $domain the language to translate the text into
+    * @param string $text the text to be translated
+    * @return string translated text
+    */
+    function dgettext($domain, $text) {
+        return $text;
+    }
+
+    /**
+    * Translates text by domain and category
+    *
+    * @access public
+    * @param string $domain the language to translate the text into
+    * @param string $text the text to be translated
+    * @param string $category the language dialect to use
+    * @return string translated text
+    */
+    function dcgettext($domain, $text, $category) {
+        return $text;
+    }
+
+    /**
+    * Binds the text domain
+    *
+    * @access public
+    * @param string $domain the language to translate the text into
+    * @param string
+    * @return string translated text
+    */
+    function bindtextdomain($domain, $directory) {
+        return $domain;
+    }
+
+    /**
+    * Sets the text domain
+    *
+    * @access public
+    * @param string $domain the language to translate the text into
+    * @return string translated text
+    */
+    function textdomain($domain) {
+        return $domain;
+    }
+}
+
+
+
+?>
Index: branches/2.0singleton/services/admins.php
===================================================================
--- branches/2.0singleton/services/admins.php	(revision 122)
+++ branches/2.0singleton/services/admins.php	(revision 122)
@@ -0,0 +1,518 @@
+<?php
+/**
+ * admins.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+// require_once dirname(__FILE__) . '/_config.inc.php';
+
+$auth->requireLogin();
+App::sslOn();
+
+require_once 'codebase/lib/PageNumbers.inc.php';
+require_once 'codebase/lib/SessionCache.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/RecordLock.inc.php';
+require_once 'codebase/lib/RecordVersion.inc.php';
+
+
+/******************************************************************************
+ * CONFIG
+ *****************************************************************************/
+
+// Titles and navigation header.
+$nav->addPage(_("Administrators"), $_SERVER['PHP_SELF']);
+
+// The object to validate form input.
+$fv = new FormValidator();
+
+// Instantiate a sorting object with the default sort and order. Add SQL for each column.
+$so = new SortOrder('admin_tbl.admin_id', 'DESC');
+$so->setColumn('admin_tbl.admin_id', 'admin_tbl.admin_id ASC', 'admin_tbl.admin_id DESC');
+$so->setColumn('admin_tbl.username', 'admin_tbl.username ASC', 'admin_tbl.username DESC');
+$so->setColumn('admin_tbl.userpass', 'admin_tbl.userpass ASC', 'admin_tbl.userpass DESC');
+$so->setColumn('admin_tbl.first_name', 'admin_tbl.first_name ASC', 'admin_tbl.first_name DESC');
+$so->setColumn('admin_tbl.last_name', 'admin_tbl.last_name ASC', 'admin_tbl.last_name DESC');
+$so->setColumn('admin_tbl.email', 'admin_tbl.email ASC', 'admin_tbl.email DESC');
+$so->setColumn('admin_tbl.user_type', 'admin_tbl.user_type ASC', 'admin_tbl.user_type DESC');
+$so->setColumn('admin_tbl.seconds_online', 'admin_tbl.seconds_online ASC', 'admin_tbl.seconds_online DESC');
+$so->setColumn('admin_tbl.last_login_datetime', 'admin_tbl.last_login_datetime ASC', 'admin_tbl.last_login_datetime DESC');
+$so->setColumn('admin_tbl.last_access_datetime', 'admin_tbl.last_access_datetime ASC', 'admin_tbl.last_access_datetime DESC');
+$so->setColumn('admin_tbl.last_login_ip', 'admin_tbl.last_login_ip ASC', 'admin_tbl.last_login_ip DESC');
+$so->setColumn('admin_tbl.added_by_user_id', 'admin_tbl.added_by_user_id ASC', 'admin_tbl.added_by_user_id DESC');
+$so->setColumn('admin_tbl.modified_by_user_id', 'admin_tbl.modified_by_user_id ASC', 'admin_tbl.modified_by_user_id DESC');
+$so->setColumn('admin_tbl.added_datetime', 'admin_tbl.added_datetime ASC', 'admin_tbl.added_datetime DESC');
+$so->setColumn('admin_tbl.modified_datetime', 'admin_tbl.modified_datetime ASC', 'admin_tbl.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'));
+
+// Search limiters retain their values between page requests.
+App::carryQuery('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)) {
+    // Break the cache because we are changing the list data.
+    SessionCache::breakCache($_SERVER['PHP_SELF']);
+}
+
+// What action to take.
+switch (getFormData('op')) {
+
+case 'add' :
+    // Initialize variables for the form template.
+    $frm =& addRecordForm();
+    $nav->addPage(_("Add Admin"));
+    $main_template = 'admin_form.ihtml';
+    break;
+
+case 'edit' :
+    // Initialize variables for the form template.
+    $frm =& editRecordForm(getFormData('admin_id'));
+    $nav->addPage(_("Edit Admin"));
+    $main_template = 'admin_form.ihtml';
+    break;
+
+case 'del' :
+    deleteRecord(getFormData('admin_id'));
+    if (App::validBoomerangURL('admins')) {
+        // Display boomerang page.
+        App::dieBoomerangURL('admins');
+    }
+    // Display default page.
+    App::dieURL($_SERVER['PHP_SELF']);
+    break;
+
+case 'insert' :
+    if (getFormdata('cancel', false)) {
+        if (App::validBoomerangURL('admins')) {
+            // Display boomerang page.
+            App::dieBoomerangURL('admins');
+        }
+        // Display default page.
+        App::dieURL($_SERVER['PHP_SELF']);
+    }
+    validateInput();
+    if ($fv->anyErrors()) {
+        $frm =& addRecordForm();
+        $frm = array_merge($frm, getFormData());
+        $nav->addPage(_("Add Admin"));
+        $main_template = 'admin_form.ihtml';
+    } else {
+        $admin_id = insertRecord(getFormData());
+        if (getFormdata('repeat', false)) {
+            // Display function again.
+            App::dieURL($_SERVER['PHP_SELF'] . '?op=add');
+        } else if (App::validBoomerangURL('admins')) {
+            // Display boomerang page.
+            App::dieBoomerangURL('admins');
+        }
+        // Display default page.
+        App::dieURL($_SERVER['PHP_SELF']);
+    }
+    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'));
+    }
+    if (getFormdata('cancel', false)) {
+        // Remove lock
+        $lock->select('admin_tbl', 'admin_id', getFormData('admin_id'));
+        $lock->remove();
+        if (App::validBoomerangURL('admins')) {
+            // Display boomerang page.
+            App::dieBoomerangURL('admins');
+        }
+        // Display default page.
+        App::dieURL($_SERVER['PHP_SELF']);
+    }
+    validateInput();
+    if ($fv->anyErrors()) {
+        $frm =& editRecordForm(getFormData('admin_id'));
+        $frm = array_merge($frm, getFormData());
+        $nav->addPage(_("Edit Admin"));
+        $main_template = 'admin_form.ihtml';
+    } else {
+        updateRecord(getFormData());
+        if (getFormdata('repeat', false)) {
+            // Display edit function with next available ID.
+            $qid = DB::query("SELECT admin_id FROM admin_tbl WHERE admin_id > '" . DB::escapeString(getFormData('admin_id')) . "' ORDER BY admin_id ASC LIMIT 1");
+            if (list($next_id) = mysql_fetch_row($qid)) {
+                App::dieURL($_SERVER['PHP_SELF'] . '?op=edit&admin_id=' . $next_id);
+            } 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');
+        }
+        // Display default page.
+        App::dieURL($_SERVER['PHP_SELF']);
+    }
+    break;
+
+default :
+    $list =& getRecordList();
+    $main_template = 'admin_list.ihtml';
+    break;
+}
+
+/******************************************************************************
+ * TEMPLATE INITIALIZATION
+ *****************************************************************************/
+
+include 'header.ihtml';
+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 <strong>%s</strong> already exists. Please choose another."), getFormData('username')));
+        }
+    }
+
+    if (getFormData('user_type') == 'root' && 'root' != $auth->getVal('user_type')) {
+        $fv->addError('user_type', sprintf(_("You do not have clearance to create a user with root privileges."), null));
+    }
+
+    $fv->numericRange('admin_id', -32768, 32767, _("<strong>Admin id</strong> must be a valid number between -32768 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', 6, 36, _("<strong>Passwords</strong> must be between 6 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->stringLength('email', 0, 255, _("<strong>Email</strong> must contain less than 256 characters."));
+    $fv->validateEmail('email');
+
+    $fv->isEmpty('user_type', _("<strong>User type</strong> cannot be blank."));
+    $fv->stringLength('user_type', 0, 255, _("<strong>User type</strong> has an invalid selection."));
+}
+
+function &addRecordForm()
+{
+    // Set default values for the reset of the fields.
+    $frm = array(
+        'admin_id' => '',
+        'old_username' => '',
+        'username' => '',
+        'userpass' => '',
+        'first_name' => '',
+        'last_name' => '',
+        'email' => '',
+        'user_type' => '',
+        'seconds_online' => '0',
+        'last_login_datetime' => '0000-00-00 00:00:00',
+        'last_access_datetime' => '0000-00-00 00:00:00',
+        'last_login_ip' => '0.0.0.0',
+        'added_by_user_id' => '',
+        'modified_by_user_id' => '',
+        'added_datetime' => '0000-00-00 00:00:00',
+        'modified_datetime' => '0000-00-00 00:00:00',
+        'new_op' => 'insert',
+        'submit_buttons' => array(
+            'submit' => _("Add Admin"),
+            'repeat' => _("Add &amp; repeat"),
+            'cancel' => _("Cancel"),
+        ),
+    );
+
+    return $frm;
+}
+
+function &editRecordForm($id)
+{
+    global $lock;
+
+    $lock->select('admin_tbl', 'admin_id', $id);
+    if ($lock->isLocked() && !$lock->isMine()) {
+        $lock->dieErrorPage();
+    }
+
+    // Get the information for the form.
+    $qid = DB::query("
+        SELECT *
+        FROM admin_tbl
+        WHERE admin_id = '" . 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();
+    }
+
+    // Lock this record.
+    $lock->set('admin_tbl', 'admin_id', $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' => '',
+        'user_type' => '',
+        'seconds_online' => '0',
+        'last_login_datetime' => '0000-00-00 00:00:00',
+        'last_access_datetime' => '0000-00-00 00:00:00',
+        'last_login_ip' => '0.0.0.0',
+        'added_by_user_id' => '',
+        'modified_by_user_id' => '',
+        'added_datetime' => '0000-00-00 00:00:00',
+        'modified_datetime' => '0000-00-00 00:00:00',
+        'new_op' => 'update',
+        'old_username' => $frm['username'],
+        'submit_buttons' => array(
+            'submit' => _("Save changes"),
+            'repeat' => _("Save &amp; edit next"),
+            'reset' => _("Reset"),
+            'cancel' => _("Cancel"),
+        ),
+    ), $frm, array('userpass' => '****************'));
+
+    return $frm;
+}
+
+function deleteRecord($id)
+{
+    global $auth, $lock;
+
+    $lock->select('admin_tbl', 'admin_id', $id);
+    if ($lock->isLocked() && !$lock->isMine()) {
+        $lock->dieErrorPage();
+    }
+
+    // Break the cache because we are changing the list data.
+    SessionCache::breakCache($_SERVER['PHP_SELF']);
+
+    // Get the information for this object.
+    $qid = DB::query("
+        SELECT username, user_type from admin_tbl
+        WHERE admin_id = '" . DB::escapeString($id) . "'
+    ");
+    if (! list($name, $user_type) = 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();
+    }
+
+    // Get the information for this object.
+    $qid = DB::query("SELECT COUNT(*) from admin_tbl");
+    list($num_admins) = mysql_fetch_row($qid);
+    if ('root' == $user_type && 'root' != $auth->getVal('user_type')) {
+        // Only root users can delete root users!
+        App::raiseMsg(_("You do not have clearance to delete a root administrator."), MSG_NOTICE, __FILE__, __LINE__);
+    } else if ($num_admins <= 1) {
+        // There must always be at least one admnistrator!
+        App::raiseMsg(_("You cannot delete the only administrator in the database. There must be at least one to log in and create other users."), MSG_NOTICE, __FILE__, __LINE__);
+    } else if ($auth->getVal('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 admin_tbl WHERE admin_id = '" . DB::escapeString($id) . "'");
+        App::raiseMsg(sprintf(_("The admin <strong>%s</strong> has been deleted."), $name), MSG_SUCCESS, __FILE__, __LINE__);
+    }
+
+    // Unlock record.
+    $lock->remove();
+}
+
+function insertRecord($frm)
+{
+    global $auth;
+
+    // Break the cache because we are changing the list data.
+    SessionCache::breakCache($_SERVER['PHP_SELF']);
+
+    // Insert record data.
+    DB::query("
+        INSERT INTO admin_tbl (
+            username,
+            first_name,
+            last_name,
+            email,
+            user_type,
+            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($frm['user_type']) . "',
+            '" . DB::escapeString($auth->getVal('user_id')) . "',
+            NOW()
+        )
+    ");
+    $last_insert_id = mysql_insert_id(DB::getDBH());
+
+    // Set admin password.
+    $auth->setPassword($last_insert_id, $frm['userpass']);
+
+    // Create version.
+    $version = RecordVersion::getInstance($GLOBALS['auth']);
+    $version->create('admin_tbl', 'admin_id', $last_insert_id, $frm['username']);
+
+    App::raiseMsg(sprintf(_("The Admin <strong>%s</strong> has been added."), $frm['username']), MSG_SUCCESS, __FILE__, __LINE__);
+
+    return $last_insert_id;
+}
+
+function updateRecord($frm)
+{
+    global $auth, $lock;
+
+    $lock->select('admin_tbl', 'admin_id', $frm['admin_id']);
+    if ($lock->isLocked() && !$lock->isMine()) {
+        $lock->dieErrorPage();
+    }
+
+    // Break the cache because we are changing the list data.
+    SessionCache::breakCache($_SERVER['PHP_SELF']);
+
+    // 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 admin_tbl SET
+            username = '" . DB::escapeString($frm['username']) . "',
+            first_name = '" . DB::escapeString($frm['first_name']) . "',
+            last_name = '" . DB::escapeString($frm['last_name']) . "',
+            email = '" . DB::escapeString($frm['email']) . "',
+            user_type = '" . DB::escapeString($frm['user_type']) . "',
+            modified_by_user_id = '" . DB::escapeString($auth->getVal('user_id')) . "',
+            modified_datetime = NOW()
+        WHERE admin_id = '" . DB::escapeString($frm['admin_id']) . "'
+    ");
+
+    // Create version.
+    $version = RecordVersion::getInstance($GLOBALS['auth']);
+    $version->create('admin_tbl', 'admin_id', $frm['admin_id'], $frm['username']);
+
+    App::raiseMsg(sprintf(_("The Admin <strong>%s</strong> has been updated."), $frm['username']), MSG_SUCCESS, __FILE__, __LINE__);
+
+    // Unlock record.
+    $lock->remove();
+}
+
+function &getRecordList()
+{
+    global $page;
+    global $so;
+
+    $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') . "
+                (
+                    admin_tbl.username LIKE '%" . DB::escapeString($qry_words[$i]) . "%'
+                    OR admin_tbl.first_name LIKE '%" . DB::escapeString($qry_words[$i]) . "%'
+                    OR admin_tbl.last_name LIKE '%" . DB::escapeString($qry_words[$i]) . "%'
+                    OR admin_tbl.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 admin_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
+            admin_tbl.*,
+            a1.username AS added_admin_username,
+            a2.username AS modified_admin_username
+        FROM admin_tbl
+        LEFT JOIN admin_tbl a1 ON (admin_tbl.added_by_user_id = a1.admin_id)
+        LEFT JOIN admin_tbl a2 ON (admin_tbl.modified_by_user_id = a2.admin_id)
+        $where_clause
+        " . $so->getSortOrderSQL() . "
+        " . $page->getLimitSQL() . "
+    ";
+
+    // 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 (Prefs::getValue('cache_hash', $_SERVER['PHP_SELF']) != $cache_hash) {
+        SessionCache::breakCache($_SERVER['PHP_SELF']);
+        Prefs::setValue('cache_hash', $cache_hash, $_SERVER['PHP_SELF']);
+    }
+
+    if (SessionCache::isCached($_SERVER['PHP_SELF']) && false) {
+        // Get the cached results.
+        $list = SessionCache::getCache($_SERVER['PHP_SELF']);
+    } else {
+        // If the list is not already cached, query now.
+        $qid = DB::query($sql);
+        // Fill an array with the items for this page.
+        while ($row = mysql_fetch_assoc($qid)) {
+            $list[] = $row;
+        }
+
+        if (isset($list) && !empty($list)) {
+            // Cache the results.
+            SessionCache::putCache($list, $_SERVER['PHP_SELF']);
+        }
+    }
+
+    return $list;
+}
+
+?>
Index: branches/2.0singleton/services/css.php
===================================================================
--- branches/2.0singleton/services/css.php	(revision 122)
+++ branches/2.0singleton/services/css.php	(revision 122)
@@ -0,0 +1,18 @@
+<?php
+/**
+ * css.php 
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ *
+ * 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.
+ */
+
+require_once dirname(__FILE__) . '/_config.inc.php';
+
+if (is_a($css, 'CSS')) {
+    $css->headers(getFormData('app'));
+    $css->output(getFormData('app'));
+}
+
+?>
Index: branches/2.0singleton/services/lock.php
===================================================================
--- branches/2.0singleton/services/lock.php	(revision 122)
+++ branches/2.0singleton/services/lock.php	(revision 122)
@@ -0,0 +1,51 @@
+<?php
+/**
+ * lock.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+// require_once dirname(__FILE__) . '/_config.inc.php';
+require_once 'codebase/lib/RecordLock.inc.php';
+
+$auth->requireLogin();
+App::sslOn();
+
+if (getFormData('boomerang', false)) {
+    // We remember which page we came from so we can go back there.
+    $boom_url = 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, 'RecordLock')) {
+    $lock =& RecordLock::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 <strong>%s</strong> 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->addPage(sprintf(_("Locked record: %s"), $lock->getTitle()));
+
+// Templates.
+include 'header.ihtml';
+$lock->printErrorHTML();
+include 'footer.ihtml';
+
+?>
Index: branches/2.0singleton/services/login.php
===================================================================
--- branches/2.0singleton/services/login.php	(revision 122)
+++ branches/2.0singleton/services/login.php	(revision 122)
@@ -0,0 +1,38 @@
+<?php
+/**
+ * login.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+// 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();
+
+$frm['username'] = getFormdata('username');
+$frm['password'] = getFormdata('password');
+
+if (getFormdata('username', false)) {
+    // Form has been submitted, check if the user login information is correct.
+
+    if ($auth->login($frm['username'], $frm['password'])) {
+        App::raiseMsg(_("You are now logged in."), MSG_SUCCESS, __FILE__, __LINE__);
+        App::dieBoomerangURL('login');
+        App::logMsg(sprintf('%s %s successfully logged-in.', $auth->getVal('auth_name'), $frm['username']), LOG_INFO, __FILE__, __LINE__);
+    } else {
+        App::raiseMsg(_("Login failed, please try again."), MSG_NOTICE, __FILE__, __LINE__);
+        App::logMsg(sprintf('%s %s failed login using (MD5ed) password: %s', ucfirst($auth->getVal('auth_name')), $frm['username'], md5($frm['password'])), LOG_NOTICE, __FILE__, __LINE__);
+    }
+}
+
+// Titles and navigation header.
+$nav->addPage(_("Login"));
+
+// Templates.
+include 'header.ihtml';
+include 'codebase/services/templates/login_form.ihtml';
+include 'footer.ihtml';
+
+?>
Index: branches/2.0singleton/services/logout.php
===================================================================
--- branches/2.0singleton/services/logout.php	(revision 122)
+++ branches/2.0singleton/services/logout.php	(revision 122)
@@ -0,0 +1,20 @@
+<?php
+/**
+ * logout.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+if (is_a($auth, 'Auth_SQL')) {
+    // Delete the current user's record locks.
+    require_once 'codebase/lib/RecordLock.inc.php';
+    $lock =& RecordLock::getInstance($auth);
+    $lock->removeAll($auth->getVal('user_id'));
+}
+
+// Logout.
+$auth->clearAuth();
+
+App::raiseMsg(sprintf(_("Your session has been logged-out."), null), MSG_SUCCESS, __FILE__, __LINE__);
+App::dieURL(App::getParam('redirect_home_url'));
+
+?>
Index: branches/2.0singleton/services/logs.php
===================================================================
--- branches/2.0singleton/services/logs.php	(revision 122)
+++ branches/2.0singleton/services/logs.php	(revision 122)
@@ -0,0 +1,262 @@
+<?php
+/**
+ * logs.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+// require_once dirname(__FILE__) . '/_config.inc.php';
+
+$auth->requireLogin();
+// $auth->requireAccessClearance(ZONE_ADMIN_APPLOG);
+App::sslOn();
+
+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 extentions will be displayed at the top of the log list.
+$valid_file_extensions = array('', 'txt', 'log');
+
+// Files that cannot be deleted (preg search expressions).
+$no_delete_files = '/^app_error_log$|^php_error_log$|^access_log$|^error_log$|^ssl_request_log$/';
+
+// Files that cannot be cleared (preg search expressions).
+$no_clear_files = '/__|^access_log$|^error_log$|^ssl_request_log$/';
+
+// Files that cannot be archived (preg search expressions).
+$no_archive_files = '/__|^access_log$|^error_log$|^ssl_request_log$/';
+
+// Files that cannot be archived (preg search expressions).
+$no_download_files = '/^$/';
+
+
+// Set the defaults and catch incoming settings.
+Prefs::setDefault('log_file', App::getParam('log_filename'), 'logs_module');
+Prefs::setValue('log_file', getFormData('log'), 'logs_module');
+
+// Titles and navigation header.
+$nav->addPage(sprintf(_("Viewing log: <em>%s</em>"), Prefs::getValue('log_file', 'logs_module')), '/admin/logs.php');
+
+/******************************************************************************
+ * MAIN
+ *****************************************************************************/
+
+// Allow realtime file stats.
+clearstatcache();
+
+// What action to take.
+switch (getFormData('op')) {
+case 'delete' :
+//     $auth->requireAccessClearance(ZONE_ADMIN_APPLOG_FUNC_RESET);
+    deleteLog(Prefs::getValue('log_file', 'logs_module'));
+    Prefs::setValue('log_file', App::getParam('log_filename'), 'logs_module');
+    if (App::validBoomerangURL('app_log')) {
+        // Display boomerang page.
+        App::dieBoomerangURL('app_log');
+    }
+    // Display default page.
+    App::dieURL($_SERVER['PHP_SELF']);
+    break;
+
+case 'clear' :
+//     $auth->requireAccessClearance(ZONE_ADMIN_APPLOG_FUNC_RESET);
+    clearLog(Prefs::getValue('log_file', 'logs_module'));
+    if (App::validBoomerangURL('app_log')) {
+        // Display boomerang page.
+        App::dieBoomerangURL('app_log');
+    }
+    // Display default page.
+    App::dieURL($_SERVER['PHP_SELF']);
+    break;
+
+case 'archive' :
+//     $auth->requireAccessClearance(ZONE_ADMIN_APPLOG_FUNC_RESET);
+    if (archiveLog(Prefs::getValue('log_file', 'logs_module'))) {
+        // 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 'ouput' :
+//     $main_template = 'ouput';
+//     break;
+
+case 'download' :
+    header('Content-Type: application/octet-stream');
+    header(sprintf('Content-Disposition: attachment; filename=%s.txt', Prefs::getValue('log_file', 'logs_module')));
+    printLog(Prefs::getValue('log_file', 'logs_module'));
+    die;
+    break;
+
+default :
+    $list =& getLog(Prefs::getValue('log_file', 'logs_module'), 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(Prefs::getValue('log_file', 'logs_module'));
+} else {
+    include 'codebase/services/templates/' . $main_template;
+}
+include 'footer.ihtml';
+
+
+/******************************************************************************
+ * FUNCTIONS
+ *****************************************************************************/
+
+function deleteLog($log_file)
+{
+    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)
+{
+    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)
+{
+    $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)
+{
+    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='')
+{
+    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;
+    }
+    $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);
+        }
+    }
+    return array_values($log);
+}
+
+function &getLogList()
+{
+    global $valid_file_extensions;
+
+    // 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(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;
+    } else {
+        return false;
+    }
+}
+
+
+
+?>
Index: branches/2.0singleton/services/password.php
===================================================================
--- branches/2.0singleton/services/password.php	(revision 122)
+++ branches/2.0singleton/services/password.php	(revision 122)
@@ -0,0 +1,69 @@
+<?php
+/**
+ * password.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+// require_once dirname(__FILE__) . '/_config.inc.php';
+
+$auth->requireLogin();
+
+require_once 'codebase/lib/FormValidator.inc.php';
+
+/******************************************************************************
+ * CODE CONFIG
+ *****************************************************************************/
+
+// Titles and navigation header.
+$nav->addPage(_("Change 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.
+//  $fv->isEmpty('oldpassword', _("You did not specify the <strong>old password</strong>."));
+    $fv->checkRegex('oldpassword', '/^[[:alnum:][:punct:]]{0,128}$/i', true, _("The <strong>Old password</strong> specified is not valid."));
+    if (!$fv->isEmpty('newpassword', _("You did not specify the <strong>New password</strong>."))) {
+        $fv->checkRegex('newpassword', '/^[[:alnum:][:punct:]]{6,128}$/i', true, _("The <strong>New password</strong> specified is not valid. A password must be 6 or more characters."));
+        if ($frm['newpassword'] != $frm['newpassword2'] && !$fv->isEmpty('newpassword2', _("You need to type the <strong>New password</strong> twice."))) {
+            $fv->addError('newpassword', _("The <strong>New passwords</strong> do not match."));
+            $fv->addError('newpassword2');
+        }
+    }
+
+    if (!$fv->anyErrors() && false === $auth->authenticate($auth->getVal('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->getVal('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->getVal('username')), LOG_INFO, __FILE__, __LINE__);
+        App::raiseMsg(sprintf(_("Password change successful for %s"), $auth->getVal('username')), MSG_SUCCESS, __FILE__, __LINE__);
+        App::dieBoomerangURL('admin_password');
+    }
+    break;
+}
+
+// Templates.
+include 'header.ihtml';
+include 'codebase/services/templates/password.ihtml';
+include 'footer.ihtml';
+
+?>
Index: branches/2.0singleton/services/phpinfo.php
===================================================================
--- branches/2.0singleton/services/phpinfo.php	(revision 122)
+++ branches/2.0singleton/services/phpinfo.php	(revision 122)
@@ -0,0 +1,13 @@
+<?php
+// 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: branches/2.0singleton/services/templates/admin_form.ihtml
===================================================================
--- branches/2.0singleton/services/templates/admin_form.ihtml	(revision 122)
+++ branches/2.0singleton/services/templates/admin_form.ihtml	(revision 122)
@@ -0,0 +1,57 @@
+<?php $fv->printErrorMessages(); ?>
+
+<form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
+<?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']; ?>" />
+
+<table>
+    <tr>
+        <td>&nbsp;</td>
+        <td><?php printSubmitButtons($frm['submit_buttons']); ?></td>
+    </tr>
+    <tr>
+        <td class="sc-right"><label for="username"<?php $fv->err('username', ' class="error"') ?>><?php echo _("Username"); ?></label></td>
+        <td>
+            <input type="text" class="sc-small" size="50" name="username" value="<?php echo oTxt($frm['username']); ?>" />
+        </td>
+    </tr>
+    <tr>
+        <td class="sc-right"><label for="userpass"<?php $fv->err('userpass', ' class="error"') ?>><?php echo _("Password"); ?></label></td>
+        <td>
+            <input type="password" class="sc-small" size="50" name="userpass" value="<?php echo oTxt($frm['userpass']); ?>" />
+        </td>
+    </tr>
+    <tr>
+        <td class="sc-right"><label for="first_name"<?php $fv->err('first_name', ' class="error"') ?>><?php echo _("First name"); ?></label></td>
+        <td>
+            <input type="text" class="sc-small" size="50" name="first_name" value="<?php echo oTxt($frm['first_name']); ?>" />
+        </td>
+    </tr>
+    <tr>
+        <td class="sc-right"><label for="last_name"<?php $fv->err('last_name', ' class="error"') ?>><?php echo _("Last name"); ?></label></td>
+        <td>
+            <input type="text" class="sc-small" size="50" name="last_name" value="<?php echo oTxt($frm['last_name']); ?>" />
+        </td>
+    </tr>
+    <tr>
+        <td class="sc-right"><label for="email"<?php $fv->err('email', ' class="error"') ?>><?php echo _("Email"); ?></label></td>
+        <td>
+            <input type="text" class="sc-medium" size="50" name="email" value="<?php echo oTxt($frm['email']); ?>" />
+        </td>
+    </tr>
+    <tr>
+        <td class="sc-right"><label for="user_type"<?php $fv->err('user_type', ' class="error"') ?>><?php echo _("User type"); ?></label></td>
+        <td>
+            <select name="user_type" class="sc-small">
+            <?php printSetSelectForm('admin_tbl', 'user_type', $frm['user_type'], true); ?>
+            </select>
+        </td>
+    </tr>
+    <tr>
+        <td>&nbsp;</td>
+        <td><?php printSubmitButtons($frm['submit_buttons']); ?></td>
+    </tr>
+</table>
+</form>
Index: branches/2.0singleton/services/templates/admin_list.ihtml
===================================================================
--- branches/2.0singleton/services/templates/admin_list.ihtml	(revision 122)
+++ branches/2.0singleton/services/templates/admin_list.ihtml	(revision 122)
@@ -0,0 +1,59 @@
+
+<?php $fv->printErrorMessages(); ?>
+<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
+<?php App::printHiddenSession(false); ?>
+<div id="commandbox">
+    <span class="sc-nowrap commandtext"><a href="<?php echo App::oHREF($_SERVER['PHP_SELF'] . '?op=add'); ?>"><?php echo _("Add administrator"); ?></a></span>
+    <br />
+
+    <input type="text" class="sc-small" size="20" name="search_query" value="<?php echo getFormData('search_query'); ?>" title="<?php echo oTxt(_("Fields searched: Admin id, Username, Userpass, First name, Last name, Email, User type, 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.")); ?>" />
+    <input type="submit" value="<?php echo _("Search"); ?>" />
+</div>
+
+<?php include 'list_info.ihtml'; ?>
+
+<table class="list">
+    <tr>
+        <th>&nbsp;</th>
+        <th>&nbsp;</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.user_type', _("User type"), '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 admin"), 'ASC'); ?></th>
+        <th><?php echo $so->printSortHeader('admin_tbl.modified_datetime', _("Last modified"), 'ASC'); ?></th>
+        <th>&nbsp;</th>
+    </tr>
+    <?php for ($i = 0; $i <= $page->last_item - $page->first_item && $page->total_items > 0; $i++) { ?>
+    <tr>
+        <td class="padleft sc-nowrap"><a title="<?php printf(_("Edit %s"), oTxt($list[$i]['username'])) ?>" href="<?php echo App::oHREF($_SERVER['PHP_SELF'] . '?op=edit&admin_id=' . $list[$i]['admin_id']); ?>"><img src="/admin/_widgets/edit.gif" alt="Edit" width="14" height="18" border="0"></a> &nbsp;</td>
+        <td class="padleft sc-nowrap"><a title="<?php printf(_("Versions of %s"), oTxt($list[$i]['username'])) ?>" href="<?php echo App::oHREF('/admin/versions.php?record_table=admin_tbl&record_key=admin_id&boomerang=true&record_val=' . $list[$i]['admin_id']); ?>"><img src="/admin/_widgets/subcategory.gif" alt="" width="18" height="14" border="0" /></a> &nbsp;</td>
+        <td class="sc-nowrap"><?php echo oTxt($list[$i]['admin_id'], true); ?> &nbsp;</td>
+        <td class="sc-nowrap"><?php echo oTxt($list[$i]['username'], true); ?> &nbsp;</td>
+        <td class="sc-nowrap"><?php echo oTxt($list[$i]['first_name'], true); ?> <?php echo oTxt($list[$i]['last_name'], true); ?> &nbsp;</td>
+        <td class="sc-nowrap"><?php echo oTxt($list[$i]['user_type'], true); ?> &nbsp;</td>
+        <td class="sc-nowrap"><?php echo round($list[$i]['seconds_online'] / 60); ?> &nbsp;</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 '0000-00-00 00:00:00' == $list[$i]['last_login_datetime'] ? '' : date(App::getParam('date_format'), strtotime($list[$i]['last_login_datetime'])); 
+            }
+        ?> &nbsp;</td>
+        <td class="sc-nowrap"><?php echo gethostbyaddr($list[$i]['last_login_ip']); ?> &nbsp;</td>
+        <td class="sc-nowrap"><?php echo '0000-00-00 00:00:00' == $list[$i]['added_datetime'] ? '' : date(App::getParam('date_format'), strtotime($list[$i]['added_datetime'])); ?> &nbsp;</td>
+        <td class="sc-nowrap"><?php echo oTxt($list[$i]['added_admin_username'], true); ?> &nbsp;</td>
+        <td class="sc-nowrap"><?php echo '0000-00-00 00:00:00' == $list[$i]['modified_datetime'] ? '' : date(App::getParam('date_format'), strtotime($list[$i]['modified_datetime'])); ?> &nbsp;</td>
+        <td class="padleft sc-nowrap" align="right"><a title="<?php printf(_("Delete %s"), oTxt($list[$i]['username'])) ?>" href="<?php echo App::oHREF($_SERVER['PHP_SELF'] . "?op=del&admin_id=" . $list[$i]['admin_id']); ?>" 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]['username'])) ?>')"><img src="/admin/_widgets/delete.gif" alt="Delete" width="16" height="17" 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 } ?>
+</form>
Index: branches/2.0singleton/services/templates/list_info.ihtml
===================================================================
--- branches/2.0singleton/services/templates/list_info.ihtml	(revision 122)
+++ branches/2.0singleton/services/templates/list_info.ihtml	(revision 122)
@@ -0,0 +1,10 @@
+<div>
+    <?php if ($page->total_pages > 1) { ?>
+    <span class="sc-nowrap commanditem commandtext" style="float: right;"><?php echo _("Pages:"); ?>&nbsp;<?php $page->printPageNumbers() ?></span>
+    <?php } ?>
+    <span class="sc-nowrap commanditem commandtext">
+        <?php printf(_("Listing <strong>%s-%s</strong> of <strong>%s</strong> results on <strong>%s</strong> pages"), (0==$page->total_items ? 0 : $page->first_item+1), (0==$page->total_items ? 0 : $page->last_item+1), $page->total_items, $page->total_pages) ?>
+        &nbsp;&nbsp; <a href="<?php echo App::oHREF($_SERVER['PHP_SELF'], false); ?>"><?php echo _("List all"); ?></a>
+        &nbsp;&nbsp; <?php echo _("Per page"); ?>:&nbsp;<?php $page->printPerPageLinks() ?>
+    </span>
+</div>
Index: branches/2.0singleton/services/templates/lock.ihtml
===================================================================
--- branches/2.0singleton/services/templates/lock.ihtml	(revision 122)
+++ branches/2.0singleton/services/templates/lock.ihtml	(revision 122)
@@ -0,0 +1,24 @@
+<?php
+if (!is_a($lock, 'RecordLock')) {
+    return null;
+}
+?>
+<form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
+    <?php App::printHiddenSession() ?>
+    <input type="hidden" name="lock_id" value="<?php echo $lock->getID(); ?>" />
+
+    <p><?php
+    printf(_("The record <strong>%s</strong> is currently being edited by <strong>%s</strong> (%d minutes elapsed). You cannot modify the record while it is locked by another user."),
+        $lock->getTitle(),
+        $lock->getEditor(),
+        date('i', $lock->getSecondsElapsed() + 60)
+    );
+    ?></p>
+
+    <?php if ($lock->getSecondsElapsed() > $lock->timeout) { ?>
+    <p><?php printf(_("You can force unlock the record if you believe the editing session has expired. You might want to confirm with before doing this."), $lock->getEditor()) ?></p>
+    <input type="submit" name="unlock" value="<?php echo _("Unlock"); ?>" />
+    <?php } ?>
+
+    <input type="submit" name="cancel" value="<?php echo _("Cancel"); ?>" />
+</form>
Index: branches/2.0singleton/services/templates/log_list.ihtml
===================================================================
--- branches/2.0singleton/services/templates/log_list.ihtml	(revision 122)
+++ branches/2.0singleton/services/templates/log_list.ihtml	(revision 122)
@@ -0,0 +1,50 @@
+<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
+<?php App::printHiddenSession(false); ?>
+
+<div id="commandbox">
+<?php if (is_array($logs) && !empty($logs)) { ?>
+    <table>
+    <?php
+    foreach ($logs as $l) {
+        ?>
+        <tr class="commandtext">
+            <td>
+                <?php if ($l['filename'] == Prefs::getValue('log_file', 'logs_module')) { ?>
+                    <span class="commanditem"><strong><?php echo sprintf(_("%s"), $l['filename']); ?></strong></span>
+                <?php } else { ?>
+                    <span class="commanditem"><a href="<?php echo App::oHREF('/admin/logs.php?log=' . $l['filename']); ?>"><strong><?php echo sprintf(_("%s"), $l['filename']); ?></strong></a></span>
+                <?php } ?>
+            </td>
+            <td>
+                <span class="commanditem"><em><?php echo sprintf(_("%01.1f KB, last modified %s"), ($l['filesize']/1024), date(App::getParam('date_format'), $l['modified'])); ?></em></span>
+            </td>
+            <td>
+                <?php if (!preg_match($no_download_files, $l['filename'])) { ?><span class="commanditem"><a href="<?php echo App::oHREF('/admin/logs.php?op=download&log=' . $l['filename']); ?>"><?php echo _("Download"); ?></a></span><?php } ?>
+                <?php if (!preg_match($no_archive_files, $l['filename'])) { ?><span class="commanditem"><a href="<?php echo App::oHREF('/admin/logs.php?op=archive&log=' . $l['filename']); ?>"><?php echo _("Archive"); ?></a></span><?php } ?>
+                <?php if (!preg_match($no_clear_files, $l['filename'])) { ?><span class="commanditem"><a href="<?php echo App::oHREF('/admin/logs.php?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></span><?php } ?>
+                <?php if (!preg_match($no_delete_files, $l['filename'])) { ?><span class="commanditem"><a href="<?php echo App::oHREF('/admin/logs.php?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></span><?php } ?>
+            </td>
+        </tr><?php
+    }
+    ?>
+    </table>
+<?php } ?>
+    <input type="text" class="sc-small" size="20" name="search_query" value="<?php echo oTxt(getFormData('search_query')); ?>" title="<?php echo _("All lines in currently viewed log file are searched."); ?>" />
+    <input type="checkbox" name="search_grep"<?php frmChecked(getFormData('search_grep')) ?> /><span class="commandtext"><?php echo _("Use Grep"); ?></span>
+    <input type="submit" name="list" value="<?php echo _("Search"); ?>" />
+</div>
+
+<?php include 'list_info.ihtml'; ?>
+
+<pre style="font: 9px monaco;">
+<?php
+for ($i = $page->first_item; $i <= $page->last_item && $page->total_items; $i++) {
+    echo $list[$i];
+}
+?>
+</pre>
+
+<?php if ($page->total_pages > 1) { ?>
+<div class="sc-nowrap commandtext" style="float: right;"><?php echo _("Pages:"); ?>&nbsp;<?php $page->printPageNumbers() ?></div>
+<?php } ?>
+</form>
Index: branches/2.0singleton/services/templates/login_form.ihtml
===================================================================
--- branches/2.0singleton/services/templates/login_form.ihtml	(revision 122)
+++ branches/2.0singleton/services/templates/login_form.ihtml	(revision 122)
@@ -0,0 +1,21 @@
+<form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
+<?php App::printHiddenSession() ?>
+<table>
+    <tr>
+        <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>
+        <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 oTxt($frm['password']); ?>" />
+        </td>
+    </tr>
+    <tr>
+        <td>&nbsp;</td>
+        <td><input type="submit" value="<?php echo _("Login"); ?>" /></td>
+    </tr>
+</table>
+</form>
Index: branches/2.0singleton/services/templates/password.ihtml
===================================================================
--- branches/2.0singleton/services/templates/password.ihtml	(revision 122)
+++ branches/2.0singleton/services/templates/password.ihtml	(revision 122)
@@ -0,0 +1,30 @@
+<?php $fv->printErrorMessages(); ?>
+
+<form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
+<?php App::printHiddenSession() ?>
+<input type="hidden" name="op" value="update_password" />
+<table>
+    <tr>
+        <td class="sc-right"><label for="oldpassword"<?php $fv->err('oldpassword', ' class="error"') ?>><?php echo _("Old password"); ?></label></td>
+        <td>
+            <input type="password" class="sc-medium" size="50" name="oldpassword" />
+        </td>
+    </tr>
+    <tr>
+        <td class="sc-right"><label for="newpassword"<?php $fv->err('newpassword', ' class="error"') ?>><?php echo _("New password"); ?></label></td>
+        <td>
+            <input type="password" class="sc-medium" size="50" name="newpassword" />
+        </td>
+    </tr>
+    <tr>
+        <td class="sc-right"><label for="newpassword2"<?php $fv->err('newpassword2', ' class="error"') ?>><?php echo _("New password again"); ?></label></td>
+        <td>
+            <input type="password" class="sc-medium" size="50" name="newpassword2" />
+        </td>
+    </tr>
+    <tr>
+        <td>&nbsp;</td>
+        <td><input type="submit" value="<?php echo _("Change password"); ?>" /></td>
+    </tr>
+</table>
+</form>
Index: branches/2.0singleton/services/templates/versions_diff.ihtml
===================================================================
--- branches/2.0singleton/services/templates/versions_diff.ihtml	(revision 122)
+++ branches/2.0singleton/services/templates/versions_diff.ihtml	(revision 122)
@@ -0,0 +1,28 @@
+<div id="commandbox">
+<?php if (!getFormData('current', false)) { ?>
+    <span class="sc-nowrap commandtext"><a href="<?php echo App::oHREF($_SERVER['PHP_SELF'] . '?op=restore', array('version_id', 'version_title')); ?>"><?php echo _("Restore this saved version"); ?></a></span>
+<?php } else { ?>
+    <span class="sc-nowrap commandtext">&nbsp;</span>
+<?php } ?>
+</div>
+
+<?php if (is_array($data) && !empty($data) && is_array($current) && !empty($current)) { ?>
+<table class="list" border="0" cellspacing="0" cellpadding="4">
+    <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>
+<?php
+foreach ($current as $k=>$v_c) {
+    $v_d = $data[$k];
+    $style = $v_d != $v_c ? ' style="color: #c00;"' : '';
+    ?>
+    <tr>
+        <td class="padleft sc-nowrap"<?php echo $style; ?>><label><?php echo $k; ?></label></td>
+        <td class="sc-padleft"><?php echo $v_d; ?></td>
+        <td class="sc-padleft"><?php echo $v_c; ?></td>
+    </tr>
+<?php } ?>
+</table>
+<?php } ?>
Index: branches/2.0singleton/services/templates/versions_list.ihtml
===================================================================
--- branches/2.0singleton/services/templates/versions_list.ihtml	(revision 122)
+++ branches/2.0singleton/services/templates/versions_list.ihtml	(revision 122)
@@ -0,0 +1,34 @@
+<form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
+<?php App::printHiddenSession() ?>
+<input type="submit" name="op" value="<?php echo _("Cancel"); ?>" />
+<table class="list" border="0" cellspacing="0" cellpadding="4">
+<tr>
+    <th class="sc-padleft" colspan="3">&nbsp;</th>
+    <th class="sc-padleft"><?php echo _("ID"); ?></th>
+    <th class="sc-padleft"><?php echo _("Saved"); ?></th>
+    <th class="sc-padleft"><?php echo _("By"); ?></th>
+</tr>
+<?php
+$first = true;
+foreach ($versions as $v) { ?>
+<tr>
+    <?php if ($first) { ?>
+    <td class="padleft sc-nowrap">[<a href="<?php echo App::oHREF($_SERVER['PHP_SELF'] . '?op=view&current=true&version_id=' . $v['version_id']); ?>">view</a>]</td>
+    <td class="padleft sc-nowrap" colspan="2">(<?php echo _("Current record"); ?>)</td>
+    <?php } else { ?>
+    <td class="padleft sc-nowrap">[<a href="<?php echo App::oHREF($_SERVER['PHP_SELF'] . '?op=view&version_id=' . $v['version_id']); ?>">view</a>]</td>
+    <td class="padleft sc-nowrap">[<a href="<?php echo App::oHREF($_SERVER['PHP_SELF'] . '?op=diff&version_id=' . $v['version_id']); ?>">diff</a>]</td>
+    <td class="padleft sc-nowrap">[<a href="<?php echo App::oHREF($_SERVER['PHP_SELF'] . '?op=restore&version_id=' . $v['version_id']); ?>">restore</a>]</td>
+    <?php } ?>
+    <td class="padleft sc-nowrap"><?php echo $v['version_id']; ?></td>
+    <td class="padleft sc-nowrap"><?php echo date('d M Y H:i:s', strtotime($v['version_datetime'])); ?></td>
+    <td class="padleft sc-nowrap"><?php echo $v['editor']; ?></td>
+</tr>
+<?php
+$first = false;
+}
+?>
+</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>
+<input type="submit" name="op" value="<?php echo _("Cancel"); ?>" />
+</form>
Index: branches/2.0singleton/services/templates/versions_view.ihtml
===================================================================
--- branches/2.0singleton/services/templates/versions_view.ihtml	(revision 122)
+++ branches/2.0singleton/services/templates/versions_view.ihtml	(revision 122)
@@ -0,0 +1,20 @@
+<div id="commandbox">
+<?php if (!getFormData('current', false)) { ?>
+    <span class="sc-nowrap commandtext"><a href="<?php echo App::oHREF($_SERVER['PHP_SELF'] . '?op=restore', array('version_id', 'version_title')); ?>"><?php echo _("Restore this saved version"); ?></a></span>
+<?php } else { ?>
+    <span class="sc-nowrap commandtext">&nbsp;</span>
+<?php } ?>
+</div>
+
+<?php if (is_array($data) && !empty($data)) { ?>
+<table class="list" border="0" cellspacing="0" cellpadding="4">
+<?php
+while ((list($k, $v_d) = each($data))) {
+    ?>
+    <tr>
+        <td class="sc-padleft sc-nowrap"><label><?php echo $k; ?></label></td>
+        <td class="sc-padleft"><?php echo $v_d; ?></td>
+    </tr>
+<?php } ?>
+</table>
+<?php } ?>
Index: branches/2.0singleton/services/versions.php
===================================================================
--- branches/2.0singleton/services/versions.php	(revision 122)
+++ branches/2.0singleton/services/versions.php	(revision 122)
@@ -0,0 +1,117 @@
+<?php
+/**
+ * versions.php
+ * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
+ */
+
+// require_once dirname(__FILE__) . '/_config.inc.php';
+
+$auth->requireLogin();
+App::sslOn();
+
+require_once 'codebase/lib/RecordVersion.inc.php';
+require_once 'codebase/lib/RecordLock.inc.php';
+
+/******************************************************************************
+ * CODE CONFIG
+ *****************************************************************************/
+
+$version = RecordVersion::getInstance($GLOBALS['auth']);
+
+// 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->addPage(_("Versions"), $_SERVER['PHP_SELF']);
+
+/******************************************************************************
+ * 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") :
+    App::dieBoomerangURL('versions', false);
+    break;
+
+case 'view' :
+    $data = $version->getData($version_id);
+    $versionrecord = $version->getVerson($version_id);
+    $nav->addPage(sprintf(_("View <em>%s</em> version %s (%s)"), $versionrecord['version_title'], $version_id, $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 <strong>%s</strong> is identical to the current record"), $version_id), MSG_NOTICE, __FILE__, __LINE__);
+    }
+    $nav->addPage(sprintf(_("Difference between version %s (%s) and current record."), $version_id, $versionrecord['version_datetime']));
+    $main_template = 'versions_diff.ihtml';
+    break;
+
+case 'restore' :
+    if (!isset($lock) || !is_a($lock, 'RecordLock')) {
+        $lock =& RecordLock::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 <strong>%s</strong> has been replaced with <strong>%s</strong> version <strong>%s</strong> from <strong>%s</strong>."), 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);
+    $_POST['version_title'] = $versions[0]['version_title'];
+    if (is_array($versions) && !empty($versions)) {
+        $nav->addPage(sprintf(_("%s versions of record <em>%s</em>"), sizeof($versions), $versions[0]['version_title']));
+        $main_template = 'versions_list.ihtml';
+    } else {
+        App::raiseMsg(sprintf(_("No saved versions available for this record"), null), MSG_NOTICE, __FILE__, __LINE__);
+        App::dieBoomerangURL('versions');
+    }
+}
+
+
+/******************************************************************************
+ * TEMPLATE INITIALIZATION
+ *****************************************************************************/
+
+include 'header.ihtml';
+include 'codebase/services/templates/' . $main_template;
+include 'footer.ihtml';
+
+?>
Index: branches/2.0singleton/tests/AppTest.php
===================================================================
--- branches/2.0singleton/tests/AppTest.php	(revision 122)
+++ branches/2.0singleton/tests/AppTest.php	(revision 122)
@@ -0,0 +1,181 @@
+<?php
+/**
+ * PHPUnit test case for App
+ *
+ * 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 AppTest extends PHPUnit_TestCase {
+
+    var $App;
+
+    function AppTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        $this->App =& $app;
+    }
+
+    function tearDown()
+    {
+        unset($this->App);
+    }
+
+    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->_params['test_config_value']);
+    }
+
+    function test_getParam()
+    {
+        $this->App->_params['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()
+    {
+        $qid = DB::query("SELECT 2 + 2");
+        list($result) = mysql_fetch_row($qid);
+        $this->assertEquals('4', $result);
+    }
+
+    function test_raisemsg()
+    {
+        $expected = 'My message';
+        App::raiseMsg($expected, MSG_NOTICE, __FILE__, __LINE__);
+        $msg = current($_SESSION[$this->App->app]['messages']);
+        $this->assertEquals($expected, $msg['message']);
+    }
+
+    function test_printraisedmessages()
+    {
+        ob_start();
+        App::printraisedmessages();
+        $result = ob_get_clean();
+        $this->assertContains('My message', $result, 'Raised message not found in output.');
+    }
+
+    function test_logmsg()
+    {
+        $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()
+    {
+        $_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()
+    {
+        ob_start();
+        App::printhiddensession();
+        $result = ob_get_clean();
+        $this->assertContains(session_name(), $result);
+    }
+
+//     function test_dieurl()
+//     {
+//         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: branches/2.0singleton/tests/Auth_FileTest.php
===================================================================
--- branches/2.0singleton/tests/Auth_FileTest.php	(revision 122)
+++ branches/2.0singleton/tests/Auth_FileTest.php	(revision 122)
@@ -0,0 +1,100 @@
+<?php
+/**
+ * PHPUnit test case for Auth_File
+ *
+ * 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(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']['authenticated'], 'testuser authentication not found in session.');
+    }
+
+    function test_clearauth()
+    {
+        $result = $this->Auth_File->login('testuser', 'testpass');
+        $this->Auth_File->clearauth();
+        $this->assertFalse($_SESSION['_auth']['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->clearauth();
+        $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: branches/2.0singleton/tests/Auth_SQLTest.php
===================================================================
--- branches/2.0singleton/tests/Auth_SQLTest.php	(revision 122)
+++ branches/2.0singleton/tests/Auth_SQLTest.php	(revision 122)
@@ -0,0 +1,227 @@
+<?php
+/**
+ * PHPUnit test case for Auth_SQL
+ *
+ * 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_SQLTest extends PHPUnit_TestCase {
+
+    var $Auth_SQL;
+
+    function Auth_SQLTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    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
+        ));
+
+        // Use fresh user table.
+        $this->Auth_SQL->initDB(true);
+
+        // Insert test data.
+        DB::query("
+            INSERT INTO test_user_tbl (
+                username,
+                userpass,
+                first_name,
+                last_name,
+                email,
+                user_type
+            ) VALUES (
+                'testuser',
+                md5('testpass'),
+                'John',
+                'Doe',
+                'root@localhost',
+                'admin'
+            )
+        ");
+
+    }
+
+    function tearDown()
+    {
+        unset($this->Auth_SQL);
+        DB::query("DROP TABLE IF EXISTS test_user_tbl");
+        DB::query("DROP TABLE IF EXISTS test_login_tbl");
+    }
+
+    function test_setval()
+    {
+        $this->Auth_SQL->setval('testuserkey', 'testuserval');
+        $this->assertEquals('testuserval', $_SESSION[$this->Auth_SQL->_sess]['user_data']['testuserkey']);
+    }
+
+    function test_getval()
+    {
+        $_SESSION[$this->Auth_SQL->_sess]['user_data']['testuserkey'] = 'testuserval';
+        $val = $this->Auth_SQL->getVal('testuserkey');
+        $this->assertEquals('testuserval', $val);
+    }
+
+    function test_setparam()
+    {
+        $this->Auth_SQL->setParam(array(
+            'login_url'         => 'testloginurl.php'
+        ));
+        $this->assertEquals('testloginurl.php', $this->Auth_SQL->_params['login_url']);
+    }
+
+    function test_getparam()
+    {
+        $this->Auth_SQL->_params['login_url'] = 'testloginurl.php';
+        $param = $this->Auth_SQL->getParam('login_url');
+        $this->assertEquals('testloginurl.php', $param);
+    }
+
+    function test_clearauth()
+    {
+        $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->clearauth();
+        $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->assertTrue($true, 'User login failed, but should have succeeded.');
+
+        echo "Testing wrong password...\n";
+        $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->clearauth();
+        $after_logged_in = $this->Auth_SQL->isloggedin();
+        $this->assertFalse($after_logged_in, '3. User is still logged in but should not be.');
+
+        echo "Testing wrong password...\n";
+        $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->clearauth();
+        $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()
+    {
+        $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::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('xCVcvd');
+        $this->assertRegExp('/[bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZaeiouyAEIOUY0123456789][bcdfghjklmnprstvwxzBCDFGHJKLMNPRSTVWXZ][aeiouyAEIOUY][bcdfghjklmnprstvwxz][aeiouy][0123456789]/', $result, 'Generated password does not match intended pattern');
+    }
+
+    function test_encryptpassword()
+    {
+        $result = $this->Auth_SQL->encryptpassword('123');
+        $this->assertEquals('202cb962ac59075b964b07152d234b70', $result);
+    }
+
+    function test_setpassword()
+    {
+        $this->Auth_SQL->setpassword(null, '123');
+        $qid = DB::query("
+            SELECT userpass
+            FROM test_user_tbl
+        ");
+        list($pass) = mysql_fetch_row($qid);
+        $this->assertEquals('202cb962ac59075b964b07152d234b70', $pass);
+    }
+
+    function test_resetpassword()
+    {
+        $result = $this->Auth_SQL->resetpassword(1, 'Because this is a test.');
+        $this->assertType('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);
+//     }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('Auth_SQLTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/AuthorizeNetTest.php
===================================================================
--- branches/2.0singleton/tests/AuthorizeNetTest.php	(revision 122)
+++ branches/2.0singleton/tests/AuthorizeNetTest.php	(revision 122)
@@ -0,0 +1,104 @@
+<?php
+/**
+ * PHPUnit test case for AuthorizeNet
+ *
+ * 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 AuthorizeNetTest extends PHPUnit_TestCase {
+
+    var $AuthorizeNet;
+
+    function AuthorizeNetTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    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',
+        ));
+    }
+
+    function tearDown()
+    {
+        unset($this->AuthorizeNet);
+    }
+
+    function test_setparam()
+    {
+        $this->AuthorizeNet->setparam(array('x_Login' => 'myaccount2'));
+        $this->assertEquals('myaccount2', $this->AuthorizeNet->_params['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);
+//     }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('AuthorizeNetTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/CSSTest.php
===================================================================
--- branches/2.0singleton/tests/CSSTest.php	(revision 122)
+++ branches/2.0singleton/tests/CSSTest.php	(revision 122)
@@ -0,0 +1,71 @@
+<?php
+/**
+ * PHPUnit test case for CSS
+ *
+ * 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 CSSTest extends PHPUnit_TestCase {
+
+    var $CSS;
+    var $test_css_file;
+
+    function CSSTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('CSSTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/DBSessionHandlerTest.php
===================================================================
--- branches/2.0singleton/tests/DBSessionHandlerTest.php	(revision 122)
+++ branches/2.0singleton/tests/DBSessionHandlerTest.php	(revision 122)
@@ -0,0 +1,78 @@
+<?php
+/**
+ * PHPUnit test case for DBSessionHandler
+ *
+ * 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 DBSessionHandlerTest extends PHPUnit_TestCase {
+
+    var $DBSessionHandler;
+
+    function DBSessionHandlerTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/DBSessionHandler.inc.php';
+        session_write_close();
+        $this->DBSessionHandler = new DBSessionHandler($app->db);
+        $this->DBSessionHandler = new DBSessionHandler(null, array(
+            'db_server' => $app->getParam('db_server'),
+            'db_name' => $app->getParam('db_name'),
+            'db_user' => $app->getParam('db_user'),
+            'db_pass' => $app->getParam('db_pass'),
+            'db_always_debug' => $app->getParam('db_always_debug'),
+            'db_debug' => $app->getParam('db_debug'),
+            'db_die_on_failure' => $app->getParam('db_die_on_failure'),
+        ));
+    }
+
+    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()
+    {
+        $this->DBSessionHandler->DBsessionwrite(session_id(), serialize($_SESSION));
+    }
+
+    function test_DBsessiondestroy()
+    {
+        $this->DBSessionHandler->DBsessiondestroy(session_id());
+    }
+
+    function test_DBsessiongarbage()
+    {
+        $this->DBSessionHandler->DBsessiongarbage(0);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('DBSessionHandlerTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/EmailTest.php
===================================================================
--- branches/2.0singleton/tests/EmailTest.php	(revision 122)
+++ branches/2.0singleton/tests/EmailTest.php	(revision 122)
@@ -0,0 +1,107 @@
+<?php
+/**
+ * PHPUnit test case for Email
+ *
+ * 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
+ */
+require_once 'PHPUnit.php';
+class EmailTest extends PHPUnit_TestCase {
+
+    var $Email;
+
+    function EmailTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    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()
+//     {
+//         $result   = $this->Email->send(PARAM);
+//         $expected = EXPECTED_VAL;
+//         $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(41, $gc, 'Unexpected good count.');
+        $this->assertEquals(47, $bc, 'Unexpected bad count.');
+        echo "Good addresses: $gc\n";
+        echo "Bad addresses: $bc\n";
+//         echo join("\n", $bad);
+//         echo "\n";
+    }
+
+}
+// Running the test.
+$suite  = new PHPUnit_TestSuite('EmailTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/FormValidatorTest.php
===================================================================
--- branches/2.0singleton/tests/FormValidatorTest.php	(revision 122)
+++ branches/2.0singleton/tests/FormValidatorTest.php	(revision 122)
@@ -0,0 +1,154 @@
+<?php
+/**
+ * PHPUnit test case for FormValidator
+ *
+ * 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 FormValidatorTest extends PHPUnit_TestCase {
+
+    var $FormValidator;
+
+    function FormValidatorTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    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->geterrorlist();
+    }
+
+    function test_adderror()
+    {
+        $this->FormValidator->addError('name', _("This is an error."), MSG_ERR, __FILE__, __LINE__);
+    }
+
+    function test_anyerrors()
+    {
+        $result = $this->FormValidator->anyerrors();
+        $this->assertFalse($result);
+    }
+
+    function test_reseterrorlist()
+    {
+        $this->FormValidator->addError('name', _("This is an error."), MSG_ERR, __FILE__, __LINE__);
+        $this->FormValidator->reseterrorlist();
+        $this->assertFalse($this->FormValidator->anyerrors());
+    }
+
+    function test_printerrormessages()
+    {
+        $this->FormValidator->addError('name', _("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('name', _("This is an error."), MSG_ERR, __FILE__, __LINE__);
+        ob_start();
+        $this->FormValidator->err('name', 'printthis');
+        $result = ob_get_clean();
+        $this->assertContains('printthis', $result);
+    }
+
+    function test_notempty()
+    {
+        $this->FormValidator->isEmpty('name', _("Error message"));
+    }
+
+    function test_isempty()
+    {
+        $result = $this->FormValidator->isempty('name', _("Error message"));
+    }
+
+    function test_isstring()
+    {
+        $result = $this->FormValidator->isstring('name', _("Error message"));
+    }
+
+    function test_isnumber()
+    {
+        $result = $this->FormValidator->isnumber('name', _("Error message"));
+    }
+
+    function test_isinteger()
+    {
+        $result = $this->FormValidator->isinteger('name', _("Error message"));
+    }
+
+    function test_isfloat()
+    {
+        $result = $this->FormValidator->isfloat('name', _("Error message"));
+    }
+
+    function test_isarray()
+    {
+        $result = $this->FormValidator->isarray('name', _("Error message"));
+    }
+
+    function test_checkregex()
+    {
+        $result = $this->FormValidator->checkregex('name', '/\w/', true, _("Error message"));
+    }
+
+    function test_stringlength()
+    {
+        $this->FormValidator->stringLength('name', 0, 255, _("Error message"));
+    }
+
+    function test_numericrange()
+    {
+        $this->FormValidator->numericrange('name', 0, 255, _("Error message"));
+    }
+
+    function test_validateemail()
+    {
+        $this->FormValidator->validateemail('name');
+    }
+
+    function test_validatephone()
+    {
+        $this->FormValidator->validatephone('name');
+    }
+
+    function test_validatestrdate()
+    {
+        $this->FormValidator->validatestrdate('name');
+    }
+
+    function test_validateccnumber()
+    {
+        $this->FormValidator->validateccnumber('name');
+    }
+
+    function test_validatefile()
+    {
+        $this->FormValidator->validatefile('name');
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('FormValidatorTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/Google_APITest.php
===================================================================
--- branches/2.0singleton/tests/Google_APITest.php	(revision 122)
+++ branches/2.0singleton/tests/Google_APITest.php	(revision 122)
@@ -0,0 +1,76 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for GoogleAPI
+ *
+ * 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 GoogleAPITest extends PHPUnit_TestCase {
+
+    var $GoogleAPI;
+
+    function GoogleAPITest($name)
+    {
+        $this->PHPUnit_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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('GoogleAPITest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/HierarchyTest.php
===================================================================
--- branches/2.0singleton/tests/HierarchyTest.php	(revision 122)
+++ branches/2.0singleton/tests/HierarchyTest.php	(revision 122)
@@ -0,0 +1,160 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for Hierarchy
+ *
+ * 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 HierarchyTest extends PHPUnit_TestCase {
+
+    var $Hierarchy;
+
+    function HierarchyTest($name)
+    {
+        $this->PHPUnit_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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('HierarchyTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/ImageThumbTest.php
===================================================================
--- branches/2.0singleton/tests/ImageThumbTest.php	(revision 122)
+++ branches/2.0singleton/tests/ImageThumbTest.php	(revision 122)
@@ -0,0 +1,104 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for ImageThumb
+ *
+ * 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 ImageThumbTest extends PHPUnit_TestCase {
+
+    var $ImageThumb;
+
+    function ImageThumbTest($name)
+    {
+        $this->PHPUnit_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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('ImageThumbTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/MCVETest.php
===================================================================
--- branches/2.0singleton/tests/MCVETest.php	(revision 122)
+++ branches/2.0singleton/tests/MCVETest.php	(revision 122)
@@ -0,0 +1,76 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for MCVE
+ *
+ * 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 MCVETest extends PHPUnit_TestCase {
+
+    var $MCVE;
+
+    function MCVETest($name)
+    {
+        $this->PHPUnit_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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('MCVETest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/NavTest.php
===================================================================
--- branches/2.0singleton/tests/NavTest.php	(revision 122)
+++ branches/2.0singleton/tests/NavTest.php	(revision 122)
@@ -0,0 +1,118 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for Nav
+ *
+ * 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 NavTest extends PHPUnit_TestCase {
+
+    var $Nav;
+
+    function NavTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Nav.inc.php';
+        $this->Nav =& new Nav(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->Nav);
+    }
+
+    function test_addpage()
+    {
+        $result = $this->Nav->addpage(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setfeature()
+    {
+        $result = $this->Nav->setfeature(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_clearpath()
+    {
+        $result = $this->Nav->clearpath(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getfeature()
+    {
+        $result = $this->Nav->getfeature(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_gettitle()
+    {
+        $result = $this->Nav->gettitle(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printtitle()
+    {
+        $result = $this->Nav->printtitle(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getpath()
+    {
+        $result = $this->Nav->getpath(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printpath()
+    {
+        $result = $this->Nav->printpath(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getbreadcrumbs()
+    {
+        $result = $this->Nav->getbreadcrumbs(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_printbreadcrumbs()
+    {
+        $result = $this->Nav->printbreadcrumbs(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_currentpage()
+    {
+        $result = $this->Nav->currentpage(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('NavTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/PEditTest.php
===================================================================
--- branches/2.0singleton/tests/PEditTest.php	(revision 122)
+++ branches/2.0singleton/tests/PEditTest.php	(revision 122)
@@ -0,0 +1,139 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for PEdit
+ *
+ * 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
+ */
+require_once 'PHPUnit.php';
+class PEditTest extends PHPUnit_TestCase {
+
+    var $PEdit;
+
+    function PEditTest($name)
+    {
+        $this->PHPUnit_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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('PEditTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/PageNumbersTest.php
===================================================================
--- branches/2.0singleton/tests/PageNumbersTest.php	(revision 122)
+++ branches/2.0singleton/tests/PageNumbersTest.php	(revision 122)
@@ -0,0 +1,118 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for PageNumbers
+ *
+ * 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 PageNumbersTest extends PHPUnit_TestCase {
+
+    var $PageNumbers;
+
+    function PageNumbersTest($name)
+    {
+        $this->PHPUnit_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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('PageNumbersTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/PageSequenceTest.php
===================================================================
--- branches/2.0singleton/tests/PageSequenceTest.php	(revision 122)
+++ branches/2.0singleton/tests/PageSequenceTest.php	(revision 122)
@@ -0,0 +1,167 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for PageSequence
+ *
+ * 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 PageSequenceTest extends PHPUnit_TestCase {
+
+    var $PageSequence;
+
+    function PageSequenceTest($name)
+    {
+        $this->PHPUnit_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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('PageSequenceTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/PayPalTest.php
===================================================================
--- branches/2.0singleton/tests/PayPalTest.php	(revision 122)
+++ branches/2.0singleton/tests/PayPalTest.php	(revision 122)
@@ -0,0 +1,119 @@
+<?php
+/**
+ * PHPUnit test case for PayPal
+ *
+ * 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 PayPalTest extends PHPUnit_TestCase {
+
+    var $PayPal;
+
+    function PayPalTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    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));
+    }
+
+    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));
+    }
+
+    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();
+//     }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('PayPalTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/PrefsTest.php
===================================================================
--- branches/2.0singleton/tests/PrefsTest.php	(revision 122)
+++ branches/2.0singleton/tests/PrefsTest.php	(revision 122)
@@ -0,0 +1,97 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for Prefs
+ *
+ * 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 PrefsTest extends PHPUnit_TestCase {
+
+    var $Prefs;
+
+    function PrefsTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Prefs.inc.php';
+        $this->Prefs =& new Prefs(PARAM);
+    }
+
+    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_getvalue()
+    {
+        $result = $this->Prefs->getvalue(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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('PrefsTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/RecordLockTest.php
===================================================================
--- branches/2.0singleton/tests/RecordLockTest.php	(revision 122)
+++ branches/2.0singleton/tests/RecordLockTest.php	(revision 122)
@@ -0,0 +1,192 @@
+<?php
+/**
+ * PHPUnit test case for RecordLock
+ *
+ * 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 RecordLockTest extends PHPUnit_TestCase {
+
+    var $RecordLock;
+    var $Auth_SQL;
+
+    function RecordLockTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/RecordLock.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::query("
+            INSERT INTO test_user_tbl (
+                username,
+                userpass,
+                first_name,
+                last_name,
+                email,
+                user_type
+            ) VALUES (
+                'testuser',
+                md5('testpass'),
+                'John',
+                'Doe',
+                'john@example.com',
+                'admin'
+            )
+        ");
+        $this->Auth_SQL->login('testuser', 'testpass');
+
+        $this->RecordLock =& RecordLock::getInstance($this->Auth_SQL);
+        $this->RecordLock->setParam(array('db_table' => 'test_lock_tbl'));
+
+        // Use fresh lock table.
+        $this->RecordLock->initDB(true);
+    }
+
+    function tearDown()
+    {
+        unset($this->RecordLock);
+        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 = RecordLock::getinstance($this->Auth_SQL);
+        $this->assertTrue($result && is_a($result, 'RecordLock'));
+    }
+
+    function test_setparam()
+    {
+        $this->RecordLock->setparam(array('var'=>'val'));
+    }
+
+    function test_getparam()
+    {
+        $this->RecordLock->getparam('var');
+    }
+
+    function test_select()
+    {
+        $this->RecordLock->set('test_user_tbl', 'user_id', '1');
+        $result = $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+        $this->assertTrue($result);
+    }
+
+    function test_islocked()
+    {
+        $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+
+        $this->RecordLock->set('test_user_tbl', 'user_id', '1');
+        $result = $this->RecordLock->islocked();
+        $this->assertTrue($result, 'Lock was not set.');
+
+        $this->RecordLock->remove();
+        $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+        $result = $this->RecordLock->islocked();
+        $this->assertFalse($result, 'Lock was not removed.');
+    }
+
+    function test_ismine()
+    {
+        $this->RecordLock->set('test_user_tbl', 'user_id', '1');
+        $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+        $result = $this->RecordLock->ismine();
+        $this->assertTrue($result);
+    }
+
+    function test_set()
+    {
+        $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+        $this->RecordLock->remove();
+        $this->RecordLock->set('test_user_tbl', 'user_id', '1');
+        $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+        $result = $this->RecordLock->islocked();
+        $this->assertTrue($result);
+    }
+
+    function test_remove()
+    {
+        $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+        $this->RecordLock->set('test_user_tbl', 'user_id', '1');
+        $this->RecordLock->remove();
+        $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+        $result = $this->RecordLock->islocked();
+        $this->assertFalse($result);
+    }
+
+    function test_removeall()
+    {
+        $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+        $this->RecordLock->set('test_user_tbl', 'user_id', '1');
+        $this->RecordLock->removeall();
+        $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+        $result = $this->RecordLock->islocked();
+        $this->assertFalse($result);
+    }
+
+    function test__auto_timeout()
+    {
+        $this->RecordLock->_auto_timeout();
+    }
+
+    function test_getid()
+    {
+        $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+        $this->RecordLock->set('test_user_tbl', 'user_id', '1');
+        $result = $this->RecordLock->getid();
+        $this->assertTrue('' != $result);
+        $this->assertTrue(is_numeric($result));
+    }
+
+    function test_gettitle()
+    {
+        $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+        $this->RecordLock->set('test_user_tbl', 'user_id', '1', 'userlock');
+        $result = $this->RecordLock->gettitle();
+        $this->assertEquals('userlock', $result);
+    }
+
+    function test_geteditor()
+    {
+        $this->RecordLock->select('test_user_tbl', 'user_id', '1');
+        $this->RecordLock->set('test_user_tbl', 'user_id', '1', 'userlock');
+        $result = $this->RecordLock->geteditor();
+        $this->assertEquals('testuser', $result);
+    }
+
+    function test_getsecondselapsed()
+    {
+        $result = $this->RecordLock->getsecondselapsed();
+        $this->assertType('integer', $result);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('RecordLockTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/RecordVersionTest.php
===================================================================
--- branches/2.0singleton/tests/RecordVersionTest.php	(revision 122)
+++ branches/2.0singleton/tests/RecordVersionTest.php	(revision 122)
@@ -0,0 +1,174 @@
+<?php
+/**
+ * PHPUnit test case for RecordVersion
+ *
+ * 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 RecordVersionTest extends PHPUnit_TestCase {
+
+    var $RecordVersion;
+    var $Auth_SQL;
+
+    function RecordVersionTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/RecordVersion.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::query("
+            INSERT INTO test_user_tbl (
+                username,
+                userpass,
+                first_name,
+                last_name,
+                email,
+                user_type
+            ) VALUES (
+                'testuser',
+                md5('testpass'),
+                'John',
+                'Doe',
+                'john@example.com',
+                'admin'
+            )
+        ");
+
+        $this->Auth_SQL->login('testuser', 'testpass');
+
+        $this->RecordVersion =& RecordVersion::getInstance($this->Auth_SQL);
+        $this->RecordVersion->setParam(array('db_table' => 'test_version_tbl'));
+
+        // Use fresh version table.
+        $this->RecordVersion->initDB(true);
+    }
+
+    function tearDown()
+    {
+        unset($this->RecordVersion);
+        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 = RecordVersion::getinstance($this->Auth_SQL);
+        $this->assertTrue($result && is_a($result, 'RecordVersion'));
+    }
+
+    function test_setparam()
+    {
+        $this->RecordVersion->setparam(array('var'=>'val'));
+    }
+
+    function test_getparam()
+    {
+        $this->RecordVersion->getparam('var');
+    }
+
+    function test_create()
+    {
+        $result = $this->RecordVersion->create('test_user_tbl', 'user_id', '1', 'Test User', 'First version of user');
+        $this->assertTrue($result);
+    }
+
+    function test_restore()
+    {
+        $current = $this->RecordVersion->getCurrent('test_user_tbl', 'user_id', '1');
+        $version_id = $this->RecordVersion->create('test_user_tbl', 'user_id', '1', 'Test User', 'First version of user');
+        $versioned = $this->RecordVersion->getData($version_id);
+        $result = $this->RecordVersion->restore($version_id);
+        $restored = $this->RecordVersion->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->RecordVersion->create('test_user_tbl', 'user_id', '1', 'Test 1', '1 version of user');
+        $this->RecordVersion->create('test_user_tbl', 'user_id', '1', 'Test 2', '2 version of user');
+        $this->RecordVersion->create('test_user_tbl', 'user_id', '1', 'Test 3', '3 version of user');
+
+        $a = $this->RecordVersion->getList('test_user_tbl', 'user_id', '1');
+
+        $this->RecordVersion->deleteold('test_user_tbl', 'user_id', '1');
+        $b = $this->RecordVersion->getList('test_user_tbl', 'user_id', '1');
+
+        $this->RecordVersion->setparam(array('max_qty'=>2));
+        $this->RecordVersion->setparam(array('min_qty'=>1));
+        $this->RecordVersion->setparam(array('min_days'=>0));
+        $this->RecordVersion->deleteold('test_user_tbl', 'user_id', '1');
+        $c = $this->RecordVersion->getList('test_user_tbl', 'user_id', '1');
+
+        $this->RecordVersion->setparam(array('max_qty'=>0));
+        $this->RecordVersion->setparam(array('min_qty'=>0));
+        $this->RecordVersion->setparam(array('min_days'=>0));
+        $this->RecordVersion->deleteold('test_user_tbl', 'user_id', '1');
+        $d = $this->RecordVersion->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->RecordVersion->create('test_user_tbl', 'user_id', '1', 'Test 1', '1 version of user');
+        $this->RecordVersion->create('test_user_tbl', 'user_id', '1', 'Test 2', '2 version of user');
+        $this->RecordVersion->create('test_user_tbl', 'user_id', '1', 'Test 3', '3 version of user');
+        $result = $this->RecordVersion->getList('test_user_tbl', 'user_id', '1');
+        $this->assertTrue(is_array($result) && !empty($result));
+    }
+
+    function test_getverson()
+    {
+        $version_id = $this->RecordVersion->create('test_user_tbl', 'user_id', '1', 'Test User', 'First version of user');
+        $result = $this->RecordVersion->getverson($version_id);
+        $this->assertTrue(is_array($result) && !empty($result));
+    }
+
+    function test_getdata()
+    {
+        $version_id = $this->RecordVersion->create('test_user_tbl', 'user_id', '1', 'Test User', 'First version of user');
+        $result = $this->RecordVersion->getdata($version_id);
+        $this->assertTrue(is_array($result) && !empty($result));
+    }
+
+    function test_getcurrent()
+    {
+        $result = $this->RecordVersion->getCurrent('test_user_tbl', 'user_id', '1');
+        $this->assertTrue(is_array($result) && !empty($result));
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('RecordVersionTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/ScriptTimerTest.php
===================================================================
--- branches/2.0singleton/tests/ScriptTimerTest.php	(revision 122)
+++ branches/2.0singleton/tests/ScriptTimerTest.php	(revision 122)
@@ -0,0 +1,62 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for ScriptTimer
+ *
+ * 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 ScriptTimerTest extends PHPUnit_TestCase {
+
+    var $ScriptTimer;
+
+    function ScriptTimerTest($name)
+    {
+        $this->PHPUnit_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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('ScriptTimerTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/SessionCacheTest.php
===================================================================
--- branches/2.0singleton/tests/SessionCacheTest.php	(revision 122)
+++ branches/2.0singleton/tests/SessionCacheTest.php	(revision 122)
@@ -0,0 +1,90 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for SessionCache
+ *
+ * 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 SessionCacheTest extends PHPUnit_TestCase {
+
+    var $SessionCache;
+
+    function SessionCacheTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/SessionCache.inc.php';
+        $this->SessionCache =& new SessionCache(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->SessionCache);
+    }
+
+    function test_getinstance()
+    {
+        $result = $this->SessionCache->getinstance(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_setparam()
+    {
+        $result = $this->SessionCache->setparam(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getparam()
+    {
+        $result = $this->SessionCache->getparam(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_putcache()
+    {
+        $result = $this->SessionCache->putcache(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_getcache()
+    {
+        $result = $this->SessionCache->getcache(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_iscached()
+    {
+        $result = $this->SessionCache->iscached(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+    function test_breakcache()
+    {
+        $result = $this->SessionCache->breakcache(PARAM);
+        $expected = EXPECTED_VAL;
+        $this->assertEquals($expected, $result);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('SessionCacheTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/SortOrderTest.php
===================================================================
--- branches/2.0singleton/tests/SortOrderTest.php	(revision 122)
+++ branches/2.0singleton/tests/SortOrderTest.php	(revision 122)
@@ -0,0 +1,76 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for SortOrder
+ *
+ * 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 SortOrderTest extends PHPUnit_TestCase {
+
+    var $SortOrder;
+
+    function SortOrderTest($name)
+    {
+        $this->PHPUnit_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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('SortOrderTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/SpellCheckTest.php
===================================================================
--- branches/2.0singleton/tests/SpellCheckTest.php	(revision 122)
+++ branches/2.0singleton/tests/SpellCheckTest.php	(revision 122)
@@ -0,0 +1,139 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for SpellCheck
+ *
+ * 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 SpellCheckTest extends PHPUnit_TestCase {
+
+    var $SpellCheck;
+
+    function SpellCheckTest($name)
+    {
+        $this->PHPUnit_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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('SpellCheckTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/TemplateGlueTest.php
===================================================================
--- branches/2.0singleton/tests/TemplateGlueTest.php	(revision 122)
+++ branches/2.0singleton/tests/TemplateGlueTest.php	(revision 122)
@@ -0,0 +1,41 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for TemplateGlue
+ *
+ * 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 TemplateGlueTest extends PHPUnit_TestCase {
+
+    var $TemplateGlue;
+
+    function TemplateGlueTest($name)
+    {
+        $this->PHPUnit_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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('TemplateGlueTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/UploadTest.php
===================================================================
--- branches/2.0singleton/tests/UploadTest.php	(revision 122)
+++ branches/2.0singleton/tests/UploadTest.php	(revision 122)
@@ -0,0 +1,111 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for Upload
+ *
+ * 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 UploadTest extends PHPUnit_TestCase {
+
+    var $Upload;
+
+    function UploadTest($name)
+    {
+        $this->PHPUnit_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);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('UploadTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/UtilitiesTest.php
===================================================================
--- branches/2.0singleton/tests/UtilitiesTest.php	(revision 122)
+++ branches/2.0singleton/tests/UtilitiesTest.php	(revision 122)
@@ -0,0 +1,41 @@
+<?php
+/// FIXME: Tests not implemented.
+return;
+
+/**
+ * PHPUnit test case for Utilities
+ *
+ * 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 UtilitiesTest extends PHPUnit_TestCase {
+
+    var $Utilities;
+
+    function UtilitiesTest($name)
+    {
+        $this->PHPUnit_TestCase($name);
+    }
+
+    function setUp()
+    {
+        require dirname(__FILE__) . '/_config.inc.php';
+        require_once '../lib/Utilities.inc.php';
+        $this->Utilities =& new Utilities(PARAM);
+    }
+
+    function tearDown()
+    {
+        unset($this->Utilities);
+    }
+
+}
+// Running the test.
+$suite = new PHPUnit_TestSuite('UtilitiesTest');
+$result = PHPUnit::run($suite);
+echo $result->toString();
+?>
Index: branches/2.0singleton/tests/_config.inc.php
===================================================================
--- branches/2.0singleton/tests/_config.inc.php	(revision 122)
+++ branches/2.0singleton/tests/_config.inc.php	(revision 122)
@@ -0,0 +1,34 @@
+<?php
+require_once '../lib/App.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' => 'localhost',
+    'db_name' => 'test',
+    'db_user' => 'root',
+    '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: branches/2.0singleton/tests/_email_test_addresses.txt
===================================================================
--- branches/2.0singleton/tests/_email_test_addresses.txt	(revision 122)
+++ branches/2.0singleton/tests/_email_test_addresses.txt	(revision 122)
@@ -0,0 +1,88 @@
+ 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 >
+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: branches/2.0singleton/tests/_skel.php
===================================================================
--- branches/2.0singleton/tests/_skel.php	(revision 122)
+++ branches/2.0singleton/tests/_skel.php	(revision 122)
@@ -0,0 +1,59 @@
+#!/usr/local/bin/php
+<?php
+/**
+ * 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: branches/2.0singleton/tests/_test_htpasswd
===================================================================
--- branches/2.0singleton/tests/_test_htpasswd	(revision 122)
+++ branches/2.0singleton/tests/_test_htpasswd	(revision 122)
@@ -0,0 +1,1 @@
+testuser:nOZSYqRQlsv1o
Index: branches/2.0singleton/tests/run_tests.sh
===================================================================
--- branches/2.0singleton/tests/run_tests.sh	(revision 122)
+++ branches/2.0singleton/tests/run_tests.sh	(revision 122)
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+# Be in the directory with all the tests.
+cd `dirname $0`;
+
+# Run tests sequentially.
+for foo in *Test.php; 
+do
+    /usr/local/bin/php $foo;
+done;
