Index: trunk/lib/CSS.inc.php
===================================================================
--- trunk/lib/CSS.inc.php	(revision 89)
+++ trunk/lib/CSS.inc.php	(revision 92)
@@ -14,5 +14,5 @@
     var $_css_files = array('default' => array());
 
-    // Cache style sheets?
+    // CSS object parameters.
     var $_params = array(
         'cache_css' => true,
Index: trunk/lib/Hierarchy.inc.php
===================================================================
--- trunk/lib/Hierarchy.inc.php	(revision 89)
+++ trunk/lib/Hierarchy.inc.php	(revision 92)
@@ -677,6 +677,4 @@
      * @return array  Details of from the node table of all nodes below the
      *                specified node: (type, id, title, indent level, selected status)
-     *
-     * FIX ME: $include_curr doesn't work because the current node will break with an infinite loop.
      */
     function &getNodeList($preselected=null, $child_type=null, $child_id=null, $type_constraint=null, $include_curr=false, $order='', $_indent=0, $_return_flag=true)
@@ -709,18 +707,17 @@
             for ($i=0; $i<$num_children; $i++) {
 
-                $row = $my_children[$i];
                 // Preventing circular references.
-                if ($row['child_type'] == $child_type && $row['child_id'] == $child_id) {
-                    App::logMsg(sprintf(_("Circular reference detected: %s has itself as a parent."), $this->toStringID($row['child_type'], $row['child_id'])), LOG_ERR, __FILE__, __LINE__);
+                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;
                 }
-                $row['indent'] = $_indent;
-                if (in_array($this->toStringID($row['child_type'], $row['child_id']), $preselected)) {
-                    $row['selected'] = true;
+
+                $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[] = $row;
-                unset($row);
-
-                // This is so we test if each node is a string only once. We store the result in the is_a_leaf array statically.
+                $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']);
Index: trunk/lib/PEdit.inc.php
===================================================================
--- trunk/lib/PEdit.inc.php	(revision 89)
+++ trunk/lib/PEdit.inc.php	(revision 92)
@@ -5,6 +5,6 @@
  * 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. On save, a mass search and replace is complated to inssert the new
- * data into the old file. A copy of the previous version is saved with the unix
+ * 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.
  *
@@ -12,70 +12,132 @@
  * and call printing/editing functions where you want data and forms to
  * show up. Below is an example of use:
- *
- *  // Initialize.
- *  require_once 'codebase/lib/PEdit.inc.php';
- *  $p = new PEdit($auth->hasClearance('pedit'));
- *
- *  $title = <<<P_E_D_I_T_title
- *  Using Burritos to Improve Student Learnin'
- *  P_E_D_I_T_title;
- *  $p->set($title, 'title', 'textbox');
- *
- *  // Begin content. Include a header or something right here.
- *  $p->printContent('title');
- *
- *  // Prints beginning form tags and special hidden forms. (Only happens if page is NOT a archived version.)
- *  $p->formBegin();
- *
- *  // Print editing form elements. (Only happens if op == Edit.)
- *  $p->printForm('title');
- *
- *  // Print versions list. (Only happens if op == Versions.)
- *  $p->printVersions();
- *
- *  // Prints ending form tags and command buttons.(Only happens if page is NOT a archived version.)
- *  $p->formEnd();
- *
+ 
+ // 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 1.1
+ * @version 2.0
  */
-class PEdit
-{
-    var $_data = array();       // Array to store editable data.
-    var $_filename = '';        // Full file path to current file.
-    var $_authorized = false;   // User is authenticated to see extended functions.
-    var $versions_min_qty = 20; // Keep at least this many versions of each file.
-    var $versions_min_days = 10;// Keep ALL versions within this many days, even if MORE than versions_min_qty.
-
-    // Tags that are not stripped from the POSTed data.
-    var $allowed_tags = '<p><h1><h2><h3><h4><h5><h6><div><br><hr><a><img><i><em><b><strong><small><blockquote><ul><ol><li><dl><dt><dd><map><area><table><tr><td>';
-
-
-    /**
-     * Constructs a new PEdit object. Initializes what file is being operated on
-     * (SCRIPT_FILENAME) and what that operation is. The two
+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, '' - default). They die redirect so you see
+     * 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($authorized=false)
-    {
-        if ($authorized === true) {
+    function PEdit($params)
+    {
+        $this->setParam($params);
+        
+        if ($this->getParam('authorized') === true) {
             $this->_authorized = true;
         }
-
-        $this->_filename = $_SERVER['SCRIPT_FILENAME'];
-        if (empty($this->_filename)) {
-            App::logMsg(sprintf('PEdit error: server variable SCRIPT_FILENAME must be defined.', null), LOG_WARNING, __FILE__, __LINE__);
-            die;
-        }
-
+        
+        // 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' :
@@ -85,8 +147,20 @@
             break;
         case 'Restore' :
-            if ($this->_restoreVersion(getFormData('with_file'))) {
+            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();
         }
     }
@@ -97,165 +171,31 @@
      * @access public
      *
-     * @param string $content         The variable containing the text to store.
-     * @param string $name            The name of the variable.
-     * @param string $type            The type of form element to use.
-     * @param optional int $form_size The size of the form element.
-     */
-    function set($content, $name, $type, $form_size=null)
-    {
-        $this->_data[$name] = array(
-            'type' => $type,
-            'content' => $content,
-            'form_size' => $form_size
-        );
-    }
-
-    /**
-     * Stores a checkbox 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 string $name               The name of the variable.
-     * @param string $corresponding_text The text that corresponds to this checkbox.
-     */
-    function setCheckbox($content, $name, $corresponding_text)
-    {
-        if (isset($content) && isset($name) && isset($corresponding_text)) {
-            $this->_data[$name] = array(
-                'type' => 'checkbox',
-                'content' => $content,
-                'corresponding_text' => $corresponding_text
-            );
-        }
-    }
-
-    /**
-     * Tests if we are should display page contents.
-     *
-     * @access public
-     *
-     * @return bool        true if we are displaying page normally, false if editing page, or viewing versions.
-     */
-    function displayMode()
-    {
+     * @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 true;
-        }
-    }
-
-
-    /**
-     * 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.
-            $versions = $this->_getVersions();
-            ?><h1><?php printf(_("%s saved versions of %s"), sizeof($versions), basename($this->_filename)); ?></h1><?php
-            if (is_array($versions) && !empty($versions)) {
-                ?><table border="0" cellspacing="0" cellpadding="4"><?php
-                foreach ($versions as $v) {
-                    ?>
-                    <tr>
-                    <td nowrap="nowrap"><p><?php echo date('r', $v['unixtime']); ?></p></td>
-                    <td nowrap="nowrap"><p>&nbsp;&nbsp;&nbsp;<?php printf(_("%s bytes"), $v['filesize']); ?></p></td>
-                    <td nowrap="nowrap"><p>&nbsp;&nbsp;&nbsp;[<a href="<?php echo App::oHREF(dirname($_SERVER['PHP_SELF']) . (preg_match('!/$!', dirname($_SERVER['PHP_SELF'])) ? '' : '/') . $v['filename']); ?>" target="_blank"><?php echo _("view"); ?></a>]</p></td>
-                    <td nowrap="nowrap"><p>&nbsp;&nbsp;&nbsp;[<a href="<?php echo App::oHREF($_SERVER['PHP_SELF'] . '?op=Restore&with_file=' . $v['filename'] . '&file_hash=' . md5('frog_guts' . $this->_filename)); ?>"><?php echo _("restore"); ?></a>]</p></td>
-                    </tr>
-                    <?php
-                }
-                ?></table><?php
-            ?><div class="help"><?php printf(_("When there are more than %s versions, those over %s days old are deleted."), $this->versions_min_qty, $this->versions_min_days); ?></div><?php
-            }
-        }
-    }
-
-    /**
-     * 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 getContent($name, $preserve_html=true)
-    {
-        if ($this->op != 'Edit' && $this->op != 'Versions' && isset($this->_data[$name]['content'])) {
-            // Print content.
-            switch ($this->_data[$name]['type']) {
-            case 'checkbox' :
-                return 'off' == $this->_data[$name]['content'] ? '' : oTxt($this->_data[$name]['corresponding_text'], $preserve_html);
-            default :
-                return trim(oTxt($this->_data[$name]['content'], $preserve_html));
-            }
-        }
-    }
-
-    /**
-     * Prints the contents of a data variable. The variable must first be 'set'.
-     *
-     * @access public
-     *
-     * @param string $name      The name of the variable to print.
-     */
-    function printContent($name, $preserve_html=true)
-    {
-        echo $this->getContent($name, $preserve_html);
-    }
-
-    /**
-     * Prints the HTML forms corresponding to pedit variables. Each variable
-     * must first be 'set'.
-     *
-     * @access public
-     *
-     * @param string $name      The name of the variable.
-     */
-    function printForm($name)
-    {
-        if ($this->_authorized && $this->op == 'Edit' && isset($this->_data[$name]['type'])) {
-            // Print edit form.
-            switch ($this->_data[$name]['type']) {
-            case 'textbox' :
-                $rows = is_numeric($this->_data[$name]['form_size']) ? $this->_data[$name]['form_size'] : 50;
-                echo '<input class="monospaced" style="width: 100%;" type="text" name="data[' . $name . ']" value="' . oTxt($this->_data[$name]['content']) . '" size="' . $rows . '" /><br />';
-                break;
-            case 'textarea' :
-                $rows = is_numeric($this->_data[$name]['form_size']) ? $this->_data[$name]['form_size'] : 20;
-                echo '<textarea class="monospaced" style="width: 100%;" rows="' . $rows . '" cols="60" name="data[' . $name . ']">' . oTxt($this->_data[$name]['content']) . '</textarea><br />';
-                break;
-            case 'checkbox' :
-                $checked = ('off' == $this->_data[$name]['content']) ? '' : 'checked="checked" ';
-                // Note hidden form below. If the checkbox is not checked, the hidden variable will send "off" as the variable,
-                // otherwise if the form is not posted for that checkbox the update will not occur.
-                ?>
-                <table border="0" cellspacing="0" cellpadding="2"><tr>
-                <td><input type="hidden" name="data[<?php echo $name; ?>]" value="off" /><input type="checkbox" name="data[<?php echo $name; ?>]" <?php echo $checked; ?>/></td>
-                <td><?php echo oTxt($this->_data[$name]['corresponding_text']); ?></td>
-                </tr></table>
-                <?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) && !empty($this->_data)) {
-            foreach ($this->_data as $name=>$d) {
-                $this->printForm($name);
-            }
+            return $this->_data[$name]['content'];
+        } else {
+            return '';
         }
     }
@@ -268,12 +208,22 @@
     function formBegin()
     {
-        if (!$this->_authorized || preg_match('/\.php__/', $this->_filename)) {
-            // Don't show form elements for versioned documents.
+        if (!$this->_authorized) {
             return false;
         }
         ?>
-        <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
-        <input type="hidden" name="filename" value="<?php echo $this->_filename; ?>" />
-        <input type="hidden" name="file_hash" value="<?php echo md5('frog_guts' . $this->_filename); ?>" />
+        <style type="text/css" media="screen">
+        /* <![CDATA[ */
+            #sc-pedit-form input[type="text"], textarea {
+                width: 100%;
+            }
+            #sc-pedit-form textarea {
+                height: 30em;
+            }
+        /* ]]> */
+        </style>
+        
+        <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();
@@ -281,10 +231,55 @@
         case 'Edit' :
             ?>
-            <div class="pedit_buttons">
-            <input type="submit" name="op" value="<?php echo _("Save"); ?>" />
-            <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" />
+            <div class="sc-pedit-buttons">
+                <input type="submit" name="op" value="<?php echo _("Save"); ?>" />
+                <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" />
             </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) {
+            $type = (isset($this->_data[$name]['type'])) ? $this->_data[$name]['type'] : $type;
+            // Print edit form.
+            switch ($type) {
+            case 'text' :
+            default :
+                ?><br /><label><?php echo ucfirst(str_replace('_', ' ', $name)); ?> <input type="text" name="_pedit_data[<?php echo $name; ?>]" id="sc-pedit-<?php echo $name; ?>" value="<?php echo oTxt($this->_data[$name]['content']); ?>" /></label><?php
+                break;
+            case 'textarea' :
+                ?><br /><label><?php echo ucfirst(str_replace('_', ' ', $name)); ?> <textarea name="_pedit_data[<?php echo $name; ?>]" id="sc-pedit-<?php echo $name; ?>"><?php echo oTxt($this->_data[$name]['content']); ?></textarea></label><?php
+                break;
+            }
         }
     }
@@ -298,5 +293,5 @@
     function formEnd()
     {
-        if (!$this->_authorized || preg_match('/\.php__/', $this->_filename)) {
+        if (!$this->_authorized || preg_match('/\.php__/', $_SERVER['PHP_SELF'])) {
             // Don't show form elements for versioned documents.
             return false;
@@ -305,7 +300,7 @@
         case 'Edit' :
             ?>
-            <div class="pedit_buttons">
-            <input type="submit" name="op" value="<?php echo _("Save"); ?>" />
-            <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" />
+            <div class="sc-pedit-buttons">
+                <input type="submit" name="op" value="<?php echo _("Save"); ?>" />
+                <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" />
             </div>
             </form>
@@ -314,15 +309,23 @@
         case 'Versions' :
             ?>
-            <div class="pedit_buttons">
-            <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" />
+            <div class="sc-pedit-buttons">
+                <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" />
             </div>
             </form>
             <?php
             break;
+        case 'View' :
+            ?>
+            <div class="sc-pedit-buttons">
+                <input type="submit" name="op" value="<?php echo _("Restore"); ?>" />
+                <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" />
+            </div>
+            <?php
+            break;
         default :
             ?>
-            <div class="pedit_buttons">
-            <input type="submit" name="op" value="<?php echo _("Edit"); ?>" />
-            <input type="submit" name="op" value="<?php echo _("Versions"); ?>" />
+            <div class="sc-pedit-buttons">
+                <input type="submit" name="op" value="<?php echo _("Edit"); ?>" />
+                <input type="submit" name="op" value="<?php echo _("Versions"); ?>" />
             </div>
             </form>
@@ -332,9 +335,102 @@
 
     /**
+     * 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"><?php
+                foreach ($version_files as $v) {
+                    ?>
+                    <tr>
+                        <td><?php echo date('r', $v['unixtime']); ?></td>
+                        <td><?php printf(_("%s bytes"), $v['filesize']); ?></td>
+                        <td><a href="<?php echo App::oHREF($_SERVER['PHP_SELF'] . '?op=View&version=' . $v['unixtime'] . '&file_hash=' . $this->_fileHash()); ?>"><?php echo _("View"); ?></a></td>
+                        <td><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><?php
+            ?><div class="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_qty')); ?></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.
      */
@@ -344,60 +440,83 @@
             return false;
         }
-        if (md5('frog_guts' . $this->_filename) != getFormData('file_hash')) {
+        if ($this->_fileHash() != getFormData('file_hash')) {
             // Posted data is NOT for this file!
-            trigger_error('PEdit error: file_hash does not match current file.', E_USER_WARNING);
-            return false;
-        }
-        $whole_file = file_get_contents($this->_filename);
-        $new_data = getFormData('data');
-        $search = array();
-        $replace = array();
+            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)) {
-            foreach ($new_data as $name=>$d) {
-                if ('' != $d && !preg_match('/P_E_D_I_T_/', $d)) {
-                    // If the new posted data is not empty, and the heredoc identifer is not in it.
-                    $block_identifier = 'P_E_D_I_T_' . preg_quote($name);
-                    if (substr_count($whole_file, $block_identifier) != 2) {
-                        App::logMsg(sprintf('PEdit error: more than one %s heredoc identifier found.', $block_identifier), LOG_NOTICE, __FILE__, __LINE__);
-                        return false;
-                    }
-                    // Strip extra linefeeds. For some reason POST data has double EOL chars when writing to a unix file.
-                    $d = preg_replace("/[\n\r]{2,}/", "\n", $d);
-                    $search[] = "/$block_identifier.*?\n$block_identifier;/s";
-                    $replace[] = "$block_identifier\n" . strip_tags($d, $this->allowed_tags) . "\n$block_identifier;";
+            // 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;
                 }
             }
-
-            // Search and replace all blocks.
-            $whole_file = preg_replace($search, $replace, $whole_file);
-
-            // Probably unnecessary, testing if resulting file is empty or smaller than input.
-            if (strlen($whole_file) < strlen(strip_tags(serialize($new_data), $this->allowed_tags))) {
-                App::logMsg(sprintf('PEdit error: saved file size (%s) is less than input data size (%s).', strlen($whole_file), strlen(strip_tags(serialize($new_data), $this->allowed_tags))), LOG_NOTICE, __FILE__, __LINE__);
-                return false;
-            }
-
-            // Make certain a version is created.
-            if (! $this->_createVersion()) {
-                App::logMsg(sprintf('PEdit error: failed creating new version of file.', null), LOG_NOTICE, __FILE__, __LINE__);
-                return false;
-            }
-
-            // Open file for writing and truncate to zero length.
-            if (is_writable($this->_filename) && $fp = fopen($this->_filename, 'w')) {
-                if (flock($fp, LOCK_EX)) {
-                    fwrite($fp, $whole_file, strlen($whole_file));
-                    flock($fp, LOCK_UN);
-                } else {
-                    App::logMsg(sprintf('PEdit error: could not lock file for writing: %s', $this->_filename), LOG_NOTICE, __FILE__, __LINE__);
+            
+            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;
                 }
-                fclose($fp);
-                // Success!
-                return true;
+            }
+        }
+
+        // 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('PEdit error: could not open file for writing: %s', $this->_filename), LOG_NOTICE, __FILE__, __LINE__);
+                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;
         }
     }
