--- database/database.mssql.orig 2004-03-01 00:19:03.189736229 -0500 +++ database/database.mssql 2004-03-01 00:29:49.239109140 -0500 @@ -140,6 +140,15 @@ ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO +CREATE TABLE [dbo].[locks] ( + [name] [varchar] (64) NOT NULL , + [microtime] [varchar] (64) NOT NULL , + [ttl] [int] NOT NULL , + [status] [smallint] NOT NULL , + [quantity] [int] NOT NULL , +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO + CREATE TABLE [dbo].[locales] ( [lid] [int] NULL , [location] [varchar] (128) NOT NULL , --- database/database.mysql.orig 2004-03-01 00:18:48.431585658 -0500 +++ database/database.mysql 2004-03-01 00:20:38.656048904 -0500 @@ -236,6 +236,22 @@ ) TYPE=MyISAM; -- +-- Table structure for table 'locks' +-- + +CREATE TABLE locks ( + name varchar(64) NOT NULL default '', + microtime varchar(64) NOT NULL default '', + ttl int NOT NULL default 0, + status tinyint(1) unsigned NOT NULL default 0, + quantity int(10) unsigned NOT NULL default 0, + PRIMARY KEY (name), + KEY (microtime), + KEY (ttl), + KEY (status), + KEY (quantity) +) TYPE=MyISAM; +-- -- Table structure for table 'moderation_filters' -- --- database/database.pgsql.orig 2004-03-01 00:18:55.904149627 -0500 +++ database/database.pgsql 2004-03-01 00:30:05.682997267 -0500 @@ -216,6 +216,23 @@ ); -- +-- Table structure for locks +-- + +CREATE TABLE locks ( + name varchar(64) NOT NULL default '', + microtime varchar(64) NOT NULL default '', + ttl int NOT NULL default 0, + status smallint unsigned NOT NULL default 0, + quantity int unsigned NOT NULL default 0, + PRIMARY KEY (name) +}; +CREATE INDEX locks_microtime ON moderation_roles(microtime); +CREATE INDEX locks_ttl ON moderation_roles(ttl); +CREATE INDEX locks_status ON moderation_roles(status); +CREATE INDEX locks_quantity ON moderation_roles(quantity); + +-- -- Table structure for locales -- --- database/updates.inc.orig 2004-03-01 00:09:09.800409029 -0500 +++ database/updates.inc 2004-03-01 00:58:43.497994169 -0500 @@ -50,7 +50,8 @@ "2004-01-11" => "update_76", "2004-01-13" => "update_77", "2004-02-03" => "update_78", - "2004-02-21" => "update_79" + "2004-02-21" => "update_79", + "2004-03-01" => "update_80" ); function update_32() { @@ -760,6 +761,39 @@ return $ret; } +function update_80() { + $ret = array(); + if ($GLOBALS["db_type"] == "pgsql") { + $ret[] = update_sql("CREATE TABLE {locks} ( + name varchar(64) NOT NULL default '', + microtime varchar(64) NOT NULL default '', + ttl int NOT NULL default 0, + status smallint unsigned NOT NULL default 0, + quantity int unsigned NOT NULL default 0, + PRIMARY KEY (name), + )"); + $ret[] = update_sql("CREATE INDEX lock_microtime ON book(microtime);"); + $ret[] = update_sql("CREATE INDEX lock_ttl ON book(ttl);"); + $ret[] = update_sql("CREATE INDEX lock_status ON book(status);"); + $ret[] = update_sql("CREATE INDEX lock_quantity ON book(quantity);"); + } + else { + $ret[] = update_sql("CREATE TABLE {locks} ( + name varchar(64) NOT NULL default '', + microtime varchar(64) NOT NULL default '', + ttl int NOT NULL default 0, + status tinyint(1) unsigned NOT NULL default 0, + quantity int unsigned NOT NULL default 0, + PRIMARY KEY (name), + KEY (microtime), + KEY (ttl), + KEY (status), + KEY (quantity) + )"); + } + return $ret; +} + function update_sql($sql) { $edit = $_POST["edit"]; $result = db_query($sql); --- includes/common.inc.orig 2004-03-01 00:06:28.140053607 -0500 +++ includes/common.inc 2004-03-01 00:06:40.942077945 -0500 @@ -1236,6 +1236,7 @@ function drupal_xml_parser_create(&$data include_once "includes/xmlrpc.inc"; include_once "includes/tablesort.inc"; include_once "includes/file.inc"; +include_once "includes/lock.inc"; // set error handler: set_error_handler("error_handler"); --- includes/lock.inc.orig 1969-12-31 19:00:00.000000000 -0500 +++ includes/lock.inc 2004-03-01 00:06:40.942077945 -0500 @@ -0,0 +1,286 @@ +ttl) { + $ttl = $attributes->ttl; + } + else { + /* We were not explicitly told how long a lock is valid, so we're going to + ** default to a relatively sane value of 15 seconds. Depending on the + ** web application, this could be way too long, or way too short... + */ + $ttl = 15; + } + if ($attributes->timeout) { + // blocking attempt to obtain a lock will timeout after this many seconds + $timeout = $attributes->timeout; + } + else { + // we'll never timeout... + $timeout = 0; + } + // initialize the loop counter, used to optionally timeout a lock attempt + $loop = 0; + + // we enter a loop to try and obtain the lock + while (1) { + /* The first step in obtaining a lock is checking to see if it is already + ** held by another process. Even if the lock exists, it may be stale + ** in which case we need to unlock it. In other words, we can't skip this + ** step. + **/ + if (!lock_db_is_locked($name, $attributes)) { + /* At this moment, we know that the lock is available, so we're going to + ** try and grab it... + */ + $key = microtime(); + if(@db_query("INSERT INTO {locks} VALUES('%s', '%s', %d, 1, 1)", $name, $key, $ttl)) { + /* The presence of a return code means that the database did not + ** return an error. In other words, we own the lock. + */ + return $key; + } + else { + /* The absence of a return code means the database returned an error. + ** In other words, we failed to get the lock. + */ + if (!$block) { + /* We were not told to block, so we need to exit reporting that we + ** failed to grab the lock. + */ + return 0; + } + /* If we got here, we failed to grab the lock, but we were told to + ** block so we're going to sleep a moment and try again. + */ + } + } + if (!$block) { + /* When checking the lock, we learned someone else already has it. We + ** were not told to block, so we need to exit reporting that we failed + ** to grab the lock. + */ + return 0; + } + /* If we got here, we're still trying to grab the lock. Let's sleep for + ** second then we'll loop around and try again. + */ + if (($timeout) && ($loop >= $timeout)) { + // We failed to obtain our lock in the allowed time. + return 0; + } + $loop++; + sleep(1); + } +} + +/** + * This is the default db_renew function. If the lock still exists in the + * database, we update it and issue a new key. + * + * @param string $name the name of the lock + * @param int $key the key for the lock + * @param object $attributes optional + * + * @return int # = new key to the lock we own; 0 = we lost the lock + */ +function lock_db_renew($name, $key, $attributes) { + if ($attributes->ttl) { + $ttl = $attributes->ttl; + } + else { + $ttl = 15; + } + $newkey = microtime(); + db_query("UPDATE {locks} SET microtime = '%s', ttl = %d WHERE name = '%s' and microtime = '%s'", $newkey, $ttl, $name, $key); + // Verify that we actually updated a database row. + if (db_affected_rows() == 1) { + // Great, our lock is still there, and we've successfully renewed it. + return ($newkey); + } + // Our lock is gone, we can't renew it. + return 0; +} + +/** + * This is the default db is_locked function. It checks if the named lock + * is locked or not. It is very unlikely that you will ever need to call this + * function directly. + * You can pass in the following attributes: + * + * @param string $name the name of the lock + * @param object $attributes optional, defined above + * + * @return int 1 = the lock is locked; 0 = the lock is not locked + */ +function lock_db_is_locked($name, $attributes) { + // See if the lock exists in the database. + $lock = db_fetch_object(db_query("SELECT status,microtime,ttl FROM {locks} WHERE name = '%s'", $name)); + if($lock->status == 1) { + // The lock is in the database, but it may be stale... + list($cur_micro, $cur_sec) = explode(" ", microtime()); + list($old_micro, $old_sec) = explode(" ", $lock->microtime); + $old_sec = $old_sec + $lock->ttl; + if (($cur_sec > $old_sec) || ($cur_sec == $old_sec && $cur_micro > $old_micro)) { + /* The lock is stale, meaning that it's been in the database too long. + ** Pass in the unique tampstamp of the lock, and try to unlock it. + */ + $key = $lock->microtime; + lock_db_unlock($name, $key, $attributes); + return 0; + } + return 1; + } + // the lock is not currently held + return 0; +} + +/** + * This is the default db unlock function. It will attempt to unlock the named + * lock. + * It does not recognize any attributes at this time. + * + * @param string $name the name of the lock + * @param int $key the key to unlock (creation timestamp) + * @param object $attributes optional, defined above + * + * @return int 1 = the lock was unlocked; 0 = the lock was not unlocked + */ +function lock_db_unlock($name, $key, $attributes) { + /* Unlocking a lock is as simple as deleting it. (We want to do that rather + ** than simply changing the status as this logic is built around the theory + ** that you can only have one lock in the table by the same name.) + ** Note: We must delay the deletion of a lock to be sure whatever logic + ** required the lock in the first place is finished. Hence the sleep before + ** the delete. + */ + db_query("DELETE FROM {locks} WHERE name = '%s' and microtime = '%s'", $name, $key); + if (db_error()) { + /* Our delete failed. Why? Probably because someone has already freed the + ** lock. In other words, it doesn't matter. But we'll leave it up to + ** whomever is actually using the lock to decide if this matters. We'll + ** return a 0 to say that we failed to unlock the lock. Most likely this + ** will be ignored. + */ + return 0; + } + // We succesfully unlocked the lock. + return 1; +} --- modules/node.module.orig 2004-03-01 00:06:18.985469057 -0500 +++ modules/node.module 2004-03-01 00:06:40.935079024 -0500 @@ -577,6 +577,12 @@ function node_settings() { $output .= form_select(t('Length of trimmed posts'), 'teaser_length', variable_get('teaser_length', 600), array(0 => t('Unlimited'), 200 => t('200 characters'), 400 => t('400 characters'), 600 => t('600 characters'), 800 => t('800 characters'), 1000 => t('1000 characters'), 1200 => t('1200 characters'), 1400 => t('1400 characters'), 1600 => t('1600 characters'), 1800 => t('1800 characters'), 2000 => t('2000 characters')), t("The maximum number of characters used in the trimmed version of a post. Drupal will use this setting to determine at which offset long posts should be trimmed. The trimmed version of a post is typically used as a teaser when displaying the post on the main page, in XML feeds, etc. To disable teasers, set to 'Unlimited'. Note that this setting will only affect new or updated content and will not affect existing teasers.")); $output .= form_radios(t('Preview post'), 'node_preview', variable_get('node_preview', 0), array(t('Optional'), t('Required')), t('Must users preview posts before submitting?')); + // locking: + // manually formatting time array so the seconds stay seconds... + $duration = array(0 => t("disabled"), 30 => t("30 sec"), 60 => t("1 min"), 120 => t("2 min"), 180 => t("3 min"), 240 => t("4 min"), 300 => t("5 min"), 600 => t("10 min"), 900 => t("15 min"), 1800 => t("30 min"), 3600 => t("1 hour")); + $group = form_select(t("Content locking"), "node_lock", variable_get("node_lock", 0), $duration, t("Content locking allows only one administrator at a time to modify any given piece of content. To enable, select how long an administrator can hold a lock on content. If the administrator takes longer than this amount of time to make changes to content, another administrator is allowed to come along and grab the lock, thereby preventing the first from saving changes.")); + $output .= form_group(t("Lock settings"), $group); + return $output; } @@ -645,7 +651,76 @@ function node_admin_edit($node) { $output .= implode("\n", module_invoke_all('node_link', $node)); return $output; +} +function node_lock($nid, $lock_type) { + if($lock->ttl = variable_get("node_lock", 0)) { + $name="lock_$nid"; + + switch ($lock_type) { + case "lock": + // get the lock and store the key in a session variable + if ($key = lock_lock($name, 0, $lock)) { + $_SESSION[$name] = $key; + } + else { + return 0; + } + break; + case "renew": + if($_SESSION[$name]) { + if($key = lock_renew($name, $_SESSION[$name], $lock)) { + // we still own the lock + $_SESSION[$name] = $key; + } + else { + // we don't own the lock + unset($_SESSION[$name]); + return 0; + } + } + // we can only try and renew if we have a key + else { + return 0; + } + break; + case "edit": + // if we already have a key, try and renew it + if ($_SESSION[$name]) { + // if we fail to renew, the key will be erased + node_lock($nid, "renew"); + } + // if we don't have a key, try and get one + if(!$_SESSION[$name] && !node_lock($nid, "lock")) { + return 0; + } + break; + case "preview": + // if we already have a key, try and renew it + if($_SESSION[$name] && !node_lock($nid, "renew")) { + return 0; + } + // if we don't have a key, we shouldn't be here + elseif (!$_SESSION[$name]) { + return 0; + } + break; + case "submit": + case "delete": + // verify that our key is still valid + if ($_SESSION[$name] && !node_lock($nid, "renew")) { + return 0; + } + // display error if we don't have a key + elseif (!$_SESSION[$name]) { + return 0; + } + lock_unlock($name, $_SESSION[$name]); + unset($_SESSION[$name]); + break; + } + } + return 1; } function node_admin_nodes() { @@ -946,20 +1021,46 @@ function node_admin() { $output = search_type('node', url('admin/node/search'), $_POST['keys']); break; case 'edit': - $output = node_admin_edit(arg(3)); + if(node_lock(arg(3), "edit")) { + $output = node_admin_edit(arg(3)); + } + else { + $output = node_display_lock_error("admin edit"); + } break; case 'delete': - $output = node_delete(array('nid' => arg(3))); + if(node_lock(arg(3), "delete")) { + $output = node_delete(array('nid' => arg(3))); + } + else { + $output = node_display_lock_error("admin delete"); + } break; case t('Preview'): - $edit = node_validate($edit, $error); - $output = node_preview($edit, $error); + if(node_lock($edit['nid'], "preview")) { + $edit = node_validate($edit, $error); + $output = node_preview($edit, $error); + } + else { + $output = node_display_lock_error("admin preview"); + } break; case t('Submit'): - $output = node_submit($edit); + if(node_lock($edit['nid'], "submit")) { + $output = node_submit($edit); + } + else { + $output = node_display_lock_error("admin submit"); + } break; case t('Delete'): - $output = node_delete($edit); + // we are still previewing during this step, not yet deleting + if(node_lock($edit['nid'], "preview")) { + $output = node_delete($edit); + } + else { + $output = node_display_lock_error("admin delete"); + } break; case t('Save configuration'): case t('Reset to defaults'): @@ -972,6 +1073,17 @@ function node_admin() { print theme('page', $output); } +function node_display_lock_error($type) { + switch($type) { + case "admin edit": + return t("Temporary access failure. Another administrater is currently making changes to this content. Please try again later."); + case "admin preview": + case "admin submit": + case "admin delete": + return t("Your session has expired. Another administrator is currently making changes to this content. Please try again later."); + } +} + function node_block($op = 'list', $delta = 0) { if ($op == 'list') { @@ -1254,7 +1366,7 @@ function node_add($type) { $edit = $_POST['edit']; /* - ** If a node type has been specified, validate it existence. If no + ** If a node type has been specified, validate its existence. If no ** (valid) node type has been provided, display a node type overview. */