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

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

Rebuilt the services/admins.php script and templates. Fixes since v2 conversion. Lots of bugs and more to come!

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