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 
98  private $mOldRevisionRecord;
99 
108  private $mNewRevisionRecord;
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 
222  private $contentHandlerFactory;
223 
227  private $revisionStore;
228 
230  private $hookRunner;
231 
233  private $wikiPageFactory;
234 
236  private $userOptionsLookup;
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() {
435  return $this->mNewRevisionRecord;
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->setRevisionIsCurrent( $this->mNewRevisionRecord->isCurrent() );
996  $out->setRevisionTimestamp( $this->mNewRevisionRecord->getTimestamp() );
997  $out->setArticleFlag( true );
998 
999  if ( !$this->hookRunner->onArticleRevisionViewCustom(
1000  $this->mNewRevisionRecord, $this->mNewPage, $this->mOldid, $out )
1001  ) {
1002  // Handled by extension
1003  // NOTE: sync with hooks called in Article::view()
1004  } else {
1005  // Normal page
1006  if ( $this->getTitle()->equals( $this->mNewPage ) ) {
1007  // If the Title stored in the context is the same as the one
1008  // of the new revision, we can use its associated WikiPage
1009  // object.
1010  $wikiPage = $this->getWikiPage();
1011  } else {
1012  // Otherwise we need to create our own WikiPage object
1013  $wikiPage = $this->wikiPageFactory->newFromTitle( $this->mNewPage );
1014  }
1015 
1016  $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRevisionRecord );
1017 
1018  # WikiPage::getParserOutput() should not return false, but just in case
1019  if ( $parserOutput ) {
1020  // Allow extensions to change parser output here
1021  if ( $this->hookRunner->onDifferenceEngineRenderRevisionAddParserOutput(
1022  $this, $out, $parserOutput, $wikiPage )
1023  ) {
1024  $out->setSections( $parserOutput->getSections() );
1025  $out->addParserOutput( $parserOutput, [
1026  'enableSectionEditLinks' => $this->mNewRevisionRecord->isCurrent()
1027  && $this->getAuthority()->probablyCan(
1028  'edit',
1029  $this->mNewRevisionRecord->getPage()
1030  ),
1031  'absoluteURLs' => $this->slotDiffOptions['expand-url'] ?? false
1032  ] );
1033  }
1034  }
1035  }
1036  }
1037 
1038  // Allow extensions to optionally not show the final patrolled link
1039  if ( $this->hookRunner->onDifferenceEngineRenderRevisionShowFinalPatrolLink() ) {
1040  # Add redundant patrol link on bottom...
1041  $out->addHTML( $this->markPatrolledLink() );
1042  }
1043  }
1044 
1051  protected function getParserOutput( WikiPage $page, RevisionRecord $revRecord ) {
1052  if ( !$revRecord->getId() ) {
1053  // WikiPage::getParserOutput wants a revision ID. Passing 0 will incorrectly show
1054  // the current revision, so fail instead. If need be, WikiPage::getParserOutput
1055  // could be made to accept a RevisionRecord instead of the id.
1056  return false;
1057  }
1058 
1059  $parserOptions = $page->makeParserOptions( $this->getContext() );
1060  return $page->getParserOutput( $parserOptions, $revRecord->getId() );
1061  }
1062 
1073  public function showDiff( $otitle, $ntitle, $notice = '' ) {
1074  // Allow extensions to affect the output here
1075  $this->hookRunner->onDifferenceEngineShowDiff( $this );
1076 
1077  $diff = $this->getDiff( $otitle, $ntitle, $notice );
1078  if ( $diff === false ) {
1079  $this->showMissingRevision();
1080  return false;
1081  }
1082 
1083  $this->showDiffStyle();
1084  if ( $this->slotDiffOptions['expand-url'] ?? false ) {
1085  $diff = Linker::expandLocalLinks( $diff );
1086  }
1087  $this->getOutput()->addHTML( $diff );
1088  return true;
1089  }
1090 
1094  public function showDiffStyle() {
1095  if ( !$this->isSlotDiffRenderer ) {
1096  $this->getOutput()->addModules( 'mediawiki.diff' );
1097  $this->getOutput()->addModuleStyles( [
1098  'mediawiki.interface.helpers.styles',
1099  'mediawiki.diff.styles'
1100  ] );
1101  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1102  $slotDiffRenderer->addModules( $this->getOutput() );
1103  }
1104  }
1105  }
1106 
1116  public function getDiff( $otitle, $ntitle, $notice = '' ) {
1117  $body = $this->getDiffBody();
1118  if ( $body === false ) {
1119  return false;
1120  }
1121 
1122  $multi = $this->getMultiNotice();
1123  // Display a message when the diff is empty
1124  if ( $body === '' ) {
1125  $notice .= '<div class="mw-diff-empty">' .
1126  $this->msg( 'diff-empty' )->parse() .
1127  "</div>\n";
1128  }
1129 
1130  return $this->addHeader( $body, $otitle, $ntitle, $multi, $notice );
1131  }
1132 
1138  public function getDiffBody() {
1139  $this->mCacheHit = true;
1140  // Check if the diff should be hidden from this user
1141  if ( !$this->isContentOverridden ) {
1142  if ( !$this->loadRevisionData() ) {
1143  return false;
1144  } elseif ( $this->mOldRevisionRecord &&
1145  !$this->mOldRevisionRecord->userCan(
1146  RevisionRecord::DELETED_TEXT,
1147  $this->getAuthority()
1148  )
1149  ) {
1150  return false;
1151  } elseif ( $this->mNewRevisionRecord &&
1152  !$this->mNewRevisionRecord->userCan(
1153  RevisionRecord::DELETED_TEXT,
1154  $this->getAuthority()
1155  ) ) {
1156  return false;
1157  }
1158  // Short-circuit
1159  if ( $this->mOldRevisionRecord === false || (
1160  $this->mOldRevisionRecord &&
1161  $this->mNewRevisionRecord &&
1162  $this->mOldRevisionRecord->getId() &&
1163  $this->mOldRevisionRecord->getId() == $this->mNewRevisionRecord->getId()
1164  ) ) {
1165  if ( $this->hookRunner->onDifferenceEngineShowEmptyOldContent( $this ) ) {
1166  return '';
1167  }
1168  }
1169  }
1170 
1171  // Cacheable?
1172  $key = false;
1173  $services = MediaWikiServices::getInstance();
1174  $cache = $services->getMainWANObjectCache();
1175  $stats = $services->getStatsdDataFactory();
1176  if ( $this->mOldid && $this->mNewid ) {
1177  // Check if subclass is still using the old way
1178  // for backwards-compatibility
1180  $this,
1181  __CLASS__,
1182  'getDiffBodyCacheKey',
1183  '1.31'
1184  );
1185  $key = null;
1186  if ( $detected ) {
1187  $key = $this->getDiffBodyCacheKey();
1188  }
1189  if ( $key === null ) {
1190  $key = $cache->makeKey( ...$this->getDiffBodyCacheKeyParams() );
1191  }
1192 
1193  // Try cache
1194  if ( !$this->mRefreshCache ) {
1195  $difftext = $cache->get( $key );
1196  if ( is_string( $difftext ) ) {
1197  $stats->updateCount( 'diff_cache.hit', 1 );
1198  $difftext = $this->localiseDiff( $difftext );
1199  $difftext .= "\n<!-- diff cache key $key -->\n";
1200 
1201  return $difftext;
1202  }
1203  } // don't try to load but save the result
1204  }
1205  $this->mCacheHit = false;
1206 
1207  // Loadtext is permission safe, this just clears out the diff
1208  if ( !$this->loadText() ) {
1209  return false;
1210  }
1211 
1212  $difftext = '';
1213  // We've checked for revdelete at the beginning of this method; it's OK to ignore
1214  // read permissions here.
1215  $slotContents = $this->getSlotContents();
1216  foreach ( $this->getSlotDiffRenderers() as $role => $slotDiffRenderer ) {
1217  $slotDiff = $slotDiffRenderer->getDiff( $slotContents[$role]['old'],
1218  $slotContents[$role]['new'] );
1219  if ( $slotDiff && $role !== SlotRecord::MAIN ) {
1220  // FIXME: ask SlotRoleHandler::getSlotNameMessage
1221  $slotTitle = $role;
1222  $difftext .= $this->getSlotHeader( $slotTitle );
1223  }
1224  $difftext .= $slotDiff;
1225  }
1226 
1227  // Save to cache for 7 days
1228  if ( !$this->hookRunner->onAbortDiffCache( $this ) ) {
1229  $stats->updateCount( 'diff_cache.uncacheable', 1 );
1230  } elseif ( $key !== false ) {
1231  $stats->updateCount( 'diff_cache.miss', 1 );
1232  $cache->set( $key, $difftext, 7 * 86400 );
1233  } else {
1234  $stats->updateCount( 'diff_cache.uncacheable', 1 );
1235  }
1236  // localise line numbers and title attribute text
1237  $difftext = $this->localiseDiff( $difftext );
1238 
1239  return $difftext;
1240  }
1241 
1248  public function getDiffBodyForRole( $role ) {
1249  $diffRenderers = $this->getSlotDiffRenderers();
1250  if ( !isset( $diffRenderers[$role] ) ) {
1251  return false;
1252  }
1253 
1254  $slotContents = $this->getSlotContents();
1255  $slotDiff = $diffRenderers[$role]->getDiff( $slotContents[$role]['old'],
1256  $slotContents[$role]['new'] );
1257  if ( !$slotDiff ) {
1258  return false;
1259  }
1260 
1261  if ( $role !== SlotRecord::MAIN ) {
1262  // TODO use human-readable role name at least
1263  $slotTitle = $role;
1264  $slotDiff = $this->getSlotHeader( $slotTitle ) . $slotDiff;
1265  }
1266 
1267  return $this->localiseDiff( $slotDiff );
1268  }
1269 
1277  protected function getSlotHeader( $headerText ) {
1278  // The old revision is missing on oldid=<first>&diff=prev; only 2 columns in that case.
1279  $columnCount = $this->mOldRevisionRecord ? 4 : 2;
1280  $userLang = $this->getLanguage()->getHtmlCode();
1281  return Html::rawElement( 'tr', [ 'class' => 'mw-diff-slot-header', 'lang' => $userLang ],
1282  Html::element( 'th', [ 'colspan' => $columnCount ], $headerText ) );
1283  }
1284 
1295  protected function getDiffBodyCacheKey() {
1296  wfDeprecated( __METHOD__, '1.31' );
1297  return null;
1298  }
1299 
1313  protected function getDiffBodyCacheKeyParams() {
1314  if ( !$this->mOldid || !$this->mNewid ) {
1315  throw new MWException( 'mOldid and mNewid must be set to get diff cache key.' );
1316  }
1317 
1318  $engine = $this->getEngine();
1319  $params = [
1320  'diff',
1321  $engine === 'php' ? false : $engine, // Back compat
1322  self::DIFF_VERSION,
1323  "old-{$this->mOldid}",
1324  "rev-{$this->mNewid}"
1325  ];
1326 
1327  if ( $engine === 'wikidiff2' ) {
1328  $params[] = phpversion( 'wikidiff2' );
1329  }
1330 
1331  if ( !$this->isSlotDiffRenderer ) {
1332  foreach ( $this->getSlotDiffRenderers() as $slotDiffRenderer ) {
1333  $params = array_merge( $params, $slotDiffRenderer->getExtraCacheKeys() );
1334  }
1335  }
1336 
1337  return $params;
1338  }
1339 
1347  public function getExtraCacheKeys() {
1348  // This method is called when the DifferenceEngine is used for a slot diff. We only care
1349  // about special things, not the revision IDs, which are added to the cache key by the
1350  // page-level DifferenceEngine, and which might not have a valid value for this object.
1351  $this->mOldid = 123456789;
1352  $this->mNewid = 987654321;
1353 
1354  // This will repeat a bunch of unnecessary key fields for each slot. Not nice but harmless.
1356  $this,
1357  __CLASS__,
1358  'getDiffBodyCacheKey',
1359  '1.31'
1360  );
1361  if ( $detected ) {
1362  $cacheString = $this->getDiffBodyCacheKey();
1363  if ( $cacheString ) {
1364  return [ $cacheString ];
1365  }
1366  }
1367 
1368  $params = $this->getDiffBodyCacheKeyParams();
1369 
1370  // Try to get rid of the standard keys to keep the cache key human-readable:
1371  // call the getDiffBodyCacheKeyParams implementation of the base class, and if
1372  // the child class includes the same keys, drop them.
1373  // Uses an obscure PHP feature where static calls to non-static methods are allowed
1374  // as long as we are already in a non-static method of the same class, and the call context
1375  // ($this) will be inherited.
1376  // phpcs:ignore Squiz.Classes.SelfMemberReference.NotUsed
1377  $standardParams = DifferenceEngine::getDiffBodyCacheKeyParams();
1378  if ( array_slice( $params, 0, count( $standardParams ) ) === $standardParams ) {
1379  $params = array_slice( $params, count( $standardParams ) );
1380  }
1381 
1382  return $params;
1383  }
1384 
1389  public function setSlotDiffOptions( $options ) {
1390  $this->slotDiffOptions = $options;
1391  }
1392 
1406  public function generateContentDiffBody( Content $old, Content $new ) {
1407  $slotDiffRenderer = $new->getContentHandler()->getSlotDiffRenderer( $this->getContext() );
1408  if (
1409  $slotDiffRenderer instanceof DifferenceEngineSlotDiffRenderer
1410  && $this->isSlotDiffRenderer
1411  ) {
1412  // Oops, we are just about to enter an infinite loop (the slot-level DifferenceEngine
1413  // called a DifferenceEngineSlotDiffRenderer that wraps the same DifferenceEngine class).
1414  // This will happen when a content model has no custom slot diff renderer, it does have
1415  // a custom difference engine, but that does not override this method.
1416  throw new Exception( get_class( $this ) . ': could not maintain backwards compatibility. '
1417  . 'Please use a SlotDiffRenderer.' );
1418  }
1419  return $slotDiffRenderer->getDiff( $old, $new ) . $this->getDebugString();
1420  }
1421 
1434  public function generateTextDiffBody( $otext, $ntext ) {
1435  $slotDiffRenderer = $this->contentHandlerFactory
1436  ->getContentHandler( CONTENT_MODEL_TEXT )
1437  ->getSlotDiffRenderer( $this->getContext() );
1438  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1439  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1440  // This is too unlikely to happen to bother handling properly.
1441  throw new Exception( 'The slot diff renderer for text content should be a '
1442  . 'TextSlotDiffRenderer subclass' );
1443  }
1444  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1445  }
1446 
1453  public static function getEngine() {
1454  $diffEngine = MediaWikiServices::getInstance()->getMainConfig()
1455  ->get( MainConfigNames::DiffEngine );
1456  $externalDiffEngine = MediaWikiServices::getInstance()->getMainConfig()
1457  ->get( MainConfigNames::ExternalDiffEngine );
1458 
1459  if ( $diffEngine === null ) {
1460  $engines = [ 'external', 'wikidiff2', 'php' ];
1461  } else {
1462  $engines = [ $diffEngine ];
1463  }
1464 
1465  $failureReason = null;
1466  foreach ( $engines as $engine ) {
1467  switch ( $engine ) {
1468  case 'external':
1469  if ( is_string( $externalDiffEngine ) ) {
1470  if ( is_executable( $externalDiffEngine ) ) {
1471  return $externalDiffEngine;
1472  }
1473  $failureReason = 'ExternalDiffEngine config points to a non-executable';
1474  if ( $diffEngine === null ) {
1475  wfDebug( "$failureReason, ignoring" );
1476  }
1477  } else {
1478  $failureReason = 'ExternalDiffEngine config is set to a non-string value';
1479  if ( $diffEngine === null && $externalDiffEngine ) {
1480  wfWarn( "$failureReason, ignoring" );
1481  }
1482  }
1483  break;
1484 
1485  case 'wikidiff2':
1486  if ( function_exists( 'wikidiff2_do_diff' ) ) {
1487  return 'wikidiff2';
1488  }
1489  $failureReason = 'wikidiff2 is not available';
1490  break;
1491 
1492  case 'php':
1493  // Always available.
1494  return 'php';
1495 
1496  default:
1497  throw new DomainException( 'Invalid value for $wgDiffEngine: ' . $engine );
1498  }
1499  }
1500  throw new UnexpectedValueException( "Cannot use diff engine '$engine': $failureReason" );
1501  }
1502 
1516  protected function textDiff( $otext, $ntext ) {
1517  wfDeprecated( __METHOD__, '1.32' );
1518  $slotDiffRenderer = $this->contentHandlerFactory
1519  ->getContentHandler( CONTENT_MODEL_TEXT )
1520  ->getSlotDiffRenderer( $this->getContext() );
1521  if ( !( $slotDiffRenderer instanceof TextSlotDiffRenderer ) ) {
1522  // Someone used the GetSlotDiffRenderer hook to replace the renderer.
1523  // This is too unlikely to happen to bother handling properly.
1524  throw new Exception( 'The slot diff renderer for text content should be a '
1525  . 'TextSlotDiffRenderer subclass' );
1526  }
1527  return $slotDiffRenderer->getTextDiff( $otext, $ntext ) . $this->getDebugString();
1528  }
1529 
1538  protected function debug( $generator = "internal" ) {
1539  if ( !$this->enableDebugComment ) {
1540  return '';
1541  }
1542  $data = [ $generator ];
1543  if ( $this->getConfig()->get( MainConfigNames::ShowHostnames ) ) {
1544  $data[] = wfHostname();
1545  }
1546  $data[] = wfTimestamp( TS_DB );
1547 
1548  return "<!-- diff generator: " .
1549  implode( " ", array_map( "htmlspecialchars", $data ) ) .
1550  " -->\n";
1551  }
1552 
1556  private function getDebugString() {
1557  $engine = self::getEngine();
1558  if ( $engine === 'wikidiff2' ) {
1559  return $this->debug( 'wikidiff2' );
1560  } elseif ( $engine === 'php' ) {
1561  return $this->debug( 'native PHP' );
1562  } else {
1563  return $this->debug( "external $engine" );
1564  }
1565  }
1566 
1573  private function localiseDiff( $text ) {
1574  $text = $this->localiseLineNumbers( $text );
1575  if ( $this->getEngine() === 'wikidiff2' &&
1576  version_compare( phpversion( 'wikidiff2' ), '1.5.1', '>=' )
1577  ) {
1578  $text = $this->addLocalisedTitleTooltips( $text );
1579  }
1580  return $text;
1581  }
1582 
1590  public function localiseLineNumbers( $text ) {
1591  return preg_replace_callback(
1592  '/<!--LINE (\d+)-->/',
1593  function ( array $matches ) {
1594  if ( $matches[1] === '1' && $this->mReducedLineNumbers ) {
1595  return '';
1596  }
1597  return $this->msg( 'lineno' )->numParams( $matches[1] )->escaped();
1598  },
1599  $text
1600  );
1601  }
1602 
1609  private function addLocalisedTitleTooltips( $text ) {
1610  return preg_replace_callback(
1611  '/class="mw-diff-movedpara-(left|right)"/',
1612  function ( array $matches ) {
1613  $key = $matches[1] === 'right' ?
1614  'diff-paragraph-moved-toold' :
1615  'diff-paragraph-moved-tonew';
1616  return $matches[0] . ' title="' . $this->msg( $key )->escaped() . '"';
1617  },
1618  $text
1619  );
1620  }
1621 
1627  public function getMultiNotice() {
1628  // The notice only make sense if we are diffing two saved revisions of the same page.
1629  if (
1630  !$this->mOldRevisionRecord || !$this->mNewRevisionRecord
1631  || !$this->mOldPage || !$this->mNewPage
1632  || !$this->mOldPage->equals( $this->mNewPage )
1633  || $this->mOldRevisionRecord->getId() === null
1634  || $this->mNewRevisionRecord->getId() === null
1635  // (T237709) Deleted revs might have different page IDs
1636  || $this->mNewPage->getArticleID() !== $this->mOldRevisionRecord->getPageId()
1637  || $this->mNewPage->getArticleID() !== $this->mNewRevisionRecord->getPageId()
1638  ) {
1639  return '';
1640  }
1641 
1642  if ( $this->mOldRevisionRecord->getTimestamp() > $this->mNewRevisionRecord->getTimestamp() ) {
1643  $oldRevRecord = $this->mNewRevisionRecord; // flip
1644  $newRevRecord = $this->mOldRevisionRecord; // flip
1645  } else { // normal case
1646  $oldRevRecord = $this->mOldRevisionRecord;
1647  $newRevRecord = $this->mNewRevisionRecord;
1648  }
1649 
1650  // Don't show the notice if too many rows must be scanned
1651  // @todo show some special message for that case
1652  $nEdits = $this->revisionStore->countRevisionsBetween(
1653  $this->mNewPage->getArticleID(),
1654  $oldRevRecord,
1655  $newRevRecord,
1656  1000
1657  );
1658  if ( $nEdits > 0 && $nEdits <= 1000 ) {
1659  $limit = 100; // use diff-multi-manyusers if too many users
1660  try {
1661  $users = $this->revisionStore->getAuthorsBetween(
1662  $this->mNewPage->getArticleID(),
1663  $oldRevRecord,
1664  $newRevRecord,
1665  null,
1666  $limit
1667  );
1668  $numUsers = count( $users );
1669 
1670  $newRevUser = $newRevRecord->getUser( RevisionRecord::RAW );
1671  $newRevUserText = $newRevUser ? $newRevUser->getName() : '';
1672  if ( $numUsers == 1 && $users[0]->getName() == $newRevUserText ) {
1673  $numUsers = 0; // special case to say "by the same user" instead of "by one other user"
1674  }
1675  } catch ( InvalidArgumentException $e ) {
1676  $numUsers = 0;
1677  }
1678 
1679  return self::intermediateEditsMsg( $nEdits, $numUsers, $limit );
1680  }
1681 
1682  return '';
1683  }
1684 
1694  public static function intermediateEditsMsg( $numEdits, $numUsers, $limit ) {
1695  if ( $numUsers === 0 ) {
1696  $msg = 'diff-multi-sameuser';
1697  } elseif ( $numUsers > $limit ) {
1698  $msg = 'diff-multi-manyusers';
1699  $numUsers = $limit;
1700  } else {
1701  $msg = 'diff-multi-otherusers';
1702  }
1703 
1704  return wfMessage( $msg )->numParams( $numEdits, $numUsers )->parse();
1705  }
1706 
1711  private function userCanEdit( RevisionRecord $revRecord ) {
1712  if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
1713  return false;
1714  }
1715 
1716  return true;
1717  }
1718 
1728  public function getRevisionHeader( RevisionRecord $rev, $complete = '' ) {
1729  $lang = $this->getLanguage();
1730  $user = $this->getUser();
1731  $revtimestamp = $rev->getTimestamp();
1732  $timestamp = $lang->userTimeAndDate( $revtimestamp, $user );
1733  $dateofrev = $lang->userDate( $revtimestamp, $user );
1734  $timeofrev = $lang->userTime( $revtimestamp, $user );
1735 
1736  $header = $this->msg(
1737  $rev->isCurrent() ? 'currentrev-asof' : 'revisionasof',
1738  $timestamp,
1739  $dateofrev,
1740  $timeofrev
1741  );
1742 
1743  if ( $complete !== 'complete' ) {
1744  return $header->escaped();
1745  }
1746 
1747  $title = $rev->getPageAsLinkTarget();
1748 
1749  $header = $this->linkRenderer->makeKnownLink( $title, $header->text(), [],
1750  [ 'oldid' => $rev->getId() ] );
1751 
1752  if ( $this->userCanEdit( $rev ) ) {
1753  $editQuery = [ 'action' => 'edit' ];
1754  if ( !$rev->isCurrent() ) {
1755  $editQuery['oldid'] = $rev->getId();
1756  }
1757 
1758  $key = $this->getAuthority()->probablyCan( 'edit', $rev->getPage() ) ? 'editold' : 'viewsourceold';
1759  $msg = $this->msg( $key )->text();
1760  $editLink = $this->msg( 'parentheses' )->rawParams(
1761  $this->linkRenderer->makeKnownLink( $title, $msg, [], $editQuery ) )->escaped();
1762  $header .= ' ' . Html::rawElement(
1763  'span',
1764  [ 'class' => 'mw-diff-edit' ],
1765  $editLink
1766  );
1767  if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1769  'span',
1770  [ 'class' => Linker::getRevisionDeletedClass( $rev ) ],
1771  $header
1772  );
1773  }
1774  } else {
1775  $header = Html::rawElement( 'span', [ 'class' => 'history-deleted' ], $header );
1776  }
1777 
1778  return $header;
1779  }
1780 
1793  public function addHeader( $diff, $otitle, $ntitle, $multi = '', $notice = '' ) {
1794  // shared.css sets diff in interface language/dir, but the actual content
1795  // is often in a different language, mostly the page content language/dir
1796  $header = Html::openElement( 'table', [
1797  'class' => [
1798  'diff',
1799  'diff-contentalign-' . $this->getDiffLang()->alignStart(),
1800  'diff-editfont-' . $this->userOptionsLookup->getOption(
1801  $this->getUser(),
1802  'editfont'
1803  )
1804  ],
1805  'data-mw' => 'interface',
1806  ] );
1807  $userLang = htmlspecialchars( $this->getLanguage()->getHtmlCode() );
1808 
1809  if ( !$diff && !$otitle ) {
1810  $header .= "
1811  <tr class=\"diff-title\" lang=\"{$userLang}\">
1812  <td class=\"diff-ntitle\">{$ntitle}</td>
1813  </tr>";
1814  $multiColspan = 1;
1815  } else {
1816  if ( $diff ) { // Safari/Chrome show broken output if cols not used
1817  $header .= "
1818  <col class=\"diff-marker\" />
1819  <col class=\"diff-content\" />
1820  <col class=\"diff-marker\" />
1821  <col class=\"diff-content\" />";
1822  $colspan = 2;
1823  $multiColspan = 4;
1824  } else {
1825  $colspan = 1;
1826  $multiColspan = 2;
1827  }
1828  if ( $otitle || $ntitle ) {
1829  // FIXME Hardcoding values from TableDiffFormatter.
1830  $deletedClass = 'diff-side-deleted';
1831  $addedClass = 'diff-side-added';
1832  $header .= "
1833  <tr class=\"diff-title\" lang=\"{$userLang}\">
1834  <td colspan=\"$colspan\" class=\"diff-otitle {$deletedClass}\">{$otitle}</td>
1835  <td colspan=\"$colspan\" class=\"diff-ntitle {$addedClass}\">{$ntitle}</td>
1836  </tr>";
1837  }
1838  }
1839 
1840  if ( $multi != '' ) {
1841  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1842  "class=\"diff-multi\" lang=\"{$userLang}\">{$multi}</td></tr>";
1843  }
1844  if ( $notice != '' ) {
1845  $header .= "<tr><td colspan=\"{$multiColspan}\" " .
1846  "class=\"diff-notice\" lang=\"{$userLang}\">{$notice}</td></tr>";
1847  }
1848 
1849  return $header . $diff . "</table>";
1850  }
1851 
1859  public function setContent( Content $oldContent, Content $newContent ) {
1860  $this->mOldContent = $oldContent;
1861  $this->mNewContent = $newContent;
1862 
1863  $this->mTextLoaded = 2;
1864  $this->mRevisionsLoaded = true;
1865  $this->isContentOverridden = true;
1866  $this->slotDiffRenderers = null;
1867  }
1868 
1874  public function setRevisions(
1875  ?RevisionRecord $oldRevision, RevisionRecord $newRevision
1876  ) {
1877  if ( $oldRevision ) {
1878  $this->mOldRevisionRecord = $oldRevision;
1879  $this->mOldid = $oldRevision->getId();
1880  $this->mOldPage = Title::newFromLinkTarget( $oldRevision->getPageAsLinkTarget() );
1881  // This method is meant for edit diffs and such so there is no reason to provide a
1882  // revision that's not readable to the user, but check it just in case.
1883  $this->mOldContent = $oldRevision->getContent( SlotRecord::MAIN,
1884  RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
1885  } else {
1886  $this->mOldPage = null;
1887  $this->mOldRevisionRecord = $this->mOldid = false;
1888  }
1889  $this->mNewRevisionRecord = $newRevision;
1890  $this->mNewid = $newRevision->getId();
1891  $this->mNewPage = Title::newFromLinkTarget( $newRevision->getPageAsLinkTarget() );
1892  $this->mNewContent = $newRevision->getContent( SlotRecord::MAIN,
1893  RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
1894 
1895  $this->mRevisionsIdsLoaded = $this->mRevisionsLoaded = true;
1896  $this->mTextLoaded = $oldRevision ? 2 : 1;
1897  $this->isContentOverridden = false;
1898  $this->slotDiffRenderers = null;
1899  }
1900 
1907  public function setTextLanguage( Language $lang ) {
1908  $this->mDiffLang = $lang;
1909  }
1910 
1923  public function mapDiffPrevNext( $old, $new ) {
1924  if ( $new === 'prev' ) {
1925  // Show diff between revision $old and the previous one. Get previous one from DB.
1926  $newid = intval( $old );
1927  $oldid = false;
1928  $newRev = $this->revisionStore->getRevisionById( $newid );
1929  if ( $newRev ) {
1930  $oldRev = $this->revisionStore->getPreviousRevision( $newRev );
1931  if ( $oldRev ) {
1932  $oldid = $oldRev->getId();
1933  }
1934  }
1935  } elseif ( $new === 'next' ) {
1936  // Show diff between revision $old and the next one. Get next one from DB.
1937  $oldid = intval( $old );
1938  $newid = false;
1939  $oldRev = $this->revisionStore->getRevisionById( $oldid );
1940  if ( $oldRev ) {
1941  $newRev = $this->revisionStore->getNextRevision( $oldRev );
1942  if ( $newRev ) {
1943  $newid = $newRev->getId();
1944  }
1945  }
1946  } else {
1947  $oldid = intval( $old );
1948  $newid = intval( $new );
1949  }
1950 
1951  // @phan-suppress-next-line PhanTypeMismatchReturn getId does not return null here
1952  return [ $oldid, $newid ];
1953  }
1954 
1955  private function loadRevisionIds() {
1956  if ( $this->mRevisionsIdsLoaded ) {
1957  return;
1958  }
1959 
1960  $this->mRevisionsIdsLoaded = true;
1961 
1962  $old = $this->mOldid;
1963  $new = $this->mNewid;
1964 
1965  list( $this->mOldid, $this->mNewid ) = self::mapDiffPrevNext( $old, $new );
1966  if ( $new === 'next' && $this->mNewid === false ) {
1967  # if no result, NewId points to the newest old revision. The only newer
1968  # revision is cur, which is "0".
1969  $this->mNewid = 0;
1970  }
1971 
1972  $this->hookRunner->onNewDifferenceEngine(
1973  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable False positive
1974  $this->getTitle(), $this->mOldid, $this->mNewid, $old, $new );
1975  }
1976 
1990  public function loadRevisionData() {
1991  if ( $this->mRevisionsLoaded ) {
1992  return $this->isContentOverridden ||
1993  ( $this->mOldRevisionRecord !== null && $this->mNewRevisionRecord !== null );
1994  }
1995 
1996  // Whether it succeeds or fails, we don't want to try again
1997  $this->mRevisionsLoaded = true;
1998 
1999  $this->loadRevisionIds();
2000 
2001  // Load the new RevisionRecord object
2002  if ( $this->mNewid ) {
2003  $this->mNewRevisionRecord = $this->revisionStore->getRevisionById( $this->mNewid );
2004  } else {
2005  $this->mNewRevisionRecord = $this->revisionStore->getRevisionByTitle( $this->getTitle() );
2006  }
2007 
2008  if ( !$this->mNewRevisionRecord instanceof RevisionRecord ) {
2009  return false;
2010  }
2011 
2012  // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
2013  $this->mNewid = $this->mNewRevisionRecord->getId();
2014  $this->mNewPage = $this->mNewid ?
2015  Title::newFromLinkTarget( $this->mNewRevisionRecord->getPageAsLinkTarget() ) :
2016  null;
2017 
2018  // Load the old RevisionRecord object
2019  $this->mOldRevisionRecord = false;
2020  if ( $this->mOldid ) {
2021  $this->mOldRevisionRecord = $this->revisionStore->getRevisionById( $this->mOldid );
2022  } elseif ( $this->mOldid === 0 ) {
2023  $revRecord = $this->revisionStore->getPreviousRevision( $this->mNewRevisionRecord );
2024  // No previous revision; mark to show as first-version only.
2025  $this->mOldid = $revRecord ? $revRecord->getId() : false;
2026  $this->mOldRevisionRecord = $revRecord ?? false;
2027  } /* elseif ( $this->mOldid === false ) leave mOldRevisionRecord false; */
2028 
2029  if ( $this->mOldRevisionRecord === null ) {
2030  return false;
2031  }
2032 
2033  if ( $this->mOldRevisionRecord && $this->mOldRevisionRecord->getId() ) {
2034  $this->mOldPage = Title::newFromLinkTarget(
2035  $this->mOldRevisionRecord->getPageAsLinkTarget()
2036  );
2037  } else {
2038  $this->mOldPage = null;
2039  }
2040 
2041  // Load tags information for both revisions
2042  $dbr = wfGetDB( DB_REPLICA );
2043  $changeTagDefStore = MediaWikiServices::getInstance()->getChangeTagDefStore();
2044  if ( $this->mOldid !== false ) {
2045  $tagIds = $dbr->selectFieldValues(
2046  'change_tag',
2047  'ct_tag_id',
2048  [ 'ct_rev_id' => $this->mOldid ],
2049  __METHOD__
2050  );
2051  $tags = [];
2052  foreach ( $tagIds as $tagId ) {
2053  try {
2054  $tags[] = $changeTagDefStore->getName( (int)$tagId );
2055  } catch ( NameTableAccessException $exception ) {
2056  continue;
2057  }
2058  }
2059  $this->mOldTags = implode( ',', $tags );
2060  } else {
2061  $this->mOldTags = false;
2062  }
2063 
2064  $tagIds = $dbr->selectFieldValues(
2065  'change_tag',
2066  'ct_tag_id',
2067  [ 'ct_rev_id' => $this->mNewid ],
2068  __METHOD__
2069  );
2070  $tags = [];
2071  foreach ( $tagIds as $tagId ) {
2072  try {
2073  $tags[] = $changeTagDefStore->getName( (int)$tagId );
2074  } catch ( NameTableAccessException $exception ) {
2075  continue;
2076  }
2077  }
2078  $this->mNewTags = implode( ',', $tags );
2079 
2080  return true;
2081  }
2082 
2091  public function loadText() {
2092  if ( $this->mTextLoaded == 2 ) {
2093  return $this->loadRevisionData() &&
2094  ( $this->mOldRevisionRecord === false || $this->mOldContent )
2095  && $this->mNewContent;
2096  }
2097 
2098  // Whether it succeeds or fails, we don't want to try again
2099  $this->mTextLoaded = 2;
2100 
2101  if ( !$this->loadRevisionData() ) {
2102  return false;
2103  }
2104 
2105  if ( $this->mOldRevisionRecord ) {
2106  $this->mOldContent = $this->mOldRevisionRecord->getContent(
2107  SlotRecord::MAIN,
2108  RevisionRecord::FOR_THIS_USER,
2109  $this->getAuthority()
2110  );
2111  if ( $this->mOldContent === null ) {
2112  return false;
2113  }
2114  }
2115 
2116  $this->mNewContent = $this->mNewRevisionRecord->getContent(
2117  SlotRecord::MAIN,
2118  RevisionRecord::FOR_THIS_USER,
2119  $this->getAuthority()
2120  );
2121  $this->hookRunner->onDifferenceEngineLoadTextAfterNewContentIsLoaded( $this );
2122  if ( $this->mNewContent === null ) {
2123  return false;
2124  }
2125 
2126  return true;
2127  }
2128 
2134  public function loadNewText() {
2135  if ( $this->mTextLoaded >= 1 ) {
2136  return $this->loadRevisionData();
2137  }
2138 
2139  $this->mTextLoaded = 1;
2140 
2141  if ( !$this->loadRevisionData() ) {
2142  return false;
2143  }
2144 
2145  $this->mNewContent = $this->mNewRevisionRecord->getContent(
2146  SlotRecord::MAIN,
2147  RevisionRecord::FOR_THIS_USER,
2148  $this->getAuthority()
2149  );
2150 
2151  $this->hookRunner->onDifferenceEngineAfterLoadNewText( $this );
2152 
2153  return true;
2154  }
2155 
2156 }
const NS_SPECIAL
Definition: Defines.php:53
const CONTENT_MODEL_TEXT
Definition: Defines.php:214
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.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
$matches
static formatSummaryRow( $tags, $page, MessageLocalizer $localizer=null)
Creates HTML for the given tags.
Definition: ChangeTags.php:193
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()
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).
setSlotDiffOptions( $options)
hasDeletedRevision()
Checks whether one of the given Revisions was deleted.
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.
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.
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.
getDiffBody()
Get the diff table body, without header.
getTitle()
1.18 Stability: stableto override Title|null
getParserOutput(WikiPage $page, RevisionRecord $revRecord)
loadRevisionData()
Load revision metadata for the specified revisions.
static getEngine()
Process DiffEngine config and get a sensible, usable engine.
bool $mRevisionsLoaded
Have the revisions been loaded.
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.
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.
LinkRenderer $linkRenderer
getDiffBodyCacheKeyParams()
Get the cache key parameters.
getDiff( $otitle, $ntitle, $notice='')
Get complete diff table, including header.
getNewid()
Get the ID of new revision (right pane) of the diff.
renderNewRevision()
Show the new revision of the page.
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.
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.
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.
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
Base class for language-specific code.
Definition: Language.php:53
static expandLocalLinks(string $html)
Helper function to expand local links.
Definition: Linker.php:1370
static getRevisionDeletedClass(RevisionRecord $revisionRecord)
Returns css class of a deleted revision.
Definition: Linker.php:1307
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:1563
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:2109
static generateRollback(RevisionRecord $revRecord, IContextSource $context=null, $options=[ 'verify'])
Generate a rollback link for a given revision.
Definition: Linker.php:1790
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:2036
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:1327
static detectDeprecatedOverride( $instance, $class, $method, $version=false, $component=false, $callerOffset=2)
Show a warning if $method declared in $class is overridden in $instance.
Definition: MWDebug.php:258
MediaWiki exception.
Definition: MWException.php:29
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:564
Class that generates HTML anchor link elements for pages.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
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:282
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:664
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:638
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:1935
getParserOutput(?ParserOptions $parserOptions=null, $oldid=null, $noCache=false)
Get a ParserOutput for the given ParserOptions and revision ID.
Definition: WikiPage.php:1271
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:26
$content
Definition: router.php:76
if(!isset( $args[0])) $lang
$header