@@ -405,45 +524,62 @@
     /**
      * Makes a copy of the current file with the unix timestamp appended to the
-     * filename. Deletes old versions based on threshold of age and qty.
+     * filename.
      *
      * @access private
-     *
-     * @param optional boolean $do_cleanup    Set to false to turn off the
-     *                                        cleanup routine.
-     *
-     * @return bool  False if unauthorized or on failure. True on success.
-     */
-    function _createVersion($do_cleanup=true)
+     * @return bool  False on failure. True on success.
+     */
+    function _createVersion()
     {
         if (!$this->_authorized) {
             return false;
         }
-        if (md5('frog_guts' . $this->_filename) != getFormData('file_hash')) {
+        if ($this->_fileHash() != getFormData('file_hash')) {
             // Posted data is NOT for this file!
-            trigger_error('PEdit error: file_hash does not match current file.', E_USER_WARNING);
-            return false;
-        }
-        $versions = $this->_getVersions();
-
-        // Clean up old versions.
-        if (is_array($versions) && sizeof($versions) > $this->versions_min_qty && $do_cleanup) {
+            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($versions);
+            $oldest = array_pop($version_files);
             // Loop while minimum X qty && minimum X days worth but never more than 100 qty.
-            while ((sizeof($versions) > $this->versions_min_qty
-            && $oldest['unixtime'] < mktime(date('H'),date('i'),date('s'),date('m'),date('d')-$this->versions_min_days,date('Y')))
-            || sizeof($versions) > 100) {
-                unlink(dirname($this->_filename) . '/' . $oldest['filename']);
-                $oldest = array_pop($versions);
-            }
-        }
-
-        // Do the actual copy. File naming scheme must be consistent!
-        if (!copy($this->_filename, $this->_filename . '__' . time() . '.php')) {
-            trigger_error('PEdit error: failed copying new version. Check file and directory permissions.', E_USER_WARNING);
-            return false;
-        }
-
-        return true;
+            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);
+            }
+        }
     }
 
