You are here

content_lock.module in Content locking (anti-concurrent editing) 7.2

Allows users to lock documents for modification.

File

content_lock.module
View source
<?php

/**
 * @file
 * Allows users to lock documents for modification.
 */

/**
 * Implements hook_permission().
 */
function content_lock_permission() {
  return array(
    'check out documents' => array(
      'title' => t('Check Out/Lock Documents'),
      'description' => t('Enables users to lock documents and requires them to respect locks others have made.'),
    ),
    'administer checked out documents' => array(
      'title' => t('Administer Checked Out Documents'),
      'description' => t('Enables administrators to view and break locks made by other users.'),
      'restrict access' => TRUE,
    ),
  );
}

/**
 * Implements hook_help().
 */
function content_lock_help($path, $arg) {
  switch ($path) {
    case 'admin/help#content_lock':
      $output = '<p>' . t("Drupal's default content locking strategy is optimistic, that is, two users may start to edit the same content and the one who is hitting the save button first wins the race, while the other is displayed a message stating <em>this content has been modified by another user, changes cannot be saved</em>.  Depending on the number of editors in your organization this might not be an acceptable solution.") . '</p>';
      $output .= '<p>' . t('The Content locking module implements pessimistic locking, which means that content will be exclusively locked whenever a user starts editing it.  The lock will be automatically released when the user submits the form or navigates away from the edit page.') . '</p>';
      $output .= '<p>' . t('Users may also permanently lock content, to prevent others from editing it.  Content locks that have been "forgotten" can be automatically released after a configurable time span.') . '</p>';
      return $output;
    case 'admin/content/content_lock':
      return '<p>' . t('Below is a list of all locked documents. Click on <em>!checkin</em> to release a lock.', array(
        '!checkin' => t('release lock'),
      )) . '</p>';
    case 'user/%/content_lock':
      return '<p>' . t('Below is a list of all documents locked by you. Click on <em>!checkin</em> to release a lock.', array(
        '!checkin' => t('release lock'),
      )) . '</p>';
  }
}

/**
 * Implements hook_menu().
 */
