MediaWiki  1.34.0
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, $this->mOldid, $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 === 'php' ? false : $engine, // Back compat
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  $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1298  ->get( 'DiffEngine' );
1299  $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1300  ->get( 'ExternalDiffEngine' );
1301 
1302  if ( $diffEngine === null ) {
1303  $engines = [ 'external', 'wikidiff2', 'php' ];
1304  } else {
1305  $engines = [ $diffEngine ];
1306  }
1307 
1308  $failureReason = null;
1309  foreach ( $engines as $engine ) {
1310  switch ( $engine ) {
1311  case 'external':
1312  if ( is_string( $externalDiffEngine ) ) {
1313  if ( is_executable( $externalDiffEngine ) ) {
1314  return $externalDiffEngine;
1315  }
1316  $failureReason = 'ExternalDiffEngine config points to a non-executable';
1317  if ( $diffEngine === null ) {
1318  wfDebug( "$failureReason, ignoring" );
1319  }
1320  } else {
1321  $failureReason = 'ExternalDiffEngine config is set to a non-string value';
1322  if ( $diffEngine === null && $externalDiffEngine ) {
1323  wfWarn( "$failureReason, ignoring" );
1324  }
1325  }
1326  break;
1327 
1328  case 'wikidiff2':
1329  if ( function_exists( 'wikidiff2_do_diff' ) ) {
1330  return 'wikidiff2';
1331  }
1332  $failureReason = 'wikidiff2 is not available';
1333  break;
1334 
1335  case 'php':
1336  // Always available.
1337  return 'php';
1338 
1339  default:
1340  throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
1341  }
1342  }
1343  throw new UnexpectedValueException( "Cannot use diff engine '$engine': $failureReason" );
1344  }
1345 
1358  protected function textDiff( $otext, $ntext ) {
1359  $slotDiffRenderer = ContentHandler::getForModelID( CONTENT_MODEL_TEXT )
1360  ->getSlotDiffRenderer( $this->getContext() );
1361  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1362  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1363  // This is too unlikely to happen to bother handling properly.
1364  throw new Exception( 'The slot diff renderer for text content should be a '
1365  . 'TextSlotDiffRenderer subclass' );
1366  }
1367  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1368  }
1369 
1378  protected function debug( $generator = "internal" ) {
1379  if ( !$this->enableDebugComment ) {
1380  return '';
1381  }
1382  $data = [ $generator ];
1383  if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1384  $data[] = wfHostname();
1385  }
1386  $data[] = wfTimestamp( TS_DB );
1387 
1388  return "<!-- diff generator: " .
1389  implode( " ", array_map( "htmlspecialchars", $data ) ) .
1390  " -->\n";
1391  }
1392 
1393  private function getDebugString() {
1394  $engine = self::getEngine();
1395  if ( $engine === 'wikidiff2' ) {
1396  return $this->debug( 'wikidiff2' );
1397  } elseif ( $engine === 'php' ) {
1398  return $this->debug( 'native PHP' );
1399  } else {
1400  return $this->debug( "external $engine" );
1401  }
1402  }
1403 
1410  private function localiseDiff( $text ) {
1411  $text = $this->localiseLineNumbers( $text );
1412  if ( $this->getEngine() === 'wikidiff2' &&
1413  version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1414  ) {
1415  $text = $this->addLocalisedTitleTooltips( $text );
1416  }
1417  return $text;
1418  }
1419 
1427  public function localiseLineNumbers( $text ) {
1428  return preg_replace_callback(
1429  '/<!--LINE (\d+)-->/',
1430  [ $this, 'localiseLineNumbersCb' ],
1431  $text
1432  );
1433  }
1434 
1435  public function localiseLineNumbersCb( $matches ) {
1436  if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1437  return '';
1438  }
1439 
1440  return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1441  }
1442 
1449  private function addLocalisedTitleTooltips( $text ) {
1450  return preg_replace_callback(
1451  '/class="mw-diff-movedpara-(left|right)"/',
1452  [ $this, 'addLocalisedTitleTooltipsCb' ],
1453  $text
1454  );
1455  }
1456 
1461  private function addLocalisedTitleTooltipsCb( array $matches ) {
1462  $key = $matches[1] === 'right' ?
1463  'diff-paragraph-moved-toold' :
1464  'diff-paragraph-moved-tonew';
1465  return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1466  }
1467 
1473  public function getMultiNotice() {
1474  // The notice only make sense if we are diffing two saved revisions of the same page.
1475  if (
1476  !$this->mOldRev || !$this->mNewRev
1477  || !$this->mOldPage || !$this->mNewPage
1478  || !$this->mOldPage->equals( $this->mNewPage )
1479  ) {
1480  return '';
1481  }
1482 
1483  if ( $this->mOldRev->getTimestamp() > $this->mNewRev->getTimestamp() ) {
1484  $oldRev = $this->mNewRev; // flip
1485  $newRev = $this->mOldRev; // flip
1486  } else { // normal case
1487  $oldRev = $this->mOldRev;
1488  $newRev = $this->mNewRev;
1489  }
1490 
1491  // Sanity: don't show the notice if too many rows must be scanned
1492  // @todo show some special message for that case
1493  $nEdits = $this->mNewPage->countRevisionsBetween( $oldRev, $newRev, 1000 );
1494  if ( $nEdits > 0 && $nEdits <= 1000 ) {
1495  $limit = 100; // use diff-multi-manyusers if too many users
1496  $users = $this->mNewPage->getAuthorsBetween( $oldRev, $newRev, $limit );
1497  $numUsers = count( $users );
1498 
1499  if ( $numUsers == 1 && $users[0] == $newRev->getUserText( RevisionRecord::RAW ) ) {
1500  $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1501  }
1502 
1503  return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1504  }
1505 
1506  return '';
1507  }
1508 
1518  public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1519  if ( $numUsers === 0 ) {
1520  $msg = 'diff-multi-sameuser';
1521  } elseif ( $numUsers > $limit ) {
1522  $msg = 'diff-multi-manyusers';
1523  $numUsers = $limit;
1524  } else {
1525  $msg = 'diff-multi-otherusers';
1526  }
1527 
1528  return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1529  }
1530 
1535  private function userCanEdit( Revision $rev ) {
1536  $user = $this->getUser();
1537 
1538  if ( !$rev->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
1539  return false;
1540  }
1541 
1542  return true;
1543  }
1544 
1554  public function getRevisionHeader( Revision $rev, $complete = '' ) {
1555  $lang = $this->getLanguage();
1556  $user = $this->getUser();
1557  $revtimestamp = $rev->getTimestamp();
1558  $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1559  $dateofrev = $lang->userDate( $revtimestamp, $user );
1560  $timeofrev = $lang->userTime( $revtimestamp, $user );
1561 
1562  $header = $this->msg(
1563  $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1564  $timestamp,
1565  $dateofrev,
1566  $timeofrev
1567  )->escaped();
1568 
1569  if ( $complete !== 'complete' ) {
1570  return $header;
1571  }
1572 
1573  $title = $rev->getTitle();
1574 
1576  [ 'oldid' => $rev->getId() ] );
1577 
1578  if ( $this->userCanEdit( $rev ) ) {
1579  $editQuery = [ 'action' => 'edit' ];
1580  if ( !$rev->isCurrent() ) {
1581  $editQuery['oldid'] = $rev->getId();
1582  }
1583 
1584  $key = MediaWikiServices::getInstance()->getPermissionManager()
1585  ->quickUserCan( 'edit', $user, $title ) ? 'editold' : 'viewsourceold';
1586  $msg = $this->msg( $key )->escaped();
1587  $editLink = $this->msg( 'parentheses' )->rawParams(
1588  Linker::linkKnown( $title, $msg, [], $editQuery ) )->escaped();
1589  $header .= ' ' . Html::rawElement(
1590  'span',
1591  [ 'class' => 'mw-diff-edit' ],
1592  $editLink
1593  );
1594  if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1595  $header = Html::rawElement(
1596  'span',
1597  [ 'class' => 'history-deleted' ],
1598  $header
1599  );
1600  }
1601  } else {
1602  $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1603  }
1604 
1605  return $header;
1606  }
1607 
1620  public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1621  // shared.css sets diff in interface language/dir, but the actual content
1622  // is often in a different language, mostly the page content language/dir
1623  $header = Html::openElement( 'table', [
1624  'class' => [ 'diff', 'diff-contentalign-' . $this->getDiffLang()->alignStart() ],
1625  'data-mw' => 'interface',
1626  ] );
1627  $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1628 
1629  if ( !$diff && !$otitle ) {
1630  $header .= "
1631  <tr class=\"diff-title\" lang=\"{$userLang}\">
1632  <td class=\"diff-ntitle\">{$ntitle}</td>
1633  </tr>";
1634  $multiColspan = 1;
1635  } else {
1636  if ( $diff ) { // Safari/Chrome show broken output if cols not used
1637  $header .= "
1638  <col class=\"diff-marker\" />
1639  <col class=\"diff-content\" />
1640  <col class=\"diff-marker\" />
1641  <col class=\"diff-content\" />";
1642  $colspan = 2;
1643  $multiColspan = 4;
1644  } else {
1645  $colspan = 1;
1646  $multiColspan = 2;
1647  }
1648  if ( $otitle || $ntitle ) {
1649  $header .= "
1650  <tr class=\"diff-title\" lang=\"{$userLang}\">
1651  <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1652  <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1653  </tr>";
1654  }
1655  }
1656 
1657  if ( $multi != '' ) {
1658  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1659  "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1660  }
1661  if ( $notice != '' ) {
1662  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1663  "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1664  }
1665 
1666  return $header . $diff . "</table>";
1667  }
1668 
1676  public function setContent( Content $oldContent, Content $newContent ) {
1677  $this->mOldContent = $oldContent;
1678  $this->mNewContent = $newContent;
1679 
1680  $this->mTextLoaded = 2;
1681  $this->mRevisionsLoaded = true;
1682  $this->isContentOverridden = true;
1683  $this->slotDiffRenderers = null;
1684  }
1685 
1691  public function setRevisions(
1692  RevisionRecord $oldRevision = null, RevisionRecord $newRevision
1693  ) {
1694  if ( $oldRevision ) {
1695  $this->mOldRev = new Revision( $oldRevision );
1696  $this->mOldid = $oldRevision->getId();
1697  $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1698  // This method is meant for edit diffs and such so there is no reason to provide a
1699  // revision that's not readable to the user, but check it just in case.
1700  $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1701  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1702  } else {
1703  $this->mOldPage = null;
1704  $this->mOldRev = $this->mOldid = false;
1705  }
1706  $this->mNewRev = new Revision( $newRevision );
1707  $this->mNewid = $newRevision->getId();
1708  $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1709  $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1710  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1711 
1712  $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1713  $this->mTextLoaded = $oldRevision ? 2 : 1;
1714  $this->isContentOverridden = false;
1715  $this->slotDiffRenderers = null;
1716  }
1717 
1724  public function setTextLanguage( Language $lang ) {
1725  $this->mDiffLang = $lang;
1726  }
1727 
1739  public function mapDiffPrevNext( $old, $new ) {
1740  $rl = MediaWikiServices::getInstance()->getRevisionLookup();
1741  if ( $new === 'prev' ) {
1742  // Show diff between revision $old and the previous one. Get previous one from DB.
1743  $newid = intval( $old );
1744  $oldid = false;
1745  $newRev = $rl->getRevisionById( $newid );
1746  if ( $newRev ) {
1747  $oldRev = $rl->getPreviousRevision( $newRev );
1748  if ( $oldRev ) {
1749  $oldid = $oldRev->getId();
1750  }
1751  }
1752  } elseif ( $new === 'next' ) {
1753  // Show diff between revision $old and the next one. Get next one from DB.
1754  $oldid = intval( $old );
1755  $newid = false;
1756  $oldRev = $rl->getRevisionById( $oldid );
1757  if ( $oldRev ) {
1758  $newRev = $rl->getNextRevision( $oldRev );
1759  if ( $newRev ) {
1760  $newid = $newRev->getId();
1761  }
1762  }
1763  } else {
1764  $oldid = intval( $old );
1765  $newid = intval( $new );
1766  }
1767 
1768  return [ $oldid, $newid ];
1769  }
1770 
1774  private function loadRevisionIds() {
1775  if ( $this->mRevisionsIdsLoaded ) {
1776  return;
1777  }
1778 
1779  $this->mRevisionsIdsLoaded = true;
1780 
1781  $old = $this->mOldid;
1782  $new = $this->mNewid;
1783 
1784  list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1785  if ( $new === 'next' && $this->mNewid === false ) {
1786  # if no result, NewId points to the newest old revision. The only newer
1787  # revision is cur, which is "0".
1788  $this->mNewid = 0;
1789  }
1790 
1791  Hooks::run(
1792  'NewDifferenceEngine',
1793  [ $this->getTitle(), &$this->mOldid, &$this->mNewid, $old, $new ]
1794  );
1795  }
1796 
1810  public function loadRevisionData() {
1811  if ( $this->mRevisionsLoaded ) {
1812  return $this->isContentOverridden || ( $this->mOldRev !== null && $this->mNewRev !== null );
1813  }
1814 
1815  // Whether it succeeds or fails, we don't want to try again
1816  $this->mRevisionsLoaded = true;
1817 
1818  $this->loadRevisionIds();
1819 
1820  // Load the new revision object
1821  if ( $this->mNewid ) {
1822  $this->mNewRev = Revision::newFromId( $this->mNewid );
1823  } else {
1824  $this->mNewRev = Revision::newFromTitle(
1825  $this->getTitle(),
1826  false,
1827  Revision::READ_NORMAL
1828  );
1829  }
1830 
1831  if ( !$this->mNewRev instanceof Revision ) {
1832  return false;
1833  }
1834 
1835  // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1836  $this->mNewid = $this->mNewRev->getId();
1837  if ( $this->mNewid ) {
1838  $this->mNewPage = $this->mNewRev->getTitle();
1839  } else {
1840  $this->mNewPage = null;
1841  }
1842 
1843  // Load the old revision object
1844  $this->mOldRev = false;
1845  if ( $this->mOldid ) {
1846  $this->mOldRev = Revision::newFromId( $this->mOldid );
1847  } elseif ( $this->mOldid === 0 ) {
1848  $rev = $this->mNewRev->getPrevious();
1849  if ( $rev ) {
1850  $this->mOldid = $rev->getId();
1851  $this->mOldRev = $rev;
1852  } else {
1853  // No previous revision; mark to show as first-version only.
1854  $this->mOldid = false;
1855  $this->mOldRev = false;
1856  }
1857  } /* elseif ( $this->mOldid === false ) leave mOldRev false; */
1858 
1859  if ( is_null( $this->mOldRev ) ) {
1860  return false;
1861  }
1862 
1863  if ( $this->mOldRev && $this->mOldRev->getId() ) {
1864  $this->mOldPage = $this->mOldRev->getTitle();
1865  } else {
1866  $this->mOldPage = null;
1867  }
1868 
1869  // Load tags information for both revisions
1870  $dbr = wfGetDB( DB_REPLICA );
1871  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
1872  if ( $this->mOldid !== false ) {
1873  $tagIds = $dbr->selectFieldValues(
1874  'change_tag',
1875  'ct_tag_id',
1876  [ 'ct_rev_id' => $this->mOldid ],
1877  __METHOD__
1878  );
1879  $tags = [];
1880  foreach ( $tagIds as $tagId ) {
1881  try {
1882  $tags[] = $changeTagDefStore->getName( (int)$tagId );
1883  } catch ( NameTableAccessException $exception ) {
1884  continue;
1885  }
1886  }
1887  $this->mOldTags = implode( ',', $tags );
1888  } else {
1889  $this->mOldTags = false;
1890  }
1891 
1892  $tagIds = $dbr->selectFieldValues(
1893  'change_tag',
1894  'ct_tag_id',
1895  [ 'ct_rev_id' => $this->mNewid ],
1896  __METHOD__
1897  );
1898  $tags = [];
1899  foreach ( $tagIds as $tagId ) {
1900  try {
1901  $tags[] = $changeTagDefStore->getName( (int)$tagId );
1902  } catch ( NameTableAccessException $exception ) {
1903  continue;
1904  }
1905  }
1906  $this->mNewTags = implode( ',', $tags );
1907 
1908  return true;
1909  }
1910 
1919  public function loadText() {
1920  if ( $this->mTextLoaded == 2 ) {
1921  return $this->loadRevisionData() && ( $this->mOldRev === false || $this->mOldContent )
1922  && $this->mNewContent;
1923  }
1924 
1925  // Whether it succeeds or fails, we don't want to try again
1926  $this->mTextLoaded = 2;
1927 
1928  if ( !$this->loadRevisionData() ) {
1929  return false;
1930  }
1931 
1932  if ( $this->mOldRev ) {
1933  $this->mOldContent = $this->mOldRev->getContent(
1934  RevisionRecord::FOR_THIS_USER, $this->getUser()
1935  );
1936  if ( $this->mOldContent === null ) {
1937  return false;
1938  }
1939  }
1940 
1941  $this->mNewContent = $this->mNewRev->getContent(
1942  RevisionRecord::FOR_THIS_USER, $this->getUser()
1943  );
1944  Hooks::run( 'DifferenceEngineLoadTextAfterNewContentIsLoaded', [ $this ] );
1945  if ( $this->mNewContent === null ) {
1946  return false;
1947  }
1948 
1949  return true;
1950  }
1951 
1957  public function loadNewText() {
1958  if ( $this->mTextLoaded >= 1 ) {
1959  return $this->loadRevisionData();
1960  }
1961 
1962  $this->mTextLoaded = 1;
1963 
1964  if ( !$this->loadRevisionData() ) {
1965  return false;
1966  }
1967 
1968  $this->mNewContent = $this->mNewRev->getContent(
1969  RevisionRecord::FOR_THIS_USER, $this->getUser()
1970  );
1971 
1972  Hooks::run( 'DifferenceEngineAfterLoadNewText', [ $this ] );
1973 
1974  return true;
1975  }
1976 
1977 }
Content\getContentHandler
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
Revision\newFromArchiveRow
static newFromArchiveRow( $row, $overrides=[])
Make a fake revision object from an archive table row.
Definition: Revision.php:172
DifferenceEngine\$mRevisionsIdsLoaded
bool $mRevisionsIdsLoaded
Have the revisions IDs been loaded.
Definition: DifferenceEngine.php:147
DifferenceEngine\revisionDeleteLink
revisionDeleteLink( $rev)
Definition: DifferenceEngine.php:827
ContextSource\$context
IContextSource $context
Definition: ContextSource.php:33
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:63
Revision\getTimestamp
getTimestamp()
Definition: Revision.php:798
ContentHandler\getForModelID
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
Definition: ContentHandler.php:254
DifferenceEngine\getSlotContents
getSlotContents()
Get the old and new content objects for all slots.
Definition: DifferenceEngine.php:276
wfMergeErrorArrays
wfMergeErrorArrays(... $args)
Merge arrays in the style of getUserPermissionsErrors, with duplicate removal e.g.
Definition: GlobalFunctions.php:181
DifferenceEngine\markPatrolledLink
markPatrolledLink()
Build a link to mark a change as patrolled.
Definition: DifferenceEngine.php:733
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:40
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
DifferenceEngine\getEngine
static getEngine()
Process DiffEngine config and get a sane, usable engine.
Definition: DifferenceEngine.php:1296
DifferenceEngine\$mTextLoaded
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
Definition: DifferenceEngine.php:153
DifferenceEngine\addLocalisedTitleTooltipsCb
addLocalisedTitleTooltipsCb(array $matches)
Definition: DifferenceEngine.php:1461
DifferenceEngine\getDiffBodyCacheKeyParams
getDiffBodyCacheKeyParams()
Get the cache key parameters.
Definition: DifferenceEngine.php:1173
Revision\newFromId
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:119
DifferenceEngine\$unhide
bool $unhide
Show rev_deleted content if allowed.
Definition: DifferenceEngine.php:184
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:117
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:33
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:207
DifferenceEngine\setReducedLineNumbers
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
Definition: DifferenceEngine.php:325
DifferenceEngine\setContent
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
Definition: DifferenceEngine.php:1676
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1869
DifferenceEngine\getNewid
getNewid()
Get the ID of new revision (right pane) of the diff.
Definition: DifferenceEngine.php:369
DifferenceEngine\getOldRevision
getOldRevision()
Get the left side of the diff.
Definition: DifferenceEngine.php:381
DifferenceEngine\getOldid
getOldid()
Get the ID of old revision (left pane) of the diff.
Definition: DifferenceEngine.php:357
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:47
DifferenceEngine\$mRevisionsLoaded
bool $mRevisionsLoaded
Have the revisions been loaded.
Definition: DifferenceEngine.php:150
DifferenceEngine\deletedIdMarker
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
Definition: DifferenceEngine.php:436
Linker\revComment
static revComment(Revision $rev, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition: Linker.php:1577
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:1961
wfHostname
wfHostname()
Fetch server name for use in error reporting etc.
Definition: GlobalFunctions.php:1326
Linker\linkKnown
static linkKnown( $target, $html=null, $customAttribs=[], $query=[], $options=[ 'known'])
Identical to link(), except $options defaults to 'known'.
Definition: Linker.php:141
DifferenceEngine\$isSlotDiffRenderer
bool $isSlotDiffRenderer
Temporary hack for B/C while slot diff related methods of DifferenceEngine are being deprecated.
Definition: DifferenceEngine.php:198
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1264
Revision\getArchiveQueryInfo
static getArchiveQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new archived revision objec...
Definition: Revision.php:329
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:83
DifferenceEngine\addLocalisedTitleTooltips
addLocalisedTitleTooltips( $text)
Add title attributes for tooltips on moved paragraph indicators.
Definition: DifferenceEngine.php:1449
Revision\isCurrent
isCurrent()
Definition: Revision.php:805
ContextSource\getRequest
getRequest()
Definition: ContextSource.php:71
PermissionsError
Show an error when a user tries to do something they do not have the necessary permissions for.
Definition: PermissionsError.php:30
DifferenceEngine\showDiffStyle
showDiffStyle()
Add style sheets for diff display.
Definition: DifferenceEngine.php:971
DifferenceEngine\loadNewText
loadNewText()
Load the text of the new revision, not the old one.
Definition: DifferenceEngine.php:1957
Revision\getId
getId()
Get revision ID.
Definition: Revision.php:442
ContextSource\getUser
getUser()
Definition: ContextSource.php:120
DifferenceEngine\showDiff
showDiff( $otitle, $ntitle, $notice='')
Get the diff text, send it to the OutputPage object Returns false if the diff could not be generated,...
Definition: DifferenceEngine.php:951
DifferenceEngine\getDiffBodyCacheKey
getDiffBodyCacheKey()
Returns the cache key for diff body text or content.
Definition: DifferenceEngine.php:1156
DifferenceEngine\localiseDiff
localiseDiff( $text)
Localise diff output.
Definition: DifferenceEngine.php:1410
DifferenceEngine\$mNewid
int string false null $mNewid
Revision ID for the new revision.
Definition: DifferenceEngine.php:77
DifferenceEngine\generateTextDiffBody
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
Definition: DifferenceEngine.php:1278
$dbr
$dbr
Definition: testCompression.php:50
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:128
Revision
Definition: Revision.php:40
DifferenceEngine\$mReducedLineNumbers
bool $mReducedLineNumbers
If true, line X is not displayed when X is 1, for example to increase readability and conserve space ...
Definition: DifferenceEngine.php:178
Revision\newFromTitle
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target.
Definition: Revision.php:138
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:49
DifferenceEngine\loadRevisionData
loadRevisionData()
Load revision metadata for the specified revisions.
Definition: DifferenceEngine.php:1810
DifferenceEngine\localiseLineNumbersCb
localiseLineNumbersCb( $matches)
Definition: DifferenceEngine.php:1435
MWException
MediaWiki exception.
Definition: MWException.php:26
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:142
DifferenceEngine\getDiffBodyForRole
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
Definition: DifferenceEngine.php:1110
DifferenceEngine\$slotDiffRenderers
SlotDiffRenderer[] $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
Definition: DifferenceEngine.php:190
Linker\generateRollback
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1811
DifferenceEngine\wasCacheHit
wasCacheHit()
Definition: DifferenceEngine.php:346
DifferenceEngine\$mNewTags
string[] null $mNewTags
Change tags of $mNewRev or null if it does not exist / is not saved.
Definition: DifferenceEngine.php:127
wfIncrStats
wfIncrStats( $key, $count=1)
Increment a statistics counter.
Definition: GlobalFunctions.php:1161
DifferenceEngine\getDiffLang
getDiffLang()
Get the language of the difference engine, defaults to page content language.
Definition: DifferenceEngine.php:334
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2575
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $forceParse=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1233
ContextSource\getOutput
getOutput()
Definition: ContextSource.php:112
$matches
$matches
Definition: NoLocalSettings.php:24
DifferenceEngine\getSlotHeader
getSlotHeader( $headerText)
Get a slot header for inclusion in a diff body (as a table row).
Definition: DifferenceEngine.php:1139
ContextSource
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Definition: ContextSource.php:29
DifferenceEngine\$mOldRev
Revision null false $mOldRev
Old revision (left pane).
Definition: DifferenceEngine.php:90
ContextSource\getWikiPage
getWikiPage()
Get the WikiPage object.
Definition: ContextSource.php:104
DifferenceEngine\$mNewContent
Content null $mNewContent
Definition: DifferenceEngine.php:141
DifferenceEngine\deletedLink
deletedLink( $id)
Look up a special:Undelete link to the given deleted revision id, as a workaround for being unable to...
Definition: DifferenceEngine.php:402
ChangesList\flag
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
Definition: ChangesList.php:262
DifferenceEngine\getDiffBody
getDiffBody()
Get the diff table body, without header.
Definition: DifferenceEngine.php:1014
DifferenceEngine\getRevisionHeader
getRevisionHeader(Revision $rev, $complete='')
Get a header for a specified revision.
Definition: DifferenceEngine.php:1554
DifferenceEngine\getSlotDiffRenderers
getSlotDiffRenderers()
Definition: DifferenceEngine.php:241
$generator
$generator
Definition: generateLocalAutoload.php:13
$title
$title
Definition: testCompression.php:34
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
DifferenceEngine\loadText
loadText()
Load the text of the revisions, as well as revision data.
Definition: DifferenceEngine.php:1919
DifferenceEngine\userCanEdit
userCanEdit(Revision $rev)
Definition: DifferenceEngine.php:1535
Linker\revUserTools
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
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:913
ContextSource\setContext
setContext(IContextSource $context)
Definition: ContextSource.php:55
DifferenceEngine\$mCacheHit
bool $mCacheHit
Was the diff fetched from cache?
Definition: DifferenceEngine.php:166
deprecatePublicProperty
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
Definition: DeprecationHelper.php:68
DifferenceEngine\markAsSlotDiffRenderer
markAsSlotDiffRenderer()
Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
Definition: DifferenceEngine.php:267
DifferenceEngine\$enableDebugComment
$enableDebugComment
Set this to true to add debug info to the HTML output.
Definition: DifferenceEngine.php:173
Revision\RevisionRecord\getId
getId()
Get revision ID.
Definition: RevisionRecord.php:273
Revision\getTitle
getTitle()
Returns the title of the page associated with this entry.
Definition: Revision.php:559
DifferenceEngine\__construct
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
#-
Definition: DifferenceEngine.php:210
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:168
DifferenceEngine\getDebugString
getDebugString()
Definition: DifferenceEngine.php:1393
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:613
DifferenceEngine\setTextLanguage
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
Definition: DifferenceEngine.php:1724
$content
$content
Definition: router.php:78
DifferenceEngine\$mOldTags
string[] null $mOldTags
Change tags of $mOldRev or null if it does not exist / is not saved.
Definition: DifferenceEngine.php:121
$header
$header
Definition: updateCredits.php:41
Linker\getRevDeleteLink
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:2110
Revision\isDeleted
isDeleted( $field)
Definition: Revision.php:692
DifferenceEngine\getTitle
getTitle()
Definition: DifferenceEngine.php:314
DifferenceEngine\showDiffPage
showDiffPage( $diffOnly=false)
Definition: DifferenceEngine.php:468
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:268
DifferenceEngine\$mOldid
int false null $mOldid
Revision ID for the old revision.
Definition: DifferenceEngine.php:69
Linker\titleAttrib
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:2026
DifferenceEngine\$isContentOverridden
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e....
Definition: DifferenceEngine.php:163
DifferenceEngine\DIFF_VERSION
const DIFF_VERSION
Constant to indicate diff cache compatibility.
Definition: DifferenceEngine.php:61
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:53
DifferenceEngine\intermediateEditsMsg
static intermediateEditsMsg( $numEdits, $numUsers, $limit)
Get a notice about how many intermediate edits and users there are.
Definition: DifferenceEngine.php:1518
DifferenceEngine\showMissingRevision
showMissingRevision()
Definition: DifferenceEngine.php:445
Content
Base interface for content objects.
Definition: Content.php:34
DifferenceEngine\loadRevisionIds
loadRevisionIds()
Load revision IDs.
Definition: DifferenceEngine.php:1774
DifferenceEngine\getParserOutput
getParserOutput(WikiPage $page, Revision $rev)
Definition: DifferenceEngine.php:927
DifferenceEngine\getNewRevision
getNewRevision()
Get the right side of the diff.
Definition: DifferenceEngine.php:390
Revision\RevisionRecord\getPageAsLinkTarget
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
Definition: RevisionRecord.php:345
Title
Represents a title within MediaWiki.
Definition: Title.php:42
SlotDiffRenderer
Renders a diff for a single slot (that is, a diff between two content objects).
Definition: SlotDiffRenderer.php:39
$cache
$cache
Definition: mcc.php:33
DifferenceEngine\$mDiffLang
Language $mDiffLang
Definition: DifferenceEngine.php:144
DifferenceEngine\setRevisions
setRevisions(RevisionRecord $oldRevision=null, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
Definition: DifferenceEngine.php:1691
DifferenceEngine\getMarkPatrolledLinkInfo
getMarkPatrolledLinkInfo()
Returns an array of meta data needed to build a "mark as patrolled" link and adds the mediawiki....
Definition: DifferenceEngine.php:765
DifferenceEngine\$mOldPage
Title null $mOldPage
Title of $mOldRev or null if the old revision does not exist or does not belong to a page.
Definition: DifferenceEngine.php:108
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:79
DifferenceEngine\getExtraCacheKeys
getExtraCacheKeys()
Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys().
Definition: DifferenceEngine.php:1207
getTitle
getTitle()
Definition: RevisionSearchResultTrait.php:74
DifferenceEngine
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
Definition: DifferenceEngine.php:51
MediaWiki\Storage\NameTableAccessException
Exception representing a failure to look up a row from a name table.
Definition: NameTableAccessException.php:32
DifferenceEngineSlotDiffRenderer
B/C adapter for turning a DifferenceEngine into a SlotDiffRenderer.
Definition: DifferenceEngineSlotDiffRenderer.php:31
RecentChange\isInRCLifespan
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...
Definition: RecentChange.php:1130
Revision\RevisionRecord\getContent
getContent( $role, $audience=self::FOR_PUBLIC, User $user=null)
Returns the Content of the given slot of this revision.
Definition: RevisionRecord.php:167
DifferenceEngine\$mMarkPatrolledLink
string $mMarkPatrolledLink
Link to action=markpatrolled.
Definition: DifferenceEngine.php:181
DifferenceEngine\localiseLineNumbers
localiseLineNumbers( $text)
Replace line numbers with the text in the user's language.
Definition: DifferenceEngine.php:1427
DifferenceEngine\$mOldContent
Content null $mOldContent
Definition: DifferenceEngine.php:134
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1065
DifferenceEngine\debug
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend.
Definition: DifferenceEngine.php:1378
DifferenceEngine\renderNewRevision
renderNewRevision()
Show the new revision of the page.
Definition: DifferenceEngine.php:841
CONTENT_MODEL_TEXT
const CONTENT_MODEL_TEXT
Definition: Defines.php:218
DifferenceEngine\getDiff
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
Definition: DifferenceEngine.php:992
DifferenceEngine\$mNewRev
Revision null $mNewRev
New revision (right pane).
Definition: DifferenceEngine.php:101
DeprecationHelper
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated.
Definition: DeprecationHelper.php:45
DifferenceEngine\generateContentDiffBody
generateContentDiffBody(Content $old, Content $new)
Generate a diff, no caching.
Definition: DifferenceEngine.php:1250
DifferenceEngine\mapDiffPrevNext
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
Definition: DifferenceEngine.php:1739
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
DifferenceEngine\addHeader
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
Definition: DifferenceEngine.php:1620
ChangeTags\formatSummaryRow
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:94
Language
Internationalisation code.
Definition: Language.php:37
DifferenceEngine\textDiff
textDiff( $otext, $ntext)
Generates diff, to be wrapped internally in a logging/instrumentation.
Definition: DifferenceEngine.php:1358
Revision\userCan
userCan( $field, User $user=null)
Determine if the current user is allowed to view a particular field of this revision,...
Definition: Revision.php:1028
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
DifferenceEngine\$mNewPage
Title null $mNewPage
Title of $mNewRev or null if the new revision does not exist or does not belong to a page.
Definition: DifferenceEngine.php:115
TextSlotDiffRenderer
Renders a slot diff by doing a text diff on the native representation.
Definition: TextSlotDiffRenderer.php:37
DifferenceEngine\$mRefreshCache
bool $mRefreshCache
Refresh the diff cache.
Definition: DifferenceEngine.php:187
DifferenceEngine\getMultiNotice
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
Definition: DifferenceEngine.php:1473