MediaWiki  master
Article.php
Go to the documentation of this file.
1 <?php
23 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
40 use Wikimedia\IPUtils;
41 use Wikimedia\NonSerializable\NonSerializableTrait;
42 
52 class Article implements Page {
53  use ProtectedHookAccessorTrait;
54  use NonSerializableTrait;
55 
61  protected $mContext;
62 
64  protected $mPage;
65 
70  public $mOldId;
71 
73  public $mRedirectedFrom = null;
74 
76  public $mRedirectUrl = false;
77 
82  private $fetchResult = null;
83 
89  public $mParserOutput = null;
90 
96  protected $viewIsRenderAction = false;
97 
101  protected $linkRenderer;
102 
106  private $revisionStore;
107 
111  private $watchlistManager;
112 
116  private $userNameUtils;
117 
121  private $userOptionsLookup;
122 
124  private $commentFormatter;
125 
127  private $wikiPageFactory;
128 
130  private $jobQueueGroup;
131 
138  private $mRevisionRecord = null;
139 
144  public function __construct( Title $title, $oldId = null ) {
145  $this->mOldId = $oldId;
146  $this->mPage = $this->newPage( $title );
147 
148  $services = MediaWikiServices::getInstance();
149  $this->linkRenderer = $services->getLinkRenderer();
150  $this->revisionStore = $services->getRevisionStore();
151  $this->watchlistManager = $services->getWatchlistManager();
152  $this->userNameUtils = $services->getUserNameUtils();
153  $this->userOptionsLookup = $services->getUserOptionsLookup();
154  $this->commentFormatter = $services->getCommentFormatter();
155  $this->wikiPageFactory = $services->getWikiPageFactory();
156  $this->jobQueueGroup = $services->getJobQueueGroup();
157  }
158 
163  protected function newPage( Title $title ) {
164  return new WikiPage( $title );
165  }
166 
172  public static function newFromID( $id ) {
173  $t = Title::newFromID( $id );
174  return $t == null ? null : new static( $t );
175  }
176 
184  public static function newFromTitle( $title, IContextSource $context ) {
185  if ( $title->getNamespace() === NS_MEDIA ) {
186  // XXX: This should not be here, but where should it go?
187  $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
188  }
189 
190  $page = null;
191  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
192  Hooks::runner()->onArticleFromTitle( $title, $page, $context );
193  if ( !$page ) {
194  switch ( $title->getNamespace() ) {
195  case NS_FILE:
196  $page = new ImagePage( $title );
197  break;
198  case NS_CATEGORY:
199  $page = new CategoryPage( $title );
200  break;
201  default:
202  $page = new Article( $title );
203  }
204  }
205  $page->setContext( $context );
206 
207  return $page;
208  }
209 
217  public static function newFromWikiPage( WikiPage $page, IContextSource $context ) {
218  $article = self::newFromTitle( $page->getTitle(), $context );
219  $article->mPage = $page; // override to keep process cached vars
220  return $article;
221  }
222 
228  public function getRedirectedFrom() {
229  return $this->mRedirectedFrom;
230  }
231 
237  public function setRedirectedFrom( Title $from ) {
238  $this->mRedirectedFrom = $from;
239  }
240 
246  public function getTitle() {
247  return $this->mPage->getTitle();
248  }
249 
256  public function getPage() {
257  return $this->mPage;
258  }
259 
260  public function clear() {
261  $this->mRedirectedFrom = null; # Title object if set
262  $this->mRedirectUrl = false;
263  $this->mRevisionRecord = null;
264  $this->fetchResult = null;
265 
266  // TODO hard-deprecate direct access to public fields
267 
268  $this->mPage->clear();
269  }
270 
278  public function getOldID() {
279  if ( $this->mOldId === null ) {
280  $this->mOldId = $this->getOldIDFromRequest();
281  }
282 
283  return $this->mOldId;
284  }
285 
291  public function getOldIDFromRequest() {
292  $this->mRedirectUrl = false;
293 
294  $request = $this->getContext()->getRequest();
295  $oldid = $request->getIntOrNull( 'oldid' );
296 
297  if ( $oldid === null ) {
298  return 0;
299  }
300 
301  if ( $oldid !== 0 ) {
302  # Load the given revision and check whether the page is another one.
303  # In that case, update this instance to reflect the change.
304  if ( $oldid === $this->mPage->getLatest() ) {
305  $this->mRevisionRecord = $this->mPage->getRevisionRecord();
306  } else {
307  $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
308  if ( $this->mRevisionRecord !== null ) {
309  $revPageId = $this->mRevisionRecord->getPageId();
310  // Revision title doesn't match the page title given?
311  if ( $this->mPage->getId() != $revPageId ) {
312  $this->mPage = $this->wikiPageFactory->newFromID( $revPageId );
313  }
314  }
315  }
316  }
317 
318  $oldRev = $this->mRevisionRecord;
319  if ( $request->getRawVal( 'direction' ) === 'next' ) {
320  $nextid = 0;
321  if ( $oldRev ) {
322  $nextRev = $this->revisionStore->getNextRevision( $oldRev );
323  if ( $nextRev ) {
324  $nextid = $nextRev->getId();
325  }
326  }
327  if ( $nextid ) {
328  $oldid = $nextid;
329  $this->mRevisionRecord = null;
330  } else {
331  $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' );
332  }
333  } elseif ( $request->getRawVal( 'direction' ) === 'prev' ) {
334  $previd = 0;
335  if ( $oldRev ) {
336  $prevRev = $this->revisionStore->getPreviousRevision( $oldRev );
337  if ( $prevRev ) {
338  $previd = $prevRev->getId();
339  }
340  }
341  if ( $previd ) {
342  $oldid = $previd;
343  $this->mRevisionRecord = null;
344  }
345  }
346 
347  return $oldid;
348  }
349 
359  public function fetchRevisionRecord() {
360  if ( $this->fetchResult ) {
361  return $this->mRevisionRecord;
362  }
363 
364  $oldid = $this->getOldID();
365 
366  // $this->mRevisionRecord might already be fetched by getOldIDFromRequest()
367  if ( !$this->mRevisionRecord ) {
368  if ( !$oldid ) {
369  $this->mRevisionRecord = $this->mPage->getRevisionRecord();
370 
371  if ( !$this->mRevisionRecord ) {
372  wfDebug( __METHOD__ . " failed to find page data for title " .
373  $this->getTitle()->getPrefixedText() );
374 
375  // Output for this case is done by showMissingArticle().
376  $this->fetchResult = Status::newFatal( 'noarticletext' );
377  return null;
378  }
379  } else {
380  $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
381 
382  if ( !$this->mRevisionRecord ) {
383  wfDebug( __METHOD__ . " failed to load revision, rev_id $oldid" );
384 
385  $this->fetchResult = Status::newFatal( 'missing-revision', $oldid );
386  return null;
387  }
388  }
389  }
390 
391  if ( !$this->mRevisionRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getContext()->getAuthority() ) ) {
392  wfDebug( __METHOD__ . " failed to retrieve content of revision " . $this->mRevisionRecord->getId() );
393 
394  // Output for this case is done by showDeletedRevisionHeader().
395  // title used in wikilinks, should not contain whitespaces
396  $this->fetchResult = new Status;
397  $title = $this->getTitle()->getPrefixedDBkey();
398 
399  if ( $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
400  $this->fetchResult->fatal( 'rev-suppressed-text' );
401  } else {
402  $this->fetchResult->fatal( 'rev-deleted-text-permission', $title );
403  }
404 
405  return null;
406  }
407 
408  $this->fetchResult = Status::newGood( $this->mRevisionRecord );
409  return $this->mRevisionRecord;
410  }
411 
417  public function isCurrent() {
418  # If no oldid, this is the current version.
419  if ( $this->getOldID() == 0 ) {
420  return true;
421  }
422 
423  return $this->mPage->exists() &&
424  $this->mRevisionRecord &&
425  $this->mRevisionRecord->isCurrent();
426  }
427 
436  public function getRevIdFetched() {
437  if ( $this->fetchResult && $this->fetchResult->isOK() ) {
439  $rev = $this->fetchResult->getValue();
440  return $rev->getId();
441  } else {
442  return $this->mPage->getLatest();
443  }
444  }
445 
450  public function view() {
451  $context = $this->getContext();
452  $useFileCache = $context->getConfig()->get( MainConfigNames::UseFileCache );
453 
454  # Get variables from query string
455  # As side effect this will load the revision and update the title
456  # in a revision ID is passed in the request, so this should remain
457  # the first call of this method even if $oldid is used way below.
458  $oldid = $this->getOldID();
459 
460  $authority = $context->getAuthority();
461  # Another check in case getOldID() is altering the title
462  $permissionStatus = PermissionStatus::newEmpty();
463  if ( !$authority
464  ->authorizeRead( 'read', $this->getTitle(), $permissionStatus )
465  ) {
466  wfDebug( __METHOD__ . ": denied on secondary read check" );
467  throw new PermissionsError( 'read', $permissionStatus );
468  }
469 
470  $outputPage = $context->getOutput();
471  # getOldID() may as well want us to redirect somewhere else
472  if ( $this->mRedirectUrl ) {
473  $outputPage->redirect( $this->mRedirectUrl );
474  wfDebug( __METHOD__ . ": redirecting due to oldid" );
475 
476  return;
477  }
478 
479  # If we got diff in the query, we want to see a diff page instead of the article.
480  if ( $context->getRequest()->getCheck( 'diff' ) ) {
481  wfDebug( __METHOD__ . ": showing diff page" );
482  $this->showDiffPage();
483 
484  return;
485  }
486 
487  # Set page title (may be overridden from ParserOutput if title conversion is enabled or DISPLAYTITLE is used)
489  str_replace( '_', ' ', $this->getTitle()->getNsText() ),
490  ':',
491  $this->getTitle()->getText()
492  ) );
493 
494  $outputPage->setArticleFlag( true );
495  # Allow frames by default
496  $outputPage->setPreventClickjacking( false );
497 
498  $parserOptions = $this->getParserOptions();
499 
500  $poOptions = [];
501  # Allow extensions to vary parser options used for article rendering
502  Hooks::runner()->onArticleParserOptions( $this, $parserOptions );
503  # Render printable version, use printable version cache
504  if ( $outputPage->isPrintable() ) {
505  $parserOptions->setIsPrintable( true );
506  $poOptions['enableSectionEditLinks'] = false;
507  $outputPage->prependHTML(
509  $outputPage->msg( 'printableversion-deprecated-warning' )->escaped()
510  )
511  );
512  } elseif ( $this->viewIsRenderAction || !$this->isCurrent() ||
513  !$authority->probablyCan( 'edit', $this->getTitle() )
514  ) {
515  $poOptions['enableSectionEditLinks'] = false;
516  }
517 
518  # Try client and file cache
519  if ( $oldid === 0 && $this->mPage->checkTouched() ) {
520  # Try to stream the output from file cache
521  if ( $useFileCache && $this->tryFileCache() ) {
522  wfDebug( __METHOD__ . ": done file cache" );
523  # tell wgOut that output is taken care of
524  $outputPage->disable();
525  $this->mPage->doViewUpdates( $authority, $oldid );
526 
527  return;
528  }
529  }
530 
531  $this->showRedirectedFromHeader();
532  $this->showNamespaceHeader();
533 
534  if ( $this->viewIsRenderAction ) {
535  $poOptions += [ 'absoluteURLs' => true ];
536  }
537  $poOptions += [ 'includeDebugInfo' => true ];
538 
539  try {
540  $continue =
541  $this->generateContentOutput( $authority, $parserOptions, $oldid, $outputPage, $poOptions );
542  } catch ( BadRevisionException $e ) {
543  $continue = false;
544  $this->showViewError( wfMessage( 'badrevision' )->text() );
545  }
546 
547  if ( !$continue ) {
548  return;
549  }
550 
551  # For the main page, overwrite the <title> element with the con-
552  # tents of 'pagetitle-view-mainpage' instead of the default (if
553  # that's not empty).
554  # This message always exists because it is in the i18n files
555  if ( $this->getTitle()->isMainPage() ) {
556  $msg = $context->msg( 'pagetitle-view-mainpage' )->inContentLanguage();
557  if ( !$msg->isDisabled() ) {
558  $outputPage->setHTMLTitle( $msg->text() );
559  }
560  }
561 
562  # Use adaptive TTLs for CDN so delayed/failed purges are noticed less often.
563  # This could use getTouched(), but that could be scary for major template edits.
564  $outputPage->adaptCdnTTL( $this->mPage->getTimestamp(), IExpiringStore::TTL_DAY );
565 
566  $this->showViewFooter();
567  $this->mPage->doViewUpdates( $authority, $oldid, $this->fetchRevisionRecord() );
568 
569  # Load the postEdit module if the user just saved this revision
570  # See also EditPage::setPostEditCookie
571  $request = $context->getRequest();
573  $postEdit = $request->getCookie( $cookieKey );
574  if ( $postEdit ) {
575  # Clear the cookie. This also prevents caching of the response.
576  $request->response()->clearCookie( $cookieKey );
577  $outputPage->addJsConfigVars( 'wgPostEdit', $postEdit );
578  $outputPage->addModules( 'mediawiki.action.view.postEdit' ); // FIXME: test this
579  }
580  }
581 
594  private function generateContentOutput(
595  Authority $performer,
596  ParserOptions $parserOptions,
597  int $oldid,
598  OutputPage $outputPage,
599  array $textOptions
600  ): bool {
601  # Should the parser cache be used?
602  $useParserCache = true;
603  $pOutput = null;
604  $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
605 
606  // NOTE: $outputDone and $useParserCache may be changed by the hook
607  $this->getHookRunner()->onArticleViewHeader( $this, $outputDone, $useParserCache );
608  if ( $outputDone ) {
609  if ( $outputDone instanceof ParserOutput ) {
610  $pOutput = $outputDone;
611  }
612 
613  if ( $pOutput ) {
614  $this->doOutputMetaData( $pOutput, $outputPage );
615  }
616  return true;
617  }
618 
619  // Early abort if the page doesn't exist
620  if ( !$this->mPage->exists() ) {
621  wfDebug( __METHOD__ . ": showing missing article" );
622  $this->showMissingArticle();
623  $this->mPage->doViewUpdates( $performer );
624  return false; // skip all further output to OutputPage
625  }
626 
627  // Try the latest parser cache
628  // NOTE: try latest-revision cache first to avoid loading revision.
629  if ( $useParserCache && !$oldid ) {
630  $pOutput = $parserOutputAccess->getCachedParserOutput(
631  $this->getPage(),
632  $parserOptions,
633  null,
634  ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked
635  );
636 
637  if ( $pOutput ) {
638  $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
639  $this->doOutputMetaData( $pOutput, $outputPage );
640  return true;
641  }
642  }
643 
644  $rev = $this->fetchRevisionRecord();
645  if ( !$this->fetchResult->isOK() ) {
646  $this->showViewError( $this->fetchResult->getWikiText(
647  false, false, $this->getContext()->getLanguage()
648  ) );
649  return true;
650  }
651 
652  # Are we looking at an old revision
653  if ( $oldid ) {
654  $this->setOldSubtitle( $oldid );
655 
656  if ( !$this->showDeletedRevisionHeader() ) {
657  wfDebug( __METHOD__ . ": cannot view deleted revision" );
658  return false; // skip all further output to OutputPage
659  }
660 
661  // Try the old revision parser cache
662  // NOTE: Repeating cache check for old revision to avoid fetching $rev
663  // before it's absolutely necessary.
664  if ( $useParserCache ) {
665  $pOutput = $parserOutputAccess->getCachedParserOutput(
666  $this->getPage(),
667  $parserOptions,
668  $rev,
669  ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked in fetchRevisionRecord
670  );
671 
672  if ( $pOutput ) {
673  $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
674  $this->doOutputMetaData( $pOutput, $outputPage );
675  return true;
676  }
677  }
678  }
679 
680  # Ensure that UI elements requiring revision ID have
681  # the correct version information.
682  $outputPage->setRevisionId( $this->getRevIdFetched() );
683  $outputPage->setRevisionIsCurrent( $rev->isCurrent() );
684  # Preload timestamp to avoid a DB hit
685  $outputPage->setRevisionTimestamp( $rev->getTimestamp() );
686 
687  # Pages containing custom CSS or JavaScript get special treatment
688  if ( $this->getTitle()->isSiteConfigPage() || $this->getTitle()->isUserConfigPage() ) {
689  $dir = $this->getContext()->getLanguage()->getDir();
690  $lang = $this->getContext()->getLanguage()->getHtmlCode();
691 
692  $outputPage->wrapWikiMsg(
693  "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
694  'clearyourcache'
695  );
696  $outputPage->addModuleStyles( 'mediawiki.action.styles' );
697  } elseif ( !$this->getHookRunner()->onArticleRevisionViewCustom(
698  $rev,
699  $this->getTitle(),
700  $oldid,
701  $outputPage )
702  ) {
703  // NOTE: sync with hooks called in DifferenceEngine::renderNewRevision()
704  // Allow extensions do their own custom view for certain pages
705  $this->doOutputMetaData( $pOutput, $outputPage );
706  return true;
707  }
708 
709  # Run the parse, protected by a pool counter
710  wfDebug( __METHOD__ . ": doing uncached parse" );
711 
712  if ( !$rev ) {
713  // No revision, abort! Shouldn't happen.
714  return false;
715  }
716 
717  $opt = 0;
718 
719  // we already checked the cache in case 2, don't check again.
720  $opt |= ParserOutputAccess::OPT_NO_CHECK_CACHE;
721 
722  // we already checked in fetchRevisionRecord()
723  $opt |= ParserOutputAccess::OPT_NO_AUDIENCE_CHECK;
724 
725  if ( !$rev->getId() || !$useParserCache ) {
726  // fake revision or uncacheable options
727  $opt |= ParserOutputAccess::OPT_NO_CACHE;
728  }
729 
730  $renderStatus = $parserOutputAccess->getParserOutput(
731  $this->getPage(),
732  $parserOptions,
733  $rev,
734  $opt
735  );
736 
737  // T327164: If parsoid cache warming is enabled, we want to ensure that the page
738  // the user is currently looking at has a cached parsoid rendering, in case they
739  // open visual editor. The cache entry would typically be missing if it has expired
740  // from the cache or it was invalidated by RefreshLinksJob. When "traditional"
741  // parser output has been invalidated by RefreshLinksJob, we will render it on
742  // the fly when a user requests the page, and thereby populate the cache again,
743  // per the code above.
744  // The code below is intended to do the same for parsoid output, but asynchronously
745  // in a job, so the user does not have to wait.
746  // Note that we get here if the traditional parser output was missing from the cache.
747  // We do not check if the parsoid output is present in the cache, because that check
748  // takes time. The assumption is that if we have traditional parser output
749  // cached, we probably also have parsoid output cached.
750  // So we leave it to ParsoidCachePrewarmJob to determine whether or not parsing is
751  // needed.
752  if ( $oldid === 0 || $oldid === $this->getPage()->getLatest() ) {
753  $parsoidCacheWarmingEnabled = $this->getContext()->getConfig()
754  ->get( MainConfigNames::ParsoidCacheConfig )['WarmParsoidParserCache'];
755 
756  if ( $parsoidCacheWarmingEnabled ) {
757  $parsoidJobSpec = ParsoidCachePrewarmJob::newSpec(
758  $rev->getId(),
759  $rev->getPageId(),
760  [ 'causeAction' => 'view' ]
761  );
762  $this->jobQueueGroup->lazyPush( $parsoidJobSpec );
763  }
764  }
765 
766  $this->doOutputFromRenderStatus(
767  $rev,
768  $renderStatus,
769  $outputPage,
770  $textOptions
771  );
772 
773  if ( !$renderStatus->isOK() ) {
774  return true;
775  }
776 
777  $pOutput = $renderStatus->getValue();
778  $this->doOutputMetaData( $pOutput, $outputPage );
779  return true;
780  }
781 
786  private function doOutputMetaData( ?ParserOutput $pOutput, OutputPage $outputPage ) {
787  # Adjust title for main page & pages with displaytitle
788  if ( $pOutput ) {
789  $this->adjustDisplayTitle( $pOutput );
790  }
791 
792  # Check for any __NOINDEX__ tags on the page using $pOutput
793  $policy = $this->getRobotPolicy( 'view', $pOutput ?: null );
794  $outputPage->setIndexPolicy( $policy['index'] );
795  $outputPage->setFollowPolicy( $policy['follow'] ); // FIXME: test this
796 
797  $this->mParserOutput = $pOutput;
798  }
799 
805  private function doOutputFromParserCache(
806  ParserOutput $pOutput,
807  OutputPage $outputPage,
808  array $textOptions
809  ) {
810  # Ensure that UI elements requiring revision ID have
811  # the correct version information.
812  $oldid = $pOutput->getCacheRevisionId() ?? $this->getRevIdFetched();
813  $outputPage->setRevisionId( $oldid );
814  $outputPage->setRevisionIsCurrent( $oldid === $this->mPage->getLatest() );
815  $outputPage->addParserOutput( $pOutput, $textOptions );
816  # Preload timestamp to avoid a DB hit
817  $cachedTimestamp = $pOutput->getTimestamp();
818  if ( $cachedTimestamp !== null ) {
819  $outputPage->setRevisionTimestamp( $cachedTimestamp );
820  $this->mPage->setTimestamp( $cachedTimestamp );
821  }
822  }
823 
830  private function doOutputFromRenderStatus(
831  ?RevisionRecord $rev,
832  Status $renderStatus,
833  OutputPage $outputPage,
834  array $textOptions
835  ) {
836  $context = $this->getContext();
837  $cdnMaxageStale = $context->getConfig()->get( MainConfigNames::CdnMaxageStale );
838  $ok = $renderStatus->isOK();
839 
840  $pOutput = $ok ? $renderStatus->getValue() : null;
841 
842  // Cache stale ParserOutput object with a short expiry
843  if ( $ok && $renderStatus->hasMessage( 'view-pool-dirty-output' ) ) {
844  $outputPage->setCdnMaxage( $cdnMaxageStale );
845  $outputPage->setLastModified( $pOutput->getCacheTime() );
846  $staleReason = $renderStatus->hasMessage( 'view-pool-contention' )
847  ? $context->msg( 'view-pool-contention' )
848  : $context->msg( 'view-pool-timeout' );
849  $outputPage->addHTML( "<!-- parser cache is expired, " .
850  "sending anyway due to $staleReason-->\n" );
851  }
852 
853  if ( !$renderStatus->isOK() ) {
854  $this->showViewError( $renderStatus->getWikiText(
855  false, 'view-pool-error', $context->getLanguage()
856  ) );
857  return;
858  }
859 
860  if ( $pOutput ) {
861  $outputPage->addParserOutput( $pOutput, $textOptions );
862  }
863 
864  if ( $this->getRevisionRedirectTarget( $rev ) ) {
865  $outputPage->addSubtitle( "<span id=\"redirectsub\">" .
866  $context->msg( 'redirectpagesub' )->parse() . "</span>" );
867  }
868  }
869 
874  private function getRevisionRedirectTarget( RevisionRecord $revision ) {
875  // TODO: find a *good* place for the code that determines the redirect target for
876  // a given revision!
877  // NOTE: Use main slot content. Compare code in DerivedPageDataUpdater::revisionIsRedirect.
878  $content = $revision->getContent( SlotRecord::MAIN );
879  return $content ? $content->getRedirectTarget() : null;
880  }
881 
886  public function adjustDisplayTitle( ParserOutput $pOutput ) {
887  $out = $this->getContext()->getOutput();
888 
889  # Adjust the title if it was set by displaytitle, -{T|}- or language conversion
890  $titleText = $pOutput->getTitleText();
891  if ( strval( $titleText ) !== '' ) {
892  $out->setPageTitle( $titleText );
893  $out->setDisplayTitle( $titleText );
894  }
895  }
896 
901  protected function showDiffPage() {
902  $context = $this->getContext();
903  $request = $context->getRequest();
904  $diff = $request->getVal( 'diff' );
905  $rcid = $request->getInt( 'rcid' );
906  $purge = $request->getRawVal( 'action' ) === 'purge';
907  $unhide = $request->getInt( 'unhide' ) == 1;
908  $oldid = $this->getOldID();
909 
910  $rev = $this->fetchRevisionRecord();
911 
912  if ( !$rev ) {
913  // T213621: $rev maybe null due to either lack of permission to view the
914  // revision or actually not existing. So let's try loading it from the id
915  $rev = $this->revisionStore->getRevisionById( $oldid );
916  if ( $rev ) {
917  // Revision exists but $user lacks permission to diff it.
918  // Do nothing here.
919  // The $rev will later be used to create standard diff elements however.
920  } else {
921  $context->getOutput()->setPageTitle( $context->msg( 'errorpagetitle' ) );
922  $msg = $context->msg( 'difference-missing-revision' )
923  ->params( $oldid )
924  ->numParams( 1 )
925  ->parseAsBlock();
926  $context->getOutput()->addHTML( $msg );
927  return;
928  }
929  }
930 
931  $contentHandler = MediaWikiServices::getInstance()
932  ->getContentHandlerFactory()
933  ->getContentHandler(
934  $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
935  );
936  $de = $contentHandler->createDifferenceEngine(
937  $context,
938  $oldid,
939  $diff,
940  $rcid,
941  $purge,
942  $unhide
943  );
944  $de->setSlotDiffOptions( [
945  'diff-type' => $request->getVal( 'diff-type' ),
946  'expand-url' => $this->viewIsRenderAction
947  ] );
948  $de->showDiffPage( $this->isDiffOnlyView() );
949 
950  // Run view updates for the newer revision being diffed (and shown
951  // below the diff if not diffOnly).
952  [ , $new ] = $de->mapDiffPrevNext( $oldid, $diff );
953  // New can be false, convert it to 0 - this conveniently means the latest revision
954  $this->mPage->doViewUpdates( $context->getAuthority(), (int)$new );
955 
956  // Add link to help page; see T321569
957  $context->getOutput()->addHelpLink( 'Help:Diff' );
958  }
959 
960  protected function isDiffOnlyView() {
961  return $this->getContext()->getRequest()->getBool(
962  'diffonly',
963  $this->userOptionsLookup->getBoolOption( $this->getContext()->getUser(), 'diffonly' )
964  );
965  }
966 
974  public function getRobotPolicy( $action, ParserOutput $pOutput = null ) {
975  $context = $this->getContext();
976  $mainConfig = $context->getConfig();
977  $articleRobotPolicies = $mainConfig->get( MainConfigNames::ArticleRobotPolicies );
978  $namespaceRobotPolicies = $mainConfig->get( MainConfigNames::NamespaceRobotPolicies );
979  $defaultRobotPolicy = $mainConfig->get( MainConfigNames::DefaultRobotPolicy );
980  $title = $this->getTitle();
981  $ns = $title->getNamespace();
982 
983  # Don't index user and user talk pages for blocked users (T13443)
984  if ( ( $ns === NS_USER || $ns === NS_USER_TALK ) && !$title->isSubpage() ) {
985  $specificTarget = null;
986  $vagueTarget = null;
987  $titleText = $title->getText();
988  if ( IPUtils::isValid( $titleText ) ) {
989  $vagueTarget = $titleText;
990  } else {
991  $specificTarget = $titleText;
992  }
993  if ( DatabaseBlock::newFromTarget( $specificTarget, $vagueTarget ) instanceof DatabaseBlock ) {
994  return [
995  'index' => 'noindex',
996  'follow' => 'nofollow'
997  ];
998  }
999  }
1000 
1001  if ( $this->mPage->getId() === 0 || $this->getOldID() ) {
1002  # Non-articles (special pages etc), and old revisions
1003  return [
1004  'index' => 'noindex',
1005  'follow' => 'nofollow'
1006  ];
1007  } elseif ( $context->getOutput()->isPrintable() ) {
1008  # Discourage indexing of printable versions, but encourage following
1009  return [
1010  'index' => 'noindex',
1011  'follow' => 'follow'
1012  ];
1013  } elseif ( $context->getRequest()->getInt( 'curid' ) ) {
1014  # For ?curid=x urls, disallow indexing
1015  return [
1016  'index' => 'noindex',
1017  'follow' => 'follow'
1018  ];
1019  }
1020 
1021  # Otherwise, construct the policy based on the various config variables.
1022  $policy = self::formatRobotPolicy( $defaultRobotPolicy );
1023 
1024  if ( isset( $namespaceRobotPolicies[$ns] ) ) {
1025  # Honour customised robot policies for this namespace
1026  $policy = array_merge(
1027  $policy,
1028  self::formatRobotPolicy( $namespaceRobotPolicies[$ns] )
1029  );
1030  }
1031  if ( $title->canUseNoindex() && is_object( $pOutput ) && $pOutput->getIndexPolicy() ) {
1032  # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates
1033  # a final check that we have really got the parser output.
1034  $policy = array_merge(
1035  $policy,
1036  [ 'index' => $pOutput->getIndexPolicy() ]
1037  );
1038  }
1039 
1040  if ( isset( $articleRobotPolicies[$title->getPrefixedText()] ) ) {
1041  # (T16900) site config can override user-defined __INDEX__ or __NOINDEX__
1042  $policy = array_merge(
1043  $policy,
1044  self::formatRobotPolicy( $articleRobotPolicies[$title->getPrefixedText()] )
1045  );
1046  }
1047 
1048  return $policy;
1049  }
1050 
1058  public static function formatRobotPolicy( $policy ) {
1059  if ( is_array( $policy ) ) {
1060  return $policy;
1061  } elseif ( !$policy ) {
1062  return [];
1063  }
1064 
1065  $arr = [];
1066  foreach ( explode( ',', $policy ) as $var ) {
1067  $var = trim( $var );
1068  if ( $var === 'index' || $var === 'noindex' ) {
1069  $arr['index'] = $var;
1070  } elseif ( $var === 'follow' || $var === 'nofollow' ) {
1071  $arr['follow'] = $var;
1072  }
1073  }
1074 
1075  return $arr;
1076  }
1077 
1085  public function showRedirectedFromHeader() {
1086  $context = $this->getContext();
1087  $redirectSources = $context->getConfig()->get( MainConfigNames::RedirectSources );
1088  $outputPage = $context->getOutput();
1089  $request = $context->getRequest();
1090  $rdfrom = $request->getVal( 'rdfrom' );
1091 
1092  // Construct a URL for the current page view, but with the target title
1093  $query = $request->getValues();
1094  unset( $query['rdfrom'] );
1095  unset( $query['title'] );
1096  if ( $this->getTitle()->isRedirect() ) {
1097  // Prevent double redirects
1098  $query['redirect'] = 'no';
1099  }
1100  $redirectTargetUrl = $this->getTitle()->getLinkURL( $query );
1101 
1102  if ( isset( $this->mRedirectedFrom ) ) {
1103  // This is an internally redirected page view.
1104  // We'll need a backlink to the source page for navigation.
1105  if ( $this->getHookRunner()->onArticleViewRedirect( $this ) ) {
1106  $redir = $this->linkRenderer->makeKnownLink(
1107  $this->mRedirectedFrom,
1108  null,
1109  [],
1110  [ 'redirect' => 'no' ]
1111  );
1112 
1113  $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1114  $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1115  . "</span>" );
1116 
1117  // Add the script to update the displayed URL and
1118  // set the fragment if one was specified in the redirect
1119  $outputPage->addJsConfigVars( [
1120  'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1121  ] );
1122  $outputPage->addModules( 'mediawiki.action.view.redirect' );
1123 
1124  // Add a <link rel="canonical"> tag
1125  $outputPage->setCanonicalUrl( $this->getTitle()->getCanonicalURL() );
1126 
1127  // Tell the output object that the user arrived at this article through a redirect
1128  $outputPage->setRedirectedFrom( $this->mRedirectedFrom );
1129 
1130  return true;
1131  }
1132  } elseif ( $rdfrom ) {
1133  // This is an externally redirected view, from some other wiki.
1134  // If it was reported from a trusted site, supply a backlink.
1135  if ( $redirectSources && preg_match( $redirectSources, $rdfrom ) ) {
1136  $redir = Linker::makeExternalLink( $rdfrom, $rdfrom );
1137  $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1138  $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1139  . "</span>" );
1140 
1141  // Add the script to update the displayed URL
1142  $outputPage->addJsConfigVars( [
1143  'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1144  ] );
1145  $outputPage->addModules( 'mediawiki.action.view.redirect' );
1146 
1147  return true;
1148  }
1149  }
1150 
1151  return false;
1152  }
1153 
1158  public function showNamespaceHeader() {
1159  if ( $this->getTitle()->isTalkPage() && !$this->getContext()->msg( 'talkpageheader' )->isDisabled() ) {
1160  $this->getContext()->getOutput()->wrapWikiMsg(
1161  "<div class=\"mw-talkpageheader\">\n$1\n</div>",
1162  [ 'talkpageheader' ]
1163  );
1164  }
1165  }
1166 
1170  public function showViewFooter() {
1171  # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
1172  if ( $this->getTitle()->getNamespace() === NS_USER_TALK
1173  && IPUtils::isValid( $this->getTitle()->getText() )
1174  ) {
1175  $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' );
1176  }
1177 
1178  // Show a footer allowing the user to patrol the shown revision or page if possible
1179  $patrolFooterShown = $this->showPatrolFooter();
1180 
1181  $this->getHookRunner()->onArticleViewFooter( $this, $patrolFooterShown );
1182  }
1183 
1194  public function showPatrolFooter() {
1195  $context = $this->getContext();
1196  $mainConfig = $context->getConfig();
1197  $useNPPatrol = $mainConfig->get( MainConfigNames::UseNPPatrol );
1198  $useRCPatrol = $mainConfig->get( MainConfigNames::UseRCPatrol );
1199  $useFilePatrol = $mainConfig->get( MainConfigNames::UseFilePatrol );
1200  // Allow hooks to decide whether to not output this at all
1201  if ( !$this->getHookRunner()->onArticleShowPatrolFooter( $this ) ) {
1202  return false;
1203  }
1204 
1205  $outputPage = $context->getOutput();
1206  $user = $context->getUser();
1207  $title = $this->getTitle();
1208  $rc = false;
1209 
1210  if ( !$context->getAuthority()->probablyCan( 'patrol', $title )
1211  || !( $useRCPatrol || $useNPPatrol
1212  || ( $useFilePatrol && $title->inNamespace( NS_FILE ) ) )
1213  ) {
1214  // Patrolling is disabled or the user isn't allowed to
1215  return false;
1216  }
1217 
1218  if ( $this->mRevisionRecord
1219  && !RecentChange::isInRCLifespan( $this->mRevisionRecord->getTimestamp(), 21600 )
1220  ) {
1221  // The current revision is already older than what could be in the RC table
1222  // 6h tolerance because the RC might not be cleaned out regularly
1223  return false;
1224  }
1225 
1226  // Check for cached results
1227  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1228  $key = $cache->makeKey( 'unpatrollable-page', $title->getArticleID() );
1229  if ( $cache->get( $key ) ) {
1230  return false;
1231  }
1232 
1233  $dbr = wfGetDB( DB_REPLICA );
1234  $oldestRevisionRow = $dbr->selectRow(
1235  'revision',
1236  [ 'rev_id', 'rev_timestamp' ],
1237  [ 'rev_page' => $title->getArticleID() ],
1238  __METHOD__,
1239  [ 'ORDER BY' => [ 'rev_timestamp', 'rev_id' ] ]
1240  );
1241  $oldestRevisionTimestamp = $oldestRevisionRow ? $oldestRevisionRow->rev_timestamp : false;
1242 
1243  // New page patrol: Get the timestamp of the oldest revision which
1244  // the revision table holds for the given page. Then we look
1245  // whether it's within the RC lifespan and if it is, we try
1246  // to get the recentchanges row belonging to that entry.
1247  $recentPageCreation = false;
1248  if ( $oldestRevisionTimestamp
1249  && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 )
1250  ) {
1251  // 6h tolerance because the RC might not be cleaned out regularly
1252  $recentPageCreation = true;
1254  [ 'rc_this_oldid' => intval( $oldestRevisionRow->rev_id ) ],
1255  __METHOD__
1256  );
1257  if ( $rc ) {
1258  // Use generic patrol message for new pages
1259  $markPatrolledMsg = $context->msg( 'markaspatrolledtext' );
1260  }
1261  }
1262 
1263  // File patrol: Get the timestamp of the latest upload for this page,
1264  // check whether it is within the RC lifespan and if it is, we try
1265  // to get the recentchanges row belonging to that entry
1266  // (with rc_type = RC_LOG, rc_log_type = upload).
1267  $recentFileUpload = false;
1268  if ( ( !$rc || $rc->getAttribute( 'rc_patrolled' ) ) && $useFilePatrol
1269  && $title->getNamespace() === NS_FILE ) {
1270  // Retrieve timestamp from the current file (lastest upload)
1271  $newestUploadTimestamp = $dbr->selectField(
1272  'image',
1273  'img_timestamp',
1274  [ 'img_name' => $title->getDBkey() ],
1275  __METHOD__
1276  );
1277  if ( $newestUploadTimestamp
1278  && RecentChange::isInRCLifespan( $newestUploadTimestamp, 21600 )
1279  ) {
1280  // 6h tolerance because the RC might not be cleaned out regularly
1281  $recentFileUpload = true;
1283  [
1284  'rc_type' => RC_LOG,
1285  'rc_log_type' => 'upload',
1286  'rc_timestamp' => $newestUploadTimestamp,
1287  'rc_namespace' => NS_FILE,
1288  'rc_cur_id' => $title->getArticleID()
1289  ],
1290  __METHOD__
1291  );
1292  if ( $rc ) {
1293  // Use patrol message specific to files
1294  $markPatrolledMsg = $context->msg( 'markaspatrolledtext-file' );
1295  }
1296  }
1297  }
1298 
1299  if ( !$recentPageCreation && !$recentFileUpload ) {
1300  // Page creation and latest upload (for files) is too old to be in RC
1301 
1302  // We definitely can't patrol so cache the information
1303  // When a new file version is uploaded, the cache is cleared
1304  $cache->set( $key, '1' );
1305 
1306  return false;
1307  }
1308 
1309  if ( !$rc ) {
1310  // Don't cache: This can be hit if the page gets accessed very fast after
1311  // its creation / latest upload or in case we have high replica DB lag. In case
1312  // the revision is too old, we will already return above.
1313  return false;
1314  }
1315 
1316  if ( $rc->getAttribute( 'rc_patrolled' ) ) {
1317  // Patrolled RC entry around
1318 
1319  // Cache the information we gathered above in case we can't patrol
1320  // Don't cache in case we can patrol as this could change
1321  $cache->set( $key, '1' );
1322 
1323  return false;
1324  }
1325 
1326  if ( $rc->getPerformerIdentity()->equals( $user ) ) {
1327  // Don't show a patrol link for own creations/uploads. If the user could
1328  // patrol them, they already would be patrolled
1329  return false;
1330  }
1331 
1332  $outputPage->setPreventClickjacking( true );
1333  if ( $context->getAuthority()->isAllowed( 'writeapi' ) ) {
1334  $outputPage->addModules( 'mediawiki.misc-authed-curate' );
1335  }
1336 
1337  $link = $this->linkRenderer->makeKnownLink(
1338  $title,
1339  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $markPatrolledMsg is always set when $rc is set
1340  $markPatrolledMsg->text(),
1341  [],
1342  [
1343  'action' => 'markpatrolled',
1344  'rcid' => $rc->getAttribute( 'rc_id' ),
1345  ]
1346  );
1347 
1348  $outputPage->addModuleStyles( 'mediawiki.action.styles' );
1349  $outputPage->addHTML(
1350  "<div class='patrollink' data-mw='interface'>" .
1351  $context->msg( 'markaspatrolledlink' )->rawParams( $link )->escaped() .
1352  '</div>'
1353  );
1354 
1355  return true;
1356  }
1357 
1364  public static function purgePatrolFooterCache( $articleID ) {
1365  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1366  $cache->delete( $cache->makeKey( 'unpatrollable-page', $articleID ) );
1367  }
1368 
1373  public function showMissingArticle() {
1374  $context = $this->getContext();
1375  $send404Code = $context->getConfig()->get( MainConfigNames::Send404Code );
1376 
1377  $outputPage = $context->getOutput();
1378  // Whether the page is a root user page of an existing user (but not a subpage)
1379  $validUserPage = false;
1380 
1381  $title = $this->getTitle();
1382 
1383  $services = MediaWikiServices::getInstance();
1384 
1385  $contextUser = $context->getUser();
1386 
1387  # Show info in user (talk) namespace. Does the user exist? Is he blocked?
1388  if ( $title->getNamespace() === NS_USER
1389  || $title->getNamespace() === NS_USER_TALK
1390  ) {
1391  $rootPart = explode( '/', $title->getText() )[0];
1392  $user = User::newFromName( $rootPart, false /* allow IP users */ );
1393  $ip = $this->userNameUtils->isIP( $rootPart );
1394  $block = DatabaseBlock::newFromTarget( $user, $user );
1395 
1396  if ( $user && $user->isRegistered() && $user->isHidden() &&
1397  !$context->getAuthority()->isAllowed( 'hideuser' )
1398  ) {
1399  // T120883 if the user is hidden and the viewer cannot see hidden
1400  // users, pretend like it does not exist at all.
1401  $user = false;
1402  }
1403 
1404  if ( !( $user && $user->isRegistered() ) && !$ip ) { # User does not exist
1405  $outputPage->addHTML( Html::warningBox(
1406  $context->msg( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) )->parse(),
1407  'mw-userpage-userdoesnotexist'
1408  ) );
1409  } elseif (
1410  $block !== null &&
1411  $block->getType() != DatabaseBlock::TYPE_AUTO &&
1412  (
1413  $block->isSitewide() ||
1414  $services->getPermissionManager()->isBlockedFrom( $user, $title, true )
1415  )
1416  ) {
1417  // Show log extract if the user is sitewide blocked or is partially
1418  // blocked and not allowed to edit their user page or user talk page
1420  $outputPage,
1421  'block',
1422  $services->getNamespaceInfo()->getCanonicalName( NS_USER ) . ':' .
1423  $block->getTargetName(),
1424  '',
1425  [
1426  'lim' => 1,
1427  'showIfEmpty' => false,
1428  'msgKey' => [
1429  'blocked-notice-logextract',
1430  $user->getName() # Support GENDER in notice
1431  ]
1432  ]
1433  );
1434  $validUserPage = !$title->isSubpage();
1435  } else {
1436  $validUserPage = !$title->isSubpage();
1437  }
1438  }
1439 
1440  $this->getHookRunner()->onShowMissingArticle( $this );
1441 
1442  # Show delete and move logs if there were any such events.
1443  # The logging query can DOS the site when bots/crawlers cause 404 floods,
1444  # so be careful showing this. 404 pages must be cheap as they are hard to cache.
1445  $dbCache = MediaWikiServices::getInstance()->getMainObjectStash();
1446  $key = $dbCache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
1447  $isRegistered = $contextUser->isRegistered();
1448  $sessionExists = $context->getRequest()->getSession()->isPersistent();
1449 
1450  if ( $isRegistered || $dbCache->get( $key ) || $sessionExists ) {
1451  $logTypes = [ 'delete', 'move', 'protect' ];
1452 
1453  $dbr = wfGetDB( DB_REPLICA );
1454 
1455  $conds = [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ];
1456  // Give extensions a chance to hide their (unrelated) log entries
1457  $this->getHookRunner()->onArticle__MissingArticleConditions( $conds, $logTypes );
1459  $outputPage,
1460  $logTypes,
1461  $title,
1462  '',
1463  [
1464  'lim' => 10,
1465  'conds' => $conds,
1466  'showIfEmpty' => false,
1467  'msgKey' => [ $isRegistered || $sessionExists
1468  ? 'moveddeleted-notice'
1469  : 'moveddeleted-notice-recent'
1470  ]
1471  ]
1472  );
1473  }
1474 
1475  if ( !$this->mPage->hasViewableContent() && $send404Code && !$validUserPage ) {
1476  // If there's no backing content, send a 404 Not Found
1477  // for better machine handling of broken links.
1478  $context->getRequest()->response()->statusHeader( 404 );
1479  }
1480 
1481  // Also apply the robot policy for nonexisting pages (even if a 404 was used)
1482  $policy = $this->getRobotPolicy( 'view' );
1483  $outputPage->setIndexPolicy( $policy['index'] );
1484  $outputPage->setFollowPolicy( $policy['follow'] );
1485 
1486  $hookResult = $this->getHookRunner()->onBeforeDisplayNoArticleText( $this );
1487 
1488  if ( !$hookResult ) {
1489  return;
1490  }
1491 
1492  # Show error message
1493  $oldid = $this->getOldID();
1494  if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) {
1495  $text = $this->getTitle()->getDefaultMessageText() ?? '';
1496  $outputPage->addWikiTextAsContent( $text );
1497  } else {
1498  if ( $oldid ) {
1499  // T251066: Try loading the revision from the archive table.
1500  // Show link to view it if it exists and the user has permission to view it.
1501  $pa = new PageArchive( $title );
1502  $revRecord = $pa->getArchivedRevisionRecord( $oldid );
1503  if ( $revRecord && $revRecord->userCan(
1504  RevisionRecord::DELETED_TEXT,
1505  $context->getAuthority()
1506  ) ) {
1507  $text = $context->msg(
1508  'missing-revision-permission', $oldid,
1509  $revRecord->getTimestamp(),
1510  $title->getPrefixedDBkey()
1511  )->plain();
1512  } else {
1513  $text = $context->msg( 'missing-revision', $oldid )->plain();
1514  }
1515 
1516  } elseif ( $context->getAuthority()->probablyCan( 'edit', $title ) ) {
1517  $message = $isRegistered ? 'noarticletext' : 'noarticletextanon';
1518  $text = $context->msg( $message )->plain();
1519  } else {
1520  $text = $context->msg( 'noarticletext-nopermission' )->plain();
1521  }
1522 
1523  $dir = $context->getLanguage()->getDir();
1524  $lang = $context->getLanguage()->getHtmlCode();
1525  $outputPage->addWikiTextAsInterface( Xml::openElement( 'div', [
1526  'class' => "noarticletext mw-content-$dir",
1527  'dir' => $dir,
1528  'lang' => $lang,
1529  ] ) . "\n$text\n</div>" );
1530  }
1531  }
1532 
1537  private function showViewError( string $errortext ) {
1538  $outputPage = $this->getContext()->getOutput();
1539  $outputPage->setPageTitle( $this->getContext()->msg( 'errorpagetitle' ) );
1540  $outputPage->disableClientCache();
1541  $outputPage->setRobotPolicy( 'noindex,nofollow' );
1542  $outputPage->clearHTML();
1543  $outputPage->addHTML( Html::errorBox( $outputPage->parseAsContent( $errortext ) ) );
1544  }
1545 
1552  public function showDeletedRevisionHeader() {
1553  if ( !$this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1554  // Not deleted
1555  return true;
1556  }
1557  $outputPage = $this->getContext()->getOutput();
1558  // Used in wikilinks, should not contain whitespaces
1559  $titleText = $this->getTitle()->getPrefixedDBkey();
1560  // If the user is not allowed to see it...
1561  if ( !$this->mRevisionRecord->userCan(
1562  RevisionRecord::DELETED_TEXT,
1563  $this->getContext()->getAuthority()
1564  ) ) {
1565  $outputPage->addHTML(
1567  $outputPage->msg( 'rev-deleted-text-permission', $titleText )->parse(),
1568  'plainlinks'
1569  )
1570  );
1571 
1572  return false;
1573  // If the user needs to confirm that they want to see it...
1574  } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) != 1 ) {
1575  # Give explanation and add a link to view the revision...
1576  $oldid = intval( $this->getOldID() );
1577  $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" );
1578  $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ?
1579  'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
1580  $outputPage->addHTML(
1582  $outputPage->msg( $msg, $link )->parse(),
1583  'plainlinks'
1584  )
1585  );
1586 
1587  return false;
1588  // We are allowed to see...
1589  } else {
1590  $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
1591  ? [ 'rev-suppressed-text-view', $titleText ]
1592  : [ 'rev-deleted-text-view', $titleText ];
1593  $outputPage->addHTML(
1595  $outputPage->msg( $msg[0], $msg[1] )->parse(),
1596  'plainlinks'
1597  )
1598  );
1599 
1600  return true;
1601  }
1602  }
1603 
1612  public function setOldSubtitle( $oldid = 0 ) {
1613  if ( !$this->getHookRunner()->onDisplayOldSubtitle( $this, $oldid ) ) {
1614  return;
1615  }
1616 
1617  $context = $this->getContext();
1618  $unhide = $context->getRequest()->getInt( 'unhide' ) == 1;
1619 
1620  # Cascade unhide param in links for easy deletion browsing
1621  $extraParams = [];
1622  if ( $unhide ) {
1623  $extraParams['unhide'] = 1;
1624  }
1625 
1626  if ( $this->mRevisionRecord && $this->mRevisionRecord->getId() === $oldid ) {
1627  $revisionRecord = $this->mRevisionRecord;
1628  } else {
1629  $revisionRecord = $this->revisionStore->getRevisionById( $oldid );
1630  }
1631 
1632  $timestamp = $revisionRecord->getTimestamp();
1633 
1634  $current = ( $oldid == $this->mPage->getLatest() );
1635  $language = $context->getLanguage();
1636  $user = $context->getUser();
1637 
1638  $td = $language->userTimeAndDate( $timestamp, $user );
1639  $tddate = $language->userDate( $timestamp, $user );
1640  $tdtime = $language->userTime( $timestamp, $user );
1641 
1642  # Show user links if allowed to see them. If hidden, then show them only if requested...
1643  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable revisionRecord known to exists
1644  $userlinks = Linker::revUserTools( $revisionRecord, !$unhide );
1645 
1646  $infomsg = $current && !$context->msg( 'revision-info-current' )->isDisabled()
1647  ? 'revision-info-current'
1648  : 'revision-info';
1649 
1650  $outputPage = $context->getOutput();
1651  $outputPage->addModuleStyles( [
1652  'mediawiki.action.styles',
1653  'mediawiki.interface.helpers.styles'
1654  ] );
1655 
1656  $revisionUser = $revisionRecord->getUser();
1657  $revisionInfo = "<div id=\"mw-{$infomsg}\">" .
1658  $context->msg( $infomsg, $td )
1659  ->rawParams( $userlinks )
1660  ->params(
1661  $revisionRecord->getId(),
1662  $tddate,
1663  $tdtime,
1664  $revisionUser ? $revisionUser->getName() : ''
1665  )
1666  ->rawParams( $this->commentFormatter->formatRevision(
1667  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable revisionRecord known to exists
1668  $revisionRecord,
1669  $user,
1670  true,
1671  !$unhide
1672  ) )
1673  ->parse() .
1674  "</div>";
1675 
1676  $lnk = $current
1677  ? $context->msg( 'currentrevisionlink' )->escaped()
1678  : $this->linkRenderer->makeKnownLink(
1679  $this->getTitle(),
1680  $context->msg( 'currentrevisionlink' )->text(),
1681  [],
1682  $extraParams
1683  );
1684  $curdiff = $current
1685  ? $context->msg( 'diff' )->escaped()
1686  : $this->linkRenderer->makeKnownLink(
1687  $this->getTitle(),
1688  $context->msg( 'diff' )->text(),
1689  [],
1690  [
1691  'diff' => 'cur',
1692  'oldid' => $oldid
1693  ] + $extraParams
1694  );
1695  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable revisionRecord known to exists
1696  $prevExist = (bool)$this->revisionStore->getPreviousRevision( $revisionRecord );
1697  $prevlink = $prevExist
1698  ? $this->linkRenderer->makeKnownLink(
1699  $this->getTitle(),
1700  $context->msg( 'previousrevision' )->text(),
1701  [],
1702  [
1703  'direction' => 'prev',
1704  'oldid' => $oldid
1705  ] + $extraParams
1706  )
1707  : $context->msg( 'previousrevision' )->escaped();
1708  $prevdiff = $prevExist
1709  ? $this->linkRenderer->makeKnownLink(
1710  $this->getTitle(),
1711  $context->msg( 'diff' )->text(),
1712  [],
1713  [
1714  'diff' => 'prev',
1715  'oldid' => $oldid
1716  ] + $extraParams
1717  )
1718  : $context->msg( 'diff' )->escaped();
1719  $nextlink = $current
1720  ? $context->msg( 'nextrevision' )->escaped()
1721  : $this->linkRenderer->makeKnownLink(
1722  $this->getTitle(),
1723  $context->msg( 'nextrevision' )->text(),
1724  [],
1725  [
1726  'direction' => 'next',
1727  'oldid' => $oldid
1728  ] + $extraParams
1729  );
1730  $nextdiff = $current
1731  ? $context->msg( 'diff' )->escaped()
1732  : $this->linkRenderer->makeKnownLink(
1733  $this->getTitle(),
1734  $context->msg( 'diff' )->text(),
1735  [],
1736  [
1737  'diff' => 'next',
1738  'oldid' => $oldid
1739  ] + $extraParams
1740  );
1741 
1742  $cdel = Linker::getRevDeleteLink(
1743  $context->getAuthority(),
1744  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable revisionRecord known to exists
1745  $revisionRecord,
1746  $this->getTitle()
1747  );
1748  if ( $cdel !== '' ) {
1749  $cdel .= ' ';
1750  }
1751 
1752  // the outer div is need for styling the revision info and nav in MobileFrontend
1753  $outputPage->addSubtitle(
1755  $revisionInfo .
1756  "<div id=\"mw-revision-nav\">" . $cdel .
1757  $context->msg( 'revision-nav' )->rawParams(
1758  $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff
1759  )->escaped() . "</div>",
1760  'mw-revision'
1761  )
1762  );
1763  }
1764 
1777  public static function getRedirectHeaderHtml( Language $lang, $target, $forceKnown = false ) {
1778  if ( is_array( $target ) ) {
1779  // Up until 1.39, $target was allowed to be an array.
1780  wfDeprecatedMsg( 'The $target parameter can no longer be an array', '1.39' );
1781  $target = reset( $target ); // There really can only be one element (T296430)
1782  }
1783 
1784  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1785 
1786  $html = '<ul class="redirectText">';
1787  if ( $forceKnown ) {
1788  $link = $linkRenderer->makeKnownLink(
1789  $target,
1790  $target->getFullText(),
1791  [],
1792  // Make sure wiki page redirects are not followed
1793  $target->isRedirect() ? [ 'redirect' => 'no' ] : []
1794  );
1795  } else {
1796  $link = $linkRenderer->makeLink(
1797  $target,
1798  $target->getFullText(),
1799  [],
1800  // Make sure wiki page redirects are not followed
1801  $target->isRedirect() ? [ 'redirect' => 'no' ] : []
1802  );
1803  }
1804  $html .= '<li>' . $link . '</li>';
1805  $html .= '</ul>';
1806 
1807  $redirectToText = wfMessage( 'redirectto' )->inLanguage( $lang )->escaped();
1808 
1809  return '<div class="redirectMsg">' .
1810  '<p>' . $redirectToText . '</p>' .
1811  $html .
1812  '</div>';
1813  }
1814 
1823  public function addHelpLink( $to, $overrideBaseUrl = false ) {
1824  $out = $this->getContext()->getOutput();
1825  $msg = $out->msg( 'namespace-' . $this->getTitle()->getNamespace() . '-helppage' );
1826 
1827  if ( !$msg->isDisabled() ) {
1828  $title = Title::newFromText( $msg->plain() );
1829  if ( $title instanceof Title ) {
1830  $out->addHelpLink( $title->getLocalURL(), true );
1831  }
1832  } else {
1833  $out->addHelpLink( $to, $overrideBaseUrl );
1834  }
1835  }
1836 
1840  public function render() {
1841  $this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' );
1842  $this->getContext()->getOutput()->setArticleBodyOnly( true );
1843  // We later set 'enableSectionEditLinks=false' based on this; also used by ImagePage
1844  $this->viewIsRenderAction = true;
1845  $this->view();
1846  }
1847 
1851  public function protect() {
1852  $form = new ProtectionForm( $this );
1853  $form->execute();
1854  }
1855 
1859  public function unprotect() {
1860  $this->protect();
1861  }
1862 
1876  public function doDelete( $reason, $suppress = false, $immediate = false ) {
1877  wfDeprecated( __METHOD__, '1.37' );
1878  $error = '';
1879  $context = $this->getContext();
1880  $outputPage = $context->getOutput();
1881  $user = $context->getUser();
1882  $status = $this->mPage->doDeleteArticleReal(
1883  $reason, $user, $suppress, null, $error,
1884  null, [], 'delete', $immediate
1885  );
1886 
1887  if ( $status->isOK() ) {
1888  $deleted = $this->getTitle()->getPrefixedText();
1889 
1890  $outputPage->setPageTitle( $context->msg( 'actioncomplete' ) );
1891  $outputPage->setRobotPolicy( 'noindex,nofollow' );
1892 
1893  if ( $status->isGood() ) {
1894  $loglink = '[[Special:Log/delete|' . $context->msg( 'deletionlog' )->text() . ']]';
1895  $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
1896  $this->getHookRunner()->onArticleDeleteAfterSuccess( $this->getTitle(), $outputPage );
1897  } else {
1898  $outputPage->addWikiMsg( 'delete-scheduled', wfEscapeWikiText( $deleted ) );
1899  }
1900 
1901  $outputPage->returnToMain( false );
1902  } else {
1903  $outputPage->setPageTitle(
1904  $context->msg( 'cannotdelete-title',
1905  $this->getTitle()->getPrefixedText() )
1906  );
1907 
1908  if ( $error == '' ) {
1909  $outputPage->wrapWikiTextAsInterface(
1910  'error mw-error-cannotdelete',
1911  $status->getWikiText( false, false, $context->getLanguage() )
1912  );
1913  $deleteLogPage = new LogPage( 'delete' );
1914  $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) );
1915 
1917  $outputPage,
1918  'delete',
1919  $this->getTitle()
1920  );
1921  } else {
1922  $outputPage->addHTML( $error );
1923  }
1924  }
1925  }
1926 
1927  /* Caching functions */
1928 
1936  protected function tryFileCache() {
1937  static $called = false;
1938 
1939  if ( $called ) {
1940  wfDebug( "Article::tryFileCache(): called twice!?" );
1941  return false;
1942  }
1943 
1944  $called = true;
1945  if ( $this->isFileCacheable() ) {
1946  $cache = new HTMLFileCache( $this->getTitle(), 'view' );
1947  if ( $cache->isCacheGood( $this->mPage->getTouched() ) ) {
1948  wfDebug( "Article::tryFileCache(): about to load file" );
1949  $cache->loadFromFileCache( $this->getContext() );
1950  return true;
1951  } else {
1952  wfDebug( "Article::tryFileCache(): starting buffer" );
1953  ob_start( [ &$cache, 'saveToFileCache' ] );
1954  }
1955  } else {
1956  wfDebug( "Article::tryFileCache(): not cacheable" );
1957  }
1958 
1959  return false;
1960  }
1961 
1967  public function isFileCacheable( $mode = HTMLFileCache::MODE_NORMAL ) {
1968  $cacheable = false;
1969 
1970  if ( HTMLFileCache::useFileCache( $this->getContext(), $mode ) ) {
1971  $cacheable = $this->mPage->getId()
1972  && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect();
1973  // Extension may have reason to disable file caching on some pages.
1974  if ( $cacheable ) {
1975  $cacheable = $this->getHookRunner()->onIsFileCacheable( $this ) ?? false;
1976  }
1977  }
1978 
1979  return $cacheable;
1980  }
1981 
1995  public function getParserOutput( $oldid = null, UserIdentity $user = null ) {
1996  if ( $user === null ) {
1997  $parserOptions = $this->getParserOptions();
1998  } else {
1999  $parserOptions = $this->mPage->makeParserOptions( $user );
2000  $parserOptions->setRenderReason( 'page-view' );
2001  }
2002 
2003  return $this->mPage->getParserOutput( $parserOptions, $oldid );
2004  }
2005 
2010  public function getParserOptions() {
2011  $parserOptions = $this->mPage->makeParserOptions( $this->getContext() );
2012  $parserOptions->setRenderReason( 'page-view' );
2013  return $parserOptions;
2014  }
2015 
2022  public function setContext( $context ) {
2023  $this->mContext = $context;
2024  }
2025 
2032  public function getContext() {
2033  if ( $this->mContext instanceof IContextSource ) {
2034  return $this->mContext;
2035  } else {
2036  wfDebug( __METHOD__ . " called and \$mContext is null. " .
2037  "Return RequestContext::getMain()" );
2038  return RequestContext::getMain();
2039  }
2040  }
2041 
2051  public function __get( $fname ) {
2052  wfDeprecatedMsg( "Accessing Article::\$$fname is deprecated since MediaWiki 1.35",
2053  '1.35' );
2054 
2055  if ( property_exists( $this->mPage, $fname ) ) {
2056  return $this->mPage->$fname;
2057  }
2058  trigger_error( 'Inaccessible property via __get(): ' . $fname, E_USER_NOTICE );
2059  }
2060 
2070  public function __set( $fname, $fvalue ) {
2071  wfDeprecatedMsg( "Setting Article::\$$fname is deprecated since MediaWiki 1.35",
2072  '1.35' );
2073 
2074  if ( property_exists( $this->mPage, $fname ) ) {
2075  $this->mPage->$fname = $fvalue;
2076  // Note: extensions may want to toss on new fields
2077  } elseif ( !in_array( $fname, [ 'mContext', 'mPage' ] ) ) {
2078  $this->mPage->$fname = $fvalue;
2079  } else {
2080  trigger_error( 'Inaccessible property via __set(): ' . $fname, E_USER_NOTICE );
2081  }
2082  }
2083 
2089  public function getActionOverrides() {
2090  return $this->mPage->getActionOverrides();
2091  }
2092 
2098  public function getTimestamp() {
2099  wfDeprecated( __METHOD__, '1.35' );
2100  return $this->mPage->getTimestamp();
2101  }
2102 }
const NS_USER
Definition: Defines.php:66
const NS_FILE
Definition: Defines.php:70
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,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
getContext()
Legacy class representing an editable page and handling UI for some page actions.
Definition: Article.php:52
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition: Article.php:217
getContext()
Gets the context this Article is executed in.
Definition: Article.php:2032
getOldIDFromRequest()
Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect.
Definition: Article.php:291
getRedirectedFrom()
Get the page this view was redirected from.
Definition: Article.php:228
Title null $mRedirectedFrom
Title from which we were redirected here, if any.
Definition: Article.php:73
bool $viewIsRenderAction
Whether render() was called.
Definition: Article.php:96
view()
This is the default action of the index.php entry point: just view the page of the given title.
Definition: Article.php:450
__construct(Title $title, $oldId=null)
Definition: Article.php:144
getRobotPolicy( $action, ParserOutput $pOutput=null)
Get the robot policy to be used for the current view.
Definition: Article.php:974
static purgePatrolFooterCache( $articleID)
Purge the cache used to check if it is worth showing the patrol footer For example,...
Definition: Article.php:1364
doDelete( $reason, $suppress=false, $immediate=false)
Perform a deletion and output success or failure messages.
Definition: Article.php:1876
ParserOutput null false $mParserOutput
The ParserOutput generated for viewing the page, initialized by view().
Definition: Article.php:89
getOldID()
Definition: Article.php:278
LinkRenderer $linkRenderer
Definition: Article.php:101
getTitle()
Get the title object of the article.
Definition: Article.php:246
getActionOverrides()
Call to WikiPage function for backwards compatibility.
Definition: Article.php:2089
isDiffOnlyView()
Definition: Article.php:960
adjustDisplayTitle(ParserOutput $pOutput)
Adjust title for pages with displaytitle, -{T|}- or language conversion.
Definition: Article.php:886
showDeletedRevisionHeader()
If the revision requested for view is deleted, check permissions.
Definition: Article.php:1552
getParserOptions()
Get parser options suitable for rendering the primary article wikitext.
Definition: Article.php:2010
clear()
Definition: Article.php:260
IContextSource null $mContext
The context this Article is executed in.
Definition: Article.php:61
getParserOutput( $oldid=null, UserIdentity $user=null)
#-
Definition: Article.php:1995
static getRedirectHeaderHtml(Language $lang, $target, $forceKnown=false)
Return the HTML for the top of a redirect page.
Definition: Article.php:1777
protect()
action=protect handler
Definition: Article.php:1851
string false $mRedirectUrl
URL to redirect to or false if none.
Definition: Article.php:76
isCurrent()
Returns true if the currently-referenced revision is the current edit to this page (and it exists).
Definition: Article.php:417
showMissingArticle()
Show the error text for a missing article.
Definition: Article.php:1373
__set( $fname, $fvalue)
Definition: Article.php:2070
unprotect()
action=unprotect handler (alias)
Definition: Article.php:1859
newPage(Title $title)
Definition: Article.php:163
getPage()
Get the WikiPage object of this instance.
Definition: Article.php:256
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: Article.php:1823
getTimestamp()
Definition: Article.php:2098
static newFromID( $id)
Constructor from a page id.
Definition: Article.php:172
int null $mOldId
The oldid of the article that was requested to be shown, 0 for the current revision.
Definition: Article.php:70
static formatRobotPolicy( $policy)
Converts a String robot policy into an associative array, to allow merging of several policies using ...
Definition: Article.php:1058
fetchRevisionRecord()
Fetches the revision to work on.
Definition: Article.php:359
showPatrolFooter()
If patrol is possible, output a patrol UI box.
Definition: Article.php:1194
setOldSubtitle( $oldid=0)
Generate the navigation links when browsing through an article revisions It shows the information as:...
Definition: Article.php:1612
showViewFooter()
Show the footer section of an ordinary page view.
Definition: Article.php:1170
WikiPage $mPage
The WikiPage object of this instance.
Definition: Article.php:64
setRedirectedFrom(Title $from)
Tell the page view functions that this view was redirected from another page on the wiki.
Definition: Article.php:237
isFileCacheable( $mode=HTMLFileCache::MODE_NORMAL)
Check if the page can be cached.
Definition: Article.php:1967
tryFileCache()
checkLastModified returns true if it has taken care of all output to the client that is necessary for...
Definition: Article.php:1936
getRevIdFetched()
Use this to fetch the rev ID used on page views.
Definition: Article.php:436
showNamespaceHeader()
Show a header specific to the namespace currently being viewed, like [[MediaWiki:Talkpagetext]].
Definition: Article.php:1158
__get( $fname)
Definition: Article.php:2051
static newFromTitle( $title, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition: Article.php:184
showDiffPage()
Show a diff page according to current request variables.
Definition: Article.php:901
render()
Handle action=render.
Definition: Article.php:1840
showRedirectedFromHeader()
If this request is a redirect view, send "redirected from" subtitle to the output.
Definition: Article.php:1085
setContext( $context)
Sets the context this Article is executed in.
Definition: Article.php:2022
getCacheRevisionId()
Definition: CacheTime.php:98
getCacheTime()
Definition: CacheTime.php:67
Special handling for category description pages.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition: EditPage.php:124
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.
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:775
static errorBox( $html, $heading='', $className='')
Return an error box.
Definition: Html.php:788
Rendering of file description pages.
Definition: ImagePage.php:33
Base class for language-specific code.
Definition: Language.php:56
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition: LogPage.php:40
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...
Class that generates HTML anchor link elements for pages.
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
makeLink( $target, $text=null, array $extraAttribs=[], array $query=[])
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.
Service for getting rendered output of a given page.
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.
getPageId( $wikiId=self::LOCAL)
Get the page ID.
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
UserNameUtils service.
Provides access to user options.
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:57
disable()
Disable output completely, i.e.
addWikiMsg(... $args)
Add a wikitext-formatted message to the output.
wrapWikiTextAsInterface( $wrapperClass, $text)
Convert wikitext in the user interface language to HTML and add it to the buffer with a <div class="$...
setArticleFlag( $newVal)
Set whether the displayed content is related to the source of the corresponding article on the wiki S...
setRobotPolicy( $policy)
Set the robot policy for the page: http://www.robotstxt.org/meta.html
Definition: OutputPage.php:935
setIndexPolicy( $policy)
Set the index policy for the page, but leave the follow policy un- touched.
setPageTitle( $name)
"Page title" means the contents of <h1>.
redirect( $url, $responsecode='302')
Redirect to $url rather than displaying the normal page.
Definition: OutputPage.php:427
setLastModified( $timestamp)
Override the last modified timestamp.
Definition: OutputPage.php:924
adaptCdnTTL( $mtime, $minTTL=0, $maxTTL=0)
Get TTL in [$minTTL,$maxTTL] and pass it to lowerCdnMaxage()
setRevisionIsCurrent(bool $isCurrent)
Set whether the revision displayed (as set in ::setRevisionId()) is the latest revision of the page.
wrapWikiMsg( $wrap,... $msgSpecs)
This function takes a number of message/argument specifications, wraps them in some overall structure...
setCdnMaxage( $maxage)
Set the value of the "s-maxage" part of the "Cache-control" HTTP header.
parseAsContent( $text, $linestart=true)
Parse wikitext in the page content language and return the HTML.
addParserOutput(ParserOutput $parserOutput, $poOptions=[])
Add everything from a ParserOutput object.
addWikiTextAsInterface( $text, $linestart=true, PageReference $title=null)
Convert wikitext in the user interface language to HTML and add it to the buffer.
setFollowPolicy( $policy)
Set the follow policy for the page, but leave the index policy un- touched.
isPrintable()
Return whether the page is "printable".
addModuleStyles( $modules)
Load the styles of one or more style-only ResourceLoader modules on this page.
Definition: OutputPage.php:650
setHTMLTitle( $name)
"HTML title" means the contents of "<title>".
returnToMain( $unused=null, $returnto=null, $returntoquery=null)
Add a "return to" link pointing to a specified title, or the title indicated in the request,...
disableClientCache()
Force the page to send nocache headers.
addSubtitle( $str)
Add $str to the subtitle.
setRevisionId( $revid)
Set the revision ID which will be seen by the wiki text parser for things such as embedded {{REVISION...
clearHTML()
Clear the body HTML.
setPreventClickjacking(bool $enable)
Set the prevent-clickjacking flag.
addHTML( $text)
Append $text to the body HTML.
addJsConfigVars( $keys, $value=null)
Add one or more variables to be set in mw.config in JavaScript.
setCanonicalUrl( $url)
Set the URL to be used for the <link rel=canonical>>.
Definition: OutputPage.php:510
setRevisionTimestamp( $timestamp)
Set the timestamp of the revision which will be displayed.
setRedirectedFrom(PageReference $t)
Set $mRedirectedFrom, the page which redirected us to the current page.
prependHTML( $text)
Prepend $text to the body HTML.
addModules( $modules)
Load one or more ResourceLoader modules on this page.
Definition: OutputPage.php:624
addWikiTextAsContent( $text, $linestart=true, PageReference $title=null)
Convert wikitext in the page content language to HTML and add it to the buffer.
Used to show archived pages and eventually restore them.
Definition: PageArchive.php:31
Set options of the Parser.
setIsPrintable( $x)
Parsing the printable version of the page?
setRenderReason(string $renderReason)
Sets reason for rendering the content.
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:6480
static newSpec(int $revisionId, int $pageId, array $params=[])
Show an error when a user tries to do something they do not have the necessary permissions for.
Handles the page protection UI and backend.
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.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
isOK()
Returns whether the operation completed.
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:46
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition: Status.php:191
Represents a title within MediaWiki.
Definition: Title.php:52
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:521
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:373
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:641
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:70
static newFromName( $name, $validate='valid')
Definition: User.php:591
Base representation for an editable wiki page.
Definition: WikiPage.php:72
getTitle()
Get the title object of the article.
Definition: WikiPage.php:315
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:112
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:43
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:29
const DB_REPLICA
Definition: defines.php:26
$content
Definition: router.php:76
return true
Definition: router.php:90
if(!isset( $args[0])) $lang