Skip to content
Snippets Groups Projects
Forked from UNL Information Services / UNL-CMS
729 commits behind the upstream repository.
workbench_moderation.module 60.04 KiB
<?php

/**
 * @file
 * Content moderation for Workbench.
 *
 * Based on content_moderation.module by eugenmayer.
 * Base version 1.12.2.17 2010/04/18 11:31:29.
 */

/**
 * Implements hook_menu().
 */
function workbench_moderation_menu() {
  $items = array();

  // Display a node's moderation history
  $items["node/%node/moderation"] = array(
    'title' => 'Moderate',
    'description' => 'Show the content moderation history.',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'workbench_moderation_node_history_view',
    'page arguments' => array(1),
    'theme callback' => '_node_custom_theme',
    'access callback' => '_workbench_moderation_access',
    'access arguments' => array('view history', 1),
    'file' => 'workbench_moderation.node.inc'
  );

  // Unpublishing a live revision.
  $items["node/%node/moderation/%/unpublish"] = array(
    'title' => 'Unpublish revision',
    'description' => 'Unpublish the current live revision.',
    'type' => MENU_CALLBACK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array('workbench_moderation_node_unpublish_form', 1),
    'load arguments' => array(3),
    'theme callback' => '_node_custom_theme',
    'access callback' => '_workbench_moderation_access',
    'access arguments' => array('unpublish', 1),
    'file' => 'workbench_moderation.node.inc'
  );

  // Change the moderation state of a node.
  // Used in workbench_moderation_get_moderation_links()
  $items["node/%node/moderation/%/change-state/%"] = array(
    'title' => 'Change Moderation State',
    'page callback' => 'workbench_moderation_moderate_callback',
    'page arguments' => array(1, 5),
    'load arguments' => array(3),
    'access callback' => '_workbench_moderation_moderate_access',
    'access arguments' => array(1, 5),
    'type' => MENU_CALLBACK,
  );

  // View the current revision of a node. Redirects to node/%node if the current revision is
  // published, and to node/%node/draft if the current revision is a draft.
  $items["node/%node/current-revision"] = array(
    'page callback' => 'workbench_moderation_node_current_view',
    'page arguments' => array(1),
    'access arguments' => array('view revisions'),
    'file' => 'workbench_moderation.node.inc',
  );

  // View the current draft of a node.
  $items["node/%node/draft"] = array(
    'title' => 'View draft',
    'page callback' => 'workbench_moderation_node_view_draft',
    'page arguments' => array(1),
    'access callback' => '_workbench_moderation_access_current_draft',
    'access arguments' => array(1),
    'file' => 'workbench_moderation.node.inc',
    'type' => MENU_LOCAL_TASK,
    'weight' => -9,
  );

  // Module settings.
  $items["admin/config/workbench/moderation"] = array(
    'title' => 'Workbench Moderation',
    'description' => 'Configure content moderation.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('workbench_moderation_admin_states_form'),
    'access arguments' => array('administer workbench moderation'),
    'file' => 'workbench_moderation.admin.inc',
  );
  $items['admin/config/workbench/moderation/general'] = array(
    'title' => 'States',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -1,
  );
  $items['admin/config/workbench/moderation/transitions'] = array(
    'title' => 'Transitions',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array('workbench_moderation_admin_transitions_form'),
    'access arguments' => array('administer workbench moderation'),
    'file' => 'workbench_moderation.admin.inc',
  );
  $items['admin/config/workbench/moderation/check-permissions'] = array(
    'title' => 'Check Permissions',
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array('workbench_moderation_admin_check_role_form'),
    'access arguments' => array('administer workbench moderation'),
    'file' => 'workbench_moderation.admin.inc',
    'weight' => 10,
  );

  return $items;
}

/**
 * Implements hook_menu_alter().
 */
function workbench_moderation_menu_alter(&$items) {
  // Hijack the node/X/edit page to ensure that the right revision (most current) is displayed.
  $items['node/%node/edit']['page callback'] = 'workbench_moderation_node_edit_page_override';

  // Override the node edit menu item title.
  $items['node/%node/edit']['title callback'] = 'workbench_moderation_edit_tab_title';
  $items['node/%node/edit']['title arguments'] = array(1);

  // Override the node view menu item title.
  $items['node/%node/view']['title callback'] = 'workbench_moderation_view_tab_title';
  $items['node/%node/view']['title arguments'] = array(1);

  // Redirect node/%node/revisions
  $items['node/%node/revisions']['page callback'] = 'workbench_moderation_node_revisions_redirect';
  $items['node/%node/revisions']['page arguments'] = array(1);

  // For revert and delete operations, use our own access check.
  $items['node/%node/revisions/%/revert']['access callback'] = '_workbench_moderation_revision_access';
  $items['node/%node/revisions/%/delete']['access callback'] = '_workbench_moderation_revision_access';

  // Provide a container administration menu item, if one doesn't already exist.
  if (!isset($items['admin/config/workbench'])) {
    $items['admin/config/workbench'] = array(
      'title' => 'Workbench',
      'description' => 'Workbench',
      'page callback' => 'system_admin_menu_block_page',
      'access arguments' => array('administer site configuration'),
      'position' => 'right',
      'file' => 'system.admin.inc',
      'file path' => drupal_get_path('module', 'system'),
    );
  }
}

/**
 * Redirects 'node/%node/revisions' to node/%node/moderation
 *
 * workbench_moderation_menu_alter() changes the page callback
 * for 'node/%node/revisions' to this function
 *
 * @param $node
 *   The node being acted upon.
 */
function workbench_moderation_node_revisions_redirect($node) {
  // Redirect node types subject to moderation.
  if (workbench_moderation_node_type_moderated($node->type) === TRUE) {
    drupal_goto('node/' . $node->nid . '/moderation');
  }
  // Return the normal node revisions page for unmoderated types.
  else {
    return node_revision_overview($node);
  }
}

/**
 * Implements hook_menu_local_tasks_alter().
 *
 * Hide the node revisions tab conditionally.
 *
 * Check if the node type is subject to moderation. If so, unset the revision
 * tab. This step is necessary because hook_menu_alter cannot change the menu
 * item type on a node type by node type basis for node/%node/revision.
 *
 * Additionally, workbench_menu_alter() is used to change the page callback
 * for node/%node/revisions so that this URL redirects to node/%node/moderation
 * for node types subject to moderation.
 */
function workbench_moderation_menu_local_tasks_alter(&$data, $router_item, $root_path) {
  // Do we need to bother doing anything?
  if (empty($data['tabs'][0]['output']) || empty($router_item['page_arguments'][0])) {
    return;
  }

  // Check the path.
  $arg = arg(0, $root_path);
  $arg1 = arg(1, $root_path);
  if ($arg != 'node' || $arg1 != '%') {
    return;
  }

  // Devel module passes node as the first parameter, but we support it.
  if (count($router_item['page_arguments']) > 1 && $router_item['page_arguments'][0] == 'node') {
    array_shift($router_item['page_arguments']);
  }
  // Ensure that we have a proper node.
  if (!is_object($router_item['page_arguments'][0])) {
    return;
  }
  $node = $router_item['page_arguments'][0];

  // Here is the reason this hook implementation exists:
  // If this is a node type that gets moderated, don't show 'node/%/revisions'
  if (workbench_moderation_node_type_moderated($node->type) === TRUE) {
    foreach ($data['tabs'][0]['output'] as $key => $value) {
      if (!empty($value['#link']['path']) && $value['#link']['path'] == 'node/%/revisions') {
        unset($data['tabs'][0]['output'][$key]);
        break;
      }
    }
  }
}

