'. 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; } } /** * Implementation of hook_requirements(). * Checks if the diff modules is loaded after the node module in the hook ordering. */ function diff_requirements($phase) { // Don't check when installing if ($phase == 'install') { return; } $modules = array_keys(module_list()); if (array_search('diff', $modules) <= array_search('node', $modules)) { diff_autoadjust(); } } /** * Implementation of hook_menu() * The menu path 'node/$nid/revisions' is overriden with 'diff_diffs'. */ function diff_menu($may_cache) { $items = array(); if (!$may_cache) { if (arg(0) == 'node' && is_numeric(arg(1))) { $node = node_load(arg(1)); if ($node->nid) { $revisions_access = (user_access('view revisions') || user_access('administer nodes')) && node_access('view', $node) && db_result(db_query('SELECT COUNT(vid) FROM {node_revisions} WHERE nid = %d', arg(1))) > 1; $items[] = array( 'path' => 'node/'. arg(1) .'/revisions', 'title' => t('Revisions'), 'callback' => 'diff_diffs', 'access' => $revisions_access, 'weight' => 4, 'type' => MENU_LOCAL_TASK, ); } } } return $items; } /** * Adjust the module weights for diff to load after node module. */ function diff_autoadjust() { $modules = array_keys(module_list()); if (array_search('diff', $modules) <= array_search('node', $modules)) { module_load_install('diff'); diff_set_weight(); } } /** * Menu callback for diff related activities. */ function diff_diffs() { if (is_numeric(arg(1)) && arg(2) == 'revisions') { $op = arg(3) ? arg(3) : 'overview'; switch ($op) { case 'overview': $node = node_load(arg(1)); if ((user_access('view revisions') || user_access('administer nodes')) && node_access('view', $node)) { return diff_diffs_overview($node); } drupal_access_denied(); return; case 'view': if (is_numeric(arg(4)) && is_numeric(arg(5))) { $node = node_load(arg(1)); if ($node->nid) { if ((user_access('view revisions') || user_access('administer nodes')) && node_access('view', $node)) { drupal_set_title(t('Diff for %title', array('%title' => $node->title))); return diff_diffs_show($node, arg(4), arg(5)); } drupal_access_denied(); return; } } break; default: // A view, revert or delete operation from the orignial node module, // so call the original node module to handle this. return node_revisions(); break; } } drupal_not_found(); } /** * 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(&$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) { // 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_id, $form_values) { // the ids are ordered so the old revision is always on the left $old_vid = min($form_values['old'], $form_values['new']); $new_vid = max($form_values['old'], $form_values['new']); return 'node/'.$form_values['nid'].'/revisions/view/'.$old_vid.'/'.$new_vid; } /** * Validation for input form to select two revisions. */ function diff_node_revisions_validate($form_id, $form_values) { $old_vid = $form_values['old']; $new_vid = $form_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) { $lame_revisions = node_revision_list($node); foreach ($lame_revisions as $revision) { $node_revisions[$revision->vid] = $revision; } $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 = ''; } // display table $output .= ''; $output .= ''; $output .= ''; if ($new_log || $old_log) { $output .= ''; } $output .= ''; $output .= _diff_table_body($old_node, $new_node); $output .= '
'. $old_header .''. $new_header .'
'. $old_log .''. $new_log .'
'; $output .= '
'; 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; } /** * Create the table body of the diff between $old_node and $new_node. * The result is a html table part enclosed in tags. * * @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_table_body(&$old_node, &$new_node) { drupal_add_css(drupal_get_path('module', 'diff') .'/diff.css', 'module', 'all', FALSE); include_once('DiffEngine.php'); include_once('node.inc'); if (module_exists('upload')) { include_once('upload.inc'); } if (module_exists('content')) { include_once('cck.inc'); } $output = ''; $any_visible_change = false; $node_diffs = module_invoke_all('diff', $old_node, $new_node); foreach($node_diffs as $node_diff) { $diff = new Diff($node_diff['old'], $node_diff['new']); $formatter = new TableDiffFormatter(); if (isset($node_diff['format'])) { $formatter->show_header = $node_diff['format']['show_header']; } $formatter_output = $formatter->format($diff); if ($formatter_output) { $output .= ''. t('Changes to %name', array('%name' => $node_diff['name'])) .''; $output .= $formatter_output; $any_visible_change = true; } } if (!$any_visible_change) { $output .= '' .t('No visible changes') .''; } $output .= ''; return $output; } /** * 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(). * Used to add a 'Preview changes' button on the node edit form. */ function diff_form_alter($form_id, &$form) { if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) { // Node editing form. // Add a 'Preview changes' button. if (variable_get('show_preview_changes_'. $form['type']['#value'], TRUE) && $form['nid']['#value'] > 0) { $form['preview_changes'] = array( '#type' => 'button', '#value' => t('Preview changes'), '#weight' => 41, ); // Change the form render callback to display the new button $form['#theme'] = 'diff_node_form'; // Add a callback to handle showing the diff if requested. if (isset($form['#after_build']) && is_array($form['#after_build'])) { $form['#after_build'][] = 'diff_node_form_add_changes'; } else { $form['#after_build'] = array('diff_node_form_add_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), ); } } /** * Callback for node edit form to add the 'Preview changes' output. */ function diff_node_form_add_changes($form) { global $form_values; $op = isset($form_values['op']) ? $form_values['op'] : ''; if ($op == t('Preview changes')) { $node = (object)$form_values; $changes = ''; $changes .= ''; $changes .= _diff_table_body(node_load($form_values['nid']), $node); $changes .= '
'; $form['#prefix'] = isset($form['#prefix']) ? $changes . $form['#prefix'] : $changes; } return $form; } /** * Theme functions */ /** * A copy of the 'theme_node_form' function with the addition of the new button * to show a diff of the changes. */ function theme_diff_node_form($form) { $output = "\n
\n"; // Admin form fields and submit buttons must be rendered first, because // they need to go to the bottom of the form, and so should not be part of // the catch-all call to drupal_render(). $admin = ''; if (isset($form['author'])) { $admin .= "
\n"; $admin .= drupal_render($form['author']); $admin .= "
\n"; } if (isset($form['options'])) { $admin .= "
\n"; $admin .= drupal_render($form['options']); $admin .= "
\n"; } $buttons = drupal_render($form['preview']); $buttons .= isset($form['preview_changes']) ? drupal_render($form['preview_changes']) : ''; $buttons .= drupal_render($form['submit']); $buttons .= isset($form['delete']) ? drupal_render($form['delete']) : ''; // Everything else gets rendered here, and is displayed before the admin form // field and the submit buttons. $output .= "
\n"; $output .= drupal_render($form); $output .= "
\n"; if (!empty($admin)) { $output .= "
\n"; $output .= $admin; $output .= "
\n"; } $output .= $buttons; $output .= "
\n"; return $output; }