'. t('The diff module enables users to view the difference between two node revisions. You may disable this feature for individual content types on the content type configuration page. This module also provides a helpful %preview_changes button while editing a post.', array('%preview_changes' => t('Preview changes'), '%view_revisions' => t('view revisions'))) .'

'; return $output; } } /** * Implementation of hook_menu() */ function diff_menu() { $items = array(); $items['node/%node/revisions/view/%/%'] = array( 'title' => 'Diff', 'page callback' => 'diff_diffs_show', 'page arguments' => array(1, 4, 5), 'type' => MENU_CALLBACK, 'access callback' => '_node_revision_access', 'access arguments' => array(1), ); $items['node/%node/revisions/view/latest'] = array( 'title' => 'Diff', 'page callback' => 'diff_latest', 'page arguments' => array(1), 'type' => MENU_CALLBACK, 'access callback' => '_node_revision_access', 'access arguments' => array(1), ); return $items; } // Menu callback - show latest diff for a given node. function diff_latest($node) { $revisions = node_revision_list($node); $new = array_shift($revisions); $old = array_shift($revisions); drupal_goto("node/$node->nid/revisions/view/$old->vid/$new->vid"); } /** * Implementation of hook_menu_alter(). */ function diff_menu_alter(&$callbacks) { // Override the default 'Revisions' page $callbacks['node/%node/revisions']['page callback'] = 'diff_diffs_overview'; $callbacks['node/%node/revisions']['module'] = 'diff'; unset($callbacks['node/%node/revisions']['file']); } /** * Generate an overview table of older revisions of a node and provide * an input form to select two revisions for a comparison. */ function diff_diffs_overview(&$node) { drupal_set_title(t('Revisions for %title', array('%title' => $node->title))); return drupal_get_form('diff_node_revisions', $node); } /** * Input form to select two revisions. * * @param $node * Node whose revisions are displayed for selection. */ function diff_node_revisions($form_state, &$node) { $form = array(); $form['nid'] = array( '#type' => 'hidden', '#value' => $node->nid, ); $revision_list = node_revision_list($node); if (count($revision_list) > REVISION_LIST_SIZE) { // If the list of revisions is longer than the number shown on one page split the array. $page = isset($_GET['page']) ? $_GET['page'] : '0'; $revision_chunks = array_chunk(node_revision_list($node), REVISION_LIST_SIZE); $revisions = $revision_chunks[$page]; // Set up global pager variables as would 'pager_query' do. // These variables are then used in the theme('pager') call later. global $pager_page_array, $pager_total, $pager_total_items; $pager_total_items[0] = count($revision_list); $pager_total[0] = ceil(count($revision_list) / REVISION_LIST_SIZE); $pager_page_array[0] = max(0, min($page, ((int)$pager_total[0]) - 1)); } else { $revisions = $revision_list; } $revert_permission = FALSE; if ((user_access('revert revisions') || user_access('administer nodes')) && node_access('update', $node)) { $revert_permission = TRUE; } $delete_permission = FALSE; if (user_access('administer nodes')) { $delete_permission = TRUE; } foreach ($revisions as $revision) { $operations = array(); $revision_ids[$revision->vid] = ''; if ($revision->current_vid > 0) { $form['info'][$revision->vid] = array( '#markup' => t('!date by !username', array( '!date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid"), '!username' => theme('username', $revision))) . (($revision->log != '') ? '

'. filter_xss($revision->log) .'

' : ''), ); } else { $form['info'][$revision->vid] = array( '#markup' => t('!date by !username', array( '!date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid/revisions/$revision->vid/view"), '!username' => theme('username', $revision))) . (($revision->log != '') ? '

'. filter_xss($revision->log) .'

