You are here

diff.module in Diff 5

File

diff.module
View source
<?php

/**
 * Number of items on one page of the revision list.
 */
define('REVISION_LIST_SIZE', 50);

/**
 * Implementation of hook_help().
 */
function diff_help($section) {
  switch ($section) {
    case 'admin/help#diff':
      $output = '<p>' . 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'),
      )) . '</p>';
      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 != '' ? '<p class="revision-log">' . filter_xss($revision->log) . '</p>' : ''),
      );
    }
    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 != '' ? '<p class="revision-log">' . filter_xss($revision->log) . '</p>' : ''),
      );
      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 != '' ? '<p class="revision-log">' . filter_xss($old_node->log) . '</p>' : '';
  $new_log = $new_node->log != '' ? '<p class="revision-log">' . filter_xss($new_node->log) . '</p>' : '';

  // 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 .= '<table class="diff">';
  $output .= '<col class="diff-marker" /><col class="diff-content" /><col class="diff-marker" /><col class="diff-content" />';
  $output .= '<thead><tr><th></th><th>' . $old_header . '</th><th></th><th>' . $new_header . '</th></tr></thead>';
  if ($new_log || $old_log) {
    $output .= '<tr><td></td><td>' . $old_log . '</td><td></td><td>' . $new_log . '</td></tr>';
  }
  $output .= '<tr><td></td><td class="diff-prevlink">' . $prev_link . '</td><td></td><td class="diff-nextlink">' . $next_link . '</td></tr>';
  $output .= _diff_table_body($old_node, $new_node);
  $output .= '</table>';
  $output .= '<hr/>';
  if ($node->vid == $new_vid) {
    $output .= '<div class="diff-section-title">' . t('Current revision:') . '</div>';
  }
  else {
    $output .= '<div class="diff-section-title">' . t('Revision of !new_date:', array(
      '!new_date' => format_date($new_node->revision_timestamp),
    )) . '</div>';
  }

  // 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 <tbody> 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 = '<tbody>';
  $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 .= '<tr><td colspan="4" class="diff-section-title">' . t('Changes to %name', array(
        '%name' => $node_diff['name'],
      )) . '</td></tr>';
      $output .= $formatter_output;
      $any_visible_change = true;
    }
  }
  if (!$any_visible_change) {
    $output .= '<tr><td colspan="4" class="diff-section-title">' . t('No visible changes') . '</td></tr>';
  }
  $output .= '</tbody>';
  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' => '<strong>' . t('Preview changes') . '</strong>',
      '#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 = '<table class="diff">';
    $changes .= '<col class="diff-marker" /><col class="diff-content" /><col class="diff-marker" /><col class="diff-content" />';
    $changes .= _diff_table_body(node_load($form_values['nid']), $node);
    $changes .= '</table>';
    $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<div class=\"node-form\">\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 .= "    <div class=\"authored\">\n";
    $admin .= drupal_render($form['author']);
    $admin .= "    </div>\n";
  }
  if (isset($form['options'])) {
    $admin .= "    <div class=\"options\">\n";
    $admin .= drupal_render($form['options']);
    $admin .= "    </div>\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 .= "  <div class=\"standard\">\n";
  $output .= drupal_render($form);
  $output .= "  </div>\n";
  if (!empty($admin)) {
    $output .= "  <div class=\"admin\">\n";
    $output .= $admin;
    $output .= "  </div>\n";
  }
  $output .= $buttons;
  $output .= "</div>\n";
  return $output;
}

Functions

Namesort descending Description
diff_autoadjust Adjust the module weights for diff to load after node module.
diff_diffs Menu callback for diff related activities.
diff_diffs_overview Generate an overview table of older revisions of a node and provide an input form to select two revisions for a comparison.
diff_diffs_show Create output string for a comparison of 'node' between versions 'old_vid' and 'new_vid'.
diff_form_alter Implementation of hook_form_alter(). Used to add a 'Preview changes' button on the node edit form.
diff_help Implementation of hook_help().
diff_menu Implementation of hook_menu() The menu path 'node/$nid/revisions' is overriden with 'diff_diffs'.
diff_node_form_add_changes Callback for node edit form to add the 'Preview changes' output.
diff_node_revisions Input form to select two revisions.
diff_node_revisions_submit Submit code for input form to select two revisions.
diff_node_revisions_validate Validation for input form to select two revisions.
diff_requirements Implementation of hook_requirements(). Checks if the diff modules is loaded after the node module in the hook ordering.
theme_diff_node_form A copy of the 'theme_node_form' function with the addition of the new button to show a diff of the changes.
theme_diff_node_revisions Theme function to display the revisions formular with means to select two revisions.
_diff_get_next_vid Get the entry in the revisions list after $vid. Returns false if $vid is the last entry.
_diff_get_previous_vid Get the entry in the revision list before $vid. Returns false if $vid is the first entry.
_diff_table_body Create the table body of the diff between $old_node and $new_node. The result is a html table part enclosed in <tbody> tags.

Constants

Namesort descending Description
REVISION_LIST_SIZE Number of items on one page of the revision list.