Ticket #6775: 6775.diff

File 6775.diff, 91.2 KB (added by mdawaffe, 5 years ago)
Line 
1Index: wp-includes/default-filters.php
2===================================================================
3--- wp-includes/default-filters.php     (revision 7746)
4+++ wp-includes/default-filters.php     (working copy)
5@@ -178,6 +178,7 @@
6 add_action('init', 'smilies_init', 5);
7 add_action( 'plugins_loaded', 'wp_maybe_load_widgets', 0 );
8 add_action( 'shutdown', 'wp_ob_end_flush_all', 1);
9+add_action( 'pre_post_update', 'wp_save_revision' );
10 add_action('publish_post', '_publish_post_hook', 5, 1);
11 add_action('future_post', '_future_post_hook', 5, 2);
12 add_action('future_page', '_future_post_hook', 5, 2);
13Index: wp-includes/post-template.php
14===================================================================
15--- wp-includes/post-template.php       (revision 7746)
16+++ wp-includes/post-template.php       (working copy)
17@@ -564,4 +564,136 @@
18        return false;
19 }
20 
21-?>
22+/**
23+ * wp_post_revision_time() - returns formatted datetimestamp of a revision
24+ *
25+ * @package WordPress
26+ * @subpackage Post Revisions
27+ * @since 2.6
28+ *
29+ * @uses wp_get_revision()
30+ * @uses date_i18n()
31+ *
32+ * @param int|object $revision revision ID or revision object
33+ * @return string i18n formatted datetimestamp or localized 'Corrent Revision'
34+ */
35+function wp_post_revision_time( $revision ) {
36+       if ( !$revision = wp_get_revision( $revision ) ) {
37+               if ( $revision = get_post( $revision ) )
38+                       return __( 'Current Revision' );
39+               return $revision;
40+       }
41+
42+       $datef  = _c( 'j F, Y @ G:i|revision date format');
43+       return date_i18n( $datef, strtotime( $revision->post_date_gmt . ' +0000' ) );
44+}
45+
46+/**
47+ * wp_list_post_revisions() - echoes list of a post's revisions
48+ *
49+ * Can output either a UL with edit links or a TABLE with diff interface, and restore action links
50+ *
51+ * Second argument controls parameters:
52+ *   (bool)   parent : include the parent (the "Current Revision") in the list
53+ *   (string) format : 'list' or 'form-table'.  'list' outputs UL, 'form-table' outputs TABLE with UI
54+ *   (int)    right  : what revision is currently being viewed - used in form-table format
55+ *   (int)    left   : what revision is currently being diffed against right - used in form-table format
56+ *
57+ * @package WordPress
58+ * @subpackage Post Revisions
59+ * @since 2.6
60+ *
61+ * @uses wp_get_post_revisions()
62+ * @uses wp_post_revision_time()
63+ * @uses get_edit_post_link()
64+ * @uses get_author_name()
65+ *
66+ * @param int|object $post_id post ID or post object
67+ * @param string|array $args see description @see wp_parse_args()
68+ */
69+function wp_list_post_revisions( $post_id = 0, $args = null ) { // TODO? split into two functions (list, form-table) ?
70+       if ( !$post = get_post( $post_id ) )
71+               return;
72+
73+       if ( !$revisions = wp_get_post_revisions( $post->ID ) )
74+               return;
75+
76+       $defaults = array( 'parent' => false, 'right' => false, 'left' => false, 'format' => 'list' );
77+       extract( wp_parse_args( $args, $defaults ), EXTR_SKIP );
78+
79+       $titlef = _c( '%1$s by %2$s|post revision 1:datetime, 2:name' );
80+
81+       if ( $parent )
82+               array_unshift( $revisions, $post );
83+
84+       $rows = '';
85+       $class = false;
86+       foreach ( $revisions as $revision ) {
87+               $date = wp_post_revision_time( $revision );
88+               if ( $link = get_edit_post_link( $revision->ID ) )
89+                       $date = "<a href='$link'>$date</a>";
90+               $name = get_author_name( $revision->post_author );
91+
92+               if ( 'form-table' == $format ) {
93+                       if ( $left )
94+                               $old_checked = $left == $revision->ID ? ' checked="checked"' : '';
95+                       else
96+                               $old_checked = $new_checked ? ' checked="checked"' : '';
97+                       $new_checked = $right == $revision->ID ? ' checked="checked"' : '';
98+
99+                       $class = $class ? '' : " class='alternate'";
100+
101+                       if ( $post->ID != $revision->ID && current_user_can( 'edit_post', $post->ID ) )
102+                               $actions = '<a href="' . wp_nonce_url( add_query_arg( array( 'revision' => $revision->ID, 'diff' => false, 'restore' => 'restore' ) ), "restore-post_$post->ID|$revision->ID" ) . '">' . __( 'Restore' ) . '</a>';
103+                       else
104+                               $actions = '';
105+
106+                       $rows .= "<tr$class>\n";
107+                       $rows .= "\t<th style='white-space: nowrap' scope='row'><input type='radio' name='diff' value='$revision->ID'$old_checked /><input type='radio' name='revision' value='$revision->ID'$new_checked />\n";
108+                       $rows .= "\t<td>$date</td>\n";
109+                       $rows .= "\t<td>$name</td>\n";
110+                       $rows .= "\t<td class='action-links'>$actions</td>\n";
111+                       $rows .= "</tr>\n";
112+               } else {
113+                       $rows .= "\t<li>" . sprintf( $titlef, $date, $name ). "</li>\n";
114+               }
115+       }
116+
117+       if ( 'form-table' == $format ) : ?>
118+
119+<form action="revision.php" method="get">
120+
121+<div class="tablenav">
122+       <div class="alignleft">
123+               <input type="submit" class="button-secondary" value="<?php _e( 'Compare Revisions' ); ?>" />
124+       </div>
125+</div>
126+
127+<br class="clear" />
128+
129+<table class="widefat post-revisions">
130+       <col />
131+       <col style="width: 33%" />
132+       <col style="width: 33%" />
133+       <col style="width: 33%" />
134+<thead>
135+       <th scope="col"></th>
136+       <th scope="col"><?php _e( 'Date Created' ); ?></th>
137+       <th scope="col"><?php _e( 'Author' ); ?></th>
138+       <th scope="col" class="action-links"><?php _e( 'Actions' ); ?></th>
139+</thead>
140+<tbody>
141+
142+<?php echo $rows; ?>
143+
144+</tbody>
145+</table>
146+
147+<?php
148+       else :
149+               echo "<ul class='post-revisions'>\n";
150+               echo $rows;
151+               echo "</ul>";
152+       endif;
153+
154+}
155Index: wp-includes/post.php
156===================================================================
157--- wp-includes/post.php        (revision 7746)
158+++ wp-includes/post.php        (working copy)
159@@ -2990,4 +2990,232 @@
160        }
161 }
162 
163+/* Post Revisions */
164+
165+/**
166+ * _wp_revision_fields() - determines which fields of posts are to be saved in revisions
167+ *
168+ * Does two things. If passed a postn *array*, it will return a post array ready to be
169+ * insterted into the posts table as a post revision.
170+ * Otherwise, returns an array whose keys are the post fields to be saved post revisions.
171+ *
172+ * @package WordPress
173+ * @subpackage Post Revisions
174+ * @since 2.6
175+ *
176+ * @param array $post optional a post array to be processed for insertion as a post revision
177+ * @return array post array ready to be inserted as a post revision or array of fields that can be versioned
178+ */
179+function _wp_revision_fields( $post = null ) {
180+       static $fields = false;
181+
182+       if ( !$fields ) {
183+               // Allow these to be versioned
184+               $fields = array(
185+                       'post_title' => __( 'Title' ),
186+                       'post_author' => __( 'Author' ),
187+                       'post_content' => __( 'Content' ),
188+                       'post_excerpt' => __( 'Excerpt' ),
189+               );
190+
191+               // WP uses these internally either in versioning or elsewhere - they cannot be versioned
192+               foreach ( array( 'ID', 'post_name', 'post_parent', 'post_date', 'post_date_gmt', 'post_status', 'post_type', 'comment_count' ) as $protect )
193+                       unset( $fields[$protect] );
194+       }
195+
196+       if ( !is_array($post) )
197+               return $fields;
198+
199+       $return = array();
200+       foreach ( array_intersect( array_keys( $post ), array_keys( $fields ) ) as $field )
201+               $return[$field] = $post[$field];
202+
203+       $return['post_parent']   = $post['ID'];
204+       $return['post_status']   = 'inherit';
205+       $return['post_type']     = 'revision';
206+       $return['post_name']     = "$post[ID]-revision";
207+       $return['post_date']     = $post['post_modified'];
208+       $return['post_date_gmt'] = $post['post_modified_gmt'];
209+
210+       return $return;
211+}
212+
213+/**
214+ * wp_save_revision() - Saves an already existing post as a post revision.  Typically used immediately prior to post updates.
215+ *
216+ * @package WordPress
217+ * @subpackage Post Revisions
218+ * @since 2.6
219+ *
220+ * @uses _wp_put_revision()
221+ *
222+ * @param int $post_id The ID of the post to save as a revision
223+ * @return mixed null or 0 if error, new revision ID if success
224+ */
225+function wp_save_revision( $post_id ) {
226+       // TODO: rework autosave to use special type of post revision
227+       if ( @constant( 'DOING_AUTOSAVE' ) )
228+               return;
229+
230+       if ( !$post = get_post( $post_id, ARRAY_A ) )
231+               return;
232+
233+       // TODO: open this up for pages also
234+       if ( 'post' != $post->post_type )
235+               retun;
236+
237+       return _wp_put_revision( $post );
238+}
239+
240+/**
241+ * _wp_put_revision() - Inserts post data into the posts table as a post revision
242+ *
243+ * @package WordPress
244+ * @subpackage Post Revisions
245+ * @since 2.6
246+ *
247+ * @uses wp_insert_post()
248+ *
249+ * @param int|object|array $post post ID, post object OR post array
250+ * @return mixed null or 0 if error, new revision ID if success
251+ */
252+function _wp_put_revision( $post = null ) {
253+       if ( is_object($post) )
254+               $post = get_object_vars( $post );
255+       elseif ( !is_array($post) )
256+               $post = get_post($post, ARRAY_A);
257+
258+       if ( !$post || empty($post['ID']) )
259+               return;
260+
261+       if ( isset($post['post_type']) && 'revision' == $post_post['type'] )
262+               return new WP_Error( 'post_type', __( 'Cannot create a revision of a revision' ) );
263+
264+       $post = _wp_revision_fields( $post );
265+
266+       if ( $revision_id = wp_insert_post( $post ) )
267+               do_action( '_wp_put_revision', $revision_id );
268+
269+       return $revision_id;
270+}
271+
272+/**
273+ * wp_get_revision() - Gets a post revision
274+ *
275+ * @package WordPress
276+ * @subpackage Post Revisions
277+ * @since 2.6
278+ *
279+ * @uses get_post()
280+ *
281+ * @param int|object $post post ID or post object
282+ * @param $output optional OBJECT, ARRAY_A, or ARRAY_N
283+ * @param string $filter optional sanitation filter.  @see sanitize_post()
284+ * @return mixed null if error or post object if success
285+ */
286+function &wp_get_revision(&$post, $output = OBJECT, $filter = 'raw') {
287+       $null = null;
288+       if ( !$revision = get_post( $post, OBJECT, $filter ) )
289+               return $revision;
290+       if ( 'revision' !== $revision->post_type )
291+               return $null;
292+
293+       if ( $output == OBJECT ) {
294+               return $revision;
295+       } elseif ( $output == ARRAY_A ) {
296+               $_revision = get_object_vars($revision);
297+               return $_revision;
298+       } elseif ( $output == ARRAY_N ) {
299+               $_revision = array_values(get_object_vars($revision));
300+               return $_revision;
301+       }
302+
303+       return $revision;
304+}
305+
306+/**
307+ * wp_restore_revision() - Restores a post to the specified revision
308+ *
309+ * Can restore a past using all fields of the post revision, or only selected fields.
310+ *
311+ * @package WordPress
312+ * @subpackage Post Revisions
313+ * @since 2.6
314+ *
315+ * @uses wp_get_revision()
316+ * @uses wp_update_post()
317+ *
318+ * @param int|object $revision_id revision ID or revision object
319+ * @param array $fields optional What fields to restore from.  Defaults to all.
320+ * @return mixed null if error, false if no fields to restore, (int) post ID if success
321+ */
322+function wp_restore_revision( $revision_id, $fields = null ) {
323+       if ( !$revision = wp_get_revision( $revision_id, ARRAY_A ) )
324+               return $revision;
325+
326+       if ( !is_array( $fields ) )
327+               $fields = array_keys( _wp_revision_fields() );
328+
329+       $update = array();
330+       foreach( array_intersect( array_keys( $revision ), $fields ) as $field )
331+               $update[$field] = $revision[$field];
332+
333+       if ( !$update )
334+               return false;
335+
336+       $update['ID'] = $revision['post_parent'];
337+
338+       if ( $post_id = wp_update_post( $update ) )
339+               do_action( 'wp_restore_revision', $post_id, $revision['ID'] );
340+
341+       return $post_id;
342+}
343+
344+/**
345+ * wp_delete_revision() - Deletes a revision.
346+ *
347+ * Deletes the row from the posts table corresponding to the specified revision
348+ *
349+ * @package WordPress
350+ * @subpackage Post Revisions
351+ * @since 2.6
352+ *
353+ * @uses wp_get_revision()
354+ * @uses wp_delete_post()
355+ *
356+ * @param int|object $revision_id revision ID or revision object
357+ * @param array $fields optional What fields to restore from.  Defaults to all.
358+ * @return mixed null if error, false if no fields to restore, (int) post ID if success
359+ */
360+function wp_delete_revision( $revision_id ) {
361+       if ( !$revision = wp_get_revision( $revision_id ) )
362+               return $revision;
363+
364+       if ( $delete = wp_delete_post( $revision->ID ) )
365+               do_action( 'wp_delete_revision', $revision->ID, $revision );
366+
367+       return $delete;
368+}
369+
370+/**
371+ * wp_get_post_revisions() - Returns all revisions of specified post
372+ *
373+ * @package WordPress
374+ * @subpackage Post Revisions
375+ * @since 2.6
376+ *
377+ * @uses get_children()
378+ *
379+ * @param int|object $post_id post ID or post object
380+ * @return array empty if no revisions
381+ */
382+function wp_get_post_revisions( $post_id = 0 ) {
383+       if ( ( !$post = get_post( $post_id ) ) || empty( $post->ID ) )
384+               return array();
385+
386+       if ( !$revisions = get_children( array( 'post_parent' => $post->ID, 'post_type' => 'revision' ) ) )
387+               return array();
388+       return $revisions;
389+}
390+
391 ?>
392Index: wp-includes/wp-diff.php
393===================================================================
394--- wp-includes/wp-diff.php     (revision 0)
395+++ wp-includes/wp-diff.php     (revision 0)
396@@ -0,0 +1,318 @@
397+<?php
398+
399+if ( !class_exists( 'Text_Diff' ) ) {
400+       require( 'Text/Diff.php' );
401+       require( 'Text/Diff/Renderer.php' );
402+       require( 'Text/Diff/Renderer/inline.php' );
403+}
404+
405+
406+/* Descendent of a bastard child of piece of an old MediaWiki Diff Formatter
407+ *
408+ * Basically all that remains is the table structure and some method names.
409+ */
410+
411+class WP_Text_Diff_Renderer_Table extends Text_Diff_Renderer {
412+       var $_leading_context_lines  = 10000;
413+       var $_trailing_context_lines = 10000;
414+       var $_diff_threshold = 0.6;
415+
416+       var $inline_diff_renderer = 'WP_Text_Diff_Renderer_inline';
417+
418+       function Text_Diff_Renderer_Table( $params = array() ) {
419+               $parent = get_parent_class($this);
420+               $this->$parent( $params );
421+       }
422+
423+       function _startBlock( $header ) {
424+               return '';
425+       }
426+
427+       function _lines( $lines, $prefix=' ' ) {
428+       }
429+
430+       // HTML-escape parameter before calling this
431+       function addedLine( $line ) {
432+               return "<td>+</td><td class='diff-addedline'>{$line}</td>";
433+       }
434+
435+       // HTML-escape parameter before calling this
436+       function deletedLine( $line ) {
437+               return "<td>-</td><td class='diff-deletedline'>{$line}</td>";
438+       }
439+
440+       // HTML-escape parameter before calling this
441+       function contextLine( $line ) {
442+               return "<td> </td><td class='diff-context'>{$line}</td>";
443+       }
444+
445+       function emptyLine() {
446+               return '<td colspan="2">&nbsp;</td>';
447+       }
448+
449+       function _added( $lines, $encode = true ) {
450+               $r = '';
451+               foreach ($lines as $line) {
452+                       if ( $encode )
453+                               $line = htmlspecialchars( $line );
454+                       $r .= '<tr>' . $this->emptyLine() . $this->addedLine( $line ) . "</tr>\n";
455+               }
456+               return $r;
457+       }
458+
459+       function _deleted( $lines, $encode = true ) {
460+               $r = '';
461+               foreach ($lines as $line) {
462+                       if ( $encode )
463+                               $line = htmlspecialchars( $line );
464+                       $r .= '<tr>' . $this->deletedLine( $line ) . $this->emptyLine() . "</tr>\n";
465+               }
466+               return $r;
467+       }
468+
469+       function _context( $lines, $encode = true ) {
470+               $r = '';
471+               foreach ($lines as $line) {
472+                       if ( $encode )
473+                               $line = htmlspecialchars( $line );
474+                       $r .= '<tr>' .
475+                               $this->contextLine( $line ) . $this->contextLine( $line ) . "</tr>\n";
476+               }
477+               return $r;
478+       }
479+
480+       // Process changed lines to do word-by-word diffs for extra highlighting (TRAC style)
481+       // sometimes these lines can actually be deleted or added rows - we do additional processing
482+       // to figure that out
483+       function _changed( $orig, $final ) {
484+               $r = '';
485+
486+               // Does the aforementioned additional processing
487+               // *_matches tell what rows are "the same" in orig and final.  Those pairs will be diffed to get word changes
488+               //      match is numeric: an index in other column
489+               //      match is 'X': no match.  It is a new row
490+               // *_rows are column vectors for the orig column and the final column.
491+               //      row >= 0: an indix of the $orig or $final array
492+               //      row  < 0: a blank row for that column
493+               list($orig_matches, $final_matches, $orig_rows, $final_rows) = $this->interleave_changed_lines( $orig, $final );
494+
495+
496+               // These will hold the word changes as determined by an inline diff
497+               $orig_diffs  = array();
498+               $final_diffs = array();
499+
500+               // Compute word diffs for each matched pair using the inline diff
501+               foreach ( $orig_matches as $o => $f ) {
502+                       if ( is_numeric($o) && is_numeric($f) ) {
503+                               $text_diff = new Text_Diff( 'auto', array( array($orig[$o]), array($final[$f]) ) );
504+                               $renderer = new $this->inline_diff_renderer;
505+                               $diff = $renderer->render( $text_diff );
506+
507+                               // If they're too different, don't include any <ins> or <dels>
508+                               if ( $diff_count = preg_match_all( '!(<ins>.*?</ins>|<del>.*?</del>)!', $diff, $diff_matches ) ) {
509+                                       // length of all text between <ins> or <del>
510+                                       $stripped_matches = strlen(strip_tags( join(' ', $diff_matches[0]) ));
511+                                       // since we count lengith of text between <ins> or <del> (instead of picking just one),
512+                                       //      we double the length of chars not in those tags.
513+                                       $stripped_diff = strlen(strip_tags( $diff )) * 2 - $stripped_matches;
514+                                       $diff_ratio = $stripped_matches / $stripped_diff;
515+                                       if ( $diff_ratio > $this->_diff_threshold )
516+                                               continue; // Too different.  Don't save diffs.
517+                               }
518+
519+                               // Un-inline the diffs by removing del or ins
520+                               $orig_diffs[$o]  = preg_replace( '|<ins>.*?</ins>|', '', $diff );
521+                               $final_diffs[$f] = preg_replace( '|<del>.*?</del>|', '', $diff );
522+                       }
523+               }
524+
525+               foreach ( array_keys($orig_rows) as $row ) {
526+                       // Both columns have blanks.  Ignore them.
527+                       if ( $orig_rows[$row] < 0 && $final_rows[$row] < 0 )
528+                               continue;
529+
530+                       // If we have a word based diff, use it.  Otherwise, use the normal line.
531+                       $orig_line  = isset($orig_diffs[$orig_rows[$row]])
532+                               ? $orig_diffs[$orig_rows[$row]]
533+                               : htmlspecialchars($orig[$orig_rows[$row]]);
534+                       $final_line = isset($final_diffs[$final_rows[$row]])
535+                               ? $final_diffs[$final_rows[$row]]
536+                               : htmlspecialchars($final[$final_rows[$row]]);
537+
538+                       if ( $orig_rows[$row] < 0 ) { // Orig is blank.  This is really an added row.
539+                               $r .= $this->_added( array($final_line), false );
540+                       } elseif ( $final_rows[$row] < 0 ) { // Final is blank.  This is really a deleted row.
541+                               $r .= $this->_deleted( array($orig_line), false );
542+                       } else { // A true changed row.
543+                               $r .= '<tr>' . $this->deletedLine( $orig_line ) . $this->addedLine( $final_line ) . "</tr>\n";
544+                       }
545+               }
546+
547+               return $r;
548+       }
549+
550+       // Takes changed blocks and matches which rows in orig turned into which rows in final.
551+       // Returns
552+       //      *_matches ( which rows match with which )
553+       //      *_rows ( order of rows in each column interleaved with blank rows as necessary )
554+       function interleave_changed_lines( $orig, $final ) {
555+
556+               // Contains all pairwise string comparisons.  Keys are such that this need only be a one dimensional array.
557+               $matches = array();
558+               foreach ( array_keys($orig) as $o ) {
559+                       foreach ( array_keys($final) as $f ) {
560+                               $matches["$o,$f"] = $this->compute_string_distance( $orig[$o], $final[$f] );
561+                       }
562+               }
563+               asort($matches); // Order by string distance.
564+
565+               $orig_matches  = array();
566+               $final_matches = array();
567+
568+               foreach ( $matches as $keys => $difference ) {
569+                       list($o, $f) = explode(',', $keys);
570+                       $o = (int) $o;
571+                       $f = (int) $f;
572+
573+                       // Already have better matches for these guys
574+                       if ( isset($orig_matches[$o]) && isset($final_matches[$f]) )
575+                               continue;
576+
577+                       // First match for these guys.  Must be best match
578+                       if ( !isset($orig_matches[$o]) && !isset($final_matches[$f]) ) {
579+                               $orig_matches[$o] = $f;
580+                               $final_matches[$f] = $o;
581+                               continue;
582+                       }
583+
584+                       // Best match of this final is already taken?  Must mean this final is a new row.
585+                       if ( isset($orig_matches[$o]) )
586+                               $final_matches[$f] = 'x';
587+
588+                       // Best match of this orig is already taken?  Must mean this orig is a deleted row.
589+                       elseif ( isset($final_matches[$f]) )
590+                               $orig_matches[$o] = 'x';
591+               }
592+
593+               // We read the text in this order
594+               ksort($orig_matches);
595+               ksort($final_matches);
596+
597+
598+               // Stores rows and blanks for each column.
599+               $orig_rows = $orig_rows_copy = array_keys($orig_matches);
600+               $final_rows = array_keys($final_matches);
601+
602+               // Interleaves rows with blanks to keep matches aligned.
603+               // We may end up with some extraneous blank rows, but we'll just ignore them later.
604+               foreach ( $orig_rows_copy as $orig_row ) {
605+                       $final_pos = array_search($orig_matches[$orig_row], $final_rows, true);
606+                       $orig_pos = (int) array_search($orig_row, $orig_rows, true);
607+
608+                       if ( false === $final_pos ) { // This orig is paired with a blank final.
609+                               array_splice( $final_rows, $orig_pos, 0, -1 );
610+                       } elseif ( $final_pos < $orig_pos ) { // This orig's match is up a ways.  Pad final with blank rows.
611+                               $diff_pos = $final_pos - $orig_pos;
612+                               while ( $diff_pos < 0 )
613+                                       array_splice( $final_rows, $orig_pos, 0, $diff_pos++ );
614+                       } elseif ( $final_pos > $orig_pos ) { // This orig's match is down a ways.  Pad orig with blank rows.
615+                               $diff_pos = $orig_pos - $final_pos;
616+                               while ( $diff_pos < 0 )
617+                                       array_splice( $orig_rows, $orig_pos, 0, $diff_pos++ );
618+                       }
619+               }
620+
621+
622+               // Pad the ends with blank rows if the columns aren't the same length
623+               $diff_count = count($orig_rows) - count($final_rows);
624+               if ( $diff_count < 0 ) {
625+                       while ( $diff_count < 0 )
626+                               array_push($orig_rows, $diff_count++);
627+               } elseif ( $diff_count > 0 ) {
628+                       $diff_count = -1 * $diff_count;
629+                       while ( $diff_count < 0 )
630+                               array_push($final_rows, $diff_count++);
631+               }
632+
633+               return array($orig_matches, $final_matches, $orig_rows, $final_rows);
634+
635+/*
636+               // Debug
637+               echo "\n\n\n\n\n";
638+
639+               echo "-- DEBUG Matches: Orig -> Final --";
640+
641+               foreach ( $orig_matches as $o => $f ) {
642+                       echo "\n\n\n\n\n";
643+                       echo "ORIG: $o, FINAL: $f\n";
644+                       var_dump($orig[$o],$final[$f]);
645+               }
646+               echo "\n\n\n\n\n";
647+
648+               echo "-- DEBUG Matches: Final -> Orig --";
649+
650+               foreach ( $final_matches as $f => $o ) {
651+                       echo "\n\n\n\n\n";
652+                       echo "FINAL: $f, ORIG: $o\n";
653+                       var_dump($final[$f],$orig[$o]);
654+               }
655+               echo "\n\n\n\n\n";
656+
657+               echo "-- DEBUG Rows: Orig -- Final --";
658+
659+               echo "\n\n\n\n\n";
660+               foreach ( $orig_rows as $row => $o ) {
661+                       if ( $o < 0 )
662+                               $o = 'X';
663+                       $f = $final_rows[$row];
664+                       if ( $f < 0 )
665+                               $f = 'X';
666+                       echo "$o -- $f\n";
667+               }
668+               echo "\n\n\n\n\n";
669+
670+               echo "-- END DEBUG --";
671+
672+               echo "\n\n\n\n\n";
673+
674+               return array($orig_matches, $final_matches, $orig_rows, $final_rows);
675+*/
676+       }
677+
678+
679+       // Computes a number that is intended to reflect the "distance" between two strings.
680+       function compute_string_distance( $string1, $string2 ) {
681+               // Vectors containing character frequency for all chars in each string
682+               $chars1 = count_chars($string1);
683+               $chars2 = count_chars($string2);
684+
685+               // L1-norm of difference vector.
686+               $difference = array_sum( array_map( array(&$this, 'difference'), $chars1, $chars2 ) );
687+
688+               // $string1 has zero length? Odd.  Give huge penalty by not dividing.
689+               if ( !$string1 )
690+                       return $difference;
691+
692+               // Return distance per charcter (of string1)
693+               return $difference / strlen($string1);
694+       }
695+
696+       function difference( $a, $b ) {
697+               return abs( $a - $b );
698+       }
699+
700+}
701+
702+// Better word splitting than the PEAR package provides
703+class WP_Text_Diff_Renderer_inline extends Text_Diff_Renderer_inline {
704+
705+       function _splitOnWords($string, $newlineEscape = "\n") {
706+               $string = str_replace("\0", '', $string);
707+               $words  = preg_split( '/([^\w])/', $string, -1, PREG_SPLIT_DELIM_CAPTURE );
708+               $words  = str_replace( "\n", $newlineEscape, $words );
709+               return $words;
710+       }
711+
712+}
713+
714+?>
715Index: wp-includes/link-template.php
716===================================================================
717--- wp-includes/link-template.php       (revision 7746)
718+++ wp-includes/link-template.php       (working copy)
719@@ -442,10 +442,15 @@
720        return $link;
721 }
722 
723-function get_edit_post_link( $id = 0 ) {
724+function get_edit_post_link( $id = 0, $context = 'display' ) {
725        if ( !$post = &get_post( $id ) )
726                return;
727 
728+       if ( 'display' == $context )
729+               $action = 'action=edit&amp;';
730+       else
731+               $action = 'action=edit&';
732+
733        switch ( $post->post_type ) :
734        case 'page' :
735                if ( !current_user_can( 'edit_page', $post->ID ) )
736@@ -459,6 +464,13 @@
737                $file = 'media';
738                $var  = 'attachment_id';
739                break;
740+       case 'revision' :
741+               if ( !current_user_can( 'edit_post', $post->ID ) )
742+                       return;
743+               $file = 'revision';
744+               $var  = 'revision';
745+               $action = '';
746+               break;
747        default :
748                if ( !current_user_can( 'edit_post', $post->ID ) )
749                        return;
750@@ -467,7 +479,7 @@
751                break;
752        endswitch;
753       
754-       return apply_filters( 'get_edit_post_link', get_bloginfo( 'wpurl' ) . "/wp-admin/$file.php?action=edit&amp;$var=$post->ID", $post->ID );
755+       return apply_filters( 'get_edit_post_link', get_bloginfo( 'wpurl' ) . "/wp-admin/$file.php?{$action}$var=$post->ID", $post->ID );
756 }
757 
758 function edit_post_link( $link = 'Edit This', $before = '', $after = '' ) {
759Index: wp-includes/Text/Diff/Engine/xdiff.php
760===================================================================
761--- wp-includes/Text/Diff/Engine/xdiff.php      (revision 0)
762+++ wp-includes/Text/Diff/Engine/xdiff.php      (revision 0)
763@@ -0,0 +1,63 @@
764+<?php
765+/**
766+ * Class used internally by Diff to actually compute the diffs.
767+ *
768+ * This class uses the xdiff PECL package (http://pecl.php.net/package/xdiff)
769+ * to compute the differences between the two input arrays.
770+ *
771+ * $Horde: framework/Text_Diff/Diff/Engine/xdiff.php,v 1.6 2008/01/04 10:07:50 jan Exp $
772+ *
773+ * Copyright 2004-2008 The Horde Project (http://www.horde.org/)
774+ *
775+ * See the enclosed file COPYING for license information (LGPL). If you did
776+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
777+ *
778+ * @author  Jon Parise <jon@horde.org>
779+ * @package Text_Diff
780+ */
781+class Text_Diff_Engine_xdiff {
782+
783+    /**
784+     */
785+    function diff($from_lines, $to_lines)
786+    {
787+        array_walk($from_lines, array('Text_Diff', 'trimNewlines'));
788+        array_walk($to_lines, array('Text_Diff', 'trimNewlines'));
789+
790+        /* Convert the two input arrays into strings for xdiff processing. */
791+        $from_string = implode("\n", $from_lines);
792+        $to_string = implode("\n", $to_lines);
793+
794+        /* Diff the two strings and convert the result to an array. */
795+        $diff = xdiff_string_diff($from_string, $to_string, count($to_lines));
796+        $diff = explode("\n", $diff);
797+
798+        /* Walk through the diff one line at a time.  We build the $edits
799+         * array of diff operations by reading the first character of the
800+         * xdiff output (which is in the "unified diff" format).
801+         *
802+         * Note that we don't have enough information to detect "changed"
803+         * lines using this approach, so we can't add Text_Diff_Op_changed
804+         * instances to the $edits array.  The result is still perfectly
805+         * valid, albeit a little less descriptive and efficient. */
806+        $edits = array();
807+        foreach ($diff as $line) {
808+            switch ($line[0]) {
809+            case ' ':
810+                $edits[] = &new Text_Diff_Op_copy(array(substr($line, 1)));
811+                break;
812+
813+            case '+':
814+                $edits[] = &new Text_Diff_Op_add(array(substr($line, 1)));
815+                break;
816+
817+            case '-':
818+                $edits[] = &new Text_Diff_Op_delete(array(substr($line, 1)));
819+                break;
820+            }
821+        }
822+
823+        return $edits;
824+    }
825+
826+}
827Index: wp-includes/Text/Diff/Engine/native.php
828===================================================================
829--- wp-includes/Text/Diff/Engine/native.php     (revision 0)
830+++ wp-includes/Text/Diff/Engine/native.php     (revision 0)
831@@ -0,0 +1,437 @@
832+<?php
833+/**
834+ * $Horde: framework/Text_Diff/Diff/Engine/native.php,v 1.10 2008/01/04 10:27:53 jan Exp $
835+ *
836+ * Class used internally by Text_Diff to actually compute the diffs. This
837+ * class is implemented using native PHP code.
838+ *
839+ * The algorithm used here is mostly lifted from the perl module
840+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
841+ * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
842+ *
843+ * More ideas are taken from: http://www.ics.uci.edu/~eppstein/161/960229.html
844+ *
845+ * Some ideas (and a bit of code) are taken from analyze.c, of GNU
846+ * diffutils-2.7, which can be found at:
847+ * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
848+ *
849+ * Some ideas (subdivision by NCHUNKS > 2, and some optimizations) are from
850+ * Geoffrey T. Dairiki <dairiki@dairiki.org>. The original PHP version of this
851+ * code was written by him, and is used/adapted with his permission.
852+ *
853+ * Copyright 2004-2008 The Horde Project (http://www.horde.org/)
854+ *
855+ * See the enclosed file COPYING for license information (LGPL). If you did
856+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
857+ *
858+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
859+ * @package Text_Diff
860+ */
861+class Text_Diff_Engine_native {
862+
863+    function diff($from_lines, $to_lines)
864+    {
865+        array_walk($from_lines, array('Text_Diff', 'trimNewlines'));
866+        array_walk($to_lines, array('Text_Diff', 'trimNewlines'));
867+
868+        $n_from = count($from_lines);
869+        $n_to = count($to_lines);
870+
871+        $this->xchanged = $this->ychanged = array();
872+        $this->xv = $this->yv = array();
873+        $this->xind = $this->yind = array();
874+        unset($this->seq);
875+        unset($this->in_seq);
876+        unset($this->lcs);
877+
878+        // Skip leading common lines.
879+        for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
880+            if ($from_lines[$skip] !== $to_lines[$skip]) {
881+                break;
882+            }
883+            $this->xchanged[$skip] = $this->ychanged[$skip] = false;
884+        }
885+
886+        // Skip trailing common lines.
887+        $xi = $n_from; $yi = $n_to;
888+        for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
889+            if ($from_lines[$xi] !== $to_lines[$yi]) {
890+                break;
891+            }
892+            $this->xchanged[$xi] = $this->ychanged[$yi] = false;
893+        }
894+
895+        // Ignore lines which do not exist in both files.
896+        for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
897+            $xhash[$from_lines[$xi]] = 1;
898+        }
899+        for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
900+            $line = $to_lines[$yi];
901+            if (($this->ychanged[$yi] = empty($xhash[$line]))) {
902+                continue;
903+            }
904+            $yhash[$line] = 1;
905+            $this->yv[] = $line;
906+            $this->yind[] = $yi;
907+        }
908+        for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
909+            $line = $from_lines[$xi];
910+            if (($this->xchanged[$xi] = empty($yhash[$line]))) {
911+                continue;
912+            }
913+            $this->xv[] = $line;
914+            $this->xind[] = $xi;
915+        }
916+
917+        // Find the LCS.
918+        $this->_compareseq(0, count($this->xv), 0, count($this->yv));
919+
920+        // Merge edits when possible.
921+        $this->_shiftBoundaries($from_lines, $this->xchanged, $this->ychanged);
922+        $this->_shiftBoundaries($to_lines, $this->ychanged, $this->xchanged);
923+
924+        // Compute the edit operations.
925+        $edits = array();
926+        $xi = $yi = 0;
927+        while ($xi < $n_from || $yi < $n_to) {
928+            assert($yi < $n_to || $this->xchanged[$xi]);
929+            assert($xi < $n_from || $this->ychanged[$yi]);
930+
931+            // Skip matching "snake".
932+            $copy = array();
933+            while ($xi < $n_from && $yi < $n_to
934+                   && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
935+                $copy[] = $from_lines[$xi++];
936+                ++$yi;
937+            }
938+            if ($copy) {
939+                $edits[] = &new Text_Diff_Op_copy($copy);
940+            }
941+
942+            // Find deletes & adds.
943+            $delete = array();
944+            while ($xi < $n_from && $this->xchanged[$xi]) {
945+                $delete[] = $from_lines[$xi++];
946+            }
947+
948+            $add = array();
949+            while ($yi < $n_to && $this->ychanged[$yi]) {
950+                $add[] = $to_lines[$yi++];
951+            }
952+
953+            if ($delete && $add) {
954+                $edits[] = &new Text_Diff_Op_change($delete, $add);
955+            } elseif ($delete) {
956+                $edits[] = &new Text_Diff_Op_delete($delete);
957+            } elseif ($add) {
958+                $edits[] = &new Text_Diff_Op_add($add);
959+            }
960+        }
961+
962+        return $edits;
963+    }
964+
965+    /**
966+     * Divides the Largest Common Subsequence (LCS) of the sequences (XOFF,
967+     * XLIM) and (YOFF, YLIM) into NCHUNKS approximately equally sized
968+     * segments.
969+     *
970+     * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an array of
971+     * NCHUNKS+1 (X, Y) indexes giving the diving points between sub
972+     * sequences.  The first sub-sequence is contained in (X0, X1), (Y0, Y1),
973+     * the second in (X1, X2), (Y1, Y2) and so on.  Note that (X0, Y0) ==
974+     * (XOFF, YOFF) and (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
975+     *
976+     * This function assumes that the first lines of the specified portions of
977+     * the two files do not match, and likewise that the last lines do not
978+     * match.  The caller must trim matching lines from the beginning and end
979+     * of the portions it is going to specify.
980+     */
981+    function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks)
982+    {
983+        $flip = false;
984+
985+        if ($xlim - $xoff > $ylim - $yoff) {
986+            /* Things seems faster (I'm not sure I understand why) when the
987+             * shortest sequence is in X. */
988+            $flip = true;
989+            list ($xoff, $xlim, $yoff, $ylim)
990+                = array($yoff, $ylim, $xoff, $xlim);
991+        }
992+
993+        if ($flip) {
994+            for ($i = $ylim - 1; $i >= $yoff; $i--) {
995+                $ymatches[$this->xv[$i]][] = $i;
996+            }
997+        } else {
998+            for ($i = $ylim - 1; $i >= $yoff; $i--) {
999+                $ymatches[$this->yv[$i]][] = $i;
1000+            }
1001+        }
1002+
1003+        $this->lcs = 0;
1004+        $this->seq[0]= $yoff - 1;
1005+        $this->in_seq = array();
1006+        $ymids[0] = array();
1007+
1008+        $numer = $xlim - $xoff + $nchunks - 1;
1009+        $x = $xoff;
1010+        for ($chunk = 0; $chunk < $nchunks; $chunk++) {
1011+            if ($chunk > 0) {
1012+                for ($i = 0; $i <= $this->lcs; $i++) {
1013+                    $ymids[$i][$chunk - 1] = $this->seq[$i];
1014+                }
1015+            }
1016+
1017+            $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $chunk) / $nchunks);
1018+            for (; $x < $x1; $x++) {
1019+                $line = $flip ? $this->yv[$x] : $this->xv[$x];
1020+                if (empty($ymatches[$line])) {
1021+                    continue;
1022+                }
1023+                $matches = $ymatches[$line];
1024+                reset($matches);
1025+                while (list(, $y) = each($matches)) {
1026+                    if (empty($this->in_seq[$y])) {
1027+                        $k = $this->_lcsPos($y);
1028+                        assert($k > 0);
1029+                        $ymids[$k] = $ymids[$k - 1];
1030+                        break;
1031+                    }
1032+                }
1033+                while (list(, $y) = each($matches)) {
1034+                    if ($y > $this->seq[$k - 1]) {
1035+                        assert($y <= $this->seq[$k]);
1036+                        /* Optimization: this is a common case: next match is
1037+                         * just replacing previous match. */
1038+                        $this->in_seq[$this->seq[$k]] = false;
1039+                        $this->seq[$k] = $y;
1040+                        $this->in_seq[$y] = 1;
1041+                    } elseif (empty($this->in_seq[$y])) {
1042+                        $k = $this->_lcsPos($y);
1043+                        assert($k > 0);
1044+                        $ymids[$k] = $ymids[$k - 1];
1045+                    }
1046+                }
1047+            }
1048+        }
1049+
1050+        $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
1051+        $ymid = $ymids[$this->lcs];
1052+        for ($n = 0; $n < $nchunks - 1; $n++) {
1053+            $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
1054+            $y1 = $ymid[$n] + 1;
1055+            $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
1056+        }
1057+        $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
1058+
1059+        return array($this->lcs, $seps);
1060+    }
1061+
1062+    function _lcsPos($ypos)
1063+    {
1064+        $end = $this->lcs;
1065+        if ($end == 0 || $ypos > $this->seq[$end]) {
1066+            $this->seq[++$this->lcs] = $ypos;
1067+            $this->in_seq[$ypos] = 1;
1068+            return $this->lcs;
1069+        }
1070+
1071+        $beg = 1;
1072+        while ($beg < $end) {
1073+            $mid = (int)(($beg + $end) / 2);
1074+            if ($ypos > $this->seq[$mid]) {
1075+                $beg = $mid + 1;
1076+            } else {
1077+                $end = $mid;
1078+            }
1079+        }
1080+
1081+        assert($ypos != $this->seq[$end]);
1082+
1083+        $this->in_seq[$this->seq[$end]] = false;
1084+        $this->seq[$end] = $ypos;
1085+        $this->in_seq[$ypos] = 1;
1086+        return $end;
1087+    }
1088+
1089+    /**
1090+     * Finds LCS of two sequences.
1091+     *
1092+     * The results are recorded in the vectors $this->{x,y}changed[], by
1093+     * storing a 1 in the element for each line that is an insertion or
1094+     * deletion (ie. is not in the LCS).
1095+     *
1096+     * The subsequence of file 0 is (XOFF, XLIM) and likewise for file 1.
1097+     *
1098+     * Note that XLIM, YLIM are exclusive bounds.  All line numbers are
1099+     * origin-0 and discarded lines are not counted.
1100+     */
1101+    function _compareseq ($xoff, $xlim, $yoff, $ylim)
1102+    {
1103+        /* Slide down the bottom initial diagonal. */
1104+        while ($xoff < $xlim && $yoff < $ylim
1105+               && $this->xv[$xoff] == $this->yv[$yoff]) {
1106+            ++$xoff;
1107+            ++$yoff;
1108+        }
1109+
1110+        /* Slide up the top initial diagonal. */
1111+        while ($xlim > $xoff && $ylim > $yoff
1112+               && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
1113+            --$xlim;
1114+            --$ylim;
1115+        }
1116+
1117+        if ($xoff == $xlim || $yoff == $ylim) {
1118+            $lcs = 0;
1119+        } else {
1120+            /* This is ad hoc but seems to work well.  $nchunks =
1121+             * sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5); $nchunks =
1122+             * max(2,min(8,(int)$nchunks)); */
1123+            $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
1124+            list($lcs, $seps)
1125+                = $this->_diag($xoff, $xlim, $yoff, $ylim, $nchunks);
1126+        }
1127+
1128+        if ($lcs == 0) {
1129+            /* X and Y sequences have no common subsequence: mark all
1130+             * changed. */
1131+            while ($yoff < $ylim) {
1132+                $this->ychanged[$this->yind[$yoff++]] = 1;
1133+            }
1134+            while ($xoff < $xlim) {
1135+                $this->xchanged[$this->xind[$xoff++]] = 1;
1136+            }
1137+        } else {
1138+            /* Use the partitions to split this problem into subproblems. */
1139+            reset($seps);
1140+            $pt1 = $seps[0];
1141+            while ($pt2 = next($seps)) {
1142+                $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
1143+                $pt1 = $pt2;
1144+            }
1145+        }
1146+    }
1147+
1148+    /**
1149+     * Adjusts inserts/deletes of identical lines to join changes as much as
1150+     * possible.
1151+     *
1152+     * We do something when a run of changed lines include a line at one end
1153+     * and has an excluded, identical line at the other.  We are free to
1154+     * choose which identical line is included.  `compareseq' usually chooses
1155+     * the one at the beginning, but usually it is cleaner to consider the
1156+     * following identical line to be the "change".
1157+     *
1158+     * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
1159+     */
1160+    function _shiftBoundaries($lines, &$changed, $other_changed)
1161+    {
1162+        $i = 0;
1163+        $j = 0;
1164+
1165+        assert('count($lines) == count($changed)');
1166+        $len = count($lines);
1167+        $other_len = count($other_changed);
1168+
1169+        while (1) {
1170+            /* Scan forward to find the beginning of another run of
1171+             * changes. Also keep track of the corresponding point in the
1172+             * other file.
1173+             *
1174+             * Throughout this code, $i and $j are adjusted together so that
1175+             * the first $i elements of $changed and the first $j elements of
1176+             * $other_changed both contain the same number of zeros (unchanged
1177+             * lines).
1178+             *
1179+             * Furthermore, $j is always kept so that $j == $other_len or
1180+             * $other_changed[$j] == false. */
1181+            while ($j < $other_len && $other_changed[$j]) {
1182+                $j++;
1183+            }
1184+
1185+            while ($i < $len && ! $changed[$i]) {
1186+                assert('$j < $other_len && ! $other_changed[$j]');
1187+                $i++; $j++;
1188+                while ($j < $other_len && $other_changed[$j]) {
1189+                    $j++;
1190+                }
1191+            }
1192+
1193+            if ($i == $len) {
1194+                break;
1195+            }
1196+
1197+            $start = $i;
1198+
1199+            /* Find the end of this run of changes. */
1200+            while (++$i < $len && $changed[$i]) {
1201+                continue;
1202+            }
1203+
1204+            do {
1205+                /* Record the length of this run of changes, so that we can
1206+                 * later determine whether the run has grown. */
1207+                $runlength = $i - $start;
1208+
1209+                /* Move the changed region back, so long as the previous
1210+                 * unchanged line matches the last changed one.  This merges
1211+                 * with previous changed regions. */
1212+                while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
1213+                    $changed[--$start] = 1;
1214+                    $changed[--$i] = false;
1215+                    while ($start > 0 && $changed[$start - 1]) {
1216+                        $start--;
1217+                    }
1218+                    assert('$j > 0');
1219+                    while ($other_changed[--$j]) {
1220+                        continue;
1221+                    }
1222+                    assert('$j >= 0 && !$other_changed[$j]');
1223+                }
1224+
1225+                /* Set CORRESPONDING to the end of the changed run, at the
1226+                 * last point where it corresponds to a changed run in the
1227+                 * other file. CORRESPONDING == LEN means no such point has
1228+                 * been found. */
1229+                $corresponding = $j < $other_len ? $i : $len;
1230+
1231+                /* Move the changed region forward, so long as the first
1232+                 * changed line matches the following unchanged one.  This
1233+                 * merges with following changed regions.  Do this second, so
1234+                 * that if there are no merges, the changed region is moved
1235+                 * forward as far as possible. */
1236+                while ($i < $len && $lines[$start] == $lines[$i]) {
1237+                    $changed[$start++] = false;
1238+                    $changed[$i++] = 1;
1239+                    while ($i < $len && $changed[$i]) {
1240+                        $i++;
1241+                    }
1242+
1243+                    assert('$j < $other_len && ! $other_changed[$j]');
1244+                    $j++;
1245+                    if ($j < $other_len && $other_changed[$j]) {
1246+                        $corresponding = $i;
1247+                        while ($j < $other_len && $other_changed[$j]) {
1248+                            $j++;
1249+                        }
1250+                    }
1251+                }
1252+            } while ($runlength != $i - $start);
1253+
1254+            /* If possible, move the fully-merged run of changes back to a
1255+             * corresponding run in the other file. */
1256+            while ($corresponding < $i) {
1257+                $changed[--$start] = 1;
1258+                $changed[--$i] = 0;
1259+                assert('$j > 0');
1260+                while ($other_changed[--$j]) {
1261+                    continue;
1262+                }
1263+                assert('$j >= 0 && !$other_changed[$j]');
1264+            }
1265+        }
1266+    }
1267+
1268+}
1269Index: wp-includes/Text/Diff/Engine/string.php
1270===================================================================
1271--- wp-includes/Text/Diff/Engine/string.php     (revision 0)
1272+++ wp-includes/Text/Diff/Engine/string.php     (revision 0)
1273@@ -0,0 +1,234 @@
1274+<?php
1275+/**
1276+ * Parses unified or context diffs output from eg. the diff utility.
1277+ *
1278+ * Example:
1279+ * <code>
1280+ * $patch = file_get_contents('example.patch');
1281+ * $diff = new Text_Diff('string', array($patch));
1282+ * $renderer = new Text_Diff_Renderer_inline();
1283+ * echo $renderer->render($diff);
1284+ * </code>
1285+ *
1286+ * $Horde: framework/Text_Diff/Diff/Engine/string.php,v 1.7 2008/01/04 10:07:50 jan Exp $
1287+ *
1288+ * Copyright 2005 Örjan Persson <o@42mm.org>
1289+ * Copyright 2005-2008 The Horde Project (http://www.horde.org/)
1290+ *
1291+ * See the enclosed file COPYING for license information (LGPL). If you did
1292+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
1293+ *
1294+ * @author  Örjan Persson <o@42mm.org>
1295+ * @package Text_Diff
1296+ * @since   0.2.0
1297+ */
1298+class Text_Diff_Engine_string {
1299+
1300+    /**
1301+     * Parses a unified or context diff.
1302+     *
1303+     * First param contains the whole diff and the second can be used to force
1304+     * a specific diff type. If the second parameter is 'autodetect', the
1305+     * diff will be examined to find out which type of diff this is.
1306+     *
1307+     * @param string $diff  The diff content.
1308+     * @param string $mode  The diff mode of the content in $diff. One of
1309+     *                      'context', 'unified', or 'autodetect'.
1310+     *
1311+     * @return array  List of all diff operations.
1312+     */
1313+    function diff($diff, $mode = 'autodetect')
1314+    {
1315+        if ($mode != 'autodetect' && $mode != 'context' && $mode != 'unified') {
1316+            return PEAR::raiseError('Type of diff is unsupported');
1317+        }
1318+
1319+        if ($mode == 'autodetect') {
1320+            $context = strpos($diff, '***');
1321+            $unified = strpos($diff, '---');
1322+            if ($context === $unified) {
1323+                return PEAR::raiseError('Type of diff could not be detected');
1324+            } elseif ($context === false || $context === false) {
1325+                $mode = $context !== false ? 'context' : 'unified';
1326+            } else {
1327+                $mode = $context < $unified ? 'context' : 'unified';
1328+            }
1329+        }
1330+
1331+        // split by new line and remove the diff header
1332+        $diff = explode("\n", $diff);
1333+        array_shift($diff);
1334+        array_shift($diff);
1335+
1336+        if ($mode == 'context') {
1337+            return $this->parseContextDiff($diff);
1338+        } else {
1339+            return $this->parseUnifiedDiff($diff);
1340+        }
1341+    }
1342+
1343+    /**
1344+     * Parses an array containing the unified diff.
1345+     *
1346+     * @param array $diff  Array of lines.
1347+     *
1348+     * @return array  List of all diff operations.
1349+     */
1350+    function parseUnifiedDiff($diff)
1351+    {
1352+        $edits = array();
1353+        $end = count($diff) - 1;
1354+        for ($i = 0; $i < $end;) {
1355+            $diff1 = array();
1356+            switch (substr($diff[$i], 0, 1)) {
1357+            case ' ':
1358+                do {
1359+                    $diff1[] = substr($diff[$i], 1);
1360+                } while (++$i < $end && substr($diff[$i], 0, 1) == ' ');
1361+                $edits[] = &new Text_Diff_Op_copy($diff1);
1362+                break;
1363+
1364+            case '+':
1365+                // get all new lines
1366+                do {
1367+                    $diff1[] = substr($diff[$i], 1);
1368+                } while (++$i < $end && substr($diff[$i], 0, 1) == '+');
1369+                $edits[] = &new Text_Diff_Op_add($diff1);
1370+                break;
1371+
1372+            case '-':
1373+                // get changed or removed lines
1374+                $diff2 = array();
1375+                do {
1376+                    $diff1[] = substr($diff[$i], 1);
1377+                } while (++$i < $end && substr($diff[$i], 0, 1) == '-');
1378+
1379+                while ($i < $end && substr($diff[$i], 0, 1) == '+') {
1380+                    $diff2[] = substr($diff[$i++], 1);
1381+                }
1382+                if (count($diff2) == 0) {
1383+                    $edits[] = &new Text_Diff_Op_delete($diff1);
1384+                } else {
1385+                    $edits[] = &new Text_Diff_Op_change($diff1, $diff2);
1386+                }
1387+                break;
1388+
1389+            default:
1390+                $i++;
1391+                break;
1392+            }
1393+        }
1394+
1395+        return $edits;
1396+    }
1397+
1398+    /**
1399+     * Parses an array containing the context diff.
1400+     *
1401+     * @param array $diff  Array of lines.
1402+     *
1403+     * @return array  List of all diff operations.
1404+     */
1405+    function parseContextDiff(&$diff)
1406+    {
1407+        $edits = array();
1408+        $i = $max_i = $j = $max_j = 0;
1409+        $end = count($diff) - 1;
1410+        while ($i < $end && $j < $end) {
1411+            while ($i >= $max_i && $j >= $max_j) {
1412+                // Find the boundaries of the diff output of the two files
1413+                for ($i = $j;
1414+                     $i < $end && substr($diff[$i], 0, 3) == '***';
1415+                     $i++);
1416+                for ($max_i = $i;
1417+                     $max_i < $end && substr($diff[$max_i], 0, 3) != '---';
1418+                     $max_i++);
1419+                for ($j = $max_i;
1420+                     $j < $end && substr($diff[$j], 0, 3) == '---';
1421+                     $j++);
1422+                for ($max_j = $j;
1423+                     $max_j < $end && substr($diff[$max_j], 0, 3) != '***';
1424+                     $max_j++);
1425+            }
1426+
1427+            // find what hasn't been changed
1428+            $array = array();
1429+            while ($i < $max_i &&
1430+                   $j < $max_j &&
1431+                   strcmp($diff[$i], $diff[$j]) == 0) {
1432+                $array[] = substr($diff[$i], 2);
1433+                $i++;
1434+                $j++;
1435+            }
1436+
1437+            while ($i < $max_i && ($max_j-$j) <= 1) {
1438+                if ($diff[$i] != '' && substr($diff[$i], 0, 1) != ' ') {
1439+                    break;
1440+                }
1441+                $array[] = substr($diff[$i++], 2);
1442+            }
1443+
1444+            while ($j < $max_j && ($max_i-$i) <= 1) {
1445+                if ($diff[$j] != '' && substr($diff[$j], 0, 1) != ' ') {
1446+                    break;
1447+                }
1448+                $array[] = substr($diff[$j++], 2);
1449+            }
1450+            if (count($array) > 0) {
1451+                $edits[] = &new Text_Diff_Op_copy($array);
1452+            }
1453+
1454+            if ($i < $max_i) {
1455+                $diff1 = array();
1456+                switch (substr($diff[$i], 0, 1)) {
1457+                case '!':
1458+                    $diff2 = array();
1459+                    do {
1460+                        $diff1[] = substr($diff[$i], 2);
1461+                        if ($j < $max_j && substr($diff[$j], 0, 1) == '!') {
1462+                            $diff2[] = substr($diff[$j++], 2);
1463+                        }
1464+                    } while (++$i < $max_i && substr($diff[$i], 0, 1) == '!');
1465+                    $edits[] = &new Text_Diff_Op_change($diff1, $diff2);
1466+                    break;
1467+
1468+                case '+':
1469+                    do {
1470+                        $diff1[] = substr($diff[$i], 2);
1471+                    } while (++$i < $max_i && substr($diff[$i], 0, 1) == '+');
1472+                    $edits[] = &new Text_Diff_Op_add($diff1);
1473+                    break;
1474+
1475+                case '-':
1476+                    do {
1477+                        $diff1[] = substr($diff[$i], 2);
1478+                    } while (++$i < $max_i && substr($diff[$i], 0, 1) == '-');
1479+                    $edits[] = &new Text_Diff_Op_delete($diff1);
1480+                    break;
1481+                }
1482+            }
1483+
1484+            if ($j < $max_j) {
1485+                $diff2 = array();
1486+                switch (substr($diff[$j], 0, 1)) {
1487+                case '+':
1488+                    do {
1489+                        $diff2[] = substr($diff[$j++], 2);
1490+                    } while ($j < $max_j && substr($diff[$j], 0, 1) == '+');
1491+                    $edits[] = &new Text_Diff_Op_add($diff2);
1492+                    break;
1493+
1494+                case '-':
1495+                    do {
1496+                        $diff2[] = substr($diff[$j++], 2);
1497+                    } while ($j < $max_j && substr($diff[$j], 0, 1) == '-');
1498+                    $edits[] = &new Text_Diff_Op_delete($diff2);
1499+                    break;
1500+                }
1501+            }
1502+        }
1503+
1504+        return $edits;
1505+    }
1506+
1507+}
1508Index: wp-includes/Text/Diff/Engine/shell.php
1509===================================================================
1510--- wp-includes/Text/Diff/Engine/shell.php      (revision 0)
1511+++ wp-includes/Text/Diff/Engine/shell.php      (revision 0)
1512@@ -0,0 +1,164 @@
1513+<?php
1514+/**
1515+ * Class used internally by Diff to actually compute the diffs.
1516+ *
1517+ * This class uses the Unix `diff` program via shell_exec to compute the
1518+ * differences between the two input arrays.
1519+ *
1520+ * $Horde: framework/Text_Diff/Diff/Engine/shell.php,v 1.8 2008/01/04 10:07:50 jan Exp $
1521+ *
1522+ * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
1523+ *
1524+ * See the enclosed file COPYING for license information (LGPL). If you did
1525+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
1526+ *
1527+ * @author  Milian Wolff <mail@milianw.de>
1528+ * @package Text_Diff
1529+ * @since   0.3.0
1530+ */
1531+class Text_Diff_Engine_shell {
1532+
1533+    /**
1534+     * Path to the diff executable
1535+     *
1536+     * @var string
1537+     */
1538+    var $_diffCommand = 'diff';
1539+
1540+    /**
1541+     * Returns the array of differences.
1542+     *
1543+     * @param array $from_lines lines of text from old file
1544+     * @param array $to_lines   lines of text from new file
1545+     *
1546+     * @return array all changes made (array with Text_Diff_Op_* objects)
1547+     */
1548+    function diff($from_lines, $to_lines)
1549+    {
1550+        array_walk($from_lines, array('Text_Diff', 'trimNewlines'));
1551+        array_walk($to_lines, array('Text_Diff', 'trimNewlines'));
1552+
1553+        $temp_dir = Text_Diff::_getTempDir();
1554+
1555+        // Execute gnu diff or similar to get a standard diff file.
1556+        $from_file = tempnam($temp_dir, 'Text_Diff');
1557+        $to_file = tempnam($temp_dir, 'Text_Diff');
1558+        $fp = fopen($from_file, 'w');
1559+        fwrite($fp, implode("\n", $from_lines));
1560+        fclose($fp);
1561+        $fp = fopen($to_file, 'w');
1562+        fwrite($fp, implode("\n", $to_lines));
1563+        fclose($fp);
1564+        $diff = shell_exec($this->_diffCommand . ' ' . $from_file . ' ' . $to_file);
1565+        unlink($from_file);
1566+        unlink($to_file);
1567+
1568+        if (is_null($diff)) {
1569+            // No changes were made
1570+            return array(new Text_Diff_Op_copy($from_lines));
1571+        }
1572+
1573+        $from_line_no = 1;
1574+        $to_line_no = 1;
1575+        $edits = array();
1576+
1577+        // Get changed lines by parsing something like:
1578+        // 0a1,2
1579+        // 1,2c4,6
1580+        // 1,5d6
1581+        preg_match_all('#^(\d+)(?:,(\d+))?([adc])(\d+)(?:,(\d+))?$#m', $diff,
1582+            $matches, PREG_SET_ORDER);
1583+
1584+        foreach ($matches as $match) {
1585+            if (!isset($match[5])) {
1586+                // This paren is not set every time (see regex).
1587+                $match[5] = false;
1588+            }
1589+
1590+            if ($match[3] == 'a') {
1591+                $from_line_no--;
1592+            }
1593+
1594+            if ($match[3] == 'd') {
1595+                $to_line_no--;
1596+            }
1597+
1598+            if ($from_line_no < $match[1] || $to_line_no < $match[4]) {
1599+                // copied lines
1600+                assert('$match[1] - $from_line_no == $match[4] - $to_line_no');
1601+                array_push($edits,
1602+                    new Text_Diff_Op_copy(
1603+                        $this->_getLines($from_lines, $from_line_no, $match[1] - 1),
1604+                        $this->_getLines($to_lines, $to_line_no, $match[4] - 1)));
1605+            }
1606+
1607+            switch ($match[3]) {
1608+            case 'd':
1609+                // deleted lines
1610+                array_push($edits,
1611+                    new Text_Diff_Op_delete(
1612+                        $this->_getLines($from_lines, $from_line_no, $match[2])));
1613+                $to_line_no++;
1614+                break;
1615+
1616+            case 'c':
1617+                // changed lines
1618+                array_push($edits,
1619+                    new Text_Diff_Op_change(
1620+                        $this->_getLines($from_lines, $from_line_no, $match[2]),
1621+                        $this->_getLines($to_lines, $to_line_no, $match[5])));
1622+                break;
1623+
1624+            case 'a':
1625+                // added lines
1626+                array_push($edits,
1627+                    new Text_Diff_Op_add(
1628+                        $this->_getLines($to_lines, $to_line_no, $match[5])));
1629+                $from_line_no++;
1630+                break;
1631+            }
1632+        }
1633+
1634+        if (!empty($from_lines)) {
1635+            // Some lines might still be pending. Add them as copied
1636+            array_push($edits,
1637+                new Text_Diff_Op_copy(
1638+                    $this->_getLines($from_lines, $from_line_no,
1639+                                     $from_line_no + count($from_lines) - 1),
1640+                    $this->_getLines($to_lines, $to_line_no,
1641+                                     $to_line_no + count($to_lines) - 1)));
1642+        }
1643+
1644+        return $edits;
1645+    }
1646+
1647+    /**
1648+     * Get lines from either the old or new text
1649+     *
1650+     * @access private
1651+     *
1652+     * @param array &$text_lines Either $from_lines or $to_lines
1653+     * @param int   &$line_no    Current line number
1654+     * @param int   $end         Optional end line, when we want to chop more
1655+     *                           than one line.
1656+     *
1657+     * @return array The chopped lines
1658+     */
1659+    function _getLines(&$text_lines, &$line_no, $end = false)
1660+    {
1661+        if (!empty($end)) {
1662+            $lines = array();
1663+            // We can shift even more
1664+            while ($line_no <= $end) {
1665+                array_push($lines, array_shift($text_lines));
1666+                $line_no++;
1667+            }
1668+        } else {
1669+            $lines = array(array_shift($text_lines));
1670+            $line_no++;
1671+        }
1672+
1673+        return $lines;
1674+    }
1675+
1676+}
1677Index: wp-includes/Text/Diff/Renderer/inline.php
1678===================================================================
1679--- wp-includes/Text/Diff/Renderer/inline.php   (revision 0)
1680+++ wp-includes/Text/Diff/Renderer/inline.php   (revision 0)
1681@@ -0,0 +1,170 @@
1682+<?php
1683+/**
1684+ * "Inline" diff renderer.
1685+ *
1686+ * $Horde: framework/Text_Diff/Diff/Renderer/inline.php,v 1.21 2008/01/04 10:07:51 jan Exp $
1687+ *
1688+ * Copyright 2004-2008 The Horde Project (http://www.horde.org/)
1689+ *
1690+ * See the enclosed file COPYING for license information (LGPL). If you did
1691+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
1692+ *
1693+ * @author  Ciprian Popovici
1694+ * @package Text_Diff
1695+ */
1696+
1697+/** Text_Diff_Renderer */
1698+require_once 'Text/Diff/Renderer.php';
1699+
1700+/**
1701+ * "Inline" diff renderer.
1702+ *
1703+ * This class renders diffs in the Wiki-style "inline" format.
1704+ *
1705+ * @author  Ciprian Popovici
1706+ * @package Text_Diff
1707+ */
1708+class Text_Diff_Renderer_inline extends Text_Diff_Renderer {
1709+
1710+    /**
1711+     * Number of leading context "lines" to preserve.
1712+     */
1713+    var $_leading_context_lines = 10000;
1714+
1715+    /**
1716+     * Number of trailing context "lines" to preserve.
1717+     */
1718+    var $_trailing_context_lines = 10000;
1719+
1720+    /**
1721+     * Prefix for inserted text.
1722+     */
1723+    var $_ins_prefix = '<ins>';
1724+
1725+    /**
1726+     * Suffix for inserted text.
1727+     */
1728+    var $_ins_suffix = '</ins>';
1729+
1730+    /**
1731+     * Prefix for deleted text.
1732+     */
1733+    var $_del_prefix = '<del>';
1734+
1735+    /**
1736+     * Suffix for deleted text.
1737+     */
1738+    var $_del_suffix = '</del>';
1739+
1740+    /**
1741+     * Header for each change block.
1742+     */
1743+    var $_block_header = '';
1744+
1745+    /**
1746+     * What are we currently splitting on? Used to recurse to show word-level
1747+     * changes.
1748+     */
1749+    var $_split_level = 'lines';
1750+
1751+    function _blockHeader($xbeg, $xlen, $ybeg, $ylen)
1752+    {
1753+        return $this->_block_header;
1754+    }
1755+
1756+    function _startBlock($header)
1757+    {
1758+        return $header;
1759+    }
1760+
1761+    function _lines($lines, $prefix = ' ', $encode = true)
1762+    {
1763+        if ($encode) {
1764+            array_walk($lines, array(&$this, '_encode'));
1765+        }
1766+
1767+        if ($this->_split_level == 'words') {
1768+            return implode('', $lines);
1769+        } else {
1770+            return implode("\n", $lines) . "\n";
1771+        }
1772+    }
1773+
1774+    function _added($lines)
1775+    {
1776+        array_walk($lines, array(&$this, '_encode'));
1777+        $lines[0] = $this->_ins_prefix . $lines[0];
1778+        $lines[count($lines) - 1] .= $this->_ins_suffix;
1779+        return $this->_lines($lines, ' ', false);
1780+    }
1781+
1782+    function _deleted($lines, $words = false)
1783+    {
1784+        array_walk($lines, array(&$this, '_encode'));
1785+        $lines[0] = $this->_del_prefix . $lines[0];
1786+        $lines[count($lines) - 1] .= $this->_del_suffix;
1787+        return $this->_lines($lines, ' ', false);
1788+    }
1789+
1790+    function _changed($orig, $final)
1791+    {
1792+        /* If we've already split on words, don't try to do so again - just
1793+         * display. */
1794+        if ($this->_split_level == 'words') {
1795+            $prefix = '';
1796+            while ($orig[0] !== false && $final[0] !== false &&
1797+                   substr($orig[0], 0, 1) == ' ' &&
1798+                   substr($final[0], 0, 1) == ' ') {
1799+                $prefix .= substr($orig[0], 0, 1);
1800+                $orig[0] = substr($orig[0], 1);
1801+                $final[0] = substr($final[0], 1);
1802+            }
1803+            return $prefix . $this->_deleted($orig) . $this->_added($final);
1804+        }
1805+
1806+        $text1 = implode("\n", $orig);
1807+        $text2 = implode("\n", $final);
1808+
1809+        /* Non-printing newline marker. */
1810+        $nl = "\0";
1811+
1812+        /* We want to split on word boundaries, but we need to
1813+         * preserve whitespace as well. Therefore we split on words,
1814+         * but include all blocks of whitespace in the wordlist. */
1815+        $diff = new Text_Diff($this->_splitOnWords($text1, $nl),
1816+                              $this->_splitOnWords($text2, $nl));
1817+
1818+        /* Get the diff in inline format. */
1819+        $renderer = new Text_Diff_Renderer_inline(array_merge($this->getParams(),
1820+                                                              array('split_level' => 'words')));
1821+
1822+        /* Run the diff and get the output. */
1823+        return str_replace($nl, "\n", $renderer->render($diff)) . "\n";
1824+    }
1825+
1826+    function _splitOnWords($string, $newlineEscape = "\n")
1827+    {
1828+        // Ignore \0; otherwise the while loop will never finish.
1829+        $string = str_replace("\0", '', $string);
1830+
1831+        $words = array();
1832+        $length = strlen($string);
1833+        $pos = 0;
1834+
1835+        while ($pos < $length) {
1836+            // Eat a word with any preceding whitespace.
1837+            $spaces = strspn(substr($string, $pos), " \n");
1838+            $nextpos = strcspn(substr($string, $pos + $spaces), " \n");
1839+            $words[] = str_replace("\n", $newlineEscape, substr($string, $pos, $spaces + $nextpos));
1840+            $pos += $spaces + $nextpos;
1841+        }
1842+
1843+        return $words;
1844+    }
1845+
1846+    function _encode(&$string)
1847+    {
1848+        $string = htmlspecialchars($string);
1849+    }
1850+
1851+}
1852Index: wp-includes/Text/Diff/Renderer.php
1853===================================================================
1854--- wp-includes/Text/Diff/Renderer.php  (revision 0)
1855+++ wp-includes/Text/Diff/Renderer.php  (revision 0)
1856@@ -0,0 +1,237 @@
1857+<?php
1858+/**
1859+ * A class to render Diffs in different formats.
1860+ *
1861+ * This class renders the diff in classic diff format. It is intended that
1862+ * this class be customized via inheritance, to obtain fancier outputs.
1863+ *
1864+ * $Horde: framework/Text_Diff/Diff/Renderer.php,v 1.21 2008/01/04 10:07:50 jan Exp $
1865+ *
1866+ * Copyright 2004-2008 The Horde Project (http://www.horde.org/)
1867+ *
1868+ * See the enclosed file COPYING for license information (LGPL). If you did
1869+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
1870+ *
1871+ * @package Text_Diff
1872+ */
1873+class Text_Diff_Renderer {
1874+
1875+    /**
1876+     * Number of leading context "lines" to preserve.
1877+     *
1878+     * This should be left at zero for this class, but subclasses may want to
1879+     * set this to other values.
1880+     */
1881+    var $_leading_context_lines = 0;
1882+
1883+    /**
1884+     * Number of trailing context "lines" to preserve.
1885+     *
1886+     * This should be left at zero for this class, but subclasses may want to
1887+     * set this to other values.
1888+     */
1889+    var $_trailing_context_lines = 0;
1890+
1891+    /**
1892+     * Constructor.
1893+     */
1894+    function Text_Diff_Renderer($params = array())
1895+    {
1896+        foreach ($params as $param => $value) {
1897+            $v = '_' . $param;
1898+            if (isset($this->$v)) {
1899+                $this->$v = $value;
1900+            }
1901+        }
1902+    }
1903+
1904+    /**
1905+     * Get any renderer parameters.
1906+     *
1907+     * @return array  All parameters of this renderer object.
1908+     */
1909+    function getParams()
1910+    {
1911+        $params = array();
1912+        foreach (get_object_vars($this) as $k => $v) {
1913+            if ($k[0] == '_') {
1914+                $params[substr($k, 1)] = $v;
1915+            }
1916+        }
1917+
1918+        return $params;
1919+    }
1920+
1921+    /**
1922+     * Renders a diff.
1923+     *
1924+     * @param Text_Diff $diff  A Text_Diff object.
1925+     *
1926+     * @return string  The formatted output.
1927+     */
1928+    function render($diff)
1929+    {
1930+        $xi = $yi = 1;
1931+        $block = false;
1932+        $context = array();
1933+
1934+        $nlead = $this->_leading_context_lines;
1935+        $ntrail = $this->_trailing_context_lines;
1936+
1937+        $output = $this->_startDiff();
1938+
1939+        $diffs = $diff->getDiff();
1940+        foreach ($diffs as $i => $edit) {
1941+            /* If these are unchanged (copied) lines, and we want to keep
1942+             * leading or trailing context lines, extract them from the copy
1943+             * block. */
1944+            if (is_a($edit, 'Text_Diff_Op_copy')) {
1945+                /* Do we have any diff blocks yet? */
1946+                if (is_array($block)) {
1947+                    /* How many lines to keep as context from the copy
1948+                     * block. */
1949+                    $keep = $i == count($diffs) - 1 ? $ntrail : $nlead + $ntrail;
1950+                    if (count($edit->orig) <= $keep) {
1951+                        /* We have less lines in the block than we want for
1952+                         * context => keep the whole block. */
1953+                        $block[] = $edit;
1954+                    } else {
1955+                        if ($ntrail) {
1956+                            /* Create a new block with as many lines as we need
1957+                             * for the trailing context. */
1958+                            $context = array_slice($edit->orig, 0, $ntrail);
1959+                            $block[] = &new Text_Diff_Op_copy($context);
1960+                        }
1961+                        /* @todo */
1962+                        $output .= $this->_block($x0, $ntrail + $xi - $x0,
1963+                                                 $y0, $ntrail + $yi - $y0,
1964+                                                 $block);
1965+                        $block = false;
1966+                    }
1967+                }
1968+                /* Keep the copy block as the context for the next block. */
1969+                $context = $edit->orig;
1970+            } else {
1971+                /* Don't we have any diff blocks yet? */
1972+                if (!is_array($block)) {
1973+                    /* Extract context lines from the preceding copy block. */
1974+                    $context = array_slice($context, count($context) - $nlead);
1975+                    $x0 = $xi - count($context);
1976+                    $y0 = $yi - count($context);
1977+                    $block = array();
1978+                    if ($context) {
1979+                        $block[] = &new Text_Diff_Op_copy($context);
1980+                    }
1981+                }
1982+                $block[] = $edit;
1983+            }
1984+
1985+            if ($edit->orig) {
1986+                $xi += count($edit->orig);
1987+            }
1988+            if ($edit->final) {
1989+                $yi += count($edit->final);
1990+            }
1991+        }
1992+
1993+        if (is_array($block)) {
1994+            $output .= $this->_block($x0, $xi - $x0,
1995+                                     $y0, $yi - $y0,
1996+                                     $block);
1997+        }
1998+
1999+        return $output . $this->_endDiff();
2000+    }
2001+
2002+    function _block($xbeg, $xlen, $ybeg, $ylen, &$edits)
2003+    {
2004+        $output = $this->_startBlock($this->_blockHeader($xbeg, $xlen, $ybeg, $ylen));
2005+
2006+        foreach ($edits as $edit) {
2007+            switch (strtolower(get_class($edit))) {
2008+            case 'text_diff_op_copy':
2009+                $output .= $this->_context($edit->orig);
2010+                break;
2011+
2012+            case 'text_diff_op_add':
2013+                $output .= $this->_added($edit->final);
2014+                break;
2015+
2016+            case 'text_diff_op_delete':
2017+                $output .= $this->_deleted($edit->orig);
2018+                break;
2019+
2020+            case 'text_diff_op_change':
2021+                $output .= $this->_changed($edit->orig, $edit->final);
2022+                break;
2023+            }
2024+        }
2025+
2026+        return $output . $this->_endBlock();
2027+    }
2028+
2029+    function _startDiff()
2030+    {
2031+        return '';
2032+    }
2033+
2034+    function _endDiff()
2035+    {
2036+        return '';
2037+    }
2038+
2039+    function _blockHeader($xbeg, $xlen, $ybeg, $ylen)
2040+    {
2041+        if ($xlen > 1) {
2042+            $xbeg .= ',' . ($xbeg + $xlen - 1);
2043+        }
2044+        if ($ylen > 1) {
2045+            $ybeg .= ',' . ($ybeg + $ylen - 1);
2046+        }
2047+
2048+        // this matches the GNU Diff behaviour
2049+        if ($xlen && !$ylen) {
2050+            $ybeg--;
2051+        } elseif (!$xlen) {
2052+            $xbeg--;
2053+        }
2054+
2055+        return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
2056+    }
2057+
2058+    function _startBlock($header)
2059+    {
2060+        return $header . "\n";
2061+    }
2062+
2063+    function _endBlock()
2064+    {
2065+        return '';
2066+    }
2067+
2068+    function _lines($lines, $prefix = ' ')
2069+    {
2070+        return $prefix . implode("\n$prefix", $lines) . "\n";
2071+    }
2072+
2073+    function _context($lines)
2074+    {
2075+        return $this->_lines($lines, '  ');
2076+    }
2077+
2078+    function _added($lines)
2079+    {
2080+        return $this->_lines($lines, '> ');
2081+    }
2082+
2083+    function _deleted($lines)
2084+    {
2085+        return $this->_lines($lines, '< ');
2086+    }
2087+
2088+    function _changed($orig, $final)
2089+    {
2090+        return $this->_deleted($orig) . "---\n" . $this->_added($final);
2091+    }
2092+
2093+}
2094Index: wp-includes/Text/Diff.php
2095===================================================================
2096--- wp-includes/Text/Diff.php   (revision 0)
2097+++ wp-includes/Text/Diff.php   (revision 0)
2098@@ -0,0 +1,413 @@
2099+<?php
2100+/**
2101+ * General API for generating and formatting diffs - the differences between
2102+ * two sequences of strings.
2103+ *
2104+ * The original PHP version of this code was written by Geoffrey T. Dairiki
2105+ * <dairiki@dairiki.org>, and is used/adapted with his permission.
2106+ *
2107+ * $Horde: framework/Text_Diff/Diff.php,v 1.26 2008/01/04 10:07:49 jan Exp $
2108+ *
2109+ * Copyright 2004 Geoffrey T. Dairiki <dairiki@dairiki.org>
2110+ * Copyright 2004-2008 The Horde Project (http://www.horde.org/)
2111+ *
2112+ * See the enclosed file COPYING for license information (LGPL). If you did
2113+ * not receive this file, see http://opensource.org/licenses/lgpl-license.php.
2114+ *
2115+ * @package Text_Diff
2116+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
2117+ */
2118+class Text_Diff {
2119+
2120+    /**
2121+     * Array of changes.
2122+     *
2123+     * @var array
2124+     */
2125+    var $_edits;
2126+
2127+    /**
2128+     * Computes diffs between sequences of strings.
2129+     *
2130+     * @param string $engine     Name of the diffing engine to use.  'auto'
2131+     *                           will automatically select the best.
2132+     * @param array $params      Parameters to pass to the diffing engine.
2133+     *                           Normally an array of two arrays, each
2134+     *                           containing the lines from a file.
2135+     */
2136+    function Text_Diff($engine, $params)
2137+    {
2138+        // Backward compatibility workaround.
2139+        if (!is_string($engine)) {
2140+            $params = array($engine, $params);
2141+            $engine = 'auto';
2142+        }
2143+
2144+        if ($engine == 'auto') {
2145+            $engine = extension_loaded('xdiff') ? 'xdiff' : 'native';
2146+        } else {
2147+            $engine = basename($engine);
2148+        }
2149+
2150+        require_once 'Text/Diff/Engine/' . $engine . '.php';
2151+        $class = 'Text_Diff_Engine_' . $engine;
2152+        $diff_engine = new $class();
2153+
2154+        $this->_edits = call_user_func_array(array($diff_engine, 'diff'), $params);
2155+    }
2156+
2157+    /**
2158+     * Returns the array of differences.
2159+     */
2160+    function getDiff()
2161+    {
2162+        return $this->_edits;
2163+    }
2164+
2165+    /**
2166+     * Computes a reversed diff.
2167+     *
2168+     * Example:
2169+     * <code>
2170+     * $diff = new Text_Diff($lines1, $lines2);
2171+     * $rev = $diff->reverse();
2172+     * </code>
2173+     *
2174+     * @return Text_Diff  A Diff object representing the inverse of the
2175+     *                    original diff.  Note that we purposely don't return a
2176+     *                    reference here, since this essentially is a clone()
2177+     *                    method.
2178+     */
2179+    function reverse()
2180+    {
2181+        if (version_compare(zend_version(), '2', '>')) {
2182+            $rev = clone($this);
2183+        } else {
2184+            $rev = $this;
2185+        }
2186+        $rev->_edits = array();
2187+        foreach ($this->_edits as $edit) {
2188+            $rev->_edits[] = $edit->reverse();
2189+        }
2190+        return $rev;
2191+    }
2192+
2193+    /**
2194+     * Checks for an empty diff.
2195+     *
2196+     * @return boolean  True if two sequences were identical.
2197+     */
2198+    function isEmpty()
2199+    {
2200+        foreach ($this->_edits as $edit) {
2201+            if (!is_a($edit, 'Text_Diff_Op_copy')) {
2202+                return false;
2203+            }
2204+        }
2205+        return true;
2206+    }
2207+
2208+    /**
2209+     * Computes the length of the Longest Common Subsequence (LCS).
2210+     *
2211+     * This is mostly for diagnostic purposes.
2212+     *
2213+     * @return integer  The length of the LCS.
2214+     */
2215+    function lcs()
2216+    {
2217+        $lcs = 0;
2218+        foreach ($this->_edits as $edit) {
2219+            if (is_a($edit, 'Text_Diff_Op_copy')) {
2220+                $lcs += count($edit->orig);
2221+            }
2222+        }
2223+        return $lcs;
2224+    }
2225+
2226+    /**
2227+     * Gets the original set of lines.
2228+     *
2229+     * This reconstructs the $from_lines parameter passed to the constructor.
2230+     *
2231+     * @return array  The original sequence of strings.
2232+     */
2233+    function getOriginal()
2234+    {
2235+        $lines = array();
2236+        foreach ($this->_edits as $edit) {
2237+            if ($edit->orig) {
2238+                array_splice($lines, count($lines), 0, $edit->orig);
2239+            }
2240+        }
2241+        return $lines;
2242+    }
2243+
2244+    /**
2245+     * Gets the final set of lines.
2246+     *
2247+     * This reconstructs the $to_lines parameter passed to the constructor.
2248+     *
2249+     * @return array  The sequence of strings.
2250+     */
2251+    function getFinal()
2252+    {
2253+        $lines = array();
2254+        foreach ($this->_edits as $edit) {
2255+            if ($edit->final) {
2256+                array_splice($lines, count($lines), 0, $edit->final);
2257+            }
2258+        }
2259+        return $lines;
2260+    }
2261+
2262+    /**
2263+     * Removes trailing newlines from a line of text. This is meant to be used
2264+     * with array_walk().
2265+     *
2266+     * @param string $line  The line to trim.
2267+     * @param integer $key  The index of the line in the array. Not used.
2268+     */
2269+    function trimNewlines(&$line, $key)
2270+    {
2271+        $line = str_replace(array("\n", "\r"), '', $line);
2272+    }
2273+
2274+    /**
2275+     * Determines the location of the system temporary directory.
2276+     *
2277+     * @static
2278+     *
2279+     * @access protected
2280+     *
2281+     * @return string  A directory name which can be used for temp files.
2282+     *                 Returns false if one could not be found.
2283+     */
2284+    function _getTempDir()
2285+    {
2286+        $tmp_locations = array('/tmp', '/var/tmp', 'c:\WUTemp', 'c:\temp',
2287+                               'c:\windows\temp', 'c:\winnt\temp');
2288+
2289+        /* Try PHP's upload_tmp_dir directive. */
2290+        $tmp = ini_get('upload_tmp_dir');
2291+
2292+        /* Otherwise, try to determine the TMPDIR environment variable. */
2293+        if (!strlen($tmp)) {
2294+            $tmp = getenv('TMPDIR');
2295+        }
2296+
2297+        /* If we still cannot determine a value, then cycle through a list of
2298+         * preset possibilities. */
2299+        while (!strlen($tmp) && count($tmp_locations)) {
2300+            $tmp_check = array_shift($tmp_locations);
2301+            if (@is_dir($tmp_check)) {
2302+                $tmp = $tmp_check;
2303+            }
2304+        }
2305+
2306+        /* If it is still empty, we have failed, so return false; otherwise
2307+         * return the directory determined. */
2308+        return strlen($tmp) ? $tmp : false;
2309+    }
2310+
2311+    /**
2312+     * Checks a diff for validity.
2313+     *
2314+     * This is here only for debugging purposes.
2315+     */
2316+    function _check($from_lines, $to_lines)
2317+    {
2318+        if (serialize($from_lines) != serialize($this->getOriginal())) {
2319+            trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
2320+        }
2321+        if (serialize($to_lines) != serialize($this->getFinal())) {
2322+            trigger_error("Reconstructed final doesn't match", E_USER_ERROR);
2323+        }
2324+
2325+        $rev = $this->reverse();
2326+        if (serialize($to_lines) != serialize($rev->getOriginal())) {
2327+            trigger_error("Reversed original doesn't match", E_USER_ERROR);
2328+        }
2329+        if (serialize($from_lines) != serialize($rev->getFinal())) {
2330+            trigger_error("Reversed final doesn't match", E_USER_ERROR);
2331+        }
2332+
2333+        $prevtype = null;
2334+        foreach ($this->_edits as $edit) {
2335+            if ($prevtype == get_class($edit)) {
2336+                trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
2337+            }
2338+            $prevtype = get_class($edit);
2339+        }
2340+
2341+        return true;
2342+    }
2343+
2344+}
2345+
2346+/**
2347+ * @package Text_Diff
2348+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
2349+ */
2350+class Text_MappedDiff extends Text_Diff {
2351+
2352+    /**
2353+     * Computes a diff between sequences of strings.
2354+     *
2355+     * This can be used to compute things like case-insensitve diffs, or diffs
2356+     * which ignore changes in white-space.
2357+     *
2358+     * @param array $from_lines         An array of strings.
2359+     * @param array $to_lines           An array of strings.
2360+     * @param array $mapped_from_lines  This array should have the same size
2361+     *                                  number of elements as $from_lines.  The
2362+     *                                  elements in $mapped_from_lines and
2363+     *                                  $mapped_to_lines are what is actually
2364+     *                                  compared when computing the diff.
2365+     * @param array $mapped_to_lines    This array should have the same number
2366+     *                                  of elements as $to_lines.
2367+     */
2368+    function Text_MappedDiff($from_lines, $to_lines,
2369+                             $mapped_from_lines, $mapped_to_lines)
2370+    {
2371+        assert(count($from_lines) == count($mapped_from_lines));
2372+        assert(count($to_lines) == count($mapped_to_lines));
2373+
2374+        parent::Text_Diff($mapped_from_lines, $mapped_to_lines);
2375+
2376+        $xi = $yi = 0;
2377+        for ($i = 0; $i < count($this->_edits); $i++) {
2378+            $orig = &$this->_edits[$i]->orig;
2379+            if (is_array($orig)) {
2380+                $orig = array_slice($from_lines, $xi, count($orig));
2381+                $xi += count($orig);
2382+            }
2383+
2384+            $final = &$this->_edits[$i]->final;
2385+            if (is_array($final)) {
2386+                $final = array_slice($to_lines, $yi, count($final));
2387+                $yi += count($final);
2388+            }
2389+        }
2390+    }
2391+
2392+}
2393+
2394+/**
2395+ * @package Text_Diff
2396+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
2397+ *
2398+ * @access private
2399+ */
2400+class Text_Diff_Op {
2401+
2402+    var $orig;
2403+    var $final;
2404+
2405+    function &reverse()
2406+    {
2407+        trigger_error('Abstract method', E_USER_ERROR);
2408+    }
2409+
2410+    function norig()
2411+    {
2412+        return $this->orig ? count($this->orig) : 0;
2413+    }
2414+
2415+    function nfinal()
2416+    {
2417+        return $this->final ? count($this->final) : 0;
2418+    }
2419+
2420+}
2421+
2422+/**
2423+ * @package Text_Diff
2424+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
2425+ *
2426+ * @access private
2427+ */
2428+class Text_Diff_Op_copy extends Text_Diff_Op {
2429+
2430+    function Text_Diff_Op_copy($orig, $final = false)
2431+    {
2432+        if (!is_array($final)) {
2433+            $final = $orig;
2434+        }
2435+        $this->orig = $orig;
2436+        $this->final = $final;
2437+    }
2438+
2439+    function &reverse()
2440+    {
2441+        $reverse = &new Text_Diff_Op_copy($this->final, $this->orig);
2442+        return $reverse;
2443+    }
2444+
2445+}
2446+
2447+/**
2448+ * @package Text_Diff
2449+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
2450+ *
2451+ * @access private
2452+ */
2453+class Text_Diff_Op_delete extends Text_Diff_Op {
2454+
2455+    function Text_Diff_Op_delete($lines)
2456+    {
2457+        $this->orig = $lines;
2458+        $this->final = false;
2459+    }
2460+
2461+    function &reverse()
2462+    {
2463+        $reverse = &new Text_Diff_Op_add($this->orig);
2464+        return $reverse;
2465+    }
2466+
2467+}
2468+
2469+/**
2470+ * @package Text_Diff
2471+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
2472+ *
2473+ * @access private
2474+ */
2475+class Text_Diff_Op_add extends Text_Diff_Op {
2476+
2477+    function Text_Diff_Op_add($lines)
2478+    {
2479+        $this->final = $lines;
2480+        $this->orig = false;
2481+    }
2482+
2483+    function &reverse()
2484+    {
2485+        $reverse = &new Text_Diff_Op_delete($this->final);
2486+        return $reverse;
2487+    }
2488+
2489+}
2490+
2491+/**
2492+ * @package Text_Diff
2493+ * @author  Geoffrey T. Dairiki <dairiki@dairiki.org>
2494+ *
2495+ * @access private
2496+ */
2497+class Text_Diff_Op_change extends Text_Diff_Op {
2498+
2499+    function Text_Diff_Op_change($orig, $final)
2500+    {
2501+        $this->orig = $orig;
2502+        $this->final = $final;
2503+    }
2504+
2505+    function &reverse()
2506+    {
2507+        $reverse = &new Text_Diff_Op_change($this->final, $this->orig);
2508+        return $reverse;
2509+    }
2510+
2511+}
2512Index: wp-includes/pluggable.php
2513===================================================================
2514--- wp-includes/pluggable.php   (revision 7746)
2515+++ wp-includes/pluggable.php   (working copy)
2516@@ -1350,4 +1350,68 @@
2517 }
2518 endif;
2519 
2520+if ( !function_exists( 'wp_text_diff' ) ) :
2521+/**
2522+ * wp_text_diff() - compares two strings and outputs a human readable HTML representation of their difference
2523+ *
2524+ * Basically a wrapper for man diff(1)
2525+ *
2526+ * Must accept an optional third parameter, $args @see wp_parse_args()
2527+ *    (string) title: optional.  If present, titles the diff in a manner compatible with the output
2528+ *
2529+ * Must return the empty string if the two compared strings are found to be equivalent according to whatever metric
2530+ *
2531+ * @since 2.6
2532+ * @uses Text_Diff
2533+ * @uses WP_Text_Diff_Renderer_Table
2534+ *
2535+ * @param string $left_string "old" (left) version of string
2536+ * @param string $right_string "new" (right) version of string
2537+ * @param string|array $args @see wp_parse_args()
2538+ * @return string human readable HTML of string differences.  Empty string if strings are equivalent
2539+ */
2540+function wp_text_diff( $left_string, $right_string, $args = null ) {
2541+       $defaults = array( 'title' => '' );
2542+       $args = wp_parse_args( $args, $defaults );
2543+
2544+       // PEAR Text_Diff is lame; it includes things from include_path rather than it's own path.
2545+       // Not sure of the ramifications of disttributing modified code.
2546+       ini_set('include_path', '.' . PATH_SEPARATOR . ABSPATH . WPINC );
2547+
2548+       if ( !class_exists( 'WP_Text_Diff_Renderer_Table' ) )
2549+               require( ABSPATH . WPINC . '/wp-diff.php' );
2550+
2551+       // Normalize whitespace
2552+       $left_string  = trim($left_string);
2553+       $right_string = trim($right_string);
2554+       $left_string  = str_replace("\r", "\n", $left_string);
2555+       $right_string = str_replace("\r", "\n", $right_string);
2556+       $left_string  = preg_replace( array( '/\n+/', '/[ \t]+/' ), array( "\n", ' ' ), $left_string );
2557+       $right_string = preg_replace( array( '/\n+/', '/[ \t]+/' ), array( "\n", ' ' ), $right_string );
2558+       
2559+       $left_lines  = split("\n", $left_string);
2560+       $right_lines = split("\n", $right_string);
2561+
2562+       $text_diff = new Text_Diff($left_lines, $right_lines);
2563+       $renderer  = new WP_Text_Diff_Renderer_Table();
2564+       $diff = $renderer->render($text_diff);
2565+
2566+       ini_restore('include_path');
2567+
2568+       if ( !$diff )
2569+               return '';
2570+
2571+       $r  = "<table class='diff'>\n";
2572+       $r .= "<col class='ltype' /><col class='content' /><col class='ltype' /><col class='content' />";
2573+
2574+       if ( $args['title'] )
2575+               $r .= "<thead><tr><th colspan='4'>$args[title]</th></tr></thead>\n";
2576+
2577+       $r .= "<tbody>\n$diff\n</tbody>\n";
2578+       $r .= "</table>";
2579+
2580+       return $r;
2581+}
2582+endif;
2583+
2584 ?>
2585Index: wp-admin/admin-ajax.php
2586===================================================================
2587--- wp-admin/admin-ajax.php     (revision 7746)
2588+++ wp-admin/admin-ajax.php     (working copy)
2589@@ -460,6 +460,8 @@
2590        $x->send();
2591        break;
2592 case 'autosave' : // The name of this action is hardcoded in edit_post()
2593+       define( 'DOING_AUTOSAVE', true );
2594+
2595        $nonce_age = check_ajax_referer( 'autosave', 'autosavenonce');
2596        global $current_user;
2597 
2598Index: wp-admin/wp-admin.css
2599===================================================================
2600--- wp-admin/wp-admin.css       (revision 7746)
2601+++ wp-admin/wp-admin.css       (working copy)
2602@@ -894,6 +894,21 @@
2603        margin-right: 5px
2604 }
2605 
2606+.form-table pre {
2607+       padding: 8px;
2608+       margin: 0;
2609+       /* http://www.longren.org/2006/09/27/wrapping-text-inside-pre-tags/ */
2610+       white-space: pre-wrap; /* css-3 */
2611+       white-space: -moz-pre-wrap !important; /* Mozilla, since 1999 */
2612+       white-space: -pre-wrap; /* Opera 4-6 */
2613+       white-space: -o-pre-wrap; /* Opera 7 */
2614+       word-wrap: break-word; /* Internet Explorer 5.5+ */
2615+}
2616+
2617+table.form-table td .updated {
2618+       font-size: 13px;
2619+}
2620+
2621 /* Post Screen */
2622 
2623 #tagsdiv #newtag {
2624@@ -1464,3 +1479,27 @@
2625 .hide-if-no-js {
2626        display: none;
2627 }
2628+
2629+/* Diff */
2630+
2631+table.diff {
2632+       width: 100%;
2633+}
2634+
2635+table.diff col.content {
2636+       width: 50%;
2637+}
2638+
2639+table.diff tr {
2640+       background-color: transparent;
2641+}
2642+
2643+table.diff td, table.diff th {
2644+       padding: .5em;
2645+       font-family: monospace;
2646+       border: none;
2647+}
2648+
2649+table.diff .diff-deletedline del, table.diff .diff-addedline ins {
2650+       text-decoration: none;
2651+}
2652Index: wp-admin/post.php
2653===================================================================
2654--- wp-admin/post.php   (revision 7746)
2655+++ wp-admin/post.php   (working copy)
2656@@ -77,8 +77,8 @@
2657 
2658        if ( empty($post->ID) ) wp_die( __("You attempted to edit a post that doesn't exist. Perhaps it was deleted?") );
2659 
2660-       if ( 'page' == $post->post_type ) {
2661-               wp_redirect("page.php?action=edit&post=$post_ID");
2662+       if ( 'post' != $post->post_type ) {
2663+               wp_redirect( get_edit_post_link( $post->ID, 'url' ) );
2664                exit();
2665        }
2666 
2667Index: wp-admin/revision.php
2668===================================================================
2669--- wp-admin/revision.php       (revision 0)
2670+++ wp-admin/revision.php       (revision 0)
2671@@ -0,0 +1,132 @@
2672+<?php
2673+
2674+require_once('admin.php');
2675+
2676+$parent_file = 'edit.php';
2677+$submenu_file = 'edit.php';
2678+
2679+wp_reset_vars(array('revision', 'diff', 'restore'));
2680+
2681+$revision_id = absint($revision);
2682+$diff        = absint($diff);
2683+
2684+if ( $diff ) {
2685+       $restore = false;
2686+       $revision = get_post( $revision_id );
2687+       $post = 'revision' == $revision->post_type ? get_post( $revision->post_parent ) : get_post( $revision_id );
2688+       $left_revision = get_post( $diff );
2689+
2690+       // Don't allow reverse diffs?
2691+       if ( strtotime($revision->post_modified_gmt) < strtotime($left_revision->post_modified_gmt) ) {
2692+               wp_redirect( add_query_arg( array( 'diff' => $revision->ID, 'revision' => $diff ) ) );
2693+               exit;
2694+       }
2695+
2696+       $h2 = __( 'Compare Revisions of &#8220;%1$s&#8221;' );
2697+       $right = $revision->ID;
2698+       $left  = $left_revision->ID;
2699+
2700+       if (
2701+               // They're the same
2702+               $left_revision->ID == $revision->ID
2703+       ||
2704+               // They don't have a comment parent (and we're not comparing a revision to it's post)
2705+               ( $left_revision->ID != $revision->post_parent && $left_revision->post_parent != $revision->ID && $left_revision->post_parent != $revision->post_parent )
2706+       ||
2707+               // Neither is a revision
2708+               ( !wp_get_revision( $left_revision->ID ) && !wp_get_revision( $revision->ID ) )
2709+       ) {
2710+               wp_redirect( get_edit_post_link( $revision->ID, 'url' ) );
2711+               exit();
2712+       }
2713+} else {
2714+       $revision = wp_get_revision( $revision_id );
2715+       $post = get_post( $revision->post_parent );
2716+       $h2 = __( 'Post Revision for &#8220;%1$s&#8221; created on %2$s' );
2717+       $right = $post->ID;
2718+       $left  = $revision->ID;
2719+}
2720+
2721+if ( !$revision || !$post ) {
2722+       wp_redirect("edit.php");
2723+       exit();
2724+}
2725+
2726+if ( $restore && current_user_can( 'edit_post', $revision->post_parent ) ) {
2727+       check_admin_referer( "restore-post_$post->ID|$revision->ID" );
2728+       wp_restore_revision( $revision->ID );
2729+       wp_redirect( add_query_arg( array( 'message' => 5, 'revision' => $revision->ID ), get_edit_post_link( $post->ID, 'url' ) ) );
2730+       exit;
2731+}
2732+
2733+add_filter( '_wp_revision_field_post_author', 'get_author_name' );
2734+
2735+$title = __( 'Post Revision' );
2736+
2737+require_once( 'admin-header.php' );
2738+
2739+$post_title = '<a href="' . get_edit_post_link() . '">' . get_the_title() . '</a>';
2740+$revision_time = wp_post_revision_time( $revision );
2741+?>
2742+
2743+<div class="wrap">
2744+
2745+<h2 style="padding-right: 0"><?php printf( $h2, $post_title, $revision_time ); ?></h2>
2746+
2747+<table class="form-table ie-fixed">
2748+       <col class="th" />
2749+<?php if ( $diff ) : ?>
2750+
2751+<tr id="revision">
2752+       <th scope="row"></th>
2753+       <th scope="col" class="th-full"><?php printf( __('Older: %s'), wp_post_revision_time( $left_revision ) ); ?></td>
2754+       <th scope="col" class="th-full"><?php printf( __('Newer: %s'), wp_post_revision_time( $revision ) ); ?></td>
2755+</tr>
2756+
2757+<?php endif;
2758+
2759+// use get_post_to_edit ?
2760+$identical = true;
2761+foreach ( _wp_revision_fields() as $field => $field_title ) :
2762+       if ( !$diff )
2763+               add_filter( "_wp_revision_field_$field", 'htmlspecialchars' );
2764+       $content = apply_filters( "_wp_revision_field_$field", $revision->$field, $field );
2765+       if ( $diff ) {
2766+               $left_content = apply_filters( "_wp_revision_field_$field", $left_revision->$field, $field );
2767+               if ( !$content = wp_text_diff( $left_content, $content ) )
2768+                       continue;
2769+       }
2770+       $identical = false;
2771+       ?>
2772+
2773+       <tr id="revision-field-<?php echo $field; ?>"?>
2774+               <th scope="row"><?php echo wp_specialchars( $field_title ); ?></th>
2775+               <td colspan="2"><pre><?php echo $content; ?></pre></td>
2776+       </tr>
2777+
2778+       <?php
2779+
2780+endforeach;
2781+
2782+if ( $diff && $identical ) :
2783+
2784+       ?>
2785+
2786+       <tr><td colspan="3"><div class="updated"><p><?php _e( 'These revisions are identical' ); ?></p></div></td></tr>
2787+
2788+       <?php
2789+
2790+endif;
2791+
2792+?>
2793+
2794+</table>
2795+
2796+<br class="clear" />
2797+
2798+<h2><?php _e( 'Post Revisions' ); ?></h2>
2799+
2800+<?php
2801+       wp_list_post_revisions( $post, array( 'format' => 'form-table', 'exclude' => $revision->ID, 'parent' => true, 'right' => $right, 'left' => $left ) );
2802+
2803+       require_once( 'admin-footer.php' );
2804Index: wp-admin/edit-form-advanced.php
2805===================================================================
2806--- wp-admin/edit-form-advanced.php     (revision 7746)
2807+++ wp-admin/edit-form-advanced.php     (working copy)
2808@@ -6,6 +6,7 @@
2809 $messages[2] = __('Custom field updated.');
2810 $messages[3] = __('Custom field deleted.');
2811 $messages[4] = __('Post updated.');
2812+$messages[5] = sprintf( __('Post restored to revision from %s'), wp_post_revision_time( $_GET['revision'] ) );
2813 ?>
2814 <?php if (isset($_GET['message'])) : ?>
2815 <div id="message" class="updated fade"><p><?php echo $messages[$_GET['message']]; ?></p></div>
2816@@ -336,6 +337,15 @@
2817 </div>
2818 <?php endif; ?>
2819 
2820+<?php if ( isset($post_ID) && 0 < $post_ID && wp_get_post_revisions( $post_ID ) ) : ?>
2821+<div id="revisionsdiv" class="postbox <?php echo postbox_classes('revisionsdiv', 'post'); ?>">
2822+<h3><?php _e('Post Revisions'); ?></h3>
2823+<div class="inside">
2824+<?php wp_list_post_revisions(); ?>
2825+</div>
2826+</div>
2827+<?php endif; ?>
2828+
2829 <?php do_meta_boxes('post', 'advanced', $post); ?>
2830 
2831 <?php do_action('dbx_post_sidebar'); ?>
2832Index: wp-admin/css/ie.css
2833===================================================================
2834--- wp-admin/css/ie.css (revision 7746)
2835+++ wp-admin/css/ie.css (working copy)
2836@@ -111,7 +111,10 @@
2837 .tablenav-pages {
2838        display: block;
2839        margin-top: -3px;
2840+}
2841 
2842+table.ie-fixed {
2843+       table-layout: fixed;
2844 }
2845 
2846 #post-search .button, #widget-search .button {
2847Index: wp-admin/css/colors-fresh.css
2848===================================================================
2849--- wp-admin/css/colors-fresh.css       (revision 7746)
2850+++ wp-admin/css/colors-fresh.css       (working copy)
2851@@ -2,7 +2,7 @@
2852        border-color: #999;
2853 }
2854 
2855-body   {
2856+body, .form-table pre {
2857        background-color: #fff;
2858        color: #333;
2859 }
2860@@ -684,3 +684,18 @@
2861        background-color: #ddd;
2862        color: #333;
2863 }
2864+
2865+/* Diff */
2866+
2867+table.diff .diff-deletedline {
2868+       background-color: #ffdddd;
2869+}
2870+table.diff .diff-deletedline del {
2871+       background-color: #ff9999;
2872+}
2873+table.diff .diff-addedline {
2874+       background-color: #ddffdd;
2875+}
2876+table.diff .diff-addedline ins {
2877+       background-color: #99ff99;
2878+}
2879Index: wp-admin/css/colors-classic.css
2880===================================================================
2881--- wp-admin/css/colors-classic.css     (revision 7746)
2882+++ wp-admin/css/colors-classic.css     (working copy)
2883@@ -2,7 +2,7 @@
2884        border-color: #999;
2885 }
2886 
2887-body   {
2888+body, .form-table pre {
2889        background-color: #fff;
2890        color: #333;
2891 }
2892@@ -713,3 +713,18 @@
2893        background-color: #ddd;
2894        color: #333;
2895 }
2896+
2897+/* Diff */
2898+
2899+table.diff .diff-deletedline {
2900+       background-color: #ffdddd;
2901+}
2902+table.diff .diff-deletedline del {
2903+       background-color: #ff9999;
2904+}
2905+table.diff .diff-addedline {
2906+       background-color: #ddffdd;
2907+}
2908+table.diff .diff-addedline ins {
2909+       background-color: #99ff99;
2910+}
2911Index: wp-admin/page.php
2912===================================================================
2913--- wp-admin/page.php   (revision 7746)
2914+++ wp-admin/page.php   (working copy)
2915@@ -70,8 +70,8 @@
2916 
2917        if ( empty($post->ID) ) wp_die( __("You attempted to edit a page that doesn't exist. Perhaps it was deleted?") );
2918 
2919-       if ( 'post' == $post->post_type ) {
2920-               wp_redirect("post.php?action=edit&post=$post_ID");
2921+       if ( 'page' != $post->post_type ) {
2922+               wp_redirect( get_edit_post_link( $post_ID, 'url' ) );
2923                exit();
2924        }
2925