/**
 * Change the name of the node edit tab, conditionally.
 *
 * - Don't change the title if the content is not under moderation.
 *
 * - If a piece of content has a published revision and the published revision
 *   is also the current moderation revision, the "Edit" tab should be titled
 *   "Create draft".
 *
 * - If a piece of content has a published revision and the current moderation
 *   revision is a newer, or if the content has no published revision, the
 *   "Edit" tab should be titled "Edit draft".
 *
 * @param $node
 *   The node being acted upon.
 *
 * @return
 *   The title for the tab.
 */
function workbench_moderation_edit_tab_title($node) {
  // Use the normal tab title if the node type is not under moderation.
  if (!workbench_moderation_node_type_moderated($node->type)) {
    return t('Edit');
  }

  // Is the latest draft published?
  $state = $node->workbench_moderation;
  if (!empty($state['published']) && $state['published']->vid == $state['current']->vid) {
    return t('New draft');
  }

  // The latest draft is not published.
  return t('Edit draft');
}

/**
 * Change the name of the node view tab, conditionally.
 *
 * - Don't change the title if the content is not under moderation.
 *
 * - If a piece of content has a published revision, the "View" tab should be
 *   titled "View published".
 *
 * - Otherwise, it should be titled "View draft".
 *
 * @param $node
 *   The node being acted upon.
 *
 * @return
 *   The title for the tab.
 */
function workbench_moderation_view_tab_title($node) {
  // Use the normal tab title if the node type is not under moderation.
  if (!workbench_moderation_node_type_moderated($node->type)) {
    return t('View');
  }

  // Is there a published revision?
  $state = $node->workbench_moderation;
  if (!empty($state['published'])) {
    return t('View published');
  }
  return t('View draft');
}


/**
 * Implements hook_admin_paths().
 */
function workbench_moderation_admin_paths() {
  if (variable_get('node_admin_theme')) {
    $paths = array(
      'node/*/moderation' => TRUE,
    );
    return $paths;
  }
}

/**
 * Implements hook_theme().
 */
function workbench_moderation_theme() {
  return array(
    'workbench_moderation_admin_states_form' => array(
      'file' => 'workbench_moderation.admin.inc',
      'render element' => 'form',
    ),
    'workbench_moderation_admin_transitions_form' => array(
      'file' => 'workbench_moderation.admin.inc',
      'render element' => 'form',
    ),
  );
}

/**
 * Implements hook_permission().
 *
 * Provides permissions for each state to state change.
 */
function workbench_moderation_permission() {
  $permissions = array();
  $permissions['view all unpublished content'] = array(
    'title' => t('View all unpublished content'),
  );
  $permissions['administer workbench moderation'] = array(
    'title' => t('Administer Workbench Moderation'),
  );
  $permissions['bypass workbench moderation'] = array(
    'title' => t('Bypass moderation restrictions'),
    'restrict access' => TRUE,
  );
  $permissions['view moderation history'] = array(
    'title' => t('View moderation history'),
  );
  $permissions['view moderation messages'] = array(
    'title' => t('View the moderation messages on a node')
  );
  $permissions['unpublish live revision'] = array(
    'title' => t('Unpublish the current live revision'),
  );
  $permissions['use workbench_moderation my drafts tab'] = array(
    'title' => t('Use "My Drafts" workbench tab')
  );
  $permissions['use workbench_moderation needs review tab'] = array(
    'title' => t('Use "Needs Review" workbench tab')
  );

  // Per-node-type, per-transition permissions. Used by workbench_moderation_state_allowed().
  $node_types = workbench_moderation_moderate_node_types();
  $transitions = workbench_moderation_transitions();
  foreach ($transitions as $t) {
    $from_state = check_plain($t->from_name);
    $to_state = check_plain($t->to_name);
    // Always set a permission to perform all moderation states.
    $permissions["moderate content from $from_state to $to_state"] = array(
      'title' => t('Moderate all content from !from_state to !to_state', array('!from_state' => $from_state, '!to_state' => $to_state)),
    );
    // Per-node type permissions are very complex, and should only be used if
    // absolutely needed. For right now, this is hardcoded to OFF. To enable it,
    // Add this line to settings.php and then reset permissions.
    //   $conf['workbench_moderation_per_node_type'] = TRUE;
    if (variable_get('workbench_moderation_per_node_type', FALSE)) {
      foreach ($node_types as $node_type) {
        $permissions["moderate $node_type state from $from_state to $to_state"] = array(
          'title' => t('Moderate @node_type state from !from_state to !to_state', array('@node_type' => node_type_get_name($node_type), '!from_state' => $from_state, '!to_state' => $to_state)),
        );
      }
    }
  }
  return $permissions;
}

/**
 * Implements hook_node_access().
 *
 * Allows users with the 'view all unpublished content' permission to do so.
 */
function workbench_moderation_node_access($node, $op, $account) {
  if ($op == 'view' && !$node->status && user_access('view all unpublished content', $account)) {
    return NODE_ACCESS_ALLOW;
  }
  return NODE_ACCESS_IGNORE;
}

/**
 * Custom access handler for node operations.
 *
 * @param $op
 *   The operation being requested.
 * @param $node
 *   The node being acted upon.
 *
 * @return
 *   Boolean TRUE or FALSE.
 */
function _workbench_moderation_access($op, $node) {
  // If we do not control this node type, deny access.
  if (workbench_moderation_node_type_moderated($node->type) === FALSE) {
    return FALSE;
  }

  $access = TRUE;

  // The user must be able to view the moderation history.
  $access &= user_access('view moderation history');

  // The user must be able to edit this node.
  $access &= node_access('update', $node);

  if ($op == 'unpublish') {
    $access &= (user_access('unpublish live revision') || user_access('bypass workbench moderation'));
  }

  // Allow other modules to change our rule set.
  drupal_alter('workbench_moderation_access', $access, $op, $node);

  return $access;
}

/**
 * Wrapper for the 'revert' and 'delete' operations of _node_revision_access().
 *
 * Drupal core's "current revision" of a node is the version in {node}; for
 * Workbench Moderation, latest revision in {node_revision} is the current
 * revision. For nodes with a published revision, Workbench Moderation keeps
 * that revision in {node}, whether or not it is the current revision.
 */
