<?php

/**
 * @file
 * Allows content to be updated and reviewed before submitting it for
 * publication, while the current live revision remains unchanged and publicly
 * visible until the changes have been reviewed and found fit for publication
 * by a moderator.
 */

// The 3 states a piece of content may be saved as.
define('REVISIONING_NO_REVISION', 0);
define('REVISIONING_NEW_REVISION_NO_MODERATION', 1);
define('REVISIONING_NEW_REVISION_WITH_MODERATION', 2);

// Paths node/%nid/view, node/%nid/edit open current revision.
define('REVISIONING_LOAD_CURRENT', 0);
// Paths node/%nid/view, node/%nid/edit open latest revison.
define('REVISIONING_LOAD_LATEST', 1);

define('REVISIONING_NEW_REVISION_WHEN_NOT_PENDING', 0);
define('REVISIONING_NEW_REVISION_EVERY_SAVE', 1);

define('REVISIONING_REVISIONS_BLOCK_OLDEST_AT_TOP', 0);
define('REVISIONING_REVISIONS_BLOCK_NEWEST_AT_TOP', 1);

module_load_include('inc', 'revisioning', 'revisioning_api');
module_load_include('inc', 'revisioning', 'revisioning.pages');
module_load_include('inc', 'revisioning', 'revisioning_theme');
module_load_include('inc', 'revisioning', 'revisioning_tokens');
module_load_include('inc', 'revisioning', 'revisioning_triggers_actions');
module_load_include('inc', 'revisioning', 'revisioning.taxonomy');
if (module_exists('rules')) {
  // This is not supposed to be necessary, but without it bad things happen.
  module_load_include('inc', 'revisioning', 'revisioning.rules');
}


/**
 * Implements hook_help().
 */
function revisioning_help($path, $arg) {
  switch ($path) {
    case 'admin/help#revisioning':
      $s = t('For documentation and tutorials see the <a href="@revisioning">Revisioning project page</a>',
        array('@revisioning' => url('http://drupal.org/project/revisioning')));
      break;

    case 'node/%/revisions':
      $s = t('To edit, publish or delete one of the revisions below, click on its saved date.');
      break;

    case 'admin/structure/trigger/revisioning':
      $s = t("Below you can assign actions to run when certain publication-related events happen. For example, you could send an e-mail to an author when their pending content is pubished.");
      break;

    case 'accessible-content/i-created/pending':
      $s = t('Showing all <em>pending</em> content <em>you created</em> and still have at least view access to.');
      break;

    case 'accessible-content/i-last-modified/pending':
      $s = t('Showing all <em>pending</em> content <em>you last modified</em> and still have at least view access to.');
      break;

    case 'accessible-content/i-can-edit/pending':
      $s = t('Showing all <em>pending</em> content you can <em>edit</em>.');
      break;

    case 'accessible-content/i-can-view/pending':
      $s = t('Showing all <em>pending</em> content you have at least <em>view</em> access to.');
      break;
  }
  return empty($s) ? '' : $s . '<br/>';
}

/**
 * Implements hook_permission().
 *
 * Revisioning permissions. Note that permissions to view, revert and delete
 * revisions already exist in node.module.
 */
function revisioning_permission() {

  $edit_description = t('Also requires edit permission from either the Node or other content access module(s).');

  $moderated_content_types = implode(', ', revisioning_moderated_content_types(FALSE));
  $publish_description = empty($moderated_content_types)
    ? t('Please select one or more content types for moderation by ticking the <em>New revision in draft, pending moderation</em> <strong>Publishing option</strong> at <em>Structure >> Content types >> edit</em>.')
    : t('Applies to content types that are subject to moderation, i.e.: %moderated_content_types.',
        array('%moderated_content_types' => $moderated_content_types));

  if (!empty($moderated_content_types) && variable_get('revisioning_require_update_to_publish', TRUE)) {
    $publish_description .= ' <br/>' . $edit_description;
  }

  $permissions = array(
    'view revision status messages' => array(
      'title' => t('View revision status messages'),
      'description' => '',
    ),
    'edit revisions' => array(
      'title' => t('Edit content revisions'),
      'description' => $edit_description,
    ),
    'publish revisions' => array(
      'title' => t("Publish content revisions (of anyone's content)"),
      'description' => $publish_description,
    ),
    'unpublish current revision' => array(
      'title' => t("Unpublish current revision (of anyone's content)"),
      'description' => $publish_description,
    ),
  );
  // Add per node-type view permissions in same way as edit permissions of node
  // module, but only for moderated content-types.
  foreach (node_type_get_types() as $type) {
    $machine_name = check_plain($type->type);
    if (revisioning_content_is_moderated($machine_name)) {
      $permissions['view revisions of own ' . $machine_name . ' content'] = array(
        'title' => t('%type-name: View revisions of own content', array('%type-name' => $type->name)));
      $permissions['view revisions of any ' . $machine_name . ' content'] = array(
        'title' => t("%type-name: View revisions of anyone's content", array('%type-name' => $type->name)));
      $permissions['publish revisions of own ' . $machine_name . ' content'] = array(
        'title' => t('%type-name: Publish revisions of own content', array('%type-name' => $type->name)));
      $permissions['publish revisions of any ' . $machine_name . ' content'] = array(
        'title' => t("%type-name: Publish revisions of anyone's content", array('%type-name' => $type->name)));
    }
  }
  return $permissions;
}

/**
 * Implements hook_menu().
 *
 * Define new menu items.
 * Existing menu items are modified through hook_menu_alter().
 */
