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

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

Q - increased some LOG_DEBUG messages to LOG_INFO so we can run with debugging off and still get the important ones.

File size: 18.0 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, 'get') || !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_INFO, __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_number SMALLINT(11) UNSIGNED NOT NULL DEFAULT '0',
102                version_notes VARCHAR(255) NOT NULL DEFAULT '',
103                saved_by_user_id SMALLINT(11) NOT NULL DEFAULT '0',
104                version_datetime DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
105                PRIMARY KEY (version_id),
106                KEY record_table (record_table),
107                KEY record_key (record_key),
108                KEY record_val (record_val)
109            )");
110
111            if (!$db->columnExists($this->getParam('db_table'), array(
112                'version_id',
113                'record_table',
114                'record_key',
115                'record_val',
116                'version_data',
117                'version_title',
118                'version_number',
119                'version_notes',
120                'saved_by_user_id',
121                'version_datetime',
122            ), false, false)) {
123                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
124                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
125            }
126        }
127        $_db_tested = true;
128    }
129
130    /**
131     * Set the params of this object.
132     *
133     * @param  array $params   Array of param keys and values to set.
134     */
135    function setParam($params=null)
136    {
137        if (isset($params) && is_array($params)) {
138            // Merge new parameters with old overriding only those passed.
139            $this->_params = array_merge($this->_params, $params);
140        }
141    }
142
143    /**
144     * Return the value of a parameter, if it exists.
145     *
146     * @access public
147     * @param string $param        Which parameter to return.
148     * @return mixed               Configured parameter value.
149     */
150    function getParam($param)
151    {
152        $app =& App::getInstance();
153   
154        if (isset($this->_params[$param])) {
155            return $this->_params[$param];
156        } else {
157            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
158            return null;
159        }
160    }
161
162    /**
163     * Saves a version of the current record into the version table.
164     *
165     * @param string $record_table  The table containing the record.
166     * @param string $record_key    The key column for the record.
167     * @param string $record_val    The value of the key column for the record.
168     * @param string $title         The title of this record. Only used for human presentation.
169     *
170     * @return int                  The id for the version (mysql last insert id).
171     */
172    function create($record_table, $record_key, $record_val, $title='', $notes='')
173    {
174        $app =& App::getInstance();
175        $db =& DB::getInstance();
176
177        $this->initDB();
178
179        // Get current record.
180        if (!$record = $this->getCurrent($record_table, $record_key, $record_val)) {
181            $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__);
182            return false;
183        }
184       
185        // Get previous version_number.
186        $qid = $db->query("
187            SELECT MAX(version_number) FROM " . $db->escapeString($this->getParam('db_table')) . "
188            WHERE record_table = '" . $db->escapeString($record_table) . "'
189            AND record_key = '" . $db->escapeString($record_key) . "'
190            AND record_val = '" . $db->escapeString($record_val) . "'
191        ");
192        list($last_version_number) = mysql_fetch_row($qid);
193
194        // Clean-up old versions.
195        $this->deleteOld($record_table, $record_key, $record_val);
196
197        // Save as new version.
198        // TODO: after MySQL 5.0.23 is released this query could benefit from INSERT DELAYED.
199        $db->query("
200            INSERT INTO " . $db->escapeString($this->getParam('db_table')) . " (
201                record_table,
202                record_key,
203                record_val,
204                version_data,
205                version_title,
206                version_number,
207                version_notes,
208                saved_by_user_id,
209                version_datetime
210            ) VALUES (
211                '" . $db->escapeString($record_table) . "',
212                '" . $db->escapeString($record_key) . "',
213                '" . $db->escapeString($record_val) . "',
214                '" . $db->escapeString(gzcompress(serialize($record), 9)) . "',
215                '" . $db->escapeString($title) . "',
216                '" . $db->escapeString($last_version_number + 1) . "',
217                '" . $db->escapeString($notes) . "',
218                '" . $db->escapeString($this->_auth->get('user_id')) . "',
219                NOW()
220            )
221        ");
222
223        return mysql_insert_id($db->getDBH());
224    }
225
226    /**
227     * Copy a version back into it's original table.
228     *
229     * @param string $version_id    The id of the version to restore.
230     *
231     * @return int                  The id for the version (mysql last insert id).
232     */
233    function restore($version_id)
234    {
235        $app =& App::getInstance();
236        $db =& DB::getInstance();
237
238        $this->initDB();
239
240        // Get version data.
241        $qid = $db->query("
242            SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . "
243            WHERE version_id = '" . $db->escapeString($version_id) . "'
244        ");
245        if (!$record = mysql_fetch_assoc($qid)) {
246            $app->raiseMsg(sprintf(_("Version ID %s%s not found."), $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), MSG_WARNING, __FILE__, __LINE__);
247            $app->logMsg(sprintf('Version ID %s%s not found.', $version_id, (empty($record['version_title']) ? '' : ' (' . $record['version_title'] . ')')), LOG_WARNING, __FILE__, __LINE__);
248            return false;
249        }
250        $data = unserialize(gzuncompress($record['version_data']));
251
252        // Ensure saved db columns match current table schema.
253        if (!$db->columnExists($record['record_table'], array_keys($data), $this->getParam('db_schema_strict'))) {
254            $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__);
255            $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__);
256            return false;
257        }
258
259        // SQLize the keys of the specified versioned record.
260        $replace_keys = join(",\n", array_map(array($db, 'escapeString'), array_keys($data)));
261
262        // SQLize the keys of the values of the specified versioned record. (These are more complex because we need to account for SQL null values.)
263        $replace_values = '';
264        $comma = '';
265        foreach ($data as $v) {
266            $replace_values .= is_null($v) ? "$comma\nNULL" : "$comma\n'" . $db->escapeString($v) . "'";
267            $comma = ',';
268        }
269
270        // Replace current record with specified versioned record.
271        $db->query("
272            REPLACE INTO " . $record['record_table'] . " (
273                $replace_keys
274            ) VALUES (
275                $replace_values
276            )
277        ");
278
279        return $record;
280    }
281
282    /**
283     * Version garbage collection. Deletes versions older than min_days
284     * when quantity of versions exceeds min_qty. If quantity
285     * exceeds 100 within min_days, the oldest are deleted to bring the
286     * quantity back down to min_qty.
287     *
288     * @param string $record_table  The table containing the record.
289     * @param string $record_key    The key column for the record.
290     * @param string $record_val    The value of the key column for the record.
291     *
292     * @return mixed                Array of versions, or false if none.
293     */
294    function deleteOld($record_table, $record_key, $record_val)
295    {
296        $db =& DB::getInstance();
297   
298        $this->initDB();
299
300        // Get total number of versions for this record.
301        $qid = $db->query("
302            SELECT COUNT(*) FROM " . $db->escapeString($this->getParam('db_table')) . "
303            WHERE record_table = '" . $db->escapeString($record_table) . "'
304            AND record_key = '" . $db->escapeString($record_key) . "'
305            AND record_val = '" . $db->escapeString($record_val) . "'
306        ");
307        list($v_count) = mysql_fetch_row($qid);
308
309        if ($v_count > $this->getParam('min_qty')) {
310            if ($v_count > $this->getParam('max_qty')) {
311                // To prevent a record bomb, limit max number of versions to max_qty.
312                // First query for oldest records, selecting enough to bring total number down to min_qty.
313                $qid = $db->query("
314                    SELECT version_id FROM " . $db->escapeString($this->getParam('db_table')) . "
315                    WHERE record_table = '" . $db->escapeString($record_table) . "'
316                    AND record_key = '" . $db->escapeString($record_key) . "'
317                    AND record_val = '" . $db->escapeString($record_val) . "'
318                    ORDER BY version_datetime ASC
319                    LIMIT " . ($v_count - $this->getParam('min_qty')) . "
320                ");
321                while (list($old_id) = mysql_fetch_row($qid)) {
322                    $old_versions[] = $old_id;
323                }
324                $db->query("
325                    DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
326                    WHERE version_id IN ('" . join("','", $old_versions) . "')
327                ");
328            } else {
329                // Delete versions older than min_days, while still keeping min_qty.
330                $qid = $db->query("
331                    SELECT version_id FROM " . $db->escapeString($this->getParam('db_table')) . "
332                    WHERE record_table = '" . $db->escapeString($record_table) . "'
333                    AND record_key = '" . $db->escapeString($record_key) . "'
334                    AND record_val = '" . $db->escapeString($record_val) . "'
335                    AND DATE_ADD(version_datetime, INTERVAL '" . $this->getParam('min_days') . "' DAY) < NOW()
336                    ORDER BY version_datetime ASC
337                    LIMIT " . ($v_count - $this->getParam('min_qty')) . "
338                ");
339                while (list($old_id) = mysql_fetch_row($qid)) {
340                    $old_versions[] = $old_id;
341                }
342                if (sizeof($old_versions) > 0) {
343                    $db->query("
344                        DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
345                        WHERE version_id IN ('" . join("','", $old_versions) . "')
346                    ");
347                }
348            }
349        }
350    }
351
352    /**
353     * Get a list of versions of specified record.
354     *
355     * @param string $record_table  The table containing the record.
356     * @param string $record_key    The key column for the record.
357     * @param string $record_val    The value of the key column for the record.
358     *
359     * @return mixed                Array of versions, or false if none.
360     */
361    function getList($record_table, $record_key, $record_val)
362    {
363        $db =& DB::getInstance();
364   
365        $this->initDB();
366
367        // Get versions of this record.
368        $qid = $db->query("
369            SELECT
370                version_id,
371                saved_by_user_id,
372                version_datetime,
373                version_title,
374                version_number,
375                version_notes
376            FROM " . $db->escapeString($this->getParam('db_table')) . "
377            WHERE record_table = '" . $db->escapeString($record_table) . "'
378            AND record_key = '" . $db->escapeString($record_key) . "'
379            AND record_val = '" . $db->escapeString($record_val) . "'
380            ORDER BY version_datetime DESC
381        ");
382        $versions = array();
383        while ($row = mysql_fetch_assoc($qid)) {
384            // Get admin usernames.
385            $row['editor'] = $this->_auth->getUsername($row['saved_by_user_id']);
386            $versions[] = $row;
387        }
388        return $versions;
389    }
390
391    /**
392     * Get the version record for a specified version id.
393     *
394     * @param string $version_id    The id of the version to restore.
395     *
396     * @return mixed                Array of data saved in version, or false if none.
397     */
398    function getVerson($version_id)
399    {
400        $db =& DB::getInstance();
401   
402        $this->initDB();
403
404        // Get version data.
405        $qid = $db->query("
406            SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . "
407            WHERE version_id = '" . $db->escapeString($version_id) . "'
408        ");
409        return mysql_fetch_assoc($qid);
410    }
411
412    /**
413     * Get the data stored for a specified version id.
414     *
415     * @param string $version_id    The id of the version to restore.
416     *
417     * @return mixed                Array of data saved in version, or false if none.
418     */
419    function getData($version_id)
420    {
421        $db =& DB::getInstance();
422   
423        $this->initDB();
424
425        // Get version data.
426        $qid = $db->query("
427            SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . "
428            WHERE version_id = '" . $db->escapeString($version_id) . "'
429        ");
430        $record = mysql_fetch_assoc($qid);
431        if (isset($record['version_data'])) {
432            return unserialize(gzuncompress($record['version_data']));
433        } else {
434            return false;
435        }
436    }
437
438    /**
439     * Get the current record data from the original table.
440     *
441     * @param string $version_id    The id of the version to restore.
442     *
443     * @return mixed                Array of data saved in version, or false if none.
444     */
445    function getCurrent($record_table, $record_key, $record_val)
446    {
447        $db =& DB::getInstance();
448   
449        $this->initDB();
450
451        $qid = $db->query("
452            SELECT * FROM " . $db->escapeString($record_table) . "
453            WHERE " . $db->escapeString($record_key) . " = '" . $db->escapeString($record_val) . "'
454        ");
455        if ($record = mysql_fetch_assoc($qid)) {
456            return $record;
457        } else {
458            return false;
459        }
460    }
461
462
463} // End of class.
464?>
Note: See TracBrowser for help on using the repository browser.