source: trunk/lib/Version.inc.php @ 146

Last change on this file since 146 was 146, checked in by scdev, 18 years ago

Q - added persistant database storage to Prefs.inc.php. Modified getParam failure log type to LOG_DEBUG in all classes.

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