function revisioning_menu() {
  $items = array();

  // Start with the Revisioning config menu item, put under Content Authoring.
  $items['admin/config/content/revisioning'] = array(
    'title' => 'Revisioning',
    'description' => 'Configure how content view and edit links behave. Customise revision summary listing.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('revisioning_admin_configure'),
    'access arguments' => array('administer site configuration'),
    'file' => 'revisioning.admin.inc',
  );

  // Plain link, not a tab, to allow users to unpublish a node.
  $items['node/%node/unpublish-current'] = array(
    // 'title' => t(Unpublish current revision'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('revisioning_unpublish_confirm', 1),
    'access callback' => '_revisioning_access_node_revision',
    'access arguments' => array('unpublish current revision', 1),
    'type' => MENU_CALLBACK,
  );

  // Revision tab local subtasks (i.e. secondary tabs), up to 8 of them:
  // list, view, edit, publish, unpublish, revert, delete and compare.
  // All revision operations 'node/%node/revisions/%vid/<op>' are defined as
  // local subtasks (subtabs) secondary to the primary 'node/%node/revisions'
  // local task (primary tab).
  //
  // Subtab to the Revisions primary tab to allow going back to the revisions
  // list without clicking the primary tab for a second time, which also works.
  $items['node/%node/revisions/list'] = array(
    'title' => 'List all revisions',
    'access callback' => '_revisioning_access_node_revision',
    'access arguments' => array('view revision list', 1),
    'type' => MENU_LOCAL_TASK,
    'weight' => -20,
  );

  $items['node/%node/revisions/delete-archived'] = array(
    'title' => 'Delete archived revisions',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('revisioning_delete_archived_confirm', 1),
    'access callback' => '_revisioning_access_node_revision',
    'access arguments' => array('delete archived revisions', 1),
    'type' => MENU_CALLBACK,
  );

  // View revision local subtask.
  // Note the use of %vid as opposed to %. This allows us to manipulate the
  // second argument in the path through vid_to_arg().
  $items['node/%node/revisions/%vid/view'] = array(
    'title' => 'View',
    'load arguments' => array(3),
    'page callback' => '_revisioning_view_revision',
    'page arguments' => array(1),
    'access callback' => '_revisioning_access_node_revision',
    'access arguments' => array('view revisions', 1),
    'type' => MENU_LOCAL_TASK,
    'weight' => -10,
    // 'tab_parent' => 'node/%/revisions',
  );
  // Edit revision local subtask.
  $items['node/%node/revisions/%vid/edit'] = array(
    'title' => 'Edit',
    'load arguments' => array(3),
    'page callback' => '_revisioning_edit_revision',
    'page arguments' => array(1),
    'access callback' => '_revisioning_access_node_revision',
    'access arguments' => array('edit revisions', 1),
    'file' => 'node.pages.inc',
    'file path' => drupal_get_path('module', 'node'),
    'type' => MENU_LOCAL_TASK,
    'weight' => -7,
    // 'tab_parent' => 'node/%/revisions',
  );
  // Publish revision local subtask.
  // As the menu is content type unaware, a further check on
  // node->revision_moderation must be made to determine whether it is
  // appropriate to show this tab.
  // This is done in _revisioning_access_node_revision.
  $items['node/%node/revisions/%vid/publish'] = array(
    'title' => 'Publish',
    'load arguments' => array(3),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('revisioning_publish_confirm', 1),
    'access callback' => '_revisioning_access_node_revision',
    'access arguments' => array('publish revisions', 1),
    'type' => MENU_LOCAL_TASK,
    'weight' => -4,
  );
  // Unpublish node local subtask.
  // As the menu is content type unaware, a further check on
  // node->revision_moderation must be made to determine whether it is
  // appropriate to show this tab.
  // This is done in _revisioning_access_node_revision.
  $items['node/%node/revisions/%vid/unpublish'] = array(
    'title' => 'Unpublish',
    'load arguments' => array(3),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('revisioning_unpublish_confirm', 1),
    'access callback' => '_revisioning_access_node_revision',
    'access arguments' => array('unpublish current revision', 1),
    'type' => MENU_LOCAL_TASK,
    'weight' => -3,
  );
  // Revert to revision local subtask.
  $items['node/%node/revisions/%vid/revert'] = array(
    'title' => 'Revert to this',
    'load arguments' => array(3),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('node_revision_revert_confirm', 1),
    'access callback' => '_revisioning_access_node_revision',
    'access arguments' => array('revert revisions', 1),
    'file' => 'node.pages.inc',
    'file path' => drupal_get_path('module', 'node'),
    'type' => MENU_LOCAL_TASK,
    'weight' => -2,
  );
  // Delete revision local subtask.
  $items['node/%node/revisions/%vid/delete'] = array(
    'title' => 'Delete',
    'load arguments' => array(3),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('node_revision_delete_confirm', 1),
    'access callback' => '_revisioning_access_node_revision',
    'access arguments' => array('delete revisions', 1),
    'file' => 'node.pages.inc',
    'file path' => drupal_get_path('module', 'node'),
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
  );

  // If Diff module is enabled, provide a "Compare to current" local subtask.
  if (module_exists('diff')) {
    $items['node/%node/revisions/%vid/compare'] = array(
      'title' => 'Compare to current',
      'load arguments' => array(3),
      'page callback' => '_revisioning_compare_to_current_revision',
      'page arguments' => array(1),
      'access callback' => '_revisioning_access_node_revision',
      'access arguments' => array('compare to current', 1),
      'type' => MENU_LOCAL_TASK,
      'weight' => 0,
      // 'tab_parent' => 'node/%/revisions',
    );
  }
  return $items;
}

/**
 * Implements hook_menu_alter().
 *
 * Modify menu items defined in other modules (in particular the Node module).
 */
