Index: checkout.module =================================================================== --- checkout.module (revision 1333) +++ checkout.module (working copy) @@ -1,358 +1,360 @@ -'. t("Drupal, by default, does not take into account the possibility of multiple users wanting to update the same document (node) at the same time. There is consequently a danger that two users try to edit a node at the same time (a race condition), with one user overwriting the other's modifications.") .'

'; - $output .= '

'. t("The checkout module offers active protection against concurrent edits. When a user begins to modify a document, it is considered being 'checked out' of the system for exclusive editing access by that user. It only becomes available to other users once the editor navigates out of the edit page - by submitting the content, viewing the node, or selecting another menu item. The system also takes into account that some users edit multiple documents at once, so there is a direct correlation between a browser session and the locks that are placed upon a document.") .'

'; - $output .= '

'. t('If permission has been given at the access control page, it is possible for users to keep a document checked out when they submit their changes; they can therefore be sure that their document is locked between sessions.', array('!uri' => url('admin/user/access'))) .'

'; - $output .= '

'. t('A cron job is provided which will automatically check in documents that have been checked out beyond a configurable period of time.') .'

'; - return $output; - case 'admin/content/node/checkout': - return '

'. t("Below is a list of all checked out documents. Click on 'check-in' to release a editing lock.") .'

'; - } - if (arg(0) == 'user' && is_numeric(arg(1)) && arg(2) == 'checkout') { - return '

'. t("Below is a list of all documents checked out by you. Click on 'check-in' to release a editing lock.") .'

