source: trunk/lib/RecordVersion.inc.php @ 1

Last change on this file since 1 was 1, checked in by scdev, 19 years ago

Initial import.

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