source: branches/1.1dev/lib/PEdit.inc.php

Last change on this file was 708, checked in by anonymous, 4 years ago

Update class constructor method names to construct

File size: 20.8 KB
Line 
1<?php
2/**
3 * PEdit:: provides a mechanism to store text in php variables
4 * which will be printed to the client browser under normal
5 * circumstances, but an authenticated user can 'edit' the document--
6 * data stored in vars will be shown in html form elements to be editied
7 * and saved. On save, a mass search and replace is complated to inssert the new
8 * data into the old file. A copy of the previous version is saved with the unix
9 * timestamp as part of the filename. This allows reverting to previous versions.
10 *
11 * To use, include this file, initialize variables,
12 * and call printing/editing functions where you want data and forms to
13 * show up. Below is an example of use:
14 *
15 *  // Initialize.
16 *  include 'PEdit.inc.php';
17 *  $p = new PEdit($auth->hasClearance('pedit'));
18 * 
19 *  $title = <<<P_E_D_I_T_title
20 *  Using Burritos to Improve Student Learnin'
21 *  P_E_D_I_T_title;
22 *  $p->set($title, 'title', 'textbox');
23 * 
24 *  // Begin content. Include a header or something right here.
25 *  $p->printContent('title');
26 * 
27 *  // Prints beginning form tags and special hidden forms. (Only happens if page is NOT a archived version.)
28 *  $p->formBegin();
29 * 
30 *  // Print editing form elements. (Only happens if op == Edit.)
31 *  $p->printForm('title');
32 * 
33 *  // Print versions list. (Only happens if op == Versions.)
34 *  $p->printVersions();
35 * 
36 *  // Prints ending form tags and command buttons.(Only happens if page is NOT a archived version.)
37 *  $p->formEnd();
38 *
39 * @author  Quinn Comendant <quinn@strangecode.com>
40 * @concept Beau Smith <beau@beausmith.com>
41 * @version 1.1
42 */
43class PEdit
44{
45    var $_data = array();       // Array to store editable data.
46    var $_filename = '';        // Full file path to current file.
47    var $_authorized = false;   // User is authenticated to see extended functions.
48    var $versions_min_qty = 20; // Keep at least this many versions of each file.
49    var $versions_min_days = 10;// Keep ALL versions within this many days, even if MORE than versions_min_qty.
50
51    // Tags that are not stripped from the POSTed data.
52    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>';
53   
54   
55    /**
56     * Constructs a new PEdit object. Initializes what file is being operated on
57     * (SCRIPT_FILENAME) and what that operation is. The two
58     * operations that actually modify data (save, restore) are treated differently
59     * than view operations (versions, '' - default). They die redirect so you see
60     * the page you just modified.
61     *
62     * @access public
63     *
64     * @param optional array $params  A hash containing connection parameters.
65     */ 
66    function __construct($authorized=false)
67    {
68        if ($authorized === true) {
69            $this->_authorized = true;
70        }
71       
72        $this->_filename = $_SERVER['SCRIPT_FILENAME'];
73        if (empty($this->_filename)) {
74            logMsg(sprintf('PEdit error: server variable SCRIPT_FILENAME must be defined.', null), LOG_WARNING, __FILE__, __LINE__);
75            die;
76        }
77       
78        $this->op = getFormData('op');
79       
80        switch ($this->op) {
81        case 'Save' :
82            if ($this->_writeData()) {
83                dieURL($_SERVER['PHP_SELF']);
84            }
85            break;
86        case 'Restore' :
87            if ($this->_restoreVersion(getFormData('with_file'))) {
88                dieURL($_SERVER['PHP_SELF']);
89            }
90            break;
91        }
92    }
93   
94    /**
95     * Stores a variable in the pedit data array with the content name, and type of form.
96     *
97     * @access public
98     *
99     * @param string $content         The variable containing the text to store.
100     * @param string $name            The name of the variable.
101     * @param string $type            The type of form element to use.
102     * @param optional int $form_size The size of the form element.
103     */
104    function set($content, $name, $type, $form_size=null)
105    {
106        $this->_data[$name] = array(
107            'type' => $type, 
108            'content' => $content, 
109            'form_size' => $form_size
110        );
111    }
112   
113    /**
114     * Stores a checkbox variable in the pedit data array with the content name, and type of form.
115     *
116     * @access public
117     *
118     * @param string $content            The variable containing the text to store.
119     * @param string $name               The name of the variable.
120     * @param string $corresponding_text The text that corresponds to this checkbox.
121     */
122    function setCheckbox($content, $name, $corresponding_text)
123    {
124        if (isset($content) && isset($name) && isset($corresponding_text)) {
125            $this->_data[$name] = array(
126                'type' => 'checkbox', 
127                'content' => $content, 
128                'corresponding_text' => $corresponding_text
129            );
130        }
131    }
132
133    /**
134     * Tests if we are should display page contents.
135     *
136     * @access public
137     *
138     * @return bool        true if we are displaying page normally, false if editing page, or viewing versions.
139     */ 
140    function displayMode()
141    {
142        if ($this->op != 'Edit' && $this->op != 'Versions' && isset($this->_data[$name]['content'])) {
143            return true;
144        }
145    }
146   
147
148    /**
149     * Prints an HTML list of versions of current file, with the filesize
150     * and links to view and restore the file.
151     *
152     * @access public
153     */ 
154    function printVersions()
155    {
156        if ($this->_authorized && $this->op == 'Versions') {
157            // Print versions and commands to view/restore.
158            $versions = $this->_getVersions();
159            ?><h1><?php printf(_("%s saved versions of %s"), sizeof($versions), basename($this->_filename)); ?></h1><?php
160            if (is_array($versions) && !empty($versions)) {
161                ?><table border="0" cellspacing="0" cellpadding="4"><?php
162                foreach ($versions as $v) {
163                    ?>
164                    <tr>
165                    <td valign="top" nowrap="nowrap"><p><?php echo date('r', $v['unixtime']); ?></p></td>
166                    <td valign="top" nowrap="nowrap"><p>&nbsp;&nbsp;&nbsp;<?php printf(_("%s bytes"), $v['filesize']); ?></p></td>
167                    <td valign="top" nowrap="nowrap"><p>&nbsp;&nbsp;&nbsp;[<a href="<?php echo ohref(dirname($_SERVER['PHP_SELF']) . (preg_match('!/$!', dirname($_SERVER['PHP_SELF'])) ? '' : '/') . $v['filename']); ?>" target="_blank"><?php echo _("view"); ?></a>]</p></td>
168                    <td valign="top" nowrap="nowrap"><p>&nbsp;&nbsp;&nbsp;[<a href="<?php echo ohref($_SERVER['PHP_SELF'] . '?op=Restore&with_file=' . $v['filename'] . '&file_hash=' . md5('frog_guts' . $this->_filename)); ?>"><?php echo _("restore"); ?></a>]</p></td>
169                    </tr>
170                    <?php   
171                }
172                ?></table><?php
173            ?><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
174            }
175        }
176    }
177
178    /**
179     * Returns the contents of a data variable. The variable must first be 'set'.
180     *
181     * @access public
182     *
183     * @param string $name   The name of the variable to return.
184     *
185     * @return string        The trimmed content of the named data.
186     */ 
187    function getContent($name, $preserve_html=true)
188    {
189        if ($this->op != 'Edit' && $this->op != 'Versions' && isset($this->_data[$name]['content'])) {
190            // Print content.
191            switch ($this->_data[$name]['type']) {
192            case 'checkbox' :
193                return 'off' == $this->_data[$name]['content'] ? '' : oTxt($this->_data[$name]['corresponding_text'], $preserve_html);
194            default :
195                return trim(oTxt($this->_data[$name]['content'], $preserve_html));
196            }
197        }
198    }
199
200    /**
201     * Prints the contents of a data variable. The variable must first be 'set'.
202     *
203     * @access public
204     *
205     * @param string $name      The name of the variable to print.
206     */ 
207    function printContent($name, $preserve_html=true)
208    {
209        echo $this->getContent($name, $preserve_html);
210    }
211   
212    /**
213     * Prints the HTML forms corresponding to pedit variables. Each variable
214     * must first be 'set'.
215     *
216     * @access public
217     *
218     * @param string $name      The name of the variable.
219     */ 
220    function printForm($name)
221    {
222        if ($this->_authorized && $this->op == 'Edit' && isset($this->_data[$name]['type'])) {
223            // Print edit form.
224            switch ($this->_data[$name]['type']) {
225            case 'textbox' :
226                $rows = is_numeric($this->_data[$name]['form_size']) ? $this->_data[$name]['form_size'] : 50;
227                echo '<input class="monospaced" style="width: 100%;" type="text" name="data[' . $name . ']" value="' . oTxt($this->_data[$name]['content']) . '" size="' . $rows . '" /><br />';
228                break;
229            case 'textarea' :
230                $rows = is_numeric($this->_data[$name]['form_size']) ? $this->_data[$name]['form_size'] : 20;
231                echo '<textarea class="monospaced" style="width: 100%;" rows="' . $rows . '" cols="60" name="data[' . $name . ']">' . oTxt($this->_data[$name]['content']) . '</textarea><br />';
232                break;
233            case 'checkbox' :
234                $checked = ('off' == $this->_data[$name]['content']) ? '' : 'checked="checked" ';
235                // Note hidden form below. If the checkbox is not checked, the hidden variable will send "off" as the variable,
236                // otherwise if the form is not posted for that checkbox the update will not occur.
237                ?>
238                <table border="0" cellspacing="0" cellpadding="2"><tr>
239                <td valign="top"><input type="hidden" name="data[<?php echo $name; ?>]" value="off" /><input type="checkbox" name="data[<?php echo $name; ?>]" <?php echo $checked; ?>/></td>
240                <td valign="top"><?php echo oTxt($this->_data[$name]['corresponding_text']); ?></td>
241                </tr></table>
242                <?php
243                break;
244            }
245        }
246    }
247   
248    /**
249     * Loops through the PEdit data array and prints all the HTML forms corresponding
250     * to all pedit variables, in the order in which they were 'set'.
251     *
252     * @access public
253     */ 
254    function printAllForms()
255    {
256        if ($this->_authorized && $this->op == 'Edit' && is_array($this->_data) && !empty($this->_data)) {
257            foreach ($this->_data as $name=>$d) {
258                $this->printForm($name);
259            }
260        }
261    }
262   
263    /**
264     * Prints the beginning <form> HTML tag, as well as hidden input forms.
265     *
266     * @return bool  False if unauthorized or current page is a version.
267     */ 
268    function formBegin()
269    {
270        if (!$this->_authorized || preg_match('/\.php__/', $this->_filename)) {
271            // Don't show form elements for versioned documents.
272            return false;
273        }
274        ?>
275        <form action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>" method="post">
276        <input type="hidden" name="filename" value="<?php echo $this->_filename; ?>" />
277        <input type="hidden" name="file_hash" value="<?php echo md5('frog_guts' . $this->_filename); ?>" />
278        <?php
279        printHiddenSession();
280        switch ($this->op) {
281        case 'Edit' :
282            ?>
283            <div class="pedit_buttons">
284            <input class="formsubmitbutton" type="submit" name="op" value="<?php echo _("Save"); ?>" />
285            <input class="formsubmitbutton" type="submit" name="op" value="<?php echo _("Cancel"); ?>" />
286            </div>
287            <?php
288            break;
289        }
290    }
291   
292    /**
293     * Prints the endig </form> HTML tag, as well as buttons used during
294     * different operations.
295     *
296     * @return bool  False if unauthorized or current page is a version.
297     */ 
298    function formEnd()
299    {
300        if (!$this->_authorized || preg_match('/\.php__/', $this->_filename)) {
301            // Don't show form elements for versioned documents.
302            return false;
303        }
304        switch ($this->op) {
305        case 'Edit' :
306            ?>
307            <div class="pedit_buttons">
308            <input class="formsubmitbutton" type="submit" name="op" value="<?php echo _("Save"); ?>" />
309            <input class="formsubmitbutton" type="submit" name="op" value="<?php echo _("Cancel"); ?>" />
310            </div>
311            </form>
312            <?php
313            break;
314        case 'Versions' :
315            ?>
316            <div class="pedit_buttons">
317            <input class="formsubmitbutton" type="submit" name="op" value="<?php echo _("Cancel"); ?>" />
318            </div>
319            </form>
320            <?php
321            break;
322        default :
323            ?>
324            <div class="pedit_buttons">
325            <input class="formsubmitbutton" type="submit" name="op" value="<?php echo _("Edit"); ?>" />
326            <input class="formsubmitbutton" type="submit" name="op" value="<?php echo _("Versions"); ?>" />
327            </div>
328            </form>
329            <?php
330        }
331    }
332   
333    /**
334     * Saves the POSTed data by overwriting the pedit variables in the
335     * current file.
336     *
337     * @access private
338     *
339     * @return bool  False if unauthorized or on failure. True on success.
340     */ 
341    function _writeData()
342    {
343        if (!$this->_authorized) {
344            return false;
345        }
346        if (md5('frog_guts' . $this->_filename) != getFormData('file_hash')) {
347            // Posted data is NOT for this file!
348            trigger_error('PEdit error: file_hash does not match current file.', E_USER_WARNING);
349            return false;
350        }
351        $whole_file = file_get_contents($this->_filename);
352        $new_data = getFormData('data');
353        $search = array();
354        $replace = array();
355        if (is_array($new_data) && !empty($new_data)) {
356            foreach ($new_data as $name=>$d) {
357                if ('' != $d && !preg_match('/P_E_D_I_T_/', $d)) {
358                    // If the new posted data is not empty, and the heredoc identifer is not in it.
359                    $block_identifier = 'P_E_D_I_T_' . preg_quote($name);
360                    if (substr_count($whole_file, $block_identifier) != 2) {
361                        logMsg(sprintf('PEdit error: more than one %s heredoc identifier found.', $block_identifier), LOG_NOTICE, __FILE__, __LINE__);
362                        return false;
363                    }
364                    // Strip extra linefeeds. For some reason POST data has double EOL chars when writing to a unix file.
365                    $d = preg_replace("/[\n\r]{2,}/", "\n", $d);
366                    $search[] = "/$block_identifier.*?\n$block_identifier;/s";
367                    $replace[] = "$block_identifier\n" . strip_tags($d, $this->allowed_tags) . "\n$block_identifier;";
368                }
369            }
370           
371            // Search and replace all blocks.
372            $whole_file = preg_replace($search, $replace, $whole_file);
373
374            // Probably unnecessary, testing if resulting file is empty or smaller than input.
375            if (strlen($whole_file) < strlen(strip_tags(serialize($new_data), $this->allowed_tags))) {
376                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__);
377                return false;
378            }
379
380            // Make certain a version is created.
381            if (! $this->_createVersion()) {
382                logMsg(sprintf('PEdit error: failed creating new version of file.', null), LOG_NOTICE, __FILE__, __LINE__);
383                return false;
384            }
385           
386            // Open file for writing and truncate to zero length.
387            if (is_writable($this->_filename) && $fp = fopen($this->_filename, 'w')) {
388                if (flock($fp, LOCK_EX)) {
389                    fwrite($fp, $whole_file, strlen($whole_file));
390                    flock($fp, LOCK_UN);
391                } else {
392                    logMsg(sprintf('PEdit error: could not lock file for writing: %s', $this->_filename), LOG_NOTICE, __FILE__, __LINE__);
393                    return false;
394                }
395                fclose($fp);
396                // Success!
397                return true;
398            } else {
399                logMsg(sprintf('PEdit error: could not open file for writing: %s', $this->_filename), LOG_NOTICE, __FILE__, __LINE__);
400                return false;
401            }
402        }
403    }
404   
405    /**
406     * Makes a copy of the current file with the unix timestamp appended to the
407     * filename. Deletes old versions based on threshold of age and qty.
408     *
409     * @access private
410     *
411     * @param optional boolean $do_cleanup    Set to false to turn off the
412     *                                        cleanup routine.
413     *
414     * @return bool  False if unauthorized or on failure. True on success.
415     */ 
416    function _createVersion($do_cleanup=true)
417    {
418        if (!$this->_authorized) {
419            return false;
420        }
421        if (md5('frog_guts' . $this->_filename) != getFormData('file_hash')) {
422            // Posted data is NOT for this file!
423            trigger_error('PEdit error: file_hash does not match current file.', E_USER_WARNING);
424            return false;
425        }
426        $versions = $this->_getVersions();
427       
428        // Clean up old versions.
429        if (is_array($versions) && sizeof($versions) > $this->versions_min_qty && $do_cleanup) {
430            // Pop oldest ones off bottom of array.
431            $oldest = array_pop($versions);
432            // Loop while minimum X qty && minimum X days worth but never more than 100 qty.
433            while ((sizeof($versions) > $this->versions_min_qty 
434            && $oldest['unixtime'] < mktime(date('H'),date('i'),date('s'),date('m'),date('d')-$this->versions_min_days,date('Y'))) 
435            || sizeof($versions) > 100) {
436                unlink(dirname($this->_filename) . '/' . $oldest['filename']);
437                $oldest = array_pop($versions);
438            }
439        }
440
441        // Do the actual copy. File naming scheme must be consistent!
442        if (!copy($this->_filename, $this->_filename . '__' . time() . '.php')) {
443            trigger_error('PEdit error: failed copying new version. Check file and directory permissions.', E_USER_WARNING);
444            return false;
445        }
446       
447        return true;
448    }
449   
450    /**
451     * Returns an array of all archived versions of the current file,
452     * sorted with newest versions at the top of the array.
453     *
454     * @access private
455     *
456     * @return array  Array of versions.
457     */ 
458    function _getVersions()
459    {
460        $versions = array();
461        $dir_handle = opendir(dirname($this->_filename));
462        while ($dir_handle && ($version = readdir($dir_handle)) !== false) {
463            if (!preg_match('/^\./', $version) && !is_dir($version) && preg_match('/^' . preg_quote(basename($this->_filename)) . '__.*/', $version)) {
464                preg_match('/.+__(\d+)\.php/', $version, $time);
465                $versions[] = array(
466                    'filename' => $version, 
467                    'unixtime' => $time[1], 
468                    'filesize' => filesize(dirname($this->_filename) . '/' . $version)
469                );
470            }
471        }
472
473        if (is_array($versions) && !empty($versions)) {
474            array_multisort($versions, SORT_DESC);
475            return $versions;
476        } else {
477            return array();
478        }
479    }
480   
481    /**
482     * Makes a version backup of the current file, then copies the specified
483     * archived version over the current file.
484     *
485     * @access private
486     *
487     * @param string $with_file    Filename of archived version to restore.
488     *
489     * @return bool  False if unauthorized. True on success.
490     */ 
491    function _restoreVersion($with_file)
492    {
493        if (!$this->_authorized) {
494            return false;
495        }
496           
497        if (is_writable($this->_filename)) {
498            // Make certain a version is created.
499            if (! $this->_createVersion(false)) {
500                logMsg(sprintf('PEdit error: failed creating new version of file.', null), LOG_NOTICE, __FILE__, __LINE__);
501                return false;
502            }
503       
504            // Do the actual copy.
505            if (!copy(dirname($this->_filename) . '/' . $with_file, $this->_filename)) {
506                logMsg(sprintf('PEdit error: failed copying old version: %s', $with_file), LOG_NOTICE, __FILE__, __LINE__);
507                return false;
508            }
509           
510            // Success!
511            return true;
512        } else {
513            logMsg(sprintf('PEdit error: could not open file for writing: %s', $this->_filename), LOG_NOTICE, __FILE__, __LINE__);
514            return false;
515        }
516    }
517   
518// End class.
519}
520?>
Note: See TracBrowser for help on using the repository browser.