@@ -453,25 +589,24 @@
      *
      * @access private
-     *
      * @return array  Array of versions.
      */
     function _getVersions()
     {
-        $versions = array();
-        $dir_handle = opendir(dirname($this->_filename));
-        while ($dir_handle && ($version = readdir($dir_handle)) !== false) {
-            if (!preg_match('/^\./', $version) && !is_dir($version) && preg_match('/^' . preg_quote(basename($this->_filename)) . '__.*/', $version)) {
-                preg_match('/.+__(\d+)\.php/', $version, $time);
-                $versions[] = array(
-                    'filename' => $version,
+        $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->_filename) . '/' . $version)
+                    'filesize' => filesize(dirname($this->_data_file) . '/' . $version_file)
                 );
             }
         }
 
-        if (is_array($versions) && !empty($versions)) {
-            array_multisort($versions, SORT_DESC);
-            return $versions;
+        if (is_array($version_files) && !empty($version_files)) {
+            array_multisort($version_files, SORT_DESC);
+            return $version_files;
         } else {
             return array();
@@ -484,34 +619,37 @@
      *
      * @access private
-     *
-     * @param string $with_file    Filename of archived version to restore.
-     *
-     * @return bool  False if unauthorized. True on success.
-     */
-    function _restoreVersion($with_file)
+     * @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;
         }
-
-        if (is_writable($this->_filename)) {
-            // Make certain a version is created.
-            if (! $this->_createVersion(false)) {
-                App::logMsg(sprintf('PEdit error: failed creating new version of file.', null), LOG_NOTICE, __FILE__, __LINE__);
-                return false;
-            }
-
-            // Do the actual copy.
-            if (!copy(dirname($this->_filename) . '/' . $with_file, $this->_filename)) {
-                App::logMsg(sprintf('PEdit error: failed copying old version: %s', $with_file), LOG_NOTICE, __FILE__, __LINE__);
-                return false;
-            }
-
-            // Success!
-            return true;
-        } else {
-            App::logMsg(sprintf('PEdit error: could not open file for writing: %s', $this->_filename), LOG_NOTICE, __FILE__, __LINE__);
-            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;
     }
 
Index: trunk/lib/ScriptTimer.inc.php
===================================================================
--- trunk/lib/ScriptTimer.inc.php	(revision 89)
+++ trunk/lib/ScriptTimer.inc.php	(revision 92)
@@ -1,30 +1,31 @@
 <?php
 /**
- * ScriptTimer.inc.php
+ * ScriptTimer.inc.php 
  * Code by Strangecode :: www.strangecode.com :: This document contains copyrighted information
  */
-
+ 
 class ScriptTimer {
 
-    var $time_format = '%.10f';
-    var $_timing_start_times;
-    var $_timing_stop_times;
-
-    function start($name='default')
-    {
+    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')
-    {
+	}
+	
+	function stop($name='default')
+	{
         $this->_timing_stop_times[$name] = explode(' ', microtime());
-    }
-
-    function getTime($name='default')
-    {
+        $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());
@@ -32,11 +33,28 @@
             $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);
 
-        return sprintf($this->time_format, $current);
-    }
+        echo '<pre>';
+	    foreach ($this->_timing_cumulative_times as $name => $time) {
+	        printf("\n%-{$name_len}s $this->time_format", $name, $time);
+	    }
+        echo '</pre>';
+	}
 }
 
