added diff headers for HTML diff
[lhc/web/wiklou.git] / includes / DifferenceEngine.php
1 <?php
2 /**
3 * @defgroup DifferenceEngine DifferenceEngine
4 */
5
6 /**
7 * Constant to indicate diff cache compatibility.
8 * Bump this when changing the diff formatting in a way that
9 * fixes important bugs or such to force cached diff views to
10 * clear.
11 */
12 define( 'MW_DIFF_VERSION', '1.11a' );
13
14 /**
15 * @todo document
16 * @ingroup DifferenceEngine
17 */
18 class DifferenceEngine {
19 /**#@+
20 * @private
21 */
22 var $mOldid, $mNewid, $mTitle;
23 var $mOldtitle, $mNewtitle, $mPagetitle;
24 var $mOldtext, $mNewtext;
25 var $mOldPage, $mNewPage;
26 var $mRcidMarkPatrolled;
27 var $mOldRev, $mNewRev;
28 var $mRevisionsLoaded = false; // Have the revisions been loaded
29 var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
30 /**#@-*/
31
32 /**
33 * Constructor
34 * @param $titleObj Title object that the diff is associated with
35 * @param $old Integer: old ID we want to show and diff with.
36 * @param $new String: either 'prev' or 'next'.
37 * @param $rcid Integer: ??? FIXME (default 0)
38 * @param $refreshCache boolean If set, refreshes the diff cache
39 */
40 function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false ) {
41 $this->mTitle = $titleObj;
42 wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
43
44 if ( 'prev' === $new ) {
45 # Show diff between revision $old and the previous one.
46 # Get previous one from DB.
47 #
48 $this->mNewid = intval($old);
49
50 $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
51
52 } elseif ( 'next' === $new ) {
53 # Show diff between revision $old and the previous one.
54 # Get previous one from DB.
55 #
56 $this->mOldid = intval($old);
57 $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
58 if ( false === $this->mNewid ) {
59 # if no result, NewId points to the newest old revision. The only newer
60 # revision is cur, which is "0".
61 $this->mNewid = 0;
62 }
63
64 } else {
65 $this->mOldid = intval($old);
66 $this->mNewid = intval($new);
67 }
68 $this->mRcidMarkPatrolled = intval($rcid); # force it to be an integer
69 $this->mRefreshCache = $refreshCache;
70 }
71
72 function getTitle() {
73 return $this->mTitle;
74 }
75
76 function showDiffPage( $diffOnly = false ) {
77 global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol, $wgEnableHtmlDiff;
78 wfProfileIn( __METHOD__ );
79
80
81 # If external diffs are enabled both globally and for the user,
82 # we'll use the application/x-external-editor interface to call
83 # an external diff tool like kompare, kdiff3, etc.
84 if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
85 global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
86 $wgOut->disable();
87 header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding );
88 $url1=$this->mTitle->getFullURL("action=raw&oldid=".$this->mOldid);
89 $url2=$this->mTitle->getFullURL("action=raw&oldid=".$this->mNewid);
90 $special=$wgLang->getNsText(NS_SPECIAL);
91 $control=<<<CONTROL
92 [Process]
93 Type=Diff text
94 Engine=MediaWiki
95 Script={$wgServer}{$wgScript}
96 Special namespace={$special}
97
98 [File]
99 Extension=wiki
100 URL=$url1
101
102 [File 2]
103 Extension=wiki
104 URL=$url2
105 CONTROL;
106 echo($control);
107 return;
108 }
109
110 $wgOut->setArticleFlag( false );
111 if ( ! $this->loadRevisionData() ) {
112 $t = $this->mTitle->getPrefixedText();
113 $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
114 $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
115 $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
116 wfProfileOut( __METHOD__ );
117 return;
118 }
119
120 wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
121
122 if ( $this->mNewRev->isCurrent() ) {
123 $wgOut->setArticleFlag( true );
124 }
125
126 # mOldid is false if the difference engine is called with a "vague" query for
127 # a diff between a version V and its previous version V' AND the version V
128 # is the first version of that article. In that case, V' does not exist.
129 if ( $this->mOldid === false ) {
130 $this->showFirstRevision();
131 $this->renderNewRevision(); // should we respect $diffOnly here or not?
132 wfProfileOut( __METHOD__ );
133 return;
134 }
135
136 $wgOut->suppressQuickbar();
137
138 $oldTitle = $this->mOldPage->getPrefixedText();
139 $newTitle = $this->mNewPage->getPrefixedText();
140 if( $oldTitle == $newTitle ) {
141 $wgOut->setPageTitle( $newTitle );
142 } else {
143 $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
144 }
145 $wgOut->setSubtitle( wfMsg( 'difference' ) );
146 $wgOut->setRobotPolicy( 'noindex,nofollow' );
147
148 if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) {
149 $wgOut->loginToUse();
150 $wgOut->output();
151 wfProfileOut( __METHOD__ );
152 exit;
153 }
154
155 $sk = $wgUser->getSkin();
156
157 // Check if page is editable
158 $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
159 if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) {
160 $rollback = '&nbsp;&nbsp;&nbsp;' . $sk->generateRollback( $this->mNewRev );
161 } else {
162 $rollback = '';
163 }
164
165 // Prepare a change patrol link, if applicable
166 if( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) {
167 // If we've been given an explicit change identifier, use it; saves time
168 if( $this->mRcidMarkPatrolled ) {
169 $rcid = $this->mRcidMarkPatrolled;
170 } else {
171 // Look for an unpatrolled change corresponding to this diff
172 $db = wfGetDB( DB_SLAVE );
173 $change = RecentChange::newFromConds(
174 array(
175 // Add redundant user,timestamp condition so we can use the existing index
176 'rc_user_text' => $this->mNewRev->getRawUserText(),
177 'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
178 'rc_this_oldid' => $this->mNewid,
179 'rc_last_oldid' => $this->mOldid,
180 'rc_patrolled' => 0
181 ),
182 __METHOD__
183 );
184 if( $change instanceof RecentChange ) {
185 $rcid = $change->mAttribs['rc_id'];
186 } else {
187 // None found
188 $rcid = 0;
189 }
190 }
191 // Build the link
192 if( $rcid ) {
193 $patrol = ' <span class="patrollink">[' . $sk->makeKnownLinkObj(
194 $this->mTitle,
195 wfMsgHtml( 'markaspatrolleddiff' ),
196 "action=markpatrolled&rcid={$rcid}"
197 ) . ']</span>';
198 } else {
199 $patrol = '';
200 }
201 } else {
202 $patrol = '';
203 }
204
205 $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ),
206 'diff=prev&oldid='.$this->mOldid, '', '', 'id="differences-prevlink"' );
207 if ( $this->mNewRev->isCurrent() ) {
208 $nextlink = '&nbsp;';
209 } else {
210 $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
211 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' );
212 }
213
214 $oldminor = '';
215 $newminor = '';
216
217 if ($this->mOldRev->mMinorEdit == 1) {
218 $oldminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' ';
219 }
220
221 if ($this->mNewRev->mMinorEdit == 1) {
222 $newminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' ';
223 }
224
225 $rdel = ''; $ldel = '';
226 if( $wgUser->isAllowed( 'deleterevision' ) ) {
227 $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
228 if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) {
229 // If revision was hidden from sysops
230 $ldel = wfMsgHtml('rev-delundel');
231 } else {
232 $ldel = $sk->makeKnownLinkObj( $revdel,
233 wfMsgHtml('rev-delundel'),
234 'target=' . urlencode( $this->mOldRev->mTitle->getPrefixedDbkey() ) .
235 '&oldid=' . urlencode( $this->mOldRev->getId() ) );
236 // Bolden oversighted content
237 if( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) )
238 $ldel = "<strong>$ldel</strong>";
239 }
240 $ldel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$ldel</small>)</tt> ";
241 // We don't currently handle well changing the top revision's settings
242 if( $this->mNewRev->isCurrent() ) {
243 // If revision was hidden from sysops
244 $rdel = wfMsgHtml('rev-delundel');
245 } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) {
246 // If revision was hidden from sysops
247 $rdel = wfMsgHtml('rev-delundel');
248 } else {
249 $rdel = $sk->makeKnownLinkObj( $revdel,
250 wfMsgHtml('rev-delundel'),
251 'target=' . urlencode( $this->mNewRev->mTitle->getPrefixedDbkey() ) .
252 '&oldid=' . urlencode( $this->mNewRev->getId() ) );
253 // Bolden oversighted content
254 if( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) )
255 $rdel = "<strong>$rdel</strong>";
256 }
257 $rdel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$rdel</small>)</tt> ";
258 }
259
260 $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
261 '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev, true ) . "</div>" .
262 '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, true ) . $ldel . "</div>" .
263 '<div id="mw-diff-otitle4">' . $prevlink .'</div>';
264 $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
265 '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, true ) . " $rollback</div>" .
266 '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, true ) . $rdel . "</div>" .
267 '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
268
269 if( $wgEnableHtmlDiff ) {
270 $multi = $this->getMultiNotice();
271 $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
272 $this->renderHtmlDiff();
273 } else {
274
275 $this->showDiff( $oldHeader, $newHeader );
276
277 if( !$diffOnly ) {
278 $this->renderNewRevision();
279 }
280 }
281 wfProfileOut( __METHOD__ );
282 }
283
284 /**
285 * Show the new revision of the page.
286 */
287 function renderNewRevision() {
288 global $wgOut;
289 wfProfileIn( __METHOD__ );
290
291 $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
292 #add deleted rev tag if needed
293 if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
294 $wgOut->addWikiMsg( 'rev-deleted-text-permission' );
295 } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
296 $wgOut->addWikiMsg( 'rev-deleted-text-view' );
297 }
298
299 if( !$this->mNewRev->isCurrent() ) {
300 $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
301 }
302
303 $this->loadNewText();
304 if( is_object( $this->mNewRev ) ) {
305 $wgOut->setRevisionId( $this->mNewRev->getId() );
306 }
307
308 if ($this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage()) {
309 // Stolen from Article::view --AG 2007-10-11
310
311 // Give hooks a chance to customise the output
312 if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
313 // Wrap the whole lot in a <pre> and don't parse
314 $m = array();
315 preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
316 $wgOut->addHtml( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
317 $wgOut->addHtml( htmlspecialchars( $this->mNewtext ) );
318 $wgOut->addHtml( "\n</pre>\n" );
319 }
320 } else
321 $wgOut->addWikiTextTidy( $this->mNewtext );
322
323 if( !$this->mNewRev->isCurrent() ) {
324 $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
325 }
326
327 wfProfileOut( __METHOD__ );
328 }
329
330
331 function renderHtmlDiff() {
332 global $wgOut, $wgTitle, $wgParser;
333 wfProfileIn( __METHOD__ );
334
335 $this->showDiffStyle();
336
337 $wgOut->addHTML( "<hr /><h2>HTML diff</h2>\n" );
338 #add deleted rev tag if needed
339 if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
340 $wgOut->addWikiMsg( 'rev-deleted-text-permission' );
341 } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
342 $wgOut->addWikiMsg( 'rev-deleted-text-view' );
343 }
344
345 if( !$this->mNewRev->isCurrent() ) {
346 $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
347 }
348
349 $this->loadText();
350
351 // Old revision
352 if( is_object( $this->mOldRev ) ) {
353 $wgOut->setRevisionId( $this->mOldRev->getId() );
354 }
355
356 $popts = $wgOut->parserOptions();
357 $oldTidy = $popts->setTidy( true );
358
359 $parserOutput = $wgParser->parse( $this->mOldtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
360 $popts->setTidy( $oldTidy );
361
362 //only for new?
363 //$wgOut->addParserOutputNoText( $parserOutput );
364 $oldHtml = $parserOutput->getText();
365 wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$oldHtml ) );
366
367 // New revision
368 if( is_object( $this->mNewRev ) ) {
369 $wgOut->setRevisionId( $this->mNewRev->getId() );
370 }
371
372 $popts = $wgOut->parserOptions();
373 $oldTidy = $popts->setTidy( true );
374
375 $parserOutput = $wgParser->parse( $this->mNewtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
376 $popts->setTidy( $oldTidy );
377
378 $wgOut->addParserOutputNoText( $parserOutput );
379 $newHtml = $parserOutput->getText();
380 wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$newHtml ) );
381
382 unset($parserOutput, $popts);
383
384 $differ = new HTMLDiffer(new DelegatingContentHandler($wgOut));
385 $differ->htmlDiff($oldHtml, $newHtml);
386
387 wfProfileOut( __METHOD__ );
388 }
389
390 /**
391 * Show the first revision of an article. Uses normal diff headers in
392 * contrast to normal "old revision" display style.
393 */
394 function showFirstRevision() {
395 global $wgOut, $wgUser;
396 wfProfileIn( __METHOD__ );
397
398 # Get article text from the DB
399 #
400 if ( ! $this->loadNewText() ) {
401 $t = $this->mTitle->getPrefixedText();
402 $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
403 $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
404 $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
405 wfProfileOut( __METHOD__ );
406 return;
407 }
408 if ( $this->mNewRev->isCurrent() ) {
409 $wgOut->setArticleFlag( true );
410 }
411
412 # Check if user is allowed to look at this page. If not, bail out.
413 #
414 if ( !( $this->mTitle->userCanRead() ) ) {
415 $wgOut->loginToUse();
416 $wgOut->output();
417 wfProfileOut( __METHOD__ );
418 exit;
419 }
420
421 # Prepare the header box
422 #
423 $sk = $wgUser->getSkin();
424
425 $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid, '', '', 'id="differences-nextlink"' );
426 $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" .
427 $sk->revUserTools( $this->mNewRev ) . "<br />" .
428 $sk->revComment( $this->mNewRev ) . "<br />" .
429 $nextlink . "</div>\n";
430
431 $wgOut->addHTML( $header );
432
433 $wgOut->setSubtitle( wfMsg( 'difference' ) );
434 $wgOut->setRobotPolicy( 'noindex,nofollow' );
435
436 wfProfileOut( __METHOD__ );
437 }
438
439 /**
440 * Get the diff text, send it to $wgOut
441 * Returns false if the diff could not be generated, otherwise returns true
442 */
443 function showDiff( $otitle, $ntitle ) {
444 global $wgOut;
445 $diff = $this->getDiff( $otitle, $ntitle );
446 if ( $diff === false ) {
447 $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
448 return false;
449 } else {
450 $this->showDiffStyle();
451 $wgOut->addHTML( $diff );
452 return true;
453 }
454 }
455
456 /**
457 * Add style sheets and supporting JS for diff display.
458 */
459 function showDiffStyle() {
460 global $wgStylePath, $wgStyleVersion, $wgOut;
461 $wgOut->addStyle( 'common/diff.css' );
462
463 // JS is needed to detect old versions of Mozilla to work around an annoyance bug.
464 $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" );
465 }
466
467 /**
468 * Get complete diff table, including header
469 *
470 * @param Title $otitle Old title
471 * @param Title $ntitle New title
472 * @return mixed
473 */
474 function getDiff( $otitle, $ntitle ) {
475 $body = $this->getDiffBody();
476 if ( $body === false ) {
477 return false;
478 } else {
479 $multi = $this->getMultiNotice();
480 return $this->addHeader( $body, $otitle, $ntitle, $multi );
481 }
482 }
483
484 /**
485 * Get the diff table body, without header
486 *
487 * @return mixed
488 */
489 function getDiffBody() {
490 global $wgMemc;
491 wfProfileIn( __METHOD__ );
492 // Check if the diff should be hidden from this user
493 if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
494 return '';
495 } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
496 return '';
497 }
498 // Cacheable?
499 $key = false;
500 if ( $this->mOldid && $this->mNewid ) {
501 $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
502 // Try cache
503 if ( !$this->mRefreshCache ) {
504 $difftext = $wgMemc->get( $key );
505 if ( $difftext ) {
506 wfIncrStats( 'diff_cache_hit' );
507 $difftext = $this->localiseLineNumbers( $difftext );
508 $difftext .= "\n<!-- diff cache key $key -->\n";
509 wfProfileOut( __METHOD__ );
510 return $difftext;
511 }
512 } // don't try to load but save the result
513 }
514
515 // Loadtext is permission safe, this just clears out the diff
516 if ( !$this->loadText() ) {
517 wfProfileOut( __METHOD__ );
518 return false;
519 }
520
521 $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
522
523 // Save to cache for 7 days
524 if ( $key !== false && $difftext !== false ) {
525 wfIncrStats( 'diff_cache_miss' );
526 $wgMemc->set( $key, $difftext, 7*86400 );
527 } else {
528 wfIncrStats( 'diff_uncacheable' );
529 }
530 // Replace line numbers with the text in the user's language
531 if ( $difftext !== false ) {
532 $difftext = $this->localiseLineNumbers( $difftext );
533 }
534 wfProfileOut( __METHOD__ );
535 return $difftext;
536 }
537
538 /**
539 * Generate a diff, no caching
540 * $otext and $ntext must be already segmented
541 */
542 function generateDiffBody( $otext, $ntext ) {
543 global $wgExternalDiffEngine, $wgContLang;
544
545 $otext = str_replace( "\r\n", "\n", $otext );
546 $ntext = str_replace( "\r\n", "\n", $ntext );
547
548 if ( $wgExternalDiffEngine == 'wikidiff' ) {
549 # For historical reasons, external diff engine expects
550 # input text to be HTML-escaped already
551 $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
552 $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
553 if( !function_exists( 'wikidiff_do_diff' ) ) {
554 dl('php_wikidiff.so');
555 }
556 return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
557 $this->debug( 'wikidiff1' );
558 }
559
560 if ( $wgExternalDiffEngine == 'wikidiff2' ) {
561 # Better external diff engine, the 2 may some day be dropped
562 # This one does the escaping and segmenting itself
563 if ( !function_exists( 'wikidiff2_do_diff' ) ) {
564 wfProfileIn( __METHOD__ . "-dl" );
565 @dl('php_wikidiff2.so');
566 wfProfileOut( __METHOD__ . "-dl" );
567 }
568 if ( function_exists( 'wikidiff2_do_diff' ) ) {
569 wfProfileIn( 'wikidiff2_do_diff' );
570 $text = wikidiff2_do_diff( $otext, $ntext, 2 );
571 $text .= $this->debug( 'wikidiff2' );
572 wfProfileOut( 'wikidiff2_do_diff' );
573 return $text;
574 }
575 }
576 if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
577 # Diff via the shell
578 global $wgTmpDirectory;
579 $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
580 $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
581
582 $tempFile1 = fopen( $tempName1, "w" );
583 if ( !$tempFile1 ) {
584 wfProfileOut( __METHOD__ );
585 return false;
586 }
587 $tempFile2 = fopen( $tempName2, "w" );
588 if ( !$tempFile2 ) {
589 wfProfileOut( __METHOD__ );
590 return false;
591 }
592 fwrite( $tempFile1, $otext );
593 fwrite( $tempFile2, $ntext );
594 fclose( $tempFile1 );
595 fclose( $tempFile2 );
596 $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
597 wfProfileIn( __METHOD__ . "-shellexec" );
598 $difftext = wfShellExec( $cmd );
599 $difftext .= $this->debug( "external $wgExternalDiffEngine" );
600 wfProfileOut( __METHOD__ . "-shellexec" );
601 unlink( $tempName1 );
602 unlink( $tempName2 );
603 return $difftext;
604 }
605
606 # Native PHP diff
607 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
608 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
609 $diffs = new Diff( $ota, $nta );
610 $formatter = new TableDiffFormatter();
611 return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
612 $this->debug();
613 }
614
615 /**
616 * Generate a debug comment indicating diff generating time,
617 * server node, and generator backend.
618 */
619 protected function debug( $generator="internal" ) {
620 global $wgShowHostnames, $wgNodeName;
621 $data = array( $generator );
622 if( $wgShowHostnames ) {
623 $data[] = $wgNodeName;
624 }
625 $data[] = wfTimestamp( TS_DB );
626 return "<!-- diff generator: " .
627 implode( " ",
628 array_map(
629 "htmlspecialchars",
630 $data ) ) .
631 " -->\n";
632 }
633
634 /**
635 * Replace line numbers with the text in the user's language
636 */
637 function localiseLineNumbers( $text ) {
638 return preg_replace_callback( '/<!--LINE (\d+)-->/',
639 array( &$this, 'localiseLineNumbersCb' ), $text );
640 }
641
642 function localiseLineNumbersCb( $matches ) {
643 global $wgLang;
644 return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) );
645 }
646
647
648 /**
649 * If there are revisions between the ones being compared, return a note saying so.
650 */
651 function getMultiNotice() {
652 if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
653 return '';
654
655 if( !$this->mOldPage->equals( $this->mNewPage ) ) {
656 // Comparing two different pages? Count would be meaningless.
657 return '';
658 }
659
660 $oldid = $this->mOldRev->getId();
661 $newid = $this->mNewRev->getId();
662 if ( $oldid > $newid ) {
663 $tmp = $oldid; $oldid = $newid; $newid = $tmp;
664 }
665
666 $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
667 if ( !$n )
668 return '';
669
670 return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
671 }
672
673
674 /**
675 * Add the header to a diff body
676 */
677 static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
678 $header = "
679 <table class='diff'>
680 <col class='diff-marker' />
681 <col class='diff-content' />
682 <col class='diff-marker' />
683 <col class='diff-content' />
684 <tr valign='top'>
685 <td colspan='2' class='diff-otitle'>{$otitle}</td>
686 <td colspan='2' class='diff-ntitle'>{$ntitle}</td>
687 </tr>
688 ";
689
690 if ( $multi != '' )
691 $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
692
693 return $header . $diff . "</table>";
694 }
695
696 /**
697 * Use specified text instead of loading from the database
698 */
699 function setText( $oldText, $newText ) {
700 $this->mOldtext = $oldText;
701 $this->mNewtext = $newText;
702 $this->mTextLoaded = 2;
703 }
704
705 /**
706 * Load revision metadata for the specified articles. If newid is 0, then compare
707 * the old article in oldid to the current article; if oldid is 0, then
708 * compare the current article to the immediately previous one (ignoring the
709 * value of newid).
710 *
711 * If oldid is false, leave the corresponding revision object set
712 * to false. This is impossible via ordinary user input, and is provided for
713 * API convenience.
714 */
715 function loadRevisionData() {
716 global $wgLang;
717 if ( $this->mRevisionsLoaded ) {
718 return true;
719 } else {
720 // Whether it succeeds or fails, we don't want to try again
721 $this->mRevisionsLoaded = true;
722 }
723
724 // Load the new revision object
725 $this->mNewRev = $this->mNewid
726 ? Revision::newFromId( $this->mNewid )
727 : Revision::newFromTitle( $this->mTitle );
728 if( !$this->mNewRev instanceof Revision )
729 return false;
730
731 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
732 $this->mNewid = $this->mNewRev->getId();
733
734 // Check if page is editable
735 $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
736
737 // Set assorted variables
738 $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
739 $this->mNewPage = $this->mNewRev->getTitle();
740 if( $this->mNewRev->isCurrent() ) {
741 $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
742 $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) );
743 $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' );
744
745 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)";
746 $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
747
748 } else {
749 $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
750 $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid );
751 $this->mPagetitle = wfMsgHTML( 'revisionasof', $timestamp );
752
753 $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
754 $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
755 }
756 if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
757 $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
758 } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
759 $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>';
760 }
761
762 // Load the old revision object
763 $this->mOldRev = false;
764 if( $this->mOldid ) {
765 $this->mOldRev = Revision::newFromId( $this->mOldid );
766 } elseif ( $this->mOldid === 0 ) {
767 $rev = $this->mNewRev->getPrevious();
768 if( $rev ) {
769 $this->mOldid = $rev->getId();
770 $this->mOldRev = $rev;
771 } else {
772 // No previous revision; mark to show as first-version only.
773 $this->mOldid = false;
774 $this->mOldRev = false;
775 }
776 }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
777
778 if( is_null( $this->mOldRev ) ) {
779 return false;
780 }
781
782 if ( $this->mOldRev ) {
783 $this->mOldPage = $this->mOldRev->getTitle();
784
785 $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
786 $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid );
787 $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid );
788 $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) );
789
790 $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
791 . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
792 // Add an "undo" link
793 $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
794 if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
795 $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
796 }
797
798 if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
799 $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>';
800 } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
801 $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>';
802 }
803 }
804
805 return true;
806 }
807
808 /**
809 * Load the text of the revisions, as well as revision data.
810 */
811 function loadText() {
812 if ( $this->mTextLoaded == 2 ) {
813 return true;
814 } else {
815 // Whether it succeeds or fails, we don't want to try again
816 $this->mTextLoaded = 2;
817 }
818
819 if ( !$this->loadRevisionData() ) {
820 return false;
821 }
822 if ( $this->mOldRev ) {
823 $this->mOldtext = $this->mOldRev->revText();
824 if ( $this->mOldtext === false ) {
825 return false;
826 }
827 }
828 if ( $this->mNewRev ) {
829 $this->mNewtext = $this->mNewRev->revText();
830 if ( $this->mNewtext === false ) {
831 return false;
832 }
833 }
834 return true;
835 }
836
837 /**
838 * Load the text of the new revision, not the old one
839 */
840 function loadNewText() {
841 if ( $this->mTextLoaded >= 1 ) {
842 return true;
843 } else {
844 $this->mTextLoaded = 1;
845 }
846 if ( !$this->loadRevisionData() ) {
847 return false;
848 }
849 $this->mNewtext = $this->mNewRev->getText();
850 return true;
851 }
852
853
854 }
855
856 // A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
857 //
858 // Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
859 // You may copy this code freely under the conditions of the GPL.
860 //
861
862 define('USE_ASSERTS', function_exists('assert'));
863
864 /**
865 * @todo document
866 * @private
867 * @ingroup DifferenceEngine
868 */
869 class _DiffOp {
870 var $type;
871 var $orig;
872 var $closing;
873
874 function reverse() {
875 trigger_error('pure virtual', E_USER_ERROR);
876 }
877
878 function norig() {
879 return $this->orig ? sizeof($this->orig) : 0;
880 }
881
882 function nclosing() {
883 return $this->closing ? sizeof($this->closing) : 0;
884 }
885 }
886
887 /**
888 * @todo document
889 * @private
890 * @ingroup DifferenceEngine
891 */
892 class _DiffOp_Copy extends _DiffOp {
893 var $type = 'copy';
894
895 function _DiffOp_Copy ($orig, $closing = false) {
896 if (!is_array($closing))
897 $closing = $orig;
898 $this->orig = $orig;
899 $this->closing = $closing;
900 }
901
902 function reverse() {
903 return new _DiffOp_Copy($this->closing, $this->orig);
904 }
905 }
906
907 /**
908 * @todo document
909 * @private
910 * @ingroup DifferenceEngine
911 */
912 class _DiffOp_Delete extends _DiffOp {
913 var $type = 'delete';
914
915 function _DiffOp_Delete ($lines) {
916 $this->orig = $lines;
917 $this->closing = false;
918 }
919
920 function reverse() {
921 return new _DiffOp_Add($this->orig);
922 }
923 }
924
925 /**
926 * @todo document
927 * @private
928 * @ingroup DifferenceEngine
929 */
930 class _DiffOp_Add extends _DiffOp {
931 var $type = 'add';
932
933 function _DiffOp_Add ($lines) {
934 $this->closing = $lines;
935 $this->orig = false;
936 }
937
938 function reverse() {
939 return new _DiffOp_Delete($this->closing);
940 }
941 }
942
943 /**
944 * @todo document
945 * @private
946 * @ingroup DifferenceEngine
947 */
948 class _DiffOp_Change extends _DiffOp {
949 var $type = 'change';
950
951 function _DiffOp_Change ($orig, $closing) {
952 $this->orig = $orig;
953 $this->closing = $closing;
954 }
955
956 function reverse() {
957 return new _DiffOp_Change($this->closing, $this->orig);
958 }
959 }
960
961 /**
962 * Class used internally by Diff to actually compute the diffs.
963 *
964 * The algorithm used here is mostly lifted from the perl module
965 * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
966 * http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
967 *
968 * More ideas are taken from:
969 * http://www.ics.uci.edu/~eppstein/161/960229.html
970 *
971 * Some ideas are (and a bit of code) are from from analyze.c, from GNU
972 * diffutils-2.7, which can be found at:
973 * ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
974 *
975 * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
976 * are my own.
977 *
978 * Line length limits for robustness added by Tim Starling, 2005-08-31
979 * Alternative implementation added by Guy Van den Broeck, 2008-07-30
980 *
981 * @author Geoffrey T. Dairiki, Tim Starling, Guy Van den Broeck
982 * @private
983 * @ingroup DifferenceEngine
984 */
985 class _DiffEngine {
986
987 const MAX_XREF_LENGTH = 10000;
988
989 function diff ($from_lines, $to_lines){
990 wfProfileIn( __METHOD__ );
991
992 // Diff and store locally
993 $this->diff_local($from_lines, $to_lines);
994
995 // Merge edits when possible
996 $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
997 $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
998
999 // Compute the edit operations.
1000 $n_from = sizeof($from_lines);
1001 $n_to = sizeof($to_lines);
1002
1003 $edits = array();
1004 $xi = $yi = 0;
1005 while ($xi < $n_from || $yi < $n_to) {
1006 USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
1007 USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
1008
1009 // Skip matching "snake".
1010 $copy = array();
1011 while ( $xi < $n_from && $yi < $n_to
1012 && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
1013 $copy[] = $from_lines[$xi++];
1014 ++$yi;
1015 }
1016 if ($copy)
1017 $edits[] = new _DiffOp_Copy($copy);
1018
1019 // Find deletes & adds.
1020 $delete = array();
1021 while ($xi < $n_from && $this->xchanged[$xi])
1022 $delete[] = $from_lines[$xi++];
1023
1024 $add = array();
1025 while ($yi < $n_to && $this->ychanged[$yi])
1026 $add[] = $to_lines[$yi++];
1027
1028 if ($delete && $add)
1029 $edits[] = new _DiffOp_Change($delete, $add);
1030 elseif ($delete)
1031 $edits[] = new _DiffOp_Delete($delete);
1032 elseif ($add)
1033 $edits[] = new _DiffOp_Add($add);
1034 }
1035 wfProfileOut( __METHOD__ );
1036 return $edits;
1037 }
1038
1039 function diff_local ($from_lines, $to_lines) {
1040 global $wgExternalDiffEngine;
1041 wfProfileIn( __METHOD__);
1042
1043 if($wgExternalDiffEngine == 'wikidiff3'){
1044 // wikidiff3
1045 $wikidiff3 = new WikiDiff3();
1046 $wikidiff3->diff($from_lines, $to_lines);
1047 $this->xchanged = $wikidiff3->removed;
1048 $this->ychanged = $wikidiff3->added;
1049 unset($wikidiff3);
1050 }else{
1051 // old diff
1052 $n_from = sizeof($from_lines);
1053 $n_to = sizeof($to_lines);
1054 $this->xchanged = $this->ychanged = array();
1055 $this->xv = $this->yv = array();
1056 $this->xind = $this->yind = array();
1057 unset($this->seq);
1058 unset($this->in_seq);
1059 unset($this->lcs);
1060
1061 // Skip leading common lines.
1062 for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
1063 if ($from_lines[$skip] !== $to_lines[$skip])
1064 break;
1065 $this->xchanged[$skip] = $this->ychanged[$skip] = false;
1066 }
1067 // Skip trailing common lines.
1068 $xi = $n_from; $yi = $n_to;
1069 for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
1070 if ($from_lines[$xi] !== $to_lines[$yi])
1071 break;
1072 $this->xchanged[$xi] = $this->ychanged[$yi] = false;
1073 }
1074
1075 // Ignore lines which do not exist in both files.
1076 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
1077 $xhash[$this->_line_hash($from_lines[$xi])] = 1;
1078 }
1079
1080 for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
1081 $line = $to_lines[$yi];
1082 if ( ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) )
1083 continue;
1084 $yhash[$this->_line_hash($line)] = 1;
1085 $this->yv[] = $line;
1086 $this->yind[] = $yi;
1087 }
1088 for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
1089 $line = $from_lines[$xi];
1090 if ( ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) )
1091 continue;
1092 $this->xv[] = $line;
1093 $this->xind[] = $xi;
1094 }
1095
1096 // Find the LCS.
1097 $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
1098 }
1099 wfProfileOut( __METHOD__ );
1100 }
1101
1102 /**
1103 * Returns the whole line if it's small enough, or the MD5 hash otherwise
1104 */
1105 function _line_hash( $line ) {
1106 if ( strlen( $line ) > self::MAX_XREF_LENGTH ) {
1107 return md5( $line );
1108 } else {
1109 return $line;
1110 }
1111 }
1112
1113 /* Divide the Largest Common Subsequence (LCS) of the sequences
1114 * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
1115 * sized segments.
1116 *
1117 * Returns (LCS, PTS). LCS is the length of the LCS. PTS is an
1118 * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
1119 * sub sequences. The first sub-sequence is contained in [X0, X1),
1120 * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on. Note
1121 * that (X0, Y0) == (XOFF, YOFF) and
1122 * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
1123 *
1124 * This function assumes that the first lines of the specified portions
1125 * of the two files do not match, and likewise that the last lines do not
1126 * match. The caller must trim matching lines from the beginning and end
1127 * of the portions it is going to specify.
1128 */
1129 function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
1130 $flip = false;
1131
1132 if ($xlim - $xoff > $ylim - $yoff) {
1133 // Things seems faster (I'm not sure I understand why)
1134 // when the shortest sequence in X.
1135 $flip = true;
1136 list ($xoff, $xlim, $yoff, $ylim)
1137 = array( $yoff, $ylim, $xoff, $xlim);
1138 }
1139
1140 if ($flip)
1141 for ($i = $ylim - 1; $i >= $yoff; $i--)
1142 $ymatches[$this->xv[$i]][] = $i;
1143 else
1144 for ($i = $ylim - 1; $i >= $yoff; $i--)
1145 $ymatches[$this->yv[$i]][] = $i;
1146
1147 $this->lcs = 0;
1148 $this->seq[0]= $yoff - 1;
1149 $this->in_seq = array();
1150 $ymids[0] = array();
1151
1152 $numer = $xlim - $xoff + $nchunks - 1;
1153 $x = $xoff;
1154 for ($chunk = 0; $chunk < $nchunks; $chunk++) {
1155 if ($chunk > 0)
1156 for ($i = 0; $i <= $this->lcs; $i++)
1157 $ymids[$i][$chunk-1] = $this->seq[$i];
1158
1159 $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
1160 for ( ; $x < $x1; $x++) {
1161 $line = $flip ? $this->yv[$x] : $this->xv[$x];
1162 if (empty($ymatches[$line]))
1163 continue;
1164 $matches = $ymatches[$line];
1165 reset($matches);
1166 while (list ($junk, $y) = each($matches))
1167 if (empty($this->in_seq[$y])) {
1168 $k = $this->_lcs_pos($y);
1169 USE_ASSERTS && assert($k > 0);
1170 $ymids[$k] = $ymids[$k-1];
1171 break;
1172 }
1173 while (list ( /* $junk */, $y) = each($matches)) {
1174 if ($y > $this->seq[$k-1]) {
1175 USE_ASSERTS && assert($y < $this->seq[$k]);
1176 // Optimization: this is a common case:
1177 // next match is just replacing previous match.
1178 $this->in_seq[$this->seq[$k]] = false;
1179 $this->seq[$k] = $y;
1180 $this->in_seq[$y] = 1;
1181 } else if (empty($this->in_seq[$y])) {
1182 $k = $this->_lcs_pos($y);
1183 USE_ASSERTS && assert($k > 0);
1184 $ymids[$k] = $ymids[$k-1];
1185 }
1186 }
1187 }
1188 }
1189
1190 $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
1191 $ymid = $ymids[$this->lcs];
1192 for ($n = 0; $n < $nchunks - 1; $n++) {
1193 $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
1194 $y1 = $ymid[$n] + 1;
1195 $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
1196 }
1197 $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
1198
1199 return array($this->lcs, $seps);
1200 }
1201
1202 function _lcs_pos ($ypos) {
1203 $end = $this->lcs;
1204 if ($end == 0 || $ypos > $this->seq[$end]) {
1205 $this->seq[++$this->lcs] = $ypos;
1206 $this->in_seq[$ypos] = 1;
1207 return $this->lcs;
1208 }
1209
1210 $beg = 1;
1211 while ($beg < $end) {
1212 $mid = (int)(($beg + $end) / 2);
1213 if ( $ypos > $this->seq[$mid] )
1214 $beg = $mid + 1;
1215 else
1216 $end = $mid;
1217 }
1218
1219 USE_ASSERTS && assert($ypos != $this->seq[$end]);
1220
1221 $this->in_seq[$this->seq[$end]] = false;
1222 $this->seq[$end] = $ypos;
1223 $this->in_seq[$ypos] = 1;
1224 return $end;
1225 }
1226
1227 /* Find LCS of two sequences.
1228 *
1229 * The results are recorded in the vectors $this->{x,y}changed[], by
1230 * storing a 1 in the element for each line that is an insertion
1231 * or deletion (ie. is not in the LCS).
1232 *
1233 * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
1234 *
1235 * Note that XLIM, YLIM are exclusive bounds.
1236 * All line numbers are origin-0 and discarded lines are not counted.
1237 */
1238 function _compareseq ($xoff, $xlim, $yoff, $ylim) {
1239 // Slide down the bottom initial diagonal.
1240 while ($xoff < $xlim && $yoff < $ylim
1241 && $this->xv[$xoff] == $this->yv[$yoff]) {
1242 ++$xoff;
1243 ++$yoff;
1244 }
1245
1246 // Slide up the top initial diagonal.
1247 while ($xlim > $xoff && $ylim > $yoff
1248 && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
1249 --$xlim;
1250 --$ylim;
1251 }
1252
1253 if ($xoff == $xlim || $yoff == $ylim)
1254 $lcs = 0;
1255 else {
1256 // This is ad hoc but seems to work well.
1257 //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
1258 //$nchunks = max(2,min(8,(int)$nchunks));
1259 $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
1260 list ($lcs, $seps)
1261 = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
1262 }
1263
1264 if ($lcs == 0) {
1265 // X and Y sequences have no common subsequence:
1266 // mark all changed.
1267 while ($yoff < $ylim)
1268 $this->ychanged[$this->yind[$yoff++]] = 1;
1269 while ($xoff < $xlim)
1270 $this->xchanged[$this->xind[$xoff++]] = 1;
1271 } else {
1272 // Use the partitions to split this problem into subproblems.
1273 reset($seps);
1274 $pt1 = $seps[0];
1275 while ($pt2 = next($seps)) {
1276 $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
1277 $pt1 = $pt2;
1278 }
1279 }
1280 }
1281
1282 /* Adjust inserts/deletes of identical lines to join changes
1283 * as much as possible.
1284 *
1285 * We do something when a run of changed lines include a
1286 * line at one end and has an excluded, identical line at the other.
1287 * We are free to choose which identical line is included.
1288 * `compareseq' usually chooses the one at the beginning,
1289 * but usually it is cleaner to consider the following identical line
1290 * to be the "change".
1291 *
1292 * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
1293 */
1294 function _shift_boundaries ($lines, &$changed, $other_changed) {
1295 wfProfileIn( __METHOD__ );
1296 $i = 0;
1297 $j = 0;
1298
1299 USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
1300 $len = sizeof($lines);
1301 $other_len = sizeof($other_changed);
1302
1303 while (1) {
1304 /*
1305 * Scan forwards to find beginning of another run of changes.
1306 * Also keep track of the corresponding point in the other file.
1307 *
1308 * Throughout this code, $i and $j are adjusted together so that
1309 * the first $i elements of $changed and the first $j elements
1310 * of $other_changed both contain the same number of zeros
1311 * (unchanged lines).
1312 * Furthermore, $j is always kept so that $j == $other_len or
1313 * $other_changed[$j] == false.
1314 */
1315 while ($j < $other_len && $other_changed[$j])
1316 $j++;
1317
1318 while ($i < $len && ! $changed[$i]) {
1319 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
1320 $i++; $j++;
1321 while ($j < $other_len && $other_changed[$j])
1322 $j++;
1323 }
1324
1325 if ($i == $len)
1326 break;
1327
1328 $start = $i;
1329
1330 // Find the end of this run of changes.
1331 while (++$i < $len && $changed[$i])
1332 continue;
1333
1334 do {
1335 /*
1336 * Record the length of this run of changes, so that
1337 * we can later determine whether the run has grown.
1338 */
1339 $runlength = $i - $start;
1340
1341 /*
1342 * Move the changed region back, so long as the
1343 * previous unchanged line matches the last changed one.
1344 * This merges with previous changed regions.
1345 */
1346 while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
1347 $changed[--$start] = 1;
1348 $changed[--$i] = false;
1349 while ($start > 0 && $changed[$start - 1])
1350 $start--;
1351 USE_ASSERTS && assert('$j > 0');
1352 while ($other_changed[--$j])
1353 continue;
1354 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
1355 }
1356
1357 /*
1358 * Set CORRESPONDING to the end of the changed run, at the last
1359 * point where it corresponds to a changed run in the other file.
1360 * CORRESPONDING == LEN means no such point has been found.
1361 */
1362 $corresponding = $j < $other_len ? $i : $len;
1363
1364 /*
1365 * Move the changed region forward, so long as the
1366 * first changed line matches the following unchanged one.
1367 * This merges with following changed regions.
1368 * Do this second, so that if there are no merges,
1369 * the changed region is moved forward as far as possible.
1370 */
1371 while ($i < $len && $lines[$start] == $lines[$i]) {
1372 $changed[$start++] = false;
1373 $changed[$i++] = 1;
1374 while ($i < $len && $changed[$i])
1375 $i++;
1376
1377 USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
1378 $j++;
1379 if ($j < $other_len && $other_changed[$j]) {
1380 $corresponding = $i;
1381 while ($j < $other_len && $other_changed[$j])
1382 $j++;
1383 }
1384 }
1385 } while ($runlength != $i - $start);
1386
1387 /*
1388 * If possible, move the fully-merged run of changes
1389 * back to a corresponding run in the other file.
1390 */
1391 while ($corresponding < $i) {
1392 $changed[--$start] = 1;
1393 $changed[--$i] = 0;
1394 USE_ASSERTS && assert('$j > 0');
1395 while ($other_changed[--$j])
1396 continue;
1397 USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
1398 }
1399 }
1400 wfProfileOut( __METHOD__ );
1401 }
1402 }
1403
1404 /**
1405 * Class representing a 'diff' between two sequences of strings.
1406 * @todo document
1407 * @private
1408 * @ingroup DifferenceEngine
1409 */
1410 class Diff
1411 {
1412 var $edits;
1413
1414 /**
1415 * Constructor.
1416 * Computes diff between sequences of strings.
1417 *
1418 * @param $from_lines array An array of strings.
1419 * (Typically these are lines from a file.)
1420 * @param $to_lines array An array of strings.
1421 */
1422 function Diff($from_lines, $to_lines) {
1423 $eng = new _DiffEngine;
1424 $this->edits = $eng->diff($from_lines, $to_lines);
1425 //$this->_check($from_lines, $to_lines);
1426 }
1427
1428 /**
1429 * Compute reversed Diff.
1430 *
1431 * SYNOPSIS:
1432 *
1433 * $diff = new Diff($lines1, $lines2);
1434 * $rev = $diff->reverse();
1435 * @return object A Diff object representing the inverse of the
1436 * original diff.
1437 */
1438 function reverse () {
1439 $rev = $this;
1440 $rev->edits = array();
1441 foreach ($this->edits as $edit) {
1442 $rev->edits[] = $edit->reverse();
1443 }
1444 return $rev;
1445 }
1446
1447 /**
1448 * Check for empty diff.
1449 *
1450 * @return bool True iff two sequences were identical.
1451 */
1452 function isEmpty () {
1453 foreach ($this->edits as $edit) {
1454 if ($edit->type != 'copy')
1455 return false;
1456 }
1457 return true;
1458 }
1459
1460 /**
1461 * Compute the length of the Longest Common Subsequence (LCS).
1462 *
1463 * This is mostly for diagnostic purposed.
1464 *
1465 * @return int The length of the LCS.
1466 */
1467 function lcs () {
1468 $lcs = 0;
1469 foreach ($this->edits as $edit) {
1470 if ($edit->type == 'copy')
1471 $lcs += sizeof($edit->orig);
1472 }
1473 return $lcs;
1474 }
1475
1476 /**
1477 * Get the original set of lines.
1478 *
1479 * This reconstructs the $from_lines parameter passed to the
1480 * constructor.
1481 *
1482 * @return array The original sequence of strings.
1483 */
1484 function orig() {
1485 $lines = array();
1486
1487 foreach ($this->edits as $edit) {
1488 if ($edit->orig)
1489 array_splice($lines, sizeof($lines), 0, $edit->orig);
1490 }
1491 return $lines;
1492 }
1493
1494 /**
1495 * Get the closing set of lines.
1496 *
1497 * This reconstructs the $to_lines parameter passed to the
1498 * constructor.
1499 *
1500 * @return array The sequence of strings.
1501 */
1502 function closing() {
1503 $lines = array();
1504
1505 foreach ($this->edits as $edit) {
1506 if ($edit->closing)
1507 array_splice($lines, sizeof($lines), 0, $edit->closing);
1508 }
1509 return $lines;
1510 }
1511
1512 /**
1513 * Check a Diff for validity.
1514 *
1515 * This is here only for debugging purposes.
1516 */
1517 function _check ($from_lines, $to_lines) {
1518 wfProfileIn( __METHOD__ );
1519 if (serialize($from_lines) != serialize($this->orig()))
1520 trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
1521 if (serialize($to_lines) != serialize($this->closing()))
1522 trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
1523
1524 $rev = $this->reverse();
1525 if (serialize($to_lines) != serialize($rev->orig()))
1526 trigger_error("Reversed original doesn't match", E_USER_ERROR);
1527 if (serialize($from_lines) != serialize($rev->closing()))
1528 trigger_error("Reversed closing doesn't match", E_USER_ERROR);
1529
1530
1531 $prevtype = 'none';
1532 foreach ($this->edits as $edit) {
1533 if ( $prevtype == $edit->type )
1534 trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
1535 $prevtype = $edit->type;
1536 }
1537
1538 $lcs = $this->lcs();
1539 trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE);
1540 wfProfileOut( __METHOD__ );
1541 }
1542 }
1543
1544 /**
1545 * @todo document, bad name.
1546 * @private
1547 * @ingroup DifferenceEngine
1548 */
1549 class MappedDiff extends Diff
1550 {
1551 /**
1552 * Constructor.
1553 *
1554 * Computes diff between sequences of strings.
1555 *
1556 * This can be used to compute things like
1557 * case-insensitve diffs, or diffs which ignore
1558 * changes in white-space.
1559 *
1560 * @param $from_lines array An array of strings.
1561 * (Typically these are lines from a file.)
1562 *
1563 * @param $to_lines array An array of strings.
1564 *
1565 * @param $mapped_from_lines array This array should
1566 * have the same size number of elements as $from_lines.
1567 * The elements in $mapped_from_lines and
1568 * $mapped_to_lines are what is actually compared
1569 * when computing the diff.
1570 *
1571 * @param $mapped_to_lines array This array should
1572 * have the same number of elements as $to_lines.
1573 */
1574 function MappedDiff($from_lines, $to_lines,
1575 $mapped_from_lines, $mapped_to_lines) {
1576 wfProfileIn( __METHOD__ );
1577
1578 assert(sizeof($from_lines) == sizeof($mapped_from_lines));
1579 assert(sizeof($to_lines) == sizeof($mapped_to_lines));
1580
1581 $this->Diff($mapped_from_lines, $mapped_to_lines);
1582
1583 $xi = $yi = 0;
1584 for ($i = 0; $i < sizeof($this->edits); $i++) {
1585 $orig = &$this->edits[$i]->orig;
1586 if (is_array($orig)) {
1587 $orig = array_slice($from_lines, $xi, sizeof($orig));
1588 $xi += sizeof($orig);
1589 }
1590
1591 $closing = &$this->edits[$i]->closing;
1592 if (is_array($closing)) {
1593 $closing = array_slice($to_lines, $yi, sizeof($closing));
1594 $yi += sizeof($closing);
1595 }
1596 }
1597 wfProfileOut( __METHOD__ );
1598 }
1599 }
1600
1601 /**
1602 * A class to format Diffs
1603 *
1604 * This class formats the diff in classic diff format.
1605 * It is intended that this class be customized via inheritance,
1606 * to obtain fancier outputs.
1607 * @todo document
1608 * @private
1609 * @ingroup DifferenceEngine
1610 */
1611 class DiffFormatter {
1612 /**
1613 * Number of leading context "lines" to preserve.
1614 *
1615 * This should be left at zero for this class, but subclasses
1616 * may want to set this to other values.
1617 */
1618 var $leading_context_lines = 0;
1619
1620 /**
1621 * Number of trailing context "lines" to preserve.
1622 *
1623 * This should be left at zero for this class, but subclasses
1624 * may want to set this to other values.
1625 */
1626 var $trailing_context_lines = 0;
1627
1628 /**
1629 * Format a diff.
1630 *
1631 * @param $diff object A Diff object.
1632 * @return string The formatted output.
1633 */
1634 function format($diff) {
1635 wfProfileIn( __METHOD__ );
1636
1637 $xi = $yi = 1;
1638 $block = false;
1639 $context = array();
1640
1641 $nlead = $this->leading_context_lines;
1642 $ntrail = $this->trailing_context_lines;
1643
1644 $this->_start_diff();
1645
1646 foreach ($diff->edits as $edit) {
1647 if ($edit->type == 'copy') {
1648 if (is_array($block)) {
1649 if (sizeof($edit->orig) <= $nlead + $ntrail) {
1650 $block[] = $edit;
1651 }
1652 else{
1653 if ($ntrail) {
1654 $context = array_slice($edit->orig, 0, $ntrail);
1655 $block[] = new _DiffOp_Copy($context);
1656 }
1657 $this->_block($x0, $ntrail + $xi - $x0,
1658 $y0, $ntrail + $yi - $y0,
1659 $block);
1660 $block = false;
1661 }
1662 }
1663 $context = $edit->orig;
1664 }
1665 else {
1666 if (! is_array($block)) {
1667 $context = array_slice($context, sizeof($context) - $nlead);
1668 $x0 = $xi - sizeof($context);
1669 $y0 = $yi - sizeof($context);
1670 $block = array();
1671 if ($context)
1672 $block[] = new _DiffOp_Copy($context);
1673 }
1674 $block[] = $edit;
1675 }
1676
1677 if ($edit->orig)
1678 $xi += sizeof($edit->orig);
1679 if ($edit->closing)
1680 $yi += sizeof($edit->closing);
1681 }
1682
1683 if (is_array($block))
1684 $this->_block($x0, $xi - $x0,
1685 $y0, $yi - $y0,
1686 $block);
1687
1688 $end = $this->_end_diff();
1689 wfProfileOut( __METHOD__ );
1690 return $end;
1691 }
1692
1693 function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
1694 wfProfileIn( __METHOD__ );
1695 $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
1696 foreach ($edits as $edit) {
1697 if ($edit->type == 'copy')
1698 $this->_context($edit->orig);
1699 elseif ($edit->type == 'add')
1700 $this->_added($edit->closing);
1701 elseif ($edit->type == 'delete')
1702 $this->_deleted($edit->orig);
1703 elseif ($edit->type == 'change')
1704 $this->_changed($edit->orig, $edit->closing);
1705 else
1706 trigger_error('Unknown edit type', E_USER_ERROR);
1707 }
1708 $this->_end_block();
1709 wfProfileOut( __METHOD__ );
1710 }
1711
1712 function _start_diff() {
1713 ob_start();
1714 }
1715
1716 function _end_diff() {
1717 $val = ob_get_contents();
1718 ob_end_clean();
1719 return $val;
1720 }
1721
1722 function _block_header($xbeg, $xlen, $ybeg, $ylen) {
1723 if ($xlen > 1)
1724 $xbeg .= "," . ($xbeg + $xlen - 1);
1725 if ($ylen > 1)
1726 $ybeg .= "," . ($ybeg + $ylen - 1);
1727
1728 return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
1729 }
1730
1731 function _start_block($header) {
1732 echo $header . "\n";
1733 }
1734
1735 function _end_block() {
1736 }
1737
1738 function _lines($lines, $prefix = ' ') {
1739 foreach ($lines as $line)
1740 echo "$prefix $line\n";
1741 }
1742
1743 function _context($lines) {
1744 $this->_lines($lines);
1745 }
1746
1747 function _added($lines) {
1748 $this->_lines($lines, '>');
1749 }
1750 function _deleted($lines) {
1751 $this->_lines($lines, '<');
1752 }
1753
1754 function _changed($orig, $closing) {
1755 $this->_deleted($orig);
1756 echo "---\n";
1757 $this->_added($closing);
1758 }
1759 }
1760
1761 /**
1762 * A formatter that outputs unified diffs
1763 * @ingroup DifferenceEngine
1764 */
1765
1766 class UnifiedDiffFormatter extends DiffFormatter {
1767 var $leading_context_lines = 2;
1768 var $trailing_context_lines = 2;
1769
1770 function _added($lines) {
1771 $this->_lines($lines, '+');
1772 }
1773 function _deleted($lines) {
1774 $this->_lines($lines, '-');
1775 }
1776 function _changed($orig, $closing) {
1777 $this->_deleted($orig);
1778 $this->_added($closing);
1779 }
1780 function _block_header($xbeg, $xlen, $ybeg, $ylen) {
1781 return "@@ -$xbeg,$xlen +$ybeg,$ylen @@";
1782 }
1783 }
1784
1785 /**
1786 * A pseudo-formatter that just passes along the Diff::$edits array
1787 * @ingroup DifferenceEngine
1788 */
1789 class ArrayDiffFormatter extends DiffFormatter {
1790 function format($diff) {
1791 $oldline = 1;
1792 $newline = 1;
1793 $retval = array();
1794 foreach($diff->edits as $edit)
1795 switch($edit->type) {
1796 case 'add':
1797 foreach($edit->closing as $l) {
1798 $retval[] = array(
1799 'action' => 'add',
1800 'new'=> $l,
1801 'newline' => $newline++
1802 );
1803 }
1804 break;
1805 case 'delete':
1806 foreach($edit->orig as $l) {
1807 $retval[] = array(
1808 'action' => 'delete',
1809 'old' => $l,
1810 'oldline' => $oldline++,
1811 );
1812 }
1813 break;
1814 case 'change':
1815 foreach($edit->orig as $i => $l) {
1816 $retval[] = array(
1817 'action' => 'change',
1818 'old' => $l,
1819 'new' => @$edit->closing[$i],
1820 'oldline' => $oldline++,
1821 'newline' => $newline++,
1822 );
1823 }
1824 break;
1825 case 'copy':
1826 $oldline += count($edit->orig);
1827 $newline += count($edit->orig);
1828 }
1829 return $retval;
1830 }
1831 }
1832
1833 /**
1834 * Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
1835 *
1836 */
1837
1838 define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
1839
1840 /**
1841 * @todo document
1842 * @private
1843 * @ingroup DifferenceEngine
1844 */
1845 class _HWLDF_WordAccumulator {
1846 function _HWLDF_WordAccumulator () {
1847 $this->_lines = array();
1848 $this->_line = '';
1849 $this->_group = '';
1850 $this->_tag = '';
1851 }
1852
1853 function _flushGroup ($new_tag) {
1854 if ($this->_group !== '') {
1855 if ($this->_tag == 'ins')
1856 $this->_line .= '<ins class="diffchange diffchange-inline">' .
1857 htmlspecialchars ( $this->_group ) . '</ins>';
1858 elseif ($this->_tag == 'del')
1859 $this->_line .= '<del class="diffchange diffchange-inline">' .
1860 htmlspecialchars ( $this->_group ) . '</del>';
1861 else
1862 $this->_line .= htmlspecialchars ( $this->_group );
1863 }
1864 $this->_group = '';
1865 $this->_tag = $new_tag;
1866 }
1867
1868 function _flushLine ($new_tag) {
1869 $this->_flushGroup($new_tag);
1870 if ($this->_line != '')
1871 array_push ( $this->_lines, $this->_line );
1872 else
1873 # make empty lines visible by inserting an NBSP
1874 array_push ( $this->_lines, NBSP );
1875 $this->_line = '';
1876 }
1877
1878 function addWords ($words, $tag = '') {
1879 if ($tag != $this->_tag)
1880 $this->_flushGroup($tag);
1881
1882 foreach ($words as $word) {
1883 // new-line should only come as first char of word.
1884 if ($word == '')
1885 continue;
1886 if ($word[0] == "\n") {
1887 $this->_flushLine($tag);
1888 $word = substr($word, 1);
1889 }
1890 assert(!strstr($word, "\n"));
1891 $this->_group .= $word;
1892 }
1893 }
1894
1895 function getLines() {
1896 $this->_flushLine('~done');
1897 return $this->_lines;
1898 }
1899 }
1900
1901 /**
1902 * @todo document
1903 * @private
1904 * @ingroup DifferenceEngine
1905 */
1906 class WordLevelDiff extends MappedDiff {
1907 const MAX_LINE_LENGTH = 10000;
1908
1909 function WordLevelDiff ($orig_lines, $closing_lines) {
1910 wfProfileIn( __METHOD__ );
1911
1912 list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
1913 list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
1914
1915 $this->MappedDiff($orig_words, $closing_words,
1916 $orig_stripped, $closing_stripped);
1917 wfProfileOut( __METHOD__ );
1918 }
1919
1920 function _split($lines) {
1921 wfProfileIn( __METHOD__ );
1922
1923 $words = array();
1924 $stripped = array();
1925 $first = true;
1926 foreach ( $lines as $line ) {
1927 # If the line is too long, just pretend the entire line is one big word
1928 # This prevents resource exhaustion problems
1929 if ( $first ) {
1930 $first = false;
1931 } else {
1932 $words[] = "\n";
1933 $stripped[] = "\n";
1934 }
1935 if ( strlen( $line ) > self::MAX_LINE_LENGTH ) {
1936 $words[] = $line;
1937 $stripped[] = $line;
1938 } else {
1939 $m = array();
1940 if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
1941 $line, $m))
1942 {
1943 $words = array_merge( $words, $m[0] );
1944 $stripped = array_merge( $stripped, $m[1] );
1945 }
1946 }
1947 }
1948 wfProfileOut( __METHOD__ );
1949 return array($words, $stripped);
1950 }
1951
1952 function orig () {
1953 wfProfileIn( __METHOD__ );
1954 $orig = new _HWLDF_WordAccumulator;
1955
1956 foreach ($this->edits as $edit) {
1957 if ($edit->type == 'copy')
1958 $orig->addWords($edit->orig);
1959 elseif ($edit->orig)
1960 $orig->addWords($edit->orig, 'del');
1961 }
1962 $lines = $orig->getLines();
1963 wfProfileOut( __METHOD__ );
1964 return $lines;
1965 }
1966
1967 function closing () {
1968 wfProfileIn( __METHOD__ );
1969 $closing = new _HWLDF_WordAccumulator;
1970
1971 foreach ($this->edits as $edit) {
1972 if ($edit->type == 'copy')
1973 $closing->addWords($edit->closing);
1974 elseif ($edit->closing)
1975 $closing->addWords($edit->closing, 'ins');
1976 }
1977 $lines = $closing->getLines();
1978 wfProfileOut( __METHOD__ );
1979 return $lines;
1980 }
1981 }
1982
1983 /**
1984 * Wikipedia Table style diff formatter.
1985 * @todo document
1986 * @private
1987 * @ingroup DifferenceEngine
1988 */
1989 class TableDiffFormatter extends DiffFormatter {
1990 function TableDiffFormatter() {
1991 $this->leading_context_lines = 2;
1992 $this->trailing_context_lines = 2;
1993 }
1994
1995 public static function escapeWhiteSpace( $msg ) {
1996 $msg = preg_replace( '/^ /m', '&nbsp; ', $msg );
1997 $msg = preg_replace( '/ $/m', ' &nbsp;', $msg );
1998 $msg = preg_replace( '/ /', '&nbsp; ', $msg );
1999 return $msg;
2000 }
2001
2002 function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
2003 $r = '<tr><td colspan="2" class="diff-lineno"><!--LINE '.$xbeg."--></td>\n" .
2004 '<td colspan="2" class="diff-lineno"><!--LINE '.$ybeg."--></td></tr>\n";
2005 return $r;
2006 }
2007
2008 function _start_block( $header ) {
2009 echo $header;
2010 }
2011
2012 function _end_block() {
2013 }
2014
2015 function _lines( $lines, $prefix=' ', $color='white' ) {
2016 }
2017
2018 # HTML-escape parameter before calling this
2019 function addedLine( $line ) {
2020 return $this->wrapLine( '+', 'diff-addedline', $line );
2021 }
2022
2023 # HTML-escape parameter before calling this
2024 function deletedLine( $line ) {
2025 return $this->wrapLine( '-', 'diff-deletedline', $line );
2026 }
2027
2028 # HTML-escape parameter before calling this
2029 function contextLine( $line ) {
2030 return $this->wrapLine( ' ', 'diff-context', $line );
2031 }
2032
2033 private function wrapLine( $marker, $class, $line ) {
2034 if( $line !== '' ) {
2035 // The <div> wrapper is needed for 'overflow: auto' style to scroll properly
2036 $line = Xml::tags( 'div', null, $this->escapeWhiteSpace( $line ) );
2037 }
2038 return "<td class='diff-marker'>$marker</td><td class='$class'>$line</td>";
2039 }
2040
2041 function emptyLine() {
2042 return '<td colspan="2">&nbsp;</td>';
2043 }
2044
2045 function _added( $lines ) {
2046 foreach ($lines as $line) {
2047 echo '<tr>' . $this->emptyLine() .
2048 $this->addedLine( '<ins class="diffchange">' .
2049 htmlspecialchars ( $line ) . '</ins>' ) . "</tr>\n";
2050 }
2051 }
2052
2053 function _deleted($lines) {
2054 foreach ($lines as $line) {
2055 echo '<tr>' . $this->deletedLine( '<del class="diffchange">' .
2056 htmlspecialchars ( $line ) . '</del>' ) .
2057 $this->emptyLine() . "</tr>\n";
2058 }
2059 }
2060
2061 function _context( $lines ) {
2062 foreach ($lines as $line) {
2063 echo '<tr>' .
2064 $this->contextLine( htmlspecialchars ( $line ) ) .
2065 $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n";
2066 }
2067 }
2068
2069 function _changed( $orig, $closing ) {
2070 wfProfileIn( __METHOD__ );
2071
2072 $diff = new WordLevelDiff( $orig, $closing );
2073 $del = $diff->orig();
2074 $add = $diff->closing();
2075
2076 # Notice that WordLevelDiff returns HTML-escaped output.
2077 # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
2078
2079 while ( $line = array_shift( $del ) ) {
2080 $aline = array_shift( $add );
2081 echo '<tr>' . $this->deletedLine( $line ) .
2082 $this->addedLine( $aline ) . "</tr>\n";
2083 }
2084 foreach ($add as $line) { # If any leftovers
2085 echo '<tr>' . $this->emptyLine() .
2086 $this->addedLine( $line ) . "</tr>\n";
2087 }
2088 wfProfileOut( __METHOD__ );
2089 }
2090 }