--- modules/node.module.orig 2004-02-21 15:28:07.000000000 -0500 +++ modules/node.module 2004-02-23 20:41:48.388165786 -0500 @@ -643,7 +643,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("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() { @@ -944,20 +1013,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'): @@ -970,6 +1065,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') { @@ -1252,7 +1358,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. */ --- modules/system.module.orig 2004-02-22 21:30:55.000000000 -0500 +++ modules/system.module 2004-02-23 08:13:13.499447040 -0500 @@ -121,6 +121,13 @@ function system_view_general() { $output .= form_group(t("Cache settings"), $group); + // 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"), "lock", variable_get("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); + // file system: if (!file_check_directory(variable_get('file_directory_path', 'files'))) { $error['file_directory_path'] = theme('error', t('Directory does not exist, or is not writable.')); --- includes/bootstrap.inc.orig 2004-02-21 15:27:29.000000000 -0500 +++ includes/bootstrap.inc 2004-02-21 14:13:19.000000000 -0500 @@ -251,6 +251,7 @@ function watchdog($type, $message, $link include_once "includes/database.inc"; include_once "includes/session.inc"; include_once "includes/module.inc"; +include_once "includes/lock.inc"; // initialize configuration variables, using values from conf.php if available: $conf = variable_init(isset($conf) ? $conf : array()); --- includes/lock.inc.orig 2004-02-21 15:27:42.000000000 -0500 +++ includes/lock.inc 2004-02-23 20:43:59.248563323 -0500 @@ -0,0 +1,288 @@ +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 = time() + $ttl; + @db_query("INSERT INTO {locks} VALUES('%s', 1, 1, %d)", $name, $key); + /* We're potentially racing with other processes that are also trying + ** to grab the lock, so we need to check if we actually got it. + */ + if (db_error()) { + /* The database returned an error which means that someone grabbed the + ** lock before we did. + */ + 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. + */ + } + else { + /* The database did not return an error which means that we own the + ** lock. Success! + */ + return $key; + } + } + 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=time() + $ttl; + if ($newkey == $key) $newkey++; + db_query("UPDATE {locks} SET timestamp = %d WHERE name = '%s' and timestamp = %d", $newkey, $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,timestamp FROM {locks} WHERE name = '%s'", $name)); + if($lock->status == 1) { + // The lock is in the database, but it may be stale... + if($lock->timestamp < time()) { + /* 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->timestamp; + 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. + */ + sleep(2); + db_query("DELETE FROM {locks} WHERE name = '%s' and timestamp = %d", $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; +}