/[drupal]/contributions/modules/diff/diff.module
ViewVC logotype

Contents of /contributions/modules/diff/diff.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.26 - (show annotations) (download) (as text)
Tue Sep 2 18:36:44 2008 UTC (14 months, 3 weeks ago) by weitzman
Branch: MAIN
CVS Tags: HEAD
Changes since 1.25: +166 -420 lines
File MIME type: text/x-php
Add back a diff that is compatible with D7 core. See http://drupal.org/node/120955
1 <?php
2 // $Id$
3
4 /**
5 * @file
6 * Provides functionality to show a diff between two node revisions.
7 */
8
9 /**
10 * Number of items on one page of the revision list.
11 */
12 define('REVISION_LIST_SIZE', 50);
13
14 /**
15 * Implementation of hook_help().
16 */
17 function diff_help($path, $arg) {
18 switch ($path) {
19 case 'admin/help#diff':
20 $output = '<p>'. 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'))) .'</p>';
21 return $output;
22 }
23 }
24
25 /**
26 * Implementation of hook_menu()
27 */
28 function diff_menu() {
29 $items = array();
30 $items['node/%node/revisions/view/%/%'] = array(
31 'title' => 'Diff',
32 'page callback' => 'diff_diffs_show',
33 'page arguments' => array(1, 4, 5),
34 'type' => MENU_CALLBACK,
35 'access callback' => '_node_revision_access',
36 'access arguments' => array(1),
37 );
38 $items['node/%node/revisions/view/latest'] = array(
39 'title' => 'Diff',
40 'page callback' => 'diff_latest',
41 'page arguments' => array(1),
42 'type' => MENU_CALLBACK,
43 'access callback' => '_node_revision_access',
44 'access arguments' => array(1),
45 );
46 return $items;
47 }
48
49 // Menu callback - show latest diff for a given node.
50 function diff_latest($node) {
51 $revisions = node_revision_list($node);
52 $new = array_shift($revisions);
53 $old = array_shift($revisions);
54 drupal_goto("node/$node->nid/revisions/view/$old->vid/$new->vid");
55 }
56
57 /**
58 * Implementation of hook_menu_alter().
59 */
60 function diff_menu_alter(&$callbacks) {
61 // Override the default 'Revisions' page
62 $callbacks['node/%node/revisions']['page callback'] = 'diff_diffs_overview';
63 $callbacks['node/%node/revisions']['module'] = 'diff';
64 unset($callbacks['node/%node/revisions']['file']);
65 }
66
67 /**
68 * Generate an overview table of older revisions of a node and provide
69 * an input form to select two revisions for a comparison.
70 */
71 function diff_diffs_overview(&$node) {
72 drupal_set_title(t('Revisions for %title', array('%title' => $node->title)));
73 return drupal_get_form('diff_node_revisions', $node);
74 }
75
76 /**
77 * Input form to select two revisions.
78 *
79 * @param $node
80 * Node whose revisions are displayed for selection.
81 */
82 function diff_node_revisions($form_state, &$node) {
83 $form = array();
84
85 $form['nid'] = array(
86 '#type' => 'hidden',
87 '#value' => $node->nid,
88 );
89
90 $revision_list = node_revision_list($node);
91
92 if (count($revision_list) > REVISION_LIST_SIZE) {
93 // If the list of revisions is longer than the number shown on one page split the array.
94 $page = isset($_GET['page']) ? $_GET['page'] : '0';
95 $revision_chunks = array_chunk(node_revision_list($node), REVISION_LIST_SIZE);
96 $revisions = $revision_chunks[$page];
97 // Set up global pager variables as would 'pager_query' do.
98 // These variables are then used in the theme('pager') call later.
99 global $pager_page_array, $pager_total, $pager_total_items;
100 $pager_total_items[0] = count($revision_list);
101 $pager_total[0] = ceil(count($revision_list) / REVISION_LIST_SIZE);
102 $pager_page_array[0] = max(0, min($page, ((int)$pager_total[0]) - 1));
103 }
104 else {
105 $revisions = $revision_list;
106 }
107
108 $revert_permission = FALSE;
109 if ((user_access('revert revisions') || user_access('administer nodes')) && node_access('update', $node)) {
110 $revert_permission = TRUE;
111 }
112 $delete_permission = FALSE;
113 if (user_access('administer nodes')) {
114 $delete_permission = TRUE;
115 }
116
117 foreach ($revisions as $revision) {
118 $operations = array();
119 $revision_ids[$revision->vid] = '';
120 if ($revision->current_vid > 0) {
121 $form['info'][$revision->vid] = array(
122 '#markup' => t('!date by !username', array(
123 '!date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid"),
124 '!username' => theme('username', $revision)))
125 . (($revision->log != '') ? '<p class="revision-log">'. filter_xss($revision->log) .'</p>' : ''),
126 );
127 }
128 else {
129 $form['info'][$revision->vid] = array(
130 '#markup' => t('!date by !username', array(
131 '!date' => l(format_date($revision->timestamp, 'small'), "node/$node->nid/revisions/$revision->vid/view"),
132 '!username' => theme('username', $revision)))
133 . (($revision->log != '') ? '<p class="revision-log">'. filter_xss($revision->log) .'</p>' : '')
134 );
135 if ($revert_permission) {
136 $operations[] = array('#markup' => l(t('revert'), "node/$node->nid/revisions/$revision->vid/revert"));
137 }
138 if ($delete_permission) {
139 $operations[] = array('#markup' => l(t('delete'), "node/$node->nid/revisions/$revision->vid/delete"));
140 }
141 // Set a dummy, even if the user has no permission for the other
142 // operations, so that we can check if the operations array is
143 // empty to know if the row denotes the current revision.
144 $operations[] = array();
145 }
146 $form['operations'][$revision->vid] = $operations;
147
148 }
149 $new_vid = key($revision_ids);
150 next($revision_ids);
151 $old_vid = key($revision_ids);
152 $form['diff']['old'] = array(
153 '#type' => 'radios',
154 '#options' => $revision_ids,
155 '#default_value' => $old_vid
156 );
157 $form['diff']['new'] = array(
158 '#type' => 'radios',
159 '#options' => $revision_ids,
160 '#default_value' => $new_vid
161 );
162 $form['submit'] = array('#type' => 'submit', '#value' => t('Show diff'));
163
164 if (count($revision_list) > REVISION_LIST_SIZE) {
165 $form['#suffix'] = theme('pager', NULL, REVISION_LIST_SIZE, 0);
166 }
167
168 return $form;
169 }
170
171 /**
172 * Theme function to display the revisions table with means to select
173 * two revisions.
174 */
175 function theme_diff_node_revisions($form) {
176 $output = '';
177
178 // Overview table:
179 $header = array(
180 t('Revision'),
181 array('data' => drupal_render($form['submit']), 'colspan' => 2),
182 array('data' => t('Operations'), 'colspan' => 2)
183 );
184 if (isset($form['info']) && is_array($form['info'])) {
185 foreach (element_children($form['info']) as $key) {
186 $row = array();
187 if (isset($form['operations'][$key][0])) {
188 // Note: even if the commands for revert and delete are not permitted,
189 // the array is not empty since we set a dummy in this case.
190 $row[] = drupal_render($form['info'][$key]);
191 $row[] = drupal_render($form['diff']['old'][$key]);
192 $row[] = drupal_render($form['diff']['new'][$key]);
193 $row[] = drupal_render($form['operations'][$key][0]);
194 $row[] = drupal_render($form['operations'][$key][1]);
195 $rows[] = $row;
196 }
197 else {
198 // its the current revision (no commands to revert or delete)
199 $row[] = array('data' => drupal_render($form['info'][$key]), 'class' => 'revision-current');
200 $row[] = array('data' => drupal_render($form['diff']['old'][$key]), 'class' => 'revision-current');
201 $row[] = array('data' => drupal_render($form['diff']['new'][$key]), 'class' => 'revision-current');
202 $row[] = array('data' => theme('placeholder', t('current revision')), 'class' => 'revision-current', 'colspan' => '2');
203 $rows[] = array(
204 'data' => $row,
205 'class' => 'error',
206 );
207 }
208 }
209 }
210 $output .= theme('table', $header, $rows);
211 $output .= drupal_render($form);
212 return $output;
213 }
214
215 /**
216 * Submit handler which redirects to the selected diff page.
217 */
218 function diff_node_revisions_submit($form, &$form_state) {
219 // The ids are ordered so the old revision is always on the left.
220 $old_vid = min($form_state['values']['old'], $form_state['values']['new']);
221 $new_vid = max($form_state['values']['old'], $form_state['values']['new']);
222 $form_state['redirect'] = 'node/'. $form_state['values']['nid'] .'/revisions/view/'. $old_vid .'/'. $new_vid;
223 }
224
225 /**
226 * Validation for input form to select two revisions.
227 */
228 function diff_node_revisions_validate($form, &$form_state) {
229 $old_vid = $form_state['values']['old'];
230 $new_vid = $form_state['values']['new'];
231 if ($old_vid==$new_vid || !$old_vid || !$new_vid) {
232 form_set_error('diff', t('Select different revisions to compare.'));
233 }
234 }
235
236 /**
237 * Menu callback. Show differences between two node revisions.
238 *
239 * @param $node
240 * Node on which to perform comparison
241 * @param $old_vid
242 * Version ID of the old revision.
243 * @param $new_vid
244 * Version ID of the new revision.
245 */
246 function diff_diffs_show(&$node, $old_vid, $new_vid) {
247
248 // Set same title as on the 'Revisions' tab for consistency
249 drupal_set_title(t('Revisions for %title', array('%title' => $node->title)));
250
251 $node_revisions = node_revision_list($node);
252
253 $old_node = node_load($node->nid, $old_vid);
254 $new_node = node_load($node->nid, $new_vid);
255
256 // Generate table header (date, username, logmessage).
257 $old_header = t('!date by !username', array(
258 '!date' => l(format_date($old_node->revision_timestamp), "node/$node->nid/revisions/$old_node->vid/view"),
259 '!username' => theme('username', $node_revisions[$old_vid]),
260 ));
261 $new_header = t('!date by !username', array(
262 '!date' => l(format_date($new_node->revision_timestamp), "node/$node->nid/revisions/$new_node->vid/view"),
263 '!username' => theme('username', $node_revisions[$new_vid]),
264 ));
265
266 $old_log = $old_node->log != '' ? '<p class="revision-log">'. filter_xss($old_node->log) .'</p>' : '';
267 $new_log = $new_node->log != '' ? '<p class="revision-log">'. filter_xss($new_node->log) .'</p>' : '';
268
269 // Generate previous diff/next diff links.
270 $next_vid = _diff_get_next_vid($node_revisions, $new_vid);
271 if ($next_vid) {
272 $next_link = l(t('next diff >'), 'node/'. $node->nid .'/revisions/view/'. $new_vid .'/'. $next_vid);
273 }
274 else {
275 $next_link = '';
276 }
277 $prev_vid = _diff_get_previous_vid($node_revisions, $old_vid);
278 if ($prev_vid) {
279 $prev_link = l(t('< previous diff'), 'node/'. $node->nid .'/revisions/view/'. $prev_vid .'/'. $old_vid);
280 }
281 else {
282 $prev_link = '';
283 }
284
285 $cols = _diff_default_cols();
286 $header = _diff_default_header($old_header, $new_header);
287 $rows = array();
288 if ($old_log || $new_log) {
289 $rows[] = array(
290 array(
291 'data' => $old_log,
292 'colspan' => 2
293 ),
294 array(
295 'data' => $new_log,
296 'colspan' => 2
297 )
298 );
299 }
300 $rows[] = array(
301 array(
302 'data' => $prev_link,
303 'class' => 'diff-prevlink',
304 'colspan' => 2
305 ),
306 array(
307 'data' => $next_link,
308 'class' => 'diff-nextlink',
309 'colspan' => 2
310 )
311 );
312 $rows = array_merge($rows, _diff_body_rows($old_node, $new_node));
313 $output = theme('table', $header, $rows, array('class' => 'diff'), NULL, $cols);
314
315 if ($node->vid == $new_vid) {
316 $output .= '<div class="diff-section-title">'. t('Current revision:') .'</div>';
317 }
318 else {
319 $output .= '<div class="diff-section-title">'. t('Revision of !new_date:', array('!new_date' => format_date($new_node->revision_timestamp))) .'</div>';
320 }
321 // Don't include node links (final argument) when viewing the diff.
322 $output .= node_view($new_node, FALSE, FALSE, FALSE);
323 return $output;
324 }
325
326 /**
327 * Creates an array of rows which represent a diff between $old_node and $new_node.
328 *
329 * @param $old_node
330 * Node for comparison which will be displayed on the left side.
331 * @param $new_node
332 * Node for comparison which will be displayed on the right side.
333 */
334 function _diff_body_rows(&$old_node, &$new_node) {
335 drupal_add_css(drupal_get_path('module', 'diff') .'/diff.css', 'module', 'all', FALSE);
336 module_load_include('inc', 'diff', 'diff.engine');
337 include_once('node.inc');
338 if (module_exists('taxonomy')) {
339 module_load_include('inc', 'diff', 'taxonomy');
340 }
341 if (module_exists('upload')) {
342 module_load_include('inc', 'diff', 'upload');
343 }
344
345 $rows = array();
346 $any_visible_change = FALSE;
347 $node_diffs = module_invoke_all('diff', $old_node, $new_node);
348
349 // We start off assuming all form elements are in the correct order.
350 $node_diffs['#sorted'] = TRUE;
351
352 // Recurse through all child elements.
353 $count = 0;
354 foreach (element_children($node_diffs) as $key) {
355 // Assign a decimal placeholder weight to preserve original array order.
356 if (!isset($node_diffs[$key]['#weight'])) {
357 $node_diffs[$key]['#weight'] = $count/1000;
358 }
359 else {
360 // If one of the child elements has a weight then we will need to sort
361 // later.
362 unset($node_diffs['#sorted']);
363 }
364 $count++;
365 }
366
367 // One of the children has a #weight.
368 if (!isset($node_diffs['#sorted'])) {
369 uasort($node_diffs, "element_sort");
370 }
371
372 foreach ($node_diffs as $node_diff) {
373 $diff = new Diff($node_diff['#old'], $node_diff['#new']);
374 $formatter = new DrupalDiffFormatter();
375 if (isset($node_diff['#format'])) {
376 $formatter->show_header = $node_diff['#format']['show_header'];
377 }
378 $diff_rows = $formatter->format($diff);
379 if (count($diff_rows)) {
380 $rows[] = array(
381 array(
382 'data' => t('Changes to %name', array('%name' => $node_diff['#name'])),
383 'class' => 'diff-section-title',
384 'colspan' => 4
385 )
386 );
387 $rows = array_merge($rows, $diff_rows);
388 $any_visible_change = TRUE;
389 }
390 }
391 if (!$any_visible_change) {
392 $rows[] = array(
393 array(
394 'data' => t('No visible changes'),
395 'class' => 'diff-section-title',
396 'colspan' => 4
397 ),
398 );
399 // Needed to keep Safari happy.
400 $rows[] = array(
401 array('data' => ''),
402 array('data' => ''),
403 array('data' => ''),
404 array('data' => ''),
405 );
406 }
407 return $rows;
408 }
409
410 /**
411 * Get the entry in the revisions list after $vid.
412 * Returns FALSE if $vid is the last entry.
413 *
414 * @param $node_revisions
415 * Array of node revision IDs in descending order.
416 * @param $vid
417 * Version ID to look for.
418 */
419 function _diff_get_next_vid(&$node_revisions, $vid) {
420 $previous = NULL;
421 foreach ($node_revisions as $revision) {
422 if ($revision->vid == $vid) {
423 return ($previous ? $previous->vid : FALSE);
424 }
425 $previous = $revision;
426 }
427 return FALSE;
428 }
429
430 /**
431 * Get the entry in the revision list before $vid.
432 * Returns FALSE if $vid is the first entry.
433 *
434 * @param $node_revisions
435 * Array of node revision IDs in descending order.
436 * @param $vid
437 * Version ID to look for.
438 */
439 function _diff_get_previous_vid(&$node_revisions, $vid) {
440 $previous = NULL;
441 foreach ($node_revisions as $revision) {
442 if ($previous && $previous->vid == $vid) {
443 return $revision->vid;
444 }
445 $previous = $revision;
446 }
447 return FALSE;
448 }
449
450 /**
451 * Implementation of hook_form_alter().
452 */
453 function diff_form_alter(&$form, $form_state, $form_id) {
454 if (isset($form['type']['#value']) && $form['type']['#value'] .'_node_form' == $form_id) {
455 // Add a 'Preview changes' button on the node edit form.
456 if (variable_get('show_preview_changes_'. $form['type']['#value'], TRUE) && $form['nid']['#value'] > 0) {
457 $form['buttons']['preview_changes'] = array(
458 '#type' => 'submit',
459 '#value' => t('Preview changes'),
460 '#weight' => 12,
461 '#submit' => array('diff_node_form_build_preview_changes')
462 );
463 }
464 }
465 elseif ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
466 // Node type edit form.
467 // Add checkbox to activate 'Preview changes' button per node type.
468 $form['workflow']['show_preview_changes'] = array(
469 '#type' => 'checkbox',
470 '#title' => t('Show %preview_changes button on node edit form', array('%preview_changes' => t('Preview changes'))),
471 '#prefix' => '<strong>'. t('Preview changes') .'</strong>',
472 '#weight' => 10,
473 '#default_value' => variable_get('show_preview_changes_'. $form['#node_type']->type, TRUE),
474 );
475 }
476 }
477
478 /**
479 * Submit handler for 'Preview changes' button.
480 */
481 function diff_node_form_build_preview_changes($form, &$form_state) {
482 $node = node_form_submit_build_node($form, $form_state);
483
484 // Create diff of old node and edited node
485 $rows = _diff_body_rows(node_load($form_state['values']['nid']), $node);
486 $cols = _diff_default_cols();
487 $header = _diff_default_header();
488 $changes = theme('diff_table', $header, $rows, array('class' => 'diff'), NULL, $cols);
489 // Prepend diff to edit form
490 $form_state['node_preview'] = isset($form_state['node_preview']) ? $changes . $form_state['node_preview'] : $changes;
491 }
492
493 /**
494 * Theme function for a header line in the diff.
495 */
496 function theme_diff_header_line($lineno) {
497 return '<strong>'. t('Line %lineno', array('%lineno' => $lineno)) .'</strong>';
498 }
499
500 /**
501 * Theme function for a content line in the diff.
502 */
503 function theme_diff_content_line($line) {
504 return '<div>'. $line .'</div>';
505 }
506
507 /**
508 * Theme function for an empty line in the diff.
509 */
510 function theme_diff_empty_line($line) {
511 return $line;
512 }
513
514 /**
515 * Implementation of hook_theme().
516 */
517 function diff_theme() {
518 return array(
519 'diff_node_revisions' => array(
520 'arguments' => array('form' => NULL),
521 ),
522 'diff_header_line' => array(
523 'arguments' => array('lineno' => NULL),
524 ),
525 'diff_content_line' => array(
526 'arguments' => array('line' => NULL),
527 ),
528 'diff_empty_line' => array(
529 'arguments' => array('line' => NULL),
530 ),
531 );
532 }
533
534 /**
535 * Helper function to create default 'cols' array for diff table.
536 */
537 function _diff_default_cols() {
538 return array(
539 array(
540 array(
541 'class' => 'diff-marker',
542 ),
543 array(
544 'class' => 'diff-content',
545 ),
546 array(
547 'class' => 'diff-marker',
548 ),
549 array(
550 'class' => 'diff-content',
551 ),
552 ),
553 );
554 }
555
556 /**
557 * Helper function to create default 'header' array for diff table.
558 */
559 function _diff_default_header($old_header = '', $new_header = '') {
560 return array(
561 array(
562 'data' => $old_header,
563 'colspan' => 2
564 ),
565 array(
566 'data' => $new_header,
567 'colspan' => 2
568 )
569 );
570 }

  ViewVC Help
Powered by ViewVC 1.1.2