]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/diff/DifferenceInterface.php
MediaWiki 1.16.4
[autoinstalls/mediawiki.git] / includes / diff / DifferenceInterface.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         var $mCacheHit = false; // Was the diff fetched from cache?
31
32         /**
33          * Set this to true to add debug info to the HTML output.
34          * Warning: this may cause RSS readers to spuriously mark articles as "new"
35          * (bug 20601)
36          */
37         var $enableDebugComment = false;
38
39         // If true, line X is not displayed when X is 1, for example to increase
40         // readability and conserve space with many small diffs.
41         protected $mReducedLineNumbers = false;
42
43         protected $unhide = false;
44         /**#@-*/
45
46         /**
47          * Constructor
48          * @param $titleObj Title object that the diff is associated with
49          * @param $old Integer: old ID we want to show and diff with.
50          * @param $new String: either 'prev' or 'next'.
51          * @param $rcid Integer: ??? FIXME (default 0)
52          * @param $refreshCache boolean If set, refreshes the diff cache
53          * @param $unhide boolean If set, allow viewing deleted revs
54          */
55         function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0,
56                 $refreshCache = false, $unhide = false )
57         {
58                 if ( $titleObj ) {
59                         $this->mTitle = $titleObj;
60                 } else {
61                         global $wgTitle;
62                         $this->mTitle = $wgTitle;
63                 }
64                 wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
65
66                 if ( 'prev' === $new ) {
67                         # Show diff between revision $old and the previous one.
68                         # Get previous one from DB.
69                         $this->mNewid = intval($old);
70                         $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
71                 } elseif ( 'next' === $new ) {
72                         # Show diff between revision $old and the next one.
73                         # Get next one from DB.
74                         $this->mOldid = intval($old);
75                         $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
76                         if ( false === $this->mNewid ) {
77                                 # if no result, NewId points to the newest old revision. The only newer
78                                 # revision is cur, which is "0".
79                                 $this->mNewid = 0;
80                         }
81                 } else {
82                         $this->mOldid = intval($old);
83                         $this->mNewid = intval($new);
84                         wfRunHooks( 'NewDifferenceEngine', array(&$titleObj, &$this->mOldid, &$this->mNewid, $old, $new) ); 
85                 }
86                 $this->mRcidMarkPatrolled = intval($rcid);  # force it to be an integer
87                 $this->mRefreshCache = $refreshCache;
88                 $this->unhide = $unhide;
89         }
90
91         function setReducedLineNumbers( $value = true ) {
92                 $this->mReducedLineNumbers = $value;
93         }
94
95         function getTitle() {
96                 return $this->mTitle;
97         }
98         
99         function wasCacheHit() {
100                 return $this->mCacheHit;
101         }
102         
103         function getOldid() {
104                 return $this->mOldid;
105         }
106         
107         function getNewid() {
108                 return $this->mNewid;
109         }
110
111         function showDiffPage( $diffOnly = false ) {
112                 global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol;
113                 wfProfileIn( __METHOD__ );
114
115                 # Allow frames except in certain special cases
116                 $wgOut->allowClickjacking();
117
118                 # If external diffs are enabled both globally and for the user,
119                 # we'll use the application/x-external-editor interface to call
120                 # an external diff tool like kompare, kdiff3, etc.
121                 if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
122                         global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
123                         $wgOut->disable();
124                         header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding );
125                         $url1=$this->mTitle->getFullURL( array(
126                                 'action' => 'raw',
127                                 'oldid' => $this->mOldid
128                         ) );
129                         $url2=$this->mTitle->getFullURL( array(
130                                 'action' => 'raw',
131                                 'oldid' => $this->mNewid
132                         ) );
133                         $special=$wgLang->getNsText(NS_SPECIAL);
134                         $control=<<<CONTROL
135                         [Process]
136                         Type=Diff text
137                         Engine=MediaWiki
138                         Script={$wgServer}{$wgScript}
139                         Special namespace={$special}
140
141                         [File]
142                         Extension=wiki
143                         URL=$url1
144
145                         [File 2]
146                         Extension=wiki
147                         URL=$url2
148 CONTROL;
149                         echo($control);
150                         return;
151                 }
152
153                 $wgOut->setArticleFlag( false );
154                 if ( !$this->loadRevisionData() ) {
155                         $t = $this->mTitle->getPrefixedText();
156                         $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
157                         $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
158                         $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
159                         wfProfileOut( __METHOD__ );
160                         return;
161                 }
162
163                 wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
164
165                 if ( $this->mNewRev->isCurrent() ) {
166                         $wgOut->setArticleFlag( true );
167                 }
168
169                 # mOldid is false if the difference engine is called with a "vague" query for
170                 # a diff between a version V and its previous version V' AND the version V
171                 # is the first version of that article. In that case, V' does not exist.
172                 if ( $this->mOldid === false ) {
173                         $this->showFirstRevision();
174                         $this->renderNewRevision();  // should we respect $diffOnly here or not?
175                         wfProfileOut( __METHOD__ );
176                         return;
177                 }
178
179                 $wgOut->suppressQuickbar();
180
181                 $oldTitle = $this->mOldPage->getPrefixedText();
182                 $newTitle = $this->mNewPage->getPrefixedText();
183                 if( $oldTitle == $newTitle ) {
184                         $wgOut->setPageTitle( $newTitle );
185                 } else {
186                         $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
187                 }
188                 $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
189                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
190
191                 if ( !$this->mOldPage->userCanRead() || !$this->mNewPage->userCanRead() ) {
192                         $wgOut->loginToUse();
193                         $wgOut->output();
194                         $wgOut->disable();
195                         wfProfileOut( __METHOD__ );
196                         return;
197                 }
198
199                 $sk = $wgUser->getSkin();
200
201                 // Check if page is editable
202                 $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
203                 if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) {
204                         $wgOut->preventClickjacking();
205                         $rollback = '&nbsp;&nbsp;&nbsp;' . $sk->generateRollback( $this->mNewRev );
206                 } else {
207                         $rollback = '';
208                 }
209
210                 // Prepare a change patrol link, if applicable
211                 if( $wgUseRCPatrol && $this->mTitle->userCan('patrol') ) {
212                         // If we've been given an explicit change identifier, use it; saves time
213                         if( $this->mRcidMarkPatrolled ) {
214                                 $rcid = $this->mRcidMarkPatrolled;
215                                 $rc = RecentChange::newFromId( $rcid );
216                                 // Already patrolled?
217                                 $rcid = is_object($rc) && !$rc->getAttribute('rc_patrolled') ? $rcid : 0;
218                         } else {
219                                 // Look for an unpatrolled change corresponding to this diff
220                                 $db = wfGetDB( DB_SLAVE );
221                                 $change = RecentChange::newFromConds(
222                                         array(
223                                         // Redundant user,timestamp condition so we can use the existing index
224                                                 'rc_user_text'  => $this->mNewRev->getRawUserText(),
225                                                 'rc_timestamp'  => $db->timestamp( $this->mNewRev->getTimestamp() ),
226                                                 'rc_this_oldid' => $this->mNewid,
227                                                 'rc_last_oldid' => $this->mOldid,
228                                                 'rc_patrolled'  => 0
229                                         ),
230                                         __METHOD__
231                                 );
232                                 if( $change instanceof RecentChange ) {
233                                         $rcid = $change->mAttribs['rc_id'];
234                                         $this->mRcidMarkPatrolled = $rcid;
235                                 } else {
236                                         // None found
237                                         $rcid = 0;
238                                 }
239                         }
240                         // Build the link
241                         if( $rcid ) {
242                                 $patrol = ' <span class="patrollink">[' . $sk->link(
243                                         $this->mTitle, 
244                                         wfMsgHtml( 'markaspatrolleddiff' ),
245                                         array(),
246                                         array(
247                                                 'action' => 'markpatrolled',
248                                                 'rcid' => $rcid
249                                         ),
250                                         array(
251                                                 'known',
252                                                 'noclasses'
253                                         )
254                                 ) . ']</span>';
255                         } else {
256                                 $patrol = '';
257                         }
258                 } else {
259                         $patrol = '';
260                 }
261
262                 # Carry over 'diffonly' param via navigation links
263                 if( $diffOnly != $wgUser->getBoolOption('diffonly') ) {
264                         $query['diffonly'] = $diffOnly;
265                 }
266
267                 # Make "previous revision link"
268                 $query['diff'] = 'prev';
269                 $query['oldid'] = $this->mOldid;
270                 # Cascade unhide param in links for easy deletion browsing
271                 if( $this->unhide ) {
272                         $query['unhide'] = 1;
273                 }
274                 $prevlink = $sk->link(
275                         $this->mTitle,
276                         wfMsgHtml( 'previousdiff' ),
277                         array(
278                                 'id' => 'differences-prevlink'
279                         ),
280                         $query,
281                         array(
282                                 'known',
283                                 'noclasses'
284                         )
285                 );
286
287                 # Make "next revision link"
288                 $query['diff'] = 'next';
289                 $query['oldid'] = $this->mNewid;
290                 # Skip next link on the top revision
291                 if( $this->mNewRev->isCurrent() ) {
292                         $nextlink = '&nbsp;';
293                 } else {
294                         $nextlink = $sk->link(
295                                 $this->mTitle,
296                                 wfMsgHtml( 'nextdiff' ),
297                                 array(
298                                         'id' => 'differences-nextlink'
299                                 ),
300                                 $query,
301                                 array(
302                                         'known',
303                                         'noclasses'
304                                 )
305                         );
306                 }
307
308                 $oldminor = '';
309                 $newminor = '';
310
311                 if( $this->mOldRev->isMinor() ) {
312                         $oldminor = ChangesList::flag( 'minor' );
313                 }
314                 if( $this->mNewRev->isMinor() ) {
315                         $newminor = ChangesList::flag( 'minor' );
316                 }
317
318                 # Handle RevisionDelete links...
319                 $ldel = $this->revisionDeleteLink( $this->mOldRev );
320                 $rdel = $this->revisionDeleteLink( $this->mNewRev );
321
322                 $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
323                         '<div id="mw-diff-otitle2">' .
324                                 $sk->revUserTools( $this->mOldRev, !$this->unhide ).'</div>' .
325                         '<div id="mw-diff-otitle3">' . $oldminor .
326                                 $sk->revComment( $this->mOldRev, !$diffOnly, !$this->unhide ).$ldel.'</div>' .
327                         '<div id="mw-diff-otitle4">' . $prevlink .'</div>';
328                 $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
329                         '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, !$this->unhide ) .
330                                 " $rollback</div>" .
331                         '<div id="mw-diff-ntitle3">' . $newminor .
332                                 $sk->revComment( $this->mNewRev, !$diffOnly, !$this->unhide ).$rdel.'</div>' .
333                         '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
334
335                 # Check if this user can see the revisions
336                 $allowed = $this->mOldRev->userCan(Revision::DELETED_TEXT)
337                         && $this->mNewRev->userCan(Revision::DELETED_TEXT);
338                 # Check if one of the revisions is deleted/suppressed
339                 $deleted = $suppressed = false;
340                 if( $this->mOldRev->isDeleted(Revision::DELETED_TEXT) ) {
341                         $deleted = true; // old revisions text is hidden
342                         if( $this->mOldRev->isDeleted(Revision::DELETED_RESTRICTED) )
343                                 $suppressed = true; // also suppressed
344                 }
345                 if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
346                         $deleted = true; // new revisions text is hidden
347                         if( $this->mNewRev->isDeleted(Revision::DELETED_RESTRICTED) )
348                                 $suppressed = true; // also suppressed
349                 }
350                 # If the diff cannot be shown due to a deleted revision, then output
351                 # the diff header and links to unhide (if available)...
352                 if( $deleted && (!$this->unhide || !$allowed) ) {
353                         $this->showDiffStyle();
354                         $multi = $this->getMultiNotice();
355                         $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
356                         if( !$allowed ) {
357                                 $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
358                                 # Give explanation for why revision is not visible
359                                 $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
360                                         array( $msg ) );
361                         } else {
362                                 # Give explanation and add a link to view the diff...
363                                 $link = $this->mTitle->getFullUrl( array(
364                                         'diff' => $this->mNewid,
365                                         'oldid' => $this->mOldid,
366                                         'unhide' => 1
367                                 ) );
368                                 $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
369                                 $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", array( $msg, $link ) );
370                         }
371                 # Otherwise, output a regular diff...
372                 } else {
373                         # Add deletion notice if the user is viewing deleted content
374                         $notice = '';
375                         if( $deleted ) {
376                                 $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
377                                 $notice = "<div class='mw-warning plainlinks'>\n".wfMsgExt($msg,'parseinline')."</div>\n";
378                         }
379                         $this->showDiff( $oldHeader, $newHeader, $notice );
380                         if( !$diffOnly ) {
381                                 $this->renderNewRevision();
382                         }
383                 }
384                 wfProfileOut( __METHOD__ );
385         }
386         
387         protected function revisionDeleteLink( $rev ) {
388                 global $wgUser;
389                 $link = '';
390                 $canHide = $wgUser->isAllowed( 'deleterevision' );
391                 // Show del/undel link if:
392                 // (a) the user can delete revisions, or
393                 // (b) the user can view deleted revision *and* this one is deleted
394                 if( $canHide || ($rev->getVisibility() && $wgUser->isAllowed( 'deletedhistory' )) ) {
395                         $sk = $wgUser->getSkin();
396                         if( !$rev->userCan( Revision::DELETED_RESTRICTED ) ) {
397                                 $link = $sk->revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
398                         } else {
399                                 $query = array(
400                                         'type'   => 'revision',
401                                         'target' => $rev->mTitle->getPrefixedDbkey(),
402                                         'ids'    => $rev->getId()
403                                 );
404                                 $link = $sk->revDeleteLink( $query,
405                                         $rev->isDeleted( Revision::DELETED_RESTRICTED ), $canHide );
406                         }
407                         $link = '&nbsp;&nbsp;&nbsp;' . $link . ' ';
408                 }
409                 return $link;
410         }
411
412         /**
413          * Show the new revision of the page.
414          */
415         function renderNewRevision() {
416                 global $wgOut, $wgUser;
417                 wfProfileIn( __METHOD__ );
418
419                 $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
420                 # Add deleted rev tag if needed
421                 if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
422                         $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-permission' );
423                 } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
424                         $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", 'rev-deleted-text-view' );
425                 }
426
427                 if( !$this->mNewRev->isCurrent() ) {
428                         $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
429                 }
430
431                 $this->loadNewText();
432                 if( is_object( $this->mNewRev ) ) {
433                         $wgOut->setRevisionId( $this->mNewRev->getId() );
434                 }
435
436                 if( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
437                         // Stolen from Article::view --AG 2007-10-11
438                         // Give hooks a chance to customise the output
439                         if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
440                                 // Wrap the whole lot in a <pre> and don't parse
441                                 $m = array();
442                                 preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
443                                 $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
444                                 $wgOut->addHTML( htmlspecialchars( $this->mNewtext ) );
445                                 $wgOut->addHTML( "\n</pre>\n" );
446                         }
447                 } else {
448                         $wgOut->addWikiTextTidy( $this->mNewtext );
449                 }
450
451                 if( is_object( $this->mNewRev ) && !$this->mNewRev->isCurrent() ) {
452                         $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
453                 }
454                 # Add redundant patrol link on bottom...
455                 if( $this->mRcidMarkPatrolled && $this->mTitle->quickUserCan('patrol') ) {
456                         $sk = $wgUser->getSkin();
457                         $wgOut->addHTML(
458                                 "<div class='patrollink'>[" . $sk->link(
459                                         $this->mTitle,
460                                         wfMsgHtml( 'markaspatrolleddiff' ),
461                                         array(),
462                                         array(
463                                                 'action' => 'markpatrolled',
464                                                 'rcid' => $this->mRcidMarkPatrolled
465                                         )
466                                 ) . ']</div>'
467                          );
468                 }
469
470                 wfProfileOut( __METHOD__ );
471         }
472
473         /**
474          * Show the first revision of an article. Uses normal diff headers in
475          * contrast to normal "old revision" display style.
476          */
477         function showFirstRevision() {
478                 global $wgOut, $wgUser;
479                 wfProfileIn( __METHOD__ );
480
481                 # Get article text from the DB
482                 #
483                 if ( ! $this->loadNewText() ) {
484                         $t = $this->mTitle->getPrefixedText();
485                         $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
486                         $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
487                         $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
488                         wfProfileOut( __METHOD__ );
489                         return;
490                 }
491                 if ( $this->mNewRev->isCurrent() ) {
492                         $wgOut->setArticleFlag( true );
493                 }
494
495                 # Check if user is allowed to look at this page. If not, bail out.
496                 #
497                 if ( !$this->mTitle->userCanRead() ) {
498                         $wgOut->loginToUse();
499                         $wgOut->output();
500                         wfProfileOut( __METHOD__ );
501                         throw new MWException("Permission Error: you do not have access to view this page");
502                 }
503
504                 # Prepare the header box
505                 #
506                 $sk = $wgUser->getSkin();
507
508                 $next = $this->mTitle->getNextRevisionID( $this->mNewid );
509                 if( !$next ) {
510                         $nextlink = '';
511                 } else {
512                         $nextlink = '<br />' . $sk->link(
513                                 $this->mTitle,
514                                 wfMsgHtml( 'nextdiff' ),
515                                 array(
516                                         'id' => 'differences-nextlink'
517                                 ),
518                                 array(
519                                         'diff' => 'next',
520                                         'oldid' => $this->mNewid,
521                                 ),
522                                 array(
523                                         'known',
524                                         'noclasses'
525                                 )
526                         );
527                 }
528                 $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\">" .
529                         $sk->revUserTools( $this->mNewRev ) . "<br />" . $sk->revComment( $this->mNewRev ) . $nextlink . "</div>\n";
530
531                 $wgOut->addHTML( $header );
532
533                 $wgOut->setSubtitle( wfMsgExt( 'difference', array( 'parseinline' ) ) );
534                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
535
536                 wfProfileOut( __METHOD__ );
537         }
538
539         /**
540          * Get the diff text, send it to $wgOut
541          * Returns false if the diff could not be generated, otherwise returns true
542          */
543         function showDiff( $otitle, $ntitle, $notice = '' ) {
544                 global $wgOut;
545                 $diff = $this->getDiff( $otitle, $ntitle, $notice );
546                 if ( $diff === false ) {
547                         $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
548                         return false;
549                 } else {
550                         $this->showDiffStyle();
551                         $wgOut->addHTML( $diff );
552                         return true;
553                 }
554         }
555
556         /**
557          * Add style sheets and supporting JS for diff display.
558          */
559         function showDiffStyle() {
560                 global $wgStylePath, $wgStyleVersion, $wgOut;
561                 $wgOut->addStyle( 'common/diff.css' );
562
563                 // JS is needed to detect old versions of Mozilla to work around an annoyance bug.
564                 $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" );
565         }
566
567         /**
568          * Get complete diff table, including header
569          *
570          * @param Title $otitle Old title
571          * @param Title $ntitle New title
572          * @param string $notice HTML between diff header and body
573          * @return mixed
574          */
575         function getDiff( $otitle, $ntitle, $notice = '' ) {
576                 $body = $this->getDiffBody();
577                 if ( $body === false ) {
578                         return false;
579                 } else {
580                         $multi = $this->getMultiNotice();
581                         return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
582                 }
583         }
584
585         /**
586          * Get the diff table body, without header
587          *
588          * @return mixed
589          */
590         function getDiffBody() {
591                 global $wgMemc;
592                 wfProfileIn( __METHOD__ );
593                 $this->mCacheHit = true;
594                 // Check if the diff should be hidden from this user
595                 if ( !$this->loadRevisionData() )
596                         return '';
597                 if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
598                         return '';
599                 } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
600                         return '';
601                 } else if ( $this->mOldRev && $this->mNewRev && $this->mOldRev->getID() == $this->mNewRev->getID() ) {
602                         return '';
603                 }
604                 // Cacheable?
605                 $key = false;
606                 if ( $this->mOldid && $this->mNewid ) {
607                         $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
608                         // Try cache
609                         if ( !$this->mRefreshCache ) {
610                                 $difftext = $wgMemc->get( $key );
611                                 if ( $difftext ) {
612                                         wfIncrStats( 'diff_cache_hit' );
613                                         $difftext = $this->localiseLineNumbers( $difftext );
614                                         $difftext .= "\n<!-- diff cache key $key -->\n";
615                                         wfProfileOut( __METHOD__ );
616                                         return $difftext;
617                                 }
618                         } // don't try to load but save the result
619                 }
620                 $this->mCacheHit = false;
621
622                 // Loadtext is permission safe, this just clears out the diff
623                 if ( !$this->loadText() ) {
624                         wfProfileOut( __METHOD__ );
625                         return false;
626                 }
627
628                 $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
629
630                 // Save to cache for 7 days
631                 if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
632                         wfIncrStats( 'diff_uncacheable' );
633                 } else if ( $key !== false && $difftext !== false ) {
634                         wfIncrStats( 'diff_cache_miss' );
635                         $wgMemc->set( $key, $difftext, 7*86400 );
636                 } else {
637                         wfIncrStats( 'diff_uncacheable' );
638                 }
639                 // Replace line numbers with the text in the user's language
640                 if ( $difftext !== false ) {
641                         $difftext = $this->localiseLineNumbers( $difftext );
642                 }
643                 wfProfileOut( __METHOD__ );
644                 return $difftext;
645         }
646
647         /**
648          * Make sure the proper modules are loaded before we try to
649          * make the diff
650          */
651         private function initDiffEngines() {
652                 global $wgExternalDiffEngine;
653                 if ( $wgExternalDiffEngine == 'wikidiff' && !function_exists( 'wikidiff_do_diff' ) ) {
654                         wfProfileIn( __METHOD__ . '-php_wikidiff.so' );
655                         wfSuppressWarnings();
656                         dl( 'php_wikidiff.so' );
657                         wfRestoreWarnings();
658                         wfProfileOut( __METHOD__ . '-php_wikidiff.so' );
659                 }
660                 else if ( $wgExternalDiffEngine == 'wikidiff2' && !function_exists( 'wikidiff2_do_diff' ) ) {
661                         wfProfileIn( __METHOD__ . '-php_wikidiff2.so' );
662                         wfSuppressWarnings();
663                         dl( 'php_wikidiff2.so' );
664                         wfRestoreWarnings();
665                         wfProfileOut( __METHOD__ . '-php_wikidiff2.so' );
666                 }
667         }
668
669         /**
670          * Generate a diff, no caching
671          * $otext and $ntext must be already segmented
672          */
673         function generateDiffBody( $otext, $ntext ) {
674                 global $wgExternalDiffEngine, $wgContLang;
675
676                 $otext = str_replace( "\r\n", "\n", $otext );
677                 $ntext = str_replace( "\r\n", "\n", $ntext );
678
679                 $this->initDiffEngines();
680
681                 if ( $wgExternalDiffEngine == 'wikidiff' && function_exists( 'wikidiff_do_diff' ) ) {
682                         # For historical reasons, external diff engine expects
683                         # input text to be HTML-escaped already
684                         $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
685                         $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
686                         return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
687                         $this->debug( 'wikidiff1' );
688                 }
689
690                 if ( $wgExternalDiffEngine == 'wikidiff2' && function_exists( 'wikidiff2_do_diff' ) ) {
691                         # Better external diff engine, the 2 may some day be dropped
692                         # This one does the escaping and segmenting itself
693                         wfProfileIn( 'wikidiff2_do_diff' );
694                         $text = wikidiff2_do_diff( $otext, $ntext, 2 );
695                         $text .= $this->debug( 'wikidiff2' );
696                         wfProfileOut( 'wikidiff2_do_diff' );
697                         return $text;
698                 }
699                 if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
700                         # Diff via the shell
701                         global $wgTmpDirectory;
702                         $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
703                         $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
704
705                         $tempFile1 = fopen( $tempName1, "w" );
706                         if ( !$tempFile1 ) {
707                                 wfProfileOut( __METHOD__ );
708                                 return false;
709                         }
710                         $tempFile2 = fopen( $tempName2, "w" );
711                         if ( !$tempFile2 ) {
712                                 wfProfileOut( __METHOD__ );
713                                 return false;
714                         }
715                         fwrite( $tempFile1, $otext );
716                         fwrite( $tempFile2, $ntext );
717                         fclose( $tempFile1 );
718                         fclose( $tempFile2 );
719                         $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
720                         wfProfileIn( __METHOD__ . "-shellexec" );
721                         $difftext = wfShellExec( $cmd );
722                         $difftext .= $this->debug( "external $wgExternalDiffEngine" );
723                         wfProfileOut( __METHOD__ . "-shellexec" );
724                         unlink( $tempName1 );
725                         unlink( $tempName2 );
726                         return $difftext;
727                 }
728
729                 # Native PHP diff
730                 $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
731                 $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
732                 $diffs = new Diff( $ota, $nta );
733                 $formatter = new TableDiffFormatter();
734                 return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
735                 $this->debug();
736         }
737
738         /**
739          * Generate a debug comment indicating diff generating time,
740          * server node, and generator backend.
741          */
742         protected function debug( $generator="internal" ) {
743                 global $wgShowHostnames;
744                 if ( !$this->enableDebugComment ) {
745                         return '';
746                 }
747                 $data = array( $generator );
748                 if( $wgShowHostnames ) {
749                         $data[] = wfHostname();
750                 }
751                 $data[] = wfTimestamp( TS_DB );
752                 return "<!-- diff generator: " .
753                 implode( " ",
754                 array_map(
755                                         "htmlspecialchars",
756                 $data ) ) .
757                         " -->\n";
758         }
759
760         /**
761          * Replace line numbers with the text in the user's language
762          */
763         function localiseLineNumbers( $text ) {
764                 return preg_replace_callback( '/<!--LINE (\d+)-->/',
765                 array( &$this, 'localiseLineNumbersCb' ), $text );
766         }
767
768         function localiseLineNumbersCb( $matches ) {
769                 global $wgLang;
770                 if ( $matches[1] === '1' && $this->mReducedLineNumbers ) return '';
771                 return wfMsgExt( 'lineno', 'escape', $wgLang->formatNum( $matches[1] ) );
772         }
773
774
775         /**
776          * If there are revisions between the ones being compared, return a note saying so.
777          */
778         function getMultiNotice() {
779                 if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
780                 return '';
781
782                 if( !$this->mOldPage->equals( $this->mNewPage ) ) {
783                         // Comparing two different pages? Count would be meaningless.
784                         return '';
785                 }
786
787                 $oldid = $this->mOldRev->getId();
788                 $newid = $this->mNewRev->getId();
789                 if ( $oldid > $newid ) {
790                         $tmp = $oldid; $oldid = $newid; $newid = $tmp;
791                 }
792
793                 $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
794                 if ( !$n )
795                 return '';
796
797                 return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
798         }
799
800
801         /**
802          * Add the header to a diff body
803          */
804         static function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
805                 $header = "<table class='diff'>";
806                 if( $diff ) { // Safari/Chrome show broken output if cols not used
807                         $header .= "
808                         <col class='diff-marker' />
809                         <col class='diff-content' />
810                         <col class='diff-marker' />
811                         <col class='diff-content' />";
812                         $colspan = 2;
813                         $multiColspan = 4;
814                 } else {
815                         $colspan = 1;
816                         $multiColspan = 2;
817                 }
818                 $header .= "
819                 <tr valign='top'>
820                 <td colspan='$colspan' class='diff-otitle'>{$otitle}</td>
821                 <td colspan='$colspan' class='diff-ntitle'>{$ntitle}</td>
822                 </tr>";
823
824                 if ( $multi != '' ) {
825                         $header .= "<tr><td colspan='{$multiColspan}' align='center' class='diff-multi'>{$multi}</td></tr>";
826                 }
827                 if ( $notice != '' ) {
828                         $header .= "<tr><td colspan='{$multiColspan}' align='center'>{$notice}</td></tr>";
829                 }
830
831                 return $header . $diff . "</table>";
832         }
833
834         /**
835          * Use specified text instead of loading from the database
836          */
837         function setText( $oldText, $newText ) {
838                 $this->mOldtext = $oldText;
839                 $this->mNewtext = $newText;
840                 $this->mTextLoaded = 2;
841                 $this->mRevisionsLoaded = true;
842         }
843
844         /**
845          * Load revision metadata for the specified articles. If newid is 0, then compare
846          * the old article in oldid to the current article; if oldid is 0, then
847          * compare the current article to the immediately previous one (ignoring the
848          * value of newid).
849          *
850          * If oldid is false, leave the corresponding revision object set
851          * to false. This is impossible via ordinary user input, and is provided for
852          * API convenience.
853          */
854         function loadRevisionData() {
855                 global $wgLang, $wgUser;
856                 if ( $this->mRevisionsLoaded ) {
857                         return true;
858                 } else {
859                         // Whether it succeeds or fails, we don't want to try again
860                         $this->mRevisionsLoaded = true;
861                 }
862
863                 // Load the new revision object
864                 $this->mNewRev = $this->mNewid
865                         ? Revision::newFromId( $this->mNewid )
866                         : Revision::newFromTitle( $this->mTitle );
867                 if( !$this->mNewRev instanceof Revision )
868                         return false;
869
870                 // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
871                 $this->mNewid = $this->mNewRev->getId();
872
873                 // Check if page is editable
874                 $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
875
876                 // Set assorted variables
877                 $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
878                 $dateofrev = $wgLang->date( $this->mNewRev->getTimestamp(), true );
879                 $timeofrev = $wgLang->time( $this->mNewRev->getTimestamp(), true );
880                 $this->mNewPage = $this->mNewRev->getTitle();
881                 if( $this->mNewRev->isCurrent() ) {
882                         $newLink = $this->mNewPage->escapeLocalUrl( array(
883                                 'oldid' => $this->mNewid
884                         ) );
885                         $this->mPagetitle = htmlspecialchars( wfMsg(
886                                 'currentrev-asof',
887                                 $timestamp,
888                                 $dateofrev,
889                                 $timeofrev
890                         ) );
891                         $newEdit = $this->mNewPage->escapeLocalUrl( array(
892                                 'action' => 'edit'
893                         ) );
894
895                         $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
896                         $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
897                 } else {
898                         $newLink = $this->mNewPage->escapeLocalUrl( array(
899                                 'oldid' => $this->mNewid
900                         ) );
901                         $newEdit = $this->mNewPage->escapeLocalUrl( array(
902                                 'action' => 'edit',
903                                 'oldid' => $this->mNewid
904                         ) );
905                         $this->mPagetitle = htmlspecialchars( wfMsg(
906                                 'revisionasof',
907                                 $timestamp,
908                                 $dateofrev,
909                                 $timeofrev
910                         ) );
911
912                         $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
913                         $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
914                 }
915                 if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
916                         $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
917                 } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
918                         $this->mNewtitle = "<span class='history-deleted'>{$this->mNewtitle}</span>";
919                 }
920
921                 // Load the old revision object
922                 $this->mOldRev = false;
923                 if( $this->mOldid ) {
924                         $this->mOldRev = Revision::newFromId( $this->mOldid );
925                 } elseif ( $this->mOldid === 0 ) {
926                         $rev = $this->mNewRev->getPrevious();
927                         if( $rev ) {
928                                 $this->mOldid = $rev->getId();
929                                 $this->mOldRev = $rev;
930                         } else {
931                                 // No previous revision; mark to show as first-version only.
932                                 $this->mOldid = false;
933                                 $this->mOldRev = false;
934                         }
935                 }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
936
937                 if( is_null( $this->mOldRev ) ) {
938                         return false;
939                 }
940
941                 if ( $this->mOldRev ) {
942                         $this->mOldPage = $this->mOldRev->getTitle();
943
944                         $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
945                         $dateofrev = $wgLang->date( $this->mOldRev->getTimestamp(), true );
946                         $timeofrev = $wgLang->time( $this->mOldRev->getTimestamp(), true );
947                         $oldLink = $this->mOldPage->escapeLocalUrl( array(
948                                 'oldid' => $this->mOldid
949                         ) );
950                         $oldEdit = $this->mOldPage->escapeLocalUrl( array(
951                                 'action' => 'edit',
952                                 'oldid' => $this->mOldid
953                         ) );
954                         $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t, $dateofrev, $timeofrev ) );
955
956                         $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
957                         . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
958                         // Add an "undo" link
959                         $newUndo = $this->mNewPage->escapeLocalUrl( array(
960                                 'action' => 'edit',
961                                 'undoafter' => $this->mOldid,
962                                 'undo' => $this->mNewid
963                         ) );
964                         $htmlLink = htmlspecialchars( wfMsg( 'editundo' ) );
965                         $htmlTitle = $wgUser->getSkin()->tooltip( 'undo' );
966                         if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
967                                 $this->mNewtitle .= " (<a href='$newUndo' $htmlTitle>" . $htmlLink . "</a>)";
968                         }
969
970                         if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
971                                 $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>';
972                         } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
973                                 $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>';
974                         }
975                 }
976
977                 return true;
978         }
979
980         /**
981          * Load the text of the revisions, as well as revision data.
982          */
983         function loadText() {
984                 if ( $this->mTextLoaded == 2 ) {
985                         return true;
986                 } else {
987                         // Whether it succeeds or fails, we don't want to try again
988                         $this->mTextLoaded = 2;
989                 }
990
991                 if ( !$this->loadRevisionData() ) {
992                         return false;
993                 }
994                 if ( $this->mOldRev ) {
995                         $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
996                         if ( $this->mOldtext === false ) {
997                                 return false;
998                         }
999                 }
1000                 if ( $this->mNewRev ) {
1001                         $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
1002                         if ( $this->mNewtext === false ) {
1003                                 return false;
1004                         }
1005                 }
1006                 return true;
1007         }
1008
1009         /**
1010          * Load the text of the new revision, not the old one
1011          */
1012         function loadNewText() {
1013                 if ( $this->mTextLoaded >= 1 ) {
1014                         return true;
1015                 } else {
1016                         $this->mTextLoaded = 1;
1017                 }
1018                 if ( !$this->loadRevisionData() ) {
1019                         return false;
1020                 }
1021                 $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
1022                 return true;
1023         }
1024 }