'; - } -} - -/** - * Implementation of hook_menu(). - */ -function checkout_menu($may_cache) { - global $user; - $items = array(); - - if ($may_cache) { - $admin_access = user_access('administer checked out documents'); - $items[] = array('path' => 'admin/content/node/checkout', - 'title' => t('Checked out'), - 'callback' => 'checkout_admin_overview', - 'access' => $admin_access, - 'weight' => 5, - 'type' => MENU_LOCAL_TASK); - $items[] = array('path' => 'admin/content/node/checkout/release', - 'title' => t('Check-in content'), - 'callback' => 'checkout_admin_release', - 'access' => $admin_access, - 'type' => MENU_CALLBACK); - } - else { - if (arg(0) == 'user' && is_numeric(arg(1)) && $user->uid == arg(1)) { - $user_access = user_access('check out documents'); - $items[] = array('path' => 'user/'. arg(1) .'/checkout', - 'title' => t('Track check-outs'), - 'callback' => 'checkout_user_overview', - 'callback arguments' => array(arg(1)), - 'access' => $user_access, - 'weight' => 5, - 'type' => MENU_LOCAL_TASK); - $items[] = array('path' => 'user/'. arg(1) .'/checkout/release', - 'title' => t('Check-in content'), - 'callback' => 'checkout_user_release', - 'callback arguments' => array(arg(1)), - 'access' => $user_access, - 'type' => MENU_CALLBACK); - } - } - - return $items; -} - -/** - * Implementation of hook_form_alter(). - */ -function checkout_form_alter($form_id, &$form) { - if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) { - if (user_access('keep documents checked out')) { - $node = $form['#node']; - if ($node->nid) { - // Place checkbox immediately after the log message - $form['checkout'] = array( - '#type' => 'checkbox', - '#title' => t('Keep document checked out'), - '#return_value' => 1, - '#weight' => 20.1, - '#default_value' => FALSE, - '#description' => t('Check this box if you wish to keep this document locked for editing after submit.'), - ); - } - } - } - else if ($form_id == 'node_configure') { - // Make sure our element appears before the submit buttons - $form['buttons']['#weight'] = 10; - - $period = array(0 => t('Disabled')) + drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval'); - $form['checkout_clear'] = array( - '#type' => 'select', - '#title' => t('Automatic check-in'), - '#default_value' => variable_get('checkout_clear', 0), - '#options' => $period, - '#description' => t('The period after which checked out documents will be automatically released.'), - ); - } -} - -/** - * Implementation of hook_nodeapi(). - */ -function checkout_nodeapi(&$node, $op, $teaser, $page) { - global $user; - - switch ($op) { - case 'prepare': - if ($node->nid > 0) { - $data = checkout_checkout($node->nid); - if ($data && $data->uid != $user->uid) { - checkout_message($data); // won't return - } - } - break; - - case 'update': - if (empty($node->checkout)) { - checkout_release($node->nid); - } - else { - db_query("UPDATE {checkout} SET persistent = 1 WHERE nid = %d", $node->nid); - } - break; - - case 'delete': - checkout_release($node->nid); - break; - } -} - -/** - * Implementation of hook_cron(). - * - * Release nodes that have been locked longer than the configured period. - */ -function checkout_cron() { - $checkout_clear = variable_get('checkout_clear', 0); - if ($checkout_clear > 0) { - $result = db_query('DELETE FROM {checkout} WHERE timestamp < %d', time() - $checkout_clear); - if ($num = db_affected_rows($result)) { - $message = t('Released @count document(s) checked out for more than @period.', array('@count' => $num, '@period' => format_interval($checkout_clear))); - drupal_set_message($message); - watchdog('checkout', $message); - } - } -} - -/** - * Implementation of hook_exit(). - * - * Release the node that has been last edited, but only if we're coming - * directly from an edit page to protect against unlocking nodes while - * editing/surfing in concurrent browser windows. - */ -function checkout_exit() { - global $user; - - if ($user->uid) { - $edit_types = array('edit', 'classify', 'outline'); - $request = array_reverse(explode('/', request_uri())); - $referer = array_reverse(explode('/', referer_uri())); - - if (!in_array($request[0], $edit_types) && in_array($referer[0], $edit_types)) { - $nid = $referer[1]; - if (!db_result(db_query("SELECT persistent FROM {checkout} WHERE nid = %d", $nid))) { - checkout_release($nid); - } - } - } -} - -/** - * Lock a node for editing. - * - * If the document isn't currently checked out the returned info object has - * just one property: the user id of the current user. Otherwise it contains - * the information about when and by whom the document was checked out. - * - * @param integer $nid - * A node id. - * - * @return object A check-out info object. - */ -function checkout_checkout($nid) { - global $user; - - db_lock_table('checkout'); - - $result = db_query("SELECT nid, uid, timestamp FROM {checkout} WHERE nid = %d", $nid); - if (db_num_rows($result)) { - $info = db_fetch_object($result); - } - else if (user_access('check out documents')) { - db_query("INSERT INTO {checkout} (nid, uid, persistent, timestamp) VALUES (%d, %d, %d, %d)", $nid, $user->uid, 0, time()); - - $info = new stdClass; - $info->uid = $user->uid; - } - else { - $info = NULL; - } - - db_unlock_tables(); - - return $info; -} - -/** - * Display an error message and send the user back to the node view. - * - * @param object $info - * A check-out info object. - * - * @return void This function doesn't return. - * - * @see checkout_checkout() - */ -function checkout_message($info) { - $username = theme('username', user_load(array('uid' => $info->uid))); - $date = format_date($info->timestamp, 'medium'); - $message = t('This document is locked for editing by !name since @date.', array('!name' => $username, '@date' => $date)); - - if (user_access('administer checked out documents')) { - $uri = url("admin/content/node/checkout/release/$info->nid", 'destination='. request_uri()); - $message .= '
'. t('Click here to check back in now.', array('!uri' => $uri)); - } - - drupal_set_message($message, 'error'); - - drupal_goto("node/$info->nid"); -} - -/** - * Release a node. - * - * @param integer $nid - * A node id. - */ -function checkout_release($nid) { - db_query('DELETE FROM {checkout} WHERE nid = %d', $nid); -} - -/** - * Return a list of all checked out documents. - * - * @return string $output - */ -function checkout_admin_overview() { - $header = array( - array('data' => t('Title'), 'field' => 'n.title', 'sort' => 'asc'), - array('data' => t('Username'), 'field' => 'u.name'), - array('data' => t('Check-out date'), 'field' => 'c.timestamp'), - t('Operations'), - ); - - $rows = array(); - $result = pager_query('SELECT c.nid, c.uid, c.timestamp, n.title, u.name FROM {checkout} c INNER JOIN {node} n ON n.nid = c.nid INNER JOIN {users} u ON u.uid = c.uid'. tablesort_sql($header), 50, 0, NULL); - - while ($data = db_fetch_object($result)) { - $row = array(); - $row[] = l($data->title, "node/$data->nid"); - $row[] = theme('username', user_load(array('uid' => $data->uid))); - $row[] = format_date($data->timestamp, 'small'); - $row[] = l(t('check-in'), "admin/content/node/checkout/release/$data->nid"); - $rows[] = $row; - } - - $output = theme('table', $header, $rows, array('id' => 'checkout')); - if (!$rows) { - $output .= t('No documents checked out.'); - } - else if ($pager = theme('pager', array(), 50, 0)) { - $output .= $pager; - } - - return $output; -} - -/** - * Menu callback; releases a document editing lock. - * - * @param integer $nid - * A node id. - * - * @return void This function doesn't return. - */ -function checkout_admin_release($nid) { - checkout_release($nid); - drupal_set_message(t('The editing lock has been released.')); - drupal_goto('admin/content/node/checkout'); -} - -/** - * Return a list of a users's checked out documents. - * - * @param integer $uid - * A user id. - * - * @return string $output - */ -function checkout_user_overview($uid) { - $header = array( - array('data' => t('Title'), 'field' => 'n.title', 'sort' => 'asc'), - array('data' => t('Check-out date'), 'field' => 'c.timestamp'), - t('Operations'), - ); - - $rows = array(); - $result = pager_query('SELECT c.nid, c.timestamp, n.title FROM {checkout} c INNER JOIN {node} n ON n.nid = c.nid WHERE c.uid = %d'. tablesort_sql($header), 50, 0, NULL, $uid); - - while ($data = db_fetch_object($result)) { - $row = array(); - $row[] = l($data->title, "node/$data->nid"); - $row[] = format_date($data->timestamp, 'small'); - $row[] = l(t('check-in'), "user/$uid/checkout/release/$data->nid"); - $rows[] = $row; - } - - $output = theme('table', $header, $rows, array('id' => 'checkout')); - if (!$rows) { - $output .= t('No documents checked out.'); - } - else if ($pager = theme('pager', array(), 50, 0)) { - $output .= $pager; - } - - return $output; -} - -/** - * Menu callback; releases a document editing lock. - * - * @param integer $uid - * A user id. - * @param integer $nid - * A node id. - * - * @return void This function doesn't return. - */ -function checkout_user_release($uid, $nid) { - checkout_release($nid); - drupal_set_message(t('The editing lock has been released.')); - drupal_goto("user/$uid/checkout"); -} - +'. t("Drupal, by default, does not take into account the possibility of multiple users wanting to update the same document (node) at the same time. There is consequently a danger that two users try to edit a node at the same time (a race condition), with one user overwriting the other's modifications.") .'

