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

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

Many minor fixes during pulso development

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