' : '') ); if ($revert_permission) { $operations[] = array('#markup' => l(t('revert'), "node/$node->nid/revisions/$revision->vid/revert")); } if ($delete_permission) { $operations[] = array('#markup' => l(t('delete'), "node/$node->nid/revisions/$revision->vid/delete")); } // Set a dummy, even if the user has no permission for the other // operations, so that we can check if the operations array is // empty to know if the row denotes the current revision. $operations[] = array(); } $form['operations'][$revision->vid] = $operations; } $new_vid = key($revision_ids); next($revision_ids); $old_vid = key($revision_ids); $form['diff']['old'] = array( '#type' => 'radios', '#options' => $revision_ids, '#default_value' => $old_vid ); $form['diff']['new'] = array( '#type' => 'radios', '#options' => $revision_ids, '#default_value' => $new_vid ); $form['submit'] = array('#type' => 'submit', '#value' => t('Show diff')); if (count($revision_list) > REVISION_LIST_SIZE) { $form['#suffix'] = theme('pager', NULL, REVISION_LIST_SIZE, 0); } return $form; } /** * Theme function to display the revisions table with means to select * two revisions. */ function theme_diff_node_revisions($form) { $output = ''; // Overview table: $header = array( t('Revision'), array('data' => drupal_render($form['submit']), 'colspan' => 2), array('data' => t('Operations'), 'colspan' => 2) ); if (isset($form['info']) && is_array($form['info'])) { foreach (element_children($form['info']) as $key) { $row = array(); if (isset($form['operations'][$key][0])) { // Note: even if the commands for revert and delete are not permitted, // the array is not empty since we set a dummy in this case. $row[] = drupal_render($form['info'][$key]); $row[] = drupal_render($form['diff']['old'][$key]); $row[] = drupal_render($form['diff']['new'][$key]); $row[] = drupal_render($form['operations'][$key][0]); $row[] = drupal_render($form['operations'][$key][1]); $rows[] = $row; } else { // its the current revision (no commands to revert or delete) $row[] = array('data' => drupal_render($form['info'][$key]), 'class' => 'revision-current'); $row[] = array('data' => drupal_render($form['diff']['old'][$key]), 'class' => 'revision-current'); $row[] = array('data' => drupal_render($form['diff']['new'][$key]), 'class' => 'revision-current'); $row[] = array('data' => theme('placeholder', t('current revision')), 'class' => 'revision-current', 'colspan' => '2'); $rows[] = array( 'data' => $row, 'class' => 'error', ); } } } $output .= theme('table', $header, $rows); $output .= drupal_render($form); return $output; } /** * Submit handler which redirects to the selected diff page. */ function diff_node_revisions_submit($form, &$form_state) { // The ids are ordered so the old revision is always on the left. $old_vid = min($form_state['values']['old'], $form_state['values']['new']); $new_vid = max($form_state['values']['old'], $form_state['values']['new']); $form_state['redirect'] = 'node/'. $form_state['values']['nid'] .'/revisions/view/'. $old_vid .'/'. $new_vid; } /** * Validation for input form to select two revisions. */ function diff_node_revisions_validate($form, &$form_state) { $old_vid = $form_state['values']['old']; $new_vid = $form_state['values']['new']; if ($old_vid==$new_vid || !$old_vid || !$new_vid) { form_set_error('diff', t('Select different revisions to compare.')); } } /** * Menu callback. Show differences between two node revisions. * * @param $node * Node on which to perform comparison * @param $old_vid * Version ID of the old revision. * @param $new_vid * Version ID of the new revision. */ function diff_diffs_show(&$node, $old_vid, $new_vid) { // Set same title as on the 'Revisions' tab for consistency drupal_set_title(t('Revisions for %title', array('%title' => $node->title))); $node_revisions = node_revision_list($node); $old_node = node_load($node->nid, $old_vid); $new_node = node_load($node->nid, $new_vid); // Generate table header (date, username, logmessage). $old_header = t('!date by !username', array( '!date' => l(format_date($old_node->revision_timestamp), "node/$node->nid/revisions/$old_node->vid/view"), '!username' => theme('username', $node_revisions[$old_vid]), )); $new_header = t('!date by !username', array( '!date' => l(format_date($new_node->revision_timestamp), "node/$node->nid/revisions/$new_node->vid/view"), '!username' => theme('username', $node_revisions[$new_vid]), )); $old_log = $old_node->log != '' ? '

