MediaWiki  master
DifferenceEngine.php
Go to the documentation of this file.
1 <?php
37 
61 
63 
70  private const DIFF_VERSION = '1.12';
71 
78  protected $mOldid;
79 
86  protected $mNewid;
87 
99 
109 
115  protected $mOldPage;
116 
122  protected $mNewPage;
123 
128  private $mOldTags;
129 
134  private $mNewTags;
135 
141  private $mOldContent;
142 
148  private $mNewContent;
149 
151  protected $mDiffLang;
152 
154  private $mRevisionsIdsLoaded = false;
155 
157  protected $mRevisionsLoaded = false;
158 
160  protected $mTextLoaded = 0;
161 
170  protected $isContentOverridden = false;
171 
173  protected $mCacheHit = false;
174 
181  public $enableDebugComment = false;
182 
186  protected $mReducedLineNumbers = false;
187 
189  protected $mMarkPatrolledLink = null;
190 
192  protected $unhide = false;
193 
195  protected $mRefreshCache = false;
196 
198  protected $slotDiffRenderers = null;
199 
206  protected $isSlotDiffRenderer = false;
207 
212  private $slotDiffOptions = [];
213 
217  protected $linkRenderer;
218 
223 
227  private $revisionStore;
228 
230  private $hookRunner;
231 
234 
237 
248  public function __construct( $context = null, $old = 0, $new = 0, $rcid = 0,
249  $refreshCache = false, $unhide = false
250  ) {
251  $this->deprecatePublicProperty( 'mOldid', '1.32', __CLASS__ );
252  $this->deprecatePublicProperty( 'mNewid', '1.32', __CLASS__ );
253  $this->deprecatePublicProperty( 'mOldPage', '1.32', __CLASS__ );
254  $this->deprecatePublicProperty( 'mNewPage', '1.32', __CLASS__ );
255  $this->deprecatePublicProperty( 'mOldContent', '1.32', __CLASS__ );
256  $this->deprecatePublicProperty( 'mNewContent', '1.32', __CLASS__ );
257  $this->deprecatePublicProperty( 'mRevisionsLoaded', '1.32', __CLASS__ );
258  $this->deprecatePublicProperty( 'mTextLoaded', '1.32', __CLASS__ );
259  $this->deprecatePublicProperty( 'mCacheHit', '1.32', __CLASS__ );
260 
261  if ( $context instanceof IContextSource ) {
262  $this->setContext( $context );
263  }
264 
265  wfDebug( "DifferenceEngine old '$old' new '$new' rcid '$rcid'" );
266 
267  $this->mOldid = $old;
268  $this->mNewid = $new;
269  $this->mRefreshCache = $refreshCache;
270  $this->unhide = $unhide;
271 
272  $services = MediaWikiServices::getInstance();
273  $this->linkRenderer = $services->getLinkRenderer();
274  $this->contentHandlerFactory = $services->getContentHandlerFactory();
275  $this->revisionStore = $services->getRevisionStore();
276  $this->hookRunner = new HookRunner( $services->getHookContainer() );
277  $this->wikiPageFactory = $services->getWikiPageFactory();
278  $this->userOptionsLookup = $services->getUserOptionsLookup();
279  }
280 
285  protected function getSlotDiffRenderers() {
286  if ( $this->isSlotDiffRenderer ) {
287  throw new LogicException( __METHOD__ . ' called in slot diff renderer mode' );
288  }
289 
290  if ( $this->slotDiffRenderers === null ) {
291  if ( !$this->loadRevisionData() ) {
292  return [];
293  }
294 
295  $slotContents = $this->getSlotContents();
296  $this->slotDiffRenderers = array_map( function ( array $contents ) {
298  $content = $contents['new'] ?: $contents['old'];
299  return $content->getContentHandler()->getSlotDiffRenderer(
300  $this->getContext(),
301  $this->slotDiffOptions
302  );
303  }, $slotContents );
304  }
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 => [ 'old' => $this->mOldContent, 'new' => $this->mNewContent ]
328  ];
329  } elseif ( !$this->loadRevisionData() ) {
330  return [];
331  }
332 
333  $newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
334  $oldSlots = $this->mOldRevisionRecord ?
335  $this->mOldRevisionRecord->getPrimarySlots()->getSlots() :
336  [];
337  // The order here will determine the visual order of the diff. The current logic is
338  // slots of the new revision first in natural order, then deleted ones. This is ad hoc
339  // and should not be relied on - in the future we may want the ordering to depend
340  // on the page type.
341  $roles = array_keys( array_merge( $newSlots, $oldSlots ) );
342 
343  $slots = [];
344  foreach ( $roles as $role ) {
345  $slots[$role] = [
346  'old' => isset( $oldSlots[$role] ) ? $oldSlots[$role]->getContent() : null,
347  'new' => isset( $newSlots[$role] ) ? $newSlots[$role]->getContent() : null,
348  ];
349  }
350  // move main slot to front
351  if ( isset( $slots[SlotRecord::MAIN] ) ) {
352  $slots = [ SlotRecord::MAIN => $slots[SlotRecord::MAIN] ] + $slots;
353  }
354  return $slots;
355  }
356 
358  public function getTitle() {
359  // T202454 avoid errors when there is no title
360  return parent::getTitle() ?: Title::makeTitle( NS_SPECIAL, 'BadTitle/DifferenceEngine' );
361  }
362 
369  public function setReducedLineNumbers( $value = true ) {
370  $this->mReducedLineNumbers = $value;
371  }
372 
378  public function getDiffLang() {
379  if ( $this->mDiffLang === null ) {
380  # Default language in which the diff text is written.
381  $this->mDiffLang = $this->getTitle()->getPageLanguage();
382  }
383 
384  return $this->mDiffLang;
385  }
386 
390  public function wasCacheHit() {
391  return $this->mCacheHit;
392  }
393 
401  public function getOldid() {
402  $this->loadRevisionIds();
403 
404  return $this->mOldid;
405  }
406 
413  public function getNewid() {
414  $this->loadRevisionIds();
415 
416  return $this->mNewid;
417  }
418 
425  public function getOldRevision() {
426  return $this->mOldRevisionRecord ?: null;
427  }
428 
434  public function getNewRevision() {
436  }
437 
446  public function deletedLink( $id ) {
447  if ( $this->getAuthority()->isAllowed( 'deletedhistory' ) ) {
448  $dbr = wfGetDB( DB_REPLICA );
449  $arQuery = $this->revisionStore->getArchiveQueryInfo();
450  $row = $dbr->selectRow(
451  $arQuery['tables'],
452  array_merge( $arQuery['fields'], [ 'ar_namespace', 'ar_title' ] ),
453  [ 'ar_rev_id' => $id ],
454  __METHOD__,
455  [],
456  $arQuery['joins']
457  );
458  if ( $row ) {
459  $revRecord = $this->revisionStore->newRevisionFromArchiveRow( $row );
460  $title = Title::makeTitleSafe( $row->ar_namespace, $row->ar_title );
461 
462  return SpecialPage::getTitleFor( 'Undelete' )->getFullURL( [
463  'target' => $title->getPrefixedText(),
464  'timestamp' => $revRecord->getTimestamp()
465  ] );
466  }
467  }
468 
469  return false;
470  }
471 
479  public function deletedIdMarker( $id ) {
480  $link = $this->deletedLink( $id );
481  if ( $link ) {
482  return "[$link $id]";
483  } else {
484  return (string)$id;
485  }
486  }
487 
488  private function showMissingRevision() {
489  $out = $this->getOutput();
490 
491  $missing = [];
492  if ( $this->mOldid && ( !$this->mOldRevisionRecord || !$this->mOldContent ) ) {
493  $missing[] = $this->deletedIdMarker( $this->mOldid );
494  }
495  if ( !$this->mNewRevisionRecord || !$this->mNewContent ) {
496  $missing[] = $this->deletedIdMarker( $this->mNewid );
497  }
498 
499  $out->setPageTitle( $this->msg( 'errorpagetitle' ) );
500  $msg = $this->msg( 'difference-missing-revision' )
501  ->params( $this->getLanguage()->listToText( $missing ) )
502  ->numParams( count( $missing ) )
503  ->parseAsBlock();
504  $out->addHTML( $msg );
505  }
506 
512  public function hasDeletedRevision() {
513  $this->loadRevisionData();
514  return (
515  $this->mNewRevisionRecord &&
516  $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
517  ) ||
518  (
519  $this->mOldRevisionRecord &&
520  $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT )
521  );
522  }
523 
530  public function getPermissionErrors( Authority $performer ) {
531  $this->loadRevisionData();
532  $permStatus = PermissionStatus::newEmpty();
533  if ( $this->mNewPage ) {
534  $performer->authorizeRead( 'read', $this->mNewPage, $permStatus );
535  }
536  if ( $this->mOldPage ) {
537  $performer->authorizeRead( 'read', $this->mOldPage, $permStatus );
538  }
539  return $permStatus->toLegacyErrorArray();
540  }
541 
547  public function hasSuppressedRevision() {
548  return $this->hasDeletedRevision() && (
549  ( $this->mOldRevisionRecord &&
550  $this->mOldRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) ||
551  ( $this->mNewRevisionRecord &&
552  $this->mNewRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) )
553  );
554  }
555 
567  public function isUserAllowedToSeeRevisions( Authority $performer ) {
568  $this->loadRevisionData();
569 
570  if ( $this->mOldRevisionRecord && !$this->mOldRevisionRecord->userCan(
571  RevisionRecord::DELETED_TEXT,
572  $performer
573  ) ) {
574  return false;
575  }
576 
577  // $this->mNewRev will only be falsy if a loading error occurred
578  // (in which case the user is allowed to see).
579  return !$this->mNewRevisionRecord || $this->mNewRevisionRecord->userCan(
580  RevisionRecord::DELETED_TEXT,
581  $performer
582  );
583  }
584 
592  public function shouldBeHiddenFromUser( Authority $performer ) {
593  return $this->hasDeletedRevision() && ( !$this->unhide ||
594  !$this->isUserAllowedToSeeRevisions( $performer ) );
595  }
596 
600  public function showDiffPage( $diffOnly = false ) {
601  # Allow frames except in certain special cases
602  $out = $this->getOutput();
603  $out->setPreventClickjacking( false );
604  $out->setRobotPolicy( 'noindex,nofollow' );
605 
606  // Allow extensions to add any extra output here
607  $this->hookRunner->onDifferenceEngineShowDiffPage( $out );
608 
609  if ( !$this->loadRevisionData() ) {
610  if ( $this->hookRunner->onDifferenceEngineShowDiffPageMaybeShowMissingRevision( $this ) ) {
611  $this->showMissingRevision();
612  }
613  return;
614  }
615 
616  $user = $this->getUser();
617  $permErrors = $this->getPermissionErrors( $this->getAuthority() );
618  if ( $permErrors ) {
619  throw new PermissionsError( 'read', $permErrors );
620  }
621 
622  $rollback = '';
623 
624  $query = $this->slotDiffOptions;
625  # Carry over 'diffonly' param via navigation links
626  if ( $diffOnly != MediaWikiServices::getInstance()
627  ->getUserOptionsLookup()->getBoolOption( $user, 'diffonly' )
628  ) {
629  $query['diffonly'] = $diffOnly;
630  }
631  # Cascade unhide param in links for easy deletion browsing
632  if ( $this->unhide ) {
633  $query['unhide'] = 1;
634  }
635 
636  # Check if one of the revisions is deleted/suppressed
637  $deleted = $this->hasDeletedRevision();
638  $suppressed = $this->hasSuppressedRevision();
639  $allowed = $this->isUserAllowedToSeeRevisions( $this->getAuthority() );
640 
641  $revisionTools = [];
642 
643  # mOldRevisionRecord is false if the difference engine is called with a "vague" query for
644  # a diff between a version V and its previous version V' AND the version V
645  # is the first version of that article. In that case, V' does not exist.
646  if ( $this->mOldRevisionRecord === false ) {
647  if ( $this->mNewPage ) {
648  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
649  }
650  $samePage = true;
651  $oldHeader = '';
652  // Allow extensions to change the $oldHeader variable
653  $this->hookRunner->onDifferenceEngineOldHeaderNoOldRev( $oldHeader );
654  } else {
655  $this->hookRunner->onDifferenceEngineViewHeader( $this );
656 
657  if ( !$this->mOldPage || !$this->mNewPage ) {
658  // XXX say something to the user?
659  $samePage = false;
660  } elseif ( $this->mNewPage->equals( $this->mOldPage ) ) {
661  $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
662  $samePage = true;
663  } else {
664  $out->setPageTitle( $this->msg( 'difference-title-multipage',
665  $this->mOldPage->getPrefixedText(), $this->mNewPage->getPrefixedText() ) );
666  $out->addSubtitle( $this->msg( 'difference-multipage' ) );
667  $samePage = false;
668  }
669 
670  if ( $samePage && $this->mNewPage &&
671  $this->getAuthority()->probablyCan( 'edit', $this->mNewPage )
672  ) {
673  if ( $this->mNewRevisionRecord->isCurrent() &&
674  $this->getAuthority()->probablyCan( 'rollback', $this->mNewPage )
675  ) {
676  $rollbackLink = Linker::generateRollback(
677  $this->mNewRevisionRecord,
678  $this->getContext(),
679  [ 'noBrackets' ]
680  );
681  if ( $rollbackLink ) {
682  $out->setPreventClickjacking( true );
683  $rollback = "\u{00A0}\u{00A0}\u{00A0}" . $rollbackLink;
684  }
685  }
686 
687  if ( $this->userCanEdit( $this->mOldRevisionRecord ) &&
688  $this->userCanEdit( $this->mNewRevisionRecord )
689  ) {
690  $undoLink = $this->linkRenderer->makeKnownLink(
691  $this->mNewPage,
692  $this->msg( 'editundo' )->text(),
693  [ 'title' => Linker::titleAttrib( 'undo' ) ],
694  [
695  'action' => 'edit',
696  'undoafter' => $this->mOldid,
697  'undo' => $this->mNewid
698  ]
699  );
700  $revisionTools['mw-diff-undo'] = $undoLink;
701  }
702  }
703  # Make "previous revision link"
704  $hasPrevious = $samePage && $this->mOldPage &&
705  $this->revisionStore->getPreviousRevision( $this->mOldRevisionRecord );
706  if ( $hasPrevious ) {
707  $prevlink = $this->linkRenderer->makeKnownLink(
708  $this->mOldPage,
709  $this->msg( 'previousdiff' )->text(),
710  [ 'id' => 'differences-prevlink' ],
711  [ 'diff' => 'prev', 'oldid' => $this->mOldid ] + $query
712  );
713  } else {
714  $prevlink = "\u{00A0}";
715  }
716 
717  if ( $this->mOldRevisionRecord->isMinor() ) {
718  $oldminor = ChangesList::flag( 'minor' );
719  } else {
720  $oldminor = '';
721  }
722 
723  $oldRevRecord = $this->mOldRevisionRecord;
724 
725  $ldel = $this->revisionDeleteLink( $oldRevRecord );
726  $oldRevisionHeader = $this->getRevisionHeader( $oldRevRecord, 'complete' );
727  $oldChangeTags = ChangeTags::formatSummaryRow( $this->mOldTags, 'diff', $this->getContext() );
728  $oldRevComment = Linker::revComment( $oldRevRecord, !$diffOnly, !$this->unhide );
729 
730  if ( $oldRevComment === '' ) {
731  $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
732  $oldRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
733  }
734 
735  $oldHeader = '<div id="mw-diff-otitle1"><strong>' . $oldRevisionHeader . '</strong></div>' .
736  '<div id="mw-diff-otitle2">' .
737  Linker::revUserTools( $oldRevRecord, !$this->unhide ) . '</div>' .
738  '<div id="mw-diff-otitle3">' . $oldminor . $oldRevComment . $ldel . '</div>' .
739  '<div id="mw-diff-otitle5">' . $oldChangeTags[0] . '</div>' .
740  '<div id="mw-diff-otitle4">' . $prevlink . '</div>';
741 
742  // Allow extensions to change the $oldHeader variable
743  $this->hookRunner->onDifferenceEngineOldHeader(
744  $this, $oldHeader, $prevlink, $oldminor, $diffOnly, $ldel, $this->unhide );
745  }
746 
747  $out->addJsConfigVars( [
748  'wgDiffOldId' => $this->mOldid,
749  'wgDiffNewId' => $this->mNewid,
750  ] );
751 
752  # Make "next revision link"
753  # Skip next link on the top revision
754  if ( $samePage && $this->mNewPage && !$this->mNewRevisionRecord->isCurrent() ) {
755  $nextlink = $this->linkRenderer->makeKnownLink(
756  $this->mNewPage,
757  $this->msg( 'nextdiff' )->text(),
758  [ 'id' => 'differences-nextlink' ],
759  [ 'diff' => 'next', 'oldid' => $this->mNewid ] + $query
760  );
761  } else {
762  $nextlink = "\u{00A0}";
763  }
764 
765  if ( $this->mNewRevisionRecord->isMinor() ) {
766  $newminor = ChangesList::flag( 'minor' );
767  } else {
768  $newminor = '';
769  }
770 
771  # Handle RevisionDelete links...
772  $rdel = $this->revisionDeleteLink( $this->mNewRevisionRecord );
773 
774  # Allow extensions to define their own revision tools
775  $this->hookRunner->onDiffTools(
776  $this->mNewRevisionRecord,
777  $revisionTools,
778  $this->mOldRevisionRecord ?: null,
779  $user
780  );
781 
782  $formattedRevisionTools = [];
783  // Put each one in parentheses (poor man's button)
784  foreach ( $revisionTools as $key => $tool ) {
785  $toolClass = is_string( $key ) ? $key : 'mw-diff-tool';
786  $element = Html::rawElement(
787  'span',
788  [ 'class' => $toolClass ],
789  $this->msg( 'parentheses' )->rawParams( $tool )->escaped()
790  );
791  $formattedRevisionTools[] = $element;
792  }
793 
794  $newRevRecord = $this->mNewRevisionRecord;
795 
796  $newRevisionHeader = $this->getRevisionHeader( $newRevRecord, 'complete' ) .
797  ' ' . implode( ' ', $formattedRevisionTools );
798  $newChangeTags = ChangeTags::formatSummaryRow( $this->mNewTags, 'diff', $this->getContext() );
799  $newRevComment = Linker::revComment( $newRevRecord, !$diffOnly, !$this->unhide );
800 
801  if ( $newRevComment === '' ) {
802  $defaultComment = $this->msg( 'changeslist-nocomment' )->escaped();
803  $newRevComment = "<span class=\"comment mw-comment-none\">$defaultComment</span>";
804  }
805 
806  $newHeader = '<div id="mw-diff-ntitle1"><strong>' . $newRevisionHeader . '</strong></div>' .
807  '<div id="mw-diff-ntitle2">' . Linker::revUserTools( $newRevRecord, !$this->unhide ) .
808  " $rollback</div>" .
809  '<div id="mw-diff-ntitle3">' . $newminor . $newRevComment . $rdel . '</div>' .
810  '<div id="mw-diff-ntitle5">' . $newChangeTags[0] . '</div>' .
811  '<div id="mw-diff-ntitle4">' . $nextlink . $this->markPatrolledLink() . '</div>';
812 
813  // Allow extensions to change the $newHeader variable
814  $this->hookRunner->onDifferenceEngineNewHeader( $this, $newHeader,
815  $formattedRevisionTools, $nextlink, $rollback, $newminor, $diffOnly,
816  $rdel, $this->unhide );
817 
818  # If the diff cannot be shown due to a deleted revision, then output
819  # the diff header and links to unhide (if available)...
820  if ( $this->shouldBeHiddenFromUser( $this->getAuthority() ) ) {
821  $this->showDiffStyle();
822  $multi = $this->getMultiNotice();
823  $out->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
824  if ( !$allowed ) {
825  # Give explanation for why revision is not visible
826  $msg = [ $suppressed ? 'rev-suppressed-no-diff' : 'rev-deleted-no-diff' ];
827  } else {
828  # Give explanation and add a link to view the diff...
829  $query = $this->getRequest()->appendQueryValue( 'unhide', '1' );
830  $msg = [
831  $suppressed ? 'rev-suppressed-unhide-diff' : 'rev-deleted-unhide-diff',
832  $this->getTitle()->getFullURL( $query )
833  ];
834  }
835  $out->addHtml( Html::warningBox( $this->msg( ...$msg )->parse(), 'plainlinks' ) );
836  # Otherwise, output a regular diff...
837  } else {
838  # Add deletion notice if the user is viewing deleted content
839  $notice = '';
840  if ( $deleted ) {
841  $msg = $suppressed ? 'rev-suppressed-diff-view' : 'rev-deleted-diff-view';
842  $notice = Html::warningBox( $this->msg( $msg )->parse(), 'plainlinks' );
843  }
844  $this->showDiff( $oldHeader, $newHeader, $notice );
845  if ( !$diffOnly ) {
846  $this->renderNewRevision();
847  }
848  }
849  }
850 
861  public function markPatrolledLink() {
862  if ( $this->mMarkPatrolledLink === null ) {
863  $linkInfo = $this->getMarkPatrolledLinkInfo();
864  // If false, there is no patrol link needed/allowed
865  if ( !$linkInfo || !$this->mNewPage ) {
866  $this->mMarkPatrolledLink = '';
867  } else {
868  $this->mMarkPatrolledLink = ' <span class="patrollink" data-mw="interface">[' .
869  $this->linkRenderer->makeKnownLink(
870  $this->mNewPage,
871  $this->msg( 'markaspatrolleddiff' )->text(),
872  [],
873  [
874  'action' => 'markpatrolled',
875  'rcid' => $linkInfo['rcid'],
876  ]
877  ) . ']</span>';
878  // Allow extensions to change the markpatrolled link
879  $this->hookRunner->onDifferenceEngineMarkPatrolledLink( $this,
880  $this->mMarkPatrolledLink, $linkInfo['rcid'] );
881  }
882  }
884  }
885 
893  protected function getMarkPatrolledLinkInfo() {
894  $user = $this->getUser();
895  $config = $this->getConfig();
896 
897  // Prepare a change patrol link, if applicable
898  if (
899  // Is patrolling enabled and the user allowed to?
900  $config->get( MainConfigNames::UseRCPatrol ) &&
901  $this->mNewPage &&
902  $this->getAuthority()->probablyCan( 'patrol', $this->mNewPage ) &&
903  // Only do this if the revision isn't more than 6 hours older
904  // than the Max RC age (6h because the RC might not be cleaned out regularly)
905  RecentChange::isInRCLifespan( $this->mNewRevisionRecord->getTimestamp(), 21600 )
906  ) {
907  // Look for an unpatrolled change corresponding to this diff
908  $change = RecentChange::newFromConds(
909  [
910  'rc_this_oldid' => $this->mNewid,
911  'rc_patrolled' => RecentChange::PRC_UNPATROLLED
912  ],
913  __METHOD__
914  );
915 
916  if ( $change && !$change->getPerformerIdentity()->equals( $user ) ) {
917  $rcid = $change->getAttribute( 'rc_id' );
918  } else {
919  // None found or the page has been created by the current user.
920  // If the user could patrol this it already would be patrolled
921  $rcid = 0;
922  }
923 
924  // Allow extensions to possibly change the rcid here
925  // For example the rcid might be set to zero due to the user
926  // being the same as the performer of the change but an extension
927  // might still want to show it under certain conditions
928  $this->hookRunner->onDifferenceEngineMarkPatrolledRCID( $rcid, $this, $change, $user );
929 
930  // Build the link
931  if ( $rcid ) {
932  $this->getOutput()->setPreventClickjacking( true );
933  if ( $this->getAuthority()->isAllowed( 'writeapi' ) ) {
934  $this->getOutput()->addModules( 'mediawiki.misc-authed-curate' );
935  }
936 
937  return [ 'rcid' => $rcid ];
938  }
939  }
940 
941  // No mark as patrolled link applicable
942  return false;
943  }
944 
950  private function revisionDeleteLink( RevisionRecord $revRecord ) {
951  $link = Linker::getRevDeleteLink(
952  $this->getAuthority(),
953  $revRecord,
954  $revRecord->getPageAsLinkTarget()
955  );
956  if ( $link !== '' ) {
957  $link = "\u{00A0}\u{00A0}\u{00A0}" . $link . ' ';
958  }
959 
960  return $link;
961  }
962 
968  public function renderNewRevision() {
969  if ( $this->isContentOverridden ) {
970  // The code below only works with a RevisionRecord object. We could construct a
971  // fake RevisionRecord (here or in setContent), but since this does not seem
972  // needed at the moment, we'll just fail for now.
973  throw new LogicException(
974  __METHOD__
975  . ' is not supported after calling setContent(). Use setRevisions() instead.'
976  );
977  }
978 
979  $out = $this->getOutput();
980  $revHeader = $this->getRevisionHeader( $this->mNewRevisionRecord );
981  # Add "current version as of X" title
982  $out->addHTML( "<hr class='diff-hr' id='mw-oldid' />
983  <h2 class='diff-currentversion-title'>{$revHeader}</h2>\n" );
984  # Page content may be handled by a hooked call instead...
985  if ( $this->hookRunner->onArticleContentOnDiff( $this, $out ) ) {
986  $this->loadNewText();
987  if ( !$this->mNewPage ) {
988  // New revision is unsaved; bail out.
989  // TODO in theory rendering the new revision is a meaningful thing to do
990  // even if it's unsaved, but a lot of untangling is required to do it safely.
991  return;
992  }
993 
994  $out->setRevisionId( $this->mNewid );
995  $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
996  $out->setArticleFlag( true );
997 
998  if ( !$this->hookRunner->onArticleRevisionViewCustom(
999  $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1000  ) {
1001  // Handled by extension
1002  // NOTE: sync with hooks called in Article::view()
1003  } else {
1004  // Normal page
1005  if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1006  // If the Title stored in the context is the same as the one
1007  // of the new revision, we can use its associated WikiPage
1008  // object.
1009  $wikiPage = $this->getWikiPage();
1010  } else {
1011  // Otherwise we need to create our own WikiPage object
1012  $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1013  }
1014 
1015  $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRevisionRecord );
1016 
1017  # WikiPage::getParserOutput() should not return false, but just in case
1018  if ( $parserOutput ) {
1019  // Allow extensions to change parser output here
1020  if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1021  $this, $out, $parserOutput, $wikiPage )
1022  ) {
1023  $out->addParserOutput( $parserOutput, [
1024  'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1025  && $this->getAuthority()->probablyCan(
1026  'edit',
1027  $this->mNewRevisionRecord->getPage()
1028  ),
1029  'absoluteURLs' => $this->slotDiffOptions['expand-url'] ?? false
1030  ] );
1031  }
1032  }
1033  }
1034  }
1035 
1036  // Allow extensions to optionally not show the final patrolled link
1037  if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1038  # Add redundant patrol link on bottom...
1039  $out->addHTML( $this->markPatrolledLink() );
1040  }
1041  }
1042 
1049  protected function getParserOutput( WikiPage $page, RevisionRecord $revRecord ) {
1050  if ( !$revRecord->getId() ) {
1051  // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
1052  // the current revision, so fail instead. If need be, WikiPage::getParserOutput
1053  // could be made to accept a RevisionRecord instead of the id.
1054  return false;
1055  }
1056 
1057  $parserOptions = $page->makeParserOptions( $this->getContext() );
1058  return $page->getParserOutput( $parserOptions, $revRecord->getId() );
1059  }
1060 
1071  public function showDiff( $otitle, $ntitle, $notice = '' ) {
1072  // Allow extensions to affect the output here
1073  $this->hookRunner->onDifferenceEngineShowDiff( $this );
1074 
1075  $diff = $this->getDiff( $otitle, $ntitle, $notice );
1076  if ( $diff === false ) {
1077  $this->showMissingRevision();
1078  return false;
1079  }
1080 
1081  $this->showDiffStyle();
1082  if ( $this->slotDiffOptions['expand-url'] ?? false ) {
1083  $diff = Linker::expandLocalLinks( $diff );
1084  }
1085  $this->getOutput()->addHTML( $diff );
1086  return true;
1087  }
1088 
1092  public function showDiffStyle() {
1093  if ( !$this->isSlotDiffRenderer ) {
1094  $this->getOutput()->addModules( 'mediawiki.diff' );
1095  $this->getOutput()->addModuleStyles( [
1096  'mediawiki.interface.helpers.styles',
1097  'mediawiki.diff.styles'
1098  ] );
1099  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1100  $slotDiffRenderer->addModules( $this->getOutput() );
1101  }
1102  }
1103  }
1104 
1114  public function getDiff( $otitle, $ntitle, $notice = '' ) {
1115  $body = $this->getDiffBody();
1116  if ( $body === false ) {
1117  return false;
1118  }
1119 
1120  $multi = $this->getMultiNotice();
1121  // Display a message when the diff is empty
1122  if ( $body === '' ) {
1123  $notice .= '<div class="mw-diff-empty">' .
1124  $this->msg( 'diff-empty' )->parse() .
1125  "</div>\n";
1126  }
1127 
1128  return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1129  }
1130 
1136  public function getDiffBody() {
1137  $this->mCacheHit = true;
1138  // Check if the diff should be hidden from this user
1139  if ( !$this->isContentOverridden ) {
1140  if ( !$this->loadRevisionData() ) {
1141  return false;
1142  } elseif ( $this->mOldRevisionRecord &&
1143  !$this->mOldRevisionRecord->userCan(
1144  RevisionRecord::DELETED_TEXT,
1145  $this->getAuthority()
1146  )
1147  ) {
1148  return false;
1149  } elseif ( $this->mNewRevisionRecord &&
1150  !$this->mNewRevisionRecord->userCan(
1151  RevisionRecord::DELETED_TEXT,
1152  $this->getAuthority()
1153  ) ) {
1154  return false;
1155  }
1156  // Short-circuit
1157  if ( $this->mOldRevisionRecord === false || (
1158  $this->mOldRevisionRecord &&
1159  $this->mNewRevisionRecord &&
1160  $this->mOldRevisionRecord->getId() &&
1161  $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1162  ) ) {
1163  if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1164  return '';
1165  }
1166  }
1167  }
1168 
1169  // Cacheable?
1170  $key = false;
1171  $services = MediaWikiServices::getInstance();
1172  $cache = $services->getMainWANObjectCache();
1173  $stats = $services->getStatsdDataFactory();
1174  if ( $this->mOldid && $this->mNewid ) {
1175  // Check if subclass is still using the old way
1176  // for backwards-compatibility
1177  $key = $this->getDiffBodyCacheKey();
1178  if ( $key === null ) {
1179  $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1180  }
1181 
1182  // Try cache
1183  if ( !$this->mRefreshCache ) {
1184  $difftext = $cache->get( $key );
1185  if ( is_string( $difftext ) ) {
1186  $stats->updateCount( 'diff_cache.hit', 1 );
1187  $difftext = $this->localiseDiff( $difftext );
1188  $difftext .= "\n<!-- diff cache key $key -->\n";
1189 
1190  return $difftext;
1191  }
1192  } // don't try to load but save the result
1193  }
1194  $this->mCacheHit = false;
1195 
1196  // Loadtext is permission safe, this just clears out the diff
1197  if ( !$this->loadText() ) {
1198  return false;
1199  }
1200 
1201  $difftext = '';
1202  // We've checked for revdelete at the beginning of this method; it's OK to ignore
1203  // read permissions here.
1204  $slotContents = $this->getSlotContents();
1205  foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1206  $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1207  $slotContents[$role]['new'] );
1208  if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1209  // FIXME: ask SlotRoleHandler::getSlotNameMessage
1210  $slotTitle = $role;
1211  $difftext .= $this->getSlotHeader( $slotTitle );
1212  }
1213  $difftext .= $slotDiff;
1214  }
1215 
1216  // Save to cache for 7 days
1217  if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1218  $stats->updateCount( 'diff_cache.uncacheable', 1 );
1219  } elseif ( $key !== false ) {
1220  $stats->updateCount( 'diff_cache.miss', 1 );
1221  $cache->set( $key, $difftext, 7 * 86400 );
1222  } else {
1223  $stats->updateCount( 'diff_cache.uncacheable', 1 );
1224  }
1225  // localise line numbers and title attribute text
1226  $difftext = $this->localiseDiff( $difftext );
1227 
1228  return $difftext;
1229  }
1230 
1237  public function getDiffBodyForRole( $role ) {
1238  $diffRenderers = $this->getSlotDiffRenderers();
1239  if ( !isset( $diffRenderers[$role] ) ) {
1240  return false;
1241  }
1242 
1243  $slotContents = $this->getSlotContents();
1244  $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1245  $slotContents[$role]['new'] );
1246  if ( !$slotDiff ) {
1247  return false;
1248  }
1249 
1250  if ( $role !== SlotRecord::MAIN ) {
1251  // TODO use human-readable role name at least
1252  $slotTitle = $role;
1253  $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1254  }
1255 
1256  return $this->localiseDiff( $slotDiff );
1257  }
1258 
1266  protected function getSlotHeader( $headerText ) {
1267  // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1268  $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1269  $userLang = $this->getLanguage()->getHtmlCode();
1270  return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1271  Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1272  }
1273 
1283  protected function getDiffBodyCacheKey() {
1284  return null;
1285  }
1286 
1300  protected function getDiffBodyCacheKeyParams() {
1301  if ( !$this->mOldid || !$this->mNewid ) {
1302  throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1303  }
1304 
1305  $engine = $this->getEngine();
1306  $params = [
1307  'diff',
1308  $engine === 'php' ? false : $engine, // Back compat
1310  "old-{$this->mOldid}",
1311  "rev-{$this->mNewid}"
1312  ];
1313 
1314  if ( $engine === 'wikidiff2' ) {
1315  $params[] = phpversion( 'wikidiff2' );
1316  }
1317 
1318  if ( !$this->isSlotDiffRenderer ) {
1319  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1320  $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1321  }
1322  }
1323 
1324  return $params;
1325  }
1326 
1334  public function getExtraCacheKeys() {
1335  // This method is called when the DifferenceEngine is used for a slot diff. We only care
1336  // about special things, not the revision IDs, which are added to the cache key by the
1337  // page-level DifferenceEngine, and which might not have a valid value for this object.
1338  $this->mOldid = 123456789;
1339  $this->mNewid = 987654321;
1340 
1341  // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1342  $cacheString = $this->getDiffBodyCacheKey();
1343  if ( $cacheString ) {
1344  return [ $cacheString ];
1345  }
1346 
1347  $params = $this->getDiffBodyCacheKeyParams();
1348 
1349  // Try to get rid of the standard keys to keep the cache key human-readable:
1350  // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1351  // the child class includes the same keys, drop them.
1352  // Uses an obscure PHP feature where static calls to non-static methods are allowed
1353  // as long as we are already in a non-static method of the same class, and the call context
1354  // ($this) will be inherited.
1355  // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1356  $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1357  if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1358  $params = array_slice( $params, count( $standardParams ) );
1359  }
1360 
1361  return $params;
1362  }
1363 
1368  public function setSlotDiffOptions( $options ) {
1369  $this->slotDiffOptions = $options;
1370  }
1371 
1385  public function generateContentDiffBody( Content $old, Content $new ) {
1386  $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1387  if (
1388  $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1389  && $this->isSlotDiffRenderer
1390  ) {
1391  // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1392  // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1393  // This will happen when a content model has no custom slot diff renderer, it does have
1394  // a custom difference engine, but that does not override this method.
1395  throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1396  . 'Please use a SlotDiffRenderer.' );
1397  }
1398  return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1399  }
1400 
1413  public function generateTextDiffBody( $otext, $ntext ) {
1414  $slotDiffRenderer = $this->contentHandlerFactory
1415  ->getContentHandler( CONTENT_MODEL_TEXT )
1416  ->getSlotDiffRenderer( $this->getContext() );
1417  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1418  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1419  // This is too unlikely to happen to bother handling properly.
1420  throw new Exception( 'The slot diff renderer for text content should be a '
1421  . 'TextSlotDiffRenderer subclass' );
1422  }
1423  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1424  }
1425 
1432  public static function getEngine() {
1433  $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1434  ->get( MainConfigNames::DiffEngine );
1435  $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1436  ->get( MainConfigNames::ExternalDiffEngine );
1437 
1438  if ( $diffEngine === null ) {
1439  $engines = [ 'external', 'wikidiff2', 'php' ];
1440  } else {
1441  $engines = [ $diffEngine ];
1442  }
1443 
1444  $failureReason = null;
1445  foreach ( $engines as $engine ) {
1446  switch ( $engine ) {
1447  case 'external':
1448  if ( is_string( $externalDiffEngine ) ) {
1449  if ( is_executable( $externalDiffEngine ) ) {
1450  return $externalDiffEngine;
1451  }
1452  $failureReason = 'ExternalDiffEngine config points to a non-executable';
1453  if ( $diffEngine === null ) {
1454  wfDebug( "$failureReason, ignoring" );
1455  }
1456  } else {
1457  $failureReason = 'ExternalDiffEngine config is set to a non-string value';
1458  if ( $diffEngine === null && $externalDiffEngine ) {
1459  wfWarn( "$failureReason, ignoring" );
1460  }
1461  }
1462  break;
1463 
1464  case 'wikidiff2':
1465  if ( function_exists( 'wikidiff2_do_diff' ) ) {
1466  return 'wikidiff2';
1467  }
1468  $failureReason = 'wikidiff2 is not available';
1469  break;
1470 
1471  case 'php':
1472  // Always available.
1473  return 'php';
1474 
1475  default:
1476  throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
1477  }
1478  }
1479  throw new UnexpectedValueException( "Cannot use diff engine '$engine': $failureReason" );
1480  }
1481 
1494  protected function textDiff( $otext, $ntext ) {
1495  $slotDiffRenderer = $this->contentHandlerFactory
1496  ->getContentHandler( CONTENT_MODEL_TEXT )
1497  ->getSlotDiffRenderer( $this->getContext() );
1498  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1499  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1500  // This is too unlikely to happen to bother handling properly.
1501  throw new Exception( 'The slot diff renderer for text content should be a '
1502  . 'TextSlotDiffRenderer subclass' );
1503  }
1504  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1505  }
1506 
1515  protected function debug( $generator = "internal" ) {
1516  if ( !$this->enableDebugComment ) {
1517  return '';
1518  }
1519  $data = [ $generator ];
1520  if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) {
1521  $data[] = wfHostname();
1522  }
1523  $data[] = wfTimestamp( TS_DB );
1524 
1525  return "<!-- diff generator: " .
1526  implode( " ", array_map( "htmlspecialchars", $data ) ) .
1527  " -->\n";
1528  }
1529 
1533  private function getDebugString() {
1534  $engine = self::getEngine();
1535  if ( $engine === 'wikidiff2' ) {
1536  return $this->debug( 'wikidiff2' );
1537  } elseif ( $engine === 'php' ) {
1538  return $this->debug( 'native PHP' );
1539  } else {
1540  return $this->debug( "external $engine" );
1541  }
1542  }
1543 
1550  private function localiseDiff( $text ) {
1551  $text = $this->localiseLineNumbers( $text );
1552  if ( $this->getEngine() === 'wikidiff2' &&
1553  version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1554  ) {
1555  $text = $this->addLocalisedTitleTooltips( $text );
1556  }
1557  return $text;
1558  }
1559 
1567  public function localiseLineNumbers( $text ) {
1568  return preg_replace_callback(
1569  '/<!--LINE (\d+)-->/',
1570  function ( array $matches ) {
1571  if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1572  return '';
1573  }
1574  return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1575  },
1576  $text
1577  );
1578  }
1579 
1586  private function addLocalisedTitleTooltips( $text ) {
1587  return preg_replace_callback(
1588  '/class="mw-diff-movedpara-(left|right)"/',
1589  function ( array $matches ) {
1590  $key = $matches[1] === 'right' ?
1591  'diff-paragraph-moved-toold' :
1592  'diff-paragraph-moved-tonew';
1593  return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1594  },
1595  $text
1596  );
1597  }
1598 
1604  public function getMultiNotice() {
1605  // The notice only make sense if we are diffing two saved revisions of the same page.
1606  if (
1607  !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1608  || !$this->mOldPage || !$this->mNewPage
1609  || !$this->mOldPage->equals( $this->mNewPage )
1610  || $this->mOldRevisionRecord->getId() === null
1611  || $this->mNewRevisionRecord->getId() === null
1612  // (T237709) Deleted revs might have different page IDs
1613  || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1614  || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1615  ) {
1616  return '';
1617  }
1618 
1619  if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1620  $oldRevRecord = $this->mNewRevisionRecord; // flip
1621  $newRevRecord = $this->mOldRevisionRecord; // flip
1622  } else { // normal case
1623  $oldRevRecord = $this->mOldRevisionRecord;
1624  $newRevRecord = $this->mNewRevisionRecord;
1625  }
1626 
1627  // Don't show the notice if too many rows must be scanned
1628  // @todo show some special message for that case
1629  $nEdits = $this->revisionStore->countRevisionsBetween(
1630  $this->mNewPage->getArticleID(),
1631  $oldRevRecord,
1632  $newRevRecord,
1633  1000
1634  );
1635  if ( $nEdits > 0 && $nEdits <= 1000 ) {
1636  $limit = 100; // use diff-multi-manyusers if too many users
1637  try {
1638  $users = $this->revisionStore->getAuthorsBetween(
1639  $this->mNewPage->getArticleID(),
1640  $oldRevRecord,
1641  $newRevRecord,
1642  null,
1643  $limit
1644  );
1645  $numUsers = count( $users );
1646 
1647  $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1648  $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1649  if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1650  $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1651  }
1652  } catch ( InvalidArgumentException $e ) {
1653  $numUsers = 0;
1654  }
1655 
1656  return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1657  }
1658 
1659  return '';
1660  }
1661 
1671  public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1672  if ( $numUsers === 0 ) {
1673  $msg = 'diff-multi-sameuser';
1674  } elseif ( $numUsers > $limit ) {
1675  $msg = 'diff-multi-manyusers';
1676  $numUsers = $limit;
1677  } else {
1678  $msg = 'diff-multi-otherusers';
1679  }
1680 
1681  return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1682  }
1683 
1688  private function userCanEdit( RevisionRecord $revRecord ) {
1689  if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1690  return false;
1691  }
1692 
1693  return true;
1694  }
1695 
1705  public function getRevisionHeader( RevisionRecord $rev, $complete = '' ) {
1706  $lang = $this->getLanguage();
1707  $user = $this->getUser();
1708  $revtimestamp = $rev->getTimestamp();
1709  $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1710  $dateofrev = $lang->userDate( $revtimestamp, $user );
1711  $timeofrev = $lang->userTime( $revtimestamp, $user );
1712 
1713  $header = $this->msg(
1714  $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1715  $timestamp,
1716  $dateofrev,
1717  $timeofrev
1718  );
1719 
1720  if ( $complete !== 'complete' ) {
1721  return $header->escaped();
1722  }
1723 
1724  $title = $rev->getPageAsLinkTarget();
1725 
1726  $header = $this->linkRenderer->makeKnownLink( $title, $header->text(), [],
1727  [ 'oldid' => $rev->getId() ] );
1728 
1729  if ( $this->userCanEdit( $rev ) ) {
1730  $editQuery = [ 'action' => 'edit' ];
1731  if ( !$rev->isCurrent() ) {
1732  $editQuery['oldid'] = $rev->getId();
1733  }
1734 
1735  $key = $this->getAuthority()->probablyCan( 'edit', $rev->getPage() ) ? 'editold' : 'viewsourceold';
1736  $msg = $this->msg( $key )->text();
1737  $editLink = $this->msg( 'parentheses' )->rawParams(
1738  $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery ) )->escaped();
1739  $header .= ' ' . Html::rawElement(
1740  'span',
1741  [ 'class' => 'mw-diff-edit' ],
1742  $editLink
1743  );
1744  if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1746  'span',
1747  [ 'class' => Linker::getRevisionDeletedClass( $rev ) ],
1748  $header
1749  );
1750  }
1751  } else {
1752  $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1753  }
1754 
1755  return $header;
1756  }
1757 
1770  public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1771  // shared.css sets diff in interface language/dir, but the actual content
1772  // is often in a different language, mostly the page content language/dir
1773  $header = Html::openElement( 'table', [
1774  'class' => [
1775  'diff',
1776  'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1777  'diff-editfont-' . $this->userOptionsLookup->getOption(
1778  $this->getUser(),
1779  'editfont'
1780  )
1781  ],
1782  'data-mw' => 'interface',
1783  ] );
1784  $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1785 
1786  if ( !$diff && !$otitle ) {
1787  $header .= "
1788  <tr class=\"diff-title\" lang=\"{$userLang}\">
1789  <td class=\"diff-ntitle\">{$ntitle}</td>
1790  </tr>";
1791  $multiColspan = 1;
1792  } else {
1793  if ( $diff ) { // Safari/Chrome show broken output if cols not used
1794  $header .= "
1795  <col class=\"diff-marker\" />
1796  <col class=\"diff-content\" />
1797  <col class=\"diff-marker\" />
1798  <col class=\"diff-content\" />";
1799  $colspan = 2;
1800  $multiColspan = 4;
1801  } else {
1802  $colspan = 1;
1803  $multiColspan = 2;
1804  }
1805  if ( $otitle || $ntitle ) {
1806  // FIXME Hardcoding values from TableDiffFormatter.
1807  $deletedClass = 'diff-side-deleted';
1808  $addedClass = 'diff-side-added';
1809  $header .= "
1810  <tr class=\"diff-title\" lang=\"{$userLang}\">
1811  <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
1812  <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
1813  </tr>";
1814  }
1815  }
1816 
1817  if ( $multi != '' ) {
1818  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1819  "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1820  }
1821  if ( $notice != '' ) {
1822  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1823  "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1824  }
1825 
1826  return $header . $diff . "</table>";
1827  }
1828 
1836  public function setContent( Content $oldContent, Content $newContent ) {
1837  $this->mOldContent = $oldContent;
1838  $this->mNewContent = $newContent;
1839 
1840  $this->mTextLoaded = 2;
1841  $this->mRevisionsLoaded = true;
1842  $this->isContentOverridden = true;
1843  $this->slotDiffRenderers = null;
1844  }
1845 
1851  public function setRevisions(
1852  ?RevisionRecord $oldRevision, RevisionRecord $newRevision
1853  ) {
1854  if ( $oldRevision ) {
1855  $this->mOldRevisionRecord = $oldRevision;
1856  $this->mOldid = $oldRevision->getId();
1857  $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1858  // This method is meant for edit diffs and such so there is no reason to provide a
1859  // revision that's not readable to the user, but check it just in case.
1860  $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1861  RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
1862  } else {
1863  $this->mOldPage = null;
1864  $this->mOldRevisionRecord = $this->mOldid = false;
1865  }
1866  $this->mNewRevisionRecord = $newRevision;
1867  $this->mNewid = $newRevision->getId();
1868  $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1869  $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1870  RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
1871 
1872  $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1873  $this->mTextLoaded = $oldRevision ? 2 : 1;
1874  $this->isContentOverridden = false;
1875  $this->slotDiffRenderers = null;
1876  }
1877 
1884  public function setTextLanguage( Language $lang ) {
1885  $this->mDiffLang = $lang;
1886  }
1887 
1900  public function mapDiffPrevNext( $old, $new ) {
1901  if ( $new === 'prev' ) {
1902  // Show diff between revision $old and the previous one. Get previous one from DB.
1903  $newid = intval( $old );
1904  $oldid = false;
1905  $newRev = $this->revisionStore->getRevisionById( $newid );
1906  if ( $newRev ) {
1907  $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
1908  if ( $oldRev ) {
1909  $oldid = $oldRev->getId();
1910  }
1911  }
1912  } elseif ( $new === 'next' ) {
1913  // Show diff between revision $old and the next one. Get next one from DB.
1914  $oldid = intval( $old );
1915  $newid = false;
1916  $oldRev = $this->revisionStore->getRevisionById( $oldid );
1917  if ( $oldRev ) {
1918  $newRev = $this->revisionStore->getNextRevision( $oldRev );
1919  if ( $newRev ) {
1920  $newid = $newRev->getId();
1921  }
1922  }
1923  } else {
1924  $oldid = intval( $old );
1925  $newid = intval( $new );
1926  }
1927 
1928  // @phan-suppress-next-line PhanTypeMismatchReturn getId does not return null here
1929  return [ $oldid, $newid ];
1930  }
1931 
1932  private function loadRevisionIds() {
1933  if ( $this->mRevisionsIdsLoaded ) {
1934  return;
1935  }
1936 
1937  $this->mRevisionsIdsLoaded = true;
1938 
1939  $old = $this->mOldid;
1940  $new = $this->mNewid;
1941 
1942  list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1943  if ( $new === 'next' && $this->mNewid === false ) {
1944  # if no result, NewId points to the newest old revision. The only newer
1945  # revision is cur, which is "0".
1946  $this->mNewid = 0;
1947  }
1948 
1949  $this->hookRunner->onNewDifferenceEngine(
1950  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
1951  $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
1952  }
1953 
1967  public function loadRevisionData() {
1968  if ( $this->mRevisionsLoaded ) {
1969  return $this->isContentOverridden ||
1970  ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
1971  }
1972 
1973  // Whether it succeeds or fails, we don't want to try again
1974  $this->mRevisionsLoaded = true;
1975 
1976  $this->loadRevisionIds();
1977 
1978  // Load the new RevisionRecord object
1979  if ( $this->mNewid ) {
1980  $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
1981  } else {
1982  $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
1983  }
1984 
1985  if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
1986  return false;
1987  }
1988 
1989  // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
1990  $this->mNewid = $this->mNewRevisionRecord->getId();
1991  $this->mNewPage = $this->mNewid ?
1992  Title::newFromLinkTarget( $this->mNewRevisionRecord->getPageAsLinkTarget() ) :
1993  null;
1994 
1995  // Load the old RevisionRecord object
1996  $this->mOldRevisionRecord = false;
1997  if ( $this->mOldid ) {
1998  $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
1999  } elseif ( $this->mOldid === 0 ) {
2000  $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2001  // No previous revision; mark to show as first-version only.
2002  $this->mOldid = $revRecord ? $revRecord->getId() : false;
2003  $this->mOldRevisionRecord = $revRecord ?? false;
2004  } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2005 
2006  if ( $this->mOldRevisionRecord === null ) {
2007  return false;
2008  }
2009 
2010  if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2011  $this->mOldPage = Title::newFromLinkTarget(
2012  $this->mOldRevisionRecord->getPageAsLinkTarget()
2013  );
2014  } else {
2015  $this->mOldPage = null;
2016  }
2017 
2018  // Load tags information for both revisions
2019  $dbr = wfGetDB( DB_REPLICA );
2020  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2021  if ( $this->mOldid !== false ) {
2022  $tagIds = $dbr->selectFieldValues(
2023  'change_tag',
2024  'ct_tag_id',
2025  [ 'ct_rev_id' => $this->mOldid ],
2026  __METHOD__
2027  );
2028  $tags = [];
2029  foreach ( $tagIds as $tagId ) {
2030  try {
2031  $tags[] = $changeTagDefStore->getName( (int)$tagId );
2032  } catch ( NameTableAccessException $exception ) {
2033  continue;
2034  }
2035  }
2036  $this->mOldTags = implode( ',', $tags );
2037  } else {
2038  $this->mOldTags = false;
2039  }
2040 
2041  $tagIds = $dbr->selectFieldValues(
2042  'change_tag',
2043  'ct_tag_id',
2044  [ 'ct_rev_id' => $this->mNewid ],
2045  __METHOD__
2046  );
2047  $tags = [];
2048  foreach ( $tagIds as $tagId ) {
2049  try {
2050  $tags[] = $changeTagDefStore->getName( (int)$tagId );
2051  } catch ( NameTableAccessException $exception ) {
2052  continue;
2053  }
2054  }
2055  $this->mNewTags = implode( ',', $tags );
2056 
2057  return true;
2058  }
2059 
2068  public function loadText() {
2069  if ( $this->mTextLoaded == 2 ) {
2070  return $this->loadRevisionData() &&
2071  ( $this->mOldRevisionRecord === false || $this->mOldContent )
2072  && $this->mNewContent;
2073  }
2074 
2075  // Whether it succeeds or fails, we don't want to try again
2076  $this->mTextLoaded = 2;
2077 
2078  if ( !$this->loadRevisionData() ) {
2079  return false;
2080  }
2081 
2082  if ( $this->mOldRevisionRecord ) {
2083  $this->mOldContent = $this->mOldRevisionRecord->getContent(
2084  SlotRecord::MAIN,
2085  RevisionRecord::FOR_THIS_USER,
2086  $this->getAuthority()
2087  );
2088  if ( $this->mOldContent === null ) {
2089  return false;
2090  }
2091  }
2092 
2093  $this->mNewContent = $this->mNewRevisionRecord->getContent(
2094  SlotRecord::MAIN,
2095  RevisionRecord::FOR_THIS_USER,
2096  $this->getAuthority()
2097  );
2098  $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2099  if ( $this->mNewContent === null ) {
2100  return false;
2101  }
2102 
2103  return true;
2104  }
2105 
2111  public function loadNewText() {
2112  if ( $this->mTextLoaded >= 1 ) {
2113  return $this->loadRevisionData();
2114  }
2115 
2116  $this->mTextLoaded = 1;
2117 
2118  if ( !$this->loadRevisionData() ) {
2119  return false;
2120  }
2121 
2122  $this->mNewContent = $this->mNewRevisionRecord->getContent(
2123  SlotRecord::MAIN,
2124  RevisionRecord::FOR_THIS_USER,
2125  $this->getAuthority()
2126  );
2127 
2128  $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2129 
2130  return true;
2131  }
2132 
2133 }
const NS_SPECIAL
Definition: Defines.php:53
const CONTENT_MODEL_TEXT
Definition: Defines.php:213
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated or implementati...
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfHostname()
Get host name of the current machine, for use in error reporting.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$matches
static formatSummaryRow( $tags, $page, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:182
static flag( $flag, IContextSource $context=null)
Make an "<abbr>" element for a given change flag.
The simplest way of implementing IContextSource is to hold a RequestContext as a member variable and ...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
IContextSource $context
getWikiPage()
Get the WikiPage object.
getContext()
Get the base IContextSource object.
setContext(IContextSource $context)
B/C adapter for turning a DifferenceEngine into a SlotDiffRenderer.
DifferenceEngine is responsible for rendering the difference between two revisions as HTML.
getDiffBodyCacheKey()
Returns the cache key for diff body text or content.
bool $enableDebugComment
Set this to true to add debug info to the HTML output.
bool $unhide
Show rev_deleted content if allowed.
bool $isContentOverridden
Was the content overridden via setContent()? If the content was overridden, most internal state (e....
getExtraCacheKeys()
Implements DifferenceEngineSlotDiffRenderer::getExtraCacheKeys().
markAsSlotDiffRenderer()
Mark this DifferenceEngine as a slot renderer (as opposed to a page renderer).
getSlotHeader( $headerText)
Get a slot header for inclusion in a diff body (as a table row).
bool $mRevisionsIdsLoaded
Have the revisions IDs been loaded.
setSlotDiffOptions( $options)
hasDeletedRevision()
Checks whether one of the given Revisions was deleted.
IContentHandlerFactory $contentHandlerFactory
int $mTextLoaded
How many text blobs have been loaded, 0, 1 or 2?
deletedIdMarker( $id)
Build a wikitext link toward a deleted revision, if viewable.
SlotDiffRenderer[] null $slotDiffRenderers
DifferenceEngine classes for the slots, keyed by role name.
getDiffBodyForRole( $role)
Get the diff table body for one slot, without header.
WikiPageFactory $wikiPageFactory
RevisionStore $revisionStore
revisionDeleteLink(RevisionRecord $revRecord)
getOldid()
Get the ID of old revision (left pane) of the diff.
setRevisions(?RevisionRecord $oldRevision, RevisionRecord $newRevision)
Use specified text instead of loading from the database.
bool $isSlotDiffRenderer
Temporary hack for B/C while slot diff related methods of DifferenceEngine are being deprecated.
generateTextDiffBody( $otext, $ntext)
Generate a diff, no caching.
loadNewText()
Load the text of the new revision, not the old one.
showDiffPage( $diffOnly=false)
loadText()
Load the text of the revisions, as well as revision data.
int string false null $mNewid
Revision ID for the new revision.
const DIFF_VERSION
Constant to indicate diff cache compatibility.
mapDiffPrevNext( $old, $new)
Maps a revision pair definition as accepted by DifferenceEngine constructor to a pair of actual integ...
getPermissionErrors(Authority $performer)
Get the permission errors associated with the revisions for the current diff.
Content null $mNewContent
getDiffBody()
Get the diff table body, without header.
getTitle()
1.18 Stability: stableto override Title|null
string false null $mOldTags
Change tags of old revision or null if it does not exist / is not saved.
getParserOutput(WikiPage $page, RevisionRecord $revRecord)
loadRevisionData()
Load revision metadata for the specified revisions.
UserOptionsLookup $userOptionsLookup
static getEngine()
Process DiffEngine config and get a sensible, usable engine.
bool $mRevisionsLoaded
Have the revisions been loaded.
Content null $mOldContent
getNewRevision()
Get the right side of the diff.
showDiff( $otitle, $ntitle, $notice='')
Get the diff text, send it to the OutputPage object Returns false if the diff could not be generated,...
localiseLineNumbers( $text)
Replace line numbers with the text in the user's language.
getSlotContents()
Get the old and new content objects for all slots.
string $mMarkPatrolledLink
Link to action=markpatrolled.
array $slotDiffOptions
A set of options that will be passed to the SlotDiffRenderer upon creation.
deletedLink( $id)
Look up a special:Undelete link to the given deleted revision id, as a workaround for being unable to...
bool $mReducedLineNumbers
If true, line X is not displayed when X is 1, for example to increase readability and conserve space ...
__construct( $context=null, $old=0, $new=0, $rcid=0, $refreshCache=false, $unhide=false)
#-
Title null $mNewPage
Title of new revision or null if the new revision does not exist or does not belong to a page.
bool $mCacheHit
Was the diff fetched from cache?
getMultiNotice()
If there are revisions between the ones being compared, return a note saying so.
isUserAllowedToSeeRevisions(Authority $performer)
Checks whether the current user has permission for accessing the revisions of the diff.
int false null $mOldid
Revision ID for the old revision.
debug( $generator="internal")
Generate a debug comment indicating diff generating time, server node, and generator backend.
addHeader( $diff, $otitle, $ntitle, $multi='', $notice='')
Add the header to a diff body.
bool $mRefreshCache
Refresh the diff cache.
string null $mNewTags
Change tags of new revision or null if it does not exist / is not saved.
LinkRenderer $linkRenderer
getDiffBodyCacheKeyParams()
Get the cache key parameters.
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
RevisionRecord null $mNewRevisionRecord
New revision (right pane).
getNewid()
Get the ID of new revision (right pane) of the diff.
renderNewRevision()
Show the new revision of the page.
addLocalisedTitleTooltips( $text)
Add title attributes for tooltips on moved paragraph indicators.
setContent(Content $oldContent, Content $newContent)
Use specified text instead of loading from the database.
setTextLanguage(Language $lang)
Set the language in which the diff text is written.
generateContentDiffBody(Content $old, Content $new)
Generate a diff, no caching.
localiseDiff( $text)
Localise diff output.
shouldBeHiddenFromUser(Authority $performer)
Checks whether the diff should be hidden from the current user This is based on whether the user is a...
getRevisionHeader(RevisionRecord $rev, $complete='')
Get a header for a specified revision.
getMarkPatrolledLinkInfo()
Returns an array of meta data needed to build a "mark as patrolled" link and adds a JS module to the ...
setReducedLineNumbers( $value=true)
Set reduced line numbers mode.
RevisionRecord null false $mOldRevisionRecord
Old revision (left pane).
textDiff( $otext, $ntext)
Generates diff, to be wrapped internally in a logging/instrumentation.
static intermediateEditsMsg( $numEdits, $numUsers, $limit)
Get a notice about how many intermediate edits and users there are.
Title null $mOldPage
Title of old revision or null if the old revision does not exist or does not belong to a page.
getDiffLang()
Get the language of the difference engine, defaults to page content language.
showDiffStyle()
Add style sheets for diff display.
markPatrolledLink()
Build a link to mark a change as patrolled.
hasSuppressedRevision()
Checks whether one of the given Revisions was suppressed.
getOldRevision()
Get the left side of the diff.
userCanEdit(RevisionRecord $revRecord)
static element( $element, $attribs=[], $contents='')
Identical to rawElement(), but HTML-escapes $contents (like Xml::element()).
Definition: Html.php:236
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:214
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:775
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:256
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:45
static expandLocalLinks(string $html)
Helper function to expand local links.
Definition: Linker.php:1394
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition: Linker.php:1331
static revComment(RevisionRecord $revRecord, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition: Linker.php:1587
static getRevDeleteLink(Authority $performer, RevisionRecord $revRecord, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition: Linker.php:2139
static generateRollback(RevisionRecord $revRecord, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1814
static titleAttrib( $name, $options=null, array $msgParams=[], $localizer=null)
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
Definition: Linker.php:2066
static revUserTools(RevisionRecord $revRecord, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1351
MediaWiki exception.
Definition: MWException.php:31
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:562
Class that generates HTML anchor link elements for pages.
A class containing constants representing the names of configuration variables.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Service for creating WikiPage objects.
A StatusValue for permission errors.
Page revision base class.
getPage()
Returns the page this revision belongs to.
isCurrent()
Checks whether the revision record is a stored current revision.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getContent( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns the Content of the given slot of this revision.
getPageAsLinkTarget()
Returns the title of the page this revision is associated with as a LinkTarget object.
userCan( $field, Authority $performer)
Determine if the give authority is allowed to view a particular field of this revision,...
isDeleted( $field)
MCR migration note: this replaced Revision::isDeleted.
getId( $wikiId=self::LOCAL)
Get revision ID.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
Exception representing a failure to look up a row from a name table.
Provides access to user options.
Show an error when a user tries to do something they do not have the necessary permissions for.
const PRC_UNPATROLLED
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...
static newFromConds( $conds, $fname=__METHOD__, $dbType=DB_REPLICA)
Find the first recent change matching some specific conditions.
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,...
Renders a slot diff by doing a text diff on the native representation.
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:281
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:663
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:637
Base representation for an editable wiki page.
Definition: WikiPage.php:62
makeParserOptions( $context)
Get parser options suitable for rendering the primary article wikitext.
Definition: WikiPage.php:2021
getParserOutput(?ParserOptions $parserOptions=null, $oldid=null, $noCache=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1274
Base interface for content objects.
Definition: Content.php:35
getContentHandler()
Convenience method that returns the ContentHandler singleton for handling the content model that this...
Interface for objects which can provide a MediaWiki context on request.
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
authorizeRead(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize read access.
$cache
Definition: mcc.php:33
const DB_REPLICA
Definition: defines.php:25
$content
Definition: router.php:76
if(!isset( $args[0])) $lang
$header