--- 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 @@
+<?php
+
+/* ***********************************
+ *    external functions (lock API)
+ * ***********************************/
+
+/**
+ * This function allows you to (attempt to) grab a lock.  If you succesfully
+ *  grab a lock, you now own it and you can be sure that nobody else can get
+ *  it until you free it (or it times out).
+ * This function is actually little more than a wrapper, allowing you to easily
+ *  develop your own locking mechanisms if you so require.  For example, you
+ *  may wish to implement row-level locking, or file-based locking, neither of
+ *  which is currently provided by this include file.
+ * The built in mechanisms are quite powerful, however, so the vast majority 
+ *  of users will not need to write their own locking mechanisms.
+ *
+ * Built in lock types:
+ *  db - generic locking in a single database table
+ *  db_rw - more complex read-many write-one locking (**currently disabled**)
+ * 
+ * @param  string  $name       the name of the lock
+ * @param  string  $block      0 = don't block (return immediately),
+ *                             1 = block (don't return until we have the lock)
+ * @param  object  $attributes opptional items used by some types of lock
+ * @param  string  $type       specify what type of locking mechanism to use
+ *
+ * @return int     # = key to the lock we own;  0 = we do not own the lock
+ */
+function lock_lock($name, $block = 0, $attributes = NULL, $type = "db") {
+  $lock = "lock_$type"."_lock";
+  return $lock($name, $block, $attributes);
+}
+
+/**
+ * This function allows you to renew a lock if you own it.  This is useful for
+ *  lengthy interactive tasks with users.  If renew lock returns with a value, 
+ *  you know that you still own the lock.
+ * This function is actually just a wrapper, allowing you to easily use the
+ *  built in locking mechanisms, or even to develop your own. See the comments 
+ *  in 'lock_lock()' for more information.
+ *
+ * @param  string  $name       the name of the lock
+ * @param  int     $key        the key for the lock
+ * @param  object  $attributes opptional items used by some types of lock
+ * @param  string  $type       specify what type of locking mechanism to use
+ *
+ * @return int     # = new key to the renewed lock; 0 = we lost the lock
+ */
+
+function lock_renew($name, $key, $attributes = NULL, $type = "db") {
+  $lock = "lock_$type"."_renew";
+  return $lock($name, $key, $attributes);
+}
+
+/**  
+ * This function allows you to check the status of a named lock.  If the lock
+ *  is held by you are another process, it returns 1.  If the lock is not
+ *  held by anyone, it returns 0.
+ * This function is actually just a wrapper, allowing you to easily use the
+ *  built in locking mechanisms, or even to develop your own. See the comments 
+ *  in 'lock_lock()' for more information.
+ *
+ * @param  string  $name       the name of the lock
+ * @param  object  $attributes opptional items used by some types of lock
+ * @param  string  $type       specify what type of locking mechanism to use
+ *
+ * @return int     1 = lock is held;  0 = lock is not held
+ */
+function lock_is_locked($name, $attributes = NULL, $type = "db") {
+  $is_locked = "lock_$type"."_is_locked";
+  return $is_locked($name, $attributes);
+}
+
+/**
+ * This function allows you to unlock a lock.  If it succeeds in unlocking the
+ *  lock, it returns a 1.  If it fails, it returns a 0.
+ * This function is actually just a wrapper, allowing you to easily use the
+ *  built in locking mechanisms, or even to develop your own.  See the comments
+ *  in 'lock_lock()' for more information.
+ *
+ * @param  string  $name       the name of the lock
+ * @param  object  $attributes opptional items used by some types of lock
+ * @param  string  $type       specify what type of locking mechanism to use
+ *
+ * @return int     1 = succesfully unlocked lock; 0 = failed to unlock lock
+ */
+function lock_unlock($name, $key, $attributes = NULL, $type = "db") {
+  $unlock = "lock_$type"."_unlock";
+  return $unlock($name, $key, $attributes);
+}
+
+/* **********************************************
+ *    internal functions (don't call directly)
+ * **********************************************/
+
+/**
+ * This is the default lock type.  The locks are stored in a database table.
+ *  You can optionally pass in the following attributes to control locking
+ *  behavior:
+ *  -  ttl:     after this number of seconds the lock will be automatically
+ *               unlocked without an explicit call to lock_unlock().  This is 
+ *               primarily to handle the inconsistent nature of http.  Defaults
+ *               to 15 seconds.
+ *  -  timeout: if blocking while trying to obtain a lock, you can optionally
+ *               specify in seconds how long before we give up.  (otherwise
+ *               we'll try forever until we get the lock)
+ *
+ * @param  string  $name       the name of the lock
+ * @param  string  $block      1 = block, 0 = don't block
+ * @param  object  $attributes optional, defined above
+ *
+ * @return int     1 = we own the lock;  0 = we failed to obtain the lock
+ */
+function lock_db_lock($name, $block, $attributes) {
+  /* Due to the nature of http, it's very possible that someone will grab a lock
+  **  and then fail to free it.  Because of this, we have to specify how long
+  **  the lock is valid before we determine it to be stale and unlock it.
+  */
+  if ($attributes->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;
+}
