* Copyright 2001-2012 Strangecode, LLC * * This file is part of The Strangecode Codebase. * * The Strangecode Codebase is free software: you can redistribute it and/or * modify it under the terms of the GNU General Public License as published by the * Free Software Foundation, either version 3 of the License, or (at your option) * any later version. * * The Strangecode Codebase is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * The Strangecode Codebase. If not, see . */ /** * Lock.inc.php * * The Lock class provides a system for locking abstract DB rows. * * @author Quinn Comendant * @version 2.1 */ class Lock { // A place to keep an object instance for the singleton pattern. protected static $instance = null; // Configuration of this object. protected $_params = array( // The time required to pass before a user can forcibly unlock a locked record. 'timeout' => 600, // The time after which a record will automatically become unlocked. 'auto_timeout' => 1800, // The URL to the lock script. 'error_url' => '/lock.php', // The name of the database table to store locks. 'db_table' => 'lock_tbl', // Automatically create table and verify columns. Better set to false after site launch. // This value is overwritten by the $app->getParam('db_create_tables') setting if it is available. 'create_table' => true, ); // Store lock data from DB. protected $data = array(); // Auth_SQL object from which to access a current user_id. protected $_auth = null; /** * This method enforces the singleton pattern for this class. * * @return object Reference to the global Lock object. * @access public * @static */ public static function &getInstance($auth_object=null) { if (self::$instance === null) { self::$instance = new self($auth_object); } return self::$instance; } /** * Constructor. Pass an Auth object on which to perform user lookups. * * @param mixed $auth_object An Auth_SQL or Auth_FILE object. */ public function __construct($auth_object=null) { $app =& App::getInstance(); if (!is_null($auth_object) || is_null($this->_auth)) { if (!method_exists($auth_object, 'get') || !method_exists($auth_object, 'getUsername')) { trigger_error('Constructor not provided a valid Auth_* object.', E_USER_ERROR); } $this->_auth = $auth_object; } // Get create tables config from global context. if (!is_null($app->getParam('db_create_tables'))) { $this->setParam(array('create_table' => $app->getParam('db_create_tables'))); } } /** * Setup the database table for this class. * * @access public * @author Quinn Comendant * @since 26 Aug 2005 17:09:36 */ public function initDB($recreate_db=false) { $app =& App::getInstance(); $db =& DB::getInstance(); static $_db_tested = false; if ($recreate_db || !$_db_tested && $this->getParam('create_table')) { if ($recreate_db) { $db->query("DROP TABLE IF EXISTS " . $this->getParam('db_table')); $app->logMsg(sprintf('Dropping and recreating table %s.', $this->getParam('db_table')), LOG_INFO, __FILE__, __LINE__); } $db->query(sprintf("CREATE TABLE IF NOT EXISTS %s ( lock_id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, record_table varchar(255) NOT NULL default '', record_key varchar(255) NOT NULL default '', record_val varchar(255) NOT NULL default '', title varchar(255) NOT NULL default '', set_by_admin_id smallint(11) NOT NULL default '0', lock_datetime datetime NOT NULL default '%s 00:00:00', KEY record_table (record_table), KEY record_key (record_key), KEY record_val (record_val) )", $db->escapeString($this->getParam('db_table')), $db->getParam('zero_date'))); if (!$db->columnExists($this->getParam('db_table'), array( 'lock_id', 'record_table', 'record_key', 'record_val', 'title', 'set_by_admin_id', 'lock_datetime', ), false, false)) { $app->logMsg(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), LOG_ALERT, __FILE__, __LINE__); trigger_error(sprintf('Database table %s has invalid columns. Please update this table manually.', $this->getParam('db_table')), E_USER_ERROR); } } $_db_tested = true; } /** * Set the params of this object. * * @param array $params Array of param keys and values to set. */ public function setParam($params=null) { if (isset($params) && is_array($params)) { // Merge new parameters with old overriding only those passed. $this->_params = array_merge($this->_params, $params); } } /** * Return the value of a parameter, if it exists. * * @access public * @param string $param Which parameter to return. * @return mixed Configured parameter value. */ public function getParam($param) { $app =& App::getInstance(); if (array_key_exists($param, $this->_params)) { return $this->_params[$param]; } else { $app->logMsg(sprintf('Parameter is not set: %s', $param), LOG_DEBUG, __FILE__, __LINE__); return null; } } /** * Select the lock to manipulate. * * @param mixed $record_table_or_lock_id The table containing the record to lock, * or a numeric lock_id. * @param string $record_key The key column for the record to lock. * @param string $record_val The value of the key column for the record to lock. */ public function select($record_table_or_lock_id, $record_key=null, $record_val=null) { $app =& App::getInstance(); $db =& DB::getInstance(); $this->initDB(); // Expire old locks. $this->_auto_timeout(); if (is_numeric($record_table_or_lock_id) && !isset($record_key) && !isset($record_val)) { // Get lock data by lock_id. $qid = $db->query(" SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . " WHERE lock_id = '" . $db->escapeString($record_table_or_lock_id) . "' "); } else { // Get lock data by record specs $qid = $db->query(" SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . " WHERE record_table = '" . $db->escapeString($record_table_or_lock_id) . "' AND record_key = '" . $db->escapeString($record_key) . "' AND record_val = '" . $db->escapeString($record_val) . "' "); } if ($this->data = mysql_fetch_assoc($qid)) { $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__); // FIXME: What if admin set lock, but public user is current lock user? $this->data['editor'] = $this->_auth->getUsername($this->data['set_by_admin_id']); return true; } else { return false; } } /** * Returns true if the record we instantiated with is locked. * * @return bool True if locked. */ public function isLocked() { return isset($this->data['lock_id']); } /** * Returns the status of who set the lock. Use this to ignore locks set by * the current user. * * @return bool True if current user set the lock. */ public function isMine() { $db =& DB::getInstance(); $this->initDB(); if ($this->isLocked()) { $qid = $db->query("SELECT * FROM " . $db->escapeString($this->getParam('db_table')) . " WHERE lock_id = '" . $db->escapeString($this->data['lock_id']) . "'"); if ($lock = mysql_fetch_assoc($qid)) { return ($lock['set_by_admin_id'] == $this->_auth->get('user_id')); } else { return false; } } else { return false; } } /** * Create a new lock for the specified table/key/value. * * @param string $record_table The table containing the record to lock. * @param string $record_key The key column for the record to lock. * @param string $record_val The value of the key column for the record to lock. * @param string $title A title to apply to the lock, for display purposes. * * @return int The id for the lock (mysql last insert id). */ public function set($record_table, $record_key, $record_val, $title='') { $app =& App::getInstance(); $db =& DB::getInstance(); $this->initDB(); if ($this->_auth->get('user_id') == '' || filter_var($this->_auth->get('user_id'), FILTER_VALIDATE_INT) === false) { $app->logMsg(sprintf("auth->get('user_id') returns a non-integer: %s", $this->_auth->get('user_id')), LOG_ERR, __FILE__, __LINE__); return false; } // Expire old locks. $this->_auto_timeout(); // Remove previous locks if exist. Is this better than using a REPLACE INTO? $db->query(" DELETE FROM " . $db->escapeString($this->getParam('db_table')) . " WHERE record_table = '" . $db->escapeString($record_table) . "' AND record_key = '" . $db->escapeString($record_key) . "' AND record_val = '" . $db->escapeString($record_val) . "' "); // Set new lock. $db->query(" INSERT INTO " . $db->escapeString($this->getParam('db_table')) . " ( record_table, record_key, record_val, title, set_by_admin_id, lock_datetime ) VALUES ( '" . $db->escapeString($record_table) . "', '" . $db->escapeString($record_key) . "', '" . $db->escapeString($record_val) . "', '" . $db->escapeString($title) . "', '" . $db->escapeString($this->_auth->get('user_id')) . "', NOW() ) "); $lock_id = mysql_insert_id($db->getDBH()); // Must register this locked record as the current. $this->select($lock_id); return $lock_id; } /** * Unlock the currently selected record. */ public function remove() { $app =& App::getInstance(); $db =& DB::getInstance(); $this->initDB(); // Expire old locks. $this->_auto_timeout(); if (!$this->isLocked()) { // No lock selected. return false; } // Delete a specific lock. $db->query(" DELETE FROM " . $db->escapeString($this->getParam('db_table')) . " WHERE lock_id = '" . $db->escapeString($this->data['lock_id']) . "' "); $app->logMsg(sprintf('Removing lock: %s', $this->data['lock_id']), LOG_DEBUG, __FILE__, __LINE__); return true; } /** * Unlock all records, or all records for a specified user. */ public function removeAll($user_id=null) { $app =& App::getInstance(); $db =& DB::getInstance(); $this->initDB(); // Expire old locks. $this->_auto_timeout(); if (isset($user_id)) { // Delete specific user's locks. $db->query("DELETE FROM " . $db->escapeString($this->getParam('db_table')) . " WHERE set_by_admin_id = '" . $db->escapeString($user_id) . "'"); $app->logMsg(sprintf('Record locks owned by user %s have been deleted', $this->_auth->getUsername($user_id)), LOG_DEBUG, __FILE__, __LINE__); } else { // Delete ALL locks. $db->query("DELETE FROM " . $db->escapeString($this->getParam('db_table')) . ""); $app->logMsg(sprintf('All record locks deleted by user %s', $this->_auth->get('username')), LOG_DEBUG, __FILE__, __LINE__); } } /** * Deletes all locks that are older than auto_timeout. */ public function _auto_timeout() { $db =& DB::getInstance(); static $_timeout_run = false; $this->initDB(); if (!$_timeout_run) { // Delete all old locks. $db->query(" DELETE FROM " . $db->escapeString($this->getParam('db_table')) . " WHERE DATE_ADD(lock_datetime, INTERVAL '" . $db->escapeString($this->getParam('auto_timeout')) . "' SECOND) < NOW() "); $_timeout_run = true; } } /** * Redirect to record lock error page. */ public function dieErrorPage() { $app =& App::getInstance(); $app->dieURL($this->getParam('error_url'), array('lock_id' => $this->data['lock_id'], 'boomerang' => urlencode(getenv('REQUEST_URI')))); } /** * Print error page. This method is probably not used anywhere; instead, we're including this via the template codebase/services/templates/lock.ihtml */ public function printErrorHTML() { $app =& App::getInstance(); ?>
printHiddenSession() ?>
%s is currently being edited by %s (%d minutes elapsed). You cannot modify this database record while it is locked. Please try again later."), ucfirst($this->getTitle()), $this->getEditor(), date('i', $this->getSecondsElapsed() + 60) ); ?>
getSecondsElapsed() >= $this->getParam('timeout')) { ?>
getEditor()) ?>
" /> " />
" />
data['lock_id']; } /** * Return title of locked record. */ public function getTitle() { return $this->data['title']; } /** * Return administrator username for locked record. */ public function getEditor() { return $this->data['editor']; } /** * Return total seconds since the record was locked. */ public function getSecondsElapsed() { if (isset($this->data['lock_datetime']) && false !== ($lock_timestamp = strtotime($this->data['lock_datetime'])) && $lock_timestamp < time()) { return time() - $lock_timestamp; } else { return 0; } } } // End of class.