hasClearance('pedit')); * * $title = <<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(); * * @author Quinn Comendant * @concept Beau Smith * @version 1.1 */ 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 = '



    1. '; /** * Constructs a new PEdit object. Initializes what file is being operated on * (SCRIPT_FILENAME) 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 * the page you just modified. * * @access public * * @param optional array $params A hash containing connection parameters. */ function PEdit($authorized=false) { if ($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; } $this->op = getFormData('op'); switch ($this->op) { case 'Save' : if ($this->_writeData()) { App::dieURL($_SERVER['PHP_SELF']); } break; case 'Restore' : if ($this->_restoreVersion(getFormData('with_file'))) { App::dieURL($_SERVER['PHP_SELF']); } break; } } /** * 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 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() { 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(); ?>

      _filename)); ?>

         

         []

         []

      versions_min_qty, $this->versions_min_days); ?>
      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 '
      '; break; case 'textarea' : $rows = is_numeric($this->_data[$name]['form_size']) ? $this->_data[$name]['form_size'] : 20; echo '
      '; 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. ?>
      /> _data[$name]['corresponding_text']); ?>
      _authorized && $this->op == 'Edit' && is_array($this->_data) && !empty($this->_data)) { foreach ($this->_data as $name=>$d) { $this->printForm($name); } } } /** * Prints the beginning
      HTML tag, as well as hidden input forms. * * @return bool False if unauthorized or current page is a version. */ function formBegin() { if (!$this->_authorized || preg_match('/\.php__/', $this->_filename)) { // Don't show form elements for versioned documents. return false; } ?> op) { case 'Edit' : ?>
      " /> " />
      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 || preg_match('/\.php__/', $this->_filename)) { // Don't show form elements for versioned documents. return false; } switch ($this->op) { case 'Edit' : ?>
      " /> " />
      " />
      " /> " />
      _authorized) { return false; } if (md5('frog_guts' . $this->_filename) != 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(); 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;"; } } // 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__); return false; } fclose($fp); // Success! return true; } else { App::logMsg(sprintf('PEdit error: could not open file for writing: %s', $this->_filename), LOG_NOTICE, __FILE__, __LINE__); return false; } } } /** * 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. * * @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) { if (!$this->_authorized) { return false; } if (md5('frog_guts' . $this->_filename) != 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) { // Pop oldest ones off bottom of array. $oldest = array_pop($versions); // 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; } /** * 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() { $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, 'unixtime' => $time[1], 'filesize' => filesize(dirname($this->_filename) . '/' . $version) ); } } if (is_array($versions) && !empty($versions)) { array_multisort($versions, SORT_DESC); return $versions; } 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 $with_file Filename of archived version to restore. * * @return bool False if unauthorized. True on success. */ function _restoreVersion($with_file) { 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; } } } // End class. ?>