function revisioning_menu_alter(&$items) {

  // Change to access callbacks for existing node paths so that we properly
  // control revision-related operation.
  // Some also have their page callbacks altered, e.g to load the latest
  // rather than the current revision of a node.
  // Can't change node load function to, say nid_load(), as we'll run into
  // trouble elsewhere, e.g. menu_get_object(), due to the fact that the
  // prefix, e.g. '%nid', is meant to be a type name, i.e. '%node'.
  //
  // Alter the 3 primary node page tabs: View tab, Edit tab, Revisions tab ...
  $items['node/%node']['access callback'] = '_revisioning_view_edit_access_callback';
  $items['node/%node']['access arguments'] = array('view', 1);
  $items['node/%node']['page callback'] = '_revisioning_view';
  $items['node/%node']['page arguments'] = array(1);
  // This is the MENU_DEFAULT_LOCAL_TASK, so inherits the above.
  $items['node/%node/view']['title callback'] = '_revisioning_title_for_tab';
  $items['node/%node/view']['title arguments'] = array(1, 'view');
  $items['node/%node/view']['weight'] = -10;

  $items['node/%node/edit']['access callback'] = '_revisioning_view_edit_access_callback';
  $items['node/%node/edit']['access arguments'] = array('edit', 1);
  $items['node/%node/edit']['page callback'] = '_revisioning_edit';
  $items['node/%node/edit']['page arguments'] = array(1);
  $items['node/%node/edit']['title callback'] = '_revisioning_title_for_tab';
  $items['node/%node/edit']['title arguments'] = array(1, 'edit');

  // 'Revisions' tab remains, but points to new page callback, allowing users
  // to pick the revision to view, edit, publish, revert, unpublish, delete.
  // Need to override _node_revision_access() call back as it disallows access
  // to the 'Revisions' tab when there's only one revision, which will prevent
  // users from getting to the publish/unpublish links.
  $items['node/%node/revisions']['access callback'] = '_revisioning_access_node_revision';
  $items['node/%node/revisions']['access arguments'] = array('view revision list', 1);
  $items['node/%node/revisions']['page callback'] = 'revisioning_node_overview';
  $items['node/%node/revisions']['page arguments'] = array(1);
  $items['node/%node/revisions']['title callback'] = '_revisioning_title_for_tab';
  $items['node/%node/revisions']['title arguments'] = array(1, 'revisions');

  // Remove the node.module links as we defined our own versions, using %vid
  unset($items['node/%node/revisions/%/view']);
  unset($items['node/%node/revisions/%/revert']);
  unset($items['node/%node/revisions/%/delete']);

  if (module_exists('diff')) {
    // If Diff module is enabled, make sure it uses correct access callback.
    $items['node/%node/revisions/view']['access callback'] = '_revisioning_access_node_revision';
    $items['node/%node/revisions/view']['access arguments'] = array('view revisions', 1);
  }
}

/**
 * Perform path manipulations for menu items containing the %vid wildcard.
 *
 * For example the ones from revisioning_menu().
 * @see http://drupal.org/node/500864
 */
function vid_to_arg($arg, &$map, $index) {
  if (empty($arg)) {
    // For e.g. node/%/revisions.
    // Suppresses subtabs of Revisions tab where %vid is omitted.
    $map = array();
  }
  return $arg;
}

/**
 * Implements hook_node_load().
 *
 * The same load op may occur multiple times during the same HTTP request, so
 * hooray for caching!
 *
 * hook_node_load is called when viewing a single node
 * node_load() -> node_load_multiple() ->
 * DrupalDefaultEntityController->attachLoad()
 *
 * hook_node_load is also called on the /content summary page:
 * node_admin_nodes() -> node_load_multiple() ->
 * DrupalDefaultEntityController->attachLoad()
 * 
 * We do nothing in this 2nd case.
 */
function revisioning_node_load($nodes, $types) {
  // The 'taxonomy/term/%' menu callback taxonomy_term_page() selects nodes
  // based on presence of their nids in the {taxonomy_index} table, which is
  // mainly based on publication status. Revisioning also updates the table for
  // unpublished content so that in Views we can see the terms belonging to
  // published as well as unpublished content. As a result we must re-apply
  // access control when taxonomy feeds are displayed.
  // See also revisioning_update_taxonomy_index().
  //
  $double_check_access = (strpos($_GET['q'], 'taxonomy/term') === 0) &&
    variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE);

  // At this point status, comment, promote and sticky have been set on all of
  // the $nodes according to the {node_revision} table (not the {node} table),
  // using {node.vid} as the foreign key into {node_revision}.
  $nodes_to_be_fixed = array();
  foreach ($nodes as $nid => $node) {
    if ($double_check_access && !node_access('view', $node)) {
      // At this point we cannot remove the node object from $nodes,
      // but we can set a flag to be checked in a later hook.
      $node->dont_display = TRUE;
    }
    else {
      revisioning_set_node_revision_info($node);
      if (!empty($node->revision_moderation) && !empty($node->is_current)) {
        // Hack!
        // Because of core issue [#1120272/#542290], if the current revision is
        // loaded, $node fields may in fact be those belonging to LATEST
        // revision.
        // So reload with FIELD_LOAD_REVISION. We can rely on $node->vid, that
        // attribute is set correctly.
        // Make sure to unset the already loaded fields or we end up with 2
        // copies of each field, e.g. 2 bodies, 2 tags, 2 image attachments etc.
        list($nid, $vid, $bundle) = entity_extract_ids('node', $node);
        $instances = _field_invoke_get_instances('node', $bundle, array('deleted' => FALSE));
        foreach ($instances as $instance) {
          $field_name = $instance['field_name'];
          unset($node->{$field_name});
        }
        $nodes_to_be_fixed[$nid] = $node;
      }
    }
  }
  if (!empty($nodes_to_be_fixed)) {
    field_attach_load_revision('node', $nodes_to_be_fixed);
    foreach ($nodes_to_be_fixed as $nid => $node) {
      $nodes[$nid] = $node;
    }
  }
}

