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

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

Tons of little updates and bugfixes. CSS updates to templates and core css files. File upload ability to module_maker. Remade Upload interface to use setParam/getParam.

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 (!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    {
57        if (!is_a($auth_object, 'Auth_SQL')) {
58            trigger_error('Constructor not provided a valid Auth_SQL object.', E_USER_ERROR);
59        }
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.