function content_lock_menu() {
  $items['admin/content/content_lock'] = array(
    'title' => 'Locked documents',
    'page callback' => 'content_lock_overview',
    'access callback' => 'user_access',
    'access arguments' => array(
      'administer checked out documents',
    ),
    'weight' => 5,
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/content/content_lock/release'] = array(
    'page callback' => 'content_lock_release_item',
    'page arguments' => array(
      4,
      NULL,
    ),
    'access arguments' => array(
      'administer checked out documents',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['admin/content/%/content_lock/releaseown'] = array(
    'page callback' => 'content_lock_release_own_item',
    'page arguments' => array(
      2,
      TRUE,
      FALSE,
    ),
    'access arguments' => array(
      'check out documents',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['user/%user/content_lock'] = array(
    'title' => 'Locked documents',
    'page callback' => 'content_lock_overview',
    'page arguments' => array(
      1,
    ),
    'access callback' => 'user_access',
    'access arguments' => array(
      'check out documents',
    ),
    'weight' => 5,
    'type' => MENU_LOCAL_TASK,
  );
  $items['ajax/content_lock/%/canceledit'] = array(
    'page callback' => 'content_lock_release_own_item',
    'page arguments' => array(
      2,
      FALSE,
      FALSE,
    ),
    'access callback' => 'user_access',
    'access arguments' => array(
      'check out documents',
    ),
  );
  $items['ajax/content_lock/%node/lock/%'] = array(
    'page callback' => 'content_lock_node_ajax_callback',
    'page arguments' => array(
      2,
      4,
    ),
    'access callback' => 'user_access',
    'access arguments' => array(
      'check out documents',
    ),
  );
  $items['admin/config/content/content_lock'] = array(
    'type' => MENU_NORMAL_ITEM,
    'title' => 'Content lock',
    'description' => 'Configuration options for the Content lock module',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'content_lock_admin_settings',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'content_lock.admin.inc',
  );
  return $items;
}

/**
 * Implements hook_admin_paths().
 *
 * Register ajax lock path as an admin path.
 */
function content_lock_admin_paths() {
  $paths = array(
    'ajax/content_lock/*/lock/*' => TRUE,
  );
  return $paths;
}

/**
 * Implements hook_drupal_goto_alter().
 */
function content_lock_drupal_goto_alter(&$path, &$options, &$http_response_code) {

  // Add content lock token.
  if (module_exists('entity_translation') && fnmatch('node/*/edit/add/*/*', $path) && !isset($options['content_lock_token'])) {
    $options = array(
      'query' => array(
        'content_lock_token' => drupal_get_token($path),
      ),
    );
  }
}

/**
 * Check if an internal Drupal path should be protected with a token.
 *
 * Adds requirements that certain path be accessed only through tokenized URIs
 * which are enforced by this module. This prevents people from being CSRFed
 * into locking nodes that they can access without meaning to lock them.
 *
 * @return bool
 *   Returns TRUE if the path is protected or FALSE if not protected.
 */
function content_lock_is_path_protected($path) {
  $cache =& drupal_static(__FUNCTION__, array());

  // Check cache.
  if (isset($cache[$path])) {
    return $cache[$path];
  }

  // Invoke hook and collect grants/denies for protected paths.
  $protected = array();
  foreach (module_implements('content_lock_path_protected') as $module) {
    $protected = array_merge($protected, array(
      $module => module_invoke($module, 'content_lock_path_protected', $path),
    ));
  }

  // Allow other modules to alter the returned grants/denies.
  drupal_alter('content_lock_path_protected', $protected, $path);

  // If TRUE is returned, path is protected.
  $cache[$path] = in_array(TRUE, $protected);
  return $cache[$path];
}

/**
 * Implements hook_content_lock_path_protected().
 */
function content_lock_content_lock_path_protected($path) {

  // Always protect node edit and revert paths.
  if (strpos($path, 'node/') === 0) {
    $paths_patterns = array(
      'node/*/edit',
      'node/*/edit/*',
      'node/*/revisions/*/revert',
    );
    foreach ($paths_patterns as $protected_pattern) {
      if (drupal_match_path($path, $protected_pattern)) {
        return TRUE;
      }
    }
  }

  // Allow extended tests via protection_menu_token module.
  if (function_exists('protection_menu_token_is_path_protected')) {
    return protection_menu_token_is_path_protected($path);
  }
}

/**
 * Implements hook_preprocess_link().
 */
function content_lock_preprocess_link(&$vars) {

  // Append a CSRF token to all paths that show the node add/edit form.
  if (content_lock_is_path_protected($vars['path'])) {
    if (!isset($vars['options']['query']['content_lock_token'])) {
      $vars['options']['query']['content_lock_token'] = drupal_get_token($vars['path']);
    }
  }
}

/**
 * Implements hook_node_validate().
 */
function content_lock_node_validate($node, $form, &$form_state) {
  global $user;
  if (isset($node->nid) && _content_lock_is_lockable_node($node) && user_access('check out documents')) {

    // Existing node. Check if we still own the lock.
    if ($lock = content_lock_fetch_lock($node->nid)) {
      if ($lock->uid != $user->uid) {

        // Lock is no longer ours.
        form_set_error('changed', t('Your lock has been removed!') . '<br />' . content_lock_lock_owner($lock) . '<br />' . t('You can still save the content if this user aborts the edit operation without saving changes.'));
      }
    }
    else {

      // Node is not locked. Try to re-lock if node is unchanged.
      if (node_last_changed($node->nid) > $node->changed || !content_lock_node($node->nid, $user->uid)) {
        form_set_error('alsochanged', t('Your lock has been removed due to inactivity or by an administrator. Failed to regain the lock since the document has been changed since. Please !discard.', array(
          '!discard' => l(t('discard your changes'), 'node/' . $node->nid),
        )));
      }
    }
  }
}

/**
 * Implements hook_node_update().
 */
function content_lock_node_update($node) {
  if (user_is_logged_in() && _content_lock_is_lockable_node($node)) {
    global $user;
    content_lock_release($node->nid, $user->uid);
  }
}

/**
 * Implements hook_node_delete().
 */
function content_lock_node_delete($node) {
  content_lock_release($node->nid, NULL);
}

/**
 * Implements hook_node_view().
 */
function content_lock_node_view($node, $view_mode, $langcode) {
  global $user;
  static $messages_shown = FALSE;
  if ($view_mode != 'full' || !_content_lock_is_lockable_node($node)) {
    return;
  }
  if (!$messages_shown) {
    _content_lock_show_warnings();
    $messages_shown = TRUE;
  }
  if (empty($node->in_preview)) {

    // Check if the user has pending locks and warn him.
    content_lock_warn_pending_locks($user->uid);
  }
}

/**
 * Implements hook_form_alter().
 */
function content_lock_form_alter(&$form, &$form_state, $form_id) {
  global $user;
  $node = empty($form['#node']) ? NULL : $form['#node'];
  $nid = empty($form['nid']['#value']) ? NULL : $form['nid']['#value'];
  $destination = 'node/' . $nid;

  // Ensure user acquire a lock when reverting a node to an older revision.
  if (!empty($form['#node_revision'])) {
    $node = $form['#node_revision'];
    $nid = $node->nid;
    $destination = 'node/' . $nid . '/revisions';
  }

  // **************** Restore the node format ******************************
  // This _content_lock_is_lockable_node() needs to know the original
  // node format. We either dig up a stashed content_lock_old_format or
  // initialize it here.
  // Only touch node edit forms and then only if the form is `normal'
  // and has a body with a format (#1183678).
  if (is_object($node) && is_numeric($nid) && ($form_id == $node->type . '_node_form' || $form_id == 'node_revision_revert_confirm') && isset($node->body) && is_array($node->body) && !empty($node->body[$node->language][0]['format'])) {
    $old_format = $node->body[$node->language][0]['format'];
    if (!empty($node->content_lock_old_format)) {
      $old_format = $node->content_lock_old_format;
    }
    if (!empty($form_state['values']['content_lock_old_format'])) {
      $old_format = $form_state['values']['content_lock_old_format'];
    }

    // Needs to be manually set before first form submission.
    // We set this in the $node-> namespace because content_lock_nodeapi()
    // doesn't see $form_state['values'].
    $node->content_lock_old_format = $old_format;
    $form['content_lock_old_format'] = array(
      '#type' => 'hidden',
      '#value' => $node->content_lock_old_format,
    );
  }

  // General preconditions for locking.
  // Veto-API. Let other modules veto the locking - so force skipping out
  // of any conditions they want.
  // We will use || logic, so if any module denies locking then we
  // deny locking.
  // Be sure to notice that content_lock also implements this api
  // for his own vetos.
  $skip_lock = FALSE;
  $result = module_invoke_all('content_lock_skip_locking', $node, $form_id, $form, $form_state);
  foreach ($result as $bool) {
    if (is_bool($bool)) {
      $skip_lock = $skip_lock || $bool;
    }
  }
  if ($skip_lock == FALSE) {

    // Adding cancel button, if configured.
    if (variable_get('content_lock_admin_cancelbutton', TRUE)) {
      _content_lock_add_cancelbutton($form, $form_state, $form_id);
    }

    // If we are handling a preview, skip locking.
    if (!empty($form_state['rebuild']) && $form_state['rebuild'] == TRUE) {

      // Placeholder.
    }
    else {
      if ($form_state['submitted'] === FALSE) {

        // Refuse to lock if a token is not present. This prevents a CSRF attack
        // if a user is tricked into visiting pages that cause nodes to be
        // locked.
        // A CSRF token may be missing in these cases:
        // - A node add/edit form is displayed on a path that is not protected
        //   with a CSRF token. Please add the path to the list of protected
        //   paths by implementing hook_content_lock_path_protected().
        // - The CSRF token has not been added to the URL as a query parameter,
        //   for example because the user entered node/1/edit directly in the
        //   address bar of the browser. To avoid CSRF attacks we do not lock
        //   the node automatically, but still give the user the possibility to
        //   lock the node manually using an AJAX call with a proper CSRF token.
        $menu_item = menu_get_item();
        if (empty($_GET['content_lock_token']) || !drupal_valid_token($_GET['content_lock_token'], $menu_item['href'])) {
          drupal_set_message(t('The page you are editing could not be locked automatically. Please !link to make sure other people cannot accidentally overwrite your changes.', array(
            '!link' => l(t('lock the page'), 'nojs/content_lock/' . $nid . '/lock/' . drupal_get_token($nid), array(
              'attributes' => array(
                'class' => array(
                  'use-ajax',
                ),
              ),
            )),
          )), 'error');
          if (!content_lock_is_path_protected($menu_item['path'])) {
            watchdog('content_lock', 'Attempt to load the node_form form at menu path %path which is not protected from CSRF. Developers who want to create custom node editing pages and protect them with hook_content_lock_path_protected() or use protection_menu_token module to protect this path.', array(
              '%path' => $menu_item['path'],
            ), WATCHDOG_WARNING);
          }
        }
        else {

          // Finally set the lock if everything passed.
          if (content_lock_node($nid, $user->uid) == FALSE) {

            // Could not lock node, it's locked by someone else.
            drupal_goto($destination);
          }
          elseif (variable_get('content_lock_unload_js', TRUE)) {
            $form['#after_build'][] = '_content_lock_add_unload_js';
          }
        }
      }
    }
  }
}

/**
 * Implements our own skip_locking api to implement our logic to skip locks.
 */
function content_lock_content_lock_skip_locking($node, $form_id, $form, $form_state) {
  global $user;
  $nid = empty($form['nid']['#value']) ? NULL : $form['nid']['#value'];

  // Support node revision by not requiring the form to have an 'nid' key.
  if (empty($nid) && $form_id == 'node_revision_revert_confirm' && !empty($form['#node_revision'])) {
    $nid = $node->nid;
  }

  // Locked node types. Do not mix this up with the content_types you can
  // chose on the admin form of content lock
  // this types are forced due to disfunctionality.
  // We are not allowed to lock on users form edit, as it
  // always returns to the edit form.
  $node_type_blacklist = array(
    'user' => TRUE,
  );

  // Form ids listed here will not be locked.
  // Do not lock on comment forms.
  $form_id_blacklist = array(
    'comment_form' => TRUE,
  );

  // Add the node-type administration.
  if ($node != NULL) {
    $form_id_blacklist['node_type_form'] = TRUE;
  }

  // Let other modules modify our blacklist.
  drupal_alter('content_lock_form_id_blacklist', $form_id_blacklist, $node);
  if ($node == NULL || empty($nid) || !empty($node_type_blacklist[$node->type]) || !empty($form_id_blacklist[$form_id]) || $user->uid <= 0 || !user_access('check out documents') || $form_id != $node->type . '_node_form' && $form_id != 'node_revision_revert_confirm') {

    // Preconditions failed, skip the lock.
    return TRUE;
  }

  // Check if the current node type and format type is configured to be locked
  // $node->content_lock_old_format has been set in content_lock_form_alter().
  if (!_content_lock_is_lockable_node($node)) {

    // It should not be locked, so skip the lock.
    return TRUE;
  }

  // We have no veto, so lock the node.
  return FALSE;
}

/**
 * Calculate the token required to unlock a node.
 *
 * Tokens are required because they prevent CSRF.
 *
 * @see https://security.drupal.org/node/2429
 */
function content_lock_get_release_token($nid) {
  return drupal_get_token("content_lock/release/{$nid}");
}

/**
 * Add unload js.
 *
 * @param array $form
 *   Form.
 * @param array $form_state
 *   Form state.
 *
 * @return mixed
 *   Form array
 */
function _content_lock_add_unload_js(&$form, $form_state) {
  $m = drupal_get_path('module', 'content_lock');
  drupal_add_js("{$m}/js/jquery.url.packed.js", array(
    'group' => JS_LIBRARY,
  ));
  drupal_add_js("{$m}/js/onUserExit.js", array(
    'group' => JS_LIBRARY,
  ));
  drupal_add_js("{$m}/js/content_lock_init.js");
  $nid = empty($form['nid']['#value']) ? NULL : $form['nid']['#value'];
  $internal_urls = array();
  $internal_form_selectors = array();

  // We're on a locked revision reversion page.
  if (!empty($form['#node_revision']->nid)) {
    $nid = $form['#node_revision']->nid;

    // Don't ask the user if he wants to leave the page when
    // cancelling a reversion.
    $internal_urls[] = $form['actions']['cancel']['#href'];
    $internal_form_selectors[] = '.confirmation';
  }
  $internal_urls[] = 'node/' . $nid . '/edit';
  $internal_form_selectors[] = 'form.node-form';

  // Check lock status.
  $lock = content_lock_fetch_lock($nid);
  if ($lock) {
    $lock_ajax_key = $lock->ajax_key;
  }
  else {
    $lock_ajax_key = FALSE;
  }

  // Get tokens.
  $token = content_lock_get_release_token($nid);

  // Prepare settings.
  $settings = array(
    'nid' => $nid,
    'ajax_key' => $lock_ajax_key,
    'token' => $token,
    'unload_js_message_enable' => variable_get('content_lock_unload_js_message_enable', TRUE),
    'internal_urls' => implode('|', $internal_urls),
    'internal_forms' => implode(', ', $internal_form_selectors),
  );
  if ($settings['unload_js_message_enable']) {
    $settings['unload_js_message'] = variable_get('content_lock_unload_js_message', 'If you proceed, ALL of your changes will be lost.');
  }

  /*
   * Workaround for http://drupal.org/node/1525784 where this function
   * is called multiple times when doing a file field AJAX upload and
   * array_merge_recursive() is used instead of
   * drupal_array_merge_deep_array() to construct the Drupal.settings
   * value. Not calling drupal_add_js() multiple times deprives
   * file_ajax_upload() of the ability to mess up here ;-).
   */
  $called =& drupal_static(__FUNCTION__ . '__called');
  if (!empty($called)) {
    $called++;
    return $form;
  }
  $called = 1;
  drupal_add_js(array(
    'content_lock' => $settings,
  ), 'setting');
  return $form;
}

/**
 * Get verbose.
 */
function _content_lock_verbose() {
  return variable_get('content_lock_admin_verbose', TRUE);
}

/**
 * Add cancel button.
 */
function _content_lock_add_cancelbutton(&$form, $form_state, $form_id) {

  // If we're on the node form.
  $node = empty($form['#node']) ? NULL : $form['#node'];
  $nid = empty($form['nid']['#value']) ? NULL : $form['nid']['#value'];
  if (!empty($form['#node_revision'])) {
    $node = $form['#node_revision'];
    $nid = $node->nid;
  }
  if (!empty($node) && !empty($nid) && ($form_id == $node->type . '_node_form' || $form_id == 'node_revision_revert_confirm')) {

    // Revert node.
    if ($form_id == 'node_revision_revert_confirm') {

      /* Hijack the default cancel link to become a lock-releasing link */
      $destination = 'node/' . $nid . '/revisions';
      if (!empty($form['actions']['cancel']['#href'])) {
        $destination = $form['actions']['cancel']['#href'];
      }
      $form['actions']['cancel']['#href'] = 'admin/content/' . $nid . '/content_lock/releaseown';
      $form['actions']['cancel'] += array(
        '#options' => array(),
      );
      $form['actions']['cancel']['#options'] += array(
        'query' => array(
          'token' => content_lock_get_release_token($nid),
          'destination' => $destination,
        ),
      );
    }
    else {
      if ($node->nid) {
        $form['actions']['cancel'] = array(
          '#type' => 'button',
          '#weight' => 2000,
          '#value' => t('Cancel edit'),
          '#validate' => array(
            'content_lock_cancel_submit',
          ),
        );
        if (isset($form['actions']['delete'])) {
          $form['actions']['delete']['#weight'] = 2001;
        }
      }
    }
  }
}

/**
 * Callback for a cancel request on a form.
 */
function content_lock_cancel_submit(&$form, &$form_state) {

  // Release the node.
  content_lock_release_own_item($form['#node']->nid, TRUE, TRUE);
}

/**
 * Fetch the lock for a node.
 *
 * @param string $nid
 *   A node id.
 *
 * @return object
 *   The lock for the node. FALSE, if the document is not locked.
 */
function content_lock_fetch_lock($nid) {
  $query = db_select('content_lock', 'c')
    ->fields('c')
    ->condition('c.nid', $nid);
  $u = $query
    ->leftJoin('users', 'u', '%alias.uid = c.uid');
  $query
    ->fields($u, array(
    'name',
  ));
  return $query
    ->execute()
    ->fetchObject();
}

/**
 * Tell who has locked node.
 *
 * @param object $lock
 *   The lock for a node.
 *
 * @return string
 *   String with the message.
 */
function content_lock_lock_owner($lock) {
  $username = theme('username', array(
    'account' => user_load($lock->uid),
  ));
  $date = format_date($lock->timestamp, 'medium');
  return t('This document is locked for editing by !name since @date.', array(
    '!name' => $username,
    '@date' => $date,
  ));
}

/**
 * Implements hook_user_logout().
 */
function content_lock_user_logout($account) {

  // Removing all locks, as the user logs out.
  _content_lock_release_all_user_locks($account->uid);
}

/**
 * AJAX callback to lock a node manually.
 *
 * @param object $node
 *   The node to lock.
 * @param string $token
 *   The CSRF token.
 */
function content_lock_node_ajax_callback($node, $token) {
  global $user;

  // Only lock the node if we have a valid CSRF token.
  if (drupal_valid_token($token, $node->nid)) {
    content_lock_node($node->nid, $user->uid);

    // Add the javascript that unlocks the node when the user navigates away
    // from the page.
    $form = array(
      'nid' => array(
        '#value' => $node->nid,
      ),
    );
    _content_lock_add_unload_js($form, array());
  }
  else {
    drupal_set_message(t('The content could not be locked.'));
  }
  $commands = array();
  $commands[] = ajax_command_remove('div.messages');
  $commands[] = ajax_command_before('#block-system-main', theme('status_messages'));
  ajax_deliver(array(
    '#type' => 'ajax',
    '#commands' => $commands,
  ));
}

/**
 * Try to lock a document for editing.
 *
 * If the lock exists, a new AJAX unlock key is created to combat AJAX
 * unlocks during page reloads. See http://drupal.org/node/1049708.
 *
 * @param int $nid
 *   A node id.
 * @param int $uid
 *   The user id to lock the node for.
 * @param bool $quiet
 *   Suppress any normal user messages.
 *
 * @return bool
 *   FALSE, if a document has already been locked by someone else.
 */
function content_lock_node($nid, $uid, $quiet = FALSE) {
  $lock = content_lock_fetch_lock($nid);
  if ($lock != FALSE && $lock->uid != $uid) {
    $message = content_lock_lock_owner($lock);
    if (user_access('administer checked out documents')) {
      $url = "admin/content/content_lock/release/{$nid}";
    }
    if (isset($url)) {
      $token = content_lock_get_release_token($nid);
      $message .= '<br />';
      $message .= t('Click !here to check back in now.', array(
        '!here' => l(t('here'), $url, array(
          'query' => array(
            'token' => $token,
            'destination' => $_GET['q'],
          ),
        )),
      ));
    }
    if (!empty($message)) {
      drupal_set_message($message, 'warning', FALSE);
    }
    return FALSE;
  }
  else {

    // No lock yet, create one.
    if ($lock == FALSE) {

      // Lock node.
      $data = array(
        'nid' => $nid,
        'uid' => $uid,
        'timestamp' => time(),
        'ajax_key' => rand(),
      );
      drupal_write_record('content_lock', $data);
      if (_content_lock_verbose() && !$quiet) {
        drupal_set_message(t('This document is now locked against simultaneous editing.'), 'status', FALSE);
      }
      module_invoke_all('content_lock_locked', $nid, $uid);
    }
    else {

      /* A lock already exists: update its AJAX key */
      $lock->ajax_key = rand();
      if (!drupal_write_record('content_lock', $lock, array(
        'nid',
      ))) {

        /*
         * we encountered a race condition where the lock was deleted
         * between when we loaded it and when we tried to update it
         * with a new key. Recreate the lock then:
         */
        drupal_write_record('content_lock', $lock);
      }
    }
  }
  return TRUE;
}

/**
 * Release a locked node.
 *
 * @param int $nid
 *    The node id to release the edit lock for.
 * @param int $uid
 *    If set, verify that a lock belongs to this user prior to release.
 */
function content_lock_release($nid, $uid = NULL) {
  $query = db_delete('content_lock')
    ->condition('nid', $nid);
  if (!empty($uid)) {
    $query
      ->condition('uid', $uid);
  }
  $query
    ->execute();
  module_invoke_all('content_lock_release', $nid);
}

/**
 * Release all locks.
 */
function _content_lock_release_all_user_locks($uid) {
  db_delete('content_lock')
    ->condition('uid', $uid)
    ->execute();
}

/**
 * Build an overview of locked documents.
 *
 * @param object $account
 *    User account.
 *
 * @return string
 *    Overview list.
 *
 * @throws \Exception
 */
function content_lock_overview($account = NULL) {
  global $user;

  // @TODO old checkout code, review.
  $header = array(
    array(
      'data' => t('Title'),
      'field' => 'n.title',
      'sort' => 'asc',
    ),
  );

  // In the case of an admin, we do not have uid, as he sees all locks.
  if (!is_object($account)) {
    $header[] = array(
      'data' => t('Username'),
      'field' => 'u.name',
    );
    $uid = NULL;
  }
  else {
    $uid = $account->uid;
  }
  $header[] = array(
    'data' => t('Locked since'),
    'field' => 'c.timestamp',
  );
  if ($uid == $user->uid || user_access('administer checked out documents')) {
    $header[] = t('Operations');
  }
  $query = db_select('content_lock', 'c')
    ->extend('TableSort')
    ->fields('c');
  $n = $query
    ->join('node', 'n', '%alias.nid = c.nid');
  $query
    ->fields($n, array(
    'title',
  ));
  $u = $query
    ->join('users', 'u', '%alias.uid = c.uid');
  $query
    ->fields($u, array(
    'name',
  ));
  if ($uid) {
    $query
      ->condition('c.uid', $uid);
  }
  $query
    ->orderByHeader($header);
  $rows = array();
  foreach ($query
    ->execute() as $data) {
    $url = $uid ? "admin/content/" . $data->nid . "/content_lock/releaseown" : 'admin/content/content_lock/release/' . $data->nid;
    $row = array();
    $row[] = l($data->title, "node/{$data->nid}");
    if (!$uid) {
      $row[] = theme('username', array(
        'account' => user_load($data->uid),
      ));
    }
    $row[] = format_date($data->timestamp, 'small');
    if ($uid == $user->uid || user_access('administer checked out documents')) {
      $row[] = l(t('release lock'), $url, array(
        'query' => array(
          'token' => content_lock_get_release_token($data->nid),
        ),
      ));
    }
    $rows[] = $row;
  }
  $output = theme('table', array(
    'header' => $header,
    'rows' => $rows,
    'attributes' => array(
      'id' => 'content_lock',
    ),
    'empty' => t('No locked documents.'),
  ));
  $output .= theme('pager', array(
    'quantity' => 50,
  ));
  return $output;
}

/**
 * Menu callback.
 *
 * Release a locked node for all users or a specific user.
 *
 * @param int $nid
 *   A node id.
 * @param object $account
 *   A user object. If passed, the lock will only be released if this
 *   user owned it.
 *
 * @return int
 *   This function will execute a redirect and not return.
 *
 * @throws \Exception
 */
function content_lock_release_item($nid, $account = NULL) {
  global $user;
  if (empty($_GET['token']) || !drupal_valid_token($_GET['token'], "content_lock/release/{$nid}")) {
    return MENU_ACCESS_DENIED;
  }
  if (!$account && _content_lock_verbose()) {

    /*
     * Enable our "lock released" message to inform the user who
     * likely owned the lock which is to be broken.
     */
    $lock = content_lock_fetch_lock($nid);
  }
  content_lock_release($nid, $account ? $account->uid : NULL);
  if (_content_lock_verbose()) {
    if (!empty($lock) && !$account && $user->uid != $lock->uid) {
      $lock_account = user_load($lock->uid);
      drupal_set_message(t('The editing lock held by !user has been released.', array(
        '!user' => theme('username', array(
          'account' => $lock_account,
        )),
      )), 'status', FALSE);
    }
    else {
      drupal_set_message(t('The editing lock has been released.'), 'status', FALSE);
    }
  }
  drupal_goto($account ? "user/{$account->uid}/content_lock" : 'admin/content/content_lock');
}

/**
 * Warn pending locks.
 *
 * For every lock a user current have on any nodes, print a warning message
 * with an link to release this node.
 *
 * @param int $uid
 *    User ID.
 */
function content_lock_warn_pending_locks($uid) {

  // Cache.
  static $warned_nodes = array();
  static $content_lock_messages_printed = FALSE;
  if ($content_lock_messages_printed) {
    return;
  }
  if (array_key_exists($uid, $warned_nodes)) {

    // Placeholder.
  }
  else {

    // Load form db.
    $warned_nodes[$uid] = array();
    $query = db_select('content_lock', 'c')
      ->fields('c', array(
      'nid',
    ))
      ->condition('c.uid', $uid);
    $n = $query
      ->leftJoin('node', 'n', '%alias.nid = c.nid');
    $query
      ->fields($n, array(
      'title',
    ));
    foreach ($query
      ->execute() as $lock) {
      $warned_nodes[$uid][] = $lock;
    }
  }
  foreach ($warned_nodes[$uid] as $lock) {
    $nodetitle_link = l($lock->title, "node/{$lock->nid}");
    $token = content_lock_get_release_token($lock->nid);
    $releasethelock_link = l(t('release the lock'), "admin/content/{$lock->nid}/content_lock/releaseown", array(
      'query' => array(
        'token' => $token,
      ),
    ));
    _content_lock_save_lock_warning(t("The node '!nodetitle_link' is locked by you. You may want to '!releasethelock_link' in order to allow others to edit.", array(
      '!nodetitle_link' => $nodetitle_link,
      '!releasethelock_link' => $releasethelock_link,
    )), $lock->nid);
  }
  $content_lock_messages_printed = TRUE;
}

/**
 * Save lock warning.
 *
 * @param string $message
 *    Message string.
 * @param int $nid
 *    Node id.
 */
function _content_lock_save_lock_warning($message, $nid) {
  if (empty($_SESSION['content_lock'])) {
    $_SESSION['content_lock'] = '';
  }
  $data = unserialize($_SESSION['content_lock']);
  if (!is_array($data)) {
    $data = array();
  }
  if (array_key_exists($nid, $data)) {
    return;
  }
  $data[$nid] = $message;
  $_SESSION['content_lock'] = serialize($data);
}

/**
 * Show warnings.
 */
function _content_lock_show_warnings() {
  global $user;
  if (empty($_SESSION['content_lock'])) {
    return;
  }
  $data = unserialize($_SESSION['content_lock']);
  if (!is_array($data) || count($data) == 0) {
    return;
  }
  foreach ($data as $nid => $messsage) {
    if (_content_lock_still_locked($user->uid, $nid)) {
      drupal_set_message($messsage, 'warning', FALSE);
    }
  }
  $_SESSION['content_lock'] = '';
}

/**
 * Check lock status.
 *
 * @param int $uid
 *    User ID.
 * @param int $nid
 *    Node ID.
 *
 * @return bool
 *    Return TRUE OR FALSE.
 */
function _content_lock_still_locked($uid, $nid) {
  $query = db_select('content_lock', 'c')
    ->condition('nid', $nid)
    ->condition('uid', $uid)
    ->countQuery();
  $result = $query
    ->execute();
  return (bool) $result
    ->fetchField();
}

/**
 * Release the lock of a node.
 *
 * We are using the current users uid, so the user only can delete his
 * own locks. We never fail, as if the lock does not exist,
 * the node is unlocked anyway.
 *
 * @param bool $response
 *   When set to FALSE, indicates that the request was made through
 *   ajax. This means that we shouldn't talk to the user. It also
 *   means that we should compare the ajax_key to fix the page Reload
 *   bug (http://drupal.org/node/1049708). In the page reload bug, the
 *   browser sends a request to load the edit page and simultaneously
 *   sends an AJAX request asking for the node to be unlocked. By
 *   changing the ajax_key when responding to the browser, we can
 *   detect that the soon-to-come ajax request is from the previous
 *   page load and that it should be ignored.
 * @param bool $ignore_token
 *   Use this to disable the anti-CSRF token check. This should only
 *   be disabled when some other means is being used to prevent
 *   CSRF. Drupal forms, for example, are already protected by the
 *   equivalent of a token—we need not and may not go adding tokens to
 *   the node forms we hijack.
 *
 * @return int
 *   Return int.
 */
function content_lock_release_own_item($nid, $response = TRUE, $ignore_token = FALSE) {
  global $user;
  if (!$ignore_token) {
    if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], "content_lock/release/{$nid}")) {
      return MENU_ACCESS_DENIED;
    }
  }
  if ($nid != NULL) {

    /*
     * Imply that this is an AJAX request if we aren't expected to
     * interface with a human.
     */
    if (!$response) {
      $lock = content_lock_fetch_lock($nid);
      if (empty($lock) || strcmp($_GET['k'], $lock->ajax_key)) {

        // We have no lock or the key doesn't match. Don't unlock the node.
        drupal_exit();
      }
    }
    content_lock_release($nid, $user->uid);

    // Set drupal_get_messages().
    if ($response) {
      drupal_goto("node/{$nid}");
    }
    else {
      drupal_exit();
    }
  }
  else {

    // If a user was creating a node and canceled.
    if ($response) {
      drupal_goto();
    }
    else {
      drupal_exit();
    }
  }
}

/**
 * Check whether a node is configured to be protected by content_lock.
 */
function _content_lock_is_lockable_node($node) {
  return !in_array(FALSE, module_invoke_all('content_lock_node_lockable', $node));
}

/**
 * Our own hook_content_lock_node_lockable().
 */
function content_lock_content_lock_node_lockable($node) {
  static $lockable = array();

  // To catch the case where the user is changing the input format,
  // we store the original input format. Remember that not all nodes
  // store formats in the same way nor even have formats (#1183678).
  $format = '';
  if (isset($node->body) && is_array($node->body) && !empty($node->body[$node->language][0]['format'])) {
    $format = $node->body[$node->language][0]['format'];
  }
  if (!empty($node->content_lock_old_format)) {
    $format = $node->content_lock_old_format;
  }

  // Check for a cache hit.
  if (isset($lockable[$format][$node->nid])) {
    return $lockable[$format][$node->nid];
  }
  $types = array_filter(variable_get('content_lock_allowed_node_types', array()));

  // Let other modules modify our blacklist.
  drupal_alter('content_lock_node_type_blacklist', $types, $node);
  $formats = array_filter(variable_get('content_lock_allowed_formats', array()));
  $lockable[$format][$node->nid] = FALSE;

  // Determine if the node is of a lockable content type or text format.
  if ((empty($types) || in_array($node->type, $types)) && (empty($formats) || in_array($format, $formats))) {
    $lockable[$format][$node->nid] = TRUE;
  }
  return $lockable[$format][$node->nid];
}

/**
 * Views integration.
 */
function content_lock_views_api() {
  return array(
    'api' => 2.0,
    'path' => drupal_get_path('module', 'content_lock') . '/views',
  );
}

/**
 * Implements hook_ctools_plugin_directory().
 */
function content_lock_ctools_plugin_directory($owner, $plugin_type) {
  if ($owner == 'ctools' && $plugin_type == 'access') {
    return "plugins/{$plugin_type}";
  }
}

Functions

Namesort descending Description
content_lock_admin_paths Implements hook_admin_paths().
content_lock_cancel_submit Callback for a cancel request on a form.
content_lock_content_lock_node_lockable Our own hook_content_lock_node_lockable().
content_lock_content_lock_path_protected Implements hook_content_lock_path_protected().
content_lock_content_lock_skip_locking Implements our own skip_locking api to implement our logic to skip locks.
content_lock_ctools_plugin_directory Implements hook_ctools_plugin_directory().
content_lock_drupal_goto_alter Implements hook_drupal_goto_alter().
content_lock_fetch_lock Fetch the lock for a node.
content_lock_form_alter Implements hook_form_alter().
content_lock_get_release_token Calculate the token required to unlock a node.
content_lock_help Implements hook_help().
content_lock_is_path_protected Check if an internal Drupal path should be protected with a token.
content_lock_lock_owner Tell who has locked node.
content_lock_menu Implements hook_menu().
content_lock_node Try to lock a document for editing.
content_lock_node_ajax_callback AJAX callback to lock a node manually.
content_lock_node_delete Implements hook_node_delete().
content_lock_node_update Implements hook_node_update().
content_lock_node_validate Implements hook_node_validate().
content_lock_node_view Implements hook_node_view().
content_lock_overview Build an overview of locked documents.
content_lock_permission Implements hook_permission().
content_lock_preprocess_link Implements hook_preprocess_link().
content_lock_release Release a locked node.
content_lock_release_item Menu callback.
content_lock_release_own_item Release the lock of a node.
content_lock_user_logout Implements hook_user_logout().
content_lock_views_api Views integration.
content_lock_warn_pending_locks Warn pending locks.
_content_lock_add_cancelbutton Add cancel button.
_content_lock_add_unload_js Add unload js.
_content_lock_is_lockable_node Check whether a node is configured to be protected by content_lock.
_content_lock_release_all_user_locks Release all locks.
_content_lock_save_lock_warning Save lock warning.
_content_lock_show_warnings Show warnings.
_content_lock_still_locked Check lock status.
_content_lock_verbose Get verbose.