source: branches/eli_branch/lib/Version.inc.php @ 530

Last change on this file since 530 was 439, checked in by anonymous, 11 years ago

added public and private keywords to all properties and methods, changed old classname constructor function to construct, removed more ?> closing tags

File size: 19.1 KB
Line 
1<?php
2/**
3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
5 * Copyright 2001-2012 Strangecode, LLC
6 *
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.
13 *
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.
18 *
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/**
24 * Version.inc.php
25 *
26 * The Version class provides a system for saving, reviewing, and
27 * restoring versions of a record of any DB table. All the data in the record is
28 * serialized, compressed, and saved in a blob in the version_tbl. Restoring a
29 * version simply does a REPLACE INTO of the data. It is very simple, and works
30 * with multiple database tables, but the drawback is that relationships for
31 * a record cannot be retained. For example, an article from an article_tbl can
32 * be saved, but not categories associated to the record in a category_article_tbl.
33 * The restored article will simple retain the relationships that the previous
34 * current article had.
35 *
36 * @author  Quinn Comendant <quinn@strangecode.com>
37 * @version 2.1
38 */
39class Version {
40
41    // Configuration of this object.
42    private $_params = array(
43        'max_qty' => 100, // Never have more than this many versions of each record.
44        'min_qty' => 25, // Keep at least this many versions of each record.
45        'min_days' => 7, // Keep ALL versions within this many days, even if MORE than min_qty.
46        'db_table' => 'version_tbl',
47       
48        // Automatically create table and verify columns. Better set to false after site launch.
49        // This value is overwritten by the $app->getParam('db_create_tables') setting if it is available.
50        'create_table' => true,
51        // If true, makes an exact comparison of saved vs. live table schemas. If false, just checks that the saved columns are available.
52        'db_schema_strict' => true,
53    );
54
55    // Auth_SQL object from which to access a current user_id.
56    private $_auth;
57
58    /**
59     * This method enforces the singleton pattern for this class.
60     *
61     * @return  object  Reference to the global Lock object.
62     * @access  public
63     * @static
64     */
65    public static function &getInstance($auth_object)
66    {
67        static $instance = null;
68
69        if ($instance === null) {
70            $instance = new Version($auth_object);
71        }
72
73        return $instance;
74    }
75
76    /**
77     * Constructor. Pass an Auth object on which to perform user lookups.
78     *
79     * @param mixed  $auth_object  An Auth_SQL object.
80     */
81    public function __construct($auth_object)
82    {
83        $app =& App::getInstance();
84
85        if (!method_exists($auth_object, 'get') || !method_exists($auth_object, 'getUsername')) {
86            trigger_error('Constructor not provided a valid Auth_* object.', E_USER_ERROR);
87        }
88
89        $this->_auth = $auth_object;
90
91        // Get create tables config from global context.
92        if (!is_null($app->getParam('db_create_tables'))) {
93            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
94        }
95    }
96
97    /**
98     * Setup the database table for this class.
99     *
100     * @access  public
101     * @author  Quinn Comendant <quinn@strangecode.com>
102     * @since   26 Aug 2005 17:09:36
103     */
104    public function initDB($recreate_db=false)
105    {
106        $app =& App::getInstance();
107        $db =& DB::getInstance();
108
109        static $_db_tested = false;
110
111        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
112            if ($recreate_db) {
113                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
114                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__);
115            }
116            $db->query("CREATE TABLE IF NOT EXISTS " . $db->escapeString($this->getParam('db_table')) . " (
117                version_id INT NOT NULL AUTO_INCREMENT,
118                record_table VARCHAR(255) NOT NULL DEFAULT '',
119                record_key VARCHAR(255) NOT NULL DEFAULT '',
120                record_val VARCHAR(255) NOT NULL DEFAULT '',
121                version_data MEDIUMBLOB NOT NULL,
122                version_title VARCHAR(255) NOT NULL DEFAULT '',
123                version_number SMALLINT(11) UNSIGNED NOT NULL DEFAULT '0',
124                version_notes VARCHAR(255) NOT NULL DEFAULT '',
125                saved_by_user_id SMALLINT(11) NOT NULL DEFAULT '0',
126                version_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
127                PRIMARY KEY (version_id),
128                KEY record_table (record_table),
129                KEY record_key (record_key),
130                KEY record_val (record_val)
131            )");
132
133            if (!$db->columnExists($this->getParam('db_table'), array(
134                'version_id',
135                'record_table',
136                'record_key',
137                'record_val',
138                'version_data',
139                'version_title',
140                'version_number',
141                'version_notes',
142                'saved_by_user_id',
143                'version_datetime',
144            ), false, false)) {
145                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
146                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
147            }
148        }
149        $_db_tested = true;
150    }
151
152    /**
153     * Set the params of this object.
154     *
155     * @param  array $params   Array of param keys and values to set.
156     */
157    public function setParam($params=null)
158    {
159        if (isset($params) && is_array($params)) {
160            // Merge new parameters with old overriding only those passed.
161            $this->_params = array_merge($this->_params, $params);
162        }
163    }
164
165    /**
166     * Return the value of a parameter, if it exists.
167     *
168     * @access public
169     * @param string $param        Which parameter to return.
170     * @return mixed               Configured parameter value.
171     */
172    public function getParam($param)
173    {
174        $app =& App::getInstance();
175   
176        if (isset($this->_params[$param])) {
177            return $this->_params[$param];
178        } else {
179            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
180            return null;
181        }
182    }
183
184    /**
185     * Saves a version of the current record into the version table.
186     *
187     * @param string $record_table  The table containing the record.
188     * @param string $record_key    The key column for the record.
189     * @param string $record_val    The value of the key column for the record.
190     * @param string $title         The title of this record. Only used for human presentation.
191     *
192     * @return int                  The id for the version (mysql last insert id).
193     */
194    public function create($record_table, $record_key, $record_val, $title='', $notes='')
195    {
196        $app =& App::getInstance();
197        $db =& DB::getInstance();
198
199        $this->initDB();
200
201        // Get current record.
202        if (!$record = $this->getCurrent($record_table, $record_key, $record_val)) {
203            $app->logMsg(sprintf('Could not create %s version, record not found: %s, %s, %s.', $title, $record_table, $record_key, $record_val), LOG_ERR, __FILE__, __LINE__);
204            return false;
205        }
206       
207        // Get previous version_number.
208        $qid = $db->query("
209            SELECT MAX(version_number) FROM " . $db->escapeString($this->getParam('db_table')) . "
210            WHERE record_table = '" . $db->escapeString($record_table) . "'
211            AND record_key = '" . $db->escapeString($record_key) . "'
212            AND record_val = '" . $db->escapeString($record_val) . "'
213        ");
214        list($last_version_number) = mysql_fetch_row($qid);
215
216        // Clean-up old versions.
217        $this->deleteOld($record_table, $record_key, $record_val);
218
219        // Save as new version.
220        // TODO: after MySQL 5.0.23 is released this query could benefit from INSERT DELAYED.
221        $db->query("
222            INSERT INTO " . $db->escapeString($this->getParam('db_table')) . " (
223                record_table,
224                record_key,
225                record_val,
226                version_data,
227                version_title,
228                version_number,
229                version_notes,
230                saved_by_user_id,
231                version_datetime
232            ) VALUES (
233                '" . $db->escapeString($record_table) . "',
234                '" . $db->escapeString($record_key) . "',
235                '" . $db->escapeString($record_val) . "',
236                '" . $db->escapeString(gzcompress(serialize($record), 9)) . "',
237                '" . $db->escapeString($title) . "',
238                '" . $db->escapeString($last_version_number + 1) . "',
239                '" . $db->escapeString($notes) . "',
240                '" . $db->escapeString($this->_auth->get('user_id')) . "',
241                NOW()
242            )
243        ");
244
245        return mysql_insert_id($db->getDBH());
246    }
247
248    /**
249     * Copy a version back into it's original table.
250     *
251     * @param string $version_id    The id of the version to restore.
252     *
253     * @return int                  The id for the version (mysql last insert id).
254     */
255    public function restore($version_id)
256    {
257        $app =& App::getInstance();
258        $db =& DB::getInstance();
259
260        $this->initDB();
261
262        // Get version data.
263        $qid = $db->query("
264            SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . "
265            WHERE version_id = '" . $db->escapeString($version_id) . "'
266        ");
267        if (!$record = mysql_fetch_assoc($qid)) {
268            $app->raiseMsg(sprintf(_("Version ID %s%s not found."), $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), MSG_WARNING, __FILE__, __LINE__);
269            $app->logMsg(sprintf('Version ID %s%s not found.', $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), LOG_WARNING, __FILE__, __LINE__);
270            return false;
271        }
272        $data = unserialize(gzuncompress($record['version_data']));
273
274        // Ensure saved db columns match current table schema.
275        if (!$db->columnExists($record['record_table'], array_keys($data), $this->getParam('db_schema_strict'))) {
276            $app->raiseMsg(sprintf(_("Version ID %s%s is not compatible with the current database table."), $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), MSG_ERR, __FILE__, __LINE__);
277            $app->logMsg(sprintf('Version ID %s%s restoration failed, DB schema does not match for table %s.', $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')'), $record['record_table']), LOG_ALERT, __FILE__, __LINE__);
278            return false;
279        }
280
281        // SQLize the keys of the specified versioned record.
282        $replace_keys = join(",\n", array_map(array($db, 'escapeString'), array_keys($data)));
283
284        // SQLize the keys of the values of the specified versioned record. (These are more complex because we need to account for SQL null values.)
285        $replace_values = '';
286        $comma = '';
287        foreach ($data as $v) {
288            $replace_values .= is_null($v) ? "$comma\nNULL" : "$comma\n'" . $db->escapeString($v) . "'";
289            $comma = ',';
290        }
291
292        // Replace current record with specified versioned record.
293        $db->query("
294            REPLACE INTO " . $record['record_table'] . " (
295                $replace_keys
296            ) VALUES (
297                $replace_values
298            )
299        ");
300
301        return $record;
302    }
303
304    /**
305     * Version garbage collection. Deletes versions older than min_days
306     * when quantity of versions exceeds min_qty. If quantity
307     * exceeds 100 within min_days, the oldest are deleted to bring the
308     * quantity back down to min_qty.
309     *
310     * @param string $record_table  The table containing the record.
311     * @param string $record_key    The key column for the record.
312     * @param string $record_val    The value of the key column for the record.
313     *
314     * @return mixed                Array of versions, or false if none.
315     */
316    public function deleteOld($record_table, $record_key, $record_val)
317    {
318        $db =& DB::getInstance();
319   
320        $this->initDB();
321
322        // Get total number of versions for this record.
323        $qid = $db->query("
324            SELECT COUNT(*) FROM " . $db->escapeString($this->getParam('db_table')) . "
325            WHERE record_table = '" . $db->escapeString($record_table) . "'
326            AND record_key = '" . $db->escapeString($record_key) . "'
327            AND record_val = '" . $db->escapeString($record_val) . "'
328        ");
329        list($v_count) = mysql_fetch_row($qid);
330
331        if ($v_count > $this->getParam('min_qty')) {
332            if ($v_count > $this->getParam('max_qty')) {
333                // To prevent a record bomb, limit max number of versions to max_qty.
334                // First query for oldest records, selecting enough to bring total number down to min_qty.
335                $qid = $db->query("
336                    SELECT version_id FROM " . $db->escapeString($this->getParam('db_table')) . "
337                    WHERE record_table = '" . $db->escapeString($record_table) . "'
338                    AND record_key = '" . $db->escapeString($record_key) . "'
339                    AND record_val = '" . $db->escapeString($record_val) . "'
340                    ORDER BY version_datetime ASC
341                    LIMIT " . ($v_count - $this->getParam('min_qty')) . "
342                ");
343                while (list($old_id) = mysql_fetch_row($qid)) {
344                    $old_versions[] = $old_id;
345                }
346                $db->query("
347                    DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
348                    WHERE version_id IN ('" . join("','", $old_versions) . "')
349                ");
350            } else {
351                // Delete versions older than min_days, while still keeping min_qty.
352                $qid = $db->query("
353                    SELECT version_id FROM " . $db->escapeString($this->getParam('db_table')) . "
354                    WHERE record_table = '" . $db->escapeString($record_table) . "'
355                    AND record_key = '" . $db->escapeString($record_key) . "'
356                    AND record_val = '" . $db->escapeString($record_val) . "'
357                    AND DATE_ADD(version_datetime, INTERVAL '" . $this->getParam('min_days') . "' DAY) < NOW()
358                    ORDER BY version_datetime ASC
359                    LIMIT " . ($v_count - $this->getParam('min_qty')) . "
360                ");
361                while (list($old_id) = mysql_fetch_row($qid)) {
362                    $old_versions[] = $old_id;
363                }
364                if (sizeof($old_versions) > 0) {
365                    $db->query("
366                        DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
367                        WHERE version_id IN ('" . join("','", $old_versions) . "')
368                    ");
369                }
370            }
371        }
372    }
373
374    /**
375     * Get a list of versions of specified record.
376     *
377     * @param string $record_table  The table containing the record.
378     * @param string $record_key    The key column for the record.
379     * @param string $record_val    The value of the key column for the record.
380     *
381     * @return mixed                Array of versions, or false if none.
382     */
383    public function getList($record_table, $record_key, $record_val)
384    {
385        $db =& DB::getInstance();
386   
387        $this->initDB();
388
389        // Get versions of this record.
390        $qid = $db->query("
391            SELECT
392                version_id,
393                saved_by_user_id,
394                version_datetime,
395                version_title,
396                version_number,
397                version_notes
398            FROM " . $db->escapeString($this->getParam('db_table')) . "
399            WHERE record_table = '" . $db->escapeString($record_table) . "'
400            AND record_key = '" . $db->escapeString($record_key) . "'
401            AND record_val = '" . $db->escapeString($record_val) . "'
402            ORDER BY version_datetime DESC
403        ");
404        $versions = array();
405        while ($row = mysql_fetch_assoc($qid)) {
406            // Get admin usernames.
407            $row['editor'] = $this->_auth->getUsername($row['saved_by_user_id']);
408            $versions[] = $row;
409        }
410        return $versions;
411    }
412
413    /**
414     * Get the version record for a specified version id.
415     *
416     * @param string $version_id    The id of the version to restore.
417     *
418     * @return mixed                Array of data saved in version, or false if none.
419     */
420    public function getVerson($version_id)
421    {
422        $db =& DB::getInstance();
423   
424        $this->initDB();
425
426        // Get version data.
427        $qid = $db->query("
428            SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . "
429            WHERE version_id = '" . $db->escapeString($version_id) . "'
430        ");
431        return mysql_fetch_assoc($qid);
432    }
433
434    /**
435     * Get the data stored for a specified version id.
436     *
437     * @param string $version_id    The id of the version to restore.
438     *
439     * @return mixed                Array of data saved in version, or false if none.
440     */
441    public function getData($version_id)
442    {
443        $db =& DB::getInstance();
444   
445        $this->initDB();
446
447        // Get version data.
448        $qid = $db->query("
449            SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . "
450            WHERE version_id = '" . $db->escapeString($version_id) . "'
451        ");
452        $record = mysql_fetch_assoc($qid);
453        if (isset($record['version_data'])) {
454            return unserialize(gzuncompress($record['version_data']));
455        } else {
456            return false;
457        }
458    }
459
460    /**
461     * Get the current record data from the original table.
462     *
463     * @param string $version_id    The id of the version to restore.
464     *
465     * @return mixed                Array of data saved in version, or false if none.
466     */
467    public function getCurrent($record_table, $record_key, $record_val)
468    {
469        $db =& DB::getInstance();
470   
471        $this->initDB();
472
473        $qid = $db->query("
474            SELECT * FROM " . $db->escapeString($record_table) . "
475            WHERE " . $db->escapeString($record_key) . " = '" . $db->escapeString($record_val) . "'
476        ");
477        if ($record = mysql_fetch_assoc($qid)) {
478            return $record;
479        } else {
480            return false;
481        }
482    }
483
484
485} // End of class.
Note: See TracBrowser for help on using the repository browser.