source: trunk/lib/PEdit.inc.php @ 484

Last change on this file since 484 was 484, checked in by anonymous, 10 years ago

Changed private methods and properties to protected. A few minor bug fixes.

File size: 28.1 KB
RevLine 
[1]1<?php
2/**
[362]3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
[396]5 * Copyright 2001-2012 Strangecode, LLC
[441]6 *
[362]7 * This file is part of The Strangecode Codebase.
8 *
9 * The Strangecode Codebase is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
[441]13 *
[362]14 * The Strangecode Codebase is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
[441]18 *
[362]19 * You should have received a copy of the GNU General Public License along with
20 * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23/**
[136]24 * PEdit.inc.php
25 *
26 * PEdit provides a mechanism to store text in php variables
[42]27 * which will be printed to the client browser under normal
[1]28 * circumstances, but an authenticated user can 'edit' the document--
[334]29 * data stored in vars will be shown in html form elements to be edited
[92]30 * and saved. Posted data is stored in XML format in a specified data dir.
31 * A copy of the previous version is saved with the unix
[1]32 * timestamp as part of the filename. This allows reverting to previous versions.
33 *
34 * To use, include this file, initialize variables,
35 * and call printing/editing functions where you want data and forms to
[136]36 * show up.
[441]37 *
[136]38 * @author  Quinn Comendant <quinn@strangecode.com>
39 * @concept Beau Smith <beau@beausmith.com>
[441]40 * @version 2.0
41 *
[136]42 * Example of use:
[441]43
[92]44 // Initialize PEdit object.
45 require_once 'codebase/lib/PEdit.inc.php';
46 $pedit = new PEdit(array(
47     'data_dir' => COMMON_BASE . '/html/_pedit_data',
48     'authorized' => true,
49 ));
[441]50
[92]51 // Setup content data types.
52 $pedit->set('title');
53 $pedit->set('content', array('type' => 'textarea'));
[441]54
[92]55 // After setting all parameters and data, load the data.
56 $pedit->start();
[441]57
[92]58 // Print content.
59 echo $pedit->get('title');
60 echo $pedit->get('content');
[441]61
[92]62 // Print additional PEdit functionality.
63 $pedit->formBegin();
64 $pedit->printAllForms();
65 $pedit->printVersions();
66 $pedit->formEnd();
67
[1]68 */
[92]69class PEdit {
[1]70
[92]71    // PEdit object parameters.
[484]72    protected $_params = array(
[92]73        'data_dir' => '',
74        'character_set' => 'utf-8',
75        'versions_min_qty' => 20,
76        'versions_min_days' => 10,
77    );
[42]78
[484]79    protected $_data = array(); // Array to store loaded data.
80    protected $_data_file = ''; // Full file path to the pedit data file.
81    protected $_authorized = false; // User is authenticated to see extended functions.
82    protected $_data_loaded = false;
[468]83    public $op = '';
[42]84
85    /**
[92]86     * Constructs a new PEdit object. Initializes what file is being operated with
87     * (PHP_SELF) and what that operation is. The two
[1]88     * operations that actually modify data (save, restore) are treated differently
[92]89     * than view operations (versions, view, default). They die redirect so you see
[1]90     * the page you just modified.
[42]91     *
92     * @access public
93     * @param optional array $params  A hash containing connection parameters.
94     */
[468]95    public function __construct($params)
[1]96    {
[92]97        $this->setParam($params);
[441]98
[92]99        if ($this->getParam('authorized') === true) {
[1]100            $this->_authorized = true;
101        }
[441]102
[92]103        // Setup PEAR XML libraries.
[300]104        require_once 'XML/Serializer.php';
[421]105        $this->xml_serializer = new XML_Serializer(array(
[300]106            XML_SERIALIZER_OPTION_INDENT => '',
107            XML_SERIALIZER_OPTION_LINEBREAKS => '',
108            XML_SERIALIZER_OPTION_RETURN_RESULT => true,
109            XML_SERIALIZER_OPTION_TYPEHINTS => true,
110        ));
[92]111        require_once 'XML/Unserializer.php';
[421]112        $this->xml_unserializer = new XML_Unserializer(array(
[92]113            XML_UNSERIALIZER_OPTION_COMPLEXTYPE => 'array',
114        ));
115    }
[441]116
[92]117    /**
118     * Set (or overwrite existing) parameters by passing an array of new parameters.
119     *
120     * @access public
121     * @param  array    $params     Array of parameters (key => val pairs).
122     */
[468]123    public function setParam($params)
[92]124    {
[479]125        $app =& App::getInstance();
[136]126
[92]127        if (isset($params) && is_array($params)) {
128            // Merge new parameters with old overriding only those passed.
129            $this->_params = array_merge($this->_params, $params);
130        } else {
[136]131            $app->logMsg(sprintf('Parameters are not an array: %s', $params), LOG_WARNING, __FILE__, __LINE__);
[92]132        }
133    }
[42]134
[92]135    /**
136     * Return the value of a parameter, if it exists.
137     *
138     * @access public
139     * @param string $param        Which parameter to return.
140     * @return mixed               Configured parameter value.
141     */
[468]142    public function getParam($param)
[92]143    {
[479]144        $app =& App::getInstance();
[441]145
[478]146        if (array_key_exists($param, $this->_params)) {
[92]147            return $this->_params[$param];
148        } else {
[146]149            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
[92]150            return null;
[1]151        }
[92]152    }
[441]153
[92]154    /*
155    * Load the pedit data and run automatic functions.
156    *
157    * @access   public
158    * @author   Quinn Comendant <quinn@strangecode.com>
159    * @since    12 Apr 2006 12:43:47
160    */
[468]161    public function start($initialize_data_file=false)
[92]162    {
[479]163        $app =& App::getInstance();
[136]164
[92]165        if (!is_dir($this->getParam('data_dir'))) {
166            trigger_error(sprintf('PEdit data directory not found: %s', $this->getParam('data_dir')), E_USER_WARNING);
167        }
[441]168
[92]169        // The location of the data file. (i.e.: "COMMON_DIR/html/_pedit_data/news/index.xml")
[415]170        $this->_data_file = sprintf('%s%s.xml', $this->getParam('data_dir'), $_SERVER['SCRIPT_NAME']);
[441]171
172        // Make certain the evaluated path matches the assumed path (realpath will expand /../../);
[415]173        // if realpath returns FALSE we're not concerned because it means the file doesn't exist (_initializeDataFile() will create it).
174        if (false !== realpath($this->_data_file) && $this->_data_file !== realpath($this->_data_file)) {
175            $app->logMsg(sprintf('PEdit data file not a real path: %s', $this->_data_file), LOG_CRIT, __FILE__, __LINE__);
176            trigger_error(sprintf('PEdit data file not a real path: %s', $this->_data_file), E_USER_ERROR);
177        }
[42]178
[92]179        // op is used throughout the script to determine state.
[1]180        $this->op = getFormData('op');
[42]181
[92]182        // Automatic functions based on state.
[1]183        switch ($this->op) {
184        case 'Save' :
185            if ($this->_writeData()) {
[136]186                $app->dieURL($_SERVER['PHP_SELF']);
[1]187            }
188            break;
[332]189
[1]190        case 'Restore' :
[92]191            if ($this->_restoreVersion(getFormData('version'))) {
[136]192                $app->dieURL($_SERVER['PHP_SELF']);
[1]193            }
194            break;
[332]195
[92]196        case 'View' :
197            $this->_data_file = sprintf('%s%s__%s.xml', $this->getParam('data_dir'), $_SERVER['PHP_SELF'], getFormData('version'));
[136]198            $app->raiseMsg(sprintf(_("This is <em><strong>only a preview</strong></em> of version %s."), getFormData('version')), MSG_NOTICE, __FILE__, __LINE__);
[92]199            break;
[1]200        }
[441]201
[92]202        // Load data.
203        $this->_loadDataFile();
204
205        if ($initialize_data_file === true) {
206            $this->_createVersion();
207            $this->_initializeDataFile();
208        }
[1]209    }
[42]210
[1]211    /**
212     * Stores a variable in the pedit data array with the content name, and type of form.
[42]213     *
214     * @access public
215     *
[92]216     * @param string    $content         The variable containing the text to store.
217     * @param array     $options         Additional options to store with this data.
[1]218     */
[468]219    public function set($name, $options=array())
[1]220    {
[479]221        $app =& App::getInstance();
[136]222
[92]223        $name = preg_replace('/\s/', '_', $name);
224        if (!isset($this->_data[$name])) {
225            $this->_data[$name] = array_merge(array('content' => ''), $options);
226        } else {
[136]227            $app->logMsg(sprintf('Duplicate set data: %s', $name), LOG_NOTICE, __FILE__, __LINE__);
[1]228        }
229    }
230
[42]231    /**
[92]232     * Returns the contents of a data variable. The variable must first be 'set'.
[1]233     *
[42]234     * @access public
[92]235     * @param string $name   The name of the variable to return.
236     * @return string        The trimmed content of the named data.
[42]237     */
[468]238    public function get($name)
[1]239    {
[92]240        $name = preg_replace('/\s/', '_', $name);
[1]241        if ($this->op != 'Edit' && $this->op != 'Versions' && isset($this->_data[$name]['content'])) {
[92]242            return $this->_data[$name]['content'];
243        } else {
244            return '';
[1]245        }
246    }
247
[42]248    /**
[92]249     * Prints the beginning <form> HTML tag, as well as hidden input forms.
[42]250     *
[92]251     * @return bool  False if unauthorized or current page is a version.
[42]252     */
[468]253    public function formBegin()
[1]254    {
[479]255        $app =& App::getInstance();
[136]256
[93]257        if (!$this->_authorized || empty($this->_data)) {
[92]258            return false;
259        }
[441]260        ?>
[185]261        <form action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" method="post" id="sc-pedit-form">
262        <input type="hidden" name="filename" value="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" />
[101]263        <input type="hidden" name="file_hash" value="<?php echo $this->_fileHash(); ?>" />
264        <?php
[136]265        $app->printHiddenSession();
[92]266        switch ($this->op) {
267        case 'Edit' :
[101]268            ?>
269            <div class="sc-pedit-buttons">
[247]270                <input type="submit" name="op" value="<?php echo _("Save"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Save"))?>" accesskey="<?php echo mb_substr(_("Save"), 0, 1) ?>" />
271                <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Cancel"))?>" accesskey="<?php echo mb_substr(_("Cancel"), 0, 1) ?>" />
[101]272            </div>
273            <?php
[92]274            break;
[332]275
[92]276        case 'View' :
[101]277            ?>
278            <input type="hidden" name="version" value="<?php echo getFormData('version'); ?>" />
279            <?php
[92]280            break;
[1]281        }
282    }
283
[42]284    /**
[92]285     * Loops through the PEdit data array and prints all the HTML forms corresponding
286     * to all pedit variables, in the order in which they were 'set'.
[1]287     *
[42]288     * @access public
289     */
[468]290    public function printAllForms()
[1]291    {
[92]292        if ($this->_authorized && $this->op == 'Edit' && is_array($this->_data) && $this->_data_loaded) {
293            foreach ($this->_data as $name=>$d) {
294                $this->printForm($name);
[1]295            }
296        }
297    }
298
[42]299    /**
300     * Prints the HTML forms corresponding to pedit variables. Each variable
[1]301     * must first be 'set'.
[42]302     *
303     * @access public
304     * @param string $name      The name of the variable.
[92]305     * @param string $type      Type of form to print. Currently only 'text' and 'textarea' supported.
[42]306     */
[468]307    public function printForm($name, $type='text')
[1]308    {
[441]309        if ($this->_authorized && $this->op == 'Edit' && $this->_data_loaded) {
[101]310            ?>
311            <div class="sc-pedit-item">
312            <?php
[92]313            $type = (isset($this->_data[$name]['type'])) ? $this->_data[$name]['type'] : $type;
[1]314            // Print edit form.
[92]315            switch ($type) {
316            case 'text' :
317            default :
[101]318                ?>
[332]319                <label>
320                <?php echo ucfirst(str_replace('_', ' ', $name)); ?>
[121]321                <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" />
[332]322                </label>
[101]323                <?php
[1]324                break;
[332]325
[1]326            case 'textarea' :
[101]327                ?>
[332]328                <label>
329                <?php echo ucfirst(str_replace('_', ' ', $name)); ?>
[124]330                <textarea name="_pedit_data[<?php echo $name; ?>]" id="sc-pedit-field-<?php echo $name; ?>" rows="" cols="" class="sc-full sc-tall"><?php echo oTxt($this->_data[$name]['content']); ?></textarea>
[332]331                </label>
[101]332                <?php
[1]333                break;
334            }
[101]335            ?>
336            </div>
337            <?php
[1]338        }
339    }
[42]340
341    /**
[334]342     * Prints the ending </form> HTML tag, as well as buttons used during
[42]343     * different operations.
344     *
345     * @return bool  False if unauthorized or current page is a version.
346     */
[468]347    public function formEnd()
[1]348    {
[93]349        if (!$this->_authorized || empty($this->_data)) {
[1]350            // Don't show form elements for versioned documents.
351            return false;
352        }
353        switch ($this->op) {
354        case 'Edit' :
[101]355            ?>
356            <div class="sc-pedit-buttons">
[247]357                <input type="submit" name="op" value="<?php echo _("Save"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Save"))?>" accesskey="<?php echo mb_substr(_("Save"), 0, 1) ?>" />
358                <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Cancel"))?>" accesskey="<?php echo mb_substr(_("Cancel"), 0, 1) ?>" />
[101]359            </div>
360            </form>
361            <?php
[1]362            break;
[332]363
[1]364        case 'Versions' :
[101]365            ?>
366            <div class="sc-pedit-buttons">
[247]367                <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Cancel"))?>" accesskey="<?php echo mb_substr(_("Cancel"), 0, 1) ?>" />
[101]368            </div>
369            </form>
370            <?php
[1]371            break;
[332]372
[92]373        case 'View' :
[101]374            ?>
375            <div class="sc-pedit-buttons">
[247]376                <input type="submit" name="op" value="<?php echo _("Restore"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Restore"))?>" accesskey="<?php echo mb_substr(_("Restore"), 0, 1) ?>" />
377                <input type="submit" name="op" value="<?php echo _("Versions"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Versions"))?>" accesskey="<?php echo mb_substr(_("Versions"), 0, 1) ?>" />
378                <input type="submit" name="op" value="<?php echo _("Cancel"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Cancel"))?>" accesskey="<?php echo mb_substr(_("Cancel"), 0, 1) ?>" />
[101]379            </div>
380            </form>
381            <?php
[92]382            break;
[332]383
[1]384        default :
[101]385            ?>
386            <div class="sc-pedit-buttons">
[247]387                <input type="submit" name="op" value="<?php echo _("Edit"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Edit"))?>" accesskey="<?php echo mb_substr(_("Edit"), 0, 1) ?>" />
388                <input type="submit" name="op" value="<?php echo _("Versions"); ?>" title="<?php echo preg_replace('/^(\w)/i', '($1)', _("Versions"))?>" accesskey="<?php echo mb_substr(_("Versions"), 0, 1) ?>" />
[101]389            </div>
390            </form>
391            <?php
[1]392        }
393    }
[42]394
395    /**
[92]396     * Prints an HTML list of versions of current file, with the filesize
397     * and links to view and restore the file.
398     *
399     * @access public
400     */
[468]401    public function printVersions()
[92]402    {
[479]403        $app =& App::getInstance();
[136]404
[92]405        if ($this->_authorized && $this->op == 'Versions') {
406            // Print versions and commands to view/restore.
407            $version_files = $this->_getVersions();
[101]408            ?><h1><?php printf(_("%s saved versions of %s"), sizeof($version_files), basename($_SERVER['PHP_SELF'])); ?></h1><?php
[92]409            if (is_array($version_files) && !empty($version_files)) {
[101]410                ?>
411                <table id="sc-pedit-versions-table">
412                <tr>
413                    <th><?php echo _("Date"); ?></th>
414                    <th><?php echo _("Time"); ?></th>
415                    <th><?php echo _("File size"); ?></th>
416                    <th><?php echo _("Version options"); ?></th>
417                </tr>
418                <?php
[92]419                foreach ($version_files as $v) {
[101]420                    ?>
421                    <tr>
[136]422                        <td><?php echo date($app->getParam('date_format'), $v['unixtime']); ?></td>
423                        <td><?php echo date($app->getParam('time_format'), $v['unixtime']); ?></td>
[101]424                        <td><?php echo humanFileSize($v['filesize']); ?></td>
[136]425                        <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>
[101]426                    </tr>
427                    <?php
[92]428                }
[101]429                ?>
430                </table>
[121]431                <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>
[101]432                <?php
[92]433            }
434        }
435    }
436
437    /*
[334]438    * Returns a secret hash for the current file.
[92]439    *
440    * @access   public
441    * @author   Quinn Comendant <quinn@strangecode.com>
442    * @since    12 Apr 2006 10:52:35
443    */
[484]444    protected function _fileHash()
[92]445    {
[479]446        $app =& App::getInstance();
[136]447
448        return md5($app->getParam('signing_key') . $_SERVER['PHP_SELF']);
[92]449    }
450
451    /*
452    * Load the XML data file into $this->_data.
453    *
454    * @access   public
455    * @return   bool    false on error
456    * @author   Quinn Comendant <quinn@strangecode.com>
457    * @since    11 Apr 2006 20:36:26
458    */
[484]459    protected function _loadDataFile()
[92]460    {
[479]461        $app =& App::getInstance();
[136]462
[92]463        if (!file_exists($this->_data_file)) {
464            if (!$this->_initializeDataFile()) {
[136]465                $app->logMsg(sprintf('Initializing content file failed: %s', $this->_data_file), LOG_WARNING, __FILE__, __LINE__);
[92]466                return false;
467            }
468        }
469        $xml_file_contents = file_get_contents($this->_data_file);
[441]470        $status = $this->xml_unserializer->unserialize($xml_file_contents, false);
[92]471        if (PEAR::isError($status)) {
[136]472            $app->logMsg(sprintf('XML_Unserialize error: %s', $status->getMessage()), LOG_WARNING, __FILE__, __LINE__);
[92]473            return false;
474        }
475        $xml_file_data = $this->xml_unserializer->getUnserializedData();
476
477        // Only load data specified with set(), even though there may be more in the xml file.
478        foreach ($this->_data as $name => $initial_data) {
479            if (isset($xml_file_data[$name])) {
480                $this->_data[$name] = array_merge($initial_data, $xml_file_data[$name]);
481            } else {
482                $this->_data[$name] = $initial_data;
483            }
484        }
485
486        $this->_data_loaded = true;
487        return true;
488    }
[441]489
[92]490    /*
491    * Start a new data file.
492    *
493    * @access   public
494    * @return   The success value of both xml_serializer->serialize() and _filePutContents()
495    * @author   Quinn Comendant <quinn@strangecode.com>
496    * @since    11 Apr 2006 20:53:42
497    */
[484]498    protected function _initializeDataFile()
[92]499    {
[479]500        $app =& App::getInstance();
[136]501
502        $app->logMsg(sprintf('Initializing data file: %s', $this->_data_file), LOG_INFO, __FILE__, __LINE__);
[92]503        $xml_file_contents = $this->xml_serializer->serialize($this->_data);
504        return $this->_filePutContents($this->_data_file, $xml_file_contents);
505    }
506
507    /**
[1]508     * Saves the POSTed data by overwriting the pedit variables in the
[42]509     * current file.
510     *
511     * @access private
512     * @return bool  False if unauthorized or on failure. True on success.
513     */
[484]514    protected function _writeData()
[1]515    {
[479]516        $app =& App::getInstance();
[136]517
[1]518        if (!$this->_authorized) {
519            return false;
520        }
[92]521        if ($this->_fileHash() != getFormData('file_hash')) {
[1]522            // Posted data is NOT for this file!
[136]523            $app->logMsg(sprintf('File_hash does not match current file.', null), LOG_WARNING, __FILE__, __LINE__);
[1]524            return false;
525        }
[42]526
[92]527        // Scrub incoming data. Escape tags?
528        $new_data = getFormData('_pedit_data');
[1]529
[92]530        if (is_array($new_data) && !empty($new_data)) {
[1]531            // Make certain a version is created.
[92]532            $this->_deleteOldVersions();
533            if (!$this->_createVersion()) {
[136]534                $app->logMsg(sprintf('Failed creating new version of file.', null), LOG_NOTICE, __FILE__, __LINE__);
[1]535                return false;
536            }
[441]537
[92]538            // Collect posted data that is already specified in _data (by set()).
539            foreach ($new_data as $name => $content) {
540                if (isset($this->_data[$name])) {
541                    $this->_data[$name]['content'] = $content;
542                }
543            }
[441]544
[92]545            if (is_array($this->_data) && !empty($this->_data)) {
546                $xml_file_contents = $this->xml_serializer->serialize($this->_data);
547                return $this->_filePutContents($this->_data_file, $xml_file_contents);
548            }
549        }
550    }
[441]551
[92]552    /*
553    * Writes content to the specified file.
554    *
555    * @access   public
556    * @param    string  $filename   Path to file.
557    * @param    string  $content    Data to write into file.
558    * @return   bool                Success or failure.
559    * @author   Quinn Comendant <quinn@strangecode.com>
560    * @since    11 Apr 2006 22:48:30
561    */
[484]562    protected function _filePutContents($filename, $content)
[92]563    {
[479]564        $app =& App::getInstance();
[136]565
[92]566        // Ensure requested filename is within the pedit data dir.
[247]567        if (mb_strpos($filename, $this->getParam('data_dir')) === false) {
[415]568            $app->logMsg(sprintf('Failed writing file outside pedit data_dir: %s', $filename), LOG_ERR, __FILE__, __LINE__);
[92]569            return false;
570        }
[42]571
[92]572        // Recursively create directories.
573        $subdirs = preg_split('!/!', str_replace($this->getParam('data_dir'), '', dirname($filename)), -1, PREG_SPLIT_NO_EMPTY);
[415]574        // Start with the pedit data_dir base.
[92]575        $curr_path = $this->getParam('data_dir');
576        while (!empty($subdirs)) {
577            $curr_path .= '/' . array_shift($subdirs);
578            if (!is_dir($curr_path)) {
579                if (!mkdir($curr_path)) {
[136]580                    $app->logMsg(sprintf('Failed mkdir: %s', $curr_path), LOG_ERR, __FILE__, __LINE__);
[1]581                    return false;
582                }
[92]583            }
584        }
585
586        // Open file for writing and truncate to zero length.
587        if ($fp = fopen($filename, 'w')) {
588            if (flock($fp, LOCK_EX)) {
[441]589                fwrite($fp, $content);
[92]590                flock($fp, LOCK_UN);
[1]591            } else {
[136]592                $app->logMsg(sprintf('Could not lock file for writing: %s', $filename), LOG_ERR, __FILE__, __LINE__);
[1]593                return false;
594            }
[92]595            fclose($fp);
596            // Success!
[136]597            $app->logMsg(sprintf('Wrote to file: %s', $filename), LOG_DEBUG, __FILE__, __LINE__);
[92]598            return true;
599        } else {
[136]600            $app->logMsg(sprintf('Could not open file for writing: %s', $filename), LOG_ERR, __FILE__, __LINE__);
[92]601            return false;
[1]602        }
603    }
[42]604
605    /**
606     * Makes a copy of the current file with the unix timestamp appended to the
[92]607     * filename.
[42]608     *
609     * @access private
[92]610     * @return bool  False on failure. True on success.
[42]611     */
[484]612    protected function _createVersion()
[1]613    {
[479]614        $app =& App::getInstance();
[136]615
[1]616        if (!$this->_authorized) {
617            return false;
618        }
[92]619        if ($this->_fileHash() != getFormData('file_hash')) {
[1]620            // Posted data is NOT for this file!
[136]621            $app->logMsg(sprintf('File_hash does not match current file.', null), LOG_ERR, __FILE__, __LINE__);
[1]622            return false;
623        }
[42]624
[92]625        // Ensure current data file exists.
626        if (!file_exists($this->_data_file)) {
[136]627            $app->logMsg(sprintf('Data file does not yet exist: %s', $this->_data_file), LOG_NOTICE, __FILE__, __LINE__);
[92]628            return false;
[1]629        }
630
631        // Do the actual copy. File naming scheme must be consistent!
[92]632        // filename.php.xml becomes filename.php__1124124128.xml
633        $version_file = sprintf('%s__%s.xml', preg_replace('/\.xml$/', '', $this->_data_file), time());
634        if (!copy($this->_data_file, $version_file)) {
[136]635            $app->logMsg(sprintf('Failed copying new version: %s -> %s', $this->_data_file, $version_file), LOG_ERR, __FILE__, __LINE__);
[1]636            return false;
637        }
[42]638
[1]639        return true;
640    }
[441]641
[92]642    /*
643    * Delete all versions older than versions_min_days if there are more than versions_min_qty or 100.
644    *
645    * @access   public
646    * @return bool  False on failure. True on success.
647    * @author   Quinn Comendant <quinn@strangecode.com>
648    * @since    12 Apr 2006 11:08:11
649    */
[484]650    protected function _deleteOldVersions()
[92]651    {
[479]652        $app =& App::getInstance();
[136]653
[92]654        $version_files = $this->_getVersions();
655        if (is_array($version_files) && sizeof($version_files) > $this->getParam('versions_min_qty')) {
656            // Pop oldest ones off bottom of array.
657            $oldest = array_pop($version_files);
658            // Loop while minimum X qty && minimum X days worth but never more than 100 qty.
659            while ((sizeof($version_files) > $this->getParam('versions_min_qty')
660            && $oldest['unixtime'] < mktime(date('H'), date('i'), date('s'), date('m'), date('d') - $this->getParam('versions_min_days'), date('Y')))
661            || sizeof($version_files) > 100) {
662                $del_file = dirname($this->_data_file) . '/' . $oldest['filename'];
663                if (!unlink($del_file)) {
[136]664                    $app->logMsg(sprintf('Failed deleting version: %s', $del_file), LOG_ERR, __FILE__, __LINE__);
[92]665                }
666                $oldest = array_pop($version_files);
667            }
668        }
669    }
[42]670
671    /**
672     * Returns an array of all archived versions of the current file,
[1]673     * sorted with newest versions at the top of the array.
[42]674     *
[1]675     * @access private
[42]676     * @return array  Array of versions.
677     */
[484]678    protected function _getVersions()
[1]679    {
[92]680        $version_files = array();
681        $dir_handle = opendir(dirname($this->_data_file));
682        $curr_file_preg_pattern = sprintf('/^%s__(\d+).xml$/', preg_quote(basename($_SERVER['PHP_SELF'])));
683        while ($dir_handle && ($version_file = readdir($dir_handle)) !== false) {
684            if (!preg_match('/^\./', $version_file) && !is_dir($version_file) && preg_match($curr_file_preg_pattern, $version_file, $time)) {
685                $version_files[] = array(
686                    'filename' => $version_file,
[42]687                    'unixtime' => $time[1],
[92]688                    'filesize' => filesize(dirname($this->_data_file) . '/' . $version_file)
[1]689                );
690            }
691        }
692
[92]693        if (is_array($version_files) && !empty($version_files)) {
694            array_multisort($version_files, SORT_DESC);
695            return $version_files;
[1]696        } else {
697            return array();
698        }
699    }
[42]700
701    /**
[1]702     * Makes a version backup of the current file, then copies the specified
[42]703     * archived version over the current file.
704     *
705     * @access private
[92]706     * @param string $version    Unix timestamp of archived version to restore.
707     * @return bool  False on failure. True on success.
[42]708     */
[484]709    protected function _restoreVersion($version)
[1]710    {
[479]711        $app =& App::getInstance();
[136]712
[1]713        if (!$this->_authorized) {
714            return false;
715        }
[441]716
[92]717        // The file to restore.
718        $version_file = sprintf('%s__%s.xml', preg_replace('/\.xml$/', '', $this->_data_file), $version);
[441]719
[92]720        // Ensure specified version exists.
721        if (!file_exists($version_file)) {
[334]722            $app->logMsg(sprintf('Cannot restore non-existent file: %s', $version_file), LOG_NOTICE, __FILE__, __LINE__);
[92]723            return false;
724        }
[42]725
[92]726        // Make certain a version is created.
727        if (!$this->_createVersion()) {
[136]728            $app->logMsg(sprintf('Failed creating new version of file.', null), LOG_ERR, __FILE__, __LINE__);
[92]729            return false;
730        }
[42]731
[92]732        // Do the actual copy.
733        if (!copy($version_file, $this->_data_file)) {
[136]734            $app->logMsg(sprintf('Failed copying old version: %s -> %s', $version_file, $this->_data_file), LOG_ERR, __FILE__, __LINE__);
[1]735            return false;
736        }
[92]737
738        // Success!
[136]739        $app->raiseMsg(sprintf(_("Page has been restored to version %s."), $version), MSG_SUCCESS, __FILE__, __LINE__);
[92]740        return true;
[1]741    }
[42]742
[1]743} // End class.
Note: See TracBrowser for help on using the repository browser.