MediaWiki  master
DifferenceEngine.php
Go to the documentation of this file.
1 <?php
28 
52 
54 
61  const DIFF_VERSION = '1.12';
62 
69  protected $mOldid;
70 
77  protected $mNewid;
78 
90  protected $mOldRev;
91 
101  protected $mNewRev;
102 
108  protected $mOldPage;
109 
115  protected $mNewPage;
116 
121  private $mOldTags;
122 
127  private $mNewTags;
128 
134  private $mOldContent;
135 
141  private $mNewContent;
142 
144  protected $mDiffLang;
145 
147  private $mRevisionsIdsLoaded = false;
148 
150  protected $mRevisionsLoaded = false;
151 
153  protected $mTextLoaded = 0;
154 
163  protected $isContentOverridden = false;
164 
166  protected $mCacheHit = false;
167 
173  public $enableDebugComment = false;
174 
178  protected $mReducedLineNumbers = false;
179 
181  protected $mMarkPatrolledLink = null;
182 
184  protected $unhide = false;
185 
187  protected $mRefreshCache = false;
188 
190  protected $slotDiffRenderers = null;
191 
198  protected $isSlotDiffRenderer = false;
199 
210  public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
211  $refreshCache = false, $unhide = false
212  ) {
213  $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
214  $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
215  $this->deprecatePublicProperty( 'mOldRev', '1.32', __CLASS__ );
216  $this->deprecatePublicProperty( 'mNewRev', '1.32', __CLASS__ );
217  $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
218  $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
219  $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
220  $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
221  $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
222  $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
223  $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
224 
225  if ( $context instanceof IContextSource ) {
226  $this->setContext( $context );
227  }
228 
229  wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'\n" );
230 
231  $this->mOldid = $old;
232  $this->mNewid = $new;
233  $this->mRefreshCache = $refreshCache;
234  $this->unhide = $unhide;
235  }
236 
241  protected function getSlotDiffRenderers() {
242  if ( $this->isSlotDiffRenderer ) {
243  throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
244  }
245 
246  if ( $this->slotDiffRenderers === null ) {
247  if ( !$this->loadRevisionData() ) {
248  return [];
249  }
250 
251  $slotContents = $this->getSlotContents();
252  $this->slotDiffRenderers = array_map( function ( $contents ) {
254  $content = $contents['new'] ?: $contents['old'];
255  return $content->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
256  }, $slotContents );
257  }
259  }
260 
267  public function markAsSlotDiffRenderer() {
268  $this->isSlotDiffRenderer = true;
269  }
270 
276  protected function getSlotContents() {
277  if ( $this->isContentOverridden ) {
278  return [
279  SlotRecord::MAIN => [
280  'old' => $this->mOldContent,
281  'new' => $this->mNewContent,
282  ]
283  ];
284  } elseif ( !$this->loadRevisionData() ) {
285  return [];
286  }
287 
288  $newSlots = $this->mNewRev->getRevisionRecord()->getSlots()->getSlots();
289  if ( $this->mOldRev ) {
290  $oldSlots = $this->mOldRev->getRevisionRecord()->getSlots()->getSlots();
291  } else {
292  $oldSlots = [];
293  }
294  // The order here will determine the visual order of the diff. The current logic is
295  // slots of the new revision first in natural order, then deleted ones. This is ad hoc
296  // and should not be relied on - in the future we may want the ordering to depend
297  // on the page type.
298  $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
299 
300  $slots = [];
301  foreach ( $roles as $role ) {
302  $slots[$role] = [
303  'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
304  'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
305  ];
306  }
307  // move main slot to front
308  if ( isset( $slots[SlotRecord::MAIN] ) ) {
309  $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
310  }
311  return $slots;
312  }
313 
314  public function getTitle() {
315  // T202454 avoid errors when there is no title
316  return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
317  }
318 
325  public function setReducedLineNumbers( $value = true ) {
326  $this->mReducedLineNumbers = $value;
327  }
328 
334  public function getDiffLang() {
335  if ( $this->mDiffLang === null ) {
336  # Default language in which the diff text is written.
337  $this->mDiffLang = $this->getTitle()->getPageLanguage();
338  }
339 
340  return $this->mDiffLang;
341  }
342 
346  public function wasCacheHit() {
347  return $this->mCacheHit;
348  }
349 
357  public function getOldid() {
358  $this->loadRevisionIds();
359 
360  return $this->mOldid;
361  }
362 
369  public function getNewid() {
370  $this->loadRevisionIds();
371 
372  return $this->mNewid;
373  }
374 
381  public function getOldRevision() {
382  return $this->mOldRev ? $this->mOldRev->getRevisionRecord() : null;
383  }
384 
390  public function getNewRevision() {
391  return $this->mNewRev ? $this->mNewRev->getRevisionRecord() : null;
392  }
393 
402  public function deletedLink( $id ) {
403  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
404  if ( $permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
405  $dbr = wfGetDB( DB_REPLICA );
406  $arQuery = Revision::getArchiveQueryInfo();
407  $row = $dbr->selectRow(
408  $arQuery['tables'],
409  array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
410  [ 'ar_rev_id' => $id ],
411  __METHOD__,
412  [],
413  $arQuery['joins']
414  );
415  if ( $row ) {
416  $rev = Revision::newFromArchiveRow( $row );
417  $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
418 
419  return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
420  'target' => $title->getPrefixedText(),
421  'timestamp' => $rev->getTimestamp()
422  ] );
423  }
424  }
425 
426  return false;
427  }
428 
436  public function deletedIdMarker( $id ) {
437  $link = $this->deletedLink( $id );
438  if ( $link ) {
439  return "[$link $id]";
440  } else {
441  return (string)$id;
442  }
443  }
444 
445  private function showMissingRevision() {
446  $out = $this->getOutput();
447 
448  $missing = [];
449  if ( $this->mOldRev === null ||
450  ( $this->mOldRev && $this->mOldContent === null )
451  ) {
452  $missing[] = $this->deletedIdMarker( $this->mOldid );
453  }
454  if ( $this->mNewRev === null ||
455  ( $this->mNewRev && $this->mNewContent === null )
456  ) {
457  $missing[] = $this->deletedIdMarker( $this->mNewid );
458  }
459 
460  $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
461  $msg = $this->msg( 'difference-missing-revision' )
462  ->params( $this->getLanguage()->listToText( $missing ) )
463  ->numParams( count( $missing ) )
464  ->parseAsBlock();
465  $out->addHTML( $msg );
466  }
467 
468  public function showDiffPage( $diffOnly = false ) {
469  # Allow frames except in certain special cases
470  $out = $this->getOutput();
471  $out->allowClickjacking();
472  $out->setRobotPolicy( 'noindex,nofollow' );
473 
474  // Allow extensions to add any extra output here
475  Hooks::run( 'DifferenceEngineShowDiffPage', [ $out ] );
476 
477  if ( !$this->loadRevisionData() ) {
478  if ( Hooks::run( 'DifferenceEngineShowDiffPageMaybeShowMissingRevision', [ $this ] ) ) {
479  $this->showMissingRevision();
480  }
481  return;
482  }
483 
484  $user = $this->getUser();
485  $permErrors = [];
486  if ( $this->mNewPage ) {
487  $permErrors = $this->mNewPage->getUserPermissionsErrors( 'read', $user );
488  }
489  if ( $this->mOldPage ) {
490  $permErrors = wfMergeErrorArrays( $permErrors,
491  $this->mOldPage->getUserPermissionsErrors( 'read', $user ) );
492  }
493  if ( count( $permErrors ) ) {
494  throw new PermissionsError( 'read', $permErrors );
495  }
496 
497  $rollback = '';
498 
499  $query = [];
500  # Carry over 'diffonly' param via navigation links
501  if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
502  $query['diffonly'] = $diffOnly;
503  }
504  # Cascade unhide param in links for easy deletion browsing
505  if ( $this->unhide ) {
506  $query['unhide'] = 1;
507  }
508 
509  # Check if one of the revisions is deleted/suppressed
510  $deleted = $suppressed = false;
511  $allowed = $this->mNewRev->userCan( RevisionRecord::DELETED_TEXT, $user );
512 
513  $revisionTools = [];
514 
515  # mOldRev is false if the difference engine is called with a "vague" query for
516  # a diff between a version V and its previous version V' AND the version V
517  # is the first version of that article. In that case, V' does not exist.
518  if ( $this->mOldRev === false ) {
519  if ( $this->mNewPage ) {
520  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
521  }
522  $samePage = true;
523  $oldHeader = '';
524  // Allow extensions to change the $oldHeader variable
525  Hooks::run( 'DifferenceEngineOldHeaderNoOldRev', [ &$oldHeader ] );
526  } else {
527  Hooks::run( 'DiffViewHeader', [ $this, $this->mOldRev, $this->mNewRev ] );
528 
529  if ( !$this->mOldPage || !$this->mNewPage ) {
530  // XXX say something to the user?
531  $samePage = false;
532  } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
533  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
534  $samePage = true;
535  } else {
536  $out->setPageTitle( $this->msg( 'difference-title-multipage',
537  $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
538  $out->addSubtitle( $this->msg( 'difference-multipage' ) );
539  $samePage = false;
540  }
541 
542  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
543 
544  if ( $samePage && $this->mNewPage && $permissionManager->quickUserCan(
545  'edit', $user, $this->mNewPage
546  ) ) {
547  if ( $this->mNewRev->isCurrent() && $permissionManager->quickUserCan(
548  'rollback', $user, $this->mNewPage
549  ) ) {
550  $rollbackLink = Linker::generateRollback( $this->mNewRev, $this->getContext(),
551  [ 'noBrackets' ] );
552  if ( $rollbackLink ) {
553  $out->preventClickjacking();
554  $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
555  }
556  }
557 
558  if ( $this->userCanEdit( $this->mOldRev ) &&
559  $this->userCanEdit( $this->mNewRev )
560  ) {
561  $undoLink = Html::element( 'a', [
562  'href' => $this->mNewPage->getLocalURL( [
563  'action' => 'edit',
564  'undoafter' => $this->mOldid,
565  'undo' => $this->mNewid
566  ] ),
567  'title' => Linker::titleAttrib( 'undo' ),
568  ],
569  $this->msg( 'editundo' )->text()
570  );
571  $revisionTools['mw-diff-undo'] = $undoLink;
572  }
573  }
574 
575  # Make "previous revision link"
576  if ( $samePage && $this->mOldPage && $this->mOldRev->getPrevious() ) {
577  $prevlink = Linker::linkKnown(
578  $this->mOldPage,
579  $this->msg( 'previousdiff' )->escaped(),
580  [ 'id' => 'differences-prevlink' ],
581  [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
582  );
583  } else {
584  $prevlink = "\u{00A0}";
585  }
586 
587  if ( $this->mOldRev->isMinor() ) {
588  $oldminor = ChangesList::flag( 'minor' );
589  } else {
590  $oldminor = '';
591  }
592 
593  $ldel = $this->revisionDeleteLink( $this->mOldRev );
594  $oldRevisionHeader = $this->getRevisionHeader( $this->mOldRev, 'complete' );
595  $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
596 
597  $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
598  '<div id="mw-diff-otitle2">' .
599  Linker::revUserTools( $this->mOldRev, !$this->unhide ) . '</div>' .
600  '<div id="mw-diff-otitle3">' . $oldminor .
601  Linker::revComment( $this->mOldRev, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
602  '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
603  '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
604 
605  // Allow extensions to change the $oldHeader variable
606  Hooks::run( 'DifferenceEngineOldHeader', [ $this, &$oldHeader, $prevlink, $oldminor,
607  $diffOnly, $ldel, $this->unhide ] );
608 
609  if ( $this->mOldRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
610  $deleted = true; // old revisions text is hidden
611  if ( $this->mOldRev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
612  $suppressed = true; // also suppressed
613  }
614  }
615 
616  # Check if this user can see the revisions
617  if ( !$this->mOldRev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
618  $allowed = false;
619  }
620  }
621 
622  $out->addJsConfigVars( [
623  'wgDiffOldId' => $this->mOldid,
624  'wgDiffNewId' => $this->mNewid,
625  ] );
626 
627  # Make "next revision link"
628  # Skip next link on the top revision
629  if ( $samePage && $this->mNewPage && !$this->mNewRev->isCurrent() ) {
630  $nextlink = Linker::linkKnown(
631  $this->mNewPage,
632  $this->msg( 'nextdiff' )->escaped(),
633  [ 'id' => 'differences-nextlink' ],
634  [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
635  );
636  } else {
637  $nextlink = "\u{00A0}";
638  }
639 
640  if ( $this->mNewRev->isMinor() ) {
641  $newminor = ChangesList::flag( 'minor' );
642  } else {
643  $newminor = '';
644  }
645 
646  # Handle RevisionDelete links...
647  $rdel = $this->revisionDeleteLink( $this->mNewRev );
648 
649  # Allow extensions to define their own revision tools
650  Hooks::run( 'DiffRevisionTools',
651  [ $this->mNewRev, &$revisionTools, $this->mOldRev, $user ] );
652  $formattedRevisionTools = [];
653  // Put each one in parentheses (poor man's button)
654  foreach ( $revisionTools as $key => $tool ) {
655  $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
656  $element = Html::rawElement(
657  'span',
658  [ 'class' => $toolClass ],
659  $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
660  );
661  $formattedRevisionTools[] = $element;
662  }
663  $newRevisionHeader = $this->getRevisionHeader( $this->mNewRev, 'complete' ) .
664  ' ' . implode( ' ', $formattedRevisionTools );
665  $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
666 
667  $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
668  '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $this->mNewRev, !$this->unhide ) .
669  " $rollback</div>" .
670  '<div id="mw-diff-ntitle3">' . $newminor .
671  Linker::revComment( $this->mNewRev, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
672  '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
673  '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
674 
675  // Allow extensions to change the $newHeader variable
676  Hooks::run( 'DifferenceEngineNewHeader', [ $this, &$newHeader, $formattedRevisionTools,
677  $nextlink, $rollback, $newminor, $diffOnly, $rdel, $this->unhide ] );
678 
679  if ( $this->mNewRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
680  $deleted = true; // new revisions text is hidden
681  if ( $this->mNewRev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
682  $suppressed = true; // also suppressed
683  }
684  }
685 
686  # If the diff cannot be shown due to a deleted revision, then output
687  # the diff header and links to unhide (if available)...
688  if ( $deleted && ( !$this->unhide || !$allowed ) ) {
689  $this->showDiffStyle();
690  $multi = $this->getMultiNotice();
691  $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
692  if ( !$allowed ) {
693  $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
694  # Give explanation for why revision is not visible
695  $out->wrapWikiMsg( "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
696  [ $msg ] );
697  } else {
698  # Give explanation and add a link to view the diff...
699  $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
700  $link = $this->getTitle()->getFullURL( $query );
701  $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
702  $out->wrapWikiMsg(
703  "<div id='mw-$msg' class='mw-warning plainlinks'>\n$1\n</div>\n",
704  [ $msg, $link ]
705  );
706  }
707  # Otherwise, output a regular diff...
708  } else {
709  # Add deletion notice if the user is viewing deleted content
710  $notice = '';
711  if ( $deleted ) {
712  $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
713  $notice = "<div id='mw-$msg' class='mw-warning plainlinks'>\n" .
714  $this->msg( $msg )->parse() .
715  "</div>\n";
716  }
717  $this->showDiff( $oldHeader, $newHeader, $notice );
718  if ( !$diffOnly ) {
719  $this->renderNewRevision();
720  }
721  }
722  }
723 
733  public function markPatrolledLink() {
734  if ( $this->mMarkPatrolledLink === null ) {
735  $linkInfo = $this->getMarkPatrolledLinkInfo();
736  // If false, there is no patrol link needed/allowed
737  if ( !$linkInfo || !$this->mNewPage ) {
738  $this->mMarkPatrolledLink = '';
739  } else {
740  $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
742  $this->mNewPage,
743  $this->msg( 'markaspatrolleddiff' )->escaped(),
744  [],
745  [
746  'action' => 'markpatrolled',
747  'rcid' => $linkInfo['rcid'],
748  ]
749  ) . ']</span>';
750  // Allow extensions to change the markpatrolled link
751  Hooks::run( 'DifferenceEngineMarkPatrolledLink', [ $this,
752  &$this->mMarkPatrolledLink, $linkInfo['rcid'] ] );
753  }
754  }
756  }
757 
765  protected function getMarkPatrolledLinkInfo() {
766  $user = $this->getUser();
767  $config = $this->getConfig();
768  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
769 
770  // Prepare a change patrol link, if applicable
771  if (
772  // Is patrolling enabled and the user allowed to?
773  $config->get( 'UseRCPatrol' ) &&
774  $this->mNewPage &&
775  $permissionManager->quickUserCan( 'patrol', $user, $this->mNewPage ) &&
776  // Only do this if the revision isn't more than 6 hours older
777  // than the Max RC age (6h because the RC might not be cleaned out regularly)
778  RecentChange::isInRCLifespan( $this->mNewRev->getTimestamp(), 21600 )
779  ) {
780  // Look for an unpatrolled change corresponding to this diff
781  $db = wfGetDB( DB_REPLICA );
782  $change = RecentChange::newFromConds(
783  [
784  'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
785  'rc_this_oldid' => $this->mNewid,
786  'rc_patrolled' => RecentChange::PRC_UNPATROLLED
787  ],
788  __METHOD__
789  );
790 
791  if ( $change && !$change->getPerformer()->equals( $user ) ) {
792  $rcid = $change->getAttribute( 'rc_id' );
793  } else {
794  // None found or the page has been created by the current user.
795  // If the user could patrol this it already would be patrolled
796  $rcid = 0;
797  }
798 
799  // Allow extensions to possibly change the rcid here
800  // For example the rcid might be set to zero due to the user
801  // being the same as the performer of the change but an extension
802  // might still want to show it under certain conditions
803  Hooks::run( 'DifferenceEngineMarkPatrolledRCID', [ &$rcid, $this, $change, $user ] );
804 
805  // Build the link
806  if ( $rcid ) {
807  $this->getOutput()->preventClickjacking();
808  if ( $permissionManager->userHasRight( $user, 'writeapi' ) ) {
809  $this->getOutput()->addModules( 'mediawiki.page.patrol.ajax' );
810  }
811 
812  return [
813  'rcid' => $rcid,
814  ];
815  }
816  }
817 
818  // No mark as patrolled link applicable
819  return false;
820  }
821 
827  protected function revisionDeleteLink( $rev ) {
828  $link = Linker::getRevDeleteLink( $this->getUser(), $rev, $rev->getTitle() );
829  if ( $link !== '' ) {
830  $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
831  }
832 
833  return $link;
834  }
835 
841  public function renderNewRevision() {
842  if ( $this->isContentOverridden ) {
843  // The code below only works with a Revision object. We could construct a fake revision
844  // (here or in setContent), but since this does not seem needed at the moment,
845  // we'll just fail for now.
846  throw new LogicException(
847  __METHOD__
848  . ' is not supported after calling setContent(). Use setRevisions() instead.'
849  );
850  }
851 
852  $out = $this->getOutput();
853  $revHeader = $this->getRevisionHeader( $this->mNewRev );
854  # Add "current version as of X" title
855  $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
856  <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
857  # Page content may be handled by a hooked call instead...
858  if ( Hooks::run( 'ArticleContentOnDiff', [ $this, $out ] ) ) {
859  $this->loadNewText();
860  if ( !$this->mNewPage ) {
861  // New revision is unsaved; bail out.
862  // TODO in theory rendering the new revision is a meaningful thing to do
863  // even if it's unsaved, but a lot of untangling is required to do it safely.
864  return;
865  }
866 
867  $out->setRevisionId( $this->mNewid );
868  $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
869  $out->setArticleFlag( true );
870 
871  if ( !Hooks::run( 'ArticleRevisionViewCustom',
872  [ $this->mNewRev->getRevisionRecord(), $this->mNewPage, $out ] )
873  ) {
874  // Handled by extension
875  // NOTE: sync with hooks called in Article::view()
876  } elseif ( !Hooks::run( 'ArticleContentViewCustom',
877  [ $this->mNewContent, $this->mNewPage, $out ], '1.32' )
878  ) {
879  // Handled by extension
880  // NOTE: sync with hooks called in Article::view()
881  } else {
882  // Normal page
883  if ( $this->getTitle()->equals( $this->mNewPage ) ) {
884  // If the Title stored in the context is the same as the one
885  // of the new revision, we can use its associated WikiPage
886  // object.
887  $wikiPage = $this->getWikiPage();
888  } else {
889  // Otherwise we need to create our own WikiPage object
890  $wikiPage = WikiPage::factory( $this->mNewPage );
891  }
892 
893  $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
894 
895  # WikiPage::getParserOutput() should not return false, but just in case
896  if ( $parserOutput ) {
897  // Allow extensions to change parser output here
898  if ( Hooks::run( 'DifferenceEngineRenderRevisionAddParserOutput',
899  [ $this, $out, $parserOutput, $wikiPage ] )
900  ) {
901  $out->addParserOutput( $parserOutput, [
902  'enableSectionEditLinks' => $this->mNewRev->isCurrent()
903  && MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(
904  'edit',
905  $this->getUser(),
906  $this->mNewRev->getTitle()
907  )
908  ] );
909  }
910  }
911  }
912  }
913 
914  // Allow extensions to optionally not show the final patrolled link
915  if ( Hooks::run( 'DifferenceEngineRenderRevisionShowFinalPatrolLink' ) ) {
916  # Add redundant patrol link on bottom...
917  $out->addHTML( $this->markPatrolledLink() );
918  }
919  }
920 
927  protected function getParserOutput( WikiPage $page, Revision $rev ) {
928  if ( !$rev->getId() ) {
929  // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
930  // the current revision, so fail instead. If need be, WikiPage::getParserOutput
931  // could be made to accept a Revision or RevisionRecord instead of the id.
932  return false;
933  }
934 
935  $parserOptions = $page->makeParserOptions( $this->getContext() );
936  $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
937 
938  return $parserOutput;
939  }
940 
951  public function showDiff( $otitle, $ntitle, $notice = '' ) {
952  // Allow extensions to affect the output here
953  Hooks::run( 'DifferenceEngineShowDiff', [ $this ] );
954 
955  $diff = $this->getDiff( $otitle, $ntitle, $notice );
956  if ( $diff === false ) {
957  $this->showMissingRevision();
958 
959  return false;
960  } else {
961  $this->showDiffStyle();
962  $this->getOutput()->addHTML( $diff );
963 
964  return true;
965  }
966  }
967 
971  public function showDiffStyle() {
972  if ( !$this->isSlotDiffRenderer ) {
973  $this->getOutput()->addModuleStyles( [
974  'mediawiki.interface.helpers.styles',
975  'mediawiki.diff.styles'
976  ] );
977  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
978  $slotDiffRenderer->addModules( $this->getOutput() );
979  }
980  }
981  }
982 
992  public function getDiff( $otitle, $ntitle, $notice = '' ) {
993  $body = $this->getDiffBody();
994  if ( $body === false ) {
995  return false;
996  }
997 
998  $multi = $this->getMultiNotice();
999  // Display a message when the diff is empty
1000  if ( $body === '' ) {
1001  $notice .= '<div class="mw-diff-empty">' .
1002  $this->msg( 'diff-empty' )->parse() .
1003  "</div>\n";
1004  }
1005 
1006  return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1007  }
1008 
1014  public function getDiffBody() {
1015  $this->mCacheHit = true;
1016  // Check if the diff should be hidden from this user
1017  if ( !$this->isContentOverridden ) {
1018  if ( !$this->loadRevisionData() ) {
1019  return false;
1020  } elseif ( $this->mOldRev &&
1021  !$this->mOldRev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
1022  ) {
1023  return false;
1024  } elseif ( $this->mNewRev &&
1025  !$this->mNewRev->userCan( RevisionRecord::DELETED_TEXT, $this->getUser() )
1026  ) {
1027  return false;
1028  }
1029  // Short-circuit
1030  if ( $this->mOldRev === false || ( $this->mOldRev && $this->mNewRev &&
1031  $this->mOldRev->getId() && $this->mOldRev->getId() == $this->mNewRev->getId() )
1032  ) {
1033  if ( Hooks::run( 'DifferenceEngineShowEmptyOldContent', [ $this ] ) ) {
1034  return '';
1035  }
1036  }
1037  }
1038 
1039  // Cacheable?
1040  $key = false;
1041  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1042  if ( $this->mOldid && $this->mNewid ) {
1043  // Check if subclass is still using the old way
1044  // for backwards-compatibility
1045  $key = $this->getDiffBodyCacheKey();
1046  if ( $key === null ) {
1047  $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1048  }
1049 
1050  // Try cache
1051  if ( !$this->mRefreshCache ) {
1052  $difftext = $cache->get( $key );
1053  if ( is_string( $difftext ) ) {
1054  wfIncrStats( 'diff_cache.hit' );
1055  $difftext = $this->localiseDiff( $difftext );
1056  $difftext .= "\n<!-- diff cache key $key -->\n";
1057 
1058  return $difftext;
1059  }
1060  } // don't try to load but save the result
1061  }
1062  $this->mCacheHit = false;
1063 
1064  // Loadtext is permission safe, this just clears out the diff
1065  if ( !$this->loadText() ) {
1066  return false;
1067  }
1068 
1069  $difftext = '';
1070  // We've checked for revdelete at the beginning of this method; it's OK to ignore
1071  // read permissions here.
1072  $slotContents = $this->getSlotContents();
1073  foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1074  $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1075  $slotContents[$role]['new'] );
1076  if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1077  // FIXME: ask SlotRoleHandler::getSlotNameMessage
1078  $slotTitle = $role;
1079  $difftext .= $this->getSlotHeader( $slotTitle );
1080  }
1081  $difftext .= $slotDiff;
1082  }
1083 
1084  // Avoid PHP 7.1 warning from passing $this by reference
1085  $diffEngine = $this;
1086 
1087  // Save to cache for 7 days
1088  if ( !Hooks::run( 'AbortDiffCache', [ &$diffEngine ] ) ) {
1089  wfIncrStats( 'diff_cache.uncacheable' );
1090  } elseif ( $key !== false && $difftext !== false ) {
1091  wfIncrStats( 'diff_cache.miss' );
1092  $cache->set( $key, $difftext, 7 * 86400 );
1093  } else {
1094  wfIncrStats( 'diff_cache.uncacheable' );
1095  }
1096  // localise line numbers and title attribute text
1097  if ( $difftext !== false ) {
1098  $difftext = $this->localiseDiff( $difftext );
1099  }
1100 
1101  return $difftext;
1102  }
1103 
1110  public function getDiffBodyForRole( $role ) {
1111  $diffRenderers = $this->getSlotDiffRenderers();
1112  if ( !isset( $diffRenderers[$role] ) ) {
1113  return false;
1114  }
1115 
1116  $slotContents = $this->getSlotContents();
1117  $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1118  $slotContents[$role]['new'] );
1119  if ( !$slotDiff ) {
1120  return false;
1121  }
1122 
1123  if ( $role !== SlotRecord::MAIN ) {
1124  // TODO use human-readable role name at least
1125  $slotTitle = $role;
1126  $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1127  }
1128 
1129  return $this->localiseDiff( $slotDiff );
1130  }
1131 
1139  protected function getSlotHeader( $headerText ) {
1140  // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1141  $columnCount = $this->mOldRev ? 4 : 2;
1142  $userLang = $this->getLanguage()->getHtmlCode();
1143  return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1144  Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1145  }
1146 
1156  protected function getDiffBodyCacheKey() {
1157  return null;
1158  }
1159 
1173  protected function getDiffBodyCacheKeyParams() {
1174  if ( !$this->mOldid || !$this->mNewid ) {
1175  throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1176  }
1177 
1178  $engine = $this->getEngine();
1179  $params = [
1180  'diff',
1181  $engine,
1182  self::DIFF_VERSION,
1183  "old-{$this->mOldid}",
1184  "rev-{$this->mNewid}"
1185  ];
1186 
1187  if ( $engine === 'wikidiff2' ) {
1188  $params[] = phpversion( 'wikidiff2' );
1189  }
1190 
1191  if ( !$this->isSlotDiffRenderer ) {
1192  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1193  $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1194  }
1195  }
1196 
1197  return $params;
1198  }
1199 
1207  public function getExtraCacheKeys() {
1208  // This method is called when the DifferenceEngine is used for a slot diff. We only care
1209  // about special things, not the revision IDs, which are added to the cache key by the
1210  // page-level DifferenceEngine, and which might not have a valid value for this object.
1211  $this->mOldid = 123456789;
1212  $this->mNewid = 987654321;
1213 
1214  // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1215  $cacheString = $this->getDiffBodyCacheKey();
1216  if ( $cacheString ) {
1217  return [ $cacheString ];
1218  }
1219 
1220  $params = $this->getDiffBodyCacheKeyParams();
1221 
1222  // Try to get rid of the standard keys to keep the cache key human-readable:
1223  // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1224  // the child class includes the same keys, drop them.
1225  // Uses an obscure PHP feature where static calls to non-static methods are allowed
1226  // as long as we are already in a non-static method of the same class, and the call context
1227  // ($this) will be inherited.
1228  // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1229  $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1230  if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1231  $params = array_slice( $params, count( $standardParams ) );
1232  }
1233 
1234  return $params;
1235  }
1236 
1250  public function generateContentDiffBody( Content $old, Content $new ) {
1251  $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1252  if (
1253  $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1254  && $this->isSlotDiffRenderer
1255  ) {
1256  // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1257  // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1258  // This will happen when a content model has no custom slot diff renderer, it does have
1259  // a custom difference engine, but that does not override this method.
1260  throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1261  . 'Please use a SlotDiffRenderer.' );
1262  }
1263  return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1264  }
1265 
1278  public function generateTextDiffBody( $otext, $ntext ) {
1279  $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1280  ->getSlotDiffRenderer( $this->getContext() );
1281  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1282  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1283  // This is too unlikely to happen to bother handling properly.
1284  throw new Exception( 'The slot diff renderer for text content should be a '
1285  . 'TextSlotDiffRenderer subclass' );
1286  }
1287  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1288  }
1289 
1296  public static function getEngine() {
1297  $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1298  ->get( 'ExternalDiffEngine' );
1299 
1300  if ( $externalDiffEngine ) {
1301  if ( is_string( $externalDiffEngine ) ) {
1302  if ( is_executable( $externalDiffEngine ) ) {
1303  return $externalDiffEngine;
1304  }
1305  wfDebug( 'ExternalDiffEngine config points to a non-executable, ignoring' );
1306  } else {
1307  wfWarn( 'ExternalDiffEngine config is set to a non-string value, ignoring' );
1308  }
1309  }
1310 
1311  if ( function_exists( 'wikidiff2_do_diff' ) ) {
1312  return 'wikidiff2';
1313  }
1314 
1315  // Native PHP
1316  return false;
1317  }
1318 
1331  protected function textDiff( $otext, $ntext ) {
1332  $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1333  ->getSlotDiffRenderer( $this->getContext() );
1334  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1335  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1336  // This is too unlikely to happen to bother handling properly.
1337  throw new Exception( 'The slot diff renderer for text content should be a '
1338  . 'TextSlotDiffRenderer subclass' );
1339  }
1340  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1341  }
1342 
1351  protected function debug( $generator = "internal" ) {
1352  if ( !$this->enableDebugComment ) {
1353  return '';
1354  }
1355  $data = [ $generator ];
1356  if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1357  $data[] = wfHostname();
1358  }
1359  $data[] = wfTimestamp( TS_DB );
1360 
1361  return "<!-- diff generator: " .
1362  implode( " ", array_map( "htmlspecialchars", $data ) ) .
1363  " -->\n";
1364  }
1365 
1366  private function getDebugString() {
1367  $engine = self::getEngine();
1368  if ( $engine === 'wikidiff2' ) {
1369  return $this->debug( 'wikidiff2' );
1370  } elseif ( $engine === false ) {
1371  return $this->debug( 'native PHP' );
1372  } else {
1373  return $this->debug( "external $engine" );
1374  }
1375  }
1376 
1383  private function localiseDiff( $text ) {
1384  $text = $this->localiseLineNumbers( $text );
1385  if ( $this->getEngine() === 'wikidiff2' &&
1386  version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1387  ) {
1388  $text = $this->addLocalisedTitleTooltips( $text );
1389  }
1390  return $text;
1391  }
1392 
1400  public function localiseLineNumbers( $text ) {
1401  return preg_replace_callback(
1402  '/<!--LINE (\d+)-->/',
1403  [ $this, 'localiseLineNumbersCb' ],
1404  $text
1405  );
1406  }
1407 
1408  public function localiseLineNumbersCb( $matches ) {
1409  if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1410  return '';
1411  }
1412 
1413  return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1414  }
1415 
1422  private function addLocalisedTitleTooltips( $text ) {
1423  return preg_replace_callback(
1424  '/class="mw-diff-movedpara-(left|right)"/',
1425  [ $this, 'addLocalisedTitleTooltipsCb' ],
1426  $text
1427  );
1428  }
1429 
1434  private function addLocalisedTitleTooltipsCb( array $matches ) {
1435  $key = $matches[1] === 'right' ?
1436  'diff-paragraph-moved-toold' :
1437  'diff-paragraph-moved-tonew';
1438  return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1439  }
1440 
1446  public function getMultiNotice() {
1447  // The notice only make sense if we are diffing two saved revisions of the same page.
1448  if (
1449  !$this->mOldRev || !$this->mNewRev
1450  || !$this->mOldPage || !$this->mNewPage
1451  || !$this->mOldPage->equals( $this->mNewPage )
1452  ) {
1453  return '';
1454  }
1455 
1456  if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1457  $oldRev = $this->mNewRev; // flip
1458  $newRev = $this->mOldRev; // flip
1459  } else { // normal case
1460  $oldRev = $this->mOldRev;
1461  $newRev = $this->mNewRev;
1462  }
1463 
1464  // Sanity: don't show the notice if too many rows must be scanned
1465  // @todo show some special message for that case
1466  $nEdits = MediaWikiServices::getInstance()->getRevisionStore()
1467  ->countRevisionsBetween(
1468  $oldRev->getRevisionRecord(),
1469  $newRev->getRevisionRecord(),
1470  1000
1471  );
1472  if ( $nEdits > 0 && $nEdits <= 1000 ) {
1473  $limit = 100; // use diff-multi-manyusers if too many users
1474  $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1475  $numUsers = count( $users );
1476 
1477  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable False positive
1478  if ( $numUsers == 1 && $users[0] == $newRev->getUserText( RevisionRecord::RAW ) ) {
1479  $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1480  }
1481 
1482  return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1483  }
1484 
1485  return '';
1486  }
1487 
1497  public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1498  if ( $numUsers === 0 ) {
1499  $msg = 'diff-multi-sameuser';
1500  } elseif ( $numUsers > $limit ) {
1501  $msg = 'diff-multi-manyusers';
1502  $numUsers = $limit;
1503  } else {
1504  $msg = 'diff-multi-otherusers';
1505  }
1506 
1507  return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1508  }
1509 
1514  private function userCanEdit( Revision $rev ) {
1515  $user = $this->getUser();
1516 
1517  if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
1518  return false;
1519  }
1520 
1521  return true;
1522  }
1523 
1533  public function getRevisionHeader( Revision $rev, $complete = '' ) {
1534  $lang = $this->getLanguage();
1535  $user = $this->getUser();
1536  $revtimestamp = $rev->getTimestamp();
1537  $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1538  $dateofrev = $lang->userDate( $revtimestamp, $user );
1539  $timeofrev = $lang->userTime( $revtimestamp, $user );
1540 
1541  $header = $this->msg(
1542  $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1543  $timestamp,
1544  $dateofrev,
1545  $timeofrev
1546  )->escaped();
1547 
1548  if ( $complete !== 'complete' ) {
1549  return $header;
1550  }
1551 
1552  $title = $rev->getTitle();
1553 
1555  [ 'oldid' => $rev->getId() ] );
1556 
1557  if ( $this->userCanEdit( $rev ) ) {
1558  $editQuery = [ 'action' => 'edit' ];
1559  if ( !$rev->isCurrent() ) {
1560  $editQuery['oldid'] = $rev->getId();
1561  }
1562 
1563  $key = MediaWikiServices::getInstance()->getPermissionManager()
1564  ->quickUserCan( 'edit', $user, $title ) ? 'editold' : 'viewsourceold';
1565  $msg = $this->msg( $key )->escaped();
1566  $editLink = $this->msg( 'parentheses' )->rawParams(
1567  Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
1568  $header .= ' ' . Html::rawElement(
1569  'span',
1570  [ 'class' => 'mw-diff-edit' ],
1571  $editLink
1572  );
1573  if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1575  'span',
1576  [ 'class' => 'history-deleted' ],
1577  $header
1578  );
1579  }
1580  } else {
1581  $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1582  }
1583 
1584  return $header;
1585  }
1586 
1599  public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1600  // shared.css sets diff in interface language/dir, but the actual content
1601  // is often in a different language, mostly the page content language/dir
1602  $header = Html::openElement( 'table', [
1603  'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1604  'data-mw' => 'interface',
1605  ] );
1606  $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1607 
1608  if ( !$diff && !$otitle ) {
1609  $header .= "
1610  <tr class=\"diff-title\" lang=\"{$userLang}\">
1611  <td class=\"diff-ntitle\">{$ntitle}</td>
1612  </tr>";
1613  $multiColspan = 1;
1614  } else {
1615  if ( $diff ) { // Safari/Chrome show broken output if cols not used
1616  $header .= "
1617  <col class=\"diff-marker\" />
1618  <col class=\"diff-content\" />
1619  <col class=\"diff-marker\" />
1620  <col class=\"diff-content\" />";
1621  $colspan = 2;
1622  $multiColspan = 4;
1623  } else {
1624  $colspan = 1;
1625  $multiColspan = 2;
1626  }
1627  if ( $otitle || $ntitle ) {
1628  $header .= "
1629  <tr class=\"diff-title\" lang=\"{$userLang}\">
1630  <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1631  <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1632  </tr>";
1633  }
1634  }
1635 
1636  if ( $multi != '' ) {
1637  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1638  "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1639  }
1640  if ( $notice != '' ) {
1641  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1642  "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1643  }
1644 
1645  return $header . $diff . "</table>";
1646  }
1647 
1655  public function setContent( Content $oldContent, Content $newContent ) {
1656  $this->mOldContent = $oldContent;
1657  $this->mNewContent = $newContent;
1658 
1659  $this->mTextLoaded = 2;
1660  $this->mRevisionsLoaded = true;
1661  $this->isContentOverridden = true;
1662  $this->slotDiffRenderers = null;
1663  }
1664 
1670  public function setRevisions(
1671  ?RevisionRecord $oldRevision, RevisionRecord $newRevision
1672  ) {
1673  if ( $oldRevision ) {
1674  $this->mOldRev = new Revision( $oldRevision );
1675  $this->mOldid = $oldRevision->getId();
1676  $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1677  // This method is meant for edit diffs and such so there is no reason to provide a
1678  // revision that's not readable to the user, but check it just in case.
1679  $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1680  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1681  } else {
1682  $this->mOldPage = null;
1683  $this->mOldRev = $this->mOldid = false;
1684  }
1685  $this->mNewRev = new Revision( $newRevision );
1686  $this->mNewid = $newRevision->getId();
1687  $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1688  $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1689  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1690 
1691  $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1692  $this->mTextLoaded = $oldRevision ? 2 : 1;
1693  $this->isContentOverridden = false;
1694  $this->slotDiffRenderers = null;
1695  }
1696 
1703  public function setTextLanguage( Language $lang ) {
1704  $this->mDiffLang = $lang;
1705  }
1706 
1718  public function mapDiffPrevNext( $old, $new ) {
1719  $rl = MediaWikiServices::getInstance()->getRevisionLookup();
1720  if ( $new === 'prev' ) {
1721  // Show diff between revision $old and the previous one. Get previous one from DB.
1722  $newid = intval( $old );
1723  $oldid = false;
1724  $newRev = $rl->getRevisionById( $newid );
1725  if ( $newRev ) {
1726  $oldRev = $rl->getPreviousRevision( $newRev );
1727  if ( $oldRev ) {
1728  $oldid = $oldRev->getId();
1729  }
1730  }
1731  } elseif ( $new === 'next' ) {
1732  // Show diff between revision $old and the next one. Get next one from DB.
1733  $oldid = intval( $old );
1734  $newid = false;
1735  $oldRev = $rl->getRevisionById( $oldid );
1736  if ( $oldRev ) {
1737  $newRev = $rl->getNextRevision( $oldRev );
1738  if ( $newRev ) {
1739  $newid = $newRev->getId();
1740  }
1741  }
1742  } else {
1743  $oldid = intval( $old );
1744  $newid = intval( $new );
1745  }
1746 
1747  return [ $oldid, $newid ];
1748  }
1749 
1753  private function loadRevisionIds() {
1754  if ( $this->mRevisionsIdsLoaded ) {
1755  return;
1756  }
1757 
1758  $this->mRevisionsIdsLoaded = true;
1759 
1760  $old = $this->mOldid;
1761  $new = $this->mNewid;
1762 
1763  list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1764  if ( $new === 'next' && $this->mNewid === false ) {
1765  # if no result, NewId points to the newest old revision. The only newer
1766  # revision is cur, which is "0".
1767  $this->mNewid = 0;
1768  }
1769 
1770  Hooks::run(
1771  'NewDifferenceEngine',
1772  [ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1773  );
1774  }
1775 
1789  public function loadRevisionData() {
1790  if ( $this->mRevisionsLoaded ) {
1791  return $this->isContentOverridden || ( $this->mOldRev !== null && $this->mNewRev !== null );
1792  }
1793 
1794  // Whether it succeeds or fails, we don't want to try again
1795  $this->mRevisionsLoaded = true;
1796 
1797  $this->loadRevisionIds();
1798 
1799  // Load the new revision object
1800  if ( $this->mNewid ) {
1801  $this->mNewRev = Revision::newFromId( $this->mNewid );
1802  } else {
1803  $this->mNewRev = Revision::newFromTitle(
1804  $this->getTitle(),
1805  false,
1806  Revision::READ_NORMAL
1807  );
1808  }
1809 
1810  if ( !$this->mNewRev instanceof Revision ) {
1811  return false;
1812  }
1813 
1814  // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1815  $this->mNewid = $this->mNewRev->getId();
1816  if ( $this->mNewid ) {
1817  $this->mNewPage = $this->mNewRev->getTitle();
1818  } else {
1819  $this->mNewPage = null;
1820  }
1821 
1822  // Load the old revision object
1823  $this->mOldRev = false;
1824  if ( $this->mOldid ) {
1825  $this->mOldRev = Revision::newFromId( $this->mOldid );
1826  } elseif ( $this->mOldid === 0 ) {
1827  $rev = $this->mNewRev->getPrevious();
1828  if ( $rev ) {
1829  $this->mOldid = $rev->getId();
1830  $this->mOldRev = $rev;
1831  } else {
1832  // No previous revision; mark to show as first-version only.
1833  $this->mOldid = false;
1834  $this->mOldRev = false;
1835  }
1836  } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1837 
1838  if ( is_null( $this->mOldRev ) ) {
1839  return false;
1840  }
1841 
1842  if ( $this->mOldRev && $this->mOldRev->getId() ) {
1843  $this->mOldPage = $this->mOldRev->getTitle();
1844  } else {
1845  $this->mOldPage = null;
1846  }
1847 
1848  // Load tags information for both revisions
1849  $dbr = wfGetDB( DB_REPLICA );
1850  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
1851  if ( $this->mOldid !== false ) {
1852  $tagIds = $dbr->selectFieldValues(
1853  'change_tag',
1854  'ct_tag_id',
1855  [ 'ct_rev_id' => $this->mOldid ],
1856  __METHOD__
1857  );
1858  $tags = [];
1859  foreach ( $tagIds as $tagId ) {
1860  try {
1861  $tags[] = $changeTagDefStore->getName( (int)$tagId );
1862  } catch ( NameTableAccessException $exception ) {
1863  continue;
1864  }
1865  }
1866  $this->mOldTags = implode( ',', $tags );
1867  } else {
1868  $this->mOldTags = false;
1869  }
1870 
1871  $tagIds = $dbr->selectFieldValues(
1872  'change_tag',
1873  'ct_tag_id',
1874  [ 'ct_rev_id' => $this->mNewid ],
1875  __METHOD__
1876  );
1877  $tags = [];
1878  foreach ( $tagIds as $tagId ) {
1879  try {
1880  $tags[] = $changeTagDefStore->getName( (int)$tagId );
1881  } catch ( NameTableAccessException $exception ) {
1882  continue;
1883  }
1884  }
1885  $this->mNewTags = implode( ',', $tags );
1886 
1887  return true;
1888  }
1889 
1898  public function loadText() {
1899  if ( $this->mTextLoaded == 2 ) {
1900  return $this->loadRevisionData() && ( $this->mOldRev === false || $this->mOldContent )
1901  && $this->mNewContent;
1902  }
1903 
1904  // Whether it succeeds or fails, we don't want to try again
1905  $this->mTextLoaded = 2;
1906 
1907  if ( !$this->loadRevisionData() ) {
1908  return false;
1909  }
1910 
1911  if ( $this->mOldRev ) {
1912  $this->mOldContent = $this->mOldRev->getContent(
1913  RevisionRecord::FOR_THIS_USER, $this->getUser()
1914  );
1915  if ( $this->mOldContent === null ) {
1916  return false;
1917  }
1918  }
1919 
1920  $this->mNewContent = $this->mNewRev->getContent(
1921  RevisionRecord::FOR_THIS_USER, $this->getUser()
1922  );
1923  Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
1924  if ( $this->mNewContent === null ) {
1925  return false;
1926  }
1927 
1928  return true;
1929  }
1930 
1936  public function loadNewText() {
1937  if ( $this->mTextLoaded >= 1 ) {
1938  return $this->loadRevisionData();
1939  }
1940 
1941  $this->mTextLoaded = 1;
1942 
1943  if ( !$this->loadRevisionData() ) {
1944  return false;
1945  }
1946 
1947  $this->mNewContent = $this->mNewRev->getContent(
1948  RevisionRecord::FOR_THIS_USER, $this->getUser()
1949  );
1950 
1951  Hooks::run( 'DifferenceEngineAfterLoadNewText', [ $this ] );
1952 
1953  return true;
1954  }
1955 
1956 }
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:142
Revision null false $mOldRev
Old revision (left pane).
setContext(IContextSource $context)
userCan( $field, User $user=null)
Determine if the current user is allowed to view a particular field of this revision, if it&#39;s marked as deleted.
Definition: Revision.php:1022
bool $mCacheHit
Was the diff fetched from cache?
DifferenceEngine is responsible for rendering the difference between two revisions as HTML...
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
#-
$enableDebugComment
Set this to true to add debug info to the HTML output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
static newFromArchiveRow( $row, $overrides=[])
Make a fake revision object from an archive table row.
Definition: Revision.php:172
getDiffBody()
Get the diff table body, without header.
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:231
setRevisions(?RevisionRecord $oldRevision, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
userCanEdit(Revision $rev)
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
markAsSlotDiffRenderer()
Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
getTimestamp()
Definition: Revision.php:798
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Exception representing a failure to look up a row from a name table.
int false null $mOldid
Revision ID for the old revision.
wfHostname()
Fetch server name for use in error reporting etc.
if(!isset( $args[0])) $lang
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing &#39;/&#39;...
Definition: Html.php:251
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
string [] null $mOldTags
Change tags of $mOldRev or null if it does not exist / is not saved.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
static intermediateEditsMsg( $numEdits, $numUsers, $limit)
Get a notice about how many intermediate edits and users there are.
getParserOutput(WikiPage $page, Revision $rev)
const NS_SPECIAL
Definition: Defines.php:49
getMarkPatrolledLinkInfo()
Returns an array of meta data needed to build a "mark as patrolled" link and adds the mediawiki...
showDiffPage( $diffOnly=false)
const CONTENT_MODEL_TEXT
Definition: Defines.php:218
getExtraCacheKeys()
Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys().
B/C adapter for turning a DifferenceEngine into a SlotDiffRenderer.
const DIFF_VERSION
Constant to indicate diff cache compatibility.
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e...
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1806
loadRevisionIds()
Load revision IDs.
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given link target...
Definition: Revision.php:138
getNewRevision()
Get the right side of the diff.
IContextSource $context
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend...
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
Title null $mOldPage
Title of $mOldRev or null if the old revision does not exist or does not belong to a page...
generateContentDiffBody(Content $old, Content $new)
Generate a diff, no caching.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static getRevisionStore( $wiki=false)
Definition: Revision.php:63
string $mMarkPatrolledLink
Link to action=markpatrolled.
getTitle()
Returns the title of the page associated with this entry.
Definition: Revision.php:559
wfMergeErrorArrays(... $args)
Merge arrays in the style of getUserPermissionsErrors, with duplicate removal e.g.
wfIncrStats( $key, $count=1)
Increment a statistics counter.
localiseLineNumbers( $text)
Replace line numbers with the text in the user&#39;s language.
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Content null $mOldContent
renderNewRevision()
Show the new revision of the page.
static titleAttrib( $name, $options=null, array $msgParams=[])
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
Definition: Linker.php:2021
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
bool $mRefreshCache
Refresh the diff cache.
Renders a slot diff by doing a text diff on the native representation.
Revision null $mNewRev
New revision (right pane).
isDeleted( $field)
Definition: Revision.php:692
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
getId()
Get revision ID.
Definition: Revision.php:442
bool $mRevisionsIdsLoaded
Have the revisions IDs been loaded.
getSlotContents()
Get the old and new content objects for all slots.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
markPatrolledLink()
Build a link to mark a change as patrolled.
getContent( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns the Content of the given slot of this revision.
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
static getEngine()
Process ExternalDiffEngine config and get a sane, usable engine.
addLocalisedTitleTooltipsCb(array $matches)
getContext()
Get the base IContextSource object.
textDiff( $otext, $ntext)
Generates diff, to be wrapped internally in a logging/instrumentation.
$cache
Definition: mcc.php:33
Title null $mNewPage
Title of $mNewRev or null if the new revision does not exist or does not belong to a page...
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
bool $unhide
Show rev_deleted content if allowed.
static getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new archived revision objec...
Definition: Revision.php:329
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php:83
$header
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
getDiffBodyCacheKeyParams()
Get the cache key parameters.
getNewid()
Get the ID of new revision (right pane) of the diff.
getOldRevision()
Get the left side of the diff.
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:612
const PRC_UNPATROLLED
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:271
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1961
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
static isInRCLifespan( $timestamp, $tolerance=0)
Check whether the given timestamp is new enough to have a RC row with a given tolerance as the recent...
getId()
Get revision ID.
addLocalisedTitleTooltips( $text)
Add title attributes for tooltips on moved paragraph indicators.
static getRevDeleteLink(User $user, Revision $rev, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition: Linker.php:2105
getOldid()
Get the ID of old revision (left pane) of the diff.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated.
static linkKnown( $target, $html=null, $customAttribs=[], $query=[], $options=[ 'known'])
Identical to link(), except $options defaults to &#39;known&#39;.
Definition: Linker.php:141
localiseDiff( $text)
Localise diff output.
int string false null $mNewid
Revision ID for the new revision.
bool $mRevisionsLoaded
Have the revisions been loaded.
Show an error when a user tries to do something they do not have the necessary permissions for...
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
bool $isSlotDiffRenderer
Temporary hack for B/C while slot diff related methods of DifferenceEngine are being deprecated...
showDiffStyle()
Add style sheets for diff display.
bool $mReducedLineNumbers
If true, line X is not displayed when X is 1, for example to increase readability and conserve space ...
loadNewText()
Load the text of the new revision, not the old one.
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:94
static revComment(Revision $rev, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision&#39;s comment block, if the current user is allowed to view it...
Definition: Linker.php:1572
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1233
showDiff( $otitle, $ntitle, $notice='')
Get the diff text, send it to the OutputPage object Returns false if the diff could not be generated...
Page revision base class.
getDiffBodyCacheKey()
Returns the cache key for diff body text or content.
const DB_REPLICA
Definition: defines.php:25
$content
Definition: router.php:78
string [] null $mNewTags
Change tags of $mNewRev or null if it does not exist / is not saved.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:119
getSlotHeader( $headerText)
Get a slot header for inclusion in a diff body (as a table row).
getWikiPage()
Get the WikiPage object.
Content null $mNewContent
loadRevisionData()
Load revision metadata for the specified revisions.
deletedLink( $id)
Look up a special:Undelete link to the given deleted revision id, as a workaround for being unable to...
localiseLineNumbersCb( $matches)
getRevisionHeader(Revision $rev, $complete='')
Get a header for a specified revision.
SlotDiffRenderer [] $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
getDiffLang()
Get the language of the difference engine, defaults to page content language.
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
isCurrent()
Definition: Revision.php:805
loadText()
Load the text of the revisions, as well as revision data.
$matches
static revUserTools( $rev, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1124