/**
 * Implements hook_entity_info_alter().
 */
function revisioning_entity_info_alter(&$entity_info) {
  // If the Entity Translation module exists, register our own path scheme. This
  // will allow the default revision form to contain the correct translation
  // data. Override the default class with our own, so the class can tell the
  // system to use the correct path scheme.
  if (module_exists('entity_translation')) {
    $et_info = &$entity_info['node']['translation']['entity_translation'];
    $et_info['class'] = 'EntityTranslationNodeRevisioningHandler';
    $et_info['path schemes']['revisioning'] = array(
      'view path' => 'node/%node/revisions/%vid/view',
      'edit path' => 'node/%node/revisions/%vid/edit',
    );
  }
}

/**
 * Implements hook_entity_prepare_view().
 *
 * First of the dont_display hooks.
 */
function revisioning_entity_prepare_view($entities, $entity_type, $langcode) {
  if ($entity_type == 'node') {
    foreach ($entities as $node) {
      if (!empty($node->dont_display)) {
        $node->title = FALSE;
        // This == COMMENT_NODE_HIDDEN if Comment module enabled.
        $node->comment = 0;
        $node->link = FALSE;
        unset($node->body);
        unset($node->rss_elements);
      }
      else {
        // Display, however suppress comment form when revision is not current.
        if (isset($node->comment) && !empty($node->revision_moderation) && empty($node->is_current)) {
          // Prevent comment_node_view() from adding the comment form.
          // This == COMMENT_NODE_HIDDEN;
          $node->comment = 0;
        }
      }
    }
  }
}

/**
 * Implements hook_field_attach_view_alter().
 */
function revisioning_field_attach_view_alter(&$output, $context) {
  if ($context['entity_type'] == 'node' && !empty($context['entity']->dont_display)) {
    $output = array();
  }
}

/**
 * Implements hook_node_view().
 */
function revisioning_node_view($node, $view_mode, $langcode) {
  if (!empty($node->dont_display)) {
    // Suppress "Read more".
    $node->content = array();
  }
}

/**
 * Output a status message, provided teh user has the required permission.
 *
 * @param string $message
 *   The status message to be displayed.
 */
function revisioning_set_status_message($message) {
  if (user_access('view revision status messages')) {
    drupal_set_message(filter_xss($message), 'status');
  }
}

/**
 * Implements hook_node_prepare().
 *
 * Called when presenting edit form.
 */
function revisioning_node_prepare($node) {
  if (!empty($node->nid)) {
    $count = _revisioning_get_number_of_revisions_newer_than($node->vid, $node->nid);
    if ($count == 1) {
      drupal_set_message(t('Please note there is one revision more recent than the one you are about to edit.'), 'warning');
    }
    elseif ($count > 1) {
      drupal_set_message(t('Please note there are @count revisions more recent than the one you are about to edit.',
        array('@count' => $count)), 'warning');
    }
  }
}

/**
 * Implements hook_node_presave().
 *
 * Called when saving, be it an edit or when creating a node.
 *
 * Note that the following may be set programmatically on the $node object
 * before calling node_save($node):
 *
 * o $node->revision_operation, one of:
 *
 *   REVISIONING_NO_REVISION
 *   ($node->revision == $node->revision_moderation == FALSE)
 *
 *   REVISIONING_NEW_REVISION_NO_MODERATION
 *   ($node->revision == TRUE, $node->revision_moderation == FALSE)
 *
 *   REVISIONING_NEW_REVISION_WITH_MODERATION
 *   ($node->revision == $node->revision_moderation == TRUE)
 *
 * o $node->revision_condition (applies only to NEW_REVISION_WITH_MODERATION):
 *
 *   REVISIONING_NEW_REVISION_EVERY_SAVE
 *   REVISIONING_NEW_REVISION_WHEN_NOT_PENDING
 */
function revisioning_node_presave($node) {
  revisioning_set_node_revision_info($node);

  if (isset($node->revision_operation)) {
    $node->revision = ($node->revision_operation > REVISIONING_NO_REVISION);
    $node->revision_moderation = ($node->revision_operation == REVISIONING_NEW_REVISION_WITH_MODERATION);
  }

  if (!empty($node->revision_moderation) && revisioning_user_may_auto_publish($node)) {
    revisioning_set_status_message(t('Auto-publishing this revision.'));
    // Follow the default saving process making this revision current and
    // published, as opposed to pending.
    unset($node->revision_moderation);
    // This is not required for correct operation, as a revision becomes
    // pending based on vid > current_revision_id. But it looks less confusing,
    // when the "Published" box is in sync with the moderation radio buttons.
    $node->status = NODE_PUBLISHED;
    $node->auto_publish = TRUE;
  }

  if (!isset($node->nid)) {
    // New node, if moderated without Auto-publish, ignore the default Publish
    // tickbox.
    if (isset($node->revision_moderation) && $node->revision_moderation == TRUE) {
      $node->status = NODE_NOT_PUBLISHED;
    }
    // Set these for Rules, see [#1627400]
    $node->current_status  = $node->status;
    $node->current_title   = $node->title;
    $node->current_promote = $node->promote;
    $node->current_sticky  = $node->sticky;
    $node->current_comment = isset($node->comment) ? $node->comment : 0;
    return;
  }

  if (!empty($node->revision_moderation) /* || !empty($auto_publish) */) {
    // May want to do this for auto_publish too, to provide $node->current... to
    // other modules, as a courtesy.
    if (!isset($node->revision_condition) && !empty($node->revision) && !empty($node->is_pending)
      && variable_get('new_revisions_' . $node->type, REVISIONING_NEW_REVISION_WHEN_NOT_PENDING) == REVISIONING_NEW_REVISION_WHEN_NOT_PENDING) {
      revisioning_set_status_message(t('Updating existing draft, not creating new revision as this one is still pending.'));
      // To tell revisioning_node_update().
      $node->revision_condition = REVISIONING_NEW_REVISION_WHEN_NOT_PENDING;
    }
    if (isset($node->revision_condition)) {
      // Tell node_save() whether a new revision should be created.
      $node->revision = ($node->revision_condition == REVISIONING_NEW_REVISION_EVERY_SAVE);
    }

    $result = db_query("SELECT status, title, comment, promote, sticky FROM {node_revision} WHERE vid = :vid",
      array(':vid' => $node->current_revision_id));
    $current_revision = $result->fetchObject();
    // Copy from {node_revision} the field values replicated on {node} before
    // handing back to node_save(). This is a side-effect of core D7's somewhat
    // "sick" table denormalisation.
    // If the Scheduler module is used take the status from there. See
    // revisioning_scheduler_api().
    $node->current_status  = isset($node->scheduled_status) ? $node->scheduled_status : $current_revision->status;
    $node->current_title   = $current_revision->title;
    $node->current_promote = $current_revision->promote;
    $node->current_sticky  = $current_revision->sticky;
    $node->current_comment = isset($current_revision->comment) ? $current_revision->comment : 0;
  }
}