'. filter_xss($old_node->log) .'

' : ''; $new_log = $new_node->log != '' ? '

'. filter_xss($new_node->log) .'

' : ''; // Generate previous diff/next diff links. $next_vid = _diff_get_next_vid($node_revisions, $new_vid); if ($next_vid) { $next_link = l(t('next diff >'), 'node/'. $node->nid .'/revisions/view/'. $new_vid .'/'. $next_vid); } else { $next_link = ''; } $prev_vid = _diff_get_previous_vid($node_revisions, $old_vid); if ($prev_vid) { $prev_link = l(t('< previous diff'), 'node/'. $node->nid .'/revisions/view/'. $prev_vid .'/'. $old_vid); } else { $prev_link = ''; } $cols = _diff_default_cols(); $header = _diff_default_header($old_header, $new_header); $rows = array(); if ($old_log || $new_log) { $rows[] = array( array( 'data' => $old_log, 'colspan' => 2 ), array( 'data' => $new_log, 'colspan' => 2 ) ); } $rows[] = array( array( 'data' => $prev_link, 'class' => 'diff-prevlink', 'colspan' => 2 ), array( 'data' => $next_link, 'class' => 'diff-nextlink', 'colspan' => 2 ) ); $rows = array_merge($rows, _diff_body_rows($old_node, $new_node)); $output = theme('table', $header, $rows, array('class' => 'diff'), NULL, $cols); if ($node->vid == $new_vid) { $output .= '
'. t('Current revision:') .'
'; } else { $output .= '
'. t('Revision of !new_date:', array('!new_date' => format_date($new_node->revision_timestamp))) .'
'; } // Don't include node links (final argument) when viewing the diff. $output .= node_view($new_node, FALSE, FALSE, FALSE); return $output; } /** * Creates an array of rows which represent a diff between $old_node and $new_node. * * @param $old_node * Node for comparison which will be displayed on the left side. * @param $new_node * Node for comparison which will be displayed on the right side. */ function _diff_body_rows(&$old_node, &$new_node) { drupal_add_css(drupal_get_path('module', 'diff') .'/diff.css', 'module', 'all', FALSE); module_load_include('inc', 'diff', 'diff.engine'); include_once('node.inc'); if (module_exists('taxonomy')) { module_load_include('inc', 'diff', 'taxonomy'); } if (module_exists('upload')) { module_load_include('inc', 'diff', 'upload'); } $rows = array(); $any_visible_change = FALSE; $node_diffs = module_invoke_all('diff', $old_node, $new_node); // We start off assuming all form elements are in the correct order. $node_diffs['#sorted'] = TRUE; // Recurse through all child elements. $count = 0; foreach (element_children($node_diffs) as $key) { // Assign a decimal placeholder weight to preserve original array order. if (!isset($node_diffs[$key]['#weight'])) { $node_diffs[$key]['#weight'] = $count/1000; } else { // If one of the child elements has a weight then we will need to sort // later. unset($node_diffs['#sorted']); } $count++; } // One of the children has a #weight. if (!isset($node_diffs['#sorted'])) { uasort($node_diffs, "element_sort"); } foreach ($node_diffs as $node_diff) { $diff = new Diff($node_diff['#old'], $node_diff['#new']); $formatter = new DrupalDiffFormatter(); if (isset($node_diff['#format'])) { $formatter->show_header = $node_diff['#format']['show_header']; } $diff_rows = $formatter->format($diff); if (count($diff_rows)) { $rows[] = array( array( 'data' => t('Changes to %name', array('%name' => $node_diff['#name'])), 'class' => 'diff-section-title', 'colspan' => 4 ) ); $rows = array_merge($rows, $diff_rows); $any_visible_change = TRUE; } } if (!$any_visible_change) { $rows[] = array( array( 'data' => t('No visible changes'), 'class' => 'diff-section-title', 'colspan' => 4 ), ); // Needed to keep Safari happy. $rows[] = array( array('data' => ''), array('data' => ''), array('data' => ''), array('data' => ''), ); } return $rows; } /** * Get the entry in the revisions list after $vid. * Returns FALSE if $vid is the last entry. * * @param $node_revisions * Array of node revision IDs in descending order. * @param $vid * Version ID to look for. */ function _diff_get_next_vid(&$node_revisions, $vid) { $previous = NULL; foreach ($node_revisions as $revision) { if ($revision->vid == $vid) { return ($previous ? $previous->vid : FALSE); } $previous = $revision; } return FALSE; } /** * Get the entry in the revision list before $vid. * Returns FALSE if $vid is the first entry. * * @param $node_revisions * Array of node revision IDs in descending order. * @param $vid * Version ID to look for. */ function _diff_get_previous_vid(&$node_revisions, $vid) { $previous = NULL; foreach ($node_revisions as $revision) { if ($previous && $previous->vid == $vid) { return $revision->vid; } $previous = $revision; } return FALSE; } /** * Implementation of hook_form_alter(). */ function diff_form_alter(&$form, $form_state, $form_id) { if (isset($form['type']['#value']) && $form['type']['#value'] .'_node_form' == $form_id) { // Add a 'Preview changes' button on the node edit form. if (variable_get('show_preview_changes_'. $form['type']['#value'], TRUE) && $form['nid']['#value'] > 0) { $form['buttons']['preview_changes'] = array( '#type' => 'submit', '#value' => t('Preview changes'), '#weight' => 12, '#submit' => array('diff_node_form_build_preview_changes') ); } } elseif ($form_id == 'node_type_form' && isset($form['identity']['type'])) { // Node type edit form. // Add checkbox to activate 'Preview changes' button per node type. $form['workflow']['show_preview_changes'] = array( '#type' => 'checkbox', '#title' => t('Show %preview_changes button on node edit form', array('%preview_changes' => t('Preview changes'))), '#prefix' => ''. t('Preview changes') .'', '#weight' => 10, '#default_value' => variable_get('show_preview_changes_'. $form['#node_type']->type, TRUE), ); } } /** * Submit handler for 'Preview changes' button. */ function diff_node_form_build_preview_changes($form, &$form_state) { $node = node_form_submit_build_node($form, $form_state); // Create diff of old node and edited node $rows = _diff_body_rows(node_load($form_state['values']['nid']), $node); $cols = _diff_default_cols(); $header = _diff_default_header(); $changes = theme('diff_table', $header, $rows, array('class' => 'diff'), NULL, $cols); // Prepend diff to edit form $form_state['node_preview'] = isset($form_state['node_preview']) ? $changes . $form_state['node_preview'] : $changes; } /** * Theme function for a header line in the diff. */ function theme_diff_header_line($lineno) { return ''. t('Line %lineno', array('%lineno' => $lineno)) .''; } /** * Theme function for a content line in the diff. */ function theme_diff_content_line($line) { return '
'. $line .'
'; } /** * Theme function for an empty line in the diff. */ function theme_diff_empty_line($line) { return $line; } /** * Implementation of hook_theme(). */ function diff_theme() { return array( 'diff_node_revisions' => array( 'arguments' => array('form' => NULL), ), 'diff_header_line' => array( 'arguments' => array('lineno' => NULL), ), 'diff_content_line' => array( 'arguments' => array('line' => NULL), ), 'diff_empty_line' => array( 'arguments' => array('line' => NULL), ), ); } /** * Helper function to create default 'cols' array for diff table. */ function _diff_default_cols() { return array( array( array( 'class' => 'diff-marker', ), array( 'class' => 'diff-content', ), array( 'class' => 'diff-marker', ), array( 'class' => 'diff-content', ), ), ); } /** * Helper function to create default 'header' array for diff table. */ function _diff_default_header($old_header = '', $new_header = '') { return array( array( 'data' => $old_header, 'colspan' => 2 ), array( 'data' => $new_header, 'colspan' => 2 ) ); }