MediaWiki  master
DifferenceEngine.php
Go to the documentation of this file.
1 <?php
36 
60 
62 
69  private const DIFF_VERSION = '1.12';
70 
77  protected $mOldid;
78 
85  protected $mNewid;
86 
98 
108 
114  protected $mOldPage;
115 
121  protected $mNewPage;
122 
127  private $mOldTags;
128 
133  private $mNewTags;
134 
140  private $mOldContent;
141 
147  private $mNewContent;
148 
150  protected $mDiffLang;
151 
153  private $mRevisionsIdsLoaded = false;
154 
156  protected $mRevisionsLoaded = false;
157 
159  protected $mTextLoaded = 0;
160 
169  protected $isContentOverridden = false;
170 
172  protected $mCacheHit = false;
173 
180  public $enableDebugComment = false;
181 
185  protected $mReducedLineNumbers = false;
186 
188  protected $mMarkPatrolledLink = null;
189 
191  protected $unhide = false;
192 
194  protected $mRefreshCache = false;
195 
197  protected $slotDiffRenderers = null;
198 
205  protected $isSlotDiffRenderer = false;
206 
211  private $slotDiffOptions = [];
212 
216  protected $linkRenderer;
217 
222 
226  private $revisionStore;
227 
229  private $hookRunner;
230 
232  private $hookContainer;
233 
236 
247  public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
248  $refreshCache = false, $unhide = false
249  ) {
250  $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
251  $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
252  $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
253  $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
254  $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
255  $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
256  $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
257  $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
258  $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
259 
260  if ( $context instanceof IContextSource ) {
261  $this->setContext( $context );
262  }
263 
264  wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
265 
266  $this->mOldid = $old;
267  $this->mNewid = $new;
268  $this->mRefreshCache = $refreshCache;
269  $this->unhide = $unhide;
270 
271  $services = MediaWikiServices::getInstance();
272  $this->linkRenderer = $services->getLinkRenderer();
273  $this->contentHandlerFactory = $services->getContentHandlerFactory();
274  $this->revisionStore = $services->getRevisionStore();
275  $this->hookContainer = $services->getHookContainer();
276  $this->hookRunner = new HookRunner( $this->hookContainer );
277  $this->wikiPageFactory = $services->getWikiPageFactory();
278  }
279 
284  protected function getSlotDiffRenderers() {
285  if ( $this->isSlotDiffRenderer ) {
286  throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
287  }
288 
289  if ( $this->slotDiffRenderers === null ) {
290  if ( !$this->loadRevisionData() ) {
291  return [];
292  }
293 
294  $slotContents = $this->getSlotContents();
295  $this->slotDiffRenderers = array_map( function ( $contents ) {
297  $content = $contents['new'] ?: $contents['old'];
298  $context = $this->getContext();
299 
300  return $content->getContentHandler()->getSlotDiffRenderer(
301  $context,
302  $this->slotDiffOptions
303  );
304  }, $slotContents );
305  }
307  }
308 
315  public function markAsSlotDiffRenderer() {
316  $this->isSlotDiffRenderer = true;
317  }
318 
324  protected function getSlotContents() {
325  if ( $this->isContentOverridden ) {
326  return [
327  SlotRecord::MAIN => [
328  'old' => $this->mOldContent,
329  'new' => $this->mNewContent,
330  ]
331  ];
332  } elseif ( !$this->loadRevisionData() ) {
333  return [];
334  }
335 
336  $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
337  if ( $this->mOldRevisionRecord ) {
338  $oldSlots = $this->mOldRevisionRecord->getPrimarySlots()->getSlots();
339  } else {
340  $oldSlots = [];
341  }
342  // The order here will determine the visual order of the diff. The current logic is
343  // slots of the new revision first in natural order, then deleted ones. This is ad hoc
344  // and should not be relied on - in the future we may want the ordering to depend
345  // on the page type.
346  $roles = array_merge( array_keys( $newSlots ), array_keys( $oldSlots ) );
347 
348  $slots = [];
349  foreach ( $roles as $role ) {
350  $slots[$role] = [
351  'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
352  'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
353  ];
354  }
355  // move main slot to front
356  if ( isset( $slots[SlotRecord::MAIN] ) ) {
357  $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
358  }
359  return $slots;
360  }
361 
363  public function getTitle() {
364  // T202454 avoid errors when there is no title
365  return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
366  }
367 
374  public function setReducedLineNumbers( $value = true ) {
375  $this->mReducedLineNumbers = $value;
376  }
377 
383  public function getDiffLang() {
384  if ( $this->mDiffLang === null ) {
385  # Default language in which the diff text is written.
386  $this->mDiffLang = $this->getTitle()->getPageLanguage();
387  }
388 
389  return $this->mDiffLang;
390  }
391 
395  public function wasCacheHit() {
396  return $this->mCacheHit;
397  }
398 
406  public function getOldid() {
407  $this->loadRevisionIds();
408 
409  return $this->mOldid;
410  }
411 
418  public function getNewid() {
419  $this->loadRevisionIds();
420 
421  return $this->mNewid;
422  }
423 
430  public function getOldRevision() {
431  return $this->mOldRevisionRecord ?: null;
432  }
433 
439  public function getNewRevision() {
441  }
442 
451  public function deletedLink( $id ) {
452  if ( $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
453  $dbr = wfGetDB( DB_REPLICA );
455  $arQuery = $revStore->getArchiveQueryInfo();
456  $row = $dbr->selectRow(
457  $arQuery['tables'],
458  array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
459  [ 'ar_rev_id' => $id ],
460  __METHOD__,
461  [],
462  $arQuery['joins']
463  );
464  if ( $row ) {
465  $revRecord = $revStore->newRevisionFromArchiveRow( $row );
466  $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
467 
468  return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
469  'target' => $title->getPrefixedText(),
470  'timestamp' => $revRecord->getTimestamp()
471  ] );
472  }
473  }
474 
475  return false;
476  }
477 
485  public function deletedIdMarker( $id ) {
486  $link = $this->deletedLink( $id );
487  if ( $link ) {
488  return "[$link $id]";
489  } else {
490  return (string)$id;
491  }
492  }
493 
494  private function showMissingRevision() {
495  $out = $this->getOutput();
496 
497  $missing = [];
498  if ( $this->mOldRevisionRecord === null ||
499  ( $this->mOldRevisionRecord && $this->mOldContent === null )
500  ) {
501  $missing[] = $this->deletedIdMarker( $this->mOldid );
502  }
503  if ( $this->mNewRevisionRecord === null ||
504  ( $this->mNewRevisionRecord && $this->mNewContent === null )
505  ) {
506  $missing[] = $this->deletedIdMarker( $this->mNewid );
507  }
508 
509  $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
510  $msg = $this->msg( 'difference-missing-revision' )
511  ->params( $this->getLanguage()->listToText( $missing ) )
512  ->numParams( count( $missing ) )
513  ->parseAsBlock();
514  $out->addHTML( $msg );
515  }
516 
522  public function hasDeletedRevision() {
523  $this->loadRevisionData();
524  return (
525  $this->mNewRevisionRecord &&
526  $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
527  ) ||
528  (
529  $this->mOldRevisionRecord &&
530  $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
531  );
532  }
533 
540  public function getPermissionErrors( Authority $performer ) {
541  $this->loadRevisionData();
542  $permStatus = PermissionStatus::newEmpty();
543  if ( $this->mNewPage ) {
544  $performer->authorizeRead( 'read', $this->mNewPage, $permStatus );
545  }
546  if ( $this->mOldPage ) {
547  $performer->authorizeRead( 'read', $this->mOldPage, $permStatus );
548  }
549  return $permStatus->toLegacyErrorArray();
550  }
551 
557  public function hasSuppressedRevision() {
558  return $this->hasDeletedRevision() && (
559  ( $this->mOldRevisionRecord &&
560  $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
561  ( $this->mNewRevisionRecord &&
562  $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
563  );
564  }
565 
577  public function isUserAllowedToSeeRevisions( Authority $performer ) {
578  $this->loadRevisionData();
579  // $this->mNewRev will only be falsy if a loading error occurred
580  // (in which case the user is allowed to see).
581  $allowed = !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
582  RevisionRecord::DELETED_TEXT,
583  $performer
584  );
585  if ( $this->mOldRevisionRecord &&
586  !$this->mOldRevisionRecord->userCan(
587  RevisionRecord::DELETED_TEXT,
588  $performer
589  )
590  ) {
591  $allowed = false;
592  }
593  return $allowed;
594  }
595 
603  public function shouldBeHiddenFromUser( Authority $performer ) {
604  return $this->hasDeletedRevision() && ( !$this->unhide ||
605  !$this->isUserAllowedToSeeRevisions( $performer ) );
606  }
607 
611  public function showDiffPage( $diffOnly = false ) {
612  # Allow frames except in certain special cases
613  $out = $this->getOutput();
614  $out->allowClickjacking();
615  $out->setRobotPolicy( 'noindex,nofollow' );
616 
617  // Allow extensions to add any extra output here
618  $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
619 
620  if ( !$this->loadRevisionData() ) {
621  if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
622  $this->showMissingRevision();
623  }
624  return;
625  }
626 
627  $user = $this->getUser();
628  $permErrors = $this->getPermissionErrors( $this->getAuthority() );
629  if ( count( $permErrors ) ) {
630  throw new PermissionsError( 'read', $permErrors );
631  }
632 
633  $rollback = '';
634 
635  $query = $this->slotDiffOptions;
636  # Carry over 'diffonly' param via navigation links
637  if ( $diffOnly != $user->getBoolOption( 'diffonly' ) ) {
638  $query['diffonly'] = $diffOnly;
639  }
640  # Cascade unhide param in links for easy deletion browsing
641  if ( $this->unhide ) {
642  $query['unhide'] = 1;
643  }
644 
645  # Check if one of the revisions is deleted/suppressed
646  $deleted = $this->hasDeletedRevision();
647  $suppressed = $this->hasSuppressedRevision();
648  $allowed = $this->isUserAllowedToSeeRevisions( $this->getAuthority() );
649 
650  $revisionTools = [];
651 
652  # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
653  # a diff between a version V and its previous version V' AND the version V
654  # is the first version of that article. In that case, V' does not exist.
655  if ( $this->mOldRevisionRecord === false ) {
656  if ( $this->mNewPage ) {
657  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
658  }
659  $samePage = true;
660  $oldHeader = '';
661  // Allow extensions to change the $oldHeader variable
662  $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
663  } else {
664  $this->hookRunner->onDifferenceEngineViewHeader( $this );
665 
666  // DiffViewHeader hook is hard deprecated since 1.35
667  if ( $this->hookContainer->isRegistered( 'DiffViewHeader' ) ) {
668  // Only create the Revision object if needed
669  // If old or new are falsey, use null
670  $legacyOldRev = $this->mOldRevisionRecord ?
671  new Revision( $this->mOldRevisionRecord ) :
672  null;
673  $legacyNewRev = $this->mNewRevisionRecord ?
674  new Revision( $this->mNewRevisionRecord ) :
675  null;
676  $this->hookRunner->onDiffViewHeader(
677  $this,
678  $legacyOldRev,
679  $legacyNewRev
680  );
681  }
682 
683  if ( !$this->mOldPage || !$this->mNewPage ) {
684  // XXX say something to the user?
685  $samePage = false;
686  } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
687  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
688  $samePage = true;
689  } else {
690  $out->setPageTitle( $this->msg( 'difference-title-multipage',
691  $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
692  $out->addSubtitle( $this->msg( 'difference-multipage' ) );
693  $samePage = false;
694  }
695 
696  if ( $samePage && $this->mNewPage &&
697  $this->getAuthority()->probablyCan( 'edit', $this->mNewPage )
698  ) {
699  if ( $this->mNewRevisionRecord->isCurrent() &&
700  $this->getAuthority()->probablyCan( 'rollback', $this->mNewPage )
701  ) {
702  $rollbackLink = Linker::generateRollback(
703  $this->mNewRevisionRecord,
704  $this->getContext(),
705  [ 'noBrackets' ]
706  );
707  if ( $rollbackLink ) {
708  $out->preventClickjacking();
709  $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
710  }
711  }
712 
713  if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
714  $this->userCanEdit( $this->mNewRevisionRecord )
715  ) {
716  $undoLink = Html::element( 'a', [
717  'href' => $this->mNewPage->getLocalURL( [
718  'action' => 'edit',
719  'undoafter' => $this->mOldid,
720  'undo' => $this->mNewid
721  ] ),
722  'title' => Linker::titleAttrib( 'undo' ),
723  ],
724  $this->msg( 'editundo' )->text()
725  );
726  $revisionTools['mw-diff-undo'] = $undoLink;
727  }
728  }
729  # Make "previous revision link"
730  $hasPrevious = $samePage && $this->mOldPage &&
731  $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
732  if ( $hasPrevious ) {
733  $prevlink = $this->linkRenderer->makeKnownLink(
734  $this->mOldPage,
735  $this->msg( 'previousdiff' )->text(),
736  [ 'id' => 'differences-prevlink' ],
737  [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
738  );
739  } else {
740  $prevlink = "\u{00A0}";
741  }
742 
743  if ( $this->mOldRevisionRecord->isMinor() ) {
744  $oldminor = ChangesList::flag( 'minor' );
745  } else {
746  $oldminor = '';
747  }
748 
749  $oldRevRecord = $this->mOldRevisionRecord;
750 
751  $ldel = $this->revisionDeleteLink( $oldRevRecord );
752  $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' );
753  $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
754 
755  $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
756  '<div id="mw-diff-otitle2">' .
757  Linker::revUserTools( $oldRevRecord, !$this->unhide ) . '</div>' .
758  '<div id="mw-diff-otitle3">' . $oldminor .
759  Linker::revComment( $oldRevRecord, !$diffOnly, !$this->unhide ) . $ldel . '</div>' .
760  '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
761  '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
762 
763  // Allow extensions to change the $oldHeader variable
764  $this->hookRunner->onDifferenceEngineOldHeader(
765  $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
766  }
767 
768  $out->addJsConfigVars( [
769  'wgDiffOldId' => $this->mOldid,
770  'wgDiffNewId' => $this->mNewid,
771  ] );
772 
773  # Make "next revision link"
774  # Skip next link on the top revision
775  if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
776  $nextlink = $this->linkRenderer->makeKnownLink(
777  $this->mNewPage,
778  $this->msg( 'nextdiff' )->text(),
779  [ 'id' => 'differences-nextlink' ],
780  [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
781  );
782  } else {
783  $nextlink = "\u{00A0}";
784  }
785 
786  if ( $this->mNewRevisionRecord->isMinor() ) {
787  $newminor = ChangesList::flag( 'minor' );
788  } else {
789  $newminor = '';
790  }
791 
792  # Handle RevisionDelete links...
793  $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
794 
795  # Allow extensions to define their own revision tools
796  $this->hookRunner->onDiffTools(
797  $this->mNewRevisionRecord,
798  $revisionTools,
799  $this->mOldRevisionRecord ?: null,
800  $user
801  );
802 
803  # Hook deprecated since 1.35
804  if ( $this->hookContainer->isRegistered( 'DiffRevisionTools' ) ) {
805  # Only create the Revision objects if they are needed
806  $legacyOldRev = $this->mOldRevisionRecord ?
807  new Revision( $this->mOldRevisionRecord ) :
808  null;
809  $legacyNewRev = $this->mNewRevisionRecord ?
810  new Revision( $this->mNewRevisionRecord ) :
811  null;
812  $this->hookRunner->onDiffRevisionTools(
813  $legacyNewRev,
814  $revisionTools,
815  $legacyOldRev,
816  $user
817  );
818  }
819 
820  $formattedRevisionTools = [];
821  // Put each one in parentheses (poor man's button)
822  foreach ( $revisionTools as $key => $tool ) {
823  $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
824  $element = Html::rawElement(
825  'span',
826  [ 'class' => $toolClass ],
827  $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
828  );
829  $formattedRevisionTools[] = $element;
830  }
831 
832  $newRevRecord = $this->mNewRevisionRecord;
833 
834  $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) .
835  ' ' . implode( ' ', $formattedRevisionTools );
836  $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
837 
838  $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
839  '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
840  " $rollback</div>" .
841  '<div id="mw-diff-ntitle3">' . $newminor .
842  Linker::revComment( $newRevRecord, !$diffOnly, !$this->unhide ) . $rdel . '</div>' .
843  '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
844  '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
845 
846  // Allow extensions to change the $newHeader variable
847  $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
848  $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
849  $rdel, $this->unhide );
850 
851  # If the diff cannot be shown due to a deleted revision, then output
852  # the diff header and links to unhide (if available)...
853  if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) {
854  $this->showDiffStyle();
855  $multi = $this->getMultiNotice();
856  $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
857  if ( !$allowed ) {
858  $msg = $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff';
859  # Give explanation for why revision is not visible
860  $out->addHtml(
862  $this->msg( $msg )->parse(),
863  'plainlinks'
864  )
865  );
866  } else {
867  # Give explanation and add a link to view the diff...
868  $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
869  $link = $this->getTitle()->getFullURL( $query );
870  $msg = $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff';
871  $out->addHtml(
873  $this->msg( $msg, $link )->parse(),
874  'plainlinks'
875  )
876  );
877  }
878  # Otherwise, output a regular diff...
879  } else {
880  # Add deletion notice if the user is viewing deleted content
881  $notice = '';
882  if ( $deleted ) {
883  $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
884  $notice = Html::warningBox(
885  $this->msg( $msg )->parse(),
886  'plainlinks'
887  );
888  }
889  $this->showDiff( $oldHeader, $newHeader, $notice );
890  if ( !$diffOnly ) {
891  $this->renderNewRevision();
892  }
893  }
894  }
895 
906  public function markPatrolledLink() {
907  if ( $this->mMarkPatrolledLink === null ) {
908  $linkInfo = $this->getMarkPatrolledLinkInfo();
909  // If false, there is no patrol link needed/allowed
910  if ( !$linkInfo || !$this->mNewPage ) {
911  $this->mMarkPatrolledLink = '';
912  } else {
913  $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
914  $this->linkRenderer->makeKnownLink(
915  $this->mNewPage,
916  $this->msg( 'markaspatrolleddiff' )->text(),
917  [],
918  [
919  'action' => 'markpatrolled',
920  'rcid' => $linkInfo['rcid'],
921  ]
922  ) . ']</span>';
923  // Allow extensions to change the markpatrolled link
924  $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
925  $this->mMarkPatrolledLink, $linkInfo['rcid'] );
926  }
927  }
929  }
930 
938  protected function getMarkPatrolledLinkInfo() {
939  $user = $this->getUser();
940  $config = $this->getConfig();
941 
942  // Prepare a change patrol link, if applicable
943  if (
944  // Is patrolling enabled and the user allowed to?
945  $config->get( 'UseRCPatrol' ) &&
946  $this->mNewPage &&
947  $this->getAuthority()->probablyCan( 'patrol', $this->mNewPage ) &&
948  // Only do this if the revision isn't more than 6 hours older
949  // than the Max RC age (6h because the RC might not be cleaned out regularly)
950  RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
951  ) {
952  // Look for an unpatrolled change corresponding to this diff
953  $change = RecentChange::newFromConds(
954  [
955  'rc_this_oldid' => $this->mNewid,
956  'rc_patrolled' => RecentChange::PRC_UNPATROLLED
957  ],
958  __METHOD__
959  );
960 
961  if ( $change && !$change->getPerformer()->equals( $user ) ) {
962  $rcid = $change->getAttribute( 'rc_id' );
963  } else {
964  // None found or the page has been created by the current user.
965  // If the user could patrol this it already would be patrolled
966  $rcid = 0;
967  }
968 
969  // Allow extensions to possibly change the rcid here
970  // For example the rcid might be set to zero due to the user
971  // being the same as the performer of the change but an extension
972  // might still want to show it under certain conditions
973  $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
974 
975  // Build the link
976  if ( $rcid ) {
977  $this->getOutput()->preventClickjacking();
978  if ( $this->getAuthority()->isAllowed( 'writeapi' ) ) {
979  $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
980  }
981 
982  return [
983  'rcid' => $rcid,
984  ];
985  }
986  }
987 
988  // No mark as patrolled link applicable
989  return false;
990  }
991 
997  private function revisionDeleteLink( RevisionRecord $revRecord ) {
998  $link = Linker::getRevDeleteLink(
999  $this->getAuthority(),
1000  $revRecord,
1001  $revRecord->getPageAsLinkTarget()
1002  );
1003  if ( $link !== '' ) {
1004  $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
1005  }
1006 
1007  return $link;
1008  }
1009 
1015  public function renderNewRevision() {
1016  if ( $this->isContentOverridden ) {
1017  // The code below only works with a Revision object. We could construct a fake revision
1018  // (here or in setContent), but since this does not seem needed at the moment,
1019  // we'll just fail for now.
1020  throw new LogicException(
1021  __METHOD__
1022  . ' is not supported after calling setContent(). Use setRevisions() instead.'
1023  );
1024  }
1025 
1026  $out = $this->getOutput();
1027  $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
1028  # Add "current version as of X" title
1029  $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
1030  <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
1031  # Page content may be handled by a hooked call instead...
1032  if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
1033  $this->loadNewText();
1034  if ( !$this->mNewPage ) {
1035  // New revision is unsaved; bail out.
1036  // TODO in theory rendering the new revision is a meaningful thing to do
1037  // even if it's unsaved, but a lot of untangling is required to do it safely.
1038  return;
1039  }
1040 
1041  $out->setRevisionId( $this->mNewid );
1042  $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
1043  $out->setArticleFlag( true );
1044 
1045  if ( !$this->hookRunner->onArticleRevisionViewCustom(
1046  $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1047  ) {
1048  // Handled by extension
1049  // NOTE: sync with hooks called in Article::view()
1050  } else {
1051  // Normal page
1052  if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1053  // If the Title stored in the context is the same as the one
1054  // of the new revision, we can use its associated WikiPage
1055  // object.
1056  $wikiPage = $this->getWikiPage();
1057  } else {
1058  // Otherwise we need to create our own WikiPage object
1059  $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1060  }
1061 
1062  $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRevisionRecord );
1063 
1064  # WikiPage::getParserOutput() should not return false, but just in case
1065  if ( $parserOutput ) {
1066  // Allow extensions to change parser output here
1067  if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1068  $this, $out, $parserOutput, $wikiPage )
1069  ) {
1070  $out->addParserOutput( $parserOutput, [
1071  'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1072  && $this->getAuthority()->probablyCan(
1073  'edit',
1074  $this->mNewRevisionRecord->getPage()
1075  )
1076  ] );
1077  }
1078  }
1079  }
1080  }
1081 
1082  // Allow extensions to optionally not show the final patrolled link
1083  if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1084  # Add redundant patrol link on bottom...
1085  $out->addHTML( $this->markPatrolledLink() );
1086  }
1087  }
1088 
1095  protected function getParserOutput( WikiPage $page, RevisionRecord $revRecord ) {
1096  if ( !$revRecord->getId() ) {
1097  // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
1098  // the current revision, so fail instead. If need be, WikiPage::getParserOutput
1099  // could be made to accept a Revision or RevisionRecord instead of the id.
1100  return false;
1101  }
1102 
1103  $parserOptions = $page->makeParserOptions( $this->getContext() );
1104  $parserOutput = $page->getParserOutput( $parserOptions, $revRecord->getId() );
1105 
1106  return $parserOutput;
1107  }
1108 
1119  public function showDiff( $otitle, $ntitle, $notice = '' ) {
1120  // Allow extensions to affect the output here
1121  $this->hookRunner->onDifferenceEngineShowDiff( $this );
1122 
1123  $diff = $this->getDiff( $otitle, $ntitle, $notice );
1124  if ( $diff === false ) {
1125  $this->showMissingRevision();
1126 
1127  return false;
1128  } else {
1129  $this->showDiffStyle();
1130  $this->getOutput()->addHTML( $diff );
1131 
1132  return true;
1133  }
1134  }
1135 
1139  public function showDiffStyle() {
1140  if ( !$this->isSlotDiffRenderer ) {
1141  $this->getOutput()->addModuleStyles( [
1142  'mediawiki.interface.helpers.styles',
1143  'mediawiki.diff.styles'
1144  ] );
1145  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1146  $slotDiffRenderer->addModules( $this->getOutput() );
1147  }
1148  }
1149  }
1150 
1160  public function getDiff( $otitle, $ntitle, $notice = '' ) {
1161  $body = $this->getDiffBody();
1162  if ( $body === false ) {
1163  return false;
1164  }
1165 
1166  $multi = $this->getMultiNotice();
1167  // Display a message when the diff is empty
1168  if ( $body === '' ) {
1169  $notice .= '<div class="mw-diff-empty">' .
1170  $this->msg( 'diff-empty' )->parse() .
1171  "</div>\n";
1172  }
1173 
1174  return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1175  }
1176 
1182  public function getDiffBody() {
1183  $this->mCacheHit = true;
1184  // Check if the diff should be hidden from this user
1185  if ( !$this->isContentOverridden ) {
1186  if ( !$this->loadRevisionData() ) {
1187  return false;
1188  } elseif ( $this->mOldRevisionRecord &&
1189  !$this->mOldRevisionRecord->userCan(
1190  RevisionRecord::DELETED_TEXT,
1191  $this->getAuthority()
1192  )
1193  ) {
1194  return false;
1195  } elseif ( $this->mNewRevisionRecord &&
1196  !$this->mNewRevisionRecord->userCan(
1197  RevisionRecord::DELETED_TEXT,
1198  $this->getAuthority()
1199  ) ) {
1200  return false;
1201  }
1202  // Short-circuit
1203  if ( $this->mOldRevisionRecord === false || (
1204  $this->mOldRevisionRecord &&
1205  $this->mNewRevisionRecord &&
1206  $this->mOldRevisionRecord->getId() &&
1207  $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1208  ) ) {
1209  if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1210  return '';
1211  }
1212  }
1213  }
1214 
1215  // Cacheable?
1216  $key = false;
1217  $services = MediaWikiServices::getInstance();
1218  $cache = $services->getMainWANObjectCache();
1219  $stats = $services->getStatsdDataFactory();
1220  if ( $this->mOldid && $this->mNewid ) {
1221  // Check if subclass is still using the old way
1222  // for backwards-compatibility
1223  $key = $this->getDiffBodyCacheKey();
1224  if ( $key === null ) {
1225  $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1226  }
1227 
1228  // Try cache
1229  if ( !$this->mRefreshCache ) {
1230  $difftext = $cache->get( $key );
1231  if ( is_string( $difftext ) ) {
1232  $stats->updateCount( 'diff_cache.hit', 1 );
1233  $difftext = $this->localiseDiff( $difftext );
1234  $difftext .= "\n<!-- diff cache key $key -->\n";
1235 
1236  return $difftext;
1237  }
1238  } // don't try to load but save the result
1239  }
1240  $this->mCacheHit = false;
1241 
1242  // Loadtext is permission safe, this just clears out the diff
1243  if ( !$this->loadText() ) {
1244  return false;
1245  }
1246 
1247  $difftext = '';
1248  // We've checked for revdelete at the beginning of this method; it's OK to ignore
1249  // read permissions here.
1250  $slotContents = $this->getSlotContents();
1251  foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1252  $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1253  $slotContents[$role]['new'] );
1254  if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1255  // FIXME: ask SlotRoleHandler::getSlotNameMessage
1256  $slotTitle = $role;
1257  $difftext .= $this->getSlotHeader( $slotTitle );
1258  }
1259  $difftext .= $slotDiff;
1260  }
1261 
1262  // Save to cache for 7 days
1263  if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1264  $stats->updateCount( 'diff_cache.uncacheable', 1 );
1265  } elseif ( $key !== false ) {
1266  $stats->updateCount( 'diff_cache.miss', 1 );
1267  $cache->set( $key, $difftext, 7 * 86400 );
1268  } else {
1269  $stats->updateCount( 'diff_cache.uncacheable', 1 );
1270  }
1271  // localise line numbers and title attribute text
1272  $difftext = $this->localiseDiff( $difftext );
1273 
1274  return $difftext;
1275  }
1276 
1283  public function getDiffBodyForRole( $role ) {
1284  $diffRenderers = $this->getSlotDiffRenderers();
1285  if ( !isset( $diffRenderers[$role] ) ) {
1286  return false;
1287  }
1288 
1289  $slotContents = $this->getSlotContents();
1290  $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1291  $slotContents[$role]['new'] );
1292  if ( !$slotDiff ) {
1293  return false;
1294  }
1295 
1296  if ( $role !== SlotRecord::MAIN ) {
1297  // TODO use human-readable role name at least
1298  $slotTitle = $role;
1299  $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1300  }
1301 
1302  return $this->localiseDiff( $slotDiff );
1303  }
1304 
1312  protected function getSlotHeader( $headerText ) {
1313  // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1314  $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1315  $userLang = $this->getLanguage()->getHtmlCode();
1316  return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1317  Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1318  }
1319 
1329  protected function getDiffBodyCacheKey() {
1330  return null;
1331  }
1332 
1346  protected function getDiffBodyCacheKeyParams() {
1347  if ( !$this->mOldid || !$this->mNewid ) {
1348  throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1349  }
1350 
1351  $engine = $this->getEngine();
1352  $params = [
1353  'diff',
1354  $engine === 'php' ? false : $engine, // Back compat
1356  "old-{$this->mOldid}",
1357  "rev-{$this->mNewid}"
1358  ];
1359 
1360  if ( $engine === 'wikidiff2' ) {
1361  $params[] = phpversion( 'wikidiff2' );
1362  }
1363 
1364  if ( !$this->isSlotDiffRenderer ) {
1365  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1366  $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1367  }
1368  }
1369 
1370  return $params;
1371  }
1372 
1380  public function getExtraCacheKeys() {
1381  // This method is called when the DifferenceEngine is used for a slot diff. We only care
1382  // about special things, not the revision IDs, which are added to the cache key by the
1383  // page-level DifferenceEngine, and which might not have a valid value for this object.
1384  $this->mOldid = 123456789;
1385  $this->mNewid = 987654321;
1386 
1387  // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1388  $cacheString = $this->getDiffBodyCacheKey();
1389  if ( $cacheString ) {
1390  return [ $cacheString ];
1391  }
1392 
1393  $params = $this->getDiffBodyCacheKeyParams();
1394 
1395  // Try to get rid of the standard keys to keep the cache key human-readable:
1396  // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1397  // the child class includes the same keys, drop them.
1398  // Uses an obscure PHP feature where static calls to non-static methods are allowed
1399  // as long as we are already in a non-static method of the same class, and the call context
1400  // ($this) will be inherited.
1401  // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1402  $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1403  if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1404  $params = array_slice( $params, count( $standardParams ) );
1405  }
1406 
1407  return $params;
1408  }
1409 
1413  public function setSlotDiffOptions( $options ) {
1414  $this->slotDiffOptions = $options;
1415  }
1416 
1430  public function generateContentDiffBody( Content $old, Content $new ) {
1431  $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1432  if (
1433  $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1434  && $this->isSlotDiffRenderer
1435  ) {
1436  // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1437  // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1438  // This will happen when a content model has no custom slot diff renderer, it does have
1439  // a custom difference engine, but that does not override this method.
1440  throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1441  . 'Please use a SlotDiffRenderer.' );
1442  }
1443  return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1444  }
1445 
1458  public function generateTextDiffBody( $otext, $ntext ) {
1459  $slotDiffRenderer = $this->contentHandlerFactory
1460  ->getContentHandler( CONTENT_MODEL_TEXT )
1461  ->getSlotDiffRenderer( $this->getContext() );
1462  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1463  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1464  // This is too unlikely to happen to bother handling properly.
1465  throw new Exception( 'The slot diff renderer for text content should be a '
1466  . 'TextSlotDiffRenderer subclass' );
1467  }
1468  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1469  }
1470 
1477  public static function getEngine() {
1478  $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1479  ->get( 'DiffEngine' );
1480  $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1481  ->get( 'ExternalDiffEngine' );
1482 
1483  if ( $diffEngine === null ) {
1484  $engines = [ 'external', 'wikidiff2', 'php' ];
1485  } else {
1486  $engines = [ $diffEngine ];
1487  }
1488 
1489  $failureReason = null;
1490  foreach ( $engines as $engine ) {
1491  switch ( $engine ) {
1492  case 'external':
1493  if ( is_string( $externalDiffEngine ) ) {
1494  if ( is_executable( $externalDiffEngine ) ) {
1495  return $externalDiffEngine;
1496  }
1497  $failureReason = 'ExternalDiffEngine config points to a non-executable';
1498  if ( $diffEngine === null ) {
1499  wfDebug( "$failureReason, ignoring" );
1500  }
1501  } else {
1502  $failureReason = 'ExternalDiffEngine config is set to a non-string value';
1503  if ( $diffEngine === null && $externalDiffEngine ) {
1504  wfWarn( "$failureReason, ignoring" );
1505  }
1506  }
1507  break;
1508 
1509  case 'wikidiff2':
1510  if ( function_exists( 'wikidiff2_do_diff' ) ) {
1511  return 'wikidiff2';
1512  }
1513  $failureReason = 'wikidiff2 is not available';
1514  break;
1515 
1516  case 'php':
1517  // Always available.
1518  return 'php';
1519 
1520  default:
1521  throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
1522  }
1523  }
1524  throw new UnexpectedValueException( "Cannot use diff engine '$engine': $failureReason" );
1525  }
1526 
1539  protected function textDiff( $otext, $ntext ) {
1540  $slotDiffRenderer = $this->contentHandlerFactory
1541  ->getContentHandler( CONTENT_MODEL_TEXT )
1542  ->getSlotDiffRenderer( $this->getContext() );
1543  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1544  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1545  // This is too unlikely to happen to bother handling properly.
1546  throw new Exception( 'The slot diff renderer for text content should be a '
1547  . 'TextSlotDiffRenderer subclass' );
1548  }
1549  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1550  }
1551 
1560  protected function debug( $generator = "internal" ) {
1561  if ( !$this->enableDebugComment ) {
1562  return '';
1563  }
1564  $data = [ $generator ];
1565  if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
1566  $data[] = wfHostname();
1567  }
1568  $data[] = wfTimestamp( TS_DB );
1569 
1570  return "<!-- diff generator: " .
1571  implode( " ", array_map( "htmlspecialchars", $data ) ) .
1572  " -->\n";
1573  }
1574 
1578  private function getDebugString() {
1579  $engine = self::getEngine();
1580  if ( $engine === 'wikidiff2' ) {
1581  return $this->debug( 'wikidiff2' );
1582  } elseif ( $engine === 'php' ) {
1583  return $this->debug( 'native PHP' );
1584  } else {
1585  return $this->debug( "external $engine" );
1586  }
1587  }
1588 
1595  private function localiseDiff( $text ) {
1596  $text = $this->localiseLineNumbers( $text );
1597  if ( $this->getEngine() === 'wikidiff2' &&
1598  version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1599  ) {
1600  $text = $this->addLocalisedTitleTooltips( $text );
1601  }
1602  return $text;
1603  }
1604 
1612  public function localiseLineNumbers( $text ) {
1613  return preg_replace_callback(
1614  '/<!--LINE (\d+)-->/',
1615  [ $this, 'localiseLineNumbersCb' ],
1616  $text
1617  );
1618  }
1619 
1624  public function localiseLineNumbersCb( $matches ) {
1625  if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1626  return '';
1627  }
1628 
1629  return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1630  }
1631 
1638  private function addLocalisedTitleTooltips( $text ) {
1639  return preg_replace_callback(
1640  '/class="mw-diff-movedpara-(left|right)"/',
1641  [ $this, 'addLocalisedTitleTooltipsCb' ],
1642  $text
1643  );
1644  }
1645 
1650  private function addLocalisedTitleTooltipsCb( array $matches ) {
1651  $key = $matches[1] === 'right' ?
1652  'diff-paragraph-moved-toold' :
1653  'diff-paragraph-moved-tonew';
1654  return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1655  }
1656 
1662  public function getMultiNotice() {
1663  // The notice only make sense if we are diffing two saved revisions of the same page.
1664  if (
1665  !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1666  || !$this->mOldPage || !$this->mNewPage
1667  || !$this->mOldPage->equals( $this->mNewPage )
1668  || $this->mOldRevisionRecord->getId() === null
1669  || $this->mNewRevisionRecord->getId() === null
1670  // (T237709) Deleted revs might have different page IDs
1671  || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1672  || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1673  ) {
1674  return '';
1675  }
1676 
1677  if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1678  $oldRevRecord = $this->mNewRevisionRecord; // flip
1679  $newRevRecord = $this->mOldRevisionRecord; // flip
1680  } else { // normal case
1681  $oldRevRecord = $this->mOldRevisionRecord;
1682  $newRevRecord = $this->mNewRevisionRecord;
1683  }
1684 
1685  // Sanity: don't show the notice if too many rows must be scanned
1686  // @todo show some special message for that case
1687  $nEdits = $this->revisionStore->countRevisionsBetween(
1688  $this->mNewPage->getArticleID(),
1689  $oldRevRecord,
1690  $newRevRecord,
1691  1000
1692  );
1693  if ( $nEdits > 0 && $nEdits <= 1000 ) {
1694  $limit = 100; // use diff-multi-manyusers if too many users
1695  try {
1696  $users = $this->revisionStore->getAuthorsBetween(
1697  $this->mNewPage->getArticleID(),
1698  $oldRevRecord,
1699  $newRevRecord,
1700  null,
1701  $limit
1702  );
1703  $numUsers = count( $users );
1704 
1705  $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1706  $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1707  if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1708  $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1709  }
1710  } catch ( InvalidArgumentException $e ) {
1711  $numUsers = 0;
1712  }
1713 
1714  return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1715  }
1716 
1717  return '';
1718  }
1719 
1729  public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1730  if ( $numUsers === 0 ) {
1731  $msg = 'diff-multi-sameuser';
1732  } elseif ( $numUsers > $limit ) {
1733  $msg = 'diff-multi-manyusers';
1734  $numUsers = $limit;
1735  } else {
1736  $msg = 'diff-multi-otherusers';
1737  }
1738 
1739  return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1740  }
1741 
1746  private function userCanEdit( RevisionRecord $revRecord ) {
1747  if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1748  return false;
1749  }
1750 
1751  return true;
1752  }
1753 
1763  public function getRevisionHeader( $rev, $complete = '' ) {
1764  if ( $rev instanceof Revision ) {
1765  wfDeprecated( __METHOD__ . ' with a Revision object', '1.35' );
1766  $rev = $rev->getRevisionRecord();
1767  }
1768 
1769  $lang = $this->getLanguage();
1770  $user = $this->getUser();
1771  $revtimestamp = $rev->getTimestamp();
1772  $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1773  $dateofrev = $lang->userDate( $revtimestamp, $user );
1774  $timeofrev = $lang->userTime( $revtimestamp, $user );
1775 
1776  $header = $this->msg(
1777  $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1778  $timestamp,
1779  $dateofrev,
1780  $timeofrev
1781  );
1782 
1783  if ( $complete !== 'complete' ) {
1784  return $header->escaped();
1785  }
1786 
1787  $title = $rev->getPageAsLinkTarget();
1788 
1789  $header = $this->linkRenderer->makeKnownLink( $title, $header->text(), [],
1790  [ 'oldid' => $rev->getId() ] );
1791 
1792  if ( $this->userCanEdit( $rev ) ) {
1793  $editQuery = [ 'action' => 'edit' ];
1794  if ( !$rev->isCurrent() ) {
1795  $editQuery['oldid'] = $rev->getId();
1796  }
1797 
1798  $key = $this->getAuthority()->probablyCan( 'edit', $rev->getPage() ) ? 'editold' : 'viewsourceold';
1799  $msg = $this->msg( $key )->text();
1800  $editLink = $this->msg( 'parentheses' )->rawParams(
1801  $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery ) )->escaped();
1802  $header .= ' ' . Html::rawElement(
1803  'span',
1804  [ 'class' => 'mw-diff-edit' ],
1805  $editLink
1806  );
1807  if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1809  'span',
1810  [ 'class' => 'history-deleted' ],
1811  $header
1812  );
1813  }
1814  } else {
1815  $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1816  }
1817 
1818  return $header;
1819  }
1820 
1833  public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1834  // shared.css sets diff in interface language/dir, but the actual content
1835  // is often in a different language, mostly the page content language/dir
1836  $header = Html::openElement( 'table', [
1837  'class' => [
1838  'diff',
1839  'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1840  'diff-editfont-' . $this->getUser()->getOption( 'editfont' )
1841  ],
1842  'data-mw' => 'interface',
1843  ] );
1844  $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1845 
1846  if ( !$diff && !$otitle ) {
1847  $header .= "
1848  <tr class=\"diff-title\" lang=\"{$userLang}\">
1849  <td class=\"diff-ntitle\">{$ntitle}</td>
1850  </tr>";
1851  $multiColspan = 1;
1852  } else {
1853  if ( $diff ) { // Safari/Chrome show broken output if cols not used
1854  $header .= "
1855  <col class=\"diff-marker\" />
1856  <col class=\"diff-content\" />
1857  <col class=\"diff-marker\" />
1858  <col class=\"diff-content\" />";
1859  $colspan = 2;
1860  $multiColspan = 4;
1861  } else {
1862  $colspan = 1;
1863  $multiColspan = 2;
1864  }
1865  if ( $otitle || $ntitle ) {
1866  $header .= "
1867  <tr class=\"diff-title\" lang=\"{$userLang}\">
1868  <td colspan=\"$colspan\" class=\"diff-otitle\">{$otitle}</td>
1869  <td colspan=\"$colspan\" class=\"diff-ntitle\">{$ntitle}</td>
1870  </tr>";
1871  }
1872  }
1873 
1874  if ( $multi != '' ) {
1875  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1876  "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1877  }
1878  if ( $notice != '' ) {
1879  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1880  "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1881  }
1882 
1883  return $header . $diff . "</table>";
1884  }
1885 
1893  public function setContent( Content $oldContent, Content $newContent ) {
1894  $this->mOldContent = $oldContent;
1895  $this->mNewContent = $newContent;
1896 
1897  $this->mTextLoaded = 2;
1898  $this->mRevisionsLoaded = true;
1899  $this->isContentOverridden = true;
1900  $this->slotDiffRenderers = null;
1901  }
1902 
1908  public function setRevisions(
1909  ?RevisionRecord $oldRevision, RevisionRecord $newRevision
1910  ) {
1911  if ( $oldRevision ) {
1912  $this->mOldRevisionRecord = $oldRevision;
1913  $this->mOldid = $oldRevision->getId();
1914  $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1915  // This method is meant for edit diffs and such so there is no reason to provide a
1916  // revision that's not readable to the user, but check it just in case.
1917  $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1918  RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
1919  } else {
1920  $this->mOldPage = null;
1921  $this->mOldRevisionRecord = $this->mOldid = false;
1922  }
1923  $this->mNewRevisionRecord = $newRevision;
1924  $this->mNewid = $newRevision->getId();
1925  $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1926  $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1927  RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
1928 
1929  $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1930  $this->mTextLoaded = $oldRevision ? 2 : 1;
1931  $this->isContentOverridden = false;
1932  $this->slotDiffRenderers = null;
1933  }
1934 
1941  public function setTextLanguage( Language $lang ) {
1942  $this->mDiffLang = $lang;
1943  }
1944 
1957  public function mapDiffPrevNext( $old, $new ) {
1958  if ( $new === 'prev' ) {
1959  // Show diff between revision $old and the previous one. Get previous one from DB.
1960  $newid = intval( $old );
1961  $oldid = false;
1962  $newRev = $this->revisionStore->getRevisionById( $newid );
1963  if ( $newRev ) {
1964  $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
1965  if ( $oldRev ) {
1966  $oldid = $oldRev->getId();
1967  }
1968  }
1969  } elseif ( $new === 'next' ) {
1970  // Show diff between revision $old and the next one. Get next one from DB.
1971  $oldid = intval( $old );
1972  $newid = false;
1973  $oldRev = $this->revisionStore->getRevisionById( $oldid );
1974  if ( $oldRev ) {
1975  $newRev = $this->revisionStore->getNextRevision( $oldRev );
1976  if ( $newRev ) {
1977  $newid = $newRev->getId();
1978  }
1979  }
1980  } else {
1981  $oldid = intval( $old );
1982  $newid = intval( $new );
1983  }
1984 
1985  return [ $oldid, $newid ];
1986  }
1987 
1988  private function loadRevisionIds() {
1989  if ( $this->mRevisionsIdsLoaded ) {
1990  return;
1991  }
1992 
1993  $this->mRevisionsIdsLoaded = true;
1994 
1995  $old = $this->mOldid;
1996  $new = $this->mNewid;
1997 
1998  list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1999  if ( $new === 'next' && $this->mNewid === false ) {
2000  # if no result, NewId points to the newest old revision. The only newer
2001  # revision is cur, which is "0".
2002  $this->mNewid = 0;
2003  }
2004 
2005  $this->hookRunner->onNewDifferenceEngine(
2006  $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
2007  }
2008 
2022  public function loadRevisionData() {
2023  if ( $this->mRevisionsLoaded ) {
2024  return $this->isContentOverridden ||
2025  ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
2026  }
2027 
2028  // Whether it succeeds or fails, we don't want to try again
2029  $this->mRevisionsLoaded = true;
2030 
2031  $this->loadRevisionIds();
2032 
2033  // Load the new revision object
2034  if ( $this->mNewid ) {
2035  $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2036  } else {
2037  $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2038  }
2039 
2040  if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2041  return false;
2042  }
2043 
2044  // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2045  $this->mNewid = $this->mNewRevisionRecord->getId();
2046  if ( $this->mNewid ) {
2047  $this->mNewPage = Title::newFromLinkTarget(
2048  $this->mNewRevisionRecord->getPageAsLinkTarget()
2049  );
2050  } else {
2051  $this->mNewPage = null;
2052  }
2053 
2054  // Load the old revision object
2055  $this->mOldRevisionRecord = false;
2056  if ( $this->mOldid ) {
2057  $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2058  } elseif ( $this->mOldid === 0 ) {
2059  $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2060  if ( $revRecord ) {
2061  $this->mOldid = $revRecord->getId();
2062  $this->mOldRevisionRecord = $revRecord;
2063  } else {
2064  // No previous revision; mark to show as first-version only.
2065  $this->mOldid = false;
2066  $this->mOldRevisionRecord = false;
2067  }
2068  } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2069 
2070  if ( $this->mOldRevisionRecord === null ) {
2071  return false;
2072  }
2073 
2074  if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2075  $this->mOldPage = Title::newFromLinkTarget(
2076  $this->mOldRevisionRecord->getPageAsLinkTarget()
2077  );
2078  } else {
2079  $this->mOldPage = null;
2080  }
2081 
2082  // Load tags information for both revisions
2083  $dbr = wfGetDB( DB_REPLICA );
2084  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2085  if ( $this->mOldid !== false ) {
2086  $tagIds = $dbr->selectFieldValues(
2087  'change_tag',
2088  'ct_tag_id',
2089  [ 'ct_rev_id' => $this->mOldid ],
2090  __METHOD__
2091  );
2092  $tags = [];
2093  foreach ( $tagIds as $tagId ) {
2094  try {
2095  $tags[] = $changeTagDefStore->getName( (int)$tagId );
2096  } catch ( NameTableAccessException $exception ) {
2097  continue;
2098  }
2099  }
2100  $this->mOldTags = implode( ',', $tags );
2101  } else {
2102  $this->mOldTags = false;
2103  }
2104 
2105  $tagIds = $dbr->selectFieldValues(
2106  'change_tag',
2107  'ct_tag_id',
2108  [ 'ct_rev_id' => $this->mNewid ],
2109  __METHOD__
2110  );
2111  $tags = [];
2112  foreach ( $tagIds as $tagId ) {
2113  try {
2114  $tags[] = $changeTagDefStore->getName( (int)$tagId );
2115  } catch ( NameTableAccessException $exception ) {
2116  continue;
2117  }
2118  }
2119  $this->mNewTags = implode( ',', $tags );
2120 
2121  return true;
2122  }
2123 
2132  public function loadText() {
2133  if ( $this->mTextLoaded == 2 ) {
2134  return $this->loadRevisionData() &&
2135  ( $this->mOldRevisionRecord === false || $this->mOldContent )
2136  && $this->mNewContent;
2137  }
2138 
2139  // Whether it succeeds or fails, we don't want to try again
2140  $this->mTextLoaded = 2;
2141 
2142  if ( !$this->loadRevisionData() ) {
2143  return false;
2144  }
2145 
2146  if ( $this->mOldRevisionRecord ) {
2147  $this->mOldContent = $this->mOldRevisionRecord->getContent(
2148  SlotRecord::MAIN,
2149  RevisionRecord::FOR_THIS_USER,
2150  $this->getAuthority()
2151  );
2152  if ( $this->mOldContent === null ) {
2153  return false;
2154  }
2155  }
2156 
2157  $this->mNewContent = $this->mNewRevisionRecord->getContent(
2158  SlotRecord::MAIN,
2159  RevisionRecord::FOR_THIS_USER,
2160  $this->getAuthority()
2161  );
2162  $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2163  if ( $this->mNewContent === null ) {
2164  return false;
2165  }
2166 
2167  return true;
2168  }
2169 
2175  public function loadNewText() {
2176  if ( $this->mTextLoaded >= 1 ) {
2177  return $this->loadRevisionData();
2178  }
2179 
2180  $this->mTextLoaded = 1;
2181 
2182  if ( !$this->loadRevisionData() ) {
2183  return false;
2184  }
2185 
2186  $this->mNewContent = $this->mNewRevisionRecord->getContent(
2187  SlotRecord::MAIN,
2188  RevisionRecord::FOR_THIS_USER,
2189  $this->getAuthority()
2190  );
2191 
2192  $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2193 
2194  return true;
2195  }
2196 
2197 }
Content\getContentHandler
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
DifferenceEngine\$wikiPageFactory
WikiPageFactory $wikiPageFactory
Definition: DifferenceEngine.php:235
DifferenceEngine\$mRevisionsIdsLoaded
bool $mRevisionsIdsLoaded
Have the revisions IDs been loaded.
Definition: DifferenceEngine.php:153
DifferenceEngine\$mNewRevisionRecord
RevisionRecord null $mNewRevisionRecord
New revision (right pane).
Definition: DifferenceEngine.php:107
ContextSource\$context
IContextSource $context
Definition: ContextSource.php:38
ContextSource\getConfig
getConfig()
Definition: ContextSource.php:71
DifferenceEngine\getSlotContents
getSlotContents()
Get the old and new content objects for all slots.
Definition: DifferenceEngine.php:324
DifferenceEngine\markPatrolledLink
markPatrolledLink()
Build a link to mark a change as patrolled.
Definition: DifferenceEngine.php:906
ContextSource\getContext
getContext()
Get the base IContextSource object.
Definition: ContextSource.php:46
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:1477
DifferenceEngine\$mTextLoaded
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
Definition: DifferenceEngine.php:159
DifferenceEngine\addLocalisedTitleTooltipsCb
addLocalisedTitleTooltipsCb(array $matches)
Definition: DifferenceEngine.php:1650
DifferenceEngine\getDiffBodyCacheKeyParams
getDiffBodyCacheKeyParams()
Get the cache key parameters.
Definition: DifferenceEngine.php:1346
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:525
DifferenceEngine\$unhide
bool $unhide
Show rev_deleted content if allowed.
Definition: DifferenceEngine.php:191
WikiPage\getParserOutput
getParserOutput(ParserOptions $parserOptions, $oldid=null, $noCache=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1298
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:173
$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:223
DifferenceEngine\setReducedLineNumbers
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
Definition: DifferenceEngine.php:374
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:89
DifferenceEngine\setContent
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
Definition: DifferenceEngine.php:1893
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:1832
DifferenceEngine\getNewid
getNewid()
Get the ID of new revision (right pane) of the diff.
Definition: DifferenceEngine.php:418
DifferenceEngine\getOldRevision
getOldRevision()
Get the left side of the diff.
Definition: DifferenceEngine.php:430
DifferenceEngine\getOldid
getOldid()
Get the ID of old revision (left pane) of the diff.
Definition: DifferenceEngine.php:406
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:61
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:1597
DifferenceEngine\$mRevisionsLoaded
bool $mRevisionsLoaded
Have the revisions been loaded.
Definition: DifferenceEngine.php:156
DifferenceEngine\deletedIdMarker
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
Definition: DifferenceEngine.php:485
WikiPage\makeParserOptions
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2183
wfHostname
wfHostname()
Get host name of the current machine, for use in error reporting.
Definition: GlobalFunctions.php:1294
DifferenceEngine\$enableDebugComment
bool $enableDebugComment
Set this to true to add debug info to the HTML output.
Definition: DifferenceEngine.php:180
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:205
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1231
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:1638
DifferenceEngine\$mOldRevisionRecord
RevisionRecord null false $mOldRevisionRecord
Old revision (left pane).
Definition: DifferenceEngine.php:97
ContextSource\getRequest
getRequest()
Definition: ContextSource.php:80
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:1139
DifferenceEngine\loadNewText
loadNewText()
Load the text of the new revision, not the old one.
Definition: DifferenceEngine.php:2175
Html\warningBox
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:729
ContextSource\getUser
getUser()
Definition: ContextSource.php:135
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:1119
DifferenceEngine\getDiffBodyCacheKey
getDiffBodyCacheKey()
Returns the cache key for diff body text or content.
Definition: DifferenceEngine.php:1329
DifferenceEngine\localiseDiff
localiseDiff( $text)
Localise diff output.
Definition: DifferenceEngine.php:1595
DifferenceEngine\$mNewid
int string false null $mNewid
Revision ID for the new revision.
Definition: DifferenceEngine.php:85
DifferenceEngine\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: DifferenceEngine.php:221
DifferenceEngine\hasDeletedRevision
hasDeletedRevision()
Checks whether one of the given Revisions was deleted.
Definition: DifferenceEngine.php:522
DifferenceEngine\generateTextDiffBody
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
Definition: DifferenceEngine.php:1458
$dbr
$dbr
Definition: testCompression.php:54
ContextSource\getLanguage
getLanguage()
Definition: ContextSource.php:151
NS_SPECIAL
const NS_SPECIAL
Definition: Defines.php:53
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:185
DifferenceEngine\revisionDeleteLink
revisionDeleteLink(RevisionRecord $revRecord)
Definition: DifferenceEngine.php:997
DifferenceEngine\loadRevisionData
loadRevisionData()
Load revision metadata for the specified revisions.
Definition: DifferenceEngine.php:2022
DifferenceEngine\localiseLineNumbersCb
localiseLineNumbersCb( $matches)
Definition: DifferenceEngine.php:1624
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:1034
DifferenceEngine\getDiffBodyForRole
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
Definition: DifferenceEngine.php:1283
DifferenceEngine\$slotDiffRenderers
SlotDiffRenderer[] $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
Definition: DifferenceEngine.php:197
Linker\generateRollback
static generateRollback( $rev, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1849
DifferenceEngine\wasCacheHit
wasCacheHit()
Definition: DifferenceEngine.php:395
DifferenceEngine\$mNewTags
string[] null $mNewTags
Change tags of new revision or null if it does not exist / is not saved.
Definition: DifferenceEngine.php:133
DifferenceEngine\isUserAllowedToSeeRevisions
isUserAllowedToSeeRevisions(Authority $performer)
Checks whether the current user has permission for accessing the revisions of the diff.
Definition: DifferenceEngine.php:577
DifferenceEngine\getDiffLang
getDiffLang()
Get the language of the difference engine, defaults to page content language.
Definition: DifferenceEngine.php:383
DifferenceEngine\getRevisionHeader
getRevisionHeader( $rev, $complete='')
Get a header for a specified revision.
Definition: DifferenceEngine.php:1763
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2467
ContextSource\getOutput
getOutput()
Definition: ContextSource.php:125
$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:1312
ContextSource
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
Definition: ContextSource.php:32
ContextSource\getWikiPage
getWikiPage()
Get the WikiPage object.
Definition: ContextSource.php:116
DifferenceEngine\getParserOutput
getParserOutput(WikiPage $page, RevisionRecord $revRecord)
Definition: DifferenceEngine.php:1095
DifferenceEngine\$mNewContent
Content null $mNewContent
Definition: DifferenceEngine.php:147
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:451
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:270
DifferenceEngine\getDiffBody
getDiffBody()
Get the diff table body, without header.
Definition: DifferenceEngine.php:1182
DifferenceEngine\$slotDiffOptions
array $slotDiffOptions
A set of options that will be passed to the SlotDiffRenderer upon creation.
Definition: DifferenceEngine.php:211
DifferenceEngine\getSlotDiffRenderers
getSlotDiffRenderers()
Definition: DifferenceEngine.php:284
$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:626
DifferenceEngine\setRevisions
setRevisions(?RevisionRecord $oldRevision, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
Definition: DifferenceEngine.php:1908
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
Linker\getRevDeleteLink
static getRevDeleteLink(Authority $performer, $rev, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition: Linker.php:2190
DifferenceEngine\loadText
loadText()
Load the text of the revisions, as well as revision data.
Definition: DifferenceEngine.php:2132
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:1138
$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:915
ContextSource\setContext
setContext(IContextSource $context)
Definition: ContextSource.php:62
DifferenceEngine\$mCacheHit
bool $mCacheHit
Was the diff fetched from cache?
Definition: DifferenceEngine.php:172
deprecatePublicProperty
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
Definition: DeprecationHelper.php:87
DifferenceEngine\markAsSlotDiffRenderer
markAsSlotDiffRenderer()
Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
Definition: DifferenceEngine.php:315
DifferenceEngine\$hookContainer
HookContainer $hookContainer
Definition: DifferenceEngine.php:232
MediaWiki\Permissions\Authority
Definition: Authority.php:30
DifferenceEngine\__construct
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
#-
Definition: DifferenceEngine.php:247
ContextSource\msg
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Definition: ContextSource.php:195
DifferenceEngine\getDebugString
getDebugString()
Definition: DifferenceEngine.php:1578
Title\makeTitleSafe
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:652
DifferenceEngine\setTextLanguage
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
Definition: DifferenceEngine.php:1941
$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:127
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
ContextSource\getAuthority
getAuthority()
Definition: ContextSource.php:142
$header
$header
Definition: updateCredits.php:37
Revision\RevisionRecord\getId
getId( $wikiId=self::LOCAL)
Get revision ID.
Definition: RevisionRecord.php:295
DifferenceEngine\$linkRenderer
LinkRenderer $linkRenderer
Definition: DifferenceEngine.php:216
DifferenceEngine\$hookRunner
HookRunner $hookRunner
Definition: DifferenceEngine.php:229
DifferenceEngine\getTitle
getTitle()
1.18 Stability: stableto override Title|null
Definition: DifferenceEngine.php:363
DifferenceEngine\showDiffPage
showDiffPage( $diffOnly=false)
Definition: DifferenceEngine.php:611
Title\newFromLinkTarget
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:291
DifferenceEngine\$mOldid
int false null $mOldid
Revision ID for the old revision.
Definition: DifferenceEngine.php:77
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:2105
DifferenceEngine\$isContentOverridden
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e....
Definition: DifferenceEngine.php:169
DifferenceEngine\DIFF_VERSION
const DIFF_VERSION
Constant to indicate diff cache compatibility.
Definition: DifferenceEngine.php:69
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:57
MediaWiki\Permissions\PermissionStatus
A StatusValue for permission errors.
Definition: PermissionStatus.php:34
DifferenceEngine\intermediateEditsMsg
static intermediateEditsMsg( $numEdits, $numUsers, $limit)
Get a notice about how many intermediate edits and users there are.
Definition: DifferenceEngine.php:1729
DifferenceEngine\showMissingRevision
showMissingRevision()
Definition: DifferenceEngine.php:494
Content
Base interface for content objects.
Definition: Content.php:35
DifferenceEngine\loadRevisionIds
loadRevisionIds()
Definition: DifferenceEngine.php:1988
DifferenceEngine\getNewRevision
getNewRevision()
Get the right side of the diff.
Definition: DifferenceEngine.php:439
Revision\RevisionRecord\getPageAsLinkTarget
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
Definition: RevisionRecord.php:371
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:150
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:938
CONTENT_MODEL_TEXT
const CONTENT_MODEL_TEXT
Definition: Defines.php:222
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:114
RecentChange\PRC_UNPATROLLED
const PRC_UNPATROLLED
Definition: RecentChange.php:85
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:1380
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:212
getTitle
getTitle()
Definition: RevisionSearchResultTrait.php:81
DifferenceEngine
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
Definition: DifferenceEngine.php:59
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:1169
DifferenceEngine\$mMarkPatrolledLink
string $mMarkPatrolledLink
Link to action=markpatrolled.
Definition: DifferenceEngine.php:188
DifferenceEngine\localiseLineNumbers
localiseLineNumbers( $text)
Replace line numbers with the text in the user's language.
Definition: DifferenceEngine.php:1612
DifferenceEngine\hasSuppressedRevision
hasSuppressedRevision()
Checks whether one of the given Revisions was suppressed.
Definition: DifferenceEngine.php:557
DifferenceEngine\$mOldContent
Content null $mOldContent
Definition: DifferenceEngine.php:140
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:1081
DifferenceEngine\userCanEdit
userCanEdit(RevisionRecord $revRecord)
Definition: DifferenceEngine.php:1746
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:576
DifferenceEngine\debug
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend.
Definition: DifferenceEngine.php:1560
DifferenceEngine\renderNewRevision
renderNewRevision()
Show the new revision of the page.
Definition: DifferenceEngine.php:1015
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:1160
DifferenceEngine\setSlotDiffOptions
setSlotDiffOptions( $options)
Definition: DifferenceEngine.php:1413
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:1430
DifferenceEngine\mapDiffPrevNext
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
Definition: DifferenceEngine.php:1957
DifferenceEngine\addHeader
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
Definition: DifferenceEngine.php:1833
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:603
ChangeTags\formatSummaryRow
static formatSummaryRow( $tags, $page, IContextSource $context=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:176
DifferenceEngine\getPermissionErrors
getPermissionErrors(Authority $performer)
Get the permission errors associated with the revisions for the current diff.
Definition: DifferenceEngine.php:540
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:43
DifferenceEngine\textDiff
textDiff( $otext, $ntext)
Generates diff, to be wrapped internally in a logging/instrumentation.
Definition: DifferenceEngine.php:1539
DifferenceEngine\$revisionStore
RevisionStore $revisionStore
Definition: DifferenceEngine.php:226
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:172
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:121
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:194
DifferenceEngine\getMultiNotice
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
Definition: DifferenceEngine.php:1662