/**
 * Implements hook_node_update().
 *
 * Note: $node->revision_moderation and $node->revision_condition may be set
 * programmatically prior to calling node_save().
 * See also: revisioning_node_pre_save().
 */
function revisioning_node_update($node) {

  revisioning_update_taxonomy_index($node, variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE));

  // Check whether the node has a current_status property and update the
  // node->status to allow node_save()'s call to node_access_acquire_grants()
  // to create the correct node_access entry for the current_status.
  if (isset($node->current_status)) {
    $node->status = $node->current_status;
  }

  if (!empty($node->revision_moderation) && (isset($node->revision_condition) || !empty($node->revision))) {
    // Enter when revision moderation is on and revision_condition=0,1
    // Have to do this due to D7's "sick" denormalisation of node revision data.
    // Resetting the fields duplicated from new {node_revision} back to their
    // originial values to match the current revision as opposed to the latest
    // revision. The latter is done by node_save() just before it calls this
    // function.
    // By resetting {node.vid} {node.vid} < {node_revision.vid}, which makes
    // the newly created revision a pending revision in Revisioning's books.
    // Note: cannot use $node->old_vid as set by node_save(), as this refers to
    // the revision edited, which may not be the current, which is what we are
    // after here.
    db_update('node')
      ->fields(array(
        'vid'     => $node->current_revision_id,
        'status'  => $node->current_status,
        'title'   => $node->current_title,
        // In case Comment module enabled.
        'comment' => $node->current_comment,
        'promote' => $node->current_promote,
        'sticky'  => $node->current_sticky))
      ->condition('nid', $node->nid)
      ->execute();
  }
  // Generate a 'post update' event in Rules.
  module_invoke_all('revisionapi', 'post update', $node);

  // Add new revision usage records to files to prevent them being deleted.
  $fields = field_info_instances('node', $node->type);
  foreach ($fields as $field_name => $value) {
    $field_info = field_info_field($field_name);
    if ($field_info['type'] == 'file' || $field_info['type'] == 'image') {
      // See #1996412.
      $file_fields[$field_name] = $value;
    }
  }
  // Create file revision entries for files created using older versions.
  // $old_node = isset($node->original) ? $node->original : NULL;
  // [#2276657]
  $old_node = isset($node->original) && !empty($node->original) ? $node->original : NULL;
  if (isset($old_node) && !empty($file_fields)) {
    foreach ($file_fields as $file_field) {
      if ($old_files = field_get_items('node', $old_node, $file_field['field_name'], $old_node->language)) {
        foreach ($old_files as $old_single_file) {
          if (!empty($old_single_file)) {
            $old_file = (object) $old_single_file;
            file_usage_add($old_file, 'revisioning', 'revision', $old_node->vid);
          }
        }
      }
    }
  }
  if (!empty($file_fields)) {
    foreach ($file_fields as $file_field) {
      if ($files = field_get_items('node', $node, $file_field['field_name'], $node->language)) {
        foreach ($files as $single_file) {
          $file = (object) $single_file;
          file_usage_add($file, 'revisioning', 'revision', $node->vid);
        }
      }
    }
  }
}

/**
 * Implements hook_node_insert().
 *
 * New node.
 */
function revisioning_node_insert($node) {

  revisioning_update_taxonomy_index($node, variable_get('revisioning_in_views_show_unpublished_content_terms', TRUE));

  if (!empty($node->revision_moderation)) {
    revisioning_set_status_message($node->status ? t('Initial revision created and published.') : t('Initial draft created, pending publication.'));
  }

  // Add revision usage records to files to prevent them being deleted.
  $fields = field_info_instances('node', $node->type);
  foreach ($fields as $field_name => $value) {
    $field_info = field_info_field($field_name);
    if ($field_info['type'] == 'file') {
      $file_fields[$field_name] = $value;
    }
  }
  if (!empty($file_fields)) {
    foreach ($file_fields as $file_field) {
      if ($files = field_get_items('node', $node, $file_field['field_name'], $node->language)) {
        foreach ($files as $single_file) {
          $file = (object) $single_file;
          file_usage_add($file, 'revisioning', 'revision', $node->vid);
        }
      }
    }
  }
}

/**
 * Implements hook_node_delete().
 */
function revisioning_node_delete($node) {
  if ($revisions = node_revision_list($node)) {
    $vids = array_keys($revisions);
    db_delete('file_usage')->condition('module', 'revisioning')->condition('id', $vids, 'IN')->execute();
  }
}

