Revisioning project page', 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 pending content you created and still have at least view access to.'); break; case 'accessible-content/i-last-modified/pending': $s = t('Showing all pending content you last modified and still have at least view access to.'); break; case 'accessible-content/i-can-edit/pending': $s = t('Showing all pending content you can edit.'); break; case 'accessible-content/i-can-view/pending': $s = t('Showing all pending content you have at least view access to.'); break; } return empty($s) ? '' : $s . '
'; } /** * 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 New revision in draft, pending moderation Publishing option at Structure >> Content types >> edit.') : 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 .= '
' . $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/' 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'; } }