MediaWiki  master
DifferenceEngine.php
Go to the documentation of this file.
1 <?php
34 
58 
60 
67  private const DIFF_VERSION = '1.12';
68 
75  protected $mOldid;
76 
83  protected $mNewid;
84 
96 
106 
112  protected $mOldPage;
113 
119  protected $mNewPage;
120 
125  private $mOldTags;
126 
131  private $mNewTags;
132 
138  private $mOldContent;
139 
145  private $mNewContent;
146 
148  protected $mDiffLang;
149 
151  private $mRevisionsIdsLoaded = false;
152 
154  protected $mRevisionsLoaded = false;
155 
157  protected $mTextLoaded = 0;
158 
167  protected $isContentOverridden = false;
168 
170  protected $mCacheHit = false;
171 
178  public $enableDebugComment = false;
179 
183  protected $mReducedLineNumbers = false;
184 
186  protected $mMarkPatrolledLink = null;
187 
189  protected $unhide = false;
190 
192  protected $mRefreshCache = false;
193 
195  protected $slotDiffRenderers = null;
196 
203  protected $isSlotDiffRenderer = false;
204 
209  private $slotDiffOptions = [];
210 
214  protected $linkRenderer;
215 
220 
224  private $revisionStore;
225 
227  private $hookRunner;
228 
230  private $hookContainer;
231 
234 
245  public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
246  $refreshCache = false, $unhide = false
247  ) {
248  $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
249  $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
250  $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
251  $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
252  $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
253  $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
254  $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
255  $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
256  $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
257 
258  if ( $context instanceof IContextSource ) {
259  $this->setContext( $context );
260  }
261 
262  wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
263 
264  $this->mOldid = $old;
265  $this->mNewid = $new;
266  $this->mRefreshCache = $refreshCache;
267  $this->unhide = $unhide;
268 
269  $services = MediaWikiServices::getInstance();
270  $this->linkRenderer = $services->getLinkRenderer();
271  $this->contentHandlerFactory = $services->getContentHandlerFactory();
272  $this->revisionStore = $services->getRevisionStore();
273  $this->hookContainer = $services->getHookContainer();
274  $this->hookRunner = new HookRunner( $this->hookContainer );
275  $this->wikiPageFactory = $services->getWikiPageFactory();
276  }
277 
282  protected function getSlotDiffRenderers() {
283  if ( $this->isSlotDiffRenderer ) {
284  throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
285  }
286 
287  if ( $this->slotDiffRenderers === null ) {
288  if ( !$this->loadRevisionData() ) {
289  return [];
290  }
291 
292  $slotContents = $this->getSlotContents();
293  $this->slotDiffRenderers = array_map( function ( $contents ) {
295  $content = $contents['new'] ?: $contents['old'];
296  $context = $this->getContext();
297 
298  return $content->getContentHandler()->getSlotDiffRenderer(
299  $context,
300  $this->slotDiffOptions
301  );
302  }, $slotContents );
303  }
305  }
306 
313  public function markAsSlotDiffRenderer() {
314  $this->isSlotDiffRenderer = true;
315  }
316 
322  protected function getSlotContents() {
323  if ( $this->isContentOverridden ) {
324  return [
325  SlotRecord::MAIN => [
326  'old' => $this->mOldContent,
327  'new' => $this->mNewContent,
328  ]
329  ];
330  } elseif ( !$this->loadRevisionData() ) {
331  return [];
332  }
333 
334  $newSlots = $this->mNewRevisionRecord->getSlots()->getSlots();
335  if ( $this->mOldRevisionRecord ) {
336  $oldSlots = $this->mOldRevisionRecord->getSlots()->getSlots();
337  } else {
338  $oldSlots = [];
339  }
340  // The order here will determine the visual order of the diff. The current logic is
341  // slots of the new revision first in natural order, then deleted ones. This is ad hoc
342  // and should not be relied on - in the future we may want the ordering to depend
343  // on the page type.
344  $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
345 
346  $slots = [];
347  foreach ( $roles as $role ) {
348  $slots[$role] = [
349  'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
350  'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
351  ];
352  }
353  // move main slot to front
354  if ( isset( $slots[SlotRecord::MAIN] ) ) {
355  $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
356  }
357  return $slots;
358  }
359 
361  public function getTitle() {
362  // T202454 avoid errors when there is no title
363  return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
364  }
365 
372  public function setReducedLineNumbers( $value = true ) {
373  $this->mReducedLineNumbers = $value;
374  }
375 
381  public function getDiffLang() {
382  if ( $this->mDiffLang === null ) {
383  # Default language in which the diff text is written.
384  $this->mDiffLang = $this->getTitle()->getPageLanguage();
385  }
386 
387  return $this->mDiffLang;
388  }
389 
393  public function wasCacheHit() {
394  return $this->mCacheHit;
395  }
396 
404  public function getOldid() {
405  $this->loadRevisionIds();
406 
407  return $this->mOldid;
408  }
409 
416  public function getNewid() {
417  $this->loadRevisionIds();
418 
419  return $this->mNewid;
420  }
421 
428  public function getOldRevision() {
429  return $this->mOldRevisionRecord ?: null;
430  }
431 
437  public function getNewRevision() {
439  }
440 
449  public function deletedLink( $id ) {
450  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
451  if ( $permissionManager->userHasRight( $this->getUser(), 'deletedhistory' ) ) {
452  $dbr = wfGetDB( DB_REPLICA );
454  $arQuery = $revStore->getArchiveQueryInfo();
455  $row = $dbr->selectRow(
456  $arQuery['tables'],
457  array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
458  [ 'ar_rev_id' => $id ],
459  __METHOD__,
460  [],
461  $arQuery['joins']
462  );
463  if ( $row ) {
464  $revRecord = $revStore->newRevisionFromArchiveRow( $row );
465  $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
466 
467  return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
468  'target' => $title->getPrefixedText(),
469  'timestamp' => $revRecord->getTimestamp()
470  ] );
471  }
472  }
473 
474  return false;
475  }
476 
484  public function deletedIdMarker( $id ) {
485  $link = $this->deletedLink( $id );
486  if ( $link ) {
487  return "[$link $id]";
488  } else {
489  return (string)$id;
490  }
491  }
492 
493  private function showMissingRevision() {
494  $out = $this->getOutput();
495 
496  $missing = [];
497  if ( $this->mOldRevisionRecord === null ||
498  ( $this->mOldRevisionRecord && $this->mOldContent === null )
499  ) {
500  $missing[] = $this->deletedIdMarker( $this->mOldid );
501  }
502  if ( $this->mNewRevisionRecord === null ||
503  ( $this->mNewRevisionRecord && $this->mNewContent === null )
504  ) {
505  $missing[] = $this->deletedIdMarker( $this->mNewid );
506  }
507 
508  $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
509  $msg = $this->msg( 'difference-missing-revision' )
510  ->params( $this->getLanguage()->listToText( $missing ) )
511  ->numParams( count( $missing ) )
512  ->parseAsBlock();
513  $out->addHTML( $msg );
514  }
515 
521  public function hasDeletedRevision() {
522  $this->loadRevisionData();
523  return (
524  $this->mNewRevisionRecord &&
525  $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
526  ) ||
527  (
528  $this->mOldRevisionRecord &&
529  $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
530  );
531  }
532 
539  public function getPermissionErrors( User $user ) {
540  $this->loadRevisionData();
541  $permErrors = [];
542  $permManager = MediaWikiServices::getInstance()->getPermissionManager();
543  if ( $this->mNewPage ) {
544  $permErrors = $permManager->getPermissionErrors( 'read', $user, $this->mNewPage );
545  }
546  if ( $this->mOldPage ) {
547  $permErrors = wfMergeErrorArrays( $permErrors,
548  $permManager->getPermissionErrors( 'read', $user, $this->mOldPage ) );
549  }
550  return $permErrors;
551  }
552 
558  public function hasSuppressedRevision() {
559  return $this->hasDeletedRevision() && (
560  ( $this->mOldRevisionRecord &&
561  $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
562  ( $this->mNewRevisionRecord &&
563  $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
564  );
565  }
566 
578  public function isUserAllowedToSeeRevisions( $user ) {
579  $this->loadRevisionData();
580  // $this->mNewRev will only be falsy if a loading error occurred
581  // (in which case the user is allowed to see).
582  $allowed = !$this->mNewRevisionRecord || RevisionRecord::userCanBitfield(
583  $this->mNewRevisionRecord->getVisibility(),
584  RevisionRecord::DELETED_TEXT,
585  $user
586  );
587  if ( $this->mOldRevisionRecord &&
588  !RevisionRecord::userCanBitfield(
589  $this->mOldRevisionRecord->getVisibility(),
590  RevisionRecord::DELETED_TEXT,
591  $user
592  )
593  ) {
594  $allowed = false;
595  }
596  return $allowed;
597  }
598 
606  public function shouldBeHiddenFromUser( $user ) {
607  return $this->hasDeletedRevision() && ( !$this->unhide ||
608  !$this->isUserAllowedToSeeRevisions( $user ) );
609  }
610 
614  public function showDiffPage( $diffOnly = false ) {
615  # Allow frames except in certain special cases
616  $out = $this->getOutput();
617  $out->allowClickjacking();
618  $out->setRobotPolicy( 'noindex,nofollow' );
619 
620  // Allow extensions to add any extra output here
621  $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
622 
623  if ( !$this->loadRevisionData() ) {
624  if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
625  $this->showMissingRevision();
626  }
627  return;
628  }
629 
630  $user = $this->getUser();
631  $permErrors = $this->getPermissionErrors( $user );
632  if ( count( $permErrors ) ) {
633  throw new PermissionsError( 'read', $permErrors );
634  }
635 
636  $rollback = '';
637 
638  $query = $this->slotDiffOptions;
639  # Carry over 'diffonly' param via navigation links
640  if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
641  $query['diffonly'] = $diffOnly;
642  }
643  # Cascade unhide param in links for easy deletion browsing
644  if ( $this->unhide ) {
645  $query['unhide'] = 1;
646  }
647 
648  # Check if one of the revisions is deleted/suppressed
649  $deleted = $this->hasDeletedRevision();
650  $suppressed = $this->hasSuppressedRevision();
651  $allowed = $this->isUserAllowedToSeeRevisions( $user );
652 
653  $revisionTools = [];
654 
655  # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
656  # a diff between a version V and its previous version V' AND the version V
657  # is the first version of that article. In that case, V' does not exist.
658  if ( $this->mOldRevisionRecord === false ) {
659  if ( $this->mNewPage ) {
660  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
661  }
662  $samePage = true;
663  $oldHeader = '';
664  // Allow extensions to change the $oldHeader variable
665  $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
666  } else {
667  $this->hookRunner->onDifferenceEngineViewHeader( $this );
668 
669  // DiffViewHeader hook is hard deprecated since 1.35
670  if ( $this->hookContainer->isRegistered( 'DiffViewHeader' ) ) {
671  // Only create the Revision object if needed
672  // If old or new are falsey, use null
673  $legacyOldRev = $this->mOldRevisionRecord ?
674  new Revision( $this->mOldRevisionRecord ) :
675  null;
676  $legacyNewRev = $this->mNewRevisionRecord ?
677  new Revision( $this->mNewRevisionRecord ) :
678  null;
679  $this->hookRunner->onDiffViewHeader(
680  $this,
681  $legacyOldRev,
682  $legacyNewRev
683  );
684  }
685 
686  if ( !$this->mOldPage || !$this->mNewPage ) {
687  // XXX say something to the user?
688  $samePage = false;
689  } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
690  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
691  $samePage = true;
692  } else {
693  $out->setPageTitle( $this->msg( 'difference-title-multipage',
694  $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
695  $out->addSubtitle( $this->msg( 'difference-multipage' ) );
696  $samePage = false;
697  }
698 
699  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
700 
701  if ( $samePage && $this->mNewPage && $permissionManager->quickUserCan(
702  'edit', $user, $this->mNewPage
703  ) ) {
704  if ( $this->mNewRevisionRecord->isCurrent() && $permissionManager->quickUserCan(
705  'rollback', $user, $this->mNewPage
706  ) ) {
707  $rollbackLink = Linker::generateRollback(
708  $this->mNewRevisionRecord,
709  $this->getContext(),
710  [ 'noBrackets' ]
711  );
712  if ( $rollbackLink ) {
713  $out->preventClickjacking();
714  $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
715  }
716  }
717 
718  if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
719  $this->userCanEdit( $this->mNewRevisionRecord )
720  ) {
721  $undoLink = Html::element( 'a', [
722  'href' => $this->mNewPage->getLocalURL( [
723  'action' => 'edit',
724  'undoafter' => $this->mOldid,
725  'undo' => $this->mNewid
726  ] ),
727  'title' => Linker::titleAttrib( 'undo' ),
728  ],
729  $this->msg( 'editundo' )->text()
730  );
731  $revisionTools['mw-diff-undo'] = $undoLink;
732  }
733  }
734  # Make "previous revision link"
735  $hasPrevious = $samePage && $this->mOldPage &&
736  $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
737  if ( $hasPrevious ) {
738  $prevlink = $this->linkRenderer->makeKnownLink(
739  $this->mOldPage,
740  $this->msg( 'previousdiff' )->text(),
741  [ 'id' => 'differences-prevlink' ],
742  [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
743  );
744  } else {
745  $prevlink = "\u{00A0}";
746  }
747 
748  if ( $this->mOldRevisionRecord->isMinor() ) {
749  $oldminor = ChangesList::flag( 'minor' );
750  } else {
751  $oldminor = '';
752  }
753 
754  $oldRevRecord = $this->mOldRevisionRecord;
755 
756  $ldel = $this->revisionDeleteLink( $oldRevRecord );
757  $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' );
758  $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
759 
760  $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
761  '<div id="mw-diff-otitle2">' .
762  Linker::revUserTools( $oldRevRecord, !$this->unhide ) . '</div>' .
763  '<div id="mw-diff-otitle3">' . $oldminor .
764  Linker::revComment( $oldRevRecord, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
765  '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
766  '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
767 
768  // Allow extensions to change the $oldHeader variable
769  $this->hookRunner->onDifferenceEngineOldHeader(
770  $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
771  }
772 
773  $out->addJsConfigVars( [
774  'wgDiffOldId' => $this->mOldid,
775  'wgDiffNewId' => $this->mNewid,
776  ] );
777 
778  # Make "next revision link"
779  # Skip next link on the top revision
780  if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
781  $nextlink = $this->linkRenderer->makeKnownLink(
782  $this->mNewPage,
783  $this->msg( 'nextdiff' )->text(),
784  [ 'id' => 'differences-nextlink' ],
785  [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
786  );
787  } else {
788  $nextlink = "\u{00A0}";
789  }
790 
791  if ( $this->mNewRevisionRecord->isMinor() ) {
792  $newminor = ChangesList::flag( 'minor' );
793  } else {
794  $newminor = '';
795  }
796 
797  # Handle RevisionDelete links...
798  $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
799 
800  # Allow extensions to define their own revision tools
801  $this->hookRunner->onDiffTools(
802  $this->mNewRevisionRecord,
803  $revisionTools,
804  $this->mOldRevisionRecord ?: null,
805  $user
806  );
807 
808  # Hook deprecated since 1.35
809  if ( $this->hookContainer->isRegistered( 'DiffRevisionTools' ) ) {
810  # Only create the Revision objects if they are needed
811  $legacyOldRev = $this->mOldRevisionRecord ?
812  new Revision( $this->mOldRevisionRecord ) :
813  null;
814  $legacyNewRev = $this->mNewRevisionRecord ?
815  new Revision( $this->mNewRevisionRecord ) :
816  null;
817  $this->hookRunner->onDiffRevisionTools(
818  $legacyNewRev,
819  $revisionTools,
820  $legacyOldRev,
821  $user
822  );
823  }
824 
825  $formattedRevisionTools = [];
826  // Put each one in parentheses (poor man's button)
827  foreach ( $revisionTools as $key => $tool ) {
828  $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
829  $element = Html::rawElement(
830  'span',
831  [ 'class' => $toolClass ],
832  $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
833  );
834  $formattedRevisionTools[] = $element;
835  }
836 
837  $newRevRecord = $this->mNewRevisionRecord;
838 
839  $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) .
840  ' ' . implode( ' ', $formattedRevisionTools );
841  $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
842 
843  $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
844  '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
845  " $rollback</div>" .
846  '<div id="mw-diff-ntitle3">' . $newminor .
847  Linker::revComment( $newRevRecord, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
848  '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
849  '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
850 
851  // Allow extensions to change the $newHeader variable
852  $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
853  $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
854  $rdel, $this->unhide );
855 
856  # If the diff cannot be shown due to a deleted revision, then output
857  # the diff header and links to unhide (if available)...
858  if ( $this->shouldBeHiddenFromUser( $user ) ) {
859  $this->showDiffStyle();
860  $multi = $this->getMultiNotice();
861  $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
862  if ( !$allowed ) {
863  $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
864  # Give explanation for why revision is not visible
865  $out->addHtml(
867  $this->msg( $msg )->parse(),
868  'plainlinks'
869  )
870  );
871  } else {
872  # Give explanation and add a link to view the diff...
873  $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
874  $link = $this->getTitle()->getFullURL( $query );
875  $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
876  $out->addHtml(
878  $this->msg( $msg, $link )->parse(),
879  'plainlinks'
880  )
881  );
882  }
883  # Otherwise, output a regular diff...
884  } else {
885  # Add deletion notice if the user is viewing deleted content
886  $notice = '';
887  if ( $deleted ) {
888  $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
889  $notice = Html::warningBox(
890  $this->msg( $msg )->parse(),
891  'plainlinks'
892  );
893  }
894  $this->showDiff( $oldHeader, $newHeader, $notice );
895  if ( !$diffOnly ) {
896  $this->renderNewRevision();
897  }
898  }
899  }
900 
911  public function markPatrolledLink() {
912  if ( $this->mMarkPatrolledLink === null ) {
913  $linkInfo = $this->getMarkPatrolledLinkInfo();
914  // If false, there is no patrol link needed/allowed
915  if ( !$linkInfo || !$this->mNewPage ) {
916  $this->mMarkPatrolledLink = '';
917  } else {
918  $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
919  $this->linkRenderer->makeKnownLink(
920  $this->mNewPage,
921  $this->msg( 'markaspatrolleddiff' )->text(),
922  [],
923  [
924  'action' => 'markpatrolled',
925  'rcid' => $linkInfo['rcid'],
926  ]
927  ) . ']</span>';
928  // Allow extensions to change the markpatrolled link
929  $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
930  $this->mMarkPatrolledLink, $linkInfo['rcid'] );
931  }
932  }
934  }
935 
943  protected function getMarkPatrolledLinkInfo() {
944  $user = $this->getUser();
945  $config = $this->getConfig();
946  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
947 
948  // Prepare a change patrol link, if applicable
949  if (
950  // Is patrolling enabled and the user allowed to?
951  $config->get( 'UseRCPatrol' ) &&
952  $this->mNewPage &&
953  $permissionManager->quickUserCan( 'patrol', $user, $this->mNewPage ) &&
954  // Only do this if the revision isn't more than 6 hours older
955  // than the Max RC age (6h because the RC might not be cleaned out regularly)
956  RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
957  ) {
958  // Look for an unpatrolled change corresponding to this diff
959  $change = RecentChange::newFromConds(
960  [
961  'rc_this_oldid' => $this->mNewid,
962  'rc_patrolled' => RecentChange::PRC_UNPATROLLED
963  ],
964  __METHOD__
965  );
966 
967  if ( $change && !$change->getPerformer()->equals( $user ) ) {
968  $rcid = $change->getAttribute( 'rc_id' );
969  } else {
970  // None found or the page has been created by the current user.
971  // If the user could patrol this it already would be patrolled
972  $rcid = 0;
973  }
974 
975  // Allow extensions to possibly change the rcid here
976  // For example the rcid might be set to zero due to the user
977  // being the same as the performer of the change but an extension
978  // might still want to show it under certain conditions
979  $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
980 
981  // Build the link
982  if ( $rcid ) {
983  $this->getOutput()->preventClickjacking();
984  if ( $permissionManager->userHasRight( $user, 'writeapi' ) ) {
985  $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
986  }
987 
988  return [
989  'rcid' => $rcid,
990  ];
991  }
992  }
993 
994  // No mark as patrolled link applicable
995  return false;
996  }
997 
1003  private function revisionDeleteLink( RevisionRecord $revRecord ) {
1004  $link = Linker::getRevDeleteLink(
1005  $this->getUser(),
1006  $revRecord,
1007  $revRecord->getPageAsLinkTarget()
1008  );
1009  if ( $link !== '' ) {
1010  $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
1011  }
1012 
1013  return $link;
1014  }
1015 
1021  public function renderNewRevision() {
1022  if ( $this->isContentOverridden ) {
1023  // The code below only works with a Revision object. We could construct a fake revision
1024  // (here or in setContent), but since this does not seem needed at the moment,
1025  // we'll just fail for now.
1026  throw new LogicException(
1027  __METHOD__
1028  . ' is not supported after calling setContent(). Use setRevisions() instead.'
1029  );
1030  }
1031 
1032  $out = $this->getOutput();
1033  $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1034  # Add "current version as of X" title
1035  $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
1036  <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1037  # Page content may be handled by a hooked call instead...
1038  if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1039  $this->loadNewText();
1040  if ( !$this->mNewPage ) {
1041  // New revision is unsaved; bail out.
1042  // TODO in theory rendering the new revision is a meaningful thing to do
1043  // even if it's unsaved, but a lot of untangling is required to do it safely.
1044  return;
1045  }
1046 
1047  $out->setRevisionId( $this->mNewid );
1048  $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1049  $out->setArticleFlag( true );
1050 
1051  if ( !$this->hookRunner->onArticleRevisionViewCustom(
1052  $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1053  ) {
1054  // Handled by extension
1055  // NOTE: sync with hooks called in Article::view()
1056  } else {
1057  // Normal page
1058  if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1059  // If the Title stored in the context is the same as the one
1060  // of the new revision, we can use its associated WikiPage
1061  // object.
1062  $wikiPage = $this->getWikiPage();
1063  } else {
1064  // Otherwise we need to create our own WikiPage object
1065  $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1066  }
1067 
1068  $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRevisionRecord );
1069 
1070  # WikiPage::getParserOutput() should not return false, but just in case
1071  if ( $parserOutput ) {
1072  // Allow extensions to change parser output here
1073  if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1074  $this, $out, $parserOutput, $wikiPage )
1075  ) {
1076  $out->addParserOutput( $parserOutput, [
1077  'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1078  && MediaWikiServices::getInstance()->getPermissionManager()->quickUserCan(
1079  'edit',
1080  $this->getUser(),
1081  $this->mNewRevisionRecord->getPageAsLinkTarget()
1082  )
1083  ] );
1084  }
1085  }
1086  }
1087  }
1088 
1089  // Allow extensions to optionally not show the final patrolled link
1090  if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1091  # Add redundant patrol link on bottom...
1092  $out->addHTML( $this->markPatrolledLink() );
1093  }
1094  }
1095 
1102  protected function getParserOutput( WikiPage $page, RevisionRecord $revRecord ) {
1103  if ( !$revRecord->getId() ) {
1104  // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
1105  // the current revision, so fail instead. If need be, WikiPage::getParserOutput
1106  // could be made to accept a Revision or RevisionRecord instead of the id.
1107  return false;
1108  }
1109 
1110  $parserOptions = $page->makeParserOptions( $this->getContext() );
1111  $parserOutput = $page->getParserOutput( $parserOptions, $revRecord->getId() );
1112 
1113  return $parserOutput;
1114  }
1115 
1126  public function showDiff( $otitle, $ntitle, $notice = '' ) {
1127  // Allow extensions to affect the output here
1128  $this->hookRunner->onDifferenceEngineShowDiff( $this );
1129 
1130  $diff = $this->getDiff( $otitle, $ntitle, $notice );
1131  if ( $diff === false ) {
1132  $this->showMissingRevision();
1133 
1134  return false;
1135  } else {
1136  $this->showDiffStyle();
1137  $this->getOutput()->addHTML( $diff );
1138 
1139  return true;
1140  }
1141  }
1142 
1146  public function showDiffStyle() {
1147  if ( !$this->isSlotDiffRenderer ) {
1148  $this->getOutput()->addModuleStyles( [
1149  'mediawiki.interface.helpers.styles',
1150  'mediawiki.diff.styles'
1151  ] );
1152  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1153  $slotDiffRenderer->addModules( $this->getOutput() );
1154  }
1155  }
1156  }
1157 
1167  public function getDiff( $otitle, $ntitle, $notice = '' ) {
1168  $body = $this->getDiffBody();
1169  if ( $body === false ) {
1170  return false;
1171  }
1172 
1173  $multi = $this->getMultiNotice();
1174  // Display a message when the diff is empty
1175  if ( $body === '' ) {
1176  $notice .= '<div class="mw-diff-empty">' .
1177  $this->msg( 'diff-empty' )->parse() .
1178  "</div>\n";
1179  }
1180 
1181  return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1182  }
1183 
1189  public function getDiffBody() {
1190  $this->mCacheHit = true;
1191  // Check if the diff should be hidden from this user
1192  if ( !$this->isContentOverridden ) {
1193  if ( !$this->loadRevisionData() ) {
1194  return false;
1195  } elseif ( $this->mOldRevisionRecord &&
1196  !RevisionRecord::userCanBitfield(
1197  $this->mOldRevisionRecord->getVisibility(),
1198  RevisionRecord::DELETED_TEXT,
1199  $this->getUser()
1200  )
1201  ) {
1202  return false;
1203  } elseif ( $this->mNewRevisionRecord &&
1204  !RevisionRecord::userCanBitfield(
1205  $this->mNewRevisionRecord->getVisibility(),
1206  RevisionRecord::DELETED_TEXT,
1207  $this->getUser()
1208  )
1209  ) {
1210  return false;
1211  }
1212  // Short-circuit
1213  if ( $this->mOldRevisionRecord === false || (
1214  $this->mOldRevisionRecord &&
1215  $this->mNewRevisionRecord &&
1216  $this->mOldRevisionRecord->getId() &&
1217  $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1218  ) ) {
1219  if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1220  return '';
1221  }
1222  }
1223  }
1224 
1225  // Cacheable?
1226  $key = false;
1227  $services = MediaWikiServices::getInstance();
1228  $cache = $services->getMainWANObjectCache();
1229  $stats = $services->getStatsdDataFactory();
1230  if ( $this->mOldid && $this->mNewid ) {
1231  // Check if subclass is still using the old way
1232  // for backwards-compatibility
1233  $key = $this->getDiffBodyCacheKey();
1234  if ( $key === null ) {
1235  $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1236  }
1237 
1238  // Try cache
1239  if ( !$this->mRefreshCache ) {
1240  $difftext = $cache->get( $key );
1241  if ( is_string( $difftext ) ) {
1242  $stats->updateCount( 'diff_cache.hit', 1 );
1243  $difftext = $this->localiseDiff( $difftext );
1244  $difftext .= "\n<!-- diff cache key $key -->\n";
1245 
1246  return $difftext;
1247  }
1248  } // don't try to load but save the result
1249  }
1250  $this->mCacheHit = false;
1251 
1252  // Loadtext is permission safe, this just clears out the diff
1253  if ( !$this->loadText() ) {
1254  return false;
1255  }
1256 
1257  $difftext = '';
1258  // We've checked for revdelete at the beginning of this method; it's OK to ignore
1259  // read permissions here.
1260  $slotContents = $this->getSlotContents();
1261  foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1262  $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1263  $slotContents[$role]['new'] );
1264  if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1265  // FIXME: ask SlotRoleHandler::getSlotNameMessage
1266  $slotTitle = $role;
1267  $difftext .= $this->getSlotHeader( $slotTitle );
1268  }
1269  $difftext .= $slotDiff;
1270  }
1271 
1272  // Save to cache for 7 days
1273  if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1274  $stats->updateCount( 'diff_cache.uncacheable', 1 );
1275  } elseif ( $key !== false ) {
1276  $stats->updateCount( 'diff_cache.miss', 1 );
1277  $cache->set( $key, $difftext, 7 * 86400 );
1278  } else {
1279  $stats->updateCount( 'diff_cache.uncacheable', 1 );
1280  }
1281  // localise line numbers and title attribute text
1282  $difftext = $this->localiseDiff( $difftext );
1283 
1284  return $difftext;
1285  }
1286 
1293  public function getDiffBodyForRole( $role ) {
1294  $diffRenderers = $this->getSlotDiffRenderers();
1295  if ( !isset( $diffRenderers[$role] ) ) {
1296  return false;
1297  }
1298 
1299  $slotContents = $this->getSlotContents();
1300  $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1301  $slotContents[$role]['new'] );
1302  if ( !$slotDiff ) {
1303  return false;
1304  }
1305 
1306  if ( $role !== SlotRecord::MAIN ) {
1307  // TODO use human-readable role name at least
1308  $slotTitle = $role;
1309  $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1310  }
1311 
1312  return $this->localiseDiff( $slotDiff );
1313  }
1314 
1322  protected function getSlotHeader( $headerText ) {
1323  // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1324  $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1325  $userLang = $this->getLanguage()->getHtmlCode();
1326  return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1327  Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1328  }
1329 
1339  protected function getDiffBodyCacheKey() {
1340  return null;
1341  }
1342 
1356  protected function getDiffBodyCacheKeyParams() {
1357  if ( !$this->mOldid || !$this->mNewid ) {
1358  throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1359  }
1360 
1361  $engine = $this->getEngine();
1362  $params = [
1363  'diff',
1364  $engine === 'php' ? false : $engine, // Back compat
1366  "old-{$this->mOldid}",
1367  "rev-{$this->mNewid}"
1368  ];
1369 
1370  if ( $engine === 'wikidiff2' ) {
1371  $params[] = phpversion( 'wikidiff2' );
1372  }
1373 
1374  if ( !$this->isSlotDiffRenderer ) {
1375  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1376  $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1377  }
1378  }
1379 
1380  return $params;
1381  }
1382 
1390  public function getExtraCacheKeys() {
1391  // This method is called when the DifferenceEngine is used for a slot diff. We only care
1392  // about special things, not the revision IDs, which are added to the cache key by the
1393  // page-level DifferenceEngine, and which might not have a valid value for this object.
1394  $this->mOldid = 123456789;
1395  $this->mNewid = 987654321;
1396 
1397  // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1398  $cacheString = $this->getDiffBodyCacheKey();
1399  if ( $cacheString ) {
1400  return [ $cacheString ];
1401  }
1402 
1403  $params = $this->getDiffBodyCacheKeyParams();
1404 
1405  // Try to get rid of the standard keys to keep the cache key human-readable:
1406  // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1407  // the child class includes the same keys, drop them.
1408  // Uses an obscure PHP feature where static calls to non-static methods are allowed
1409  // as long as we are already in a non-static method of the same class, and the call context
1410  // ($this) will be inherited.
1411  // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1412  $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1413  if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1414  $params = array_slice( $params, count( $standardParams ) );
1415  }
1416 
1417  return $params;
1418  }
1419 
1423  public function setSlotDiffOptions( $options ) {
1424  $this->slotDiffOptions = $options;
1425  }
1426 
1440  public function generateContentDiffBody( Content $old, Content $new ) {
1441  $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1442  if (
1443  $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1444  && $this->isSlotDiffRenderer
1445  ) {
1446  // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1447  // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1448  // This will happen when a content model has no custom slot diff renderer, it does have
1449  // a custom difference engine, but that does not override this method.
1450  throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1451  . 'Please use a SlotDiffRenderer.' );
1452  }
1453  return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1454  }
1455 
1468  public function generateTextDiffBody( $otext, $ntext ) {
1469  $slotDiffRenderer = $this->contentHandlerFactory
1470  ->getContentHandler( CONTENT_MODEL_TEXT )
1471  ->getSlotDiffRenderer( $this->getContext() );
1472  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1473  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1474  // This is too unlikely to happen to bother handling properly.
1475  throw new Exception( 'The slot diff renderer for text content should be a '
1476  . 'TextSlotDiffRenderer subclass' );
1477  }
1478  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1479  }
1480 
1487  public static function getEngine() {
1488  $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1489  ->get( 'DiffEngine' );
1490  $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1491  ->get( 'ExternalDiffEngine' );
1492 
1493  if ( $diffEngine === null ) {
1494  $engines = [ 'external', 'wikidiff2', 'php' ];
1495  } else {
1496  $engines = [ $diffEngine ];
1497  }
1498 
1499  $failureReason = null;
1500  foreach ( $engines as $engine ) {
1501  switch ( $engine ) {
1502  case 'external':
1503  if ( is_string( $externalDiffEngine ) ) {
1504  if ( is_executable( $externalDiffEngine ) ) {
1505  return $externalDiffEngine;
1506  }
1507  $failureReason = 'ExternalDiffEngine config points to a non-executable';
1508  if ( $diffEngine === null ) {
1509  wfDebug( "$failureReason, ignoring" );
1510  }
1511  } else {
1512  $failureReason = 'ExternalDiffEngine config is set to a non-string value';
1513  if ( $diffEngine === null && $externalDiffEngine ) {
1514  wfWarn( "$failureReason, ignoring" );
1515  }
1516  }
1517  break;
1518 
1519  case 'wikidiff2':
1520  if ( function_exists( 'wikidiff2_do_diff' ) ) {
1521  return 'wikidiff2';
1522  }
1523  $failureReason = 'wikidiff2 is not available';
1524  break;
1525 
1526  case 'php':
1527  // Always available.
1528  return 'php';
1529 
1530  default:
1531  throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
1532  }
1533  }
1534  throw new UnexpectedValueException( "Cannot use diff engine '$engine': $failureReason" );
1535  }
1536 
1549  protected function textDiff( $otext, $ntext ) {
1550  $slotDiffRenderer = $this->contentHandlerFactory
1551  ->getContentHandler( CONTENT_MODEL_TEXT )
1552  ->getSlotDiffRenderer( $this->getContext() );
1553  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1554  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1555  // This is too unlikely to happen to bother handling properly.
1556  throw new Exception( 'The slot diff renderer for text content should be a '
1557  . 'TextSlotDiffRenderer subclass' );
1558  }
1559  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1560  }
1561 
1570  protected function debug( $generator = "internal" ) {
1571  if ( !$this->enableDebugComment ) {
1572  return '';
1573  }
1574  $data = [ $generator ];
1575  if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1576  $data[] = wfHostname();
1577  }
1578  $data[] = wfTimestamp( TS_DB );
1579 
1580  return "<!-- diff generator: " .
1581  implode( " ", array_map( "htmlspecialchars", $data ) ) .
1582  " -->\n";
1583  }
1584 
1588  private function getDebugString() {
1589  $engine = self::getEngine();
1590  if ( $engine === 'wikidiff2' ) {
1591  return $this->debug( 'wikidiff2' );
1592  } elseif ( $engine === 'php' ) {
1593  return $this->debug( 'native PHP' );
1594  } else {
1595  return $this->debug( "external $engine" );
1596  }
1597  }
1598 
1605  private function localiseDiff( $text ) {
1606  $text = $this->localiseLineNumbers( $text );
1607  if ( $this->getEngine() === 'wikidiff2' &&
1608  version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1609  ) {
1610  $text = $this->addLocalisedTitleTooltips( $text );
1611  }
1612  return $text;
1613  }
1614 
1622  public function localiseLineNumbers( $text ) {
1623  return preg_replace_callback(
1624  '/<!--LINE (\d+)-->/',
1625  [ $this, 'localiseLineNumbersCb' ],
1626  $text
1627  );
1628  }
1629 
1634  public function localiseLineNumbersCb( $matches ) {
1635  if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1636  return '';
1637  }
1638 
1639  return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1640  }
1641 
1648  private function addLocalisedTitleTooltips( $text ) {
1649  return preg_replace_callback(
1650  '/class="mw-diff-movedpara-(left|right)"/',
1651  [ $this, 'addLocalisedTitleTooltipsCb' ],
1652  $text
1653  );
1654  }
1655 
1660  private function addLocalisedTitleTooltipsCb( array $matches ) {
1661  $key = $matches[1] === 'right' ?
1662  'diff-paragraph-moved-toold' :
1663  'diff-paragraph-moved-tonew';
1664  return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1665  }
1666 
1672  public function getMultiNotice() {
1673  // The notice only make sense if we are diffing two saved revisions of the same page.
1674  if (
1675  !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1676  || !$this->mOldPage || !$this->mNewPage
1677  || !$this->mOldPage->equals( $this->mNewPage )
1678  || $this->mOldRevisionRecord->getId() === null
1679  || $this->mNewRevisionRecord->getId() === null
1680  // (T237709) Deleted revs might have different page IDs
1681  || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1682  || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1683  ) {
1684  return '';
1685  }
1686 
1687  if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1688  $oldRevRecord = $this->mNewRevisionRecord; // flip
1689  $newRevRecord = $this->mOldRevisionRecord; // flip
1690  } else { // normal case
1691  $oldRevRecord = $this->mOldRevisionRecord;
1692  $newRevRecord = $this->mNewRevisionRecord;
1693  }
1694 
1695  // Sanity: don't show the notice if too many rows must be scanned
1696  // @todo show some special message for that case
1697  $nEdits = $this->revisionStore->countRevisionsBetween(
1698  $this->mNewPage->getArticleID(),
1699  $oldRevRecord,
1700  $newRevRecord,
1701  1000
1702  );
1703  if ( $nEdits > 0 && $nEdits <= 1000 ) {
1704  $limit = 100; // use diff-multi-manyusers if too many users
1705  try {
1706  $users = $this->revisionStore->getAuthorsBetween(
1707  $this->mNewPage->getArticleID(),
1708  $oldRevRecord,
1709  $newRevRecord,
1710  null,
1711  $limit
1712  );
1713  $numUsers = count( $users );
1714 
1715  $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1716  $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1717  if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1718  $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1719  }
1720  } catch ( InvalidArgumentException $e ) {
1721  $numUsers = 0;
1722  }
1723 
1724  return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1725  }
1726 
1727  return '';
1728  }
1729 
1739  public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1740  if ( $numUsers === 0 ) {
1741  $msg = 'diff-multi-sameuser';
1742  } elseif ( $numUsers > $limit ) {
1743  $msg = 'diff-multi-manyusers';
1744  $numUsers = $limit;
1745  } else {
1746  $msg = 'diff-multi-otherusers';
1747  }
1748 
1749  return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1750  }
1751 
1756  private function userCanEdit( RevisionRecord $revRecord ) {
1757  $user = $this->getUser();
1758 
1759  if ( !RevisionRecord::userCanBitfield(
1760  $revRecord->getVisibility(),
1761  RevisionRecord::DELETED_TEXT,
1762  $user
1763  ) ) {
1764  return false;
1765  }
1766 
1767  return true;
1768  }
1769 
1779  public function getRevisionHeader( $rev, $complete = '' ) {
1780  if ( $rev instanceof Revision ) {
1781  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
1782  $rev = $rev->getRevisionRecord();
1783  }
1784 
1785  $lang = $this->getLanguage();
1786  $user = $this->getUser();
1787  $revtimestamp = $rev->getTimestamp();
1788  $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1789  $dateofrev = $lang->userDate( $revtimestamp, $user );
1790  $timeofrev = $lang->userTime( $revtimestamp, $user );
1791 
1792  $header = $this->msg(
1793  $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1794  $timestamp,
1795  $dateofrev,
1796  $timeofrev
1797  );
1798 
1799  if ( $complete !== 'complete' ) {
1800  return $header->escaped();
1801  }
1802 
1803  $title = $rev->getPageAsLinkTarget();
1804 
1805  $header = $this->linkRenderer->makeKnownLink( $title, $header->text(), [],
1806  [ 'oldid' => $rev->getId() ] );
1807 
1808  if ( $this->userCanEdit( $rev ) ) {
1809  $editQuery = [ 'action' => 'edit' ];
1810  if ( !$rev->isCurrent() ) {
1811  $editQuery['oldid'] = $rev->getId();
1812  }
1813 
1814  $key = MediaWikiServices::getInstance()->getPermissionManager()
1815  ->quickUserCan( 'edit', $user, $title ) ? 'editold' : 'viewsourceold';
1816  $msg = $this->msg( $key )->text();
1817  $editLink = $this->msg( 'parentheses' )->rawParams(
1818  $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery ) )->escaped();
1819  $header .= ' ' . Html::rawElement(
1820  'span',
1821  [ 'class' => 'mw-diff-edit' ],
1822  $editLink
1823  );
1824  if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1826  'span',
1827  [ 'class' => 'history-deleted' ],
1828  $header
1829  );
1830  }
1831  } else {
1832  $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1833  }
1834 
1835  return $header;
1836  }
1837 
1850  public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1851  // shared.css sets diff in interface language/dir, but the actual content
1852  // is often in a different language, mostly the page content language/dir
1853  $header = Html::openElement( 'table', [
1854  'class' => [
1855  'diff',
1856  'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1857  'diff-editfont-' . $this->getUser()->getOption( 'editfont' )
1858  ],
1859  'data-mw' => 'interface',
1860  ] );
1861  $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1862 
1863  if ( !$diff && !$otitle ) {
1864  $header .= "
1865  <tr class=\"diff-title\" lang=\"{$userLang}\">
1866  <td class=\"diff-ntitle\">{$ntitle}</td>
1867  </tr>";
1868  $multiColspan = 1;
1869  } else {
1870  if ( $diff ) { // Safari/Chrome show broken output if cols not used
1871  $header .= "
1872  <col class=\"diff-marker\" />
1873  <col class=\"diff-content\" />
1874  <col class=\"diff-marker\" />
1875  <col class=\"diff-content\" />";
1876  $colspan = 2;
1877  $multiColspan = 4;
1878  } else {
1879  $colspan = 1;
1880  $multiColspan = 2;
1881  }
1882  if ( $otitle || $ntitle ) {
1883  $header .= "
1884  <tr class=\"diff-title\" lang=\"{$userLang}\">
1885  <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1886  <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1887  </tr>";
1888  }
1889  }
1890 
1891  if ( $multi != '' ) {
1892  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1893  "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1894  }
1895  if ( $notice != '' ) {
1896  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1897  "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1898  }
1899 
1900  return $header . $diff . "</table>";
1901  }
1902 
1910  public function setContent( Content $oldContent, Content $newContent ) {
1911  $this->mOldContent = $oldContent;
1912  $this->mNewContent = $newContent;
1913 
1914  $this->mTextLoaded = 2;
1915  $this->mRevisionsLoaded = true;
1916  $this->isContentOverridden = true;
1917  $this->slotDiffRenderers = null;
1918  }
1919 
1925  public function setRevisions(
1926  ?RevisionRecord $oldRevision, RevisionRecord $newRevision
1927  ) {
1928  if ( $oldRevision ) {
1929  $this->mOldRevisionRecord = $oldRevision;
1930  $this->mOldid = $oldRevision->getId();
1931  $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1932  // This method is meant for edit diffs and such so there is no reason to provide a
1933  // revision that's not readable to the user, but check it just in case.
1934  $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1935  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1936  } else {
1937  $this->mOldPage = null;
1938  $this->mOldRevisionRecord = $this->mOldid = false;
1939  }
1940  $this->mNewRevisionRecord = $newRevision;
1941  $this->mNewid = $newRevision->getId();
1942  $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1943  $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1944  RevisionRecord::FOR_THIS_USER, $this->getUser() );
1945 
1946  $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1947  $this->mTextLoaded = $oldRevision ? 2 : 1;
1948  $this->isContentOverridden = false;
1949  $this->slotDiffRenderers = null;
1950  }
1951 
1958  public function setTextLanguage( Language $lang ) {
1959  $this->mDiffLang = $lang;
1960  }
1961 
1974  public function mapDiffPrevNext( $old, $new ) {
1975  if ( $new === 'prev' ) {
1976  // Show diff between revision $old and the previous one. Get previous one from DB.
1977  $newid = intval( $old );
1978  $oldid = false;
1979  $newRev = $this->revisionStore->getRevisionById( $newid );
1980  if ( $newRev ) {
1981  $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
1982  if ( $oldRev ) {
1983  $oldid = $oldRev->getId();
1984  }
1985  }
1986  } elseif ( $new === 'next' ) {
1987  // Show diff between revision $old and the next one. Get next one from DB.
1988  $oldid = intval( $old );
1989  $newid = false;
1990  $oldRev = $this->revisionStore->getRevisionById( $oldid );
1991  if ( $oldRev ) {
1992  $newRev = $this->revisionStore->getNextRevision( $oldRev );
1993  if ( $newRev ) {
1994  $newid = $newRev->getId();
1995  }
1996  }
1997  } else {
1998  $oldid = intval( $old );
1999  $newid = intval( $new );
2000  }
2001 
2002  return [ $oldid, $newid ];
2003  }
2004 
2005  private function loadRevisionIds() {
2006  if ( $this->mRevisionsIdsLoaded ) {
2007  return;
2008  }
2009 
2010  $this->mRevisionsIdsLoaded = true;
2011 
2012  $old = $this->mOldid;
2013  $new = $this->mNewid;
2014 
2015  list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
2016  if ( $new === 'next' && $this->mNewid === false ) {
2017  # if no result, NewId points to the newest old revision. The only newer
2018  # revision is cur, which is "0".
2019  $this->mNewid = 0;
2020  }
2021 
2022  $this->hookRunner->onNewDifferenceEngine(
2023  $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2024  }
2025 
2039  public function loadRevisionData() {
2040  if ( $this->mRevisionsLoaded ) {
2041  return $this->isContentOverridden ||
2042  ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
2043  }
2044 
2045  // Whether it succeeds or fails, we don't want to try again
2046  $this->mRevisionsLoaded = true;
2047 
2048  $this->loadRevisionIds();
2049 
2050  // Load the new revision object
2051  if ( $this->mNewid ) {
2052  $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2053  } else {
2054  $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2055  }
2056 
2057  if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2058  return false;
2059  }
2060 
2061  // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2062  $this->mNewid = $this->mNewRevisionRecord->getId();
2063  if ( $this->mNewid ) {
2064  $this->mNewPage = Title::newFromLinkTarget(
2065  $this->mNewRevisionRecord->getPageAsLinkTarget()
2066  );
2067  } else {
2068  $this->mNewPage = null;
2069  }
2070 
2071  // Load the old revision object
2072  $this->mOldRevisionRecord = false;
2073  if ( $this->mOldid ) {
2074  $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2075  } elseif ( $this->mOldid === 0 ) {
2076  $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2077  if ( $revRecord ) {
2078  $this->mOldid = $revRecord->getId();
2079  $this->mOldRevisionRecord = $revRecord;
2080  } else {
2081  // No previous revision; mark to show as first-version only.
2082  $this->mOldid = false;
2083  $this->mOldRevisionRecord = false;
2084  }
2085  } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2086 
2087  if ( $this->mOldRevisionRecord === null ) {
2088  return false;
2089  }
2090 
2091  if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2092  $this->mOldPage = Title::newFromLinkTarget(
2093  $this->mOldRevisionRecord->getPageAsLinkTarget()
2094  );
2095  } else {
2096  $this->mOldPage = null;
2097  }
2098 
2099  // Load tags information for both revisions
2100  $dbr = wfGetDB( DB_REPLICA );
2101  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2102  if ( $this->mOldid !== false ) {
2103  $tagIds = $dbr->selectFieldValues(
2104  'change_tag',
2105  'ct_tag_id',
2106  [ 'ct_rev_id' => $this->mOldid ],
2107  __METHOD__
2108  );
2109  $tags = [];
2110  foreach ( $tagIds as $tagId ) {
2111  try {
2112  $tags[] = $changeTagDefStore->getName( (int)$tagId );
2113  } catch ( NameTableAccessException $exception ) {
2114  continue;
2115  }
2116  }
2117  $this->mOldTags = implode( ',', $tags );
2118  } else {
2119  $this->mOldTags = false;
2120  }
2121 
2122  $tagIds = $dbr->selectFieldValues(
2123  'change_tag',
2124  'ct_tag_id',
2125  [ 'ct_rev_id' => $this->mNewid ],
2126  __METHOD__
2127  );
2128  $tags = [];
2129  foreach ( $tagIds as $tagId ) {
2130  try {
2131  $tags[] = $changeTagDefStore->getName( (int)$tagId );
2132  } catch ( NameTableAccessException $exception ) {
2133  continue;
2134  }
2135  }
2136  $this->mNewTags = implode( ',', $tags );
2137 
2138  return true;
2139  }
2140 
2149  public function loadText() {
2150  if ( $this->mTextLoaded == 2 ) {
2151  return $this->loadRevisionData() &&
2152  ( $this->mOldRevisionRecord === false || $this->mOldContent )
2153  && $this->mNewContent;
2154  }
2155 
2156  // Whether it succeeds or fails, we don't want to try again
2157  $this->mTextLoaded = 2;
2158 
2159  if ( !$this->loadRevisionData() ) {
2160  return false;
2161  }
2162 
2163  if ( $this->mOldRevisionRecord ) {
2164  $this->mOldContent = $this->mOldRevisionRecord->getContent(
2165  SlotRecord::MAIN,
2166  RevisionRecord::FOR_THIS_USER,
2167  $this->getUser()
2168  );
2169  if ( $this->mOldContent === null ) {
2170  return false;
2171  }
2172  }
2173 
2174  $this->mNewContent = $this->mNewRevisionRecord->getContent(
2175  SlotRecord::MAIN,
2176  RevisionRecord::FOR_THIS_USER,
2177  $this->getUser()
2178  );
2179  $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2180  if ( $this->mNewContent === null ) {
2181  return false;
2182  }
2183 
2184  return true;
2185  }
2186 
2192  public function loadNewText() {
2193  if ( $this->mTextLoaded >= 1 ) {
2194  return $this->loadRevisionData();
2195  }
2196 
2197  $this->mTextLoaded = 1;
2198 
2199  if ( !$this->loadRevisionData() ) {
2200  return false;
2201  }
2202 
2203  $this->mNewContent = $this->mNewRevisionRecord->getContent(
2204  SlotRecord::MAIN,
2205  RevisionRecord::FOR_THIS_USER,
2206  $this->getUser()
2207  );
2208 
2209  $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2210 
2211  return true;
2212  }
2213 
2214 }
Content\getContentHandler
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
DifferenceEngine\$wikiPageFactory
WikiPageFactory $wikiPageFactory
Definition: DifferenceEngine.php:233
DifferenceEngine\$mRevisionsIdsLoaded
bool $mRevisionsIdsLoaded
Have the revisions IDs been loaded.
Definition: DifferenceEngine.php:151
DifferenceEngine\$mNewRevisionRecord
RevisionRecord null $mNewRevisionRecord
New revision (right pane).
Definition: DifferenceEngine.php:105
ContextSource\$context
IContextSource $context
Definition: ContextSource.php:37
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:70
DifferenceEngine\getSlotContents
getSlotContents()
Get the old and new content objects for all slots.
Definition: DifferenceEngine.php:322
wfMergeErrorArrays
wfMergeErrorArrays(... $args)
Merge arrays in the style of PermissionManager::getPermissionErrors, with duplicate removal e....
Definition: GlobalFunctions.php:183
DifferenceEngine\markPatrolledLink
markPatrolledLink()
Build a link to mark a change as patrolled.
Definition: DifferenceEngine.php:911
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:45
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:1487
DifferenceEngine\$mTextLoaded
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
Definition: DifferenceEngine.php:157
DifferenceEngine\addLocalisedTitleTooltipsCb
addLocalisedTitleTooltipsCb(array $matches)
Definition: DifferenceEngine.php:1660
DifferenceEngine\getDiffBodyCacheKeyParams
getDiffBodyCacheKeyParams()
Get the cache key parameters.
Definition: DifferenceEngine.php:1356
DifferenceEngine\$unhide
bool $unhide
Show rev_deleted content if allowed.
Definition: DifferenceEngine.php:189
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $noCache=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1197
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:166
$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:221
DifferenceEngine\setReducedLineNumbers
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
Definition: DifferenceEngine.php:372
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:80
DifferenceEngine\shouldBeHiddenFromUser
shouldBeHiddenFromUser( $user)
Checks whether the diff should be hidden from the current user This is based on whether the user is a...
Definition: DifferenceEngine.php:606
DifferenceEngine\setContent
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
Definition: DifferenceEngine.php:1910
MediaWiki\Linker\LinkRenderer
Class that generates HTML links for pages.
Definition: LinkRenderer.php:41
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1831
DifferenceEngine\getNewid
getNewid()
Get the ID of new revision (right pane) of the diff.
Definition: DifferenceEngine.php:416
DifferenceEngine\getOldRevision
getOldRevision()
Get the left side of the diff.
Definition: DifferenceEngine.php:428
DifferenceEngine\getOldid
getOldid()
Get the ID of old revision (left pane) of the diff.
Definition: DifferenceEngine.php:404
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:55
Linker\revComment
static revComment( $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:1604
Linker\getRevDeleteLink
static getRevDeleteLink(User $user, $rev, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition: Linker.php:2201
DifferenceEngine\$mRevisionsLoaded
bool $mRevisionsLoaded
Have the revisions been loaded.
Definition: DifferenceEngine.php:154
DifferenceEngine\deletedIdMarker
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
Definition: DifferenceEngine.php:484
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2072
wfHostname
wfHostname()
Get host name of the current machine, for use in error reporting.
Definition: GlobalFunctions.php:1293
DifferenceEngine\$enableDebugComment
bool $enableDebugComment
Set this to true to add debug info to the HTML output.
Definition: DifferenceEngine.php:178
DifferenceEngine\$isSlotDiffRenderer
bool $isSlotDiffRenderer
Temporary hack for B/C while slot diff related methods of DifferenceEngine are being deprecated.
Definition: DifferenceEngine.php:203
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1230
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:106
DifferenceEngine\addLocalisedTitleTooltips
addLocalisedTitleTooltips( $text)
Add title attributes for tooltips on moved paragraph indicators.
Definition: DifferenceEngine.php:1648
DifferenceEngine\$mOldRevisionRecord
RevisionRecord null false $mOldRevisionRecord
Old revision (left pane).
Definition: DifferenceEngine.php:95
ContextSource\getRequest
getRequest()
Definition: ContextSource.php:79
PermissionsError
Show an error when a user tries to do something they do not have the necessary permissions for.
Definition: PermissionsError.php:31
DifferenceEngine\showDiffStyle
showDiffStyle()
Add style sheets for diff display.
Definition: DifferenceEngine.php:1146
DifferenceEngine\loadNewText
loadNewText()
Load the text of the new revision, not the old one.
Definition: DifferenceEngine.php:2192
Html\warningBox
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:729
ContextSource\getUser
getUser()
Stable to override.
Definition: ContextSource.php:134
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:1126
DifferenceEngine\getDiffBodyCacheKey
getDiffBodyCacheKey()
Returns the cache key for diff body text or content.
Definition: DifferenceEngine.php:1339
DifferenceEngine\localiseDiff
localiseDiff( $text)
Localise diff output.
Definition: DifferenceEngine.php:1605
DifferenceEngine\$mNewid
int string false null $mNewid
Revision ID for the new revision.
Definition: DifferenceEngine.php:83
DifferenceEngine\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: DifferenceEngine.php:219
DifferenceEngine\hasDeletedRevision
hasDeletedRevision()
Checks whether one of the given Revisions was deleted.
Definition: DifferenceEngine.php:521
DifferenceEngine\generateTextDiffBody
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
Definition: DifferenceEngine.php:1468
$dbr
$dbr
Definition: testCompression.php:54
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:143
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:52
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:183
DifferenceEngine\revisionDeleteLink
revisionDeleteLink(RevisionRecord $revRecord)
Definition: DifferenceEngine.php:1003
DifferenceEngine\loadRevisionData
loadRevisionData()
Load revision metadata for the specified revisions.
Definition: DifferenceEngine.php:2039
DifferenceEngine\localiseLineNumbersCb
localiseLineNumbersCb( $matches)
Definition: DifferenceEngine.php:1634
MWException
MediaWiki exception.
Definition: MWException.php:29
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1033
DifferenceEngine\getDiffBodyForRole
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
Definition: DifferenceEngine.php:1293
DifferenceEngine\$slotDiffRenderers
SlotDiffRenderer[] $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
Definition: DifferenceEngine.php:195
Linker\generateRollback
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1860
DifferenceEngine\wasCacheHit
wasCacheHit()
Definition: DifferenceEngine.php:393
DifferenceEngine\$mNewTags
string[] null $mNewTags
Change tags of new revision or null if it does not exist / is not saved.
Definition: DifferenceEngine.php:131
DifferenceEngine\getDiffLang
getDiffLang()
Get the language of the difference engine, defaults to page content language.
Definition: DifferenceEngine.php:381
DifferenceEngine\getRevisionHeader
getRevisionHeader( $rev, $complete='')
Get a header for a specified revision.
Definition: DifferenceEngine.php:1779
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2466
ContextSource\getOutput
getOutput()
Definition: ContextSource.php:124
$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:1322
ContextSource
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Definition: ContextSource.php:31
ContextSource\getWikiPage
getWikiPage()
Get the WikiPage object.
Definition: ContextSource.php:115
DifferenceEngine\getParserOutput
getParserOutput(WikiPage $page, RevisionRecord $revRecord)
Definition: DifferenceEngine.php:1102
DifferenceEngine\$mNewContent
Content null $mNewContent
Definition: DifferenceEngine.php:145
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:449
Page\WikiPageFactory
Definition: WikiPageFactory.php:19
ChangesList\flag
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
Definition: ChangesList.php:269
DifferenceEngine\getDiffBody
getDiffBody()
Get the diff table body, without header.
Definition: DifferenceEngine.php:1189
DifferenceEngine\$slotDiffOptions
array $slotDiffOptions
A set of options that will be passed to the SlotDiffRenderer upon creation.
Definition: DifferenceEngine.php:209
DifferenceEngine\getSlotDiffRenderers
getSlotDiffRenderers()
Definition: DifferenceEngine.php:282
$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:624
DifferenceEngine\setRevisions
setRevisions(?RevisionRecord $oldRevision, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
Definition: DifferenceEngine.php:1925
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:2149
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:1141
$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:914
ContextSource\setContext
setContext(IContextSource $context)
Definition: ContextSource.php:61
DifferenceEngine\$mCacheHit
bool $mCacheHit
Was the diff fetched from cache?
Definition: DifferenceEngine.php:170
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:313
Revision\RevisionRecord\getId
getId()
Get revision ID.
Definition: RevisionRecord.php:271
DifferenceEngine\$hookContainer
HookContainer $hookContainer
Definition: DifferenceEngine.php:230
DifferenceEngine\__construct
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
#-
Definition: DifferenceEngine.php:245
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:187
DifferenceEngine\getDebugString
getDebugString()
Definition: DifferenceEngine.php:1588
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:650
DifferenceEngine\setTextLanguage
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
Definition: DifferenceEngine.php:1958
$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:125
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
$header
$header
Definition: updateCredits.php:37
DifferenceEngine\$linkRenderer
LinkRenderer $linkRenderer
Definition: DifferenceEngine.php:214
DifferenceEngine\$hookRunner
HookRunner $hookRunner
Definition: DifferenceEngine.php:227
DifferenceEngine\getTitle
getTitle()
1.18 Stable to override Title|null
Definition: DifferenceEngine.php:361
DifferenceEngine\showDiffPage
showDiffPage( $diffOnly=false)
Definition: DifferenceEngine.php:614
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:289
DifferenceEngine\$mOldid
int false null $mOldid
Revision ID for the old revision.
Definition: DifferenceEngine.php:75
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:2116
DifferenceEngine\$isContentOverridden
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e....
Definition: DifferenceEngine.php:167
DifferenceEngine\DIFF_VERSION
const DIFF_VERSION
Constant to indicate diff cache compatibility.
Definition: DifferenceEngine.php:67
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:55
DifferenceEngine\intermediateEditsMsg
static intermediateEditsMsg( $numEdits, $numUsers, $limit)
Get a notice about how many intermediate edits and users there are.
Definition: DifferenceEngine.php:1739
DifferenceEngine\showMissingRevision
showMissingRevision()
Definition: DifferenceEngine.php:493
Content
Base interface for content objects.
Definition: Content.php:35
DifferenceEngine\loadRevisionIds
loadRevisionIds()
Definition: DifferenceEngine.php:2005
DifferenceEngine\getNewRevision
getNewRevision()
Get the right side of the diff.
Definition: DifferenceEngine.php:437
Revision\RevisionRecord\getVisibility
getVisibility()
Get the deletion bitfield of the revision.
Definition: RevisionRecord.php:423
Revision\RevisionRecord\getPageAsLinkTarget
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
Definition: RevisionRecord.php:343
Title
Represents a title within MediaWiki.
Definition: Title.php:46
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
DifferenceEngine\$mDiffLang
Language $mDiffLang
Definition: DifferenceEngine.php:148
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:943
CONTENT_MODEL_TEXT
const CONTENT_MODEL_TEXT
Definition: Defines.php:221
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:112
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:83
DifferenceEngine\getPermissionErrors
getPermissionErrors(User $user)
Get the permission errors associated with the revisions for the current diff.
Definition: DifferenceEngine.php:539
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:254
DifferenceEngine\getExtraCacheKeys
getExtraCacheKeys()
Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys().
Definition: DifferenceEngine.php:1390
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:212
DifferenceEngine\isUserAllowedToSeeRevisions
isUserAllowedToSeeRevisions( $user)
Checks whether the current user has permission for accessing the revisions of the diff.
Definition: DifferenceEngine.php:578
getTitle
getTitle()
Definition: RevisionSearchResultTrait.php:81
DifferenceEngine
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
Definition: DifferenceEngine.php:57
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:1163
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:159
DifferenceEngine\$mMarkPatrolledLink
string $mMarkPatrolledLink
Link to action=markpatrolled.
Definition: DifferenceEngine.php:186
DifferenceEngine\localiseLineNumbers
localiseLineNumbers( $text)
Replace line numbers with the text in the user's language.
Definition: DifferenceEngine.php:1622
DifferenceEngine\hasSuppressedRevision
hasSuppressedRevision()
Checks whether one of the given Revisions was suppressed.
Definition: DifferenceEngine.php:558
DifferenceEngine\$mOldContent
Content null $mOldContent
Definition: DifferenceEngine.php:138
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
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:1080
DifferenceEngine\userCanEdit
userCanEdit(RevisionRecord $revRecord)
Definition: DifferenceEngine.php:1756
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:571
DifferenceEngine\debug
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend.
Definition: DifferenceEngine.php:1570
DifferenceEngine\renderNewRevision
renderNewRevision()
Show the new revision of the page.
Definition: DifferenceEngine.php:1021
Html\element
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:234
DifferenceEngine\getDiff
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
Definition: DifferenceEngine.php:1167
DifferenceEngine\setSlotDiffOptions
setSlotDiffOptions( $options)
Definition: DifferenceEngine.php:1423
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:1440
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:56
DifferenceEngine\mapDiffPrevNext
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
Definition: DifferenceEngine.php:1974
DifferenceEngine\addHeader
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
Definition: DifferenceEngine.php:1850
ChangeTags\formatSummaryRow
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:174
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:1549
DifferenceEngine\$revisionStore
RevisionStore $revisionStore
Definition: DifferenceEngine.php:224
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:119
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:192
DifferenceEngine\getMultiNotice
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
Definition: DifferenceEngine.php:1672