* Copyright 2001-2010 Strangecode, LLC * * This file is part of The Strangecode Codebase. * * The Strangecode Codebase is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your option) * any later version. * * The Strangecode Codebase is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * The Strangecode Codebase. If not, see . */ /** * PEdit.inc.php * * PEdit provides a mechanism to store text in php variables * which will be printed to the client browser under normal * circumstances, but an authenticated user can 'edit' the document-- * data stored in vars will be shown in html form elements to be edited * and saved. Posted data is stored in XML format in a specified data dir. * A copy of the previous version is saved with the unix * timestamp as part of the filename. This allows reverting to previous versions. * * To use, include this file, initialize variables, * and call printing/editing functions where you want data and forms to * show up. * * @author Quinn Comendant * @concept Beau Smith * @version 2.0 * * Example of use: // Initialize PEdit object. require_once 'codebase/lib/PEdit.inc.php'; $pedit = new PEdit(array( 'data_dir' => COMMON_BASE . '/html/_pedit_data', 'authorized' => true, )); // Setup content data types. $pedit->set('title'); $pedit->set('content', array('type' => 'textarea')); // After setting all parameters and data, load the data. $pedit->start(); // Print content. echo $pedit->get('title'); echo $pedit->get('content'); // Print additional PEdit functionality. $pedit->formBegin(); $pedit->printAllForms(); $pedit->printVersions(); $pedit->formEnd(); */ class PEdit { // PEdit object parameters. 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/Serializer.php'; $this->xml_serializer =& new XML_Serializer(array( XML_SERIALIZER_OPTION_INDENT => '', XML_SERIALIZER_OPTION_LINEBREAKS => '', XML_SERIALIZER_OPTION_RETURN_RESULT => true, XML_SERIALIZER_OPTION_TYPEHINTS => true, )); require_once 'XML/Unserializer.php'; $this->xml_unserializer =& new XML_Unserializer(array( XML_UNSERIALIZER_OPTION_COMPLEXTYPE => 'array', )); } /** * Set (or overwrite existing) parameters by passing an array of new parameters. * * @access public * @param array $params Array of parameters (key => val pairs). */ function setParam($params) { $app =& App::getInstance(); if (isset($params) && is_array($params)) { // Merge new parameters with old overriding only those passed. $this->_params = array_merge($this->_params, $params); } else { $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_WARNING, __FILE__, __LINE__); } } /** * Return the value of a parameter, if it exists. * * @access public * @param string $param Which parameter to return. * @return mixed Configured parameter value. */ function getParam($param) { $app =& App::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; } } /* * Load the pedit data and run automatic functions. * * @access public * @author Quinn Comendant * @since 12 Apr 2006 12:43:47 */ function start($initialize_data_file=false) { $app =& App::getInstance(); if (!is_dir($this->getParam('data_dir'))) { trigger_error(sprintf('PEdit data directory not found: %s', $this->getParam('data_dir')), E_USER_WARNING); } // The location of the data file. (i.e.: "COMMON_DIR/html/_pedit_data/news/index.xml") $this->_data_file = sprintf('%s%s.xml', $this->getParam('data_dir'), $_SERVER['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 only a preview 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()) { $app =& App::getInstance(); $name = preg_replace('/\s/', '_', $name); if (!isset($this->_data[$name])) { $this->_data[$name] = array_merge(array('content' => ''), $options); } else { $app->logMsg(sprintf('Duplicate set data: %s', $name), LOG_NOTICE, __FILE__, __LINE__); } } /** * Returns the contents of a data variable. The variable must first be 'set'. * * @access public * @param string $name The name of the variable to return. * @return string The trimmed content of the named data. */ 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
HTML tag, as well as hidden input forms. * * @return bool False if unauthorized or current page is a version. */ function formBegin() { $app =& App::getInstance(); if (!$this->_authorized || empty($this->_data)) { return false; } ?> printHiddenSession(); switch ($this->op) { case 'Edit' : ?>
" title="" accesskey="" /> " title="" accesskey="" />
_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) { ?>
_data[$name]['type'])) ? $this->_data[$name]['type'] : $type; // Print edit form. switch ($type) { case 'text' : default : ?>
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' : ?>
" title="" accesskey="" /> " title="" accesskey="" />
" title="" accesskey="" />
" title="" accesskey="" /> " title="" accesskey="" /> " title="" accesskey="" />
" title="" accesskey="" /> " title="" accesskey="" />
_authorized && $this->op == 'Versions') { // Print versions and commands to view/restore. $version_files = $this->_getVersions(); ?>

getParam('date_format'), $v['unixtime']); ?> getParam('time_format'), $v['unixtime']); ?>
getParam('versions_min_qty'), $this->getParam('versions_min_days')); ?>
* @since 12 Apr 2006 10:52:35 */ function _fileHash() { $app =& App::getInstance(); return md5($app->getParam('signing_key') . $_SERVER['PHP_SELF']); } /* * Load the XML data file into $this->_data. * * @access public * @return bool false on error * @author Quinn Comendant * @since 11 Apr 2006 20:36:26 */ function _loadDataFile() { $app =& App::getInstance(); if (!file_exists($this->_data_file)) { if (!$this->_initializeDataFile()) { $app->logMsg(sprintf('Initializing content file failed: %s', $this->_data_file), LOG_WARNING, __FILE__, __LINE__); return false; } } $xml_file_contents = file_get_contents($this->_data_file); $status = $this->xml_unserializer->unserialize($xml_file_contents, false); if (PEAR::isError($status)) { $app->logMsg(sprintf('XML_Unserialize error: %s', $status->getMessage()), LOG_WARNING, __FILE__, __LINE__); return false; } $xml_file_data = $this->xml_unserializer->getUnserializedData(); // Only load data specified with set(), even though there may be more in the xml file. foreach ($this->_data as $name => $initial_data) { if (isset($xml_file_data[$name])) { $this->_data[$name] = array_merge($initial_data, $xml_file_data[$name]); } else { $this->_data[$name] = $initial_data; } } $this->_data_loaded = true; return true; } /* * Start a new data file. * * @access public * @return The success value of both xml_serializer->serialize() and _filePutContents() * @author Quinn Comendant * @since 11 Apr 2006 20:53:42 */ function _initializeDataFile() { $app =& App::getInstance(); $app->logMsg(sprintf('Initializing data file: %s', $this->_data_file), LOG_INFO, __FILE__, __LINE__); $xml_file_contents = $this->xml_serializer->serialize($this->_data); return $this->_filePutContents($this->_data_file, $xml_file_contents); } /** * Saves the POSTed data by overwriting the pedit variables in the * current file. * * @access private * @return bool False if unauthorized or on failure. True on success. */ function _writeData() { $app =& App::getInstance(); if (!$this->_authorized) { return false; } if ($this->_fileHash() != getFormData('file_hash')) { // Posted data is NOT for this file! $app->logMsg(sprintf('File_hash does not match current file.', null), LOG_WARNING, __FILE__, __LINE__); return false; } // Scrub incoming data. Escape tags? $new_data = getFormData('_pedit_data'); if (is_array($new_data) && !empty($new_data)) { // Make certain a version is created. $this->_deleteOldVersions(); if (!$this->_createVersion()) { $app->logMsg(sprintf('Failed creating new version of file.', null), LOG_NOTICE, __FILE__, __LINE__); return false; } // Collect posted data that is already specified in _data (by set()). foreach ($new_data as $name => $content) { if (isset($this->_data[$name])) { $this->_data[$name]['content'] = $content; } } if (is_array($this->_data) && !empty($this->_data)) { $xml_file_contents = $this->xml_serializer->serialize($this->_data); return $this->_filePutContents($this->_data_file, $xml_file_contents); } } } /* * Writes content to the specified file. * * @access public * @param string $filename Path to file. * @param string $content Data to write into file. * @return bool Success or failure. * @author Quinn Comendant * @since 11 Apr 2006 22:48:30 */ function _filePutContents($filename, $content) { $app =& App::getInstance(); // Ensure requested filename is within the pedit data dir. if (mb_strpos($filename, $this->getParam('data_dir')) === false) { $app->logMsg(sprintf('Failed writing file outside pedit _data_dir: %s', $filename), LOG_ERR, __FILE__, __LINE__); return false; } // Recursively create directories. $subdirs = preg_split('!/!', str_replace($this->getParam('data_dir'), '', dirname($filename)), -1, PREG_SPLIT_NO_EMPTY); // Start with the pedit _data_dir base. $curr_path = $this->getParam('data_dir'); while (!empty($subdirs)) { $curr_path .= '/' . array_shift($subdirs); if (!is_dir($curr_path)) { if (!mkdir($curr_path)) { $app->logMsg(sprintf('Failed mkdir: %s', $curr_path), LOG_ERR, __FILE__, __LINE__); return false; } } } // Open file for writing and truncate to zero length. if ($fp = fopen($filename, 'w')) { if (flock($fp, LOCK_EX)) { fwrite($fp, $content, mb_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() { $app =& App::getInstance(); if (!$this->_authorized) { return false; } if ($this->_fileHash() != getFormData('file_hash')) { // Posted data is NOT for this file! $app->logMsg(sprintf('File_hash does not match current file.', null), LOG_ERR, __FILE__, __LINE__); return false; } // Ensure current data file exists. if (!file_exists($this->_data_file)) { $app->logMsg(sprintf('Data file does not yet exist: %s', $this->_data_file), LOG_NOTICE, __FILE__, __LINE__); return false; } // Do the actual copy. File naming scheme must be consistent! // filename.php.xml becomes filename.php__1124124128.xml $version_file = sprintf('%s__%s.xml', preg_replace('/\.xml$/', '', $this->_data_file), time()); if (!copy($this->_data_file, $version_file)) { $app->logMsg(sprintf('Failed copying new version: %s -> %s', $this->_data_file, $version_file), LOG_ERR, __FILE__, __LINE__); return false; } return true; } /* * Delete all versions older than versions_min_days if there are more than versions_min_qty or 100. * * @access public * @return bool False on failure. True on success. * @author Quinn Comendant * @since 12 Apr 2006 11:08:11 */ function _deleteOldVersions() { $app =& App::getInstance(); $version_files = $this->_getVersions(); if (is_array($version_files) && sizeof($version_files) > $this->getParam('versions_min_qty')) { // Pop oldest ones off bottom of array. $oldest = array_pop($version_files); // Loop while minimum X qty && minimum X days worth but never more than 100 qty. while ((sizeof($version_files) > $this->getParam('versions_min_qty') && $oldest['unixtime'] < mktime(date('H'), date('i'), date('s'), date('m'), date('d') - $this->getParam('versions_min_days'), date('Y'))) || sizeof($version_files) > 100) { $del_file = dirname($this->_data_file) . '/' . $oldest['filename']; if (!unlink($del_file)) { $app->logMsg(sprintf('Failed deleting version: %s', $del_file), LOG_ERR, __FILE__, __LINE__); } $oldest = array_pop($version_files); } } } /** * Returns an array of all archived versions of the current file, * sorted with newest versions at the top of the array. * * @access private * @return array Array of versions. */ 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) { $app =& App::getInstance(); if (!$this->_authorized) { return false; } // The file to restore. $version_file = sprintf('%s__%s.xml', preg_replace('/\.xml$/', '', $this->_data_file), $version); // Ensure specified version exists. if (!file_exists($version_file)) { $app->logMsg(sprintf('Cannot restore non-existent file: %s', $version_file), LOG_NOTICE, __FILE__, __LINE__); return false; } // Make certain a version is created. if (!$this->_createVersion()) { $app->logMsg(sprintf('Failed creating new version of file.', null), LOG_ERR, __FILE__, __LINE__); return false; } // Do the actual copy. if (!copy($version_file, $this->_data_file)) { $app->logMsg(sprintf('Failed copying old version: %s -> %s', $version_file, $this->_data_file), LOG_ERR, __FILE__, __LINE__); return false; } // Success! $app->raiseMsg(sprintf(_("Page has been restored to version %s."), $version), MSG_SUCCESS, __FILE__, __LINE__); return true; } } // End class. ?>