function _workbench_moderation_revision_access($node, $op) {
  // Normal behavior for unmoderated nodes.
  if (!workbench_moderation_node_type_moderated($node->type)) {
    return _node_revision_access($node, $op);
  }

  // Prevent reverting to or deleting the current revision.
  if ($node->workbench_moderation['current']->vid == $node->workbench_moderation['my_revision']->vid) {
    return FALSE;
  }

  // Temporarily give the node an impossible revision.
  // _node_revision_access() keeps access check results in a static variable
  // indexed by revision only, not by op. Thus, subsequent checks on the same
  // vid for different ops yield the same result, regardless of permissions.
  // Setting a fake vid here allows us to store different static results per op.
  $tmp = $node->vid;
  switch ($op) {
    case 'update':
      $node->vid = -1;
      break;
    case 'delete':
      $node->vid = -2;
      break;
  }

  // Check access.
  $access = _node_revision_access($node, $op);

  // Restore the original revision id.
  $node->vid = $tmp;
  return $access;
}

/**
 * Checks if a user may make a particular transition on a node.
 *
 * @param $node
 *   The node being acted upon.
 * @param $state
 *   The new moderation state.
 *
 * @return
 *   Booelan TRUE or FALSE.
 */
function _workbench_moderation_moderate_access($node, $state) {
  global $user;

  $my_revision = $node->workbench_moderation['my_revision'];
  $next_states = workbench_moderation_states_next($my_revision->state, $user, $node->type);
  $access = node_access('update', $node, $user)       // the user can edit the node
          && $my_revision->current                    // this is the current revision (no branching the revision history)
          && (!empty($next_states))                   // there are next states the user may transition to
          && isset($next_states[$state]);             // this state is in the available next states

  // Allow other modules to change our rule set.
  $op = 'moderate';
  drupal_alter('workbench_moderation_access', $access, $op, $node);

  return $access;
}

/**
 * Checks if the user can view the current node revision.
 *
 * This is the access callback for node/%node/draft as defined in
 * workbench_moderation_menu().
 *
 * @param $node
 *   The node being acted upon.
 *
 * @return
 *   Booelan TRUE or FALSE.
 */
function _workbench_moderation_access_current_draft($node) {

  // This tab should only appear for node types under moderation
  if (!workbench_moderation_node_type_moderated($node->type)) {
    return FALSE;
  }

  $state = $node->workbench_moderation;
  return (_workbench_moderation_access('view revisions', $node)
      && !empty($state['published'])
      && $state['published']->vid != $state['current']->vid);
}

/**
 * Implements hook_help().
 */
