MediaWiki  master
Article.php
Go to the documentation of this file.
1 <?php
25 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
48 use Wikimedia\IPUtils;
50 use Wikimedia\NonSerializable\NonSerializableTrait;
51 
61 class Article implements Page {
62  use ProtectedHookAccessorTrait;
63  use NonSerializableTrait;
64 
70  protected $mContext;
71 
73  protected $mPage;
74 
79  public $mOldId;
80 
82  public $mRedirectedFrom = null;
83 
85  public $mRedirectUrl = false;
86 
91  private $fetchResult = null;
92 
98  public $mParserOutput = null;
99 
105  protected $viewIsRenderAction = false;
106 
110  protected $linkRenderer;
111 
115  private $revisionStore;
116 
120  private $userNameUtils;
121 
125  private $userOptionsLookup;
126 
128  private $commentFormatter;
129 
131  private $wikiPageFactory;
132 
134  private $jobQueueGroup;
135 
137  private $archivedRevisionLookup;
138 
145  private $mRevisionRecord = null;
146 
151  public function __construct( Title $title, $oldId = null ) {
152  $this->mOldId = $oldId;
153  $this->mPage = $this->newPage( $title );
154 
155  $services = MediaWikiServices::getInstance();
156  $this->linkRenderer = $services->getLinkRenderer();
157  $this->revisionStore = $services->getRevisionStore();
158  $this->userNameUtils = $services->getUserNameUtils();
159  $this->userOptionsLookup = $services->getUserOptionsLookup();
160  $this->commentFormatter = $services->getCommentFormatter();
161  $this->wikiPageFactory = $services->getWikiPageFactory();
162  $this->jobQueueGroup = $services->getJobQueueGroup();
163  $this->archivedRevisionLookup = $services->getArchivedRevisionLookup();
164  }
165 
170  protected function newPage( Title $title ) {
171  return new WikiPage( $title );
172  }
173 
179  public static function newFromID( $id ) {
180  $t = Title::newFromID( $id );
181  return $t === null ? null : new static( $t );
182  }
183 
191  public static function newFromTitle( $title, IContextSource $context ): self {
192  if ( $title->getNamespace() === NS_MEDIA ) {
193  // XXX: This should not be here, but where should it go?
194  $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
195  }
196 
197  $page = null;
198  ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
199  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
200  ->onArticleFromTitle( $title, $page, $context );
201  if ( !$page ) {
202  switch ( $title->getNamespace() ) {
203  case NS_FILE:
204  $page = new ImagePage( $title );
205  break;
206  case NS_CATEGORY:
207  $page = new CategoryPage( $title );
208  break;
209  default:
210  $page = new Article( $title );
211  }
212  }
213  $page->setContext( $context );
214 
215  return $page;
216  }
217 
225  public static function newFromWikiPage( WikiPage $page, IContextSource $context ) {
226  $article = self::newFromTitle( $page->getTitle(), $context );
227  $article->mPage = $page; // override to keep process cached vars
228  return $article;
229  }
230 
236  public function getRedirectedFrom() {
237  return $this->mRedirectedFrom;
238  }
239 
245  public function setRedirectedFrom( Title $from ) {
246  $this->mRedirectedFrom = $from;
247  }
248 
254  public function getTitle() {
255  return $this->mPage->getTitle();
256  }
257 
264  public function getPage() {
265  return $this->mPage;
266  }
267 
268  public function clear() {
269  $this->mRedirectedFrom = null; # Title object if set
270  $this->mRedirectUrl = false;
271  $this->mRevisionRecord = null;
272  $this->fetchResult = null;
273 
274  // TODO hard-deprecate direct access to public fields
275 
276  $this->mPage->clear();
277  }
278 
286  public function getOldID() {
287  if ( $this->mOldId === null ) {
288  $this->mOldId = $this->getOldIDFromRequest();
289  }
290 
291  return $this->mOldId;
292  }
293 
299  public function getOldIDFromRequest() {
300  $this->mRedirectUrl = false;
301 
302  $request = $this->getContext()->getRequest();
303  $oldid = $request->getIntOrNull( 'oldid' );
304 
305  if ( $oldid === null ) {
306  return 0;
307  }
308 
309  if ( $oldid !== 0 ) {
310  # Load the given revision and check whether the page is another one.
311  # In that case, update this instance to reflect the change.
312  if ( $oldid === $this->mPage->getLatest() ) {
313  $this->mRevisionRecord = $this->mPage->getRevisionRecord();
314  } else {
315  $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
316  if ( $this->mRevisionRecord !== null ) {
317  $revPageId = $this->mRevisionRecord->getPageId();
318  // Revision title doesn't match the page title given?
319  if ( $this->mPage->getId() !== $revPageId ) {
320  $this->mPage = $this->wikiPageFactory->newFromID( $revPageId );
321  }
322  }
323  }
324  }
325 
326  $oldRev = $this->mRevisionRecord;
327  if ( $request->getRawVal( 'direction' ) === 'next' ) {
328  $nextid = 0;
329  if ( $oldRev ) {
330  $nextRev = $this->revisionStore->getNextRevision( $oldRev );
331  if ( $nextRev ) {
332  $nextid = $nextRev->getId();
333  }
334  }
335  if ( $nextid ) {
336  $oldid = $nextid;
337  $this->mRevisionRecord = null;
338  } else {
339  $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' );
340  }
341  } elseif ( $request->getRawVal( 'direction' ) === 'prev' ) {
342  $previd = 0;
343  if ( $oldRev ) {
344  $prevRev = $this->revisionStore->getPreviousRevision( $oldRev );
345  if ( $prevRev ) {
346  $previd = $prevRev->getId();
347  }
348  }
349  if ( $previd ) {
350  $oldid = $previd;
351  $this->mRevisionRecord = null;
352  }
353  }
354 
355  return $oldid;
356  }
357 
367  public function fetchRevisionRecord() {
368  if ( $this->fetchResult ) {
369  return $this->mRevisionRecord;
370  }
371 
372  $oldid = $this->getOldID();
373 
374  // $this->mRevisionRecord might already be fetched by getOldIDFromRequest()
375  if ( !$this->mRevisionRecord ) {
376  if ( !$oldid ) {
377  $this->mRevisionRecord = $this->mPage->getRevisionRecord();
378 
379  if ( !$this->mRevisionRecord ) {
380  wfDebug( __METHOD__ . " failed to find page data for title " .
381  $this->getTitle()->getPrefixedText() );
382 
383  // Output for this case is done by showMissingArticle().
384  $this->fetchResult = Status::newFatal( 'noarticletext' );
385  return null;
386  }
387  } else {
388  $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
389 
390  if ( !$this->mRevisionRecord ) {
391  wfDebug( __METHOD__ . " failed to load revision, rev_id $oldid" );
392 
393  $this->fetchResult = Status::newFatal( 'missing-revision', $oldid );
394  return null;
395  }
396  }
397  }
398 
399  if ( !$this->mRevisionRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getContext()->getAuthority() ) ) {
400  wfDebug( __METHOD__ . " failed to retrieve content of revision " . $this->mRevisionRecord->getId() );
401 
402  // Output for this case is done by showDeletedRevisionHeader().
403  // title used in wikilinks, should not contain whitespaces
404  $this->fetchResult = new Status;
405  $title = $this->getTitle()->getPrefixedDBkey();
406 
407  if ( $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
408  $this->fetchResult->fatal( 'rev-suppressed-text' );
409  } else {
410  $this->fetchResult->fatal( 'rev-deleted-text-permission', $title );
411  }
412 
413  return null;
414  }
415 
416  $this->fetchResult = Status::newGood( $this->mRevisionRecord );
417  return $this->mRevisionRecord;
418  }
419 
425  public function isCurrent() {
426  # If no oldid, this is the current version.
427  if ( $this->getOldID() == 0 ) {
428  return true;
429  }
430 
431  return $this->mPage->exists() &&
432  $this->mRevisionRecord &&
433  $this->mRevisionRecord->isCurrent();
434  }
435 
444  public function getRevIdFetched() {
445  if ( $this->fetchResult && $this->fetchResult->isOK() ) {
447  $rev = $this->fetchResult->getValue();
448  return $rev->getId();
449  } else {
450  return $this->mPage->getLatest();
451  }
452  }
453 
458  public function view() {
459  $context = $this->getContext();
460  $useFileCache = $context->getConfig()->get( MainConfigNames::UseFileCache );
461 
462  # Get variables from query string
463  # As side effect this will load the revision and update the title
464  # in a revision ID is passed in the request, so this should remain
465  # the first call of this method even if $oldid is used way below.
466  $oldid = $this->getOldID();
467 
468  $authority = $context->getAuthority();
469  # Another check in case getOldID() is altering the title
470  $permissionStatus = PermissionStatus::newEmpty();
471  if ( !$authority
472  ->authorizeRead( 'read', $this->getTitle(), $permissionStatus )
473  ) {
474  wfDebug( __METHOD__ . ": denied on secondary read check" );
475  throw new PermissionsError( 'read', $permissionStatus );
476  }
477 
478  $outputPage = $context->getOutput();
479  # getOldID() may as well want us to redirect somewhere else
480  if ( $this->mRedirectUrl ) {
481  $outputPage->redirect( $this->mRedirectUrl );
482  wfDebug( __METHOD__ . ": redirecting due to oldid" );
483 
484  return;
485  }
486 
487  # If we got diff in the query, we want to see a diff page instead of the article.
488  if ( $context->getRequest()->getCheck( 'diff' ) ) {
489  wfDebug( __METHOD__ . ": showing diff page" );
490  $this->showDiffPage();
491 
492  return;
493  }
494 
495  # Set page title (may be overridden from ParserOutput if title conversion is enabled or DISPLAYTITLE is used)
497  str_replace( '_', ' ', $this->getTitle()->getNsText() ),
498  ':',
499  $this->getTitle()->getText()
500  ) );
501 
502  $outputPage->setArticleFlag( true );
503  # Allow frames by default
504  $outputPage->setPreventClickjacking( false );
505 
506  $parserOptions = $this->getParserOptions();
507 
508  $poOptions = [];
509  # Allow extensions to vary parser options used for article rendering
510  ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
511  ->onArticleParserOptions( $this, $parserOptions );
512  # Render printable version, use printable version cache
513  if ( $outputPage->isPrintable() ) {
514  $parserOptions->setIsPrintable( true );
515  $poOptions['enableSectionEditLinks'] = false;
516  $outputPage->prependHTML(
517  Html::warningBox(
518  $outputPage->msg( 'printableversion-deprecated-warning' )->escaped()
519  )
520  );
521  } elseif ( $this->viewIsRenderAction || !$this->isCurrent() ||
522  !$authority->probablyCan( 'edit', $this->getTitle() )
523  ) {
524  $poOptions['enableSectionEditLinks'] = false;
525  }
526 
527  # Try client and file cache
528  if ( $oldid === 0 && $this->mPage->checkTouched() ) {
529  # Try to stream the output from file cache
530  if ( $useFileCache && $this->tryFileCache() ) {
531  wfDebug( __METHOD__ . ": done file cache" );
532  # tell wgOut that output is taken care of
533  $outputPage->disable();
534  $this->mPage->doViewUpdates( $authority, $oldid );
535 
536  return;
537  }
538  }
539 
540  $this->showRedirectedFromHeader();
541  $this->showNamespaceHeader();
542 
543  if ( $this->viewIsRenderAction ) {
544  $poOptions += [ 'absoluteURLs' => true ];
545  }
546  $poOptions += [ 'includeDebugInfo' => true ];
547 
548  try {
549  $continue =
550  $this->generateContentOutput( $authority, $parserOptions, $oldid, $outputPage, $poOptions );
551  } catch ( BadRevisionException $e ) {
552  $continue = false;
553  $this->showViewError( wfMessage( 'badrevision' )->text() );
554  }
555 
556  if ( !$continue ) {
557  return;
558  }
559 
560  if ( $parserOptions->getUseParsoid() ) {
561  $outputPage->addModules( 'mediawiki.skinning.content.parsoid' );
562  }
563 
564  # For the main page, overwrite the <title> element with the con-
565  # tents of 'pagetitle-view-mainpage' instead of the default (if
566  # that's not empty).
567  # This message always exists because it is in the i18n files
568  if ( $this->getTitle()->isMainPage() ) {
569  $msg = $context->msg( 'pagetitle-view-mainpage' )->inContentLanguage();
570  if ( !$msg->isDisabled() ) {
571  $outputPage->setHTMLTitle( $msg->text() );
572  }
573  }
574 
575  # Use adaptive TTLs for CDN so delayed/failed purges are noticed less often.
576  # This could use getTouched(), but that could be scary for major template edits.
577  $outputPage->adaptCdnTTL( $this->mPage->getTimestamp(), ExpirationAwareness::TTL_DAY );
578 
579  $this->showViewFooter();
580  $this->mPage->doViewUpdates( $authority, $oldid, $this->fetchRevisionRecord() );
581 
582  # Load the postEdit module if the user just saved this revision
583  # See also EditPage::setPostEditCookie
584  $request = $context->getRequest();
585  $cookieKey = EditPage::POST_EDIT_COOKIE_KEY_PREFIX . $this->getRevIdFetched();
586  $postEdit = $request->getCookie( $cookieKey );
587  if ( $postEdit ) {
588  # Clear the cookie. This also prevents caching of the response.
589  $request->response()->clearCookie( $cookieKey );
590  $outputPage->addJsConfigVars( 'wgPostEdit', $postEdit );
591  $outputPage->addModules( 'mediawiki.action.view.postEdit' ); // FIXME: test this
592  if ( $this->getContext()->getConfig()->get( 'EnableEditRecovery' ) ) {
593  $outputPage->addModules( 'mediawiki.editRecovery.postEdit' );
594  }
595  }
596  }
597 
610  private function generateContentOutput(
611  Authority $performer,
612  ParserOptions $parserOptions,
613  int $oldid,
614  OutputPage $outputPage,
615  array $textOptions
616  ): bool {
617  # Should the parser cache be used?
618  $useParserCache = true;
619  $pOutput = null;
620  $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
621 
622  // NOTE: $outputDone and $useParserCache may be changed by the hook
623  $this->getHookRunner()->onArticleViewHeader( $this, $outputDone, $useParserCache );
624  if ( $outputDone ) {
625  if ( $outputDone instanceof ParserOutput ) {
626  $pOutput = $outputDone;
627  }
628 
629  if ( $pOutput ) {
630  $this->doOutputMetaData( $pOutput, $outputPage );
631  }
632  return true;
633  }
634 
635  // Early abort if the page doesn't exist
636  if ( !$this->mPage->exists() ) {
637  wfDebug( __METHOD__ . ": showing missing article" );
638  $this->showMissingArticle();
639  $this->mPage->doViewUpdates( $performer );
640  return false; // skip all further output to OutputPage
641  }
642 
643  // Try the latest parser cache
644  // NOTE: try latest-revision cache first to avoid loading revision.
645  if ( $useParserCache && !$oldid ) {
646  $pOutput = $parserOutputAccess->getCachedParserOutput(
647  $this->getPage(),
648  $parserOptions,
649  null,
650  ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked
651  );
652 
653  if ( $pOutput ) {
654  $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
655  $this->doOutputMetaData( $pOutput, $outputPage );
656  return true;
657  }
658  }
659 
660  $rev = $this->fetchRevisionRecord();
661  if ( !$this->fetchResult->isOK() ) {
662  $this->showViewError( $this->fetchResult->getWikiText(
663  false, false, $this->getContext()->getLanguage()
664  ) );
665  return true;
666  }
667 
668  # Are we looking at an old revision
669  if ( $oldid ) {
670  $this->setOldSubtitle( $oldid );
671 
672  if ( !$this->showDeletedRevisionHeader() ) {
673  wfDebug( __METHOD__ . ": cannot view deleted revision" );
674  return false; // skip all further output to OutputPage
675  }
676 
677  // Try the old revision parser cache
678  // NOTE: Repeating cache check for old revision to avoid fetching $rev
679  // before it's absolutely necessary.
680  if ( $useParserCache ) {
681  $pOutput = $parserOutputAccess->getCachedParserOutput(
682  $this->getPage(),
683  $parserOptions,
684  $rev,
685  ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked in fetchRevisionRecord
686  );
687 
688  if ( $pOutput ) {
689  $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
690  $this->doOutputMetaData( $pOutput, $outputPage );
691  return true;
692  }
693  }
694  }
695 
696  # Ensure that UI elements requiring revision ID have
697  # the correct version information. (This may be overwritten after creation of ParserOutput)
698  $outputPage->setRevisionId( $this->getRevIdFetched() );
699  $outputPage->setRevisionIsCurrent( $rev->isCurrent() );
700  # Preload timestamp to avoid a DB hit
701  $outputPage->setRevisionTimestamp( $rev->getTimestamp() );
702 
703  # Pages containing custom CSS or JavaScript get special treatment
704  if ( $this->getTitle()->isSiteConfigPage() || $this->getTitle()->isUserConfigPage() ) {
705  $dir = $this->getContext()->getLanguage()->getDir();
706  $lang = $this->getContext()->getLanguage()->getHtmlCode();
707 
708  $outputPage->wrapWikiMsg(
709  "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
710  'clearyourcache'
711  );
712  $outputPage->addModuleStyles( 'mediawiki.action.styles' );
713  } elseif ( !$this->getHookRunner()->onArticleRevisionViewCustom(
714  $rev,
715  $this->getTitle(),
716  $oldid,
717  $outputPage )
718  ) {
719  // NOTE: sync with hooks called in DifferenceEngine::renderNewRevision()
720  // Allow extensions do their own custom view for certain pages
721  $this->doOutputMetaData( $pOutput, $outputPage );
722  return true;
723  }
724 
725  # Run the parse, protected by a pool counter
726  wfDebug( __METHOD__ . ": doing uncached parse" );
727 
728  $opt = 0;
729 
730  // we already checked the cache in case 2, don't check again.
731  $opt |= ParserOutputAccess::OPT_NO_CHECK_CACHE;
732 
733  // we already checked in fetchRevisionRecord()
734  $opt |= ParserOutputAccess::OPT_NO_AUDIENCE_CHECK;
735 
736  // Attempt to trigger WikiPage::triggerOpportunisticLinksUpdate
737  // Ideally this should not be the responsibility of the ParserCache to control this.
738  // See https://phabricator.wikimedia.org/T329842#8816557 for more context.
739  $opt |= ParserOutputAccess::OPT_LINKS_UPDATE;
740 
741  if ( !$rev->getId() || !$useParserCache ) {
742  // fake revision or uncacheable options
743  $opt |= ParserOutputAccess::OPT_NO_CACHE;
744  }
745 
746  $renderStatus = $parserOutputAccess->getParserOutput(
747  $this->getPage(),
748  $parserOptions,
749  $rev,
750  $opt
751  );
752 
753  // T327164: If parsoid cache warming is enabled, we want to ensure that the page
754  // the user is currently looking at has a cached parsoid rendering, in case they
755  // open visual editor. The cache entry would typically be missing if it has expired
756  // from the cache or it was invalidated by RefreshLinksJob. When "traditional"
757  // parser output has been invalidated by RefreshLinksJob, we will render it on
758  // the fly when a user requests the page, and thereby populate the cache again,
759  // per the code above.
760  // The code below is intended to do the same for parsoid output, but asynchronously
761  // in a job, so the user does not have to wait.
762  // Note that we get here if the traditional parser output was missing from the cache.
763  // We do not check if the parsoid output is present in the cache, because that check
764  // takes time. The assumption is that if we have traditional parser output
765  // cached, we probably also have parsoid output cached.
766  // So we leave it to ParsoidCachePrewarmJob to determine whether or not parsing is
767  // needed.
768  if ( $oldid === 0 || $oldid === $this->getPage()->getLatest() ) {
769  $parsoidCacheWarmingEnabled = $this->getContext()->getConfig()
770  ->get( MainConfigNames::ParsoidCacheConfig )['WarmParsoidParserCache'];
771 
772  if ( $parsoidCacheWarmingEnabled ) {
773  $parsoidJobSpec = ParsoidCachePrewarmJob::newSpec(
774  $rev->getId(),
775  $this->getPage()->toPageRecord(),
776  [ 'causeAction' => 'view' ]
777  );
778  $this->jobQueueGroup->lazyPush( $parsoidJobSpec );
779  }
780  }
781 
782  $this->doOutputFromRenderStatus(
783  $rev,
784  $renderStatus,
785  $outputPage,
786  $textOptions
787  );
788 
789  if ( !$renderStatus->isOK() ) {
790  return true;
791  }
792 
793  $pOutput = $renderStatus->getValue();
794  $this->doOutputMetaData( $pOutput, $outputPage );
795  return true;
796  }
797 
802  private function doOutputMetaData( ?ParserOutput $pOutput, OutputPage $outputPage ) {
803  # Adjust title for main page & pages with displaytitle
804  if ( $pOutput ) {
805  $this->adjustDisplayTitle( $pOutput );
806  }
807 
808  # Check for any __NOINDEX__ tags on the page using $pOutput
809  $policy = $this->getRobotPolicy( 'view', $pOutput ?: null );
810  $outputPage->setIndexPolicy( $policy['index'] );
811  $outputPage->setFollowPolicy( $policy['follow'] ); // FIXME: test this
812 
813  $this->mParserOutput = $pOutput;
814  }
815 
821  private function doOutputFromParserCache(
822  ParserOutput $pOutput,
823  OutputPage $outputPage,
824  array $textOptions
825  ) {
826  # Ensure that UI elements requiring revision ID have
827  # the correct version information.
828  $oldid = $pOutput->getCacheRevisionId() ?? $this->getRevIdFetched();
829  $outputPage->setRevisionId( $oldid );
830  $outputPage->setRevisionIsCurrent( $oldid === $this->mPage->getLatest() );
831  $outputPage->addParserOutput( $pOutput, $textOptions );
832  # Preload timestamp to avoid a DB hit
833  $cachedTimestamp = $pOutput->getTimestamp();
834  if ( $cachedTimestamp !== null ) {
835  $outputPage->setRevisionTimestamp( $cachedTimestamp );
836  $this->mPage->setTimestamp( $cachedTimestamp );
837  }
838  }
839 
846  private function doOutputFromRenderStatus(
847  RevisionRecord $rev,
848  Status $renderStatus,
849  OutputPage $outputPage,
850  array $textOptions
851  ) {
852  $context = $this->getContext();
853  if ( !$renderStatus->isOK() ) {
854  $this->showViewError( $renderStatus->getWikiText(
855  false, 'view-pool-error', $context->getLanguage()
856  ) );
857  return;
858  }
859 
860  $pOutput = $renderStatus->getValue();
861 
862  // Cache stale ParserOutput object with a short expiry
863  if ( $renderStatus->hasMessage( 'view-pool-dirty-output' ) ) {
864  $outputPage->setCdnMaxage( $context->getConfig()->get( MainConfigNames::CdnMaxageStale ) );
865  $outputPage->setLastModified( $pOutput->getCacheTime() );
866  $staleReason = $renderStatus->hasMessage( 'view-pool-contention' )
867  ? $context->msg( 'view-pool-contention' )->escaped()
868  : $context->msg( 'view-pool-timeout' )->escaped();
869  $outputPage->addHTML( "<!-- parser cache is expired, " .
870  "sending anyway due to $staleReason-->\n" );
871 
872  // Ensure OutputPage knowns the id from the dirty cache, but keep the current flag (T341013)
873  $cachedId = $pOutput->getCacheRevisionId();
874  if ( $cachedId !== null ) {
875  $outputPage->setRevisionId( $cachedId );
876  $outputPage->setRevisionTimestamp( $pOutput->getTimestamp() );
877  }
878  }
879 
880  $outputPage->addParserOutput( $pOutput, $textOptions );
881 
882  if ( $this->getRevisionRedirectTarget( $rev ) ) {
883  $outputPage->addSubtitle( "<span id=\"redirectsub\">" .
884  $context->msg( 'redirectpagesub' )->parse() . "</span>" );
885  }
886  }
887 
892  private function getRevisionRedirectTarget( RevisionRecord $revision ) {
893  // TODO: find a *good* place for the code that determines the redirect target for
894  // a given revision!
895  // NOTE: Use main slot content. Compare code in DerivedPageDataUpdater::revisionIsRedirect.
896  $content = $revision->getContent( SlotRecord::MAIN );
897  return $content ? $content->getRedirectTarget() : null;
898  }
899 
904  public function adjustDisplayTitle( ParserOutput $pOutput ) {
905  $out = $this->getContext()->getOutput();
906 
907  # Adjust the title if it was set by displaytitle, -{T|}- or language conversion
908  $titleText = $pOutput->getTitleText();
909  if ( strval( $titleText ) !== '' ) {
910  $out->setPageTitle( $titleText );
911  $out->setDisplayTitle( $titleText );
912  }
913  }
914 
919  protected function showDiffPage() {
920  $context = $this->getContext();
921  $request = $context->getRequest();
922  $diff = $request->getVal( 'diff' );
923  $rcid = $request->getInt( 'rcid' );
924  $purge = $request->getRawVal( 'action' ) === 'purge';
925  $unhide = $request->getInt( 'unhide' ) === 1;
926  $oldid = $this->getOldID();
927 
928  $rev = $this->fetchRevisionRecord();
929 
930  if ( !$rev ) {
931  // T213621: $rev maybe null due to either lack of permission to view the
932  // revision or actually not existing. So let's try loading it from the id
933  $rev = $this->revisionStore->getRevisionById( $oldid );
934  if ( $rev ) {
935  // Revision exists but $user lacks permission to diff it.
936  // Do nothing here.
937  // The $rev will later be used to create standard diff elements however.
938  } else {
939  $context->getOutput()->setPageTitleMsg( $context->msg( 'errorpagetitle' ) );
940  $msg = $context->msg( 'difference-missing-revision' )
941  ->params( $oldid )
942  ->numParams( 1 )
943  ->parseAsBlock();
944  $context->getOutput()->addHTML( $msg );
945  return;
946  }
947  }
948 
949  $services = MediaWikiServices::getInstance();
950 
951  $contentHandler = $services
952  ->getContentHandlerFactory()
953  ->getContentHandler(
954  $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
955  );
956  $de = $contentHandler->createDifferenceEngine(
957  $context,
958  $oldid,
959  $diff,
960  $rcid,
961  $purge,
962  $unhide
963  );
964 
965  $diffType = $request->getVal( 'diff-type' );
966 
967  if ( $diffType === null ) {
968  $diffType = $this->userOptionsLookup
969  ->getOption( $context->getUser(), 'diff-type' );
970  } else {
971  $de->setExtraQueryParams( [ 'diff-type' => $diffType ] );
972  }
973 
974  $de->setSlotDiffOptions( [
975  'diff-type' => $diffType,
976  'expand-url' => $this->viewIsRenderAction,
977  'inline-toggle' => true,
978  ] );
979  $de->showDiffPage( $this->isDiffOnlyView() );
980 
981  // Run view updates for the newer revision being diffed (and shown
982  // below the diff if not diffOnly).
983  [ , $new ] = $de->mapDiffPrevNext( $oldid, $diff );
984  // New can be false, convert it to 0 - this conveniently means the latest revision
985  $this->mPage->doViewUpdates( $context->getAuthority(), (int)$new );
986 
987  // Add link to help page; see T321569
988  $context->getOutput()->addHelpLink( 'Help:Diff' );
989  }
990 
991  protected function isDiffOnlyView() {
992  return $this->getContext()->getRequest()->getBool(
993  'diffonly',
994  $this->userOptionsLookup->getBoolOption( $this->getContext()->getUser(), 'diffonly' )
995  );
996  }
997 
1005  public function getRobotPolicy( $action, ParserOutput $pOutput = null ) {
1006  $context = $this->getContext();
1007  $mainConfig = $context->getConfig();
1008  $articleRobotPolicies = $mainConfig->get( MainConfigNames::ArticleRobotPolicies );
1009  $namespaceRobotPolicies = $mainConfig->get( MainConfigNames::NamespaceRobotPolicies );
1010  $defaultRobotPolicy = $mainConfig->get( MainConfigNames::DefaultRobotPolicy );
1011  $title = $this->getTitle();
1012  $ns = $title->getNamespace();
1013 
1014  # Don't index user and user talk pages for blocked users (T13443)
1015  if ( ( $ns === NS_USER || $ns === NS_USER_TALK ) && !$title->isSubpage() ) {
1016  $specificTarget = null;
1017  $vagueTarget = null;
1018  $titleText = $title->getText();
1019  if ( IPUtils::isValid( $titleText ) ) {
1020  $vagueTarget = $titleText;
1021  } else {
1022  $specificTarget = $titleText;
1023  }
1024  if ( DatabaseBlock::newFromTarget( $specificTarget, $vagueTarget ) instanceof DatabaseBlock ) {
1025  return [
1026  'index' => 'noindex',
1027  'follow' => 'nofollow'
1028  ];
1029  }
1030  }
1031 
1032  if ( $this->mPage->getId() === 0 || $this->getOldID() ) {
1033  # Non-articles (special pages etc), and old revisions
1034  return [
1035  'index' => 'noindex',
1036  'follow' => 'nofollow'
1037  ];
1038  } elseif ( $context->getOutput()->isPrintable() ) {
1039  # Discourage indexing of printable versions, but encourage following
1040  return [
1041  'index' => 'noindex',
1042  'follow' => 'follow'
1043  ];
1044  } elseif ( $context->getRequest()->getInt( 'curid' ) ) {
1045  # For ?curid=x urls, disallow indexing
1046  return [
1047  'index' => 'noindex',
1048  'follow' => 'follow'
1049  ];
1050  }
1051 
1052  # Otherwise, construct the policy based on the various config variables.
1053  $policy = self::formatRobotPolicy( $defaultRobotPolicy );
1054 
1055  if ( isset( $namespaceRobotPolicies[$ns] ) ) {
1056  # Honour customised robot policies for this namespace
1057  $policy = array_merge(
1058  $policy,
1059  self::formatRobotPolicy( $namespaceRobotPolicies[$ns] )
1060  );
1061  }
1062  if ( $title->canUseNoindex() && $pOutput && $pOutput->getIndexPolicy() ) {
1063  # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates
1064  # a final check that we have really got the parser output.
1065  $policy = array_merge(
1066  $policy,
1067  [ 'index' => $pOutput->getIndexPolicy() ]
1068  );
1069  }
1070 
1071  if ( isset( $articleRobotPolicies[$title->getPrefixedText()] ) ) {
1072  # (T16900) site config can override user-defined __INDEX__ or __NOINDEX__
1073  $policy = array_merge(
1074  $policy,
1075  self::formatRobotPolicy( $articleRobotPolicies[$title->getPrefixedText()] )
1076  );
1077  }
1078 
1079  return $policy;
1080  }
1081 
1089  public static function formatRobotPolicy( $policy ) {
1090  if ( is_array( $policy ) ) {
1091  return $policy;
1092  } elseif ( !$policy ) {
1093  return [];
1094  }
1095 
1096  $arr = [];
1097  foreach ( explode( ',', $policy ) as $var ) {
1098  $var = trim( $var );
1099  if ( $var === 'index' || $var === 'noindex' ) {
1100  $arr['index'] = $var;
1101  } elseif ( $var === 'follow' || $var === 'nofollow' ) {
1102  $arr['follow'] = $var;
1103  }
1104  }
1105 
1106  return $arr;
1107  }
1108 
1116  public function showRedirectedFromHeader() {
1117  $context = $this->getContext();
1118  $redirectSources = $context->getConfig()->get( MainConfigNames::RedirectSources );
1119  $outputPage = $context->getOutput();
1120  $request = $context->getRequest();
1121  $rdfrom = $request->getVal( 'rdfrom' );
1122 
1123  // Construct a URL for the current page view, but with the target title
1124  $query = $request->getValues();
1125  unset( $query['rdfrom'] );
1126  unset( $query['title'] );
1127  if ( $this->getTitle()->isRedirect() ) {
1128  // Prevent double redirects
1129  $query['redirect'] = 'no';
1130  }
1131  $redirectTargetUrl = $this->getTitle()->getLinkURL( $query );
1132 
1133  if ( isset( $this->mRedirectedFrom ) ) {
1134  // This is an internally redirected page view.
1135  // We'll need a backlink to the source page for navigation.
1136  if ( $this->getHookRunner()->onArticleViewRedirect( $this ) ) {
1137  $redir = $this->linkRenderer->makeKnownLink(
1138  $this->mRedirectedFrom,
1139  null,
1140  [],
1141  [ 'redirect' => 'no' ]
1142  );
1143 
1144  $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1145  $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1146  . "</span>" );
1147 
1148  // Add the script to update the displayed URL and
1149  // set the fragment if one was specified in the redirect
1150  $outputPage->addJsConfigVars( [
1151  'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1152  ] );
1153  $outputPage->addModules( 'mediawiki.action.view.redirect' );
1154 
1155  // Add a <link rel="canonical"> tag
1156  $outputPage->setCanonicalUrl( $this->getTitle()->getCanonicalURL() );
1157 
1158  // Tell the output object that the user arrived at this article through a redirect
1159  $outputPage->setRedirectedFrom( $this->mRedirectedFrom );
1160 
1161  return true;
1162  }
1163  } elseif ( $rdfrom ) {
1164  // This is an externally redirected view, from some other wiki.
1165  // If it was reported from a trusted site, supply a backlink.
1166  if ( $redirectSources && preg_match( $redirectSources, $rdfrom ) ) {
1167  $redir = Linker::makeExternalLink( $rdfrom, $rdfrom );
1168  $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1169  $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1170  . "</span>" );
1171 
1172  // Add the script to update the displayed URL
1173  $outputPage->addJsConfigVars( [
1174  'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1175  ] );
1176  $outputPage->addModules( 'mediawiki.action.view.redirect' );
1177 
1178  return true;
1179  }
1180  }
1181 
1182  return false;
1183  }
1184 
1189  public function showNamespaceHeader() {
1190  if ( $this->getTitle()->isTalkPage() && !$this->getContext()->msg( 'talkpageheader' )->isDisabled() ) {
1191  $this->getContext()->getOutput()->wrapWikiMsg(
1192  "<div class=\"mw-talkpageheader\">\n$1\n</div>",
1193  [ 'talkpageheader' ]
1194  );
1195  }
1196  }
1197 
1201  public function showViewFooter() {
1202  # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
1203  if ( $this->getTitle()->getNamespace() === NS_USER_TALK
1204  && IPUtils::isValid( $this->getTitle()->getText() )
1205  ) {
1206  $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' );
1207  }
1208 
1209  // Show a footer allowing the user to patrol the shown revision or page if possible
1210  $patrolFooterShown = $this->showPatrolFooter();
1211 
1212  $this->getHookRunner()->onArticleViewFooter( $this, $patrolFooterShown );
1213  }
1214 
1225  public function showPatrolFooter() {
1226  $context = $this->getContext();
1227  $mainConfig = $context->getConfig();
1228  $useNPPatrol = $mainConfig->get( MainConfigNames::UseNPPatrol );
1229  $useRCPatrol = $mainConfig->get( MainConfigNames::UseRCPatrol );
1230  $useFilePatrol = $mainConfig->get( MainConfigNames::UseFilePatrol );
1231  // Allow hooks to decide whether to not output this at all
1232  if ( !$this->getHookRunner()->onArticleShowPatrolFooter( $this ) ) {
1233  return false;
1234  }
1235 
1236  $outputPage = $context->getOutput();
1237  $user = $context->getUser();
1238  $title = $this->getTitle();
1239  $rc = false;
1240 
1241  if ( !$context->getAuthority()->probablyCan( 'patrol', $title )
1242  || !( $useRCPatrol || $useNPPatrol
1243  || ( $useFilePatrol && $title->inNamespace( NS_FILE ) ) )
1244  ) {
1245  // Patrolling is disabled or the user isn't allowed to
1246  return false;
1247  }
1248 
1249  if ( $this->mRevisionRecord
1250  && !RecentChange::isInRCLifespan( $this->mRevisionRecord->getTimestamp(), 21600 )
1251  ) {
1252  // The current revision is already older than what could be in the RC table
1253  // 6h tolerance because the RC might not be cleaned out regularly
1254  return false;
1255  }
1256 
1257  // Check for cached results
1258  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1259  $key = $cache->makeKey( 'unpatrollable-page', $title->getArticleID() );
1260  if ( $cache->get( $key ) ) {
1261  return false;
1262  }
1263 
1264  $dbr = wfGetDB( DB_REPLICA );
1265  $oldestRevisionRow = $dbr->newSelectQueryBuilder()
1266  ->select( [ 'rev_id', 'rev_timestamp' ] )
1267  ->from( 'revision' )
1268  ->where( [ 'rev_page' => $title->getArticleID() ] )
1269  ->orderBy( [ 'rev_timestamp', 'rev_id' ] )
1270  ->caller( __METHOD__ )->fetchRow();
1271  $oldestRevisionTimestamp = $oldestRevisionRow ? $oldestRevisionRow->rev_timestamp : false;
1272 
1273  // New page patrol: Get the timestamp of the oldest revision which
1274  // the revision table holds for the given page. Then we look
1275  // whether it's within the RC lifespan and if it is, we try
1276  // to get the recentchanges row belonging to that entry.
1277  $recentPageCreation = false;
1278  if ( $oldestRevisionTimestamp
1279  && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 )
1280  ) {
1281  // 6h tolerance because the RC might not be cleaned out regularly
1282  $recentPageCreation = true;
1284  [
1285  'rc_this_oldid' => intval( $oldestRevisionRow->rev_id ),
1286  // Avoid selecting a categorization entry
1287  'rc_type' => RC_NEW,
1288  ],
1289  __METHOD__
1290  );
1291  if ( $rc ) {
1292  // Use generic patrol message for new pages
1293  $markPatrolledMsg = $context->msg( 'markaspatrolledtext' );
1294  }
1295  }
1296 
1297  // File patrol: Get the timestamp of the latest upload for this page,
1298  // check whether it is within the RC lifespan and if it is, we try
1299  // to get the recentchanges row belonging to that entry
1300  // (with rc_type = RC_LOG, rc_log_type = upload).
1301  $recentFileUpload = false;
1302  if ( ( !$rc || $rc->getAttribute( 'rc_patrolled' ) ) && $useFilePatrol
1303  && $title->getNamespace() === NS_FILE ) {
1304  // Retrieve timestamp from the current file (lastest upload)
1305  $newestUploadTimestamp = $dbr->newSelectQueryBuilder()
1306  ->select( 'img_timestamp' )
1307  ->from( 'image' )
1308  ->where( [ 'img_name' => $title->getDBkey() ] )
1309  ->caller( __METHOD__ )->fetchField();
1310  if ( $newestUploadTimestamp
1311  && RecentChange::isInRCLifespan( $newestUploadTimestamp, 21600 )
1312  ) {
1313  // 6h tolerance because the RC might not be cleaned out regularly
1314  $recentFileUpload = true;
1316  [
1317  'rc_type' => RC_LOG,
1318  'rc_log_type' => 'upload',
1319  'rc_timestamp' => $newestUploadTimestamp,
1320  'rc_namespace' => NS_FILE,
1321  'rc_cur_id' => $title->getArticleID()
1322  ],
1323  __METHOD__
1324  );
1325  if ( $rc ) {
1326  // Use patrol message specific to files
1327  $markPatrolledMsg = $context->msg( 'markaspatrolledtext-file' );
1328  }
1329  }
1330  }
1331 
1332  if ( !$recentPageCreation && !$recentFileUpload ) {
1333  // Page creation and latest upload (for files) is too old to be in RC
1334 
1335  // We definitely can't patrol so cache the information
1336  // When a new file version is uploaded, the cache is cleared
1337  $cache->set( $key, '1' );
1338 
1339  return false;
1340  }
1341 
1342  if ( !$rc ) {
1343  // Don't cache: This can be hit if the page gets accessed very fast after
1344  // its creation / latest upload or in case we have high replica DB lag. In case
1345  // the revision is too old, we will already return above.
1346  return false;
1347  }
1348 
1349  if ( $rc->getAttribute( 'rc_patrolled' ) ) {
1350  // Patrolled RC entry around
1351 
1352  // Cache the information we gathered above in case we can't patrol
1353  // Don't cache in case we can patrol as this could change
1354  $cache->set( $key, '1' );
1355 
1356  return false;
1357  }
1358 
1359  if ( $rc->getPerformerIdentity()->equals( $user ) ) {
1360  // Don't show a patrol link for own creations/uploads. If the user could
1361  // patrol them, they already would be patrolled
1362  return false;
1363  }
1364 
1365  $outputPage->setPreventClickjacking( true );
1366  if ( $context->getAuthority()->isAllowed( 'writeapi' ) ) {
1367  $outputPage->addModules( 'mediawiki.misc-authed-curate' );
1368  }
1369 
1370  $link = $this->linkRenderer->makeKnownLink(
1371  $title,
1372  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $markPatrolledMsg is always set when $rc is set
1373  $markPatrolledMsg->text(),
1374  [],
1375  [
1376  'action' => 'markpatrolled',
1377  'rcid' => $rc->getAttribute( 'rc_id' ),
1378  ]
1379  );
1380 
1381  $outputPage->addModuleStyles( 'mediawiki.action.styles' );
1382  $outputPage->addHTML(
1383  "<div class='patrollink' data-mw='interface'>" .
1384  $context->msg( 'markaspatrolledlink' )->rawParams( $link )->escaped() .
1385  '</div>'
1386  );
1387 
1388  return true;
1389  }
1390 
1397  public static function purgePatrolFooterCache( $articleID ) {
1398  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1399  $cache->delete( $cache->makeKey( 'unpatrollable-page', $articleID ) );
1400  }
1401 
1406  public function showMissingArticle() {
1407  $context = $this->getContext();
1408  $send404Code = $context->getConfig()->get( MainConfigNames::Send404Code );
1409 
1410  $outputPage = $context->getOutput();
1411  // Whether the page is a root user page of an existing user (but not a subpage)
1412  $validUserPage = false;
1413 
1414  $title = $this->getTitle();
1415 
1416  $services = MediaWikiServices::getInstance();
1417 
1418  $contextUser = $context->getUser();
1419 
1420  # Show info in user (talk) namespace. Does the user exist? Is he blocked?
1421  if ( $title->getNamespace() === NS_USER
1422  || $title->getNamespace() === NS_USER_TALK
1423  ) {
1424  $rootPart = $title->getRootText();
1425  $user = User::newFromName( $rootPart, false /* allow IP users */ );
1426  $ip = $this->userNameUtils->isIP( $rootPart );
1427  $block = DatabaseBlock::newFromTarget( $user, $user );
1428 
1429  if ( $user && $user->isRegistered() && $user->isHidden() &&
1430  !$context->getAuthority()->isAllowed( 'hideuser' )
1431  ) {
1432  // T120883 if the user is hidden and the viewer cannot see hidden
1433  // users, pretend like it does not exist at all.
1434  $user = false;
1435  }
1436 
1437  if ( !( $user && $user->isRegistered() ) && !$ip ) {
1438  // User does not exist
1439  $outputPage->addHTML( Html::warningBox(
1440  $context->msg( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) )->parse(),
1441  'mw-userpage-userdoesnotexist'
1442  ) );
1443 
1444  // Show renameuser log extract
1446  $outputPage,
1447  'renameuser',
1448  Title::makeTitleSafe( NS_USER, $rootPart ),
1449  '',
1450  [
1451  'lim' => 10,
1452  'showIfEmpty' => false,
1453  'msgKey' => [ 'renameuser-renamed-notice', $title->getBaseText() ]
1454  ]
1455  );
1456  } elseif (
1457  $block !== null &&
1458  $block->getType() != DatabaseBlock::TYPE_AUTO &&
1459  (
1460  $block->isSitewide() ||
1461  $services->getPermissionManager()->isBlockedFrom( $user, $title, true )
1462  )
1463  ) {
1464  // Show log extract if the user is sitewide blocked or is partially
1465  // blocked and not allowed to edit their user page or user talk page
1467  $outputPage,
1468  'block',
1469  $services->getNamespaceInfo()->getCanonicalName( NS_USER ) . ':' .
1470  $block->getTargetName(),
1471  '',
1472  [
1473  'lim' => 1,
1474  'showIfEmpty' => false,
1475  'msgKey' => [
1476  'blocked-notice-logextract',
1477  $user->getName() # Support GENDER in notice
1478  ]
1479  ]
1480  );
1481  $validUserPage = !$title->isSubpage();
1482  } else {
1483  $validUserPage = !$title->isSubpage();
1484  }
1485  }
1486 
1487  $this->getHookRunner()->onShowMissingArticle( $this );
1488 
1489  # Show delete and move logs if there were any such events.
1490  # The logging query can DOS the site when bots/crawlers cause 404 floods,
1491  # so be careful showing this. 404 pages must be cheap as they are hard to cache.
1492  $dbCache = MediaWikiServices::getInstance()->getMainObjectStash();
1493  $key = $dbCache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
1494  $isRegistered = $contextUser->isRegistered();
1495  $sessionExists = $context->getRequest()->getSession()->isPersistent();
1496 
1497  if ( $isRegistered || $dbCache->get( $key ) || $sessionExists ) {
1498  $logTypes = [ 'delete', 'move', 'protect' ];
1499 
1500  $dbr = wfGetDB( DB_REPLICA );
1501 
1502  $conds = [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ];
1503  // Give extensions a chance to hide their (unrelated) log entries
1504  $this->getHookRunner()->onArticle__MissingArticleConditions( $conds, $logTypes );
1506  $outputPage,
1507  $logTypes,
1508  $title,
1509  '',
1510  [
1511  'lim' => 10,
1512  'conds' => $conds,
1513  'showIfEmpty' => false,
1514  'msgKey' => [ $isRegistered || $sessionExists
1515  ? 'moveddeleted-notice'
1516  : 'moveddeleted-notice-recent'
1517  ]
1518  ]
1519  );
1520  }
1521 
1522  if ( !$this->mPage->hasViewableContent() && $send404Code && !$validUserPage ) {
1523  // If there's no backing content, send a 404 Not Found
1524  // for better machine handling of broken links.
1525  $context->getRequest()->response()->statusHeader( 404 );
1526  }
1527 
1528  // Also apply the robot policy for nonexisting pages (even if a 404 was used)
1529  $policy = $this->getRobotPolicy( 'view' );
1530  $outputPage->setIndexPolicy( $policy['index'] );
1531  $outputPage->setFollowPolicy( $policy['follow'] );
1532 
1533  $hookResult = $this->getHookRunner()->onBeforeDisplayNoArticleText( $this );
1534 
1535  if ( !$hookResult ) {
1536  return;
1537  }
1538 
1539  # Show error message
1540  $oldid = $this->getOldID();
1541  if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) {
1542  $text = $this->getTitle()->getDefaultMessageText() ?? '';
1543  $outputPage->addWikiTextAsContent( $text );
1544  } else {
1545  if ( $oldid ) {
1546  // T251066: Try loading the revision from the archive table.
1547  // Show link to view it if it exists and the user has permission to view it.
1548  $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord( $title, $oldid );
1549  if ( $revRecord && $revRecord->userCan(
1550  RevisionRecord::DELETED_TEXT,
1551  $context->getAuthority()
1552  ) && $context->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
1553  $text = $context->msg(
1554  'missing-revision-permission', $oldid,
1555  $revRecord->getTimestamp(),
1556  $title->getPrefixedDBkey()
1557  )->plain();
1558  } else {
1559  $text = $context->msg( 'missing-revision', $oldid )->plain();
1560  }
1561 
1562  } elseif ( $context->getAuthority()->probablyCan( 'edit', $title ) ) {
1563  $message = $isRegistered ? 'noarticletext' : 'noarticletextanon';
1564  $text = $context->msg( $message )->plain();
1565  } else {
1566  $text = $context->msg( 'noarticletext-nopermission' )->plain();
1567  }
1568 
1569  $dir = $context->getLanguage()->getDir();
1570  $lang = $context->getLanguage()->getHtmlCode();
1571  $outputPage->addWikiTextAsInterface( Xml::openElement( 'div', [
1572  'class' => "noarticletext mw-content-$dir",
1573  'dir' => $dir,
1574  'lang' => $lang,
1575  ] ) . "\n$text\n</div>" );
1576  }
1577  }
1578 
1583  private function showViewError( string $errortext ) {
1584  $outputPage = $this->getContext()->getOutput();
1585  $outputPage->setPageTitleMsg( $this->getContext()->msg( 'errorpagetitle' ) );
1586  $outputPage->disableClientCache();
1587  $outputPage->setRobotPolicy( 'noindex,nofollow' );
1588  $outputPage->clearHTML();
1589  $outputPage->addHTML( Html::errorBox( $outputPage->parseAsContent( $errortext ) ) );
1590  }
1591 
1598  public function showDeletedRevisionHeader() {
1599  if ( !$this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1600  // Not deleted
1601  return true;
1602  }
1603  $outputPage = $this->getContext()->getOutput();
1604  // Used in wikilinks, should not contain whitespaces
1605  $titleText = $this->getTitle()->getPrefixedDBkey();
1606  // If the user is not allowed to see it...
1607  if ( !$this->mRevisionRecord->userCan(
1608  RevisionRecord::DELETED_TEXT,
1609  $this->getContext()->getAuthority()
1610  ) ) {
1611  $outputPage->addHTML(
1612  Html::warningBox(
1613  $outputPage->msg( 'rev-deleted-text-permission', $titleText )->parse(),
1614  'plainlinks'
1615  )
1616  );
1617 
1618  return false;
1619  // If the user needs to confirm that they want to see it...
1620  } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) !== 1 ) {
1621  # Give explanation and add a link to view the revision...
1622  $oldid = intval( $this->getOldID() );
1623  $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" );
1624  $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ?
1625  'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
1626  $outputPage->addHTML(
1627  Html::warningBox(
1628  $outputPage->msg( $msg, $link )->parse(),
1629  'plainlinks'
1630  )
1631  );
1632 
1633  return false;
1634  // We are allowed to see...
1635  } else {
1636  $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
1637  ? [ 'rev-suppressed-text-view', $titleText ]
1638  : [ 'rev-deleted-text-view', $titleText ];
1639  $outputPage->addHTML(
1640  Html::warningBox(
1641  $outputPage->msg( $msg[0], $msg[1] )->parse(),
1642  'plainlinks'
1643  )
1644  );
1645 
1646  return true;
1647  }
1648  }
1649 
1658  public function setOldSubtitle( $oldid = 0 ) {
1659  if ( !$this->getHookRunner()->onDisplayOldSubtitle( $this, $oldid ) ) {
1660  return;
1661  }
1662 
1663  $context = $this->getContext();
1664  $unhide = $context->getRequest()->getInt( 'unhide' ) === 1;
1665 
1666  # Cascade unhide param in links for easy deletion browsing
1667  $extraParams = [];
1668  if ( $unhide ) {
1669  $extraParams['unhide'] = 1;
1670  }
1671 
1672  if ( $this->mRevisionRecord && $this->mRevisionRecord->getId() === $oldid ) {
1673  $revisionRecord = $this->mRevisionRecord;
1674  } else {
1675  $revisionRecord = $this->revisionStore->getRevisionById( $oldid );
1676  }
1677  if ( !$revisionRecord ) {
1678  throw new LogicException( 'There should be a revision record at this point.' );
1679  }
1680 
1681  $timestamp = $revisionRecord->getTimestamp();
1682 
1683  $current = ( $oldid == $this->mPage->getLatest() );
1684  $language = $context->getLanguage();
1685  $user = $context->getUser();
1686 
1687  $td = $language->userTimeAndDate( $timestamp, $user );
1688  $tddate = $language->userDate( $timestamp, $user );
1689  $tdtime = $language->userTime( $timestamp, $user );
1690 
1691  # Show user links if allowed to see them. If hidden, then show them only if requested...
1692  $userlinks = Linker::revUserTools( $revisionRecord, !$unhide );
1693 
1694  $infomsg = $current && !$context->msg( 'revision-info-current' )->isDisabled()
1695  ? 'revision-info-current'
1696  : 'revision-info';
1697 
1698  $outputPage = $context->getOutput();
1699  $outputPage->addModuleStyles( [
1700  'mediawiki.action.styles',
1701  'mediawiki.interface.helpers.styles'
1702  ] );
1703 
1704  $revisionUser = $revisionRecord->getUser();
1705  $revisionInfo = "<div id=\"mw-{$infomsg}\">" .
1706  $context->msg( $infomsg, $td )
1707  ->rawParams( $userlinks )
1708  ->params(
1709  $revisionRecord->getId(),
1710  $tddate,
1711  $tdtime,
1712  $revisionUser ? $revisionUser->getName() : ''
1713  )
1714  ->rawParams( $this->commentFormatter->formatRevision(
1715  $revisionRecord,
1716  $user,
1717  true,
1718  !$unhide
1719  ) )
1720  ->parse() .
1721  "</div>";
1722 
1723  $lnk = $current
1724  ? $context->msg( 'currentrevisionlink' )->escaped()
1725  : $this->linkRenderer->makeKnownLink(
1726  $this->getTitle(),
1727  $context->msg( 'currentrevisionlink' )->text(),
1728  [],
1729  $extraParams
1730  );
1731  $curdiff = $current
1732  ? $context->msg( 'diff' )->escaped()
1733  : $this->linkRenderer->makeKnownLink(
1734  $this->getTitle(),
1735  $context->msg( 'diff' )->text(),
1736  [],
1737  [
1738  'diff' => 'cur',
1739  'oldid' => $oldid
1740  ] + $extraParams
1741  );
1742  $prevExist = (bool)$this->revisionStore->getPreviousRevision( $revisionRecord );
1743  $prevlink = $prevExist
1744  ? $this->linkRenderer->makeKnownLink(
1745  $this->getTitle(),
1746  $context->msg( 'previousrevision' )->text(),
1747  [],
1748  [
1749  'direction' => 'prev',
1750  'oldid' => $oldid
1751  ] + $extraParams
1752  )
1753  : $context->msg( 'previousrevision' )->escaped();
1754  $prevdiff = $prevExist
1755  ? $this->linkRenderer->makeKnownLink(
1756  $this->getTitle(),
1757  $context->msg( 'diff' )->text(),
1758  [],
1759  [
1760  'diff' => 'prev',
1761  'oldid' => $oldid
1762  ] + $extraParams
1763  )
1764  : $context->msg( 'diff' )->escaped();
1765  $nextlink = $current
1766  ? $context->msg( 'nextrevision' )->escaped()
1767  : $this->linkRenderer->makeKnownLink(
1768  $this->getTitle(),
1769  $context->msg( 'nextrevision' )->text(),
1770  [],
1771  [
1772  'direction' => 'next',
1773  'oldid' => $oldid
1774  ] + $extraParams
1775  );
1776  $nextdiff = $current
1777  ? $context->msg( 'diff' )->escaped()
1778  : $this->linkRenderer->makeKnownLink(
1779  $this->getTitle(),
1780  $context->msg( 'diff' )->text(),
1781  [],
1782  [
1783  'diff' => 'next',
1784  'oldid' => $oldid
1785  ] + $extraParams
1786  );
1787 
1788  $cdel = Linker::getRevDeleteLink(
1789  $context->getAuthority(),
1790  $revisionRecord,
1791  $this->getTitle()
1792  );
1793  if ( $cdel !== '' ) {
1794  $cdel .= ' ';
1795  }
1796 
1797  // the outer div is need for styling the revision info and nav in MobileFrontend
1798  $outputPage->addSubtitle(
1799  Html::warningBox(
1800  $revisionInfo .
1801  "<div id=\"mw-revision-nav\">" . $cdel .
1802  $context->msg( 'revision-nav' )->rawParams(
1803  $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff
1804  )->escaped() . "</div>",
1805  'mw-revision'
1806  )
1807  );
1808  }
1809 
1822  public static function getRedirectHeaderHtml( Language $lang, Title $target, $forceKnown = false ) {
1823  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1824 
1825  $html = '<ul class="redirectText">';
1826  if ( $forceKnown ) {
1827  $link = $linkRenderer->makeKnownLink(
1828  $target,
1829  $target->getFullText(),
1830  [],
1831  // Make sure wiki page redirects are not followed
1832  $target->isRedirect() ? [ 'redirect' => 'no' ] : []
1833  );
1834  } else {
1835  $link = $linkRenderer->makeLink(
1836  $target,
1837  $target->getFullText(),
1838  [],
1839  // Make sure wiki page redirects are not followed
1840  $target->isRedirect() ? [ 'redirect' => 'no' ] : []
1841  );
1842  }
1843  $html .= '<li>' . $link . '</li>';
1844  $html .= '</ul>';
1845 
1846  $redirectToText = wfMessage( 'redirectto' )->inLanguage( $lang )->escaped();
1847 
1848  return '<div class="redirectMsg">' .
1849  '<p>' . $redirectToText . '</p>' .
1850  $html .
1851  '</div>';
1852  }
1853 
1862  public function addHelpLink( $to, $overrideBaseUrl = false ) {
1863  $out = $this->getContext()->getOutput();
1864  $msg = $out->msg( 'namespace-' . $this->getTitle()->getNamespace() . '-helppage' );
1865 
1866  if ( !$msg->isDisabled() ) {
1867  $title = Title::newFromText( $msg->plain() );
1868  if ( $title instanceof Title ) {
1869  $out->addHelpLink( $title->getLocalURL(), true );
1870  }
1871  } else {
1872  $out->addHelpLink( $to, $overrideBaseUrl );
1873  }
1874  }
1875 
1879  public function render() {
1880  $this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' );
1881  $this->getContext()->getOutput()->setArticleBodyOnly( true );
1882  // We later set 'enableSectionEditLinks=false' based on this; also used by ImagePage
1883  $this->viewIsRenderAction = true;
1884  $this->view();
1885  }
1886 
1890  public function protect() {
1891  $form = new ProtectionForm( $this );
1892  $form->execute();
1893  }
1894 
1898  public function unprotect() {
1899  $this->protect();
1900  }
1901 
1902  /* Caching functions */
1903 
1911  protected function tryFileCache() {
1912  static $called = false;
1913 
1914  if ( $called ) {
1915  wfDebug( "Article::tryFileCache(): called twice!?" );
1916  return false;
1917  }
1918 
1919  $called = true;
1920  if ( $this->isFileCacheable() ) {
1921  $cache = new HTMLFileCache( $this->getTitle(), 'view' );
1922  if ( $cache->isCacheGood( $this->mPage->getTouched() ) ) {
1923  wfDebug( "Article::tryFileCache(): about to load file" );
1924  $cache->loadFromFileCache( $this->getContext() );
1925  return true;
1926  } else {
1927  wfDebug( "Article::tryFileCache(): starting buffer" );
1928  ob_start( [ &$cache, 'saveToFileCache' ] );
1929  }
1930  } else {
1931  wfDebug( "Article::tryFileCache(): not cacheable" );
1932  }
1933 
1934  return false;
1935  }
1936 
1942  public function isFileCacheable( $mode = HTMLFileCache::MODE_NORMAL ) {
1943  $cacheable = false;
1944 
1945  if ( HTMLFileCache::useFileCache( $this->getContext(), $mode ) ) {
1946  $cacheable = $this->mPage->getId()
1947  && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect();
1948  // Extension may have reason to disable file caching on some pages.
1949  if ( $cacheable ) {
1950  $cacheable = $this->getHookRunner()->onIsFileCacheable( $this ) ?? false;
1951  }
1952  }
1953 
1954  return $cacheable;
1955  }
1956 
1970  public function getParserOutput( $oldid = null, UserIdentity $user = null ) {
1971  if ( $user === null ) {
1972  $parserOptions = $this->getParserOptions();
1973  } else {
1974  $parserOptions = $this->mPage->makeParserOptions( $user );
1975  $parserOptions->setRenderReason( 'page-view' );
1976  }
1977 
1978  return $this->mPage->getParserOutput( $parserOptions, $oldid );
1979  }
1980 
1985  public function getParserOptions() {
1986  $parserOptions = $this->mPage->makeParserOptions( $this->getContext() );
1987  $parserOptions->setRenderReason( 'page-view' );
1988  return $parserOptions;
1989  }
1990 
1997  public function setContext( $context ) {
1998  $this->mContext = $context;
1999  }
2000 
2007  public function getContext(): IContextSource {
2008  if ( $this->mContext instanceof IContextSource ) {
2009  return $this->mContext;
2010  } else {
2011  wfDebug( __METHOD__ . " called and \$mContext is null. " .
2012  "Return RequestContext::getMain()" );
2013  return RequestContext::getMain();
2014  }
2015  }
2016 
2026  public function __get( $fname ) {
2027  wfDeprecatedMsg( "Accessing Article::\$$fname is deprecated since MediaWiki 1.35",
2028  '1.35' );
2029 
2030  if ( property_exists( $this->mPage, $fname ) ) {
2031  return $this->mPage->$fname;
2032  }
2033  trigger_error( 'Inaccessible property via __get(): ' . $fname, E_USER_NOTICE );
2034  }
2035 
2045  public function __set( $fname, $fvalue ) {
2046  wfDeprecatedMsg( "Setting Article::\$$fname is deprecated since MediaWiki 1.35",
2047  '1.35' );
2048 
2049  if ( property_exists( $this->mPage, $fname ) ) {
2050  $this->mPage->$fname = $fvalue;
2051  // Note: extensions may want to toss on new fields
2052  } elseif ( !in_array( $fname, [ 'mContext', 'mPage' ] ) ) {
2053  $this->mPage->$fname = $fvalue;
2054  } else {
2055  trigger_error( 'Inaccessible property via __set(): ' . $fname, E_USER_NOTICE );
2056  }
2057  }
2058 
2064  public function getActionOverrides() {
2065  return $this->mPage->getActionOverrides();
2066  }
2067 }
const NS_USER
Definition: Defines.php:66
const NS_FILE
Definition: Defines.php:70
const RC_NEW
Definition: Defines.php:117
const NS_MEDIAWIKI
Definition: Defines.php:72
const RC_LOG
Definition: Defines.php:118
const NS_MEDIA
Definition: Defines.php:52
const NS_USER_TALK
Definition: Defines.php:67
const NS_CATEGORY
Definition: Defines.php:78
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
getContext()
if(!defined('MW_SETUP_CALLBACK'))
Definition: WebStart.php:88
Legacy class representing an editable page and handling UI for some page actions.
Definition: Article.php:61
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition: Article.php:225
getContext()
Gets the context this Article is executed in.
Definition: Article.php:2007
getOldIDFromRequest()
Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect.
Definition: Article.php:299
getRedirectedFrom()
Get the page this view was redirected from.
Definition: Article.php:236
Title null $mRedirectedFrom
Title from which we were redirected here, if any.
Definition: Article.php:82
bool $viewIsRenderAction
Whether render() was called.
Definition: Article.php:105
view()
This is the default action of the index.php entry point: just view the page of the given title.
Definition: Article.php:458
__construct(Title $title, $oldId=null)
Definition: Article.php:151
getRobotPolicy( $action, ParserOutput $pOutput=null)
Get the robot policy to be used for the current view.
Definition: Article.php:1005
static purgePatrolFooterCache( $articleID)
Purge the cache used to check if it is worth showing the patrol footer For example,...
Definition: Article.php:1397
ParserOutput null false $mParserOutput
The ParserOutput generated for viewing the page, initialized by view().
Definition: Article.php:98
getOldID()
Definition: Article.php:286
LinkRenderer $linkRenderer
Definition: Article.php:110
getTitle()
Get the title object of the article.
Definition: Article.php:254
getActionOverrides()
Call to WikiPage function for backwards compatibility.
Definition: Article.php:2064
isDiffOnlyView()
Definition: Article.php:991
adjustDisplayTitle(ParserOutput $pOutput)
Adjust title for pages with displaytitle, -{T|}- or language conversion.
Definition: Article.php:904
showDeletedRevisionHeader()
If the revision requested for view is deleted, check permissions.
Definition: Article.php:1598
getParserOptions()
Get parser options suitable for rendering the primary article wikitext.
Definition: Article.php:1985
clear()
Definition: Article.php:268
IContextSource null $mContext
The context this Article is executed in.
Definition: Article.php:70
static getRedirectHeaderHtml(Language $lang, Title $target, $forceKnown=false)
Return the HTML for the top of a redirect page.
Definition: Article.php:1822
getParserOutput( $oldid=null, UserIdentity $user=null)
#-
Definition: Article.php:1970
protect()
action=protect handler
Definition: Article.php:1890
string false $mRedirectUrl
URL to redirect to or false if none.
Definition: Article.php:85
isCurrent()
Returns true if the currently-referenced revision is the current edit to this page (and it exists).
Definition: Article.php:425
showMissingArticle()
Show the error text for a missing article.
Definition: Article.php:1406
__set( $fname, $fvalue)
Definition: Article.php:2045
unprotect()
action=unprotect handler (alias)
Definition: Article.php:1898
newPage(Title $title)
Definition: Article.php:170
getPage()
Get the WikiPage object of this instance.
Definition: Article.php:264
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: Article.php:1862
static newFromID( $id)
Constructor from a page id.
Definition: Article.php:179
int null $mOldId
The oldid of the article that was requested to be shown, 0 for the current revision.
Definition: Article.php:79
static formatRobotPolicy( $policy)
Converts a String robot policy into an associative array, to allow merging of several policies using ...
Definition: Article.php:1089
fetchRevisionRecord()
Fetches the revision to work on.
Definition: Article.php:367
showPatrolFooter()
If patrol is possible, output a patrol UI box.
Definition: Article.php:1225
setOldSubtitle( $oldid=0)
Generate the navigation links when browsing through an article revisions It shows the information as:...
Definition: Article.php:1658
showViewFooter()
Show the footer section of an ordinary page view.
Definition: Article.php:1201
WikiPage $mPage
The WikiPage object of this instance.
Definition: Article.php:73
setRedirectedFrom(Title $from)
Tell the page view functions that this view was redirected from another page on the wiki.
Definition: Article.php:245
isFileCacheable( $mode=HTMLFileCache::MODE_NORMAL)
Check if the page can be cached.
Definition: Article.php:1942
tryFileCache()
checkLastModified returns true if it has taken care of all output to the client that is necessary for...
Definition: Article.php:1911
getRevIdFetched()
Use this to fetch the rev ID used on page views.
Definition: Article.php:444
showNamespaceHeader()
Show a header specific to the namespace currently being viewed, like [[MediaWiki:Talkpagetext]].
Definition: Article.php:1189
__get( $fname)
Definition: Article.php:2026
static newFromTitle( $title, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition: Article.php:191
showDiffPage()
Show a diff page according to current request variables.
Definition: Article.php:919
render()
Handle action=render.
Definition: Article.php:1879
showRedirectedFromHeader()
If this request is a redirect view, send "redirected from" subtitle to the output.
Definition: Article.php:1116
setContext( $context)
Sets the context this Article is executed in.
Definition: Article.php:1997
getCacheRevisionId()
Definition: CacheTime.php:99
getCacheTime()
Definition: CacheTime.php:68
Special handling for category description pages.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Page view caching in the file system.
static useFileCache(IContextSource $context, $mode=self::MODE_NORMAL)
Check if pages can be cached for this request/user.
Rendering of file description pages.
Definition: ImagePage.php:37
Base class for language-specific code.
Definition: Language.php:61
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
This is the main service interface for converting single-line comments from various DB comment fields...
The HTML user interface for page editing.
Definition: EditPage.php:146
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:568
This class is a collection of static functions that serve two purposes:
Definition: Html.php:57
Class that generates HTML for internal links.
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
Make a link that's styled as if the target page exists (usually a "blue link", although the styling m...
makeLink( $target, $text=null, array $extraAttribs=[], array $query=[])
Render a wikilink.
Some internal bits split of from Skin.php.
Definition: Linker.php:65
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:93
parseAsContent( $text, $linestart=true)
Parse wikitext in the page content language and return the HTML.
disableClientCache()
Force the page to send nocache headers.
addJsConfigVars( $keys, $value=null)
Add one or more variables to be set in mw.config in JavaScript.
wrapWikiMsg( $wrap,... $msgSpecs)
This function takes a number of message/argument specifications, wraps them in some overall structure...
setIndexPolicy( $policy)
Set the index policy for the page, but leave the follow policy un- touched.
setPreventClickjacking(bool $enable)
Set the prevent-clickjacking flag.
disable()
Disable output completely, i.e.
setRedirectedFrom(PageReference $t)
Set $mRedirectedFrom, the page which redirected us to the current page.
setRevisionTimestamp( $timestamp)
Set the timestamp of the revision which will be displayed.
adaptCdnTTL( $mtime, $minTTL=0, $maxTTL=0)
Get TTL in [$minTTL,$maxTTL] and pass it to lowerCdnMaxage()
setCdnMaxage( $maxage)
Set the value of the "s-maxage" part of the "Cache-control" HTTP header.
setLastModified( $timestamp)
Override the last modified timestamp.
Definition: OutputPage.php:962
setRevisionIsCurrent(bool $isCurrent)
Set whether the revision displayed (as set in ::setRevisionId()) is the latest revision of the page.
setFollowPolicy( $policy)
Set the follow policy for the page, but leave the index policy un- touched.
clearHTML()
Clear the body HTML.
setPageTitle( $name)
"Page title" means the contents of <h1>.
setPageTitleMsg(Message $msg)
"Page title" means the contents of <h1>.
addModules( $modules)
Load one or more ResourceLoader modules on this page.
Definition: OutputPage.php:662
redirect( $url, $responsecode='302')
Redirect to $url rather than displaying the normal page.
Definition: OutputPage.php:465
setHTMLTitle( $name)
"HTML title" means the contents of "<title>".
prependHTML( $text)
Prepend $text to the body HTML.
setRobotPolicy( $policy)
Set the robot policy for the page: http://www.robotstxt.org/meta.html
Definition: OutputPage.php:973
setCanonicalUrl( $url)
Set the URL to be used for the <link rel=canonical>>.
Definition: OutputPage.php:548
addWikiTextAsContent( $text, $linestart=true, PageReference $title=null)
Convert wikitext in the page content language to HTML and add it to the buffer.
addHTML( $text)
Append $text to the body HTML.
setRevisionId( $revid)
Set the revision ID which will be seen by the wiki text parser for things such as embedded {{REVISION...
addSubtitle( $str)
Add $str to the subtitle.
addModuleStyles( $modules)
Load the styles of one or more style-only ResourceLoader modules on this page.
Definition: OutputPage.php:688
addParserOutput(ParserOutput $parserOutput, $poOptions=[])
Add everything from a ParserOutput object.
isPrintable()
Return whether the page is "printable".
setArticleFlag( $newVal)
Set whether the displayed content is related to the source of the corresponding article on the wiki S...
addWikiTextAsInterface( $text, $linestart=true, PageReference $title=null)
Convert wikitext in the user interface language to HTML and add it to the buffer.
Service for getting rendered output of a given page.
Handles the page protection UI and backend.
Service for creating WikiPage objects.
A StatusValue for permission errors.
Exception raised when the text of a revision is permanently missing or corrupt.
Page revision base class.
isCurrent()
Checks whether the revision record is a stored current revision.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getSlot( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns meta-data for the given slot.
getContent( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns the Content of the given slot of this revision.
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
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition: Status.php:202
Represents a title within MediaWiki.
Definition: Title.php:76
getFullText()
Get the prefixed title with spaces, plus any fragment (part beginning with '#')
Definition: Title.php:1909
isRedirect( $flags=0)
Is this an article that is a redirect page? Uses link cache, adding it if necessary.
Definition: Title.php:2623
UserNameUtils service.
Provides access to user options.
internal since 1.36
Definition: User.php:98
Set options of the Parser.
setIsPrintable( $x)
Parsing the printable version of the page?
setRenderReason(string $renderReason)
Sets reason for rendering the content.
getUseParsoid()
Parsoid-format HTML output, or legacy wikitext parser HTML?
static formatPageTitle( $nsText, $nsSeparator, $mainText)
Add HTML tags marking the parts of a page title, to be displayed in the first heading of the page.
Definition: Parser.php:6371
static newSpec(int $revisionId, PageRecord $page, array $params=[])
Show an error when a user tries to do something they do not have the necessary permissions for.
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 getMain()
Get the RequestContext object associated with the main request.
hasMessage( $message)
Returns true if the specified message is present as a warning or error.
isOK()
Returns whether the operation completed.
Base representation for an editable wiki page.
Definition: WikiPage.php:77
getTitle()
Get the title object of the article.
Definition: WikiPage.php:258
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:111
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
Interface for objects representing user identity.
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition: Page.php:30
Generic interface providing Time-To-Live constants for expirable object storage.
const DB_REPLICA
Definition: defines.php:26
$content
Definition: router.php:76
return true
Definition: router.php:90