'; + $output .= '

'. t("The checkout module offers active protection against concurrent edits. When a user begins to modify a document, it is considered being 'checked out' of the system for exclusive editing access by that user. It only becomes available to other users once the editor navigates out of the edit page - by submitting the content, viewing the node, or selecting another menu item. The system also takes into account that some users edit multiple documents at once, so there is a direct correlation between a browser session and the locks that are placed upon a document.") .'

'; + $output .= '

'. t('If permission has been given at the access control page, it is possible for users to keep a document checked out when they submit their changes; they can therefore be sure that their document is locked between sessions.', array('!uri' => url('admin/user/access'))) .'

'; + $output .= '

'. t('A cron job is provided which will automatically check in documents that have been checked out beyond a configurable period of time.') .'

'; + return $output; + case 'admin/content/node/checkout': + return '

'. t("Below is a list of all checked out documents. Click on 'check-in' to release a editing lock.") .'

'; + } + if (arg(0) == 'user' && is_numeric(arg(1)) && arg(2) == 'checkout') { + return '

'. t("Below is a list of all documents checked out by you. Click on 'check-in' to release a editing lock.") .'

'; + } +} + +/** + * Implementation of hook_menu(). + */ +function checkout_menu($may_cache) { + global $user; + $items = array(); + + if ($may_cache) { + $admin_access = user_access('administer checked out documents'); + $items[] = array('path' => 'admin/content/node/checkout', + 'title' => t('Checked out'), + 'callback' => 'checkout_admin_overview', + 'access' => $admin_access, + 'weight' => 5, + 'type' => MENU_LOCAL_TASK); + $items[] = array('path' => 'admin/content/node/checkout/release', + 'title' => t('Check-in content'), + 'callback' => 'checkout_admin_release', + 'access' => $admin_access, + 'type' => MENU_CALLBACK); + } + else { + if (arg(0) == 'user' && is_numeric(arg(1)) && $user->uid == arg(1)) { + $user_access = user_access('check out documents'); + $items[] = array('path' => 'user/'. arg(1) .'/checkout', + 'title' => t('Track check-outs'), + 'callback' => 'checkout_user_overview', + 'callback arguments' => array(arg(1)), + 'access' => $user_access, + 'weight' => 5, + 'type' => MENU_LOCAL_TASK); + $items[] = array('path' => 'user/'. arg(1) .'/checkout/release', + 'title' => t('Check-in content'), + 'callback' => 'checkout_user_release', + 'callback arguments' => array(arg(1)), + 'access' => $user_access, + 'type' => MENU_CALLBACK); + } + } + + return $items; +} + +/** + * Implementation of hook_form_alter(). + */ +function checkout_form_alter($form_id, &$form) { + if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) { + if (user_access('keep documents checked out')) { + $node = $form['#node']; + if ($node->nid) { + // Place checkbox immediately after the log message + $form['checkout'] = array( + '#type' => 'checkbox', + '#title' => t('Keep document checked out'), + '#return_value' => 1, + '#weight' => 20.1, + '#default_value' => FALSE, + '#description' => t('Check this box if you wish to keep this document locked for editing after submit.'), + ); + } + } + } + else if ($form_id == 'node_configure') { + // Make sure our element appears before the submit buttons + $form['buttons']['#weight'] = 10; + + $period = array(0 => t('Disabled')) + drupal_map_assoc(array(3600, 10800, 21600, 32400, 43200, 86400, 172800, 259200, 604800, 1209600, 2419200, 4838400, 9676800), 'format_interval'); + $form['checkout_clear'] = array( + '#type' => 'select', + '#title' => t('Automatic check-in'), + '#default_value' => variable_get('checkout_clear', 0), + '#options' => $period, + '#description' => t('The period after which checked out documents will be automatically released.'), + ); + } +} + +/** + * Implementation of hook_nodeapi(). + */ +function checkout_nodeapi(&$node, $op, $teaser, $page) { + global $user; + + switch ($op) { + case 'prepare': + if ($node->nid > 0 && ($_POST['op'] != 'Submit')) { // p.lindstrom - don't checkout on Submit + $data = checkout_checkout($node->nid); + //if ($data && $data->uid != $user->uid) { + if ($data->nid) { // p.lindstrom - lock even if same user + checkout_message($data); // won't return + } + } + break; + + case 'update': + if (empty($node->checkout)) { + checkout_release($node->nid); + } + else { + db_query("UPDATE {checkout} SET persistent = 1 WHERE nid = %d", $node->nid); + } + break; + + case 'delete': + checkout_release($node->nid); + break; + } +} + +/** + * Implementation of hook_cron(). + * + * Release nodes that have been locked longer than the configured period. + */ +function checkout_cron() { + $checkout_clear = variable_get('checkout_clear', 0); + if ($checkout_clear > 0) { + $result = db_query('DELETE FROM {checkout} WHERE timestamp < %d', time() - $checkout_clear); + if ($num = db_affected_rows($result)) { + $message = t('Released @count document(s) checked out for more than @period.', array('@count' => $num, '@period' => format_interval($checkout_clear))); + drupal_set_message($message); + watchdog('checkout', $message); + } + } +} + +/** + * Implementation of hook_exit(). + * + * Release the node that has been last edited, but only if we're coming + * directly from an edit page to protect against unlocking nodes while + * editing/surfing in concurrent browser windows. + */ +function checkout_exit() { + global $user; + + if ($user->uid) { + $edit_types = array('edit', 'classify', 'outline'); + $request = array_reverse(explode('/', request_uri())); + $referer = array_reverse(explode('/', referer_uri())); + + if (!in_array($request[0], $edit_types) && in_array(str_replace(strstr($referer[0],"?"),"",$referer[0]), $edit_types)) { // p.lindstrom - fix for case of destination query edit?destination=node/123 + $nid = $referer[1]; + if (!db_result(db_query("SELECT persistent FROM {checkout} WHERE nid = %d", $nid))) { + checkout_release($nid); + } + } + } +} + +/** + * Lock a node for editing. + * + * If the document isn't currently checked out the returned info object has + * just one property: the user id of the current user. Otherwise it contains + * the information about when and by whom the document was checked out. + * + * @param integer $nid + * A node id. + * + * @return object A check-out info object. + */ +function checkout_checkout($nid) { + global $user; + + db_lock_table('checkout'); + + $result = db_query("SELECT nid, uid, timestamp FROM {checkout} WHERE nid = %d", $nid); + if (db_num_rows($result)) { + $info = db_fetch_object($result); + } + else if (user_access('check out documents')) { + db_query("INSERT INTO {checkout} (nid, uid, persistent, timestamp) VALUES (%d, %d, %d, %d)", $nid, $user->uid, 0, time()); + + $info = new stdClass; + $info->uid = $user->uid; + drupal_set_message(t('This document is now locked against simultaneous editing. It will unlock when you navigate elsewhere.')); // p.lindstrom + } + else { + $info = NULL; + } + + db_unlock_tables(); + + return $info; +} + +/** + * Display an error message and send the user back to the node view. + * + * @param object $info + * A check-out info object. + * + * @return void This function doesn't return. + * + * @see checkout_checkout() + */ +function checkout_message($info) { + $username = theme('username', user_load(array('uid' => $info->uid))); + $date = format_date($info->timestamp, 'medium'); + $message = t('This document is locked for editing by !name since @date.', array('!name' => $username, '@date' => $date)); + + if (user_access('administer checked out documents')) { + $uri = url("admin/content/node/checkout/release/$info->nid", 'destination='. substr(request_uri(),1)); // p.lindstrom - remove leading / from request_uri + $message .= '
'. t('Click here to check back in now. WARNING: Any unsaved changes in another window will be overwritten.', array('!uri' => $uri)); + } + + drupal_set_message($message, 'error'); + + drupal_goto("node/$info->nid"); +} + +/** + * Release a node. + * + * @param integer $nid + * A node id. + */ +function checkout_release($nid) { + db_query('DELETE FROM {checkout} WHERE nid = %d', $nid); +} + +/** + * Return a list of all checked out documents. + * + * @return string $output + */ +function checkout_admin_overview() { + $header = array( + array('data' => t('Title'), 'field' => 'n.title', 'sort' => 'asc'), + array('data' => t('Username'), 'field' => 'u.name'), + array('data' => t('Check-out date'), 'field' => 'c.timestamp'), + t('Operations'), + ); + + $rows = array(); + $result = pager_query('SELECT c.nid, c.uid, c.timestamp, n.title, u.name FROM {checkout} c INNER JOIN {node} n ON n.nid = c.nid INNER JOIN {users} u ON u.uid = c.uid'. tablesort_sql($header), 50, 0, NULL); + + while ($data = db_fetch_object($result)) { + $row = array(); + $row[] = l($data->title, "node/$data->nid"); + $row[] = theme('username', user_load(array('uid' => $data->uid))); + $row[] = format_date($data->timestamp, 'small'); + $row[] = l(t('check-in'), "admin/content/node/checkout/release/$data->nid"); + $rows[] = $row; + } + + $output = theme('table', $header, $rows, array('id' => 'checkout')); + if (!$rows) { + $output .= t('No documents checked out.'); + } + else if ($pager = theme('pager', array(), 50, 0)) { + $output .= $pager; + } + + return $output; +} + +/** + * Menu callback; releases a document editing lock. + * + * @param integer $nid + * A node id. + * + * @return void This function doesn't return. + */ +function checkout_admin_release($nid) { + checkout_release($nid); + drupal_set_message(t('The editing lock has been released.')); + drupal_goto('admin/content/node/checkout'); +} + +/** + * Return a list of a users's checked out documents. + * + * @param integer $uid + * A user id. + * + * @return string $output + */ +function checkout_user_overview($uid) { + $header = array( + array('data' => t('Title'), 'field' => 'n.title', 'sort' => 'asc'), + array('data' => t('Check-out date'), 'field' => 'c.timestamp'), + t('Operations'), + ); + + $rows = array(); + $result = pager_query('SELECT c.nid, c.timestamp, n.title FROM {checkout} c INNER JOIN {node} n ON n.nid = c.nid WHERE c.uid = %d'. tablesort_sql($header), 50, 0, NULL, $uid); + + while ($data = db_fetch_object($result)) { + $row = array(); + $row[] = l($data->title, "node/$data->nid"); + $row[] = format_date($data->timestamp, 'small'); + $row[] = l(t('check-in'), "user/$uid/checkout/release/$data->nid"); + $rows[] = $row; + } + + $output = theme('table', $header, $rows, array('id' => 'checkout')); + if (!$rows) { + $output .= t('No documents checked out.'); + } + else if ($pager = theme('pager', array(), 50, 0)) { + $output .= $pager; + } + + return $output; +} + +/** + * Menu callback; releases a document editing lock. + * + * @param integer $uid + * A user id. + * @param integer $nid + * A node id. + * + * @return void This function doesn't return. + */ +function checkout_user_release($uid, $nid) { + checkout_release($nid); + drupal_set_message(t('The editing lock has been released.')); + drupal_goto("user/$uid/checkout"); +} +