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

Last change on this file since 685 was 685, checked in by anonymous, 5 years ago

Various minor changes:

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