source: branches/2.0singleton/lib/RecordVersion.inc.php @ 130

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

finished updating DB:: to $db->

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