'. t('The diff module overwrites the normal revisions view. The revisions table is enhanced with a possibility to view the difference between two node revisions. Users with the %view_revisions permission will also be able to view the changes between any two selected revisions. You may disable this for individual content types on the content type configuration page. This module also provides a nifty %preview_changes button while editing a post.', array('%preview_changes' => t('Preview changes'), '%view_revisions' => t('view revisions'))) .'
'; return $output; case 'node/%/revisions/%/view': // the following string is copied from string copied from node_help('node/%/revisions') return ''. t('The revisions let you track differences between multiple versions of a post.') .'
'; case 'node/%/revisions/view/%/%': return ''. t('Comparing two revisions:') .'
'; } } /** * Implementation of hook_menu() */ function diff_menu() { $items = array(); /** * 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'. */ $items['node/%node/revisions/list'] = array( // Not used directly, but was created to get the other menu items to work well 'title' => 'List revisions', 'page callback' => 'diff_diffs_overview', 'type' => MENU_DEFAULT_LOCAL_TASK, 'access callback' => '_node_revision_access', 'access arguments' => array(1), ); $items['node/%node/revisions/view/%/%'] = array( 'title' => 'Diff', 'page callback' => 'diff_diffs_show', 'page arguments' => array(1, 4, 5), 'type' => MENU_LOCAL_TASK, 'access callback' => '_node_revision_access', 'access arguments' => array(1), 'tab_parent' => 'node/%/revisions/list', ); $items['node/%node/revisions/view/latest'] = array( 'title' => 'Show latest diff', 'page callback' => 'diff_latest', 'page arguments' => array(1), 'type' => MENU_LOCAL_TASK, 'access callback' => '_node_revision_access', 'access arguments' => array(1), 'tab_parent' => 'node/%/revisions/view', ); 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) { // Overwrite 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']); $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'; return; } /** * Implementation of hook_block(). */ function diff_block($op = 'list', $delta = 0, $edit = array()) { switch ($op) { case 'list': return array('inline' => array('info' => t('Diff: inline diff revisions'))); case 'view': switch ($delta) { case 'inline': $block = array(); if (!empty($_SESSION['diff_inline_highlight'])) { $node = menu_get_object(); $revisions = node_revision_list($node); if (count($revisions) > 1) { $block['subject'] = t('Revisions'); $block['content'] = theme('diff_inline_revisions', $node, $revisions); } } return $block; } break; } } /** * Implementation of hook_nodeapi(). */ function diff_nodeapi(&$node, $op, $teaser, $page) { switch ($op) { case 'view': if ($page && user_access('view revisions') && variable_get('show_diff_inline_'. $node->type, FALSE)) { $revisions = node_revision_list($node); // Set the hilight flag if specified in the URL if (isset($_GET['diff'])) { $_SESSION['diff_inline_highlight'] = !empty($_GET['diff']); } // Only render the form if there are multiple revisions if (count($revisions) > 1) { drupal_add_css(drupal_get_path('module', 'diff') .'/diff.css', 'module', 'all', FALSE); $node->content['diff_inline'] = array( '#value' => theme('diff_inline_controls', $node), '#weight' => -100, ); $vids = array_keys($revisions); $position = array_search($node->vid, $vids) + 1; $old = isset($vids[$position]) ? $vids[$position] : NULL; // Set the active vid before we blow this information away from others by menu_set_active_item(). _diff_inline_set_vid($node->vid); // @TODO: This is a bad hack that should instead be a patch against the node module. menu_set_active_item("node/{$node->nid}"); // Only highlight changes if inline highlighting is enabled & there // is actually an older revision to diff against. if ($old && !empty($_SESSION['diff_inline_highlight'])) { module_load_include('php', 'diff', 'DiffEngine'); $new = drupal_clone($node); // We could use a node_load() / node_prepare() combo here but for now // we would rather save queries... $old = db_fetch_object(db_query("SELECT body, format FROM {node_revisions} WHERE nid = %d AND vid = %d", $node->nid, $old)); $old->body = check_markup($old->body, $old->format, FALSE); $new = preg_split('/(<[^>]+?>)/', $new->body, -1, PREG_SPLIT_DELIM_CAPTURE); $old = preg_split('/(<[^>]+?>)/', $old->body, -1, PREG_SPLIT_DELIM_CAPTURE); $diff = new Diff($old, $new); // Assemble highlighted output $output = ''; foreach ($diff->edits as $chunk) { switch ($chunk->type) { case 'copy': $output .= implode("", $chunk->closing); break; case 'add': foreach ($chunk->closing as $i =>$piece) { if (strpos($piece, '<') === 0 && substr($piece, strlen($piece) - 1) === '>') { $output .= $piece; } else { $output .= "{$piece}"; } } break; case 'change': foreach ($chunk->closing as $i =>$piece) { if (strpos($piece, '<') === 0 && substr($piece, strlen($piece) - 1) === '>') { $output .= $piece; } else { $output .= "{$piece}"; } } break; default: foreach ($chunk->orig as $i =>$piece) { if (strpos($piece, '<') === 0 && substr($piece, strlen($piece) - 1) === '>') { $output .= $piece; } else { $output .= "{$piece}"; } } break; } } $node->content['body']['#value'] = $output; } } } break; } } /** * 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) { $output = ''; drupal_set_title(t('Revisions for %title', array('%title' => $node->title))); $output .= drupal_get_form('diff_node_revisions', $node); return $output; } /** * Input form to select two revisions. * * @param $node * Node whose revisions are displayed for selection. */ function diff_node_revisions($form_state, &$node) { global $form_values; $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( '#value' => 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( '#value' => 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('#value' => l(t('revert'), "node/$node->nid/revisions/$revision->vid/revert")); } if ($delete_permission) { $operations[] = array('#value' => 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 formular 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 code for input form to select two revisions. */ 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.')); } } /** * Create output string for a comparison of 'node' between * versions 'old_vid' and 'new_vid'. * * @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('diff_table', $header, $rows, array('class' => 'diff'), NULL, $cols); if ($node->vid == $new_vid) { $output .= '