/**
 * Implements hook_node_access_records_alter().
 *
 * If the node is not the current node this function clears the grants array and
 * rebuilds it using the current node.
 */
function revisioning_node_access_records_alter(&$grants, $node) {

  if (!revisioning_revision_is_current($node)) {
    $current_node = node_load($node->nid, NULL, TRUE);
    $grants = array();
    foreach (module_implements('node_access_records') as $module) {
      $function = $module . '_node_access_records';
      if (function_exists($function)) {
        $result = call_user_func_array($function, array($current_node));
        if (isset($result)) {
          if (is_array($result)) {
            $grants = array_merge_recursive($grants, $result);
          }
          else {
            $grants[] = $result;
          }
        }
      }
    }
  }
}

/**
 * Implements hook_views_api().
 */
function revisioning_views_api() {
  return array(
    'api' => views_api_version(),
    'path' => drupal_get_path('module', 'revisioning') . '/views',
  );
}

/**
 * Implements hook_user_node_access().
 */
function revisioning_user_node_access($revision_op, $node, $account = NULL) {
  if (!isset($account)) {
    $account = $GLOBALS['user'];
  }

  $type = check_plain($node->type);

  switch ($revision_op) {
    case 'view current':
      break;

    case 'compare to current':
    case 'view revisions':
    case 'view revision list':
      if (user_access('view revisions', $account)) {
        break;
      }
      if (user_access('view revisions of any ' . $type . ' content', $account)) {
        break;
      }
      if (($node->uid == $account->uid) && user_access('view revisions of own ' . $type . ' content', $account)) {
        break;
      }
      return FALSE;

    case 'edit current':
      return 'update';

    case 'edit revisions':
    case 'revert revisions':
      return user_access($revision_op, $account) ? 'update' : FALSE;

    case 'publish revisions':
      $node_op = variable_get('revisioning_require_update_to_publish', TRUE) ? 'update' : 'view';
      if (user_access('publish revisions', $account)) {
        return $node_op;
      }
      if (user_access('publish revisions of any ' . $type . ' content', $account)) {
        return $node_op;
      }
      if (($node->uid == $account->uid) && user_access('publish revisions of own ' . $type . ' content', $account)) {
        return $node_op;
      }
      return FALSE;

    case 'unpublish current revision':
      $node_op = variable_get('revisioning_require_update_to_publish', TRUE) ? 'update' : 'view';
      return user_access('unpublish current revision', $account) ? $node_op : FALSE;

    case 'delete revisions':
    case 'delete archived revisions':
      if (!user_access('delete revisions', $account)) {
        return FALSE;
      }
    case 'delete node':
      return 'delete';

    default:
      drupal_set_message(t("Unknown Revisioning operation '%revision_op'. Treating as 'view'.",
        array('%revision_op' => $revision_op)), 'warning', FALSE);
  }
  return 'view';
}

/**
 * Implements hook_scheduler_api().
 */
function revisioning_scheduler_api($node, $action) {
  if ($action == 'pre_publish') {
    $node->scheduled_status = NODE_PUBLISHED;
  }
  elseif ($action == 'pre_unpublish') {
    $node->scheduled_status = NODE_NOT_PUBLISHED;
  }
}

/**
 * Test whether the supplied revision operation is appropriate for the node.
 *
 * This is irrespective of user permissions, e.g. even for an administrator it
 * doesn't make sense to publish a node that is already published or to
 * "revert" to the current revision.
 *
 * @param string $revision_op
 *   For instance 'publish revisions', 'delete revisions'
 * @param object $node
 *   The node object
 *
 * @return bool
 *   TRUE if the operation is appropriate for this node at this point
 */
function _revisioning_operation_appropriate($revision_op, $node) {

  switch ($revision_op) {

    case 'compare to current':
      // Can't compare against itself.
    case 'delete revisions':
      // If the revision is the current one, suppress the delete operation
      // @TODO ...unless it's the only revision, in which case delete the
      // entire node; however this requires a different URL.
      return !$node->is_current;

    case 'delete archived revisions':
      break;

    case 'view revision list':
      // For i.e. node revisions summary.
      if (empty($node->revision_moderation) && isset($node->num_revisions) && $node->num_revisions == 1) {
        // Suppress Revisions tab when when there's only 1 revision. This is
        // consistent with core.
        // However, when content is moderated (i.e. "New revision in draft,
        // pending moderation" is ticked) we want to be able to get to the
        // 'Unpublish current' link on this page and the 'Publish' tab on
        // the next.
        return FALSE;
      }
      break;

    case 'edit revisions':
      if (empty($node->revision_moderation) /* && !$node->is_current*/) {
        return FALSE;
      }
      break;

    case 'publish revisions':
      // If the node isn't meant to be moderated,
      // or the revision is not either pending or current but not published,
      // then disallow publication.
      if (empty($node->revision_moderation) ||
          !($node->is_pending || ($node->is_current && !$node->status))) {
        return FALSE;
      }
      break;

    case 'unpublish current revision':
      // If the node isn't meant to be moderated or it is unpublished already
      // or we're not looking at the current revision, then unpublish is not an
      // option.
      if (empty($node->revision_moderation) || !$node->status || !$node->is_current) {
        return FALSE;
      }
      break;

    case 'revert revisions':
      // If this revision is pending or current, suppress the reversion.
      if ($node->is_pending || $node->is_current) {
        return FALSE;
      }
      break;
  }
  return TRUE;
}

/**
 * Determine whether the supplied revision operation is permitted on the node.
 *
 * This requires getting through three levels of defence:
 * o Is the operation appropriate for this node at this time, e.g. a node must
 *   not be published if it already is or if it isn't under moderation control
 * o Does the user have permission for the requested REVISION operation?
 * o Does the user have the NODE access rights (view/update/delete) for this
 *   operation?
 *
 * @param string $revision_op
 *   For instance 'publish revisions', 'delete revisions'
 * @param object $node
 *   The node object
 *
 * @return bool
 *   TRUE if the user has access
 */
