source: trunk/lib/Lock.inc.php @ 523

Last change on this file since 523 was 523, checked in by anonymous, 9 years ago

First set of changes towards 2.2.0. Improved functinoality with integration in wordpress; bugs fixed.

File size: 15.8 KB
Line 
1<?php
2/**
3 * The Strangecode Codebase - a general application development framework for PHP
4 * For details visit the project site: <http://trac.strangecode.com/codebase/>
5 * Copyright 2001-2012 Strangecode, LLC
6 *
7 * This file is part of The Strangecode Codebase.
8 *
9 * The Strangecode Codebase is free software: you can redistribute it and/or
10 * modify it under the terms of the GNU General Public License as published by the
11 * Free Software Foundation, either version 3 of the License, or (at your option)
12 * any later version.
13 *
14 * The Strangecode Codebase is distributed in the hope that it will be useful, but
15 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
17 * details.
18 *
19 * You should have received a copy of the GNU General Public License along with
20 * The Strangecode Codebase. If not, see <http://www.gnu.org/licenses/>.
21 */
22
23/**
24 * Lock.inc.php
25 *
26 * The Lock class provides a system for locking abstract DB rows.
27 *
28 * @author  Quinn Comendant <quinn@strangecode.com>
29 * @version 2.1
30 */
31class Lock
32{
33    // A place to keep an object instance for the singleton pattern.
34    protected static $instance = null;
35
36    // Configuration of this object.
37    protected $_params = array(
38        'timeout' => 600,
39        'auto_timeout' => 1800,
40        'error_url' => '/lock.php',
41        'db_table' => 'lock_tbl',
42
43        // Automatically create table and verify columns. Better set to false after site launch.
44        // This value is overwritten by the $app->getParam('db_create_tables') setting if it is available.
45        'create_table' => true,
46    );
47
48    // Store lock data from DB.
49    protected $data = array();
50
51    // Auth_SQL object from which to access a current user_id.
52    protected $_auth = null;
53
54    /**
55     * This method enforces the singleton pattern for this class.
56     *
57     * @return  object  Reference to the global Lock object.
58     * @access  public
59     * @static
60     */
61    public static function &getInstance($auth_object=null)
62    {
63        if (self::$instance === null) {
64            self::$instance = new self($auth_object);
65        }
66
67        return self::$instance;
68    }
69
70    /**
71     * Constructor. Pass an Auth object on which to perform user lookups.
72     *
73     * @param mixed  $auth_object  An Auth_SQL or Auth_FILE object.
74     */
75    public function __construct($auth_object=null)
76    {
77        $app =& App::getInstance();
78
79        if (!is_null($auth_object) || is_null($this->_auth)) {
80            if (!method_exists($auth_object, 'get') || !method_exists($auth_object, 'getUsername')) {
81                trigger_error('Constructor not provided a valid Auth_* object.', E_USER_ERROR);
82            }
83
84            $this->_auth = $auth_object;
85        }
86
87        // Get create tables config from global context.
88        if (!is_null($app->getParam('db_create_tables'))) {
89            $this->setParam(array('create_table' => $app->getParam('db_create_tables')));
90        }
91    }
92
93    /**
94     * Setup the database table for this class.
95     *
96     * @access  public
97     * @author  Quinn Comendant <quinn@strangecode.com>
98     * @since   26 Aug 2005 17:09:36
99     */
100    public function initDB($recreate_db=false)
101    {
102        $app =& App::getInstance();
103        $db =& DB::getInstance();
104
105        static $_db_tested = false;
106
107        if ($recreate_db || !$_db_tested && $this->getParam('create_table')) {
108            if ($recreate_db) {
109                $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table'));
110                $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__);
111            }
112            $db->query("CREATE TABLE IF NOT EXISTS " . $db->escapeString($this->getParam('db_table')) . " (
113                lock_id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
114                record_table varchar(255) NOT NULL default '',
115                record_key varchar(255) NOT NULL default '',
116                record_val varchar(255) NOT NULL default '',
117                title varchar(255) NOT NULL default '',
118                set_by_admin_id smallint(11) NOT NULL default '0',
119                lock_datetime datetime NOT NULL default '0000-00-00 00:00:00',
120                KEY record_table (record_table),
121                KEY record_key (record_key),
122                KEY record_val (record_val)
123            )");
124
125            if (!$db->columnExists($this->getParam('db_table'), array(
126                'lock_id',
127                'record_table',
128                'record_key',
129                'record_val',
130                'title',
131                'set_by_admin_id',
132                'lock_datetime',
133            ), false, false)) {
134                $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__);
135                trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR);
136            }
137        }
138        $_db_tested = true;
139    }
140
141    /**
142     * Set the params of this object.
143     *
144     * @param  array $params   Array of param keys and values to set.
145     */
146    public function setParam($params=null)
147    {
148        if (isset($params) && is_array($params)) {
149            // Merge new parameters with old overriding only those passed.
150            $this->_params = array_merge($this->_params, $params);
151        }
152    }
153
154    /**
155     * Return the value of a parameter, if it exists.
156     *
157     * @access public
158     * @param string $param        Which parameter to return.
159     * @return mixed               Configured parameter value.
160     */
161    public function getParam($param)
162    {
163        $app =& App::getInstance();
164
165        if (array_key_exists($param, $this->_params)) {
166            return $this->_params[$param];
167        } else {
168            $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__);
169            return null;
170        }
171    }
172
173    /**
174     * Select the lock to manipulate.
175     *
176     * @param mixed  $record_table_or_lock_id  The table containing the record to lock,
177     *                                         or a numeric lock_id.
178     * @param string $record_key  The key column for the record to lock.
179     * @param string $record_val  The value of the key column for the record to lock.
180     * @param string $title       A title to apply to the lock, for display purposes.
181     */
182    public function select($record_table_or_lock_id, $record_key=null, $record_val=null)
183    {
184        $app =& App::getInstance();
185        $db =& DB::getInstance();
186
187        $this->initDB();
188
189        // Expire old locks.
190        $this->_auto_timeout();
191
192        if (is_numeric($record_table_or_lock_id) && !isset($record_key) && !isset($record_val)) {
193            // Get lock data by lock_id.
194            $qid = $db->query("
195                SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . "
196                WHERE lock_id = '" . $db->escapeString($record_table_or_lock_id) . "'
197            ");
198        } else {
199            // Get lock data by record specs
200            $qid = $db->query("
201                SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . "
202                WHERE record_table = '" . $db->escapeString($record_table_or_lock_id) . "'
203                AND record_key = '" . $db->escapeString($record_key) . "'
204                AND record_val = '" . $db->escapeString($record_val) . "'
205            ");
206        }
207        if ($this->data = mysql_fetch_assoc($qid)) {
208            $app->logMsg(sprintf('Selecting %slocked record: %s %s %s', ($this->data['set_by_admin_id'] == $this->_auth->get('user_id') ? 'self-' : ''), $record_table_or_lock_id, $record_key, $record_val), LOG_DEBUG, __FILE__, __LINE__);
209            // FIXME: What if admin set lock, but public user is current lock user?
210            $this->data['editor'] = $this->_auth->getUsername($this->data['set_by_admin_id']);
211            return true;
212        } else {
213            $app->logMsg(sprintf('No locked record: %s %s %s', $record_table_or_lock_id, $record_key, $record_val), LOG_DEBUG, __FILE__, __LINE__);
214            return false;
215        }
216    }
217
218    /**
219     * Returns true if the record we instantiated with is locked.
220     *
221     * @return bool            True if locked.
222     */
223    public function isLocked()
224    {
225        return isset($this->data['lock_id']);
226    }
227
228    /**
229     * Returns the status of who set the lock. Use this to ignore locks set by
230     * the current user.
231     *
232     * @return bool            True if current user set the lock.
233     */
234    public function isMine()
235    {
236        $db =& DB::getInstance();
237
238        $this->initDB();
239
240        if (isset($this->data['lock_id'])) {
241            $qid = $db->query("SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . " WHERE lock_id = '" . $db->escapeString($this->data['lock_id']) . "'");
242            if ($lock = mysql_fetch_assoc($qid)) {
243                return ($lock['set_by_admin_id'] == $this->_auth->get('user_id'));
244            } else {
245                return false;
246            }
247        } else {
248            return false;
249        }
250    }
251
252    /**
253     * Create a new lock for the specified table/key/value.
254     *
255     * @param string $record_table  The table containing the record to lock.
256     * @param string $record_key  The key column for the record to lock.
257     * @param string $record_val  The value of the key column for the record to lock.
258     * @param string $title       A title to apply to the lock, for display purposes.
259     *
260     * @return int            The id for the lock (mysql last insert id).
261     */
262    public function set($record_table, $record_key, $record_val, $title='')
263    {
264        $db =& DB::getInstance();
265
266        $this->initDB();
267
268        // Expire old locks.
269        $this->_auto_timeout();
270
271        // Remove previous locks if exist. Is this better than using a REPLACE INTO?
272        $db->query("
273            DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
274            WHERE record_table = '" . $db->escapeString($record_table) . "'
275            AND record_key = '" . $db->escapeString($record_key) . "'
276            AND record_val = '" . $db->escapeString($record_val) . "'
277        ");
278
279        // Set new lock.
280        $db->query("
281            INSERT INTO " . $db->escapeString($this->getParam('db_table')) . " (
282                record_table,
283                record_key,
284                record_val,
285                title,
286                set_by_admin_id,
287                lock_datetime
288            ) VALUES (
289                '" . $db->escapeString($record_table) . "',
290                '" . $db->escapeString($record_key) . "',
291                '" . $db->escapeString($record_val) . "',
292                '" . $db->escapeString($title) . "',
293                '" . $db->escapeString($this->_auth->get('user_id')) . "',
294                NOW()
295            )
296        ");
297        $lock_id = mysql_insert_id($db->getDBH());
298
299        // Must register this locked record as the current.
300        $this->select($lock_id);
301
302        return $lock_id;
303    }
304
305    /**
306     * Unlock the currently selected record.
307     */
308    public function remove()
309    {
310        $app =& App::getInstance();
311        $db =& DB::getInstance();
312
313        $this->initDB();
314
315        // Expire old locks.
316        $this->_auto_timeout();
317
318        // Delete a specific lock.
319        $db->query("
320            DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
321            WHERE lock_id = '" . $db->escapeString($this->data['lock_id']) . "'
322        ");
323
324        $app->logMsg(sprintf('Removing lock: %s', $this->data['lock_id']), LOG_DEBUG, __FILE__, __LINE__);
325    }
326
327    /**
328     * Unlock all records, or all records for a specified user.
329     */
330    public function removeAll($user_id=null)
331    {
332        $app =& App::getInstance();
333        $db =& DB::getInstance();
334
335        $this->initDB();
336
337        // Expire old locks.
338        $this->_auto_timeout();
339
340        if (isset($user_id)) {
341            // Delete specific user's locks.
342            $db->query("DELETE FROM " . $db->escapeString($this->getParam('db_table')) . " WHERE set_by_admin_id = '" . $db->escapeString($user_id) . "'");
343            $app->logMsg(sprintf('Record locks owned by user %s have been deleted', $this->_auth->getUsername($user_id)), LOG_DEBUG, __FILE__, __LINE__);
344        } else {
345            // Delete ALL locks.
346            $db->query("DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "");
347            $app->logMsg(sprintf('All record locks deleted by user %s', $this->_auth->get('username')), LOG_DEBUG, __FILE__, __LINE__);
348        }
349    }
350
351    /**
352     * Deletes all locks that are older than auto_timeout.
353     */
354    public function _auto_timeout()
355    {
356        $db =& DB::getInstance();
357
358        static $_timeout_run = false;
359
360        $this->initDB();
361
362        if (!$_timeout_run) {
363            // Delete all old locks.
364            $db->query("
365                DELETE FROM " . $db->escapeString($this->getParam('db_table')) . "
366                WHERE DATE_ADD(lock_datetime, INTERVAL '" . $this->getParam('auto_timeout') . "' SECOND) < NOW()
367            ");
368            $_timeout_run = true;
369        }
370    }
371
372    /**
373     * Redirect to record lock error page.
374     */
375    public function dieErrorPage()
376    {
377        $app =& App::getInstance();
378
379        $app->dieURL(sprintf('%s?lock_id=%s&boomerang=%s', $this->getParam('error_url'), $this->data['lock_id'], urlencode(absoluteMe())));
380    }
381
382    /**
383     * Print error page.
384     */
385    public function printErrorHTML()
386    {
387        $app =& App::getInstance();
388        ?>
389        <form method="post" action="<?php echo oTxt($_SERVER['PHP_SELF']); ?>">
390            <?php $app->printHiddenSession() ?>
391            <input type="hidden" name="lock_id" value="<?php echo $this->getID(); ?>" />
392            <div id="sc-msg" class="sc-msg">
393                <div class="sc-msg-notice">
394                <?php printf(_("The record %s is currently being edited by %s (%d minutes elapsed). You cannot modify the record while it is locked by another user."),
395                    $this->getTitle(),
396                    $this->getEditor(),
397                    date('i', $this->getSecondsElapsed() + 60)
398                ); ?>
399                </div>
400                <?php if ($this->getSecondsElapsed() >= $this->getParam('timeout')) { ?>
401                    <div class="sc-msg-notice"><?php printf(_("You can forcibly unlock the record if you believe the editing session has expired. You might want to confirm with %s before doing this."), $this->getEditor()) ?></div>
402                    <div class="sc-msg-notice">
403                        <input type="submit" name="unlock" value="<?php echo _("Unlock"); ?>" />
404                        <input type="submit" name="cancel" value="<?php echo _("Cancel"); ?>" />
405                    </div>
406                <?php } else { ?>
407                    <div class="sc-msg-notice">
408                        <input type="submit" name="cancel" value="<?php echo _("Cancel"); ?>" />
409                    </div>
410                <?php } ?>
411            </div>
412        </form>
413        <?php
414    }
415
416    /**
417     * Return lock_id of locked record.
418     */
419    public function getID()
420    {
421        return $this->data['lock_id'];
422    }
423
424    /**
425     * Return title of locked record.
426     */
427    public function getTitle()
428    {
429        return $this->data['title'];
430    }
431
432    /**
433     * Return administrator username for locked record.
434     */
435    public function getEditor()
436    {
437        return $this->data['editor'];
438    }
439
440    /**
441     * Return total seconds since the record was locked.
442     */
443    public function getSecondsElapsed()
444    {
445        if (isset($this->data['lock_datetime']) && strtotime($this->data['lock_datetime']) < time()) {
446            return time() - strtotime($this->data['lock_datetime']);
447        } else {
448            return 0;
449        }
450    }
451
452
453} // End of class.
Note: See TracBrowser for help on using the repository browser.