' . t('The Diff module replaces the normal Revisions node tab. Diff enhances the listing of revisions with an option to view the differences between any two content revisions. Access to this feature is controlled with the View revisions permission. The feature can be disabled for an entire content type on the content type configuration page. Diff also provides an optional View changes button while editing a node.') . '
';
return $output;
case 'node/%/revisions/%/view':
// The translated strings should match node_help('node/%/revisions').
return '' . t('Revisions allow you to track differences between multiple versions of your content, and revert back to older versions.') . '
';
case 'node/%/revisions/view/%/%':
return '' . t('Comparing two revisions:') . '
';
}
}
/**
* The various states that are available.
*/
function diff_available_states($entity_type = NULL) {
$states = array(
'raw' => t('Standard'),
'raw_plain' => t('Marked down'),
);
return $states;
}
/**
* Implements hook_menu().
*
* @todo: Review this.
*/
function diff_menu() {
/*
* By using MENU_LOCAL_TASK (and 'tab_parent') we can get the various
* revision-views to show the View|Edit|Revision-tabs of the node on top,
* and have the Revisions-tab open. To avoid creating/showing any extra tabs
* or sub-tabs (tasks below top level) for the various paths (i.e. "Diff",
* "Show latest" and "Show a specific revision") that need a revision-id (vid)
* parameter, we make sure to set 'tab_parent' a bit odd. This solution may
* not be the prettiest one, but by avoiding having two _LOCAL_TASKs sharing
* a parent that can be accessed by its full path, it seems to work as
* desired. Breadcrumbs work decently, at least the node link is among the
* crumbs. For some reason any breadcrumbs "before/above" the node is only
* seen at 'node/%node/revisions/%/view'.
*/
// Not used directly, but was created to get the other menu items to work.
$items['node/%node/revisions/list'] = array(
'title' => 'List revisions',
'page callback' => 'diff_diffs_overview',
'type' => MENU_DEFAULT_LOCAL_TASK,
'access callback' => 'diff_node_revision_access',
'access arguments' => array(1),
'file' => 'diff.pages.inc',
);
$items['node/%node/revisions/view'] = array(
'title' => 'Compare revisions',
'page callback' => 'diff_diffs_show',
'page arguments' => array(1, 4, 5, 6),
'type' => MENU_LOCAL_TASK,
'access callback' => 'diff_node_revision_access',
'access arguments' => array(1),
'tab_parent' => 'node/%/revisions/list',
'file' => 'diff.pages.inc',
);
$items['node/%node/revisions/view/latest'] = array(
'title' => 'Show latest difference',
'page callback' => 'diff_latest',
'page arguments' => array(1),
'type' => MENU_LOCAL_TASK,
'access arguments' => array('access content'),
'tab_parent' => 'node/%/revisions/view',
'file' => 'diff.pages.inc',
);
// Administrative settings.
$items['admin/config/content/diff'] = array(
'title' => 'Diff',
'description' => 'Diff settings.',
'file' => 'diff.admin.inc',
'page callback' => 'drupal_get_form',
'page arguments' => array('diff_admin_settings'),
'access arguments' => array('administer site configuration'),
);
$items['admin/config/content/diff/settings'] = array(
'title' => 'Settings',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
$items['admin/config/content/diff/fields'] = array(
'title' => 'Fields',
'description' => 'Field support and settings overview.',
'file' => 'diff.admin.inc',
'page callback' => 'diff_admin_field_overview',
'access arguments' => array('administer site configuration'),
'type' => MENU_LOCAL_TASK,
);
$items['admin/config/content/diff/fields/%'] = array(
'title' => 'Global field settings',
'page callback' => 'drupal_get_form',
'page arguments' => array('diff_admin_global_field_settings', 5),
'access arguments' => array('administer site configuration'),
'type' => MENU_VISIBLE_IN_BREADCRUMB,
'file' => 'diff.admin.inc',
);
$items['admin/config/content/diff/entities'] = array(
'title' => 'Entities',
'description' => 'Entity settings.',
'file' => 'diff.admin.inc',
'page callback' => 'drupal_get_form',
'page arguments' => array('diff_admin_global_entity_settings', 'node'),
'access arguments' => array('administer site configuration'),
'type' => MENU_LOCAL_TASK,
);
$items['admin/config/content/diff/entities/node'] = array(
'title' => 'Nodes',
'description' => 'Node comparison settings.',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
$items['admin/config/content/diff/entities/user'] = array(
'title' => 'Users',
'description' => 'User diff settings.',
'file' => 'diff.admin.inc',
'page callback' => 'drupal_get_form',
'page arguments' => array('diff_admin_global_entity_settings', 'user'),
'access arguments' => array('administer site configuration'),
'type' => MENU_LOCAL_TASK,
);
return $items;
}
/**
* Implements hook_menu_alter().
*/
function diff_menu_alter(&$callbacks) {
// Overwrite the default 'Revisions' page.
$callbacks['node/%node/revisions']['page callback'] = 'diff_diffs_overview';
$callbacks['node/%node/revisions']['module'] = 'diff';
$callbacks['node/%node/revisions']['file'] = 'diff.pages.inc';
$callbacks['node/%node/revisions/%/view']['tab_parent'] = 'node/%/revisions/list';
$callbacks['node/%node/revisions/%/revert']['tab_parent'] = 'node/%/revisions/%/view';
$callbacks['node/%node/revisions/%/delete']['tab_parent'] = 'node/%/revisions/%/view';
$callbacks['node/%node/revisions']['access callback']
= $callbacks['node/%node/revisions/%/view']['access callback']
= $callbacks['node/%node/revisions/%/revert']['access callback']
= $callbacks['node/%node/revisions/%/delete']['access callback'] = 'diff_node_revision_access';
}
/**
* Implements hook_admin_paths_alter().
*/
function diff_admin_paths_alter(&$paths) {
// By default, treat all diff pages as administrative.
if (variable_get('diff_admin_path_node', 1)) {
$paths['node/*/revisions/view/*/*'] = TRUE;
}
}
/**
* Access callback for the node revisions page.
*/
function diff_node_revision_access($node, $op = 'view') {
$may_revision_this_type = variable_get('diff_enable_revisions_page_node_' . $node->type, TRUE) || user_access('administer nodes');
return $may_revision_this_type && _node_revision_access($node, $op);
}
/**
* Implements hook_permission().
*/
function diff_permission() {
return array(
'diff view changes' => array(
'title' => t('Access %view button', array('%view' => t('View changes'))),
'description' => t('Controls access to the %view button when editing content.', array('%view' => t('View changes'))),
),
);
}
/**
* Implements hook_hook_info().
*/
function diff_hook_info() {
$hooks['entity_diff'] = array(
'group' => 'diff',
);
$hooks['diff'] = array(
'group' => 'diff',
);
$hooks['field_diff_view_prepare_alter'] = array(
'group' => 'diff',
);
$hooks['field_diff_view_alter'] = array(
'group' => 'diff',
);
return $hooks;
}
/**
* Implements hook_entity_info_alter().
*
* Although the module only provides an UI for comparing nodes, it has an
* extendable API for any entity, so we supply two view modes for all entities.
* - diff_standard: This view mode is used to tell the module how to compare
* individual fields. This is used on the revisions page.
*/
function diff_entity_info_alter(&$entity_info) {
foreach (array_keys($entity_info) as $entity_type) {
if (!empty($entity_info[$entity_type]['view modes'])) {
$entity_info[$entity_type]['view modes'] += array(
'diff_standard' => array(
'label' => t('Revision comparison'),
'custom settings' => FALSE,
),
);
}
}
}
/**
* Returns a list of all the existing revision numbers.
*
* Clone of node_revision_list() with revision status included. This would be
* an additional join in Drupal 8.x to the {node_field_revision} table.
*
* @param object $node
* The node object.
*
* @return array
* An associative array keyed by node revision number.
*/
function diff_node_revision_list($node) {
$revisions = array();
$result = db_query('SELECT r.vid, r.title, r.log, r.uid, n.vid AS current_vid, r.status AS status, r.timestamp, u.name FROM {node_revision} r LEFT JOIN {node} n ON n.vid = r.vid INNER JOIN {users} u ON u.uid = r.uid WHERE r.nid = :nid ORDER BY r.vid DESC', array(':nid' => $node->nid));
foreach ($result as $revision) {
$revisions[$revision->vid] = $revision;
}
return $revisions;
}
/**
* Implements hook_block_info().
*/
function diff_block_info() {
return array(
'inline' => array(
'info' => t('Inline differences'),
),
);
}
/**
* Implements hook_block_configure().
*/
function diff_block_configure($delta = '') {
$form = array();
switch ($delta) {
case 'inline':
$form['bundles'] = array(
'#type' => 'checkboxes',
'#title' => t('Enabled content types'),
'#default_value' => variable_get('diff_show_diff_inline_node_bundles', array()),
'#options' => node_type_get_names(),
'#description' => t('Show this block only on pages that display content of the given type(s).'),
);
break;
}
return $form;
}
/**
* Implements hook_block_save().
*/
function diff_block_save($delta = '', $edit = array()) {
switch ($delta) {
case 'inline':
variable_set('diff_show_diff_inline_node_bundles', $edit['bundles']);
break;
}
}
/**
* Implements hook_block_view().
*/
function diff_block_view($delta) {
if ($delta === 'inline' && user_access('view revisions') && ($node = menu_get_object()) && arg(2) !== 'edit') {
$enabled_types = variable_get('diff_show_diff_inline_node_bundles', array());
if (!empty($enabled_types[$node->type])) {
$block = array();
$revisions = diff_node_revision_list($node);
if (count($revisions) > 1) {
$block['subject'] = t('Highlight changes');
$block['content'] = drupal_get_form('diff_inline_form', $node, $revisions);
}
return $block;
}
}
}
/**
* Implements hook_node_view_alter().
*/
function diff_node_view_alter(&$build) {
$node = $build['#node'];
if (user_access('view revisions') && in_array($node->type, variable_get('diff_show_diff_inline_node_bundles', array()), TRUE)) {
// Ugly but cheap way to check that we are viewing a node's revision page.
if (arg(2) === 'revisions' && arg(3) === $node->vid) {
module_load_include('inc', 'diff', 'diff.pages');
$old_vid = _diff_get_previous_vid(node_revision_list($node), $node->vid);
$build = array('#markup' => diff_inline_show($node, $old_vid));
}
$build['#prefix'] = isset($build['#prefix']) ? "" . $build['#prefix'] : "
";
$build['#suffix'] = isset($build['#suffix']) ? $build['#suffix'] . "
" : "
";
}
}
/**
* Implements hook_form_BASE_FORM_ID_alter().
*/
function diff_form_node_form_alter(&$form, $form_state) {
// Add a 'View changes' button on the node edit form.
$node = $form['#node'];
if (variable_get('diff_show_preview_changes_node_' . $node->type, TRUE)
&& user_access('diff view changes')
&& !empty($node->nid)) {
$form['actions']['preview_changes'] = array(
'#type' => 'submit',
'#value' => t('View changes'),
'#weight' => 12,
'#submit' => array('diff_node_form_build_preview_changes'),
);
}
}
/**
* Implements hook_form_BASE_FORM_ID_alter().
*/
function diff_form_node_type_form_alter(&$form, $form_state) {
if (isset($form['type'])) {
$type = $form['#node_type'];
$form['diff'] = array(
'#title' => t('Compare revisions'),
'#type' => 'fieldset',
'#group' => 'additional_settings',
'#tree' => FALSE,
);
$form['diff']['diff_show_preview_changes_node'] = array(
'#type' => 'checkbox',
'#title' => t('Show View changes button on node edit form'),
'#weight' => 10,
'#default_value' => variable_get('diff_show_preview_changes_node_' . $type->type, TRUE),
'#description' => t('You can refine access using the "!perm" permission.', array(
'!perm' => t('Access %view button', array('%view' => t('View changes'))),
)),
);
$form['diff']['diff_enable_revisions_page_node'] = array(
'#type' => 'checkbox',
'#title' => t('Enable the Revisions page for this content type'),
'#weight' => 11,
'#default_value' => variable_get('diff_enable_revisions_page_node_' . $type->type, TRUE),
);
$options = array();
$info = entity_get_info('node');
foreach ($info['view modes'] as $view_mode => $view_mode_info) {
$options[$view_mode] = $view_mode_info['label'];
}
$form['diff']['diff_view_mode_preview_node'] = array(
'#type' => 'select',
'#title' => t('Standard comparison preview'),
'#description' => t('Governs the Current revision view mode when doing standard comparisons.'),
'#options' => $options,
'#weight' => 13,
'#default_value' => variable_get('diff_view_mode_preview_node_' . $type->type, 'full'),
'#empty_value' => '',
'#empty_option' => t('- Do not display -'),
);
}
}
/**
* Implements hook_node_type_update().
*
* This tracks the diff settings in case the node content type is renamed.
*/
function diff_node_type_update($info) {
if (!empty($info->old_type) && $info->old_type != $info->type) {
$type_variables = array(
'diff_show_preview_changes_node',
'diff_enable_revisions_page_node',
'diff_view_mode_preview_node',
);
foreach ($type_variables as $prefix) {
$setting = variable_get($prefix . '_' . $info->old_type, NULL);
if (isset($setting)) {
variable_del($prefix . '_' . $info->old_type);
variable_set($prefix . '_' . $info->type, $setting);
}
}
// Block settings are combined in a single variable.
$inline_block_types = variable_get('diff_show_diff_inline_node_bundles', array());
if (isset($inline_block_types[$info->old_type])) {
if (!empty($inline_block_types[$info->old_type])) {
$inline_block_types[$info->type] = $info->type;
}
unset($inline_block_types[$info->old_type]);
variable_set('diff_show_diff_inline_node_bundles', $inline_block_types);
}
}
}
/**
* Implements hook_node_type_delete().
*/
function diff_node_type_delete($info) {
variable_del('diff_show_preview_changes_node_' . $info->type);
variable_del('diff_enable_revisions_page_node_' . $info->type);
variable_del('diff_view_mode_preview_node_' . $info->type);
}
/**
* Submit handler for the 'View changes' action.
*
* @see node_form_build_preview()
*/
function diff_node_form_build_preview_changes($form, &$form_state) {
module_load_include('inc', 'diff', 'diff.pages');
$old_node = clone node_load($form_state['values']['nid']);
$node = node_form_submit_build_node($form, $form_state);
// Create diff of old node and edited node.
$rows = _diff_body_rows($old_node, $node);
$header = _diff_default_header(t('Original'), t('Changes'));
$changes = theme('table__diff__preview', array(
'header' => $header,
'rows' => $rows,
'attributes' => array('class' => array('diff')),
'colgroups' => _diff_default_cols(),
'sticky' => FALSE,
));
// Prepend diff to edit form.
$form_state['node_preview'] = $changes;
$form_state['rebuild'] = TRUE;
}
/**
* Implementation of hook_features_pipe_COMPONENT_alter().
*/
function diff_features_pipe_node_alter(&$pipe, $data, $export) {
if (!empty($data)) {
$variables = array(
'diff_show_preview_changes_node',
'diff_enable_revisions_page_node',
'diff_view_mode_preview_node',
);
foreach ($data as $node_type) {
foreach ($variables as $variable_name) {
$pipe['variable'][] = $variable_name . '_' . $node_type;
}
}
}
}
/**
* Implements hook_theme().
*/
function diff_theme() {
return array(
'diff_node_revisions' => array(
'render element' => 'form',
'file' => 'diff.theme.inc',
),
'diff_header_line' => array(
'arguments' => array('lineno' => NULL),
'file' => 'diff.theme.inc',
),
'diff_content_line' => array(
'arguments' => array('line' => NULL),
'file' => 'diff.theme.inc',
),
'diff_empty_line' => array(
'arguments' => array('line' => NULL),
'file' => 'diff.theme.inc',
),
'diff_inline_form' => array(
'render element' => 'form',
'file' => 'diff.theme.inc',
),
'diff_inline_metadata' => array(
'arguments' => array('node' => NULL),
'file' => 'diff.theme.inc',
),
'diff_inline_chunk' => array(
'arguments' => array('text' => '', 'type' => NULL),
'file' => 'diff.theme.inc',
),
);
}
/**
* Render the table rows for theme('table').
*
* @param string $a
* The source string to compare from.
* @param string $b
* The target string to compare to.
* @param bool $show_header
* Display diff context headers. For example, "Line x".
* @param array $line_stats
* This structure tracks line numbers across multiple calls to DiffFormatter.
*
* @return array
* Array of rows usable with theme('table').
*/
function diff_get_rows($a, $b, $show_header = FALSE, &$line_stats = NULL) {
$a = is_array($a) ? $a : explode("\n", $a);
$b = is_array($b) ? $b : explode("\n", $b);
if (!isset($line_stats)) {
$line_stats = array(
'counter' => array('x' => 0, 'y' => 0),
'offset' => array('x' => 0, 'y' => 0),
);
}
$formatter = new DrupalDiffFormatter();
// Header is the line counter.
$formatter->show_header = $show_header;
$formatter->line_stats = &$line_stats;
$diff = new Diff($a, $b);
return $formatter->format($diff);
}
/**
* Render and markup a diff of two strings into HTML markup.
*
* @param string $a
* The source string to compare from.
* @param string $b
* The target string to compare to.
*
* @return string
* String containing HTML markup.
*/
function diff_get_inline($a, $b) {
$diff = new DrupalDiffInline($a, $b);
return $diff->render();
}
/**
* Form builder: Inline diff controls.
*/
function diff_inline_form($form, $form_state, $node, $revisions) {
$form = array();
$form['node'] = array(
'#type' => 'value',
'#value' => $node,
);
$form['revision'] = array(
'#type' => 'select',
'#options' => array(0 => t('- No highlighting -')),
'#default_value' => (arg(2) === 'revisions' && arg(3) === $node->vid) ? $node->vid : 0,
'#ajax' => array(
'callback' => 'diff_inline_ajax',
'wrapper' => "node-{$node->nid}",
'method' => 'replace',
),
);
foreach ($revisions as $revision) {
$form['revision']['#options'][$revision->vid] = t('@revision by @name', array(
'@revision' => format_date($revision->timestamp, 'short'),
'@name' => format_username($revision),
));
}
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('View'),
'#submit' => array('diff_inline_form_submit'),
'#attributes' => array('class' => array('diff-js-hidden')),
);
return $form;
}
/**
* AHAH callback for rendering the inline diff of a node.
*/
function diff_inline_ajax($form, $form_state) {
module_load_include('inc', 'diff', 'diff.pages');
$node = $form['node']['#value'];
$vid = isset($form_state['values']['revision']) ? $form_state['values']['revision'] : 0;
return "" . diff_inline_show($node, $vid) . "
";
}
/**
* Form submission handler for diff_inline_form() for JS-disabled clients.
*/
function diff_inline_form_submit(&$form, &$form_state) {
if (isset($form_state['values']['revision'], $form_state['values']['node'])) {
$node = $form_state['values']['node'];
$vid = $form_state['values']['revision'];
$form_state['redirect'] = "node/{$node->nid}/revisions/{$vid}/view";
}
}
/**
* A helper function to normalise system differences.
*
* This handles differences in:
* - line endings: Mac, Windows and UNIX all use different line endings.
*/
function diff_normalise_text($text) {
$text = str_replace(array("\r\n", "\r"), "\n", $text);
return $text;
}
/**
* A wrapper function for filter_xss() to exclude all tags.
*/
function diff_filter_xss($string) {
return filter_xss($string, array());
}
/**
* Helper function to load any CSS or JScript files required by a page or form.
*/
function diff_build_attachments($jscript = FALSE) {
$attachments = array();
$theme = variable_get('diff_theme', 'default');
if ($theme) {
$attachments['css'] = array(
drupal_get_path('module', 'diff') . "/css/diff.{$theme}.css",
);
}
$type = variable_get('diff_radio_behavior', 'simple');
if ($jscript && $type) {
$attachments['js'] = array(
drupal_get_path('module', 'diff') . "/js/diff.js",
array(
'data' => array('diffRevisionRadios' => $type),
'type' => 'setting',
),
);
}
return $attachments;
}
/**
* Implements hook_entity_diff() on behalf of the Node module.
*/
function node_entity_diff($old_node, $new_node, $context) {
$result = array();
if ($context['entity_type'] == 'node') {
module_load_include('inc', 'diff', 'includes/node');
$options = variable_get('diff_additional_options_node', array('title' => 'title'));
foreach (node_entity_diff_options('node') as $key => $option_label) {
if (!empty($options[$key])) {
$func = '_node_entity_diff_additional_options_' . $key;
$result[$key] = $func($old_node, $new_node, $context);
}
}
}
return $result;
}
/**
* Implements hook_entity_diff_options() on behalf of the Node module.
*/
function node_entity_diff_options($entity_type) {
if ($entity_type == 'node') {
$options = array(
'title' => t('Title field'),
// Author field is either the owner or revision creator, neither capture
// a change in the author field.
'author' => t('Author'),
'revision_author' => t('Revision author'),
'type' => t('Node type'),
'publishing_flags' => t('Publishing options'),
// More fields that currently can not be tracked.
'created' => t('Created date'),
'changed' => t('Updated date'),
'revision_timestamp' => t('Revision timestamp'),
);
if (module_exists('comment')) {
$options['comment'] = t('Comment setting');
}
return $options;
}
}
/**
* Implements hook_entity_diff() on behalf of the User module.
*/
function user_entity_diff($old_user, $new_user, $context) {
$result = array();
if ($context['entity_type'] == 'user') {
module_load_include('inc', 'diff', 'includes/user');
$options = variable_get('diff_additional_options_user', array(
'name' => 'name',
'mail' => 'mail',
'status' => 'status',
));
foreach (user_entity_diff_options('user') as $key => $option_label) {
if (!empty($options[$key])) {
$func = '_user_entity_diff_additional_options_' . $key;
$result[$key] = $func($old_user, $new_user, $context);
}
}
}
return $result;
}
/**
* Implements hook_entity_diff_options() on behalf of the User module.
*/
function user_entity_diff_options($entity_type) {
if ($entity_type == 'user') {
$options = array(
'name' => t('Username'),
'mail' => t('E-mail address'),
'roles' => t('Roles'),
'status' => t('Status'),
'timezone' => t('Time zone'),
'password' => t('Password Hash'),
);
return $options;
}
}