2020-08-14 13:36:36 +02:00

1230 lines
46 KiB
Plaintext

<?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_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';
}
}