function _revisioning_access_node_revision($revision_op, $node) {

  if (!_revisioning_operation_appropriate($revision_op, $node)) {
    return FALSE;
  }
  // Check the revision-aspect of the operation.
  $node_op = revisioning_user_node_access($revision_op, $node);
  // ... then check with core to assess node permissions
  // node_access will invoke hook_node_access(), i.e. revisioning_node_access().
  $access = $node_op && node_access($node_op, $node);

  // Let other modules override the outcome, if there are any.
  // If any module denies access that is the final result, otherwise allow.
  $overrides = module_invoke_all('revisioning_access_node_revision', $revision_op, $node);
  return empty($overrides) ? $access : !in_array(NODE_ACCESS_DENY, $overrides, TRUE);
}

/**
 * Implements hook_node_access().
 *
 * This gets invoked from node.module/node_access() after it has checked the
 * standard node permissions using node_node_access() and just before it checks
 * the node_access grants table.
 * We basically return "don't care" except for one 'view' case, which replicates
 * the node.module. "Don't care" in this case would result in "access denied".
 */
function revisioning_node_access($node, $node_op, $account) {
  // Taken from node.module/node_access():
  // If no modules implement hook_node_grants(), the default behaviour is to
  // allow all users to view published nodes, so reflect that here,
  // augmented for the 'view revisions' family of permissions, which apply to
  // both published and unpublished nodes.
  if ($node_op == 'view' && !module_implements('node_grants')) {
    if ($node->status == NODE_PUBLISHED || (!empty($node->revision_moderation) && revisioning_user_node_access('view revisions', $node, $account))) {
      return NODE_ACCESS_ALLOW;
    }
  }
  // [#1492246]
  // Deny access to unpublished, moderated content by anonymous users.
  if (empty($node->status) && !empty($node->revision_moderation) && empty($account->uid)) {
    return NODE_ACCESS_DENY;
  }
  return NODE_ACCESS_IGNORE;
}

/**
 * Access callback function.
 *
 * Access callback for 'node/%', 'node/%/view' and 'node/%/edit' links that
 * may appear anywhere on the site.
 * At the time that this function is called the CURRENT revision will already
 * have been loaded by the system. However depending on the value of the
 * 'revisioning_view_callback' and 'revisioning_edit_callback' variables (as
 * set on the admin/config/content/revisioning page), this may not be the
 * desired revision.
 * If these variables state that the LATEST revision should be loaded, we need
 * to check at this point whether the user has permission to view this revision.
 *
 * The 'View current' and/or 'Edit current' tabs are suppressed when the current
 * revision is already displayed via one of the Revisions subtabs.
 * The 'View latest' and/or 'Edit latest' tabs are suppressed when the latest
 * revision is already displayed via one of the Revisions subtabs.
 *
 * @param string $op
 *   must be one of 'view' or 'edit'
 * @param object $node
 *   the node object
 *
 * @return bool
 *   FALSE if access to the desired revision is denied
 */
function _revisioning_view_edit_access_callback($op, $node) {

  $load_op = _revisioning_load_op($node, $op);

  $vid = arg(3);
  if (!empty($node->revision_moderation) && is_numeric($vid)) {
    // The View, Edit primary tabs are requested indirectly, in the context of
    // the secondary tabs under Revisions, e.g. node/%/revisions/%
    if ($load_op == REVISIONING_LOAD_CURRENT && $vid == $node->current_revision_id) {
      // Suppress 'View current' and 'Edit current' tabs when viewing current.
      return FALSE;
    }
    if ($load_op == REVISIONING_LOAD_LATEST && $vid == revisioning_get_latest_revision_id($node->nid)) {
      // Suppress 'View latest' and 'Edit latest' tabs when viewing latest.
      return FALSE;
    }
  }
  if ($load_op == REVISIONING_LOAD_LATEST) {
    // _revisioning_load_op has already checked permission to view latest.
    return TRUE;
  }
  $revision_op = ($op == 'view') ? 'view current' : 'edit current';
  return _revisioning_access_node_revision($revision_op, $node);
}

/**
 * Load a revision.
 *
 * Assuming that the node passed in is the current revision (core default),
 * this function determines whether the lastest revision should be loaded
 * instead, in which case it returns REVISIONING_LOAD_LATEST.
 *
 * @param object $node
 *   only nodes of content types subject to moderation are
 *   processed by this function
 * @param string $op
 *   either 'edit' or 'view'
 * @param bool $check_access
 *   whether revision access permissions should be checked; if the user has no
 *   permission to load the latest revisions, then the function returns
 *   REVISIONING_LOAD_CURRENT
 *
 * @return int
 *   REVISIONING_LOAD_LATEST or REVISIONING_LOAD_CURRENT
 */
function _revisioning_load_op($node, $op, $check_access = TRUE) {
  if (!empty($node->revision_moderation)) {
    $view_mode = (int) variable_get('revisioning_view_callback', REVISIONING_LOAD_CURRENT);
    $edit_mode = (int) variable_get('revisioning_edit_callback', REVISIONING_LOAD_CURRENT);
    $load_op = ($op == 'edit') ? $edit_mode : $view_mode;
    if ($load_op == REVISIONING_LOAD_LATEST) {
      // Site is configured to load latest revision, but we'll only do this if
      // the latest isn't loaded already and user has the permission to do so.
      $latest_vid = revisioning_get_latest_revision_id($node->nid);
      if ($latest_vid != $node->current_revision_id) {
        if (!$check_access) {
          return REVISIONING_LOAD_LATEST;
        }
        $original_vid = $node->vid;
        $node->vid = $latest_vid;
        $node->is_current = revisioning_revision_is_current($node);
        $revision_op = ($op == 'view') ? 'view revisions' : 'edit revisions';
        $access = _revisioning_access_node_revision($revision_op, $node);
        // Restore $node (even though called by value), to remain consistent.
        $node->vid = $original_vid;
        $node->is_current = revisioning_revision_is_current($node);
        if ($access) {
          return REVISIONING_LOAD_LATEST;
        }
      }
    }
  }
  return REVISIONING_LOAD_CURRENT;
}

