* 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();
?>
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.