MediaWiki  master
DifferenceEngine.php
Go to the documentation of this file.
1 <?php
35 
59 
61 
68  private const DIFF_VERSION = '1.12';
69 
76  protected $mOldid;
77 
84  protected $mNewid;
85 
97 
107 
113  protected $mOldPage;
114 
120  protected $mNewPage;
121 
126  private $mOldTags;
127 
132  private $mNewTags;
133 
139  private $mOldContent;
140 
146  private $mNewContent;
147 
149  protected $mDiffLang;
150 
152  private $mRevisionsIdsLoaded = false;
153 
155  protected $mRevisionsLoaded = false;
156 
158  protected $mTextLoaded = 0;
159 
168  protected $isContentOverridden = false;
169 
171  protected $mCacheHit = false;
172 
179  public $enableDebugComment = false;
180 
184  protected $mReducedLineNumbers = false;
185 
187  protected $mMarkPatrolledLink = null;
188 
190  protected $unhide = false;
191 
193  protected $mRefreshCache = false;
194 
196  protected $slotDiffRenderers = null;
197 
204  protected $isSlotDiffRenderer = false;
205 
210  private $slotDiffOptions = [];
211 
215  protected $linkRenderer;
216 
221 
225  private $revisionStore;
226 
228  private $hookRunner;
229 
232 
243  public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
244  $refreshCache = false, $unhide = false
245  ) {
246  $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
247  $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
248  $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
249  $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
250  $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
251  $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
252  $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
253  $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
254  $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
255 
256  if ( $context instanceof IContextSource ) {
257  $this->setContext( $context );
258  }
259 
260  wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
261 
262  $this->mOldid = $old;
263  $this->mNewid = $new;
264  $this->mRefreshCache = $refreshCache;
265  $this->unhide = $unhide;
266 
267  $services = MediaWikiServices::getInstance();
268  $this->linkRenderer = $services->getLinkRenderer();
269  $this->contentHandlerFactory = $services->getContentHandlerFactory();
270  $this->revisionStore = $services->getRevisionStore();
271  $this->hookRunner = new HookRunner( $services->getHookContainer() );
272  $this->wikiPageFactory = $services->getWikiPageFactory();
273  }
274 
279  protected function getSlotDiffRenderers() {
280  if ( $this->isSlotDiffRenderer ) {
281  throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
282  }
283 
284  if ( $this->slotDiffRenderers === null ) {
285  if ( !$this->loadRevisionData() ) {
286  return [];
287  }
288 
289  $slotContents = $this->getSlotContents();
290  $this->slotDiffRenderers = array_map( function ( $contents ) {
292  $content = $contents['new'] ?: $contents['old'];
293  $context = $this->getContext();
294 
295  return $content->getContentHandler()->getSlotDiffRenderer(
296  $context,
297  $this->slotDiffOptions
298  );
299  }, $slotContents );
300  }
302  }
303 
310  public function markAsSlotDiffRenderer() {
311  $this->isSlotDiffRenderer = true;
312  }
313 
319  protected function getSlotContents() {
320  if ( $this->isContentOverridden ) {
321  return [
322  SlotRecord::MAIN => [
323  'old' => $this->mOldContent,
324  'new' => $this->mNewContent,
325  ]
326  ];
327  } elseif ( !$this->loadRevisionData() ) {
328  return [];
329  }
330 
331  $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
332  if ( $this->mOldRevisionRecord ) {
333  $oldSlots = $this->mOldRevisionRecord->getPrimarySlots()->getSlots();
334  } else {
335  $oldSlots = [];
336  }
337  // The order here will determine the visual order of the diff. The current logic is
338  // slots of the new revision first in natural order, then deleted ones. This is ad hoc
339  // and should not be relied on - in the future we may want the ordering to depend
340  // on the page type.
341  $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
342 
343  $slots = [];
344  foreach ( $roles as $role ) {
345  $slots[$role] = [
346  'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
347  'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
348  ];
349  }
350  // move main slot to front
351  if ( isset( $slots[SlotRecord::MAIN] ) ) {
352  $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
353  }
354  return $slots;
355  }
356 
358  public function getTitle() {
359  // T202454 avoid errors when there is no title
360  return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
361  }
362 
369  public function setReducedLineNumbers( $value = true ) {
370  $this->mReducedLineNumbers = $value;
371  }
372 
378  public function getDiffLang() {
379  if ( $this->mDiffLang === null ) {
380  # Default language in which the diff text is written.
381  $this->mDiffLang = $this->getTitle()->getPageLanguage();
382  }
383 
384  return $this->mDiffLang;
385  }
386 
390  public function wasCacheHit() {
391  return $this->mCacheHit;
392  }
393 
401  public function getOldid() {
402  $this->loadRevisionIds();
403 
404  return $this->mOldid;
405  }
406 
413  public function getNewid() {
414  $this->loadRevisionIds();
415 
416  return $this->mNewid;
417  }
418 
425  public function getOldRevision() {
426  return $this->mOldRevisionRecord ?: null;
427  }
428 
434  public function getNewRevision() {
436  }
437 
446  public function deletedLink( $id ) {
447  if ( $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
448  $dbr = wfGetDB( DB_REPLICA );
450  $arQuery = $revStore->getArchiveQueryInfo();
451  $row = $dbr->selectRow(
452  $arQuery['tables'],
453  array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
454  [ 'ar_rev_id' => $id ],
455  __METHOD__,
456  [],
457  $arQuery['joins']
458  );
459  if ( $row ) {
460  $revRecord = $revStore->newRevisionFromArchiveRow( $row );
461  $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
462 
463  return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
464  'target' => $title->getPrefixedText(),
465  'timestamp' => $revRecord->getTimestamp()
466  ] );
467  }
468  }
469 
470  return false;
471  }
472 
480  public function deletedIdMarker( $id ) {
481  $link = $this->deletedLink( $id );
482  if ( $link ) {
483  return "[$link $id]";
484  } else {
485  return (string)$id;
486  }
487  }
488 
489  private function showMissingRevision() {
490  $out = $this->getOutput();
491 
492  $missing = [];
493  if ( $this->mOldRevisionRecord === null ||
494  ( $this->mOldRevisionRecord && $this->mOldContent === null )
495  ) {
496  $missing[] = $this->deletedIdMarker( $this->mOldid );
497  }
498  if ( $this->mNewRevisionRecord === null ||
499  ( $this->mNewRevisionRecord && $this->mNewContent === null )
500  ) {
501  $missing[] = $this->deletedIdMarker( $this->mNewid );
502  }
503 
504  $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
505  $msg = $this->msg( 'difference-missing-revision' )
506  ->params( $this->getLanguage()->listToText( $missing ) )
507  ->numParams( count( $missing ) )
508  ->parseAsBlock();
509  $out->addHTML( $msg );
510  }
511 
517  public function hasDeletedRevision() {
518  $this->loadRevisionData();
519  return (
520  $this->mNewRevisionRecord &&
521  $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
522  ) ||
523  (
524  $this->mOldRevisionRecord &&
525  $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
526  );
527  }
528 
535  public function getPermissionErrors( Authority $performer ) {
536  $this->loadRevisionData();
537  $permStatus = PermissionStatus::newEmpty();
538  if ( $this->mNewPage ) {
539  $performer->authorizeRead( 'read', $this->mNewPage, $permStatus );
540  }
541  if ( $this->mOldPage ) {
542  $performer->authorizeRead( 'read', $this->mOldPage, $permStatus );
543  }
544  return $permStatus->toLegacyErrorArray();
545  }
546 
552  public function hasSuppressedRevision() {
553  return $this->hasDeletedRevision() && (
554  ( $this->mOldRevisionRecord &&
555  $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
556  ( $this->mNewRevisionRecord &&
557  $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
558  );
559  }
560 
572  public function isUserAllowedToSeeRevisions( Authority $performer ) {
573  $this->loadRevisionData();
574  // $this->mNewRev will only be falsy if a loading error occurred
575  // (in which case the user is allowed to see).
576  $allowed = !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
577  RevisionRecord::DELETED_TEXT,
578  $performer
579  );
580  if ( $this->mOldRevisionRecord &&
581  !$this->mOldRevisionRecord->userCan(
582  RevisionRecord::DELETED_TEXT,
583  $performer
584  )
585  ) {
586  $allowed = false;
587  }
588  return $allowed;
589  }
590 
598  public function shouldBeHiddenFromUser( Authority $performer ) {
599  return $this->hasDeletedRevision() && ( !$this->unhide ||
600  !$this->isUserAllowedToSeeRevisions( $performer ) );
601  }
602 
606  public function showDiffPage( $diffOnly = false ) {
607  # Allow frames except in certain special cases
608  $out = $this->getOutput();
609  $out->allowClickjacking();
610  $out->setRobotPolicy( 'noindex,nofollow' );
611 
612  // Allow extensions to add any extra output here
613  $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
614 
615  if ( !$this->loadRevisionData() ) {
616  if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
617  $this->showMissingRevision();
618  }
619  return;
620  }
621 
622  $user = $this->getUser();
623  $permErrors = $this->getPermissionErrors( $this->getAuthority() );
624  if ( count( $permErrors ) ) {
625  throw new PermissionsError( 'read', $permErrors );
626  }
627 
628  $rollback = '';
629 
630  $query = $this->slotDiffOptions;
631  # Carry over 'diffonly' param via navigation links
632  if ( $diffOnly != MediaWikiServices::getInstance()
633  ->getUserOptionsLookup()->getBoolOption( $user, 'diffonly' )
634  ) {
635  $query['diffonly'] = $diffOnly;
636  }
637  # Cascade unhide param in links for easy deletion browsing
638  if ( $this->unhide ) {
639  $query['unhide'] = 1;
640  }
641 
642  # Check if one of the revisions is deleted/suppressed
643  $deleted = $this->hasDeletedRevision();
644  $suppressed = $this->hasSuppressedRevision();
645  $allowed = $this->isUserAllowedToSeeRevisions( $this->getAuthority() );
646 
647  $revisionTools = [];
648 
649  # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
650  # a diff between a version V and its previous version V' AND the version V
651  # is the first version of that article. In that case, V' does not exist.
652  if ( $this->mOldRevisionRecord === false ) {
653  if ( $this->mNewPage ) {
654  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
655  }
656  $samePage = true;
657  $oldHeader = '';
658  // Allow extensions to change the $oldHeader variable
659  $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
660  } else {
661  $this->hookRunner->onDifferenceEngineViewHeader( $this );
662 
663  if ( !$this->mOldPage || !$this->mNewPage ) {
664  // XXX say something to the user?
665  $samePage = false;
666  } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
667  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
668  $samePage = true;
669  } else {
670  $out->setPageTitle( $this->msg( 'difference-title-multipage',
671  $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
672  $out->addSubtitle( $this->msg( 'difference-multipage' ) );
673  $samePage = false;
674  }
675 
676  if ( $samePage && $this->mNewPage &&
677  $this->getAuthority()->probablyCan( 'edit', $this->mNewPage )
678  ) {
679  if ( $this->mNewRevisionRecord->isCurrent() &&
680  $this->getAuthority()->probablyCan( 'rollback', $this->mNewPage )
681  ) {
682  $rollbackLink = Linker::generateRollback(
683  $this->mNewRevisionRecord,
684  $this->getContext(),
685  [ 'noBrackets' ]
686  );
687  if ( $rollbackLink ) {
688  $out->preventClickjacking();
689  $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
690  }
691  }
692 
693  if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
694  $this->userCanEdit( $this->mNewRevisionRecord )
695  ) {
696  $undoLink = Html::element( 'a', [
697  'href' => $this->mNewPage->getLocalURL( [
698  'action' => 'edit',
699  'undoafter' => $this->mOldid,
700  'undo' => $this->mNewid
701  ] ),
702  'title' => Linker::titleAttrib( 'undo' ),
703  ],
704  $this->msg( 'editundo' )->text()
705  );
706  $revisionTools['mw-diff-undo'] = $undoLink;
707  }
708  }
709  # Make "previous revision link"
710  $hasPrevious = $samePage && $this->mOldPage &&
711  $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
712  if ( $hasPrevious ) {
713  $prevlink = $this->linkRenderer->makeKnownLink(
714  $this->mOldPage,
715  $this->msg( 'previousdiff' )->text(),
716  [ 'id' => 'differences-prevlink' ],
717  [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
718  );
719  } else {
720  $prevlink = "\u{00A0}";
721  }
722 
723  if ( $this->mOldRevisionRecord->isMinor() ) {
724  $oldminor = ChangesList::flag( 'minor' );
725  } else {
726  $oldminor = '';
727  }
728 
729  $oldRevRecord = $this->mOldRevisionRecord;
730 
731  $ldel = $this->revisionDeleteLink( $oldRevRecord );
732  $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' );
733  $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
734 
735  $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
736  '<div id="mw-diff-otitle2">' .
737  Linker::revUserTools( $oldRevRecord, !$this->unhide ) . '</div>' .
738  '<div id="mw-diff-otitle3">' . $oldminor .
739  Linker::revComment( $oldRevRecord, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
740  '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
741  '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
742 
743  // Allow extensions to change the $oldHeader variable
744  $this->hookRunner->onDifferenceEngineOldHeader(
745  $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
746  }
747 
748  $out->addJsConfigVars( [
749  'wgDiffOldId' => $this->mOldid,
750  'wgDiffNewId' => $this->mNewid,
751  ] );
752 
753  # Make "next revision link"
754  # Skip next link on the top revision
755  if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
756  $nextlink = $this->linkRenderer->makeKnownLink(
757  $this->mNewPage,
758  $this->msg( 'nextdiff' )->text(),
759  [ 'id' => 'differences-nextlink' ],
760  [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
761  );
762  } else {
763  $nextlink = "\u{00A0}";
764  }
765 
766  if ( $this->mNewRevisionRecord->isMinor() ) {
767  $newminor = ChangesList::flag( 'minor' );
768  } else {
769  $newminor = '';
770  }
771 
772  # Handle RevisionDelete links...
773  $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
774 
775  # Allow extensions to define their own revision tools
776  $this->hookRunner->onDiffTools(
777  $this->mNewRevisionRecord,
778  $revisionTools,
779  $this->mOldRevisionRecord ?: null,
780  $user
781  );
782 
783  $formattedRevisionTools = [];
784  // Put each one in parentheses (poor man's button)
785  foreach ( $revisionTools as $key => $tool ) {
786  $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
787  $element = Html::rawElement(
788  'span',
789  [ 'class' => $toolClass ],
790  $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
791  );
792  $formattedRevisionTools[] = $element;
793  }
794 
795  $newRevRecord = $this->mNewRevisionRecord;
796 
797  $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) .
798  ' ' . implode( ' ', $formattedRevisionTools );
799  $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
800 
801  $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
802  '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
803  " $rollback</div>" .
804  '<div id="mw-diff-ntitle3">' . $newminor .
805  Linker::revComment( $newRevRecord, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
806  '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
807  '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
808 
809  // Allow extensions to change the $newHeader variable
810  $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
811  $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
812  $rdel, $this->unhide );
813 
814  # If the diff cannot be shown due to a deleted revision, then output
815  # the diff header and links to unhide (if available)...
816  if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) {
817  $this->showDiffStyle();
818  $multi = $this->getMultiNotice();
819  $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
820  if ( !$allowed ) {
821  $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
822  # Give explanation for why revision is not visible
823  $out->addHtml(
825  $this->msg( $msg )->parse(),
826  'plainlinks'
827  )
828  );
829  } else {
830  # Give explanation and add a link to view the diff...
831  $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
832  $link = $this->getTitle()->getFullURL( $query );
833  $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
834  $out->addHtml(
836  $this->msg( $msg, $link )->parse(),
837  'plainlinks'
838  )
839  );
840  }
841  # Otherwise, output a regular diff...
842  } else {
843  # Add deletion notice if the user is viewing deleted content
844  $notice = '';
845  if ( $deleted ) {
846  $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
847  $notice = Html::warningBox(
848  $this->msg( $msg )->parse(),
849  'plainlinks'
850  );
851  }
852  $this->showDiff( $oldHeader, $newHeader, $notice );
853  if ( !$diffOnly ) {
854  $this->renderNewRevision();
855  }
856  }
857  }
858 
869  public function markPatrolledLink() {
870  if ( $this->mMarkPatrolledLink === null ) {
871  $linkInfo = $this->getMarkPatrolledLinkInfo();
872  // If false, there is no patrol link needed/allowed
873  if ( !$linkInfo || !$this->mNewPage ) {
874  $this->mMarkPatrolledLink = '';
875  } else {
876  $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
877  $this->linkRenderer->makeKnownLink(
878  $this->mNewPage,
879  $this->msg( 'markaspatrolleddiff' )->text(),
880  [],
881  [
882  'action' => 'markpatrolled',
883  'rcid' => $linkInfo['rcid'],
884  ]
885  ) . ']</span>';
886  // Allow extensions to change the markpatrolled link
887  $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
888  $this->mMarkPatrolledLink, $linkInfo['rcid'] );
889  }
890  }
892  }
893 
901  protected function getMarkPatrolledLinkInfo() {
902  $user = $this->getUser();
903  $config = $this->getConfig();
904 
905  // Prepare a change patrol link, if applicable
906  if (
907  // Is patrolling enabled and the user allowed to?
908  $config->get( 'UseRCPatrol' ) &&
909  $this->mNewPage &&
910  $this->getAuthority()->probablyCan( 'patrol', $this->mNewPage ) &&
911  // Only do this if the revision isn't more than 6 hours older
912  // than the Max RC age (6h because the RC might not be cleaned out regularly)
913  RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
914  ) {
915  // Look for an unpatrolled change corresponding to this diff
916  $change = RecentChange::newFromConds(
917  [
918  'rc_this_oldid' => $this->mNewid,
919  'rc_patrolled' => RecentChange::PRC_UNPATROLLED
920  ],
921  __METHOD__
922  );
923 
924  if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
925  $rcid = $change->getAttribute( 'rc_id' );
926  } else {
927  // None found or the page has been created by the current user.
928  // If the user could patrol this it already would be patrolled
929  $rcid = 0;
930  }
931 
932  // Allow extensions to possibly change the rcid here
933  // For example the rcid might be set to zero due to the user
934  // being the same as the performer of the change but an extension
935  // might still want to show it under certain conditions
936  $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
937 
938  // Build the link
939  if ( $rcid ) {
940  $this->getOutput()->preventClickjacking();
941  if ( $this->getAuthority()->isAllowed( 'writeapi' ) ) {
942  $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
943  }
944 
945  return [
946  'rcid' => $rcid,
947  ];
948  }
949  }
950 
951  // No mark as patrolled link applicable
952  return false;
953  }
954 
960  private function revisionDeleteLink( RevisionRecord $revRecord ) {
961  $link = Linker::getRevDeleteLink(
962  $this->getAuthority(),
963  $revRecord,
964  $revRecord->getPageAsLinkTarget()
965  );
966  if ( $link !== '' ) {
967  $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
968  }
969 
970  return $link;
971  }
972 
978  public function renderNewRevision() {
979  if ( $this->isContentOverridden ) {
980  // The code below only works with a RevisionRecord object. We could construct a
981  // fake RevisionRecord (here or in setContent), but since this does not seem
982  // needed at the moment, we'll just fail for now.
983  throw new LogicException(
984  __METHOD__
985  . ' is not supported after calling setContent(). Use setRevisions() instead.'
986  );
987  }
988 
989  $out = $this->getOutput();
990  $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
991  # Add "current version as of X" title
992  $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
993  <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
994  # Page content may be handled by a hooked call instead...
995  if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
996  $this->loadNewText();
997  if ( !$this->mNewPage ) {
998  // New revision is unsaved; bail out.
999  // TODO in theory rendering the new revision is a meaningful thing to do
1000  // even if it's unsaved, but a lot of untangling is required to do it safely.
1001  return;
1002  }
1003 
1004  $out->setRevisionId( $this->mNewid );
1005  $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1006  $out->setArticleFlag( true );
1007 
1008  if ( !$this->hookRunner->onArticleRevisionViewCustom(
1009  $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1010  ) {
1011  // Handled by extension
1012  // NOTE: sync with hooks called in Article::view()
1013  } else {
1014  // Normal page
1015  if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1016  // If the Title stored in the context is the same as the one
1017  // of the new revision, we can use its associated WikiPage
1018  // object.
1019  $wikiPage = $this->getWikiPage();
1020  } else {
1021  // Otherwise we need to create our own WikiPage object
1022  $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1023  }
1024 
1025  $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRevisionRecord );
1026 
1027  # WikiPage::getParserOutput() should not return false, but just in case
1028  if ( $parserOutput ) {
1029  // Allow extensions to change parser output here
1030  if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1031  $this, $out, $parserOutput, $wikiPage )
1032  ) {
1033  $out->addParserOutput( $parserOutput, [
1034  'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1035  && $this->getAuthority()->probablyCan(
1036  'edit',
1037  $this->mNewRevisionRecord->getPage()
1038  )
1039  ] );
1040  }
1041  }
1042  }
1043  }
1044 
1045  // Allow extensions to optionally not show the final patrolled link
1046  if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1047  # Add redundant patrol link on bottom...
1048  $out->addHTML( $this->markPatrolledLink() );
1049  }
1050  }
1051 
1058  protected function getParserOutput( WikiPage $page, RevisionRecord $revRecord ) {
1059  if ( !$revRecord->getId() ) {
1060  // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
1061  // the current revision, so fail instead. If need be, WikiPage::getParserOutput
1062  // could be made to accept a RevisionRecord instead of the id.
1063  return false;
1064  }
1065 
1066  $parserOptions = $page->makeParserOptions( $this->getContext() );
1067  $parserOutput = $page->getParserOutput( $parserOptions, $revRecord->getId() );
1068 
1069  return $parserOutput;
1070  }
1071 
1082  public function showDiff( $otitle, $ntitle, $notice = '' ) {
1083  // Allow extensions to affect the output here
1084  $this->hookRunner->onDifferenceEngineShowDiff( $this );
1085 
1086  $diff = $this->getDiff( $otitle, $ntitle, $notice );
1087  if ( $diff === false ) {
1088  $this->showMissingRevision();
1089 
1090  return false;
1091  } else {
1092  $this->showDiffStyle();
1093  $this->getOutput()->addHTML( $diff );
1094 
1095  return true;
1096  }
1097  }
1098 
1102  public function showDiffStyle() {
1103  if ( !$this->isSlotDiffRenderer ) {
1104  $this->getOutput()->addModules( 'mediawiki.diff' );
1105  $this->getOutput()->addModuleStyles( [
1106  'mediawiki.interface.helpers.styles',
1107  'mediawiki.diff.styles'
1108  ] );
1109  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1110  $slotDiffRenderer->addModules( $this->getOutput() );
1111  }
1112  }
1113  }
1114 
1124  public function getDiff( $otitle, $ntitle, $notice = '' ) {
1125  $body = $this->getDiffBody();
1126  if ( $body === false ) {
1127  return false;
1128  }
1129 
1130  $multi = $this->getMultiNotice();
1131  // Display a message when the diff is empty
1132  if ( $body === '' ) {
1133  $notice .= '<div class="mw-diff-empty">' .
1134  $this->msg( 'diff-empty' )->parse() .
1135  "</div>\n";
1136  }
1137 
1138  return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1139  }
1140 
1146  public function getDiffBody() {
1147  $this->mCacheHit = true;
1148  // Check if the diff should be hidden from this user
1149  if ( !$this->isContentOverridden ) {
1150  if ( !$this->loadRevisionData() ) {
1151  return false;
1152  } elseif ( $this->mOldRevisionRecord &&
1153  !$this->mOldRevisionRecord->userCan(
1154  RevisionRecord::DELETED_TEXT,
1155  $this->getAuthority()
1156  )
1157  ) {
1158  return false;
1159  } elseif ( $this->mNewRevisionRecord &&
1160  !$this->mNewRevisionRecord->userCan(
1161  RevisionRecord::DELETED_TEXT,
1162  $this->getAuthority()
1163  ) ) {
1164  return false;
1165  }
1166  // Short-circuit
1167  if ( $this->mOldRevisionRecord === false || (
1168  $this->mOldRevisionRecord &&
1169  $this->mNewRevisionRecord &&
1170  $this->mOldRevisionRecord->getId() &&
1171  $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1172  ) ) {
1173  if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1174  return '';
1175  }
1176  }
1177  }
1178 
1179  // Cacheable?
1180  $key = false;
1181  $services = MediaWikiServices::getInstance();
1182  $cache = $services->getMainWANObjectCache();
1183  $stats = $services->getStatsdDataFactory();
1184  if ( $this->mOldid && $this->mNewid ) {
1185  // Check if subclass is still using the old way
1186  // for backwards-compatibility
1187  $key = $this->getDiffBodyCacheKey();
1188  if ( $key === null ) {
1189  $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1190  }
1191 
1192  // Try cache
1193  if ( !$this->mRefreshCache ) {
1194  $difftext = $cache->get( $key );
1195  if ( is_string( $difftext ) ) {
1196  $stats->updateCount( 'diff_cache.hit', 1 );
1197  $difftext = $this->localiseDiff( $difftext );
1198  $difftext .= "\n<!-- diff cache key $key -->\n";
1199 
1200  return $difftext;
1201  }
1202  } // don't try to load but save the result
1203  }
1204  $this->mCacheHit = false;
1205 
1206  // Loadtext is permission safe, this just clears out the diff
1207  if ( !$this->loadText() ) {
1208  return false;
1209  }
1210 
1211  $difftext = '';
1212  // We've checked for revdelete at the beginning of this method; it's OK to ignore
1213  // read permissions here.
1214  $slotContents = $this->getSlotContents();
1215  foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1216  $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1217  $slotContents[$role]['new'] );
1218  if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1219  // FIXME: ask SlotRoleHandler::getSlotNameMessage
1220  $slotTitle = $role;
1221  $difftext .= $this->getSlotHeader( $slotTitle );
1222  }
1223  $difftext .= $slotDiff;
1224  }
1225 
1226  // Save to cache for 7 days
1227  if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1228  $stats->updateCount( 'diff_cache.uncacheable', 1 );
1229  } elseif ( $key !== false ) {
1230  $stats->updateCount( 'diff_cache.miss', 1 );
1231  $cache->set( $key, $difftext, 7 * 86400 );
1232  } else {
1233  $stats->updateCount( 'diff_cache.uncacheable', 1 );
1234  }
1235  // localise line numbers and title attribute text
1236  $difftext = $this->localiseDiff( $difftext );
1237 
1238  return $difftext;
1239  }
1240 
1247  public function getDiffBodyForRole( $role ) {
1248  $diffRenderers = $this->getSlotDiffRenderers();
1249  if ( !isset( $diffRenderers[$role] ) ) {
1250  return false;
1251  }
1252 
1253  $slotContents = $this->getSlotContents();
1254  $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1255  $slotContents[$role]['new'] );
1256  if ( !$slotDiff ) {
1257  return false;
1258  }
1259 
1260  if ( $role !== SlotRecord::MAIN ) {
1261  // TODO use human-readable role name at least
1262  $slotTitle = $role;
1263  $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1264  }
1265 
1266  return $this->localiseDiff( $slotDiff );
1267  }
1268 
1276  protected function getSlotHeader( $headerText ) {
1277  // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1278  $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1279  $userLang = $this->getLanguage()->getHtmlCode();
1280  return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1281  Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1282  }
1283 
1293  protected function getDiffBodyCacheKey() {
1294  return null;
1295  }
1296 
1310  protected function getDiffBodyCacheKeyParams() {
1311  if ( !$this->mOldid || !$this->mNewid ) {
1312  throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1313  }
1314 
1315  $engine = $this->getEngine();
1316  $params = [
1317  'diff',
1318  $engine === 'php' ? false : $engine, // Back compat
1320  "old-{$this->mOldid}",
1321  "rev-{$this->mNewid}"
1322  ];
1323 
1324  if ( $engine === 'wikidiff2' ) {
1325  $params[] = phpversion( 'wikidiff2' );
1326  }
1327 
1328  if ( !$this->isSlotDiffRenderer ) {
1329  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1330  $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1331  }
1332  }
1333 
1334  return $params;
1335  }
1336 
1344  public function getExtraCacheKeys() {
1345  // This method is called when the DifferenceEngine is used for a slot diff. We only care
1346  // about special things, not the revision IDs, which are added to the cache key by the
1347  // page-level DifferenceEngine, and which might not have a valid value for this object.
1348  $this->mOldid = 123456789;
1349  $this->mNewid = 987654321;
1350 
1351  // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1352  $cacheString = $this->getDiffBodyCacheKey();
1353  if ( $cacheString ) {
1354  return [ $cacheString ];
1355  }
1356 
1357  $params = $this->getDiffBodyCacheKeyParams();
1358 
1359  // Try to get rid of the standard keys to keep the cache key human-readable:
1360  // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1361  // the child class includes the same keys, drop them.
1362  // Uses an obscure PHP feature where static calls to non-static methods are allowed
1363  // as long as we are already in a non-static method of the same class, and the call context
1364  // ($this) will be inherited.
1365  // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1366  $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1367  if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1368  $params = array_slice( $params, count( $standardParams ) );
1369  }
1370 
1371  return $params;
1372  }
1373 
1377  public function setSlotDiffOptions( $options ) {
1378  $this->slotDiffOptions = $options;
1379  }
1380 
1394  public function generateContentDiffBody( Content $old, Content $new ) {
1395  $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1396  if (
1397  $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1398  && $this->isSlotDiffRenderer
1399  ) {
1400  // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1401  // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1402  // This will happen when a content model has no custom slot diff renderer, it does have
1403  // a custom difference engine, but that does not override this method.
1404  throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1405  . 'Please use a SlotDiffRenderer.' );
1406  }
1407  return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1408  }
1409 
1422  public function generateTextDiffBody( $otext, $ntext ) {
1423  $slotDiffRenderer = $this->contentHandlerFactory
1424  ->getContentHandler( CONTENT_MODEL_TEXT )
1425  ->getSlotDiffRenderer( $this->getContext() );
1426  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1427  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1428  // This is too unlikely to happen to bother handling properly.
1429  throw new Exception( 'The slot diff renderer for text content should be a '
1430  . 'TextSlotDiffRenderer subclass' );
1431  }
1432  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1433  }
1434 
1441  public static function getEngine() {
1442  $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1443  ->get( 'DiffEngine' );
1444  $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1445  ->get( 'ExternalDiffEngine' );
1446 
1447  if ( $diffEngine === null ) {
1448  $engines = [ 'external', 'wikidiff2', 'php' ];
1449  } else {
1450  $engines = [ $diffEngine ];
1451  }
1452 
1453  $failureReason = null;
1454  foreach ( $engines as $engine ) {
1455  switch ( $engine ) {
1456  case 'external':
1457  if ( is_string( $externalDiffEngine ) ) {
1458  if ( is_executable( $externalDiffEngine ) ) {
1459  return $externalDiffEngine;
1460  }
1461  $failureReason = 'ExternalDiffEngine config points to a non-executable';
1462  if ( $diffEngine === null ) {
1463  wfDebug( "$failureReason, ignoring" );
1464  }
1465  } else {
1466  $failureReason = 'ExternalDiffEngine config is set to a non-string value';
1467  if ( $diffEngine === null && $externalDiffEngine ) {
1468  wfWarn( "$failureReason, ignoring" );
1469  }
1470  }
1471  break;
1472 
1473  case 'wikidiff2':
1474  if ( function_exists( 'wikidiff2_do_diff' ) ) {
1475  return 'wikidiff2';
1476  }
1477  $failureReason = 'wikidiff2 is not available';
1478  break;
1479 
1480  case 'php':
1481  // Always available.
1482  return 'php';
1483 
1484  default:
1485  throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
1486  }
1487  }
1488  throw new UnexpectedValueException( "Cannot use diff engine '$engine': $failureReason" );
1489  }
1490 
1503  protected function textDiff( $otext, $ntext ) {
1504  $slotDiffRenderer = $this->contentHandlerFactory
1505  ->getContentHandler( CONTENT_MODEL_TEXT )
1506  ->getSlotDiffRenderer( $this->getContext() );
1507  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1508  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1509  // This is too unlikely to happen to bother handling properly.
1510  throw new Exception( 'The slot diff renderer for text content should be a '
1511  . 'TextSlotDiffRenderer subclass' );
1512  }
1513  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1514  }
1515 
1524  protected function debug( $generator = "internal" ) {
1525  if ( !$this->enableDebugComment ) {
1526  return '';
1527  }
1528  $data = [ $generator ];
1529  if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1530  $data[] = wfHostname();
1531  }
1532  $data[] = wfTimestamp( TS_DB );
1533 
1534  return "<!-- diff generator: " .
1535  implode( " ", array_map( "htmlspecialchars", $data ) ) .
1536  " -->\n";
1537  }
1538 
1542  private function getDebugString() {
1543  $engine = self::getEngine();
1544  if ( $engine === 'wikidiff2' ) {
1545  return $this->debug( 'wikidiff2' );
1546  } elseif ( $engine === 'php' ) {
1547  return $this->debug( 'native PHP' );
1548  } else {
1549  return $this->debug( "external $engine" );
1550  }
1551  }
1552 
1559  private function localiseDiff( $text ) {
1560  $text = $this->localiseLineNumbers( $text );
1561  if ( $this->getEngine() === 'wikidiff2' &&
1562  version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1563  ) {
1564  $text = $this->addLocalisedTitleTooltips( $text );
1565  }
1566  return $text;
1567  }
1568 
1576  public function localiseLineNumbers( $text ) {
1577  return preg_replace_callback(
1578  '/<!--LINE (\d+)-->/',
1579  [ $this, 'localiseLineNumbersCb' ],
1580  $text
1581  );
1582  }
1583 
1588  public function localiseLineNumbersCb( $matches ) {
1589  if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1590  return '';
1591  }
1592 
1593  return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1594  }
1595 
1602  private function addLocalisedTitleTooltips( $text ) {
1603  return preg_replace_callback(
1604  '/class="mw-diff-movedpara-(left|right)"/',
1605  [ $this, 'addLocalisedTitleTooltipsCb' ],
1606  $text
1607  );
1608  }
1609 
1614  private function addLocalisedTitleTooltipsCb( array $matches ) {
1615  $key = $matches[1] === 'right' ?
1616  'diff-paragraph-moved-toold' :
1617  'diff-paragraph-moved-tonew';
1618  return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1619  }
1620 
1626  public function getMultiNotice() {
1627  // The notice only make sense if we are diffing two saved revisions of the same page.
1628  if (
1629  !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1630  || !$this->mOldPage || !$this->mNewPage
1631  || !$this->mOldPage->equals( $this->mNewPage )
1632  || $this->mOldRevisionRecord->getId() === null
1633  || $this->mNewRevisionRecord->getId() === null
1634  // (T237709) Deleted revs might have different page IDs
1635  || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1636  || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1637  ) {
1638  return '';
1639  }
1640 
1641  if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1642  $oldRevRecord = $this->mNewRevisionRecord; // flip
1643  $newRevRecord = $this->mOldRevisionRecord; // flip
1644  } else { // normal case
1645  $oldRevRecord = $this->mOldRevisionRecord;
1646  $newRevRecord = $this->mNewRevisionRecord;
1647  }
1648 
1649  // Sanity: don't show the notice if too many rows must be scanned
1650  // @todo show some special message for that case
1651  $nEdits = $this->revisionStore->countRevisionsBetween(
1652  $this->mNewPage->getArticleID(),
1653  $oldRevRecord,
1654  $newRevRecord,
1655  1000
1656  );
1657  if ( $nEdits > 0 && $nEdits <= 1000 ) {
1658  $limit = 100; // use diff-multi-manyusers if too many users
1659  try {
1660  $users = $this->revisionStore->getAuthorsBetween(
1661  $this->mNewPage->getArticleID(),
1662  $oldRevRecord,
1663  $newRevRecord,
1664  null,
1665  $limit
1666  );
1667  $numUsers = count( $users );
1668 
1669  $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1670  $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1671  if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1672  $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1673  }
1674  } catch ( InvalidArgumentException $e ) {
1675  $numUsers = 0;
1676  }
1677 
1678  return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1679  }
1680 
1681  return '';
1682  }
1683 
1693  public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1694  if ( $numUsers === 0 ) {
1695  $msg = 'diff-multi-sameuser';
1696  } elseif ( $numUsers > $limit ) {
1697  $msg = 'diff-multi-manyusers';
1698  $numUsers = $limit;
1699  } else {
1700  $msg = 'diff-multi-otherusers';
1701  }
1702 
1703  return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1704  }
1705 
1710  private function userCanEdit( RevisionRecord $revRecord ) {
1711  if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1712  return false;
1713  }
1714 
1715  return true;
1716  }
1717 
1727  public function getRevisionHeader( RevisionRecord $rev, $complete = '' ) {
1728  $lang = $this->getLanguage();
1729  $user = $this->getUser();
1730  $revtimestamp = $rev->getTimestamp();
1731  $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1732  $dateofrev = $lang->userDate( $revtimestamp, $user );
1733  $timeofrev = $lang->userTime( $revtimestamp, $user );
1734 
1735  $header = $this->msg(
1736  $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1737  $timestamp,
1738  $dateofrev,
1739  $timeofrev
1740  );
1741 
1742  if ( $complete !== 'complete' ) {
1743  return $header->escaped();
1744  }
1745 
1746  $title = $rev->getPageAsLinkTarget();
1747 
1748  $header = $this->linkRenderer->makeKnownLink( $title, $header->text(), [],
1749  [ 'oldid' => $rev->getId() ] );
1750 
1751  if ( $this->userCanEdit( $rev ) ) {
1752  $editQuery = [ 'action' => 'edit' ];
1753  if ( !$rev->isCurrent() ) {
1754  $editQuery['oldid'] = $rev->getId();
1755  }
1756 
1757  $key = $this->getAuthority()->probablyCan( 'edit', $rev->getPage() ) ? 'editold' : 'viewsourceold';
1758  $msg = $this->msg( $key )->text();
1759  $editLink = $this->msg( 'parentheses' )->rawParams(
1760  $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery ) )->escaped();
1761  $header .= ' ' . Html::rawElement(
1762  'span',
1763  [ 'class' => 'mw-diff-edit' ],
1764  $editLink
1765  );
1766  if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1768  'span',
1769  [ 'class' => Linker::getRevisionDeletedClass( $rev ) ],
1770  $header
1771  );
1772  }
1773  } else {
1774  $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1775  }
1776 
1777  return $header;
1778  }
1779 
1792  public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1793  // shared.css sets diff in interface language/dir, but the actual content
1794  // is often in a different language, mostly the page content language/dir
1795  $header = Html::openElement( 'table', [
1796  'class' => [
1797  'diff',
1798  'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1799  'diff-editfont-' . $this->getUser()->getOption( 'editfont' )
1800  ],
1801  'data-mw' => 'interface',
1802  ] );
1803  $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1804 
1805  if ( !$diff && !$otitle ) {
1806  $header .= "
1807  <tr class=\"diff-title\" lang=\"{$userLang}\">
1808  <td class=\"diff-ntitle\">{$ntitle}</td>
1809  </tr>";
1810  $multiColspan = 1;
1811  } else {
1812  if ( $diff ) { // Safari/Chrome show broken output if cols not used
1813  $header .= "
1814  <col class=\"diff-marker\" />
1815  <col class=\"diff-content\" />
1816  <col class=\"diff-marker\" />
1817  <col class=\"diff-content\" />";
1818  $colspan = 2;
1819  $multiColspan = 4;
1820  } else {
1821  $colspan = 1;
1822  $multiColspan = 2;
1823  }
1824  if ( $otitle || $ntitle ) {
1825  $header .= "
1826  <tr class=\"diff-title\" lang=\"{$userLang}\">
1827  <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1828  <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1829  </tr>";
1830  }
1831  }
1832 
1833  if ( $multi != '' ) {
1834  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1835  "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1836  }
1837  if ( $notice != '' ) {
1838  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1839  "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1840  }
1841 
1842  return $header . $diff . "</table>";
1843  }
1844 
1852  public function setContent( Content $oldContent, Content $newContent ) {
1853  $this->mOldContent = $oldContent;
1854  $this->mNewContent = $newContent;
1855 
1856  $this->mTextLoaded = 2;
1857  $this->mRevisionsLoaded = true;
1858  $this->isContentOverridden = true;
1859  $this->slotDiffRenderers = null;
1860  }
1861 
1867  public function setRevisions(
1868  ?RevisionRecord $oldRevision, RevisionRecord $newRevision
1869  ) {
1870  if ( $oldRevision ) {
1871  $this->mOldRevisionRecord = $oldRevision;
1872  $this->mOldid = $oldRevision->getId();
1873  $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1874  // This method is meant for edit diffs and such so there is no reason to provide a
1875  // revision that's not readable to the user, but check it just in case.
1876  $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1877  RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
1878  } else {
1879  $this->mOldPage = null;
1880  $this->mOldRevisionRecord = $this->mOldid = false;
1881  }
1882  $this->mNewRevisionRecord = $newRevision;
1883  $this->mNewid = $newRevision->getId();
1884  $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1885  $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1886  RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
1887 
1888  $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1889  $this->mTextLoaded = $oldRevision ? 2 : 1;
1890  $this->isContentOverridden = false;
1891  $this->slotDiffRenderers = null;
1892  }
1893 
1900  public function setTextLanguage( Language $lang ) {
1901  $this->mDiffLang = $lang;
1902  }
1903 
1916  public function mapDiffPrevNext( $old, $new ) {
1917  if ( $new === 'prev' ) {
1918  // Show diff between revision $old and the previous one. Get previous one from DB.
1919  $newid = intval( $old );
1920  $oldid = false;
1921  $newRev = $this->revisionStore->getRevisionById( $newid );
1922  if ( $newRev ) {
1923  $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
1924  if ( $oldRev ) {
1925  $oldid = $oldRev->getId();
1926  }
1927  }
1928  } elseif ( $new === 'next' ) {
1929  // Show diff between revision $old and the next one. Get next one from DB.
1930  $oldid = intval( $old );
1931  $newid = false;
1932  $oldRev = $this->revisionStore->getRevisionById( $oldid );
1933  if ( $oldRev ) {
1934  $newRev = $this->revisionStore->getNextRevision( $oldRev );
1935  if ( $newRev ) {
1936  $newid = $newRev->getId();
1937  }
1938  }
1939  } else {
1940  $oldid = intval( $old );
1941  $newid = intval( $new );
1942  }
1943 
1944  return [ $oldid, $newid ];
1945  }
1946 
1947  private function loadRevisionIds() {
1948  if ( $this->mRevisionsIdsLoaded ) {
1949  return;
1950  }
1951 
1952  $this->mRevisionsIdsLoaded = true;
1953 
1954  $old = $this->mOldid;
1955  $new = $this->mNewid;
1956 
1957  list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1958  if ( $new === 'next' && $this->mNewid === false ) {
1959  # if no result, NewId points to the newest old revision. The only newer
1960  # revision is cur, which is "0".
1961  $this->mNewid = 0;
1962  }
1963 
1964  $this->hookRunner->onNewDifferenceEngine(
1965  $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
1966  }
1967 
1981  public function loadRevisionData() {
1982  if ( $this->mRevisionsLoaded ) {
1983  return $this->isContentOverridden ||
1984  ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
1985  }
1986 
1987  // Whether it succeeds or fails, we don't want to try again
1988  $this->mRevisionsLoaded = true;
1989 
1990  $this->loadRevisionIds();
1991 
1992  // Load the new RevisionRecord object
1993  if ( $this->mNewid ) {
1994  $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
1995  } else {
1996  $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
1997  }
1998 
1999  if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2000  return false;
2001  }
2002 
2003  // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2004  $this->mNewid = $this->mNewRevisionRecord->getId();
2005  if ( $this->mNewid ) {
2006  $this->mNewPage = Title::newFromLinkTarget(
2007  $this->mNewRevisionRecord->getPageAsLinkTarget()
2008  );
2009  } else {
2010  $this->mNewPage = null;
2011  }
2012 
2013  // Load the old RevisionRecord object
2014  $this->mOldRevisionRecord = false;
2015  if ( $this->mOldid ) {
2016  $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2017  } elseif ( $this->mOldid === 0 ) {
2018  $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2019  if ( $revRecord ) {
2020  $this->mOldid = $revRecord->getId();
2021  $this->mOldRevisionRecord = $revRecord;
2022  } else {
2023  // No previous revision; mark to show as first-version only.
2024  $this->mOldid = false;
2025  $this->mOldRevisionRecord = false;
2026  }
2027  } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2028 
2029  if ( $this->mOldRevisionRecord === null ) {
2030  return false;
2031  }
2032 
2033  if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2034  $this->mOldPage = Title::newFromLinkTarget(
2035  $this->mOldRevisionRecord->getPageAsLinkTarget()
2036  );
2037  } else {
2038  $this->mOldPage = null;
2039  }
2040 
2041  // Load tags information for both revisions
2042  $dbr = wfGetDB( DB_REPLICA );
2043  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2044  if ( $this->mOldid !== false ) {
2045  $tagIds = $dbr->selectFieldValues(
2046  'change_tag',
2047  'ct_tag_id',
2048  [ 'ct_rev_id' => $this->mOldid ],
2049  __METHOD__
2050  );
2051  $tags = [];
2052  foreach ( $tagIds as $tagId ) {
2053  try {
2054  $tags[] = $changeTagDefStore->getName( (int)$tagId );
2055  } catch ( NameTableAccessException $exception ) {
2056  continue;
2057  }
2058  }
2059  $this->mOldTags = implode( ',', $tags );
2060  } else {
2061  $this->mOldTags = false;
2062  }
2063 
2064  $tagIds = $dbr->selectFieldValues(
2065  'change_tag',
2066  'ct_tag_id',
2067  [ 'ct_rev_id' => $this->mNewid ],
2068  __METHOD__
2069  );
2070  $tags = [];
2071  foreach ( $tagIds as $tagId ) {
2072  try {
2073  $tags[] = $changeTagDefStore->getName( (int)$tagId );
2074  } catch ( NameTableAccessException $exception ) {
2075  continue;
2076  }
2077  }
2078  $this->mNewTags = implode( ',', $tags );
2079 
2080  return true;
2081  }
2082 
2091  public function loadText() {
2092  if ( $this->mTextLoaded == 2 ) {
2093  return $this->loadRevisionData() &&
2094  ( $this->mOldRevisionRecord === false || $this->mOldContent )
2095  && $this->mNewContent;
2096  }
2097 
2098  // Whether it succeeds or fails, we don't want to try again
2099  $this->mTextLoaded = 2;
2100 
2101  if ( !$this->loadRevisionData() ) {
2102  return false;
2103  }
2104 
2105  if ( $this->mOldRevisionRecord ) {
2106  $this->mOldContent = $this->mOldRevisionRecord->getContent(
2107  SlotRecord::MAIN,
2108  RevisionRecord::FOR_THIS_USER,
2109  $this->getAuthority()
2110  );
2111  if ( $this->mOldContent === null ) {
2112  return false;
2113  }
2114  }
2115 
2116  $this->mNewContent = $this->mNewRevisionRecord->getContent(
2117  SlotRecord::MAIN,
2118  RevisionRecord::FOR_THIS_USER,
2119  $this->getAuthority()
2120  );
2121  $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2122  if ( $this->mNewContent === null ) {
2123  return false;
2124  }
2125 
2126  return true;
2127  }
2128 
2134  public function loadNewText() {
2135  if ( $this->mTextLoaded >= 1 ) {
2136  return $this->loadRevisionData();
2137  }
2138 
2139  $this->mTextLoaded = 1;
2140 
2141  if ( !$this->loadRevisionData() ) {
2142  return false;
2143  }
2144 
2145  $this->mNewContent = $this->mNewRevisionRecord->getContent(
2146  SlotRecord::MAIN,
2147  RevisionRecord::FOR_THIS_USER,
2148  $this->getAuthority()
2149  );
2150 
2151  $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2152 
2153  return true;
2154  }
2155 
2156 }
Content\getContentHandler
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
DifferenceEngine\$wikiPageFactory
WikiPageFactory $wikiPageFactory
Definition: DifferenceEngine.php:231
DifferenceEngine\$mRevisionsIdsLoaded
bool $mRevisionsIdsLoaded
Have the revisions IDs been loaded.
Definition: DifferenceEngine.php:152
DifferenceEngine\$mNewRevisionRecord
RevisionRecord null $mNewRevisionRecord
New revision (right pane).
Definition: DifferenceEngine.php:106
ContextSource\$context
IContextSource $context
Definition: ContextSource.php:39
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:72
DifferenceEngine\getSlotContents
getSlotContents()
Get the old and new content objects for all slots.
Definition: DifferenceEngine.php:319
DifferenceEngine\markPatrolledLink
markPatrolledLink()
Build a link to mark a change as patrolled.
Definition: DifferenceEngine.php:869
MediaWiki\Revision\RevisionRecord\getContent
getContent( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns the Content of the given slot of this revision.
Definition: RevisionRecord.php:156
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:47
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
DifferenceEngine\getEngine
static getEngine()
Process DiffEngine config and get a sane, usable engine.
Definition: DifferenceEngine.php:1441
DifferenceEngine\$mTextLoaded
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
Definition: DifferenceEngine.php:158
DifferenceEngine\addLocalisedTitleTooltipsCb
addLocalisedTitleTooltipsCb(array $matches)
Definition: DifferenceEngine.php:1614
DifferenceEngine\getDiffBodyCacheKeyParams
getDiffBodyCacheKeyParams()
Get the cache key parameters.
Definition: DifferenceEngine.php:1310
MediaWiki\Revision\RevisionRecord\isCurrent
isCurrent()
Checks whether the revision record is a stored current revision.
Definition: RevisionRecord.php:587
Linker\revUserTools
static revUserTools(RevisionRecord $revRecord, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1323
DifferenceEngine\$unhide
bool $unhide
Show rev_deleted content if allowed.
Definition: DifferenceEngine.php:190
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $noCache=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1322
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:186
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
RecentChange\newFromConds
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
Definition: RecentChange.php:226
DifferenceEngine\setReducedLineNumbers
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
Definition: DifferenceEngine.php:369
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:88
MediaWiki\Revision\RevisionRecord\getPage
getPage()
Returns the page this revision belongs to.
Definition: RevisionRecord.php:370
DifferenceEngine\setContent
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
Definition: DifferenceEngine.php:1852
MediaWiki\Linker\LinkRenderer
Class that generates HTML links for pages.
Definition: LinkRenderer.php:43
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1692
DifferenceEngine\getNewid
getNewid()
Get the ID of new revision (right pane) of the diff.
Definition: DifferenceEngine.php:413
DifferenceEngine\getOldRevision
getOldRevision()
Get the left side of the diff.
Definition: DifferenceEngine.php:425
DifferenceEngine\getOldid
getOldid()
Get the ID of old revision (left pane) of the diff.
Definition: DifferenceEngine.php:401
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:60
DifferenceEngine\$mRevisionsLoaded
bool $mRevisionsLoaded
Have the revisions been loaded.
Definition: DifferenceEngine.php:155
DifferenceEngine\deletedIdMarker
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
Definition: DifferenceEngine.php:480
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2071
wfHostname
wfHostname()
Get host name of the current machine, for use in error reporting.
Definition: GlobalFunctions.php:1245
DifferenceEngine\$enableDebugComment
bool $enableDebugComment
Set this to true to add debug info to the HTML output.
Definition: DifferenceEngine.php:179
MediaWiki\Permissions\Authority\authorizeRead
authorizeRead(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize read access.
DifferenceEngine\$isSlotDiffRenderer
bool $isSlotDiffRenderer
Temporary hack for B/C while slot diff related methods of DifferenceEngine are being deprecated.
Definition: DifferenceEngine.php:204
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1182
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:107
DifferenceEngine\addLocalisedTitleTooltips
addLocalisedTitleTooltips( $text)
Add title attributes for tooltips on moved paragraph indicators.
Definition: DifferenceEngine.php:1602
DifferenceEngine\$mOldRevisionRecord
RevisionRecord null false $mOldRevisionRecord
Old revision (left pane).
Definition: DifferenceEngine.php:96
ContextSource\getRequest
getRequest()
Definition: ContextSource.php:81
PermissionsError
Show an error when a user tries to do something they do not have the necessary permissions for.
Definition: PermissionsError.php:32
DifferenceEngine\showDiffStyle
showDiffStyle()
Add style sheets for diff display.
Definition: DifferenceEngine.php:1102
DifferenceEngine\loadNewText
loadNewText()
Load the text of the new revision, not the old one.
Definition: DifferenceEngine.php:2134
Html\warningBox
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:755
ContextSource\getUser
getUser()
Definition: ContextSource.php:136
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:1082
DifferenceEngine\getDiffBodyCacheKey
getDiffBodyCacheKey()
Returns the cache key for diff body text or content.
Definition: DifferenceEngine.php:1293
DifferenceEngine\localiseDiff
localiseDiff( $text)
Localise diff output.
Definition: DifferenceEngine.php:1559
DifferenceEngine\$mNewid
int string false null $mNewid
Revision ID for the new revision.
Definition: DifferenceEngine.php:84
DifferenceEngine\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: DifferenceEngine.php:220
DifferenceEngine\hasDeletedRevision
hasDeletedRevision()
Checks whether one of the given Revisions was deleted.
Definition: DifferenceEngine.php:517
DifferenceEngine\generateTextDiffBody
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
Definition: DifferenceEngine.php:1422
$dbr
$dbr
Definition: testCompression.php:54
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:153
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:53
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:184
DifferenceEngine\revisionDeleteLink
revisionDeleteLink(RevisionRecord $revRecord)
Definition: DifferenceEngine.php:960
DifferenceEngine\loadRevisionData
loadRevisionData()
Load revision metadata for the specified revisions.
Definition: DifferenceEngine.php:1981
DifferenceEngine\localiseLineNumbersCb
localiseLineNumbersCb( $matches)
Definition: DifferenceEngine.php:1588
MWException
MediaWiki exception.
Definition: MWException.php:29
DifferenceEngine\getDiffBodyForRole
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
Definition: DifferenceEngine.php:1247
DifferenceEngine\$slotDiffRenderers
SlotDiffRenderer[] $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
Definition: DifferenceEngine.php:196
DifferenceEngine\wasCacheHit
wasCacheHit()
Definition: DifferenceEngine.php:390
DifferenceEngine\$mNewTags
string[] null $mNewTags
Change tags of new revision or null if it does not exist / is not saved.
Definition: DifferenceEngine.php:132
Linker\getRevisionDeletedClass
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition: Linker.php:1303
DifferenceEngine\isUserAllowedToSeeRevisions
isUserAllowedToSeeRevisions(Authority $performer)
Checks whether the current user has permission for accessing the revisions of the diff.
Definition: DifferenceEngine.php:572
DifferenceEngine\getDiffLang
getDiffLang()
Get the language of the difference engine, defaults to page content language.
Definition: DifferenceEngine.php:378
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2226
ContextSource\getOutput
getOutput()
Definition: ContextSource.php:126
$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:1276
ContextSource
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Definition: ContextSource.php:33
ContextSource\getWikiPage
getWikiPage()
Get the WikiPage object.
Definition: ContextSource.php:117
DifferenceEngine\getParserOutput
getParserOutput(WikiPage $page, RevisionRecord $revRecord)
Definition: DifferenceEngine.php:1058
DifferenceEngine\getRevisionHeader
getRevisionHeader(RevisionRecord $rev, $complete='')
Get a header for a specified revision.
Definition: DifferenceEngine.php:1727
DifferenceEngine\$mNewContent
Content null $mNewContent
Definition: DifferenceEngine.php:146
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:446
Page\WikiPageFactory
Definition: WikiPageFactory.php:20
ChangesList\flag
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
Definition: ChangesList.php:272
DifferenceEngine\getDiffBody
getDiffBody()
Get the diff table body, without header.
Definition: DifferenceEngine.php:1146
DifferenceEngine\$slotDiffOptions
array $slotDiffOptions
A set of options that will be passed to the SlotDiffRenderer upon creation.
Definition: DifferenceEngine.php:210
DifferenceEngine\getSlotDiffRenderers
getSlotDiffRenderers()
Definition: DifferenceEngine.php:279
$generator
$generator
Definition: generateLocalAutoload.php:13
$title
$title
Definition: testCompression.php:38
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:680
DifferenceEngine\setRevisions
setRevisions(?RevisionRecord $oldRevision, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
Definition: DifferenceEngine.php:1867
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:2091
$revStore
$revStore
Definition: testCompression.php:55
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:894
ContextSource\setContext
setContext(IContextSource $context)
Definition: ContextSource.php:63
DifferenceEngine\$mCacheHit
bool $mCacheHit
Was the diff fetched from cache?
Definition: DifferenceEngine.php:171
deprecatePublicProperty
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
Definition: DeprecationHelper.php:94
DifferenceEngine\markAsSlotDiffRenderer
markAsSlotDiffRenderer()
Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
Definition: DifferenceEngine.php:310
Linker\generateRollback
static generateRollback(RevisionRecord $revRecord, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:2033
MediaWiki\Permissions\Authority
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
DifferenceEngine\__construct
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
#-
Definition: DifferenceEngine.php:243
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:197
DifferenceEngine\getDebugString
getDebugString()
Definition: DifferenceEngine.php:1542
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:706
Linker\revComment
static revComment(RevisionRecord $revRecord, $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:1784
DifferenceEngine\setTextLanguage
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
Definition: DifferenceEngine.php:1900
$content
$content
Definition: router.php:76
DifferenceEngine\$mOldTags
string[] null $mOldTags
Change tags of old revision or null if it does not exist / is not saved.
Definition: DifferenceEngine.php:126
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
ContextSource\getAuthority
getAuthority()
Definition: ContextSource.php:144
$header
$header
Definition: updateCredits.php:37
MediaWiki\Revision\RevisionRecord\getPageAsLinkTarget
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
Definition: RevisionRecord.php:355
DifferenceEngine\$linkRenderer
LinkRenderer $linkRenderer
Definition: DifferenceEngine.php:215
DifferenceEngine\$hookRunner
HookRunner $hookRunner
Definition: DifferenceEngine.php:228
DifferenceEngine\getTitle
getTitle()
1.18 Stability: stableto override Title|null
Definition: DifferenceEngine.php:358
DifferenceEngine\showDiffPage
showDiffPage( $diffOnly=false)
Definition: DifferenceEngine.php:606
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:327
DifferenceEngine\$mOldid
int false null $mOldid
Revision ID for the old revision.
Definition: DifferenceEngine.php:76
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:2278
DifferenceEngine\$isContentOverridden
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e....
Definition: DifferenceEngine.php:168
DifferenceEngine\DIFF_VERSION
const DIFF_VERSION
Constant to indicate diff cache compatibility.
Definition: DifferenceEngine.php:68
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:58
MediaWiki\Permissions\PermissionStatus
A StatusValue for permission errors.
Definition: PermissionStatus.php:35
DifferenceEngine\intermediateEditsMsg
static intermediateEditsMsg( $numEdits, $numUsers, $limit)
Get a notice about how many intermediate edits and users there are.
Definition: DifferenceEngine.php:1693
DifferenceEngine\showMissingRevision
showMissingRevision()
Definition: DifferenceEngine.php:489
Content
Base interface for content objects.
Definition: Content.php:35
DifferenceEngine\loadRevisionIds
loadRevisionIds()
Definition: DifferenceEngine.php:1947
DifferenceEngine\getNewRevision
getNewRevision()
Get the right side of the diff.
Definition: DifferenceEngine.php:434
Title
Represents a title within MediaWiki.
Definition: Title.php:49
SlotDiffRenderer
Renders a diff for a single slot (that is, a diff between two content objects).
Definition: SlotDiffRenderer.php:40
$cache
$cache
Definition: mcc.php:33
MediaWiki\Revision\RevisionRecord\getId
getId( $wikiId=self::LOCAL)
Get revision ID.
Definition: RevisionRecord.php:279
DifferenceEngine\$mDiffLang
Language $mDiffLang
Definition: DifferenceEngine.php:149
MediaWiki\Revision\RevisionRecord\isDeleted
isDeleted( $field)
MCR migration note: this replaced Revision::isDeleted.
Definition: RevisionRecord.php:437
DifferenceEngine\getMarkPatrolledLinkInfo
getMarkPatrolledLinkInfo()
Returns an array of meta data needed to build a "mark as patrolled" link and adds a JS module to the ...
Definition: DifferenceEngine.php:901
CONTENT_MODEL_TEXT
const CONTENT_MODEL_TEXT
Definition: Defines.php:211
DifferenceEngine\$mOldPage
Title null $mOldPage
Title of old revision or null if the old revision does not exist or does not belong to a page.
Definition: DifferenceEngine.php:113
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:91
MediaWiki\Revision\RevisionRecord\userCan
userCan( $field, Authority $performer)
Determine if the give authority is allowed to view a particular field of this revision,...
Definition: RevisionRecord.php:509
Html\openElement
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:252
DifferenceEngine\getExtraCacheKeys
getExtraCacheKeys()
Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys().
Definition: DifferenceEngine.php:1344
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
getTitle
getTitle()
Definition: RevisionSearchResultTrait.php:81
DifferenceEngine
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
Definition: DifferenceEngine.php:58
MediaWiki\Storage\NameTableAccessException
Exception representing a failure to look up a row from a name table.
Definition: NameTableAccessException.php:33
DifferenceEngineSlotDiffRenderer
B/C adapter for turning a DifferenceEngine into a SlotDiffRenderer.
Definition: DifferenceEngineSlotDiffRenderer.php:32
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:1270
MediaWiki\Revision\RevisionRecord\getTimestamp
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
Definition: RevisionRecord.php:459
DifferenceEngine\$mMarkPatrolledLink
string $mMarkPatrolledLink
Link to action=markpatrolled.
Definition: DifferenceEngine.php:187
DifferenceEngine\localiseLineNumbers
localiseLineNumbers( $text)
Replace line numbers with the text in the user's language.
Definition: DifferenceEngine.php:1576
DifferenceEngine\hasSuppressedRevision
hasSuppressedRevision()
Checks whether one of the given Revisions was suppressed.
Definition: DifferenceEngine.php:552
DifferenceEngine\$mOldContent
Content null $mOldContent
Definition: DifferenceEngine.php:139
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:1043
DifferenceEngine\userCanEdit
userCanEdit(RevisionRecord $revRecord)
Definition: DifferenceEngine.php:1710
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:555
DifferenceEngine\debug
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend.
Definition: DifferenceEngine.php:1524
DifferenceEngine\renderNewRevision
renderNewRevision()
Show the new revision of the page.
Definition: DifferenceEngine.php:978
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:232
DifferenceEngine\getDiff
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
Definition: DifferenceEngine.php:1124
DifferenceEngine\setSlotDiffOptions
setSlotDiffOptions( $options)
Definition: DifferenceEngine.php:1377
DeprecationHelper
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated or implementati...
Definition: DeprecationHelper.php:60
DifferenceEngine\generateContentDiffBody
generateContentDiffBody(Content $old, Content $new)
Generate a diff, no caching.
Definition: DifferenceEngine.php:1394
Linker\getRevDeleteLink
static getRevDeleteLink(Authority $performer, RevisionRecord $revRecord, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition: Linker.php:2362
DifferenceEngine\mapDiffPrevNext
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
Definition: DifferenceEngine.php:1916
DifferenceEngine\addHeader
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
Definition: DifferenceEngine.php:1792
DifferenceEngine\shouldBeHiddenFromUser
shouldBeHiddenFromUser(Authority $performer)
Checks whether the diff should be hidden from the current user This is based on whether the user is a...
Definition: DifferenceEngine.php:598
ChangeTags\formatSummaryRow
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:194
DifferenceEngine\getPermissionErrors
getPermissionErrors(Authority $performer)
Get the permission errors associated with the revisions for the current diff.
Definition: DifferenceEngine.php:535
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:42
DifferenceEngine\textDiff
textDiff( $otext, $ntext)
Generates diff, to be wrapped internally in a logging/instrumentation.
Definition: DifferenceEngine.php:1503
DifferenceEngine\$revisionStore
RevisionStore $revisionStore
Definition: DifferenceEngine.php:225
MediaWiki\Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
DifferenceEngine\$mNewPage
Title null $mNewPage
Title of new revision or null if the new revision does not exist or does not belong to a page.
Definition: DifferenceEngine.php:120
TextSlotDiffRenderer
Renders a slot diff by doing a text diff on the native representation.
Definition: TextSlotDiffRenderer.php:38
DifferenceEngine\$mRefreshCache
bool $mRefreshCache
Refresh the diff cache.
Definition: DifferenceEngine.php:193
DifferenceEngine\getMultiNotice
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
Definition: DifferenceEngine.php:1626