/**
 * Display node overview.
 *
 * Display all revisions of the supplied node in a themed table with links for
 * the permitted operations above it.
 *
 * @return array
 *   render array as returned by drupal_get_form()
 */
function revisioning_node_overview($node) {
  return _revisioning_theme_revisions_summary($node);
}

/**
 * Menu callback for the primary View tab.
 *
 * This is the same callback as used in core, except that in core current and
 * latest revisions are always the same.
 */
function _revisioning_view($node) {
  if (_revisioning_load_op($node, 'view') == REVISIONING_LOAD_LATEST) {
    $vid_to_load = revisioning_get_latest_revision_id($node->nid);
    $node = node_load($node->nid, $vid_to_load);
  }
  // This is the callback used by node.module for node/%node & node/%node/view
  return node_page_view($node);
}

/**
 * Callback for the primary Edit tab.
 *
 * This is the same callback as used in core, except that in core current and
 * latest revisions are always the same.
 */
function _revisioning_edit($node) {
  if (_revisioning_load_op($node, 'edit') == REVISIONING_LOAD_LATEST) {
    $vid_to_load = revisioning_get_latest_revision_id($node->nid);
    $node = node_load($node->nid, $vid_to_load);
  }
  _revisioning_set_custom_theme_if_necessary();
  // This is the callback used by node.module for node/%node/edit
  return node_page_edit($node);
}

/**
 * Callback to view a particular revision.
 */
function _revisioning_view_revision($node) {
  if (isset($node->nid)) {
    /* For Panels: @todo test this thoroughly. See [#1567880]
    $router_item = menu_get_item('node/' . $node->nid);
    if (!empty($router_item['file'])) {
      $path = $_SERVER['DOCUMENT_ROOT'] . base_path();
      require_once $path . $router_item['file'];
    }
    // Call whatever function is assigned to the main node path but pass the
    // current node as an argument. This approach allows for the reuse of Panel
    // definition acting on node/%node.
    if (isset($router_item['page_callback'])) {
      return $router_item['page_callback']($node);
    }*/
  }
  // This is the callback used by node.module for node/%node/revisions/%/view
  return node_show($node, TRUE);
}

/**
 * Callback to edit a particular revision.
 *
 * Note that there is no equivalent of this in core and we should not allow
 * editing of a non-current revision, if $node->revision_moderation is not set.
 * This is the job of the access callback _revisioning_access_node_revision().
 */
function _revisioning_edit_revision($node) {
  _revisioning_set_custom_theme_if_necessary();
  return node_page_edit($node);
}

/**
 * Callback for the primary View, Edit and Revisions tabs titles.
 *
 * @param object $node
 *   the node object
 * @param string $tab
 *   'view', 'edit' or 'revisions'
 *
 * @return string
 *   translatable title string
 */
function _revisioning_title_for_tab($node, $tab) {
  if ($tab == 'revisions') {
    return is_numeric(arg(3)) ? t('Revision operations') : t('Revisions');
  }
  /*
  if (empty($node->revision_moderation) || $node->num_revisions <= 1) {
    return ($tab == 'edit' ? t('Edit') : t('View'));
  }
  if (_revisioning_load_op($node, $tab) == REVISIONING_LOAD_LATEST) {
    return ($tab == 'edit' ? t('Edit latest') : t('View latest'));
  }
  return ($tab == 'edit' ? t('Edit current') : t('View current'));
  */
  if (!empty($node->revision_moderation) && $node->num_revisions > 1) {
    if (_revisioning_load_op($node, $tab) == REVISIONING_LOAD_LATEST) {
      return ($tab == 'edit' ? t('Edit latest') : t('View latest'));
    }
    if (_revisioning_access_node_revision('view revisions', $node)) {
      return ($tab == 'edit' ? t('Edit current') : t('View current'));
    }
  }
  return ($tab == 'edit' ? t('Edit') : t('View'));
}

/**
 * Set custom theme.
 */
function _revisioning_set_custom_theme_if_necessary() {
  // Use the admin theme if the user specified this at Appearance >> Settings.
  // Note: first tick 'View the administration theme' at People >> Permissions.
  if (variable_get('node_admin_theme', FALSE)) {
    global $theme, $custom_theme;
    $custom_theme = variable_get('admin_theme', $theme);
  }
}

if (module_exists('diff')) {
  /**
   * Compare two revisions.
   *
   * Use diff's diff_diffs_show() function to compare specific revision to the
   * current one.
   */
  function _revisioning_compare_to_current_revision($node) {
    // For diff_diffs_show().
    module_load_include('inc', 'diff', 'diff.pages');
    // Make sure that latest of the two revisions is on the right.
    if ($node->current_revision_id < $node->vid) {
      return diff_diffs_show($node, $node->current_revision_id, $node->vid);
    }
    return diff_diffs_show($node, $node->vid, $node->current_revision_id);
  }
}

/**
 * Implements hook_page_manager_override().
 *
 * See http://drupal.org/node/1509674#comment-6702798
 */
function revisioning_page_manager_override($task_name) {
  switch ($task_name) {
    case 'node_view':
      return '_revisioning_view';
  }
}
