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