function workbench_moderation_help($path, $arg) {
  switch ($path) {
    case 'admin/help#workbench_moderation':
      return '<p>' . t("Enables you to control node display with a moderation
        workflow. You can have a 'Live revision' for all visitors and pending
        revisions which need to be approved to become the new 'Live Revision.'") . '</p>';

    case 'admin/config/workbench/moderation':
      return '<p>' . t("These are the states through which a node passes in order to become published. By
        default, the Workbench Moderation module provides the states 'Draft,' 'Needs Review,' and 'Published.'
        On this screen you may create, delete and re-order states. Additional states might include
        'Legal Review,' 'PR Review,' or any or state your site may need.") . '</p>';

    case 'admin/config/workbench/moderation/transitions':
      return '<p>' . t("The Workbench Moderation module keeeps track of when a node moves from one state to
        another. By default, nodes begin in the %draft state and end in the %published state. The transitions
        on this page control how nodes move from state to state.", array('%draft' => workbench_moderation_state_none(),
        '%published' => workbench_moderation_state_published())) . ' ' . l(t('Permission to perform these transitions
        is controlled on a role-by-role basis.'), 'admin/people/permissions', array('fragment' => 'module-workbench_moderation')) . '</p>';

    case 'admin/config/workbench/moderation/check-permissions':
      return '<p>' . t("In order to participate in the moderation process,
        Drupal users must be granted several node- and moderation- related
        permissions. This page can help check whether roles have the correct
        permissions to author, edit, moderate, and publish moderated content.") . '</p>';
  }
}

/**
 * Implements hook_views_api().
 */
function workbench_moderation_views_api() {
  return array('api' => 2.0);
}

/**
 * Implements hook_views_default_views().
 */
function workbench_moderation_views_default_views() {
  $module = 'workbench_moderation';
  $directory = 'views';
  $extension = 'view.inc';
  $name = 'view';

  // From workbench_load_all_exports().
  $return = array();
  // Find all the files in the directory with the correct extension.
  $files = file_scan_directory(drupal_get_path('module', $module) . "/$directory", "/.$extension/");
  foreach ($files as $path => $file) {
    require $path;
    if (isset($$name)) {
      $return[$$name->name] = $$name;
    }
  }

  return $return;
}


/**
 * Implements hook_node_presave().
 *
 * Ensure that a node in moderation has the proper publication status.
 * We set $node->status = 0 (unpublished) if this is a new node which has not
 * been marked as published, or if the node has no published revision.
 */
function workbench_moderation_node_presave($node) {
  global $user;
  if (isset($node->workbench_moderation_state_new)) {
    // If the new moderation state is published, or if this revision is the
    // published revision, set the node status to published.
    if ($node->workbench_moderation_state_new == workbench_moderation_state_published() || (!empty($node->workbench_moderation['published']) && $node->vid == $node->workbench_moderation['published']->vid)) {
      $node->status = 1;
    }
    else {
      $node->status = 0;
    }
  }
}

/**
 * Implements hook_node_insert().
 *
 * Wrapper call to the update hook.
 */
function workbench_moderation_node_insert($node) {
  workbench_moderation_node_update($node);
}

/**
 * Implements hook_node_update().
 *
 * Handles the submit of the node form moderation information
 */
function workbench_moderation_node_update($node) {
  global $user;

  // Don't proceed if moderation is not enabled on this content type, or if
  // we're replacing an already-published revision.
  if (!workbench_moderation_node_type_moderated($node->type) ||
      !empty($node->workbench_moderation['updating_live_revision'])) {
    return;
  }

  // Set default moderation state values.
  if (!isset($node->workbench_moderation_state_current)) {
    $node->workbench_moderation_state_current = ($node->status ? workbench_moderation_state_published() : workbench_moderation_state_none());
  };
  if (!isset($node->workbench_moderation_state_new)) {
    $node->workbench_moderation_state_new = variable_get('workbench_moderation_default_state_' . $node->type);
  };

  // If this is a new node, give it some information about 'my revision'.
  if (!isset($node->workbench_moderation)) {
    $node->workbench_moderation = array();
    $node->workbench_moderation['my_revision'] = $node->workbench_moderation['current'] = (object) array(
      'from_state' => workbench_moderation_state_none(),
      'state' => workbench_moderation_state_none(),
      'nid' => $node->nid,
      'vid' => $node->vid,
      'uid' => $user->uid,
      'current' => TRUE,
      'published' => FALSE,
      'stamp' => $node->changed,
    );
  }

  // Apply moderation changes if this is a new revision or if the moderation
  // state has changed.
  if (!empty($node->revision) || $node->workbench_moderation_state_current != $node->workbench_moderation_state_new) {
    // Update attached fields.
    field_attach_update('node', $node);
    // Moderate the node.
    workbench_moderation_moderate($node, $node->workbench_moderation_state_new);
  }

  return;
}

/**
 * Implements hook_node_load().
 *
 * Load moderation history and status on a node.
 */
function workbench_moderation_node_load($nodes, $types) {
  foreach ($nodes as $node) {
    // Add the node history
    workbench_moderation_node_data($node);
  }
}

/**
 * Implements hook_node_view().
 *
 * Display messages about the node's moderation state.
 */
function workbench_moderation_node_view($node, $view_mode = 'full') {
  // Show moderation state messages if we're on a node page.
  if (menu_get_object('node', 1)) {
    workbench_moderation_messages('view', $node);
  }
}

/**
 * Implements hook_node_delete().
 */
function workbench_moderation_node_delete($node) {
  // Delete node history when it is deleted.
  db_delete('workbench_moderation_node_history')
    ->condition('nid', $node->nid)
    ->execute();
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Add moderation rules to node types.
 */
function workbench_moderation_form_node_type_form_alter(&$form, $form_state) {
  // Get a list of moderation states.
  $options = array();
  foreach (workbench_moderation_states() as $key => $state) {
    $options[$key] = check_plain($key . ': ' . $state->description);
  }

  // Disable the 'revision' checkbox when the 'moderation' checkbox is checked, so that moderation
  // can not be enabled unless revisions are enabled.
  $form['workflow']['node_options']['revision']['#states'] = array(
    'disabled' => array(':input[name="node_options[moderation]"]' => array('checked' => TRUE)),
  );

  // Disable the 'moderation' checkbox when the 'revision' checkbox is not checked, so that
  // revisions can not be turned off without also turning off moderation.
  $form['workflow']['node_options']['#options']['moderation'] = t('Enable moderation of revisions');
  $form['workflow']['node_options']['moderation']['#description'] = t('Revisions must be enabled in order to use moderation.');
  $form['workflow']['node_options']['moderation']['#states'] = array(
    'disabled' => array(':input[name="node_options[revision]"]' => array('checked' => FALSE)),
  );

  // This select element is hidden when moderation is not enabled.
  $form['workflow']['workbench_moderation_default_state'] = array(
    '#title' => t('Default moderation state'),
    '#type' => 'select',
    '#options' => $options,
    '#default_value' => variable_get('workbench_moderation_default_state_' . $form['#node_type']->type),
    '#description' => t('Set the default moderation state for this content type. Users with additional moderation permissions will be able to set the moderation state when creating or editing nodes.'),
    '#states' => array(
      'visible' => array(':input[name="node_options[moderation]"]' => array('checked' => TRUE)),
    ),
  );
  $form['#validate'][] = 'workbench_moderation_node_type_form_validate';
}

/**
 * Validate the content type form.
 */
function workbench_moderation_node_type_form_validate($from, &$form_state) {
  // Ensure that revisions are enabled if moderation is.
  if ($form_state['values']['node_options']['moderation']) {
    $form_state['values']['node_options']['status'] = 0;
    $form_state['values']['node_options']['revision'] = 1;
  }
}

/**
 * Implements hook_form_alter().
 *
 * Forcing new reversion and publishing.
 *
 * @TODO: Use the node_forms alter instead?
 */
function workbench_moderation_form_alter(&$form, $form_state, $form_id) {
  global $user;

  // This must be a node form and a type that has moderation enabled
  if (!(isset($form['#node_edit_form']) && workbench_moderation_node_type_moderated($form['type']['#value']))) {
    return;
  }
  // Set a moderation state even if there is not one defined
  if (isset($form['#node']->workbench_moderation['current']->state)) {
    $moderation_state = $form['#node']->workbench_moderation['current']->state;
  }
  else {
    $moderation_state = variable_get('workbench_moderation_default_state_' . $form['type']['#value']);
  }

  // Store the current moderation state
  $form['workbench_moderation_state_current'] = array(
    '#type' => 'value',
    '#value' => $moderation_state
  );
  // We have a use case where a live node is being edited. This will always
  // revert back to the original node status.
  if ($moderation_state == workbench_moderation_state_published()) {
    $moderation_state = workbench_moderation_state_none();
  }
  // Get all the states *this* user can access. If states is false, this user
  // can not change the moderation state
  if ($states = workbench_moderation_states_next($moderation_state, $user, $form['type']['#value'])) {
    $current = array($moderation_state => t('Current: !state', array('!state' => $moderation_state)));
    $states = array_merge($current, $states);
    $form['options']['workbench_moderation_state_new'] = array(
      '#title' => t('Moderation state'),
      '#type' => 'select',
      '#options' => $states,
      '#default_value' => $moderation_state,
      '#description' => t('Set the moderation state for this content.'),
      '#access' => $states ? TRUE: FALSE,
    );
  }
  else {
    // Store the current moderation state
    $form['workbench_moderation_state_new'] = array(
      '#type' => 'value',
      '#value' => $moderation_state
    );
  }

  // Always create new revisions for nodes that are moderated
  $form['revision_information']['revision'] = array(
    '#type' => 'value',
    '#value' => TRUE,
  );

  // Set a default revision log message.
  $logged_name = (user_is_anonymous() ? variable_get('anonymous', t('Anonymous')) : $user->name);
  $logged_action = !empty($form['#node']->nid) ? t('Edited') : t('Created');
  $form['revision_information']['log']['#default_value'] = t('!action by @user.', array('!action' => $logged_action, '@user' => $logged_name));

  // Move the revision log into the publishing options to make things pretty.
  if ($form['options']['#access']) {
    $form['options']['log'] = $form['revision_information']['log'];
    $form['options']['log']['#title'] = t('Moderation notes');

    // Unset the old placement of the Revision log.
    unset($form['revision_information']['log']);

    // The revision information section should now be empty.
    $form['revision_information']['#access'] = FALSE;
  }

  // Users can not choose to publish content; content can only be published by
  // setting the content's moderation state to "Published".
  $form['options']['status']['#access'] = FALSE;
  $form['actions']['submit']['#submit'][] = 'workbench_moderation_node_form_submit';
  workbench_moderation_messages('edit', $form['#node']);
}

/**
 * Redirect to the current revision of a node after editing.
 */
function workbench_moderation_node_form_submit($form, &$form_state) {
  $form_state['redirect'] = array('node/' . $form_state['node']->nid . '/current-revision');
}

/**
 * Overrides the node/%/edit page to ensure the proper revision is shown.
 *
 * @param $node
 *   The node being acted upon.
 * @return
 *   A node editing form.
 */
function workbench_moderation_node_edit_page_override($node) {
  // Check to see if this is an existing node
  if (isset($node->nid)) {
    if (workbench_moderation_node_type_moderated($node->type)) {
      // Load the node moderation data
      workbench_moderation_node_data($node);
      // We ONLY edit the current revision
      $node = workbench_moderation_node_current_load($node);
    }
  }
  // Ensure we have the editing code.
  module_load_include('inc', 'node', 'node.pages');
  return node_page_edit($node);
}

/**
 * Returns the key which represents the live revision.
 *
 * @TODO: make this configurable.
 */
function workbench_moderation_state_published() {
  return 'Published';
}

/**
 * Returns the key which represents the neutral non moderated revision.
 *
 * @TODO: make this configurable.
 */
function workbench_moderation_state_none() {
  return 'Draft';
}

/**
 * Determines if this content type is set to be moderated
 *
 * @param $type
 *   String, content type name
 *
 * @return boolean
 */
function workbench_moderation_node_type_moderated($type) {
  // Is this content even in moderatation?
  $options = variable_get("node_options_$type", array());
  if (in_array('revision', $options) && in_array('moderation', $options)) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Lists content types that are moderated
 */
function workbench_moderation_moderate_node_types() {
  $types = node_type_get_types();
  $result = array();
  foreach ($types as $type) {
    // Is this content even in moderatation?
    if (workbench_moderation_node_type_moderated($type->type)) {
      $result[] = $type->type;
    }
  }
  return $result;
}

/**
 * Checks if a user may change the state of a node.
 *
 * This check is based on transition and node type. Users
 * with the 'bypass workbench moderation' permission may make any state
 * transition.
 *
 * @see workbench_moderation_permission()
 *
 * Note that we do not use content-type specific moderation by default. To
 * enable that, see the instructions in workbench_moderation_permission().
 *
 * @param $account
 *   The user account being checked.
 * @param $from_state
 *   The original moderation state.
 * @param $to_state
 *   The new moderation state.
 *
 * @return
 *   Bollean TRUE or FALSE.
 */
function workbench_moderation_state_allowed($account, $from_state, $to_state, $node_type) {
  // Allow super-users to moderate all content.
  if (user_access("bypass workbench moderation", $account)) {
    return TRUE;
  }

  // Can this user moderate all content for this transition?
  if (user_access("moderate content from $from_state to $to_state", $account)) {
    return TRUE;
  }

  // Are we using complex node type rules for this transition?
  if (variable_get('workbench_moderation_per_node_type', FALSE) && user_access("moderate $node_type state from $from_state to $to_state", $account)) {
    return TRUE;
  }

  // Default return.
  return FALSE;
}

/**
 * Adds current and live revision data to a node.
 *
 * @param $node
 *   The node being acted upon.
 */
function workbench_moderation_node_data($node) {
  // Make sure that this node type is moderated.
  if (!workbench_moderation_node_type_moderated($node->type)) {
    return;
  }

  // Path module is stupid and doesn't load its data in node_load.
  if (module_exists('path') && isset($node->nid)) {
    $path = array();
    $conditions = array('source' => 'node/' . $node->nid);
    if ($node->language != LANGUAGE_NONE) {
      $conditions['language'] = $node->language;
    }
    $path = path_load($conditions);
    if ($path === FALSE) {
      $path = array();
    }
    if (isset($node->path)) {
      $path += $node->path;
    }
    $path += array(
      'pid' => NULL,
      'source' => 'node/' . $node->nid,
      'alias' => '',
      'language' => isset($node->language) ? $node->language : LANGUAGE_NONE,
    );

    $node->path = $path;
  }
  // Build a default 'current' moderation record. Nodes will lack a
  // workbench_moderation record if moderation was not enabled for their node
  // type when they were created. In that case, assume the live node is at the
  // current revision.
  $defaults = array(
    'hid' => NULL,
    'nid' => $node->nid,
    'vid' => $node->vid,
    'from_state' => workbench_moderation_state_none(),
    'state' => ($node->status ? workbench_moderation_state_published() : workbench_moderation_state_none()),
    'uid' => $node->uid,
    'stamp' => $node->changed,
    'published' => $node->status,
    'current' => 1,
  );

  // We'll store moderation state information in an array on the node.
  $node->workbench_moderation = array();

  // Fetch the most recent revision from the {node_revision} table. This is the
  // current revision ("head").
  $query = db_select('node_revision', 'r');
  $query->addJoin('LEFT OUTER', 'workbench_moderation_node_history', 'm', 'r.vid = %alias.vid');
  $query->condition('r.nid', $node->nid)
    ->orderBy('r.vid', 'DESC')
    ->orderBy('m.hid', 'DESC')
    ->fields('m')
    ->fields('r', array('title', 'timestamp'));
  $current = $query->execute()->fetchObject();

  if (!$current) {
    $current = (object) $defaults;
  }
  else {
    // Fill in any defaults that are missing from the database record. We need
    // to maintain false-y values except for NULL, so array_filter() +
    // array_merge() wouldn't work here.
    foreach (array_keys($defaults) as $key) {
      if (is_null($current->$key)) {
        $current->$key = $defaults[$key];
      }
    }
  }

  $current->current = 1;
  $node->workbench_moderation['current'] = $current;

  // Fetch the published revision. There may not be a workbench_moderation
  // record for some nodes, but in those cases if the node is published,
  // $current->published will be TRUE.
  if ($current->published) {
    $published = $current;
  }
  else {
    // Fetch the most recent published revision.
    $query = db_select('node', 'n');
    $query->addJoin('INNER', 'node_revision', 'r', 'n.vid = r.vid');
    $query->addJoin('LEFT OUTER', 'workbench_moderation_node_history', 'm', 'r.vid = m.vid');
    $query->condition('n.nid', $node->nid)
      ->condition('n.status', 1)
      ->orderBy('m.hid', 'DESC')
      ->fields('r', array('nid', 'vid', 'title', 'timestamp'))
      ->fields('m');
    $published = $query->execute()->fetchObject();
  }
  // If we have a published copy, add that to the array.
  if ($published) {
    $published->state = workbench_moderation_state_published();
    $node->workbench_moderation['published'] = $published;
  }

  // Fetch the workbench_moderation record for this node object's revision. If
  // it is either the current or published revision of the node, that data will
  // be used.
  if ($node->vid == $current->vid) {
    $my_revision = $current;
  }
  elseif ($published && $node->vid == $published->vid) {
    $my_revision = $published;
  }
  else {
    $query = db_select('node_revision', 'r');
    $query->addJoin('LEFT OUTER', 'workbench_moderation_node_history', 'm', 'r.vid = m.vid');
    $query->condition('m.vid', $node->vid)
      ->orderBy('m.hid', 'DESC')
      ->fields('m')
      ->fields('r', array('nid', 'vid', 'title', 'timestamp'));
    $my_revision = $query->execute()->fetchObject();

    // This might happen if you're turning workbench_moderation on and off, but
    // it should be really rare. Workbench_moderation must have recorded a
    // current revision, and then the node table must contain a different and
    // unpublished revision.
    if (!$my_revision) {
      $my_revision = (object) array(
        'hid' => NULL,
        'nid' => $node->nid,
        'vid' => $node->vid,
        'from_state' => workbench_moderation_state_none(),
        'state' => workbench_moderation_state_none(),
        'uid' => $node->uid,
        'stamp' => $node->changed,
        'published' => 0,
        'current' => 0,
      );
    }
  }
  // Add my revision to the array.
  $node->workbench_moderation['my_revision'] = $my_revision;
}

/**
 * Utility function to load the current revision of a node.
 *
 * @param $node
 *   The node being acted upon.
 *
 * @return
 *   The current node according to moderation.
 */
function workbench_moderation_node_current_load($node) {
  // Is there a current revision?
  if (isset($node->workbench_moderation['current']->vid)) {
    // Ensure that we will return the current revision
    if ($node->vid != $node->workbench_moderation['current']->vid) {
      $node = node_load($node->nid, $node->workbench_moderation['current']->vid);
    }
  }
  return $node;
}


/**
 * Utility function to load the live revision of a node.
 *
 * This is encapsulated so that changes to how the moderation data is stored
 * will not impact the API.
 *
 * @param $node
 *   The node being acted upon.
 *
 * @return
 *   The node object of the live revision.
 */
function workbench_moderation_node_live_load($node) {
  // Is there a live revision of this node?
  if (isset($node->workbench_moderation['published']->vid)) {
    // If the live revision is not this revision, we need to load that revision
    if ($node->vid != $node->workbench_moderation['published']->vid) {
      return node_load($node->nid, $node->workbench_moderation['published']->vid, TRUE);
    }
    // This is the live node, return it
    return $node;
  }
}


/**
 * Utility function to determine if this node is in the live state.
 *
 * @param $node
 *   The node being acted upon.
 *
 * @return
 *   Boolean TRUE if this is the current revision. FALSE if not.
 */
function workbench_moderation_node_is_current($node) {
  if (isset($node->workbench_moderation['published']->vid) && isset($node->workbench_moderation['current']->vid)) {
    if ($node->workbench_moderation['published']->vid == $node->workbench_moderation['current']->vid) {
      return TRUE;
    }
    return FALSE;
  }
  // If not set, then TRUE.
  return TRUE;
}

/**
 * Get a list of all moderation states.
 *
 * @return
 *   An array of state objects, keyed by state name and ordered by weight. Each
 *   state object has name, description, and weight properties.
 */
function workbench_moderation_states() {
  static $states;
  if (!isset($states)) {
    $states = db_select('workbench_moderation_states', 'states')
      ->fields('states', array('name', 'description', 'weight'))
      ->orderBy('states.weight', 'ASC')
      ->execute()
      ->fetchAllAssoc('name');
  }
  return $states;
}

/**
 * Saves a new or existing moderation state.
 *
 * Moderation state names must be unique, so saving a state object with a
 * non-unique name updates the existing state.
 *
 * @TODO: add a hook here.
 *
 * @param $state
 *   An object with name, description, and weight properties.
 */
function workbench_moderation_state_save($state) {
  return db_merge('workbench_moderation_states')
    ->key(array('name' => $state->name))
    ->fields((array) $state)
    ->execute();
}

/**
 * Deletes a moderation state.
 *
 * This function also deletes any transitions that reference the deleted
 * moderation state.
 *
 * @TODO: add a hook here.
 *
 * @param $state
 *   An object with at least a name property.
 *
 */
function workbench_moderation_state_delete($state) {
  db_delete('workbench_moderation_states')
    ->condition('name', $state->name)
    ->execute();

  db_delete('workbench_moderation_transitions')
    ->condition(db_or()->condition('from_name', $state->name)->condition('to_name', $state->name))
    ->execute();
}

/**
 * Returns a list of all moderation state transitions.
 *
 * @return
 *   An array of transition objects, each with from_name and to_name properties
 *   that reference moderation states. The array is ordered by the weight of the
 *   'from' states, then by the weight of the 'to' states.
 */
function workbench_moderation_transitions() {
  static $transitions;
  if (!isset($transitions)) {
    $query = db_select('workbench_moderation_transitions', 't')
      ->fields('t', array('from_name', 'to_name'));

    $alias_from = $query->addJoin('INNER', 'workbench_moderation_states', NULL, 't.from_name = %alias.name');
    $alias_to = $query->addJoin('INNER', 'workbench_moderation_states', NULL, 't.to_name = %alias.name');

    $query
      ->orderBy("$alias_from.weight", 'ASC')
      ->orderBy("$alias_to.weight", 'ASC');

    $transitions = $query->execute()->fetchAll();
  }
  return $transitions;
}

/**
 * Saves a moderation state transition.
 *
 * @TODO: add a hook here.
 *
 * @param $transition
 *   An object with from_name and to_name properties that reference moderation
 *   states.
 */
function workbench_moderation_transition_save($transition) {
  return db_merge('workbench_moderation_transitions')
    ->key(array('from_name' => $transition->from_name, 'to_name' => $transition->to_name))
    ->fields((array) $transition)
    ->execute();
}

/**
 * Deletes a moderation state transition.
 *
 * @TODO: add a hook here.
 *
 * @param $transition
 *   An object with from_name and to_name properties that reference moderation
 *   states.
 */
function workbench_moderation_transition_delete($transition) {
  db_delete('workbench_moderation_transitions')
    ->condition('from_name', $transition->from_name)
    ->condition('to_name', $transition->to_name)
    ->execute();
}

/**
 * Provides a list of possible next states for this node.
 *
 * This function is used in permissions checks, so it should never return
 * disallowed transitions.
 *
 * @param $current_state
 *   The current moderation state.
 * @param $account
 *   The user object being checked.
 * @param $node_type
 *   The node type being acted upon.
 *
 * @return
 *   If the user may moderate a change, return an array of possible state
 *   changes. Otherwise, return FALSE.
 */
function workbench_moderation_states_next($current_state, $account, $node_type) {
  // Make sure we have a current state.
  if (!$current_state) {
    $current_state = workbench_moderation_state_none();
  }

  if (!$account) {
    $account = $user;
  }

  if (user_access('bypass workbench moderation', $account)) {
    // Some functions expect an array of $state => $state pairs.
    $states = drupal_map_assoc(array_keys(workbench_moderation_states()));
    unset($states[$current_state]);
    return $states;
  }
  else {
    // Get a list of possible transitions.
    $states = db_select('workbench_moderation_transitions', 'transitions')
      ->condition('transitions.from_name', $current_state)
      ->fields('transitions', array('to_name'))
      ->execute()
      ->fetchAllKeyed(0, 0);

    // Checks whether the user has permission to make each transition. The
    // 'bypass workbench moderation' permission is accounted for in
    // workbench_moderation_state_allowed().
    if ($states) {
      foreach ($states as $state) {
        if (!workbench_moderation_state_allowed($account, $current_state, $state, $node_type)) {
          unset($states[$state]);
        }
      }
      return $states;
    }
  }

  return FALSE;
}

/**
 * Provide quick moderation of nodes.
 *
 * Access is controlled by the menu router to these pseudo-form callbacks.
 * This function is also abstracted so that it can be called from any node
 * context.
 *
 * @see _workbench_moderation_moderate_access()
 * @see workbench_moderation_menu()
 * @see workbench_moderation_node_update()
 *
 * @param $node
 *   The node being acted upon.
 * @param $state
 *   The new moderation state requested.
 */
function workbench_moderation_moderate($node, $state) {
  global $user;

  $old_revision = $node->workbench_moderation['my_revision'];

  // Get the number of revisions for this node with vids greater than $node->vid
  $vid_count = db_select('node_revision', 'r')
    ->condition('r.nid', $node->nid)
    ->condition('r.vid', $node->vid, '>')
    ->countQuery()->execute()->fetchField();
  // If the number of greater vids is 0, then this is the most current revision
  $current = ($vid_count == 0);

  // Build a history record.
  $new_revision = (object) array(
    'from_state' => $old_revision->state,
    'state' => $state,
    'nid' => $node->nid,
    'vid' => $node->vid,
    'uid' => $user->uid,
    'current' => $current,
    'published' => ($state == workbench_moderation_state_published()),
    'stamp' => $_SERVER['REQUEST_TIME'],
  );

  // If this is the new 'current' moderation record, it should be the only one
  // flagged 'current' in {workbench_moderation_node_history}.
  if ($new_revision->current) {
    $query = db_update('workbench_moderation_node_history')
      ->condition('nid', $node->nid)
      ->fields(array('current' => 0))
      ->execute();
  }

  // If this revision is to be published, the new moderation record should be
  // the only one flagged 'published' in both
  // {workbench_moderation_node_history} AND {node_revision}
  if ($new_revision->published) {
    $query = db_update('workbench_moderation_node_history')
      ->condition('nid', $node->nid)
      ->fields(array('published' => 0))
      ->execute();
    $query = db_update('node_revision')
      ->condition('nid', $node->nid)
      ->fields(array('status' => 0))
      ->execute();
  }

  // Save the node history record.
  drupal_write_record('workbench_moderation_node_history', $new_revision);

  // Update the node's content_moderation information so that we can publish it
  // if necessary.
  $node->workbench_moderation['my_revision'] = $new_revision;
  if ($new_revision->current) {
    $node->workbench_moderation['current'] = $new_revision;
  }
  // Handle the published revision.
  if ($new_revision->published) {
    // If we're moderating a revision to the published state, mark the new
    // revision as the published revision.
    $node->workbench_moderation['published'] = $new_revision;
  }
  elseif (isset($node->workbench_moderation['published']) && $new_revision->vid == $node->workbench_moderation['published']->vid && $new_revision->from_state == workbench_moderation_state_published()) {
    // If we're moderating the published revision to a non-published state,
    // remove the workbench moderation 'published' property.
    unset($node->workbench_moderation['published']);
  }

  // If we're moderating an unpublished revision and there is an existing
  // published revision, make sure that the published revision is live.
  // We do this in a shutdown function to avoid race conditions when
  // running node_save() from within a node submission.
  if (!empty($node->workbench_moderation['published'])) {
    drupal_register_shutdown_function('workbench_moderation_store', $node);
  }
}

/**
 * Shutdown callback for saving a node revision.
 *
 * This function is called by drupal_register_shutdown_function().
 * The purpose is to delay a node_save() call so that a live revision
 * is not called during hook_node_update().
 *
 * Instead, we delay the update until the new revision is saved. This way,
 * we can more safely call the revision and pick up changes to items
 * that are not revisioned (such as menu and path assignments).
 *
 * @see workbench_moderation_moderate()
 *
 * @param $node
 *   The node being saved.
 */
function workbench_moderation_store($node) {
  if (!isset($node->nid)) {
    watchdog('Workbench moderation', 'Failed to save node revision: node not passed to shutdown function.', array(), WATCHDOG_NOTICE);
    return;
  }
  watchdog('Workbench moderation', 'Saved node revision: %node as live version for node %live.', array('%node' => $node->vid, '%live' => $node->nid), WATCHDOG_NOTICE, l($node->title, 'node/' . $node->nid));
  $live_revision = workbench_moderation_node_live_load($node);
  // Make sure we're published.
  $live_revision->status = 1;
  // Don't create a new revision.
  $live_revision->revision = 0;
  // Prevent another moderation record from being written.
  $live_revision->workbench_moderation['updating_live_revision'] = TRUE;

  // Reset flag from taxonomy_field_update() so that {taxonomy_index} values aren't written twice.
  $taxonomy_index_flag = &drupal_static('taxonomy_field_update', array());
  unset($taxonomy_index_flag[$node->nid]);

  // Save the node.
  node_save($live_revision);
}

/**
 * Helper function to redirect after a state change submission.
 *
 * @param $node
 *   The node being acted upon.
 * @param $state
 *   The new moderation state requested.
 */
function workbench_moderation_moderate_callback($node, $state) {
  workbench_moderation_moderate($node, $state);
  drupal_goto(isset($_GET['destination']) ? $_GET['destination'] : 'node/' . $node->nid . '/moderation');
}

/**
 * Generates a list of links to available moderation actions.
 *
 * @param $node
 *   The node being acted upon.
 * @param $url_options
 *   An array of options to pass, following the url() function syntax.
 *
 * @return
 *   A list of links to display with the revision.
 */
function workbench_moderation_get_moderation_links($node, $url_options = array()) {
  global $user;

  // Make sure that this node type is moderated.
  if (!workbench_moderation_node_type_moderated($node->type)) {
    return;
  }

  // Build links to available moderation states.
  $links = array();
  $my_revision = $node->workbench_moderation['my_revision'];
  if ($my_revision->vid == $node->workbench_moderation['current']->vid
      && $next_states = workbench_moderation_states_next($my_revision->state, $user, $node->type)) {
    foreach ($next_states as $state => $label) {
      $links[] = array_merge($url_options, array(
        'title' => $state,
        'href' => "node/{$node->nid}/moderation/{$node->vid}/change-state/{$state}",
      ));
    }
  }

  return $links;
}

/**
 * Generates a moderation form for a node.
 *
 * The caller of this form needs to check whether the node is in moderation.
 *
 * @param $node
 *   The node being acted upon.
 *
 * @return $form
 *   A Drupal Forms API array.
 */
function workbench_moderation_moderate_form($form, &$form_state, $node, $destination = NULL) {
  global $user;
  $form = array();

  // Build links to available moderation states.
  $links = array();
  $my_revision = $node->workbench_moderation['my_revision'];
  if ($my_revision->vid == $node->workbench_moderation['current']->vid
      && $next_states = workbench_moderation_states_next($my_revision->state, $user, $node->type)) {
    $form['#destination'] = $destination;
    $form['node'] = array(
      '#type' => 'value',
      '#value' => $node,
    );
    $form['#attributes']['class'][] = 'workbench-moderation-moderate-form';
    $form['#attached']['css'][] = drupal_get_path('module', 'workbench_moderation') . '/css/workbench_moderation.css';
    $form['state'] = array(
      '#type' => 'select',
      '#options' => $next_states,
      '#default_value' => $node->workbench_moderation['current']->state,
    );
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Apply'),
    );

    // Cache the form on first load to preserve the node for validation.
    // Otherwise, the node would be reloaded on submit, and there would be no
    // way to detect if the current revision has been changed.
    $form_state['cache'] = TRUE;
  }
  else {
    $form['#access'] = FALSE;
  }

  return $form;
}

function workbench_moderation_moderate_form_validate($form, &$form_state) {
  // Make sure that the revision that was shown to the user is still the current
  // revision before changing the current revision's state.
  $moderated_node = $form_state['values']['node'];
  $current_node = workbench_moderation_node_current_load(node_load($moderated_node->nid));
  if ($moderated_node->vid != $current_node->vid) {
    form_set_error('', t('The moderation state could not be changed because the draft has been updated by another user. Please review the current draft.'));
    // Redirect the form so that it rebuilds with the current revision.
    drupal_redirect_form($form_state);
  }
}

function workbench_moderation_moderate_form_submit($form, $form_state) {
  if (_workbench_moderation_moderate_access($form_state['values']['node'], $form_state['values']['state'])) {
    workbench_moderation_moderate($form_state['values']['node'], $form_state['values']['state']);
  }

  // This is not ideal, but if the form is invoked from a node's draft tab and
  // used to publish the node, the draft tab will not be available after
  // publishing, and Drupal's will throw an access denied error before it is
  // able to redirect to the published revision.
  if (!empty($form['#destination'])) {
    if ($form_state['values']['state'] == workbench_moderation_state_published()) {
      $form_state['redirect'] = $form['node']['#value']->uri['path'];
    }
    else {
      $form_state['redirect'] = $form['#destination'];
    }
    drupal_redirect_form($form_state);
  }
}

/**
 * Sets status messages for a node.
 *
 * Note that these status messages aren't relevant to the session, only the
 * current page view.
 *
 * @see workbench_moderation_set_message()
 *
 * @param $context
 *   A string, either 'view' or 'edit'.
 * @param $node
 *   A node object. The current menu object will be used if it is a node and
 *   this variable was not set.
 */
function workbench_moderation_messages($context, $node = NULL) {
  if (!user_access('view moderation messages')
      || (!$node && !($node = menu_get_object()))
      || !workbench_moderation_node_type_moderated($node->type)) {
    return;
  }

  $node_published = FALSE;
  $revision_published = FALSE;
  $revision_current = FALSE;

  // For new content, this property will not be set.
  if (isset($node->workbench_moderation)) {
    $state = $node->workbench_moderation;
    if (!empty($state['published'])) {
      $node_published = TRUE;
    }
    if ($state['my_revision']->published) {
      $revision_published = TRUE;
    }
    if ($state['my_revision']->vid == $state['current']->vid) {
      $revision_current = TRUE;
    }
  }

  // An array of messages to add to the general workbench block.
  $info_block_messages = array();

  if ($context == 'view') {
    $info_block_messages[] = array(
      'label' => t('Revision state'),
      'message' => check_plain($state['my_revision']->state),
    );
    $info_block_messages[] = array(
      'label' => t('Current draft'),
      'message' => !empty($revision_current) ? t('Yes') : t('No'),
    );

    // Check node access.
    drupal_static('_node_revision_access', array(), TRUE);

    // Add a moderation form.
    if ($revision_current && !$revision_published && _workbench_moderation_access('update', $node) && $moderate_form = drupal_get_form('workbench_moderation_moderate_form', $node, "node/{$node->nid}/current-revision")) {
      if ($moderate_form = drupal_render($moderate_form)) {
        $info_block_messages[] = array(
          'label' => t('Moderate'),
          'message' => $moderate_form,
        );
      }
    }

    // Add an unpublish link.
    if ($revision_published && user_access('unpublish live revision') && $link = workbench_moderation_access_link(t('Unpublish this revision'), "node/{$node->nid}/moderation/{$node->vid}/unpublish")) {
      $info_block_messages[] = array(
        'label' => t('Actions'),
        'message' => $link,
      );
    }

    // Revision navigation links. This is disabled for the time being, since
    // node tabs are lost when navigating through old revisions.
    // @TODO remove this entirely?
    if (variable_get('workbench_moderation_show_revision_navigation', FALSE) && user_access('view revisions')) {
      $links = array();

      // Get previous and next revision ids.
      $args = array(':nid' => $node->nid, ':vid' => $node->vid);
      if ($prev_vid = db_query_range("SELECT nr.vid FROM {node_revision} nr WHERE nr.nid = :nid AND nr.vid < :vid ORDER BY nr.vid DESC", 0, 1, $args)->fetchField()) {
        $links[$prev_vid] = array('title' => t('Previous revision'), 'href' => "node/{$node->nid}/revisions/{$prev_vid}/view");
      }
      if ($next_vid = db_query_range("SELECT nr.vid FROM {node_revision} nr WHERE nr.nid = :nid AND nr.vid > :vid ORDER BY nr.vid ASC", 0, 1, $args)->fetchField()) {
        $links[$next_vid] = array('title' => t('Next revision'), 'href' => "node/{$node->nid}/revisions/{$next_vid}/view");
      }
      // If the current revision is next or previous, use the "node/%node/current-revision" path.
      if (($current = $state['current']->vid) && isset($links[$current])) {
        $links[$current]['href'] = "node/{$node->nid}/current-revision";
      }

      // If the published revision is next or previous, use the "node/%node" path.
      if (isset($state['published']) && ($published = $state['published']->vid) && isset($links[$published])) {
        $links[$published]['href'] = "node/{$node->nid}";
      }

      // Link it up, with access checks.
      foreach ($links as $key => $args) {
        $links[$key] = call_user_func_array('workbench_moderation_access_link', $args);
      }

      // Post the links in a non-repeating message.
      if (!empty($links)) {
        $info_block_messages[] = array(
          'label' => t('View'),
          'message' => implode(', ', $links),
        );
      }
    }
  }
  // @TODO: Clean these up.
  elseif ($context == 'edit') {
    if ($node_published && $revision_published) {
      $info_block_messages[] = array(
        'label' => t('Status'),
        'message' => t('New draft of live content.'),
      );
    }
    elseif ($node_published && !$revision_published) {
      $info_block_messages[] = array(
        'label' => t('Status'),
        'message' => t('New draft from current revision'),
      );
      $link = workbench_moderation_access_link(t('Create a new draft from the published revision.'), "node/{$node->nid}/revisions/{$state['published']->vid}/revert");
      $info_block_messages[] = array(
        'label' => t('Actions'),
        'message' => $link,
      );
    }
    else {
      // New content.
      $info_block_messages[] = array(
        'label' => t('New content'),
        'message' => t('Your draft will be placed in moderation.'),
      );
    }
  }

  // Send the info block array to a static variable.
  workbench_moderation_set_message($info_block_messages);
}

/**
 * Builds a link for use in messages.
 *
 * @see workbench_moderation_messages()
 *
 * @param $text
 *   The link text to use.
 * @param $internal_path
 *   The Drupal path for the link.
 * @param $options
 *   Link options, following the format of url().
 *
 * @return
 *   A drupal-formatted HTML link.
 */
function workbench_moderation_access_link($text, $internal_path, $options = array()) {
  if (($item = menu_get_item($internal_path)) && !empty($item['access'])) {
    return l($text, $internal_path, $options);
  }
}

/**
 * Stores status messages for delivery.
 *
 * This function stores up moderation messages to be passed on to workbench_moderation_workbench_block().
 *
 * This function uses a static variable so that function can be called more than
 * once and the array built up.
 *
 * @see workbench_moderation_workbench_block()
 * @see workbench_moderation_messages()
 *
 * @param $new_messages
 *   An array of messages to be added to the block.
 *
 * @return
 *   An array of messages to be added to the block.
 */
function workbench_moderation_set_message($new_messages = array()) {
  static $messages = array();
  $messages = array_merge($messages, $new_messages);
  return $messages;
}

/**
 * Implements hook_block_view_workbench_block().
 *
 * Show the editorial status of this node.
 */
function workbench_moderation_workbench_block() {
  $output = array();
  foreach (workbench_moderation_set_message() as $message) {
    $output[] = t('!label: <em>!message</em>', array('!label' => $message['label'], '!message' => $message['message']));
  }

  return $output;
}