MediaWiki  master
Article.php
Go to the documentation of this file.
1 <?php
22 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
35 use Wikimedia\IPUtils;
36 use Wikimedia\NonSerializable\NonSerializableTrait;
37 
47 class Article implements Page {
48  use ProtectedHookAccessorTrait;
49  use NonSerializableTrait;
50 
56  protected $mContext;
57 
59  protected $mPage;
60 
65  public $mOldId;
66 
68  public $mRedirectedFrom = null;
69 
71  public $mRedirectUrl = false;
72 
77  private $fetchResult = null;
78 
84  public $mParserOutput = null;
85 
91  protected $viewIsRenderAction = false;
92 
96  protected $linkRenderer;
97 
101  private $revisionStore;
102 
107 
111  private $userNameUtils;
112 
119  private $mRevisionRecord = null;
120 
125  public function __construct( Title $title, $oldId = null ) {
126  $this->mOldId = $oldId;
127  $this->mPage = $this->newPage( $title );
128 
129  $services = MediaWikiServices::getInstance();
130  $this->linkRenderer = $services->getLinkRenderer();
131  $this->revisionStore = $services->getRevisionStore();
132  $this->watchlistManager = $services->getWatchlistManager();
133  $this->userNameUtils = $services->getUserNameUtils();
134  }
135 
140  protected function newPage( Title $title ) {
141  return new WikiPage( $title );
142  }
143 
149  public static function newFromID( $id ) {
150  $t = Title::newFromID( $id );
151  return $t == null ? null : new static( $t );
152  }
153 
161  public static function newFromTitle( $title, IContextSource $context ) {
162  if ( $title->getNamespace() === NS_MEDIA ) {
163  // XXX: This should not be here, but where should it go?
164  $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
165  }
166 
167  $page = null;
168  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
169  Hooks::runner()->onArticleFromTitle( $title, $page, $context );
170  if ( !$page ) {
171  switch ( $title->getNamespace() ) {
172  case NS_FILE:
173  $page = new ImagePage( $title );
174  break;
175  case NS_CATEGORY:
176  $page = new CategoryPage( $title );
177  break;
178  default:
179  $page = new Article( $title );
180  }
181  }
182  $page->setContext( $context );
183 
184  return $page;
185  }
186 
194  public static function newFromWikiPage( WikiPage $page, IContextSource $context ) {
195  $article = self::newFromTitle( $page->getTitle(), $context );
196  $article->mPage = $page; // override to keep process cached vars
197  return $article;
198  }
199 
205  public function getRedirectedFrom() {
206  return $this->mRedirectedFrom;
207  }
208 
214  public function setRedirectedFrom( Title $from ) {
215  $this->mRedirectedFrom = $from;
216  }
217 
223  public function getTitle() {
224  return $this->mPage->getTitle();
225  }
226 
233  public function getPage() {
234  return $this->mPage;
235  }
236 
237  public function clear() {
238  $this->mRedirectedFrom = null; # Title object if set
239  $this->mRedirectUrl = false;
240  $this->mRevisionRecord = null;
241  $this->fetchResult = null;
242 
243  // TODO hard-deprecate direct access to public fields
244 
245  $this->mPage->clear();
246  }
247 
255  public function getOldID() {
256  if ( $this->mOldId === null ) {
257  $this->mOldId = $this->getOldIDFromRequest();
258  }
259 
260  return $this->mOldId;
261  }
262 
268  public function getOldIDFromRequest() {
269  $this->mRedirectUrl = false;
270 
271  $request = $this->getContext()->getRequest();
272  $oldid = $request->getIntOrNull( 'oldid' );
273 
274  if ( $oldid === null ) {
275  return 0;
276  }
277 
278  if ( $oldid !== 0 ) {
279  # Load the given revision and check whether the page is another one.
280  # In that case, update this instance to reflect the change.
281  if ( $oldid === $this->mPage->getLatest() ) {
282  $this->mRevisionRecord = $this->mPage->getRevisionRecord();
283  } else {
284  $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
285  if ( $this->mRevisionRecord !== null ) {
286  $revPageId = $this->mRevisionRecord->getPageId();
287  // Revision title doesn't match the page title given?
288  if ( $this->mPage->getId() != $revPageId ) {
289  $function = get_class( $this->mPage ) . '::newFromID';
290  $this->mPage = $function( $revPageId );
291  }
292  }
293  }
294  }
295 
296  $oldRev = $this->mRevisionRecord;
297  if ( $request->getRawVal( 'direction' ) === 'next' ) {
298  $nextid = 0;
299  if ( $oldRev ) {
300  $nextRev = $this->revisionStore->getNextRevision( $oldRev );
301  if ( $nextRev ) {
302  $nextid = $nextRev->getId();
303  }
304  }
305  if ( $nextid ) {
306  $oldid = $nextid;
307  $this->mRevisionRecord = null;
308  } else {
309  $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' );
310  }
311  } elseif ( $request->getRawVal( 'direction' ) === 'prev' ) {
312  $previd = 0;
313  if ( $oldRev ) {
314  $prevRev = $this->revisionStore->getPreviousRevision( $oldRev );
315  if ( $prevRev ) {
316  $previd = $prevRev->getId();
317  }
318  }
319  if ( $previd ) {
320  $oldid = $previd;
321  $this->mRevisionRecord = null;
322  }
323  }
324 
325  return $oldid;
326  }
327 
337  public function fetchRevisionRecord() {
338  if ( $this->fetchResult ) {
339  return $this->mRevisionRecord;
340  }
341 
342  $oldid = $this->getOldID();
343 
344  // $this->mRevisionRecord might already be fetched by getOldIDFromRequest()
345  if ( !$this->mRevisionRecord ) {
346  if ( !$oldid ) {
347  $this->mRevisionRecord = $this->mPage->getRevisionRecord();
348 
349  if ( !$this->mRevisionRecord ) {
350  wfDebug( __METHOD__ . " failed to find page data for title " .
351  $this->getTitle()->getPrefixedText() );
352 
353  // Output for this case is done by showMissingArticle().
354  $this->fetchResult = Status::newFatal( 'noarticletext' );
355  return null;
356  }
357  } else {
358  $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
359 
360  if ( !$this->mRevisionRecord ) {
361  wfDebug( __METHOD__ . " failed to load revision, rev_id $oldid" );
362 
363  $this->fetchResult = Status::newFatal( 'missing-revision', $oldid );
364  return null;
365  }
366  }
367  }
368 
369  if ( !$this->mRevisionRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getContext()->getAuthority() ) ) {
370  wfDebug( __METHOD__ . " failed to retrieve content of revision " . $this->mRevisionRecord->getId() );
371 
372  // Output for this case is done by showDeletedRevisionHeader().
373  // title used in wikilinks, should not contain whitespaces
374  $this->fetchResult = new Status;
375  $title = $this->getTitle()->getPrefixedDBkey();
376 
377  if ( $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
378  $this->fetchResult->fatal( 'rev-suppressed-text' );
379  } else {
380  $this->fetchResult->fatal( 'rev-deleted-text-permission', $title );
381  }
382 
383  return null;
384  }
385 
386  $this->fetchResult = Status::newGood( $this->mRevisionRecord );
387  return $this->mRevisionRecord;
388  }
389 
395  public function isCurrent() {
396  # If no oldid, this is the current version.
397  if ( $this->getOldID() == 0 ) {
398  return true;
399  }
400 
401  return $this->mPage->exists() &&
402  $this->mRevisionRecord &&
403  $this->mRevisionRecord->isCurrent();
404  }
405 
414  public function getRevIdFetched() {
415  if ( $this->fetchResult && $this->fetchResult->isOK() ) {
416  return $this->fetchResult->value->getId();
417  } else {
418  return $this->mPage->getLatest();
419  }
420  }
421 
426  public function view() {
427  $useFileCache = MediaWikiServices::getInstance()
428  ->getMainConfig()->get( MainConfigNames::UseFileCache );
429 
430  # Get variables from query string
431  # As side effect this will load the revision and update the title
432  # in a revision ID is passed in the request, so this should remain
433  # the first call of this method even if $oldid is used way below.
434  $oldid = $this->getOldID();
435 
436  $user = $this->getContext()->getUser();
437  # Another check in case getOldID() is altering the title
438  $permissionStatus = PermissionStatus::newEmpty();
439  if ( !$this->getContext()->getAuthority()
440  ->authorizeRead( 'read', $this->getTitle(), $permissionStatus )
441  ) {
442  wfDebug( __METHOD__ . ": denied on secondary read check" );
443  throw new PermissionsError( 'read', $permissionStatus );
444  }
445 
446  $outputPage = $this->getContext()->getOutput();
447  # getOldID() may as well want us to redirect somewhere else
448  if ( $this->mRedirectUrl ) {
449  $outputPage->redirect( $this->mRedirectUrl );
450  wfDebug( __METHOD__ . ": redirecting due to oldid" );
451 
452  return;
453  }
454 
455  # If we got diff in the query, we want to see a diff page instead of the article.
456  if ( $this->getContext()->getRequest()->getCheck( 'diff' ) ) {
457  wfDebug( __METHOD__ . ": showing diff page" );
458  $this->showDiffPage();
459 
460  return;
461  }
462 
463  # Set page title (may be overridden by DISPLAYTITLE)
464  $outputPage->setPageTitle( $this->getTitle()->getPrefixedText() );
465 
466  $outputPage->setArticleFlag( true );
467  # Allow frames by default
468  $outputPage->setPreventClickjacking( false );
469 
470  $skin = $outputPage->getSkin();
471  $skinOptions = $skin->getOptions();
472 
473  $parserOptions = $this->getParserOptions();
474  $poOptions = [
475  'skin' => $skin,
476  'injectTOC' => $skinOptions['toc'],
477  ];
478  # Allow extensions to vary parser options used for article rendering
479  Hooks::runner()->onArticleParserOptions( $this, $parserOptions );
480  # Render printable version, use printable version cache
481  if ( $outputPage->isPrintable() ) {
482  $parserOptions->setIsPrintable( true );
483  $poOptions['enableSectionEditLinks'] = false;
484  $outputPage->prependHTML(
486  $outputPage->msg( 'printableversion-deprecated-warning' )->escaped()
487  )
488  );
489  } elseif ( $this->viewIsRenderAction || !$this->isCurrent() ||
490  !$this->getContext()->getAuthority()->probablyCan( 'edit', $this->getTitle() )
491  ) {
492  $poOptions['enableSectionEditLinks'] = false;
493  }
494 
495  # Try client and file cache
496  if ( $oldid === 0 && $this->mPage->checkTouched() ) {
497  # Try to stream the output from file cache
498  if ( $useFileCache && $this->tryFileCache() ) {
499  wfDebug( __METHOD__ . ": done file cache" );
500  # tell wgOut that output is taken care of
501  $outputPage->disable();
502  $this->mPage->doViewUpdates( $user, $oldid );
503 
504  return;
505  }
506  }
507 
508  $this->showRedirectedFromHeader();
509  $this->showNamespaceHeader();
510 
511  if ( $this->viewIsRenderAction ) {
512  $poOptions += [ 'absoluteURLs' => true ];
513  }
514  $poOptions += [ 'includeDebugInfo' => true ];
515 
516  $continue =
517  $this->generateContentOutput( $user, $parserOptions, $oldid, $outputPage, $poOptions );
518 
519  if ( !$continue ) {
520  return;
521  }
522 
523  # For the main page, overwrite the <title> element with the con-
524  # tents of 'pagetitle-view-mainpage' instead of the default (if
525  # that's not empty).
526  # This message always exists because it is in the i18n files
527  if ( $this->getTitle()->isMainPage() ) {
528  $msg = wfMessage( 'pagetitle-view-mainpage' )->inContentLanguage();
529  if ( !$msg->isDisabled() ) {
530  $outputPage->setHTMLTitle( $msg->page( $this->getTitle() )->text() );
531  }
532  }
533 
534  # Use adaptive TTLs for CDN so delayed/failed purges are noticed less often.
535  # This could use getTouched(), but that could be scary for major template edits.
536  $outputPage->adaptCdnTTL( $this->mPage->getTimestamp(), IExpiringStore::TTL_DAY );
537 
538  $this->showViewFooter();
539  $this->mPage->doViewUpdates( $user, $oldid, $this->fetchRevisionRecord() );
540 
541  # Load the postEdit module if the user just saved this revision
542  # See also EditPage::setPostEditCookie
543  $request = $this->getContext()->getRequest();
545  $postEdit = $request->getCookie( $cookieKey );
546  if ( $postEdit ) {
547  # Clear the cookie. This also prevents caching of the response.
548  $request->response()->clearCookie( $cookieKey );
549  $outputPage->addJsConfigVars( 'wgPostEdit', $postEdit );
550  $outputPage->addModules( 'mediawiki.action.view.postEdit' ); // FIXME: test this
551  }
552  }
553 
566  private function generateContentOutput(
567  Authority $performer,
568  ParserOptions $parserOptions,
569  int $oldid,
570  OutputPage $outputPage,
571  array $textOptions
572  ): bool {
573  # Should the parser cache be used?
574  $useParserCache = true;
575  $pOutput = null;
576  $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
577 
578  // NOTE: $outputDone and $useParserCache may be changed by the hook
579  $this->getHookRunner()->onArticleViewHeader( $this, $outputDone, $useParserCache );
580  if ( $outputDone ) {
581  if ( $outputDone instanceof ParserOutput ) {
582  $pOutput = $outputDone;
583  }
584 
585  if ( $pOutput ) {
586  $this->doOutputMetaData( $pOutput, $outputPage );
587  }
588  return true;
589  }
590 
591  // Early abort if the page doesn't exist
592  if ( !$this->mPage->exists() ) {
593  wfDebug( __METHOD__ . ": showing missing article" );
594  $this->showMissingArticle();
595  $this->mPage->doViewUpdates( $performer );
596  return false; // skip all further output to OutputPage
597  }
598 
599  // Try the latest parser cache
600  // NOTE: try latest-revision cache first to avoid loading revision.
601  if ( $useParserCache && !$oldid ) {
602  $pOutput = $parserOutputAccess->getCachedParserOutput(
603  $this->getPage(),
604  $parserOptions,
605  null,
606  ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked
607  );
608 
609  if ( $pOutput ) {
610  $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
611  $this->doOutputMetaData( $pOutput, $outputPage );
612  return true;
613  }
614  }
615 
616  $rev = $this->fetchRevisionRecord();
617  if ( !$this->fetchResult->isOK() ) {
618  $this->showViewError( $this->fetchResult->getWikiText(
619  false, false, $this->getContext()->getLanguage()
620  ) );
621  return true;
622  }
623 
624  # Are we looking at an old revision
625  if ( $oldid ) {
626  $this->setOldSubtitle( $oldid );
627 
628  if ( !$this->showDeletedRevisionHeader() ) {
629  wfDebug( __METHOD__ . ": cannot view deleted revision" );
630  return false; // skip all further output to OutputPage
631  }
632 
633  // Try the old revision parser cache
634  // NOTE: Repeating cache check for old revision to avoid fetching $rev
635  // before it's absolutely necessary.
636  if ( $useParserCache ) {
637  $pOutput = $parserOutputAccess->getCachedParserOutput(
638  $this->getPage(),
639  $parserOptions,
640  $rev,
641  ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked in fetchRevisionRecord
642  );
643 
644  if ( $pOutput ) {
645  $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
646  $this->doOutputMetaData( $pOutput, $outputPage );
647  return true;
648  }
649  }
650  }
651 
652  # Ensure that UI elements requiring revision ID have
653  # the correct version information.
654  $outputPage->setRevisionId( $this->getRevIdFetched() );
655  # Preload timestamp to avoid a DB hit
656  $outputPage->setRevisionTimestamp( $rev->getTimestamp() );
657 
658  # Pages containing custom CSS or JavaScript get special treatment
659  if ( $this->getTitle()->isSiteConfigPage() || $this->getTitle()->isUserConfigPage() ) {
660  $dir = $this->getContext()->getLanguage()->getDir();
661  $lang = $this->getContext()->getLanguage()->getHtmlCode();
662 
663  $outputPage->wrapWikiMsg(
664  "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
665  'clearyourcache'
666  );
667  $outputPage->addModuleStyles( 'mediawiki.action.styles' );
668  } elseif ( !$this->getHookRunner()->onArticleRevisionViewCustom(
669  $rev,
670  $this->getTitle(),
671  $oldid,
672  $outputPage )
673  ) {
674  // NOTE: sync with hooks called in DifferenceEngine::renderNewRevision()
675  // Allow extensions do their own custom view for certain pages
676  $this->doOutputMetaData( $pOutput, $outputPage );
677  return true;
678  }
679 
680  # Run the parse, protected by a pool counter
681  wfDebug( __METHOD__ . ": doing uncached parse" );
682 
683  if ( !$rev ) {
684  // No revision, abort! Shouldn't happen.
685  return false;
686  }
687 
688  $opt = 0;
689 
690  // we already checked the cache in case 2, don't check again.
691  $opt |= ParserOutputAccess::OPT_NO_CHECK_CACHE;
692 
693  // we already checked in fetchRevisionRecord()
694  $opt |= ParserOutputAccess::OPT_NO_AUDIENCE_CHECK;
695 
696  if ( !$rev->getId() || !$useParserCache ) {
697  // fake revision or uncacheable options
698  $opt |= ParserOutputAccess::OPT_NO_CACHE;
699  }
700 
701  $renderStatus = $parserOutputAccess->getParserOutput(
702  $this->getPage(),
703  $parserOptions,
704  $rev,
705  $opt
706  );
707 
709  $rev,
710  $renderStatus,
711  $outputPage,
712  $textOptions
713  );
714 
715  if ( !$renderStatus->isOK() ) {
716  return true;
717  }
718 
719  $pOutput = $renderStatus->getValue();
720  $this->doOutputMetaData( $pOutput, $outputPage );
721  return true;
722  }
723 
728  private function doOutputMetaData( ?ParserOutput $pOutput, OutputPage $outputPage ) {
729  # Adjust title for main page & pages with displaytitle
730  if ( $pOutput ) {
731  $this->adjustDisplayTitle( $pOutput );
732  }
733 
734  # Check for any __NOINDEX__ tags on the page using $pOutput
735  $policy = $this->getRobotPolicy( 'view', $pOutput ?: null );
736  $outputPage->setIndexPolicy( $policy['index'] );
737  $outputPage->setFollowPolicy( $policy['follow'] ); // FIXME: test this
738 
739  $this->mParserOutput = $pOutput;
740  }
741 
747  private function doOutputFromParserCache(
748  ParserOutput $pOutput,
749  OutputPage $outputPage,
750  array $textOptions
751  ) {
752  # Ensure that UI elements requiring revision ID have
753  # the correct version information.
754  $outputPage->setRevisionId( $pOutput->getCacheRevisionId() ?? $this->getRevIdFetched() );
755  # Ensure that the skin has the necessary ToC information
756  # (and do this before OutputPage::addParserOutput() calls the
757  # OutputPageParserOutput hook)
758  $outputPage->setSections( $pOutput->getSections() );
759  $outputPage->addParserOutput( $pOutput, $textOptions );
760  # Preload timestamp to avoid a DB hit
761  $cachedTimestamp = $pOutput->getTimestamp();
762  if ( $cachedTimestamp !== null ) {
763  $outputPage->setRevisionTimestamp( $cachedTimestamp );
764  $this->mPage->setTimestamp( $cachedTimestamp );
765  }
766  }
767 
774  private function doOutputFromRenderStatus(
775  ?RevisionRecord $rev,
776  Status $renderStatus,
777  OutputPage $outputPage,
778  array $textOptions
779  ) {
780  $cdnMaxageStale = MediaWikiServices::getInstance()
781  ->getMainConfig()->get( MainConfigNames::CdnMaxageStale );
782  $ok = $renderStatus->isOK();
783 
784  $pOutput = $ok ? $renderStatus->getValue() : null;
785 
786  // Cache stale ParserOutput object with a short expiry
787  if ( $ok && $renderStatus->hasMessage( 'view-pool-dirty-output' ) ) {
788  $outputPage->setCdnMaxage( $cdnMaxageStale );
789  $outputPage->setLastModified( $pOutput->getCacheTime() );
790  $staleReason = $renderStatus->hasMessage( 'view-pool-contention' )
791  ? $this->getContext()->msg( 'view-pool-contention' )
792  : $this->getContext()->msg( 'view-pool-timeout' );
793  $outputPage->addHTML( "<!-- parser cache is expired, " .
794  "sending anyway due to $staleReason-->\n" );
795  }
796 
797  if ( !$renderStatus->isOK() ) {
798  $this->showViewError( $renderStatus->getWikiText(
799  false, 'view-pool-error', $this->getContext()->getLanguage()
800  ) );
801  return;
802  }
803 
804  if ( $pOutput ) {
805  $outputPage->addParserOutput( $pOutput, $textOptions );
806  $outputPage->setSections( $pOutput->getSections() );
807  }
808 
809  if ( $this->getRevisionRedirectTarget( $rev ) ) {
810  $outputPage->addSubtitle( "<span id=\"redirectsub\">" .
811  $this->getContext()->msg( 'redirectpagesub' )->parse() . "</span>" );
812  }
813  }
814 
819  private function getRevisionRedirectTarget( RevisionRecord $revision ) {
820  // TODO: find a *good* place for the code that determines the redirect target for
821  // a given revision!
822  // NOTE: Use main slot content. Compare code in DerivedPageDataUpdater::revisionIsRedirect.
823  $content = $revision->getContent( SlotRecord::MAIN );
824  return $content ? $content->getRedirectTarget() : null;
825  }
826 
831  public function adjustDisplayTitle( ParserOutput $pOutput ) {
832  $out = $this->getContext()->getOutput();
833 
834  # Adjust the title if it was set by displaytitle, -{T|}- or language conversion
835  $titleText = $pOutput->getTitleText();
836  if ( strval( $titleText ) !== '' ) {
837  $out->setPageTitle( $titleText );
838  $out->setDisplayTitle( $titleText );
839  }
840  }
841 
846  protected function showDiffPage() {
847  $request = $this->getContext()->getRequest();
848  $user = $this->getContext()->getUser();
849  $diff = $request->getVal( 'diff' );
850  $rcid = $request->getInt( 'rcid' );
851  $diffOnly = $request->getBool( 'diffonly', $user->getOption( 'diffonly' ) );
852  $purge = $request->getRawVal( 'action' ) === 'purge';
853  $unhide = $request->getInt( 'unhide' ) == 1;
854  $oldid = $this->getOldID();
855 
856  $rev = $this->fetchRevisionRecord();
857 
858  if ( !$rev ) {
859  // T213621: $rev maybe null due to either lack of permission to view the
860  // revision or actually not existing. So let's try loading it from the id
861  $rev = $this->revisionStore->getRevisionById( $oldid );
862  if ( $rev ) {
863  // Revision exists but $user lacks permission to diff it.
864  // Do nothing here.
865  // The $rev will later be used to create standard diff elements however.
866  } else {
867  $this->getContext()->getOutput()->setPageTitle( wfMessage( 'errorpagetitle' ) );
868  $msg = $this->getContext()->msg( 'difference-missing-revision' )
869  ->params( $oldid )
870  ->numParams( 1 )
871  ->parseAsBlock();
872  $this->getContext()->getOutput()->addHTML( $msg );
873  return;
874  }
875  }
876 
877  $contentHandler = MediaWikiServices::getInstance()
878  ->getContentHandlerFactory()
879  ->getContentHandler(
880  $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
881  );
882  $de = $contentHandler->createDifferenceEngine(
883  $this->getContext(),
884  $oldid,
885  $diff,
886  $rcid,
887  $purge,
888  $unhide
889  );
890  $de->setSlotDiffOptions( [
891  'diff-type' => $request->getVal( 'diff-type' ),
892  'expand-url' => $this->viewIsRenderAction
893  ] );
894  $de->showDiffPage( $diffOnly );
895 
896  // Run view updates for the newer revision being diffed (and shown
897  // below the diff if not $diffOnly).
898  list( $old, $new ) = $de->mapDiffPrevNext( $oldid, $diff );
899  // New can be false, convert it to 0 - this conveniently means the latest revision
900  $this->mPage->doViewUpdates( $user, (int)$new );
901  }
902 
910  public function getRobotPolicy( $action, ParserOutput $pOutput = null ) {
911  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
912  $articleRobotPolicies = $mainConfig->get( MainConfigNames::ArticleRobotPolicies );
913  $namespaceRobotPolicies = $mainConfig->get( MainConfigNames::NamespaceRobotPolicies );
914  $defaultRobotPolicy = $mainConfig->get( MainConfigNames::DefaultRobotPolicy );
915  $ns = $this->getTitle()->getNamespace();
916 
917  # Don't index user and user talk pages for blocked users (T13443)
918  if ( ( $ns === NS_USER || $ns === NS_USER_TALK ) && !$this->getTitle()->isSubpage() ) {
919  $specificTarget = null;
920  $vagueTarget = null;
921  $titleText = $this->getTitle()->getText();
922  if ( IPUtils::isValid( $titleText ) ) {
923  $vagueTarget = $titleText;
924  } else {
925  $specificTarget = $titleText;
926  }
927  if ( DatabaseBlock::newFromTarget( $specificTarget, $vagueTarget ) instanceof DatabaseBlock ) {
928  return [
929  'index' => 'noindex',
930  'follow' => 'nofollow'
931  ];
932  }
933  }
934 
935  if ( $this->mPage->getId() === 0 || $this->getOldID() ) {
936  # Non-articles (special pages etc), and old revisions
937  return [
938  'index' => 'noindex',
939  'follow' => 'nofollow'
940  ];
941  } elseif ( $this->getContext()->getOutput()->isPrintable() ) {
942  # Discourage indexing of printable versions, but encourage following
943  return [
944  'index' => 'noindex',
945  'follow' => 'follow'
946  ];
947  } elseif ( $this->getContext()->getRequest()->getInt( 'curid' ) ) {
948  # For ?curid=x urls, disallow indexing
949  return [
950  'index' => 'noindex',
951  'follow' => 'follow'
952  ];
953  }
954 
955  # Otherwise, construct the policy based on the various config variables.
956  $policy = self::formatRobotPolicy( $defaultRobotPolicy );
957 
958  if ( isset( $namespaceRobotPolicies[$ns] ) ) {
959  # Honour customised robot policies for this namespace
960  $policy = array_merge(
961  $policy,
962  self::formatRobotPolicy( $namespaceRobotPolicies[$ns] )
963  );
964  }
965  if ( $this->getTitle()->canUseNoindex() && is_object( $pOutput ) && $pOutput->getIndexPolicy() ) {
966  # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates
967  # a final check that we have really got the parser output.
968  $policy = array_merge(
969  $policy,
970  [ 'index' => $pOutput->getIndexPolicy() ]
971  );
972  }
973 
974  if ( isset( $articleRobotPolicies[$this->getTitle()->getPrefixedText()] ) ) {
975  # (T16900) site config can override user-defined __INDEX__ or __NOINDEX__
976  $policy = array_merge(
977  $policy,
978  self::formatRobotPolicy( $articleRobotPolicies[$this->getTitle()->getPrefixedText()] )
979  );
980  }
981 
982  return $policy;
983  }
984 
992  public static function formatRobotPolicy( $policy ) {
993  if ( is_array( $policy ) ) {
994  return $policy;
995  } elseif ( !$policy ) {
996  return [];
997  }
998 
999  $arr = [];
1000  foreach ( explode( ',', $policy ) as $var ) {
1001  $var = trim( $var );
1002  if ( $var === 'index' || $var === 'noindex' ) {
1003  $arr['index'] = $var;
1004  } elseif ( $var === 'follow' || $var === 'nofollow' ) {
1005  $arr['follow'] = $var;
1006  }
1007  }
1008 
1009  return $arr;
1010  }
1011 
1019  public function showRedirectedFromHeader() {
1020  $redirectSources = MediaWikiServices::getInstance()
1021  ->getMainConfig()->get( MainConfigNames::RedirectSources );
1022 
1023  $context = $this->getContext();
1024  $outputPage = $context->getOutput();
1025  $request = $context->getRequest();
1026  $rdfrom = $request->getVal( 'rdfrom' );
1027 
1028  // Construct a URL for the current page view, but with the target title
1029  $query = $request->getValues();
1030  unset( $query['rdfrom'] );
1031  unset( $query['title'] );
1032  if ( $this->getTitle()->isRedirect() ) {
1033  // Prevent double redirects
1034  $query['redirect'] = 'no';
1035  }
1036  $redirectTargetUrl = $this->getTitle()->getLinkURL( $query );
1037 
1038  if ( isset( $this->mRedirectedFrom ) ) {
1039  // This is an internally redirected page view.
1040  // We'll need a backlink to the source page for navigation.
1041  if ( $this->getHookRunner()->onArticleViewRedirect( $this ) ) {
1042  $redir = $this->linkRenderer->makeKnownLink(
1043  $this->mRedirectedFrom,
1044  null,
1045  [],
1046  [ 'redirect' => 'no' ]
1047  );
1048 
1049  $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1050  $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1051  . "</span>" );
1052 
1053  // Add the script to update the displayed URL and
1054  // set the fragment if one was specified in the redirect
1055  $outputPage->addJsConfigVars( [
1056  'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1057  ] );
1058  $outputPage->addModules( 'mediawiki.action.view.redirect' );
1059 
1060  // Add a <link rel="canonical"> tag
1061  $outputPage->setCanonicalUrl( $this->getTitle()->getCanonicalURL() );
1062 
1063  // Tell the output object that the user arrived at this article through a redirect
1064  $outputPage->setRedirectedFrom( $this->mRedirectedFrom );
1065 
1066  return true;
1067  }
1068  } elseif ( $rdfrom ) {
1069  // This is an externally redirected view, from some other wiki.
1070  // If it was reported from a trusted site, supply a backlink.
1071  if ( $redirectSources && preg_match( $redirectSources, $rdfrom ) ) {
1072  $redir = Linker::makeExternalLink( $rdfrom, $rdfrom );
1073  $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1074  $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1075  . "</span>" );
1076 
1077  // Add the script to update the displayed URL
1078  $outputPage->addJsConfigVars( [
1079  'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1080  ] );
1081  $outputPage->addModules( 'mediawiki.action.view.redirect' );
1082 
1083  return true;
1084  }
1085  }
1086 
1087  return false;
1088  }
1089 
1094  public function showNamespaceHeader() {
1095  if ( $this->getTitle()->isTalkPage() && !wfMessage( 'talkpageheader' )->isDisabled() ) {
1096  $this->getContext()->getOutput()->wrapWikiMsg(
1097  "<div class=\"mw-talkpageheader\">\n$1\n</div>",
1098  [ 'talkpageheader' ]
1099  );
1100  }
1101  }
1102 
1106  public function showViewFooter() {
1107  # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
1108  if ( $this->getTitle()->getNamespace() === NS_USER_TALK
1109  && IPUtils::isValid( $this->getTitle()->getText() )
1110  ) {
1111  $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' );
1112  }
1113 
1114  // Show a footer allowing the user to patrol the shown revision or page if possible
1115  $patrolFooterShown = $this->showPatrolFooter();
1116 
1117  $this->getHookRunner()->onArticleViewFooter( $this, $patrolFooterShown );
1118  }
1119 
1130  public function showPatrolFooter() {
1131  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1132  $useNPPatrol = $mainConfig->get( MainConfigNames::UseNPPatrol );
1133  $useRCPatrol = $mainConfig->get( MainConfigNames::UseRCPatrol );
1134  $useFilePatrol = $mainConfig->get( MainConfigNames::UseFilePatrol );
1135  // Allow hooks to decide whether to not output this at all
1136  if ( !$this->getHookRunner()->onArticleShowPatrolFooter( $this ) ) {
1137  return false;
1138  }
1139 
1140  $outputPage = $this->getContext()->getOutput();
1141  $user = $this->getContext()->getUser();
1142  $title = $this->getTitle();
1143  $rc = false;
1144 
1145  if ( !$this->getContext()->getAuthority()->probablyCan( 'patrol', $title )
1146  || !( $useRCPatrol || $useNPPatrol
1147  || ( $useFilePatrol && $title->inNamespace( NS_FILE ) ) )
1148  ) {
1149  // Patrolling is disabled or the user isn't allowed to
1150  return false;
1151  }
1152 
1153  if ( $this->mRevisionRecord
1154  && !RecentChange::isInRCLifespan( $this->mRevisionRecord->getTimestamp(), 21600 )
1155  ) {
1156  // The current revision is already older than what could be in the RC table
1157  // 6h tolerance because the RC might not be cleaned out regularly
1158  return false;
1159  }
1160 
1161  // Check for cached results
1162  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1163  $key = $cache->makeKey( 'unpatrollable-page', $title->getArticleID() );
1164  if ( $cache->get( $key ) ) {
1165  return false;
1166  }
1167 
1168  $dbr = wfGetDB( DB_REPLICA );
1169  $oldestRevisionTimestamp = $dbr->selectField(
1170  'revision',
1171  'MIN( rev_timestamp )',
1172  [ 'rev_page' => $title->getArticleID() ],
1173  __METHOD__
1174  );
1175 
1176  // New page patrol: Get the timestamp of the oldest revision which
1177  // the revision table holds for the given page. Then we look
1178  // whether it's within the RC lifespan and if it is, we try
1179  // to get the recentchanges row belonging to that entry
1180  // (with rc_new = 1).
1181  $recentPageCreation = false;
1182  if ( $oldestRevisionTimestamp
1183  && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 )
1184  ) {
1185  // 6h tolerance because the RC might not be cleaned out regularly
1186  $recentPageCreation = true;
1188  [
1189  'rc_new' => 1,
1190  'rc_timestamp' => $oldestRevisionTimestamp,
1191  'rc_namespace' => $title->getNamespace(),
1192  'rc_cur_id' => $title->getArticleID()
1193  ],
1194  __METHOD__
1195  );
1196  if ( $rc ) {
1197  // Use generic patrol message for new pages
1198  $markPatrolledMsg = wfMessage( 'markaspatrolledtext' );
1199  }
1200  }
1201 
1202  // File patrol: Get the timestamp of the latest upload for this page,
1203  // check whether it is within the RC lifespan and if it is, we try
1204  // to get the recentchanges row belonging to that entry
1205  // (with rc_type = RC_LOG, rc_log_type = upload).
1206  $recentFileUpload = false;
1207  if ( ( !$rc || $rc->getAttribute( 'rc_patrolled' ) ) && $useFilePatrol
1208  && $title->getNamespace() === NS_FILE ) {
1209  // Retrieve timestamp of most recent upload
1210  $newestUploadTimestamp = $dbr->selectField(
1211  'image',
1212  'MAX( img_timestamp )',
1213  [ 'img_name' => $title->getDBkey() ],
1214  __METHOD__
1215  );
1216  if ( $newestUploadTimestamp
1217  && RecentChange::isInRCLifespan( $newestUploadTimestamp, 21600 )
1218  ) {
1219  // 6h tolerance because the RC might not be cleaned out regularly
1220  $recentFileUpload = true;
1222  [
1223  'rc_type' => RC_LOG,
1224  'rc_log_type' => 'upload',
1225  'rc_timestamp' => $newestUploadTimestamp,
1226  'rc_namespace' => NS_FILE,
1227  'rc_cur_id' => $title->getArticleID()
1228  ],
1229  __METHOD__
1230  );
1231  if ( $rc ) {
1232  // Use patrol message specific to files
1233  $markPatrolledMsg = wfMessage( 'markaspatrolledtext-file' );
1234  }
1235  }
1236  }
1237 
1238  if ( !$recentPageCreation && !$recentFileUpload ) {
1239  // Page creation and latest upload (for files) is too old to be in RC
1240 
1241  // We definitely can't patrol so cache the information
1242  // When a new file version is uploaded, the cache is cleared
1243  $cache->set( $key, '1' );
1244 
1245  return false;
1246  }
1247 
1248  if ( !$rc ) {
1249  // Don't cache: This can be hit if the page gets accessed very fast after
1250  // its creation / latest upload or in case we have high replica DB lag. In case
1251  // the revision is too old, we will already return above.
1252  return false;
1253  }
1254 
1255  if ( $rc->getAttribute( 'rc_patrolled' ) ) {
1256  // Patrolled RC entry around
1257 
1258  // Cache the information we gathered above in case we can't patrol
1259  // Don't cache in case we can patrol as this could change
1260  $cache->set( $key, '1' );
1261 
1262  return false;
1263  }
1264 
1265  if ( $rc->getPerformerIdentity()->equals( $user ) ) {
1266  // Don't show a patrol link for own creations/uploads. If the user could
1267  // patrol them, they already would be patrolled
1268  return false;
1269  }
1270 
1271  $outputPage->setPreventClickjacking( true );
1272  if ( $this->getContext()->getAuthority()->isAllowed( 'writeapi' ) ) {
1273  $outputPage->addModules( 'mediawiki.misc-authed-curate' );
1274  }
1275 
1276  $link = $this->linkRenderer->makeKnownLink(
1277  $title,
1278  // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $markPatrolledMsg is always set when $rc is set
1279  $markPatrolledMsg->text(),
1280  [],
1281  [
1282  'action' => 'markpatrolled',
1283  'rcid' => $rc->getAttribute( 'rc_id' ),
1284  ]
1285  );
1286 
1287  $outputPage->addModuleStyles( 'mediawiki.action.styles' );
1288  $outputPage->addHTML(
1289  "<div class='patrollink' data-mw='interface'>" .
1290  wfMessage( 'markaspatrolledlink' )->rawParams( $link )->escaped() .
1291  '</div>'
1292  );
1293 
1294  return true;
1295  }
1296 
1303  public static function purgePatrolFooterCache( $articleID ) {
1304  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1305  $cache->delete( $cache->makeKey( 'unpatrollable-page', $articleID ) );
1306  }
1307 
1312  public function showMissingArticle() {
1313  $send404Code = MediaWikiServices::getInstance()
1314  ->getMainConfig()->get( MainConfigNames::Send404Code );
1315 
1316  $outputPage = $this->getContext()->getOutput();
1317  // Whether the page is a root user page of an existing user (but not a subpage)
1318  $validUserPage = false;
1319 
1320  $title = $this->getTitle();
1321 
1322  $services = MediaWikiServices::getInstance();
1323 
1324  $contextUser = $this->getContext()->getUser();
1325 
1326  # Show info in user (talk) namespace. Does the user exist? Is he blocked?
1327  if ( $title->getNamespace() === NS_USER
1328  || $title->getNamespace() === NS_USER_TALK
1329  ) {
1330  $rootPart = explode( '/', $title->getText() )[0];
1331  $user = User::newFromName( $rootPart, false /* allow IP users */ );
1332  $ip = $this->userNameUtils->isIP( $rootPart );
1333  $block = DatabaseBlock::newFromTarget( $user, $user );
1334 
1335  if ( $user && $user->isRegistered() && $user->isHidden() &&
1336  !$this->getContext()->getAuthority()->isAllowed( 'hideuser' )
1337  ) {
1338  // T120883 if the user is hidden and the viewer cannot see hidden
1339  // users, pretend like it does not exist at all.
1340  $user = false;
1341  }
1342 
1343  if ( !( $user && $user->isRegistered() ) && !$ip ) { # User does not exist
1344  $outputPage->addHtml( Html::warningBox(
1345  $outputPage->msg( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) )->parse(),
1346  'mw-userpage-userdoesnotexist'
1347  ) );
1348  } elseif (
1349  $block !== null &&
1350  $block->getType() != DatabaseBlock::TYPE_AUTO &&
1351  (
1352  $block->isSitewide() ||
1353  $user->isBlockedFrom( $title, true )
1354  )
1355  ) {
1356  // Show log extract if the user is sitewide blocked or is partially
1357  // blocked and not allowed to edit their user page or user talk page
1359  $outputPage,
1360  'block',
1361  $services->getNamespaceInfo()->getCanonicalName( NS_USER ) . ':' .
1362  $block->getTargetName(),
1363  '',
1364  [
1365  'lim' => 1,
1366  'showIfEmpty' => false,
1367  'msgKey' => [
1368  'blocked-notice-logextract',
1369  $user->getName() # Support GENDER in notice
1370  ]
1371  ]
1372  );
1373  $validUserPage = !$title->isSubpage();
1374  } else {
1375  $validUserPage = !$title->isSubpage();
1376  }
1377  }
1378 
1379  $this->getHookRunner()->onShowMissingArticle( $this );
1380 
1381  # Show delete and move logs if there were any such events.
1382  # The logging query can DOS the site when bots/crawlers cause 404 floods,
1383  # so be careful showing this. 404 pages must be cheap as they are hard to cache.
1384  $dbCache = MediaWikiServices::getInstance()->getMainObjectStash();
1385  $key = $dbCache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
1386  $isRegistered = $contextUser->isRegistered();
1387  $sessionExists = $this->getContext()->getRequest()->getSession()->isPersistent();
1388 
1389  if ( $isRegistered || $dbCache->get( $key ) || $sessionExists ) {
1390  $logTypes = [ 'delete', 'move', 'protect' ];
1391 
1392  $dbr = wfGetDB( DB_REPLICA );
1393 
1394  $conds = [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ];
1395  // Give extensions a chance to hide their (unrelated) log entries
1396  $this->getHookRunner()->onArticle__MissingArticleConditions( $conds, $logTypes );
1398  $outputPage,
1399  $logTypes,
1400  $title,
1401  '',
1402  [
1403  'lim' => 10,
1404  'conds' => $conds,
1405  'showIfEmpty' => false,
1406  'msgKey' => [ $isRegistered || $sessionExists
1407  ? 'moveddeleted-notice'
1408  : 'moveddeleted-notice-recent'
1409  ]
1410  ]
1411  );
1412  }
1413 
1414  if ( !$this->mPage->hasViewableContent() && $send404Code && !$validUserPage ) {
1415  // If there's no backing content, send a 404 Not Found
1416  // for better machine handling of broken links.
1417  $this->getContext()->getRequest()->response()->statusHeader( 404 );
1418  }
1419 
1420  // Also apply the robot policy for nonexisting pages (even if a 404 was used)
1421  $policy = $this->getRobotPolicy( 'view' );
1422  $outputPage->setIndexPolicy( $policy['index'] );
1423  $outputPage->setFollowPolicy( $policy['follow'] );
1424 
1425  $hookResult = $this->getHookRunner()->onBeforeDisplayNoArticleText( $this );
1426 
1427  if ( !$hookResult ) {
1428  return;
1429  }
1430 
1431  # Show error message
1432  $oldid = $this->getOldID();
1433  if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) {
1434  $text = $this->getTitle()->getDefaultMessageText() ?? '';
1435  $outputPage->addWikiTextAsContent( $text );
1436  } else {
1437  if ( $oldid ) {
1438  // T251066: Try loading the revision from the archive table.
1439  // Show link to view it if it exists and the user has permission to view it.
1440  $pa = new PageArchive( $title );
1441  $revRecord = $pa->getArchivedRevisionRecord( $oldid );
1442  if ( $revRecord && $revRecord->userCan(
1443  RevisionRecord::DELETED_TEXT,
1444  $this->getContext()->getAuthority()
1445  ) ) {
1446  $text = wfMessage(
1447  'missing-revision-permission', $oldid,
1448  $revRecord->getTimestamp(),
1449  $title->getPrefixedDBkey()
1450  )->plain();
1451  } else {
1452  $text = wfMessage( 'missing-revision', $oldid )->plain();
1453  }
1454 
1455  } elseif ( $this->getContext()->getAuthority()->probablyCan( 'edit', $title )
1456  ) {
1457  $message = $isRegistered ? 'noarticletext' : 'noarticletextanon';
1458  $text = wfMessage( $message )->plain();
1459  } else {
1460  $text = wfMessage( 'noarticletext-nopermission' )->plain();
1461  }
1462 
1463  $dir = $this->getContext()->getLanguage()->getDir();
1464  $lang = $this->getContext()->getLanguage()->getHtmlCode();
1465  $outputPage->addWikiTextAsInterface( Xml::openElement( 'div', [
1466  'class' => "noarticletext mw-content-$dir",
1467  'dir' => $dir,
1468  'lang' => $lang,
1469  ] ) . "\n$text\n</div>" );
1470  }
1471  }
1472 
1477  private function showViewError( string $errortext ) {
1478  $outputPage = $this->getContext()->getOutput();
1479  $outputPage->setPageTitle( $this->getContext()->msg( 'errorpagetitle' ) );
1480  $outputPage->disableClientCache();
1481  $outputPage->setRobotPolicy( 'noindex,nofollow' );
1482  $outputPage->clearHTML();
1483  $outputPage->addHTML( Html::errorBox( $outputPage->parseAsContent( $errortext ) ) );
1484  }
1485 
1492  public function showDeletedRevisionHeader() {
1493  if ( !$this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1494  // Not deleted
1495  return true;
1496  }
1497  $outputPage = $this->getContext()->getOutput();
1498  $user = $this->getContext()->getUser();
1499  // Used in wikilinks, should not contain whitespaces
1500  $titleText = $this->getTitle()->getPrefixedDBkey();
1501  // If the user is not allowed to see it...
1502  if ( !$this->mRevisionRecord->userCan(
1503  RevisionRecord::DELETED_TEXT,
1504  $this->getContext()->getAuthority()
1505  ) ) {
1506  $outputPage->addHtml(
1508  $outputPage->msg( 'rev-deleted-text-permission', $titleText )->parse(),
1509  'plainlinks'
1510  )
1511  );
1512 
1513  return false;
1514  // If the user needs to confirm that they want to see it...
1515  } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) != 1 ) {
1516  # Give explanation and add a link to view the revision...
1517  $oldid = intval( $this->getOldID() );
1518  $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" );
1519  $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ?
1520  'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
1521  $outputPage->addHtml(
1523  $outputPage->msg( $msg, $link )->parse(),
1524  'plainlinks'
1525  )
1526  );
1527 
1528  return false;
1529  // We are allowed to see...
1530  } else {
1531  $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
1532  ? [ 'rev-suppressed-text-view', $titleText ]
1533  : [ 'rev-deleted-text-view', $titleText ];
1534  $outputPage->addHtml(
1536  $outputPage->msg( $msg[0], $msg[1] )->parse(),
1537  'plainlinks'
1538  )
1539  );
1540 
1541  return true;
1542  }
1543  }
1544 
1553  public function setOldSubtitle( $oldid = 0 ) {
1554  if ( !$this->getHookRunner()->onDisplayOldSubtitle( $this, $oldid ) ) {
1555  return;
1556  }
1557 
1558  $context = $this->getContext();
1559  $unhide = $context->getRequest()->getInt( 'unhide' ) == 1;
1560 
1561  # Cascade unhide param in links for easy deletion browsing
1562  $extraParams = [];
1563  if ( $unhide ) {
1564  $extraParams['unhide'] = 1;
1565  }
1566 
1567  if ( $this->mRevisionRecord && $this->mRevisionRecord->getId() === $oldid ) {
1568  $revisionRecord = $this->mRevisionRecord;
1569  } else {
1570  $revisionRecord = $this->revisionStore->getRevisionById( $oldid );
1571  }
1572 
1573  $timestamp = $revisionRecord->getTimestamp();
1574 
1575  $current = ( $oldid == $this->mPage->getLatest() );
1576  $language = $context->getLanguage();
1577  $user = $context->getUser();
1578 
1579  $td = $language->userTimeAndDate( $timestamp, $user );
1580  $tddate = $language->userDate( $timestamp, $user );
1581  $tdtime = $language->userTime( $timestamp, $user );
1582 
1583  # Show user links if allowed to see them. If hidden, then show them only if requested...
1584  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable revisionRecord known to exists
1585  $userlinks = Linker::revUserTools( $revisionRecord, !$unhide );
1586 
1587  $infomsg = $current && !$context->msg( 'revision-info-current' )->isDisabled()
1588  ? 'revision-info-current'
1589  : 'revision-info';
1590 
1591  $outputPage = $context->getOutput();
1592  $outputPage->addModuleStyles( [
1593  'mediawiki.action.styles',
1594  'mediawiki.interface.helpers.styles'
1595  ] );
1596 
1597  $revisionUser = $revisionRecord->getUser();
1598  $revisionInfo = "<div id=\"mw-{$infomsg}\">" .
1599  $context->msg( $infomsg, $td )
1600  ->rawParams( $userlinks )
1601  ->params(
1602  $revisionRecord->getId(),
1603  $tddate,
1604  $tdtime,
1605  $revisionUser ? $revisionUser->getName() : ''
1606  )
1607  ->rawParams( Linker::revComment(
1608  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable revisionRecord known to exists
1609  $revisionRecord,
1610  true,
1611  true
1612  ) )
1613  ->parse() .
1614  "</div>";
1615 
1616  $lnk = $current
1617  ? $context->msg( 'currentrevisionlink' )->escaped()
1618  : $this->linkRenderer->makeKnownLink(
1619  $this->getTitle(),
1620  $context->msg( 'currentrevisionlink' )->text(),
1621  [],
1622  $extraParams
1623  );
1624  $curdiff = $current
1625  ? $context->msg( 'diff' )->escaped()
1626  : $this->linkRenderer->makeKnownLink(
1627  $this->getTitle(),
1628  $context->msg( 'diff' )->text(),
1629  [],
1630  [
1631  'diff' => 'cur',
1632  'oldid' => $oldid
1633  ] + $extraParams
1634  );
1635  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable revisionRecord known to exists
1636  $prevExist = (bool)$this->revisionStore->getPreviousRevision( $revisionRecord );
1637  $prevlink = $prevExist
1638  ? $this->linkRenderer->makeKnownLink(
1639  $this->getTitle(),
1640  $context->msg( 'previousrevision' )->text(),
1641  [],
1642  [
1643  'direction' => 'prev',
1644  'oldid' => $oldid
1645  ] + $extraParams
1646  )
1647  : $context->msg( 'previousrevision' )->escaped();
1648  $prevdiff = $prevExist
1649  ? $this->linkRenderer->makeKnownLink(
1650  $this->getTitle(),
1651  $context->msg( 'diff' )->text(),
1652  [],
1653  [
1654  'diff' => 'prev',
1655  'oldid' => $oldid
1656  ] + $extraParams
1657  )
1658  : $context->msg( 'diff' )->escaped();
1659  $nextlink = $current
1660  ? $context->msg( 'nextrevision' )->escaped()
1661  : $this->linkRenderer->makeKnownLink(
1662  $this->getTitle(),
1663  $context->msg( 'nextrevision' )->text(),
1664  [],
1665  [
1666  'direction' => 'next',
1667  'oldid' => $oldid
1668  ] + $extraParams
1669  );
1670  $nextdiff = $current
1671  ? $context->msg( 'diff' )->escaped()
1672  : $this->linkRenderer->makeKnownLink(
1673  $this->getTitle(),
1674  $context->msg( 'diff' )->text(),
1675  [],
1676  [
1677  'diff' => 'next',
1678  'oldid' => $oldid
1679  ] + $extraParams
1680  );
1681 
1682  $cdel = Linker::getRevDeleteLink(
1683  $user,
1684  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable revisionRecord known to exists
1685  $revisionRecord,
1686  $this->getTitle()
1687  );
1688  if ( $cdel !== '' ) {
1689  $cdel .= ' ';
1690  }
1691 
1692  // the outer div is need for styling the revision info and nav in MobileFrontend
1693  $outputPage->addSubtitle(
1695  $revisionInfo .
1696  "<div id=\"mw-revision-nav\">" . $cdel .
1697  $context->msg( 'revision-nav' )->rawParams(
1698  $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff
1699  )->escaped() . "</div>",
1700  'mw-revision'
1701  )
1702  );
1703  }
1704 
1718  public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) {
1719  $lang = $this->getTitle()->getPageLanguage();
1720  $out = $this->getContext()->getOutput();
1721  if ( $appendSubtitle ) {
1722  $out->addSubtitle( wfMessage( 'redirectpagesub' ) );
1723  }
1724  $out->addModuleStyles( 'mediawiki.action.view.redirectPage' );
1725  return static::getRedirectHeaderHtml( $lang, $target, $forceKnown );
1726  }
1727 
1740  public static function getRedirectHeaderHtml( Language $lang, $target, $forceKnown = false ) {
1741  if ( is_array( $target ) ) {
1742  // Up until 1.39, $target was allowed to be an array.
1743  wfDeprecatedMsg( 'The $target parameter can no longer be an array', '1.39' );
1744  $target = reset( $target ); // There really can only be one element (T296430)
1745  }
1746 
1747  $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1748 
1749  $html = '<ul class="redirectText">';
1750  if ( $forceKnown ) {
1751  $link = $linkRenderer->makeKnownLink(
1752  $target,
1753  $target->getFullText(),
1754  [],
1755  // Make sure wiki page redirects are not followed
1756  $target->isRedirect() ? [ 'redirect' => 'no' ] : []
1757  );
1758  } else {
1759  $link = $linkRenderer->makeLink(
1760  $target,
1761  $target->getFullText(),
1762  [],
1763  // Make sure wiki page redirects are not followed
1764  $target->isRedirect() ? [ 'redirect' => 'no' ] : []
1765  );
1766  }
1767  $html .= '<li>' . $link . '</li>';
1768  $html .= '</ul>';
1769 
1770  $redirectToText = wfMessage( 'redirectto' )->inLanguage( $lang )->escaped();
1771 
1772  return '<div class="redirectMsg">' .
1773  '<p>' . $redirectToText . '</p>' .
1774  $html .
1775  '</div>';
1776  }
1777 
1786  public function addHelpLink( $to, $overrideBaseUrl = false ) {
1787  $out = $this->getContext()->getOutput();
1788  $msg = $out->msg( 'namespace-' . $this->getTitle()->getNamespace() . '-helppage' );
1789 
1790  if ( !$msg->isDisabled() ) {
1791  $title = Title::newFromText( $msg->plain() );
1792  if ( $title instanceof Title ) {
1793  $out->addHelpLink( $title->getLocalURL(), true );
1794  }
1795  } else {
1796  $out->addHelpLink( $to, $overrideBaseUrl );
1797  }
1798  }
1799 
1803  public function render() {
1804  $this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' );
1805  $this->getContext()->getOutput()->setArticleBodyOnly( true );
1806  // We later set 'enableSectionEditLinks=false' based on this; also used by ImagePage
1807  $this->viewIsRenderAction = true;
1808  $this->view();
1809  }
1810 
1814  public function protect() {
1815  $form = new ProtectionForm( $this );
1816  $form->execute();
1817  }
1818 
1822  public function unprotect() {
1823  $this->protect();
1824  }
1825 
1839  public function doDelete( $reason, $suppress = false, $immediate = false ) {
1840  wfDeprecated( __METHOD__, '1.37' );
1841  $error = '';
1842  $context = $this->getContext();
1843  $outputPage = $context->getOutput();
1844  $user = $context->getUser();
1845  $status = $this->mPage->doDeleteArticleReal(
1846  $reason, $user, $suppress, null, $error,
1847  null, [], 'delete', $immediate
1848  );
1849 
1850  if ( $status->isOK() ) {
1851  $deleted = $this->getTitle()->getPrefixedText();
1852 
1853  $outputPage->setPageTitle( wfMessage( 'actioncomplete' ) );
1854  $outputPage->setRobotPolicy( 'noindex,nofollow' );
1855 
1856  if ( $status->isGood() ) {
1857  $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]';
1858  $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
1859  $this->getHookRunner()->onArticleDeleteAfterSuccess( $this->getTitle(), $outputPage );
1860  } else {
1861  $outputPage->addWikiMsg( 'delete-scheduled', wfEscapeWikiText( $deleted ) );
1862  }
1863 
1864  $outputPage->returnToMain( false );
1865  } else {
1866  $outputPage->setPageTitle(
1867  wfMessage( 'cannotdelete-title',
1868  $this->getTitle()->getPrefixedText() )
1869  );
1870 
1871  if ( $error == '' ) {
1872  $outputPage->wrapWikiTextAsInterface(
1873  'error mw-error-cannotdelete',
1874  $status->getWikiText( false, false, $context->getLanguage() )
1875  );
1876  $deleteLogPage = new LogPage( 'delete' );
1877  $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) );
1878 
1880  $outputPage,
1881  'delete',
1882  $this->getTitle()
1883  );
1884  } else {
1885  $outputPage->addHTML( $error );
1886  }
1887  }
1888  }
1889 
1890  /* Caching functions */
1891 
1899  protected function tryFileCache() {
1900  static $called = false;
1901 
1902  if ( $called ) {
1903  wfDebug( "Article::tryFileCache(): called twice!?" );
1904  return false;
1905  }
1906 
1907  $called = true;
1908  if ( $this->isFileCacheable() ) {
1909  $cache = new HTMLFileCache( $this->getTitle(), 'view' );
1910  if ( $cache->isCacheGood( $this->mPage->getTouched() ) ) {
1911  wfDebug( "Article::tryFileCache(): about to load file" );
1912  $cache->loadFromFileCache( $this->getContext() );
1913  return true;
1914  } else {
1915  wfDebug( "Article::tryFileCache(): starting buffer" );
1916  ob_start( [ &$cache, 'saveToFileCache' ] );
1917  }
1918  } else {
1919  wfDebug( "Article::tryFileCache(): not cacheable" );
1920  }
1921 
1922  return false;
1923  }
1924 
1930  public function isFileCacheable( $mode = HTMLFileCache::MODE_NORMAL ) {
1931  $cacheable = false;
1932 
1933  if ( HTMLFileCache::useFileCache( $this->getContext(), $mode ) ) {
1934  $cacheable = $this->mPage->getId()
1935  && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect();
1936  // Extension may have reason to disable file caching on some pages.
1937  if ( $cacheable ) {
1938  $cacheable = $this->getHookRunner()->onIsFileCacheable( $this ) ?? false;
1939  }
1940  }
1941 
1942  return $cacheable;
1943  }
1944 
1958  public function getParserOutput( $oldid = null, UserIdentity $user = null ) {
1959  if ( $user === null ) {
1960  $parserOptions = $this->getParserOptions();
1961  } else {
1962  $parserOptions = $this->mPage->makeParserOptions( $user );
1963  }
1964 
1965  return $this->mPage->getParserOutput( $parserOptions, $oldid );
1966  }
1967 
1972  public function getParserOptions() {
1973  return $this->mPage->makeParserOptions( $this->getContext() );
1974  }
1975 
1982  public function setContext( $context ) {
1983  $this->mContext = $context;
1984  }
1985 
1992  public function getContext() {
1993  if ( $this->mContext instanceof IContextSource ) {
1994  return $this->mContext;
1995  } else {
1996  wfDebug( __METHOD__ . " called and \$mContext is null. " .
1997  "Return RequestContext::getMain()" );
1998  return RequestContext::getMain();
1999  }
2000  }
2001 
2011  public function __get( $fname ) {
2012  wfDeprecatedMsg( "Accessing Article::\$$fname is deprecated since MediaWiki 1.35",
2013  '1.35' );
2014 
2015  if ( property_exists( $this->mPage, $fname ) ) {
2016  return $this->mPage->$fname;
2017  }
2018  trigger_error( 'Inaccessible property via __get(): ' . $fname, E_USER_NOTICE );
2019  }
2020 
2030  public function __set( $fname, $fvalue ) {
2031  wfDeprecatedMsg( "Setting Article::\$$fname is deprecated since MediaWiki 1.35",
2032  '1.35' );
2033 
2034  if ( property_exists( $this->mPage, $fname ) ) {
2035  $this->mPage->$fname = $fvalue;
2036  // Note: extensions may want to toss on new fields
2037  } elseif ( !in_array( $fname, [ 'mContext', 'mPage' ] ) ) {
2038  $this->mPage->$fname = $fvalue;
2039  } else {
2040  trigger_error( 'Inaccessible property via __set(): ' . $fname, E_USER_NOTICE );
2041  }
2042  }
2043 
2049  public function getActionOverrides() {
2050  return $this->mPage->getActionOverrides();
2051  }
2052 
2058  public function getTimestamp() {
2059  wfDeprecated( __METHOD__, '1.35' );
2060  return $this->mPage->getTimestamp();
2061  }
2062 }
getAuthority()
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:117
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:47
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition: Article.php:194
UserNameUtils $userNameUtils
Definition: Article.php:111
getContext()
Gets the context this Article is executed in.
Definition: Article.php:1992
getOldIDFromRequest()
Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect.
Definition: Article.php:268
getRedirectedFrom()
Get the page this view was redirected from.
Definition: Article.php:205
Title null $mRedirectedFrom
Title from which we were redirected here, if any.
Definition: Article.php:68
bool $viewIsRenderAction
Whether render() was called.
Definition: Article.php:91
RevisionRecord null $mRevisionRecord
Revision to be shown.
Definition: Article.php:119
RevisionStore $revisionStore
Definition: Article.php:101
view()
This is the default action of the index.php entry point: just view the page of the given title.
Definition: Article.php:426
__construct(Title $title, $oldId=null)
Definition: Article.php:125
getRobotPolicy( $action, ParserOutput $pOutput=null)
Get the robot policy to be used for the current view.
Definition: Article.php:910
static purgePatrolFooterCache( $articleID)
Purge the cache used to check if it is worth showing the patrol footer For example,...
Definition: Article.php:1303
doDelete( $reason, $suppress=false, $immediate=false)
Perform a deletion and output success or failure messages.
Definition: Article.php:1839
ParserOutput null false $mParserOutput
The ParserOutput generated for viewing the page, initialized by view().
Definition: Article.php:84
getOldID()
Definition: Article.php:255
LinkRenderer $linkRenderer
Definition: Article.php:96
getTitle()
Get the title object of the article.
Definition: Article.php:223
getActionOverrides()
Call to WikiPage function for backwards compatibility.
Definition: Article.php:2049
adjustDisplayTitle(ParserOutput $pOutput)
Adjust title for pages with displaytitle, -{T|}- or language conversion.
Definition: Article.php:831
showDeletedRevisionHeader()
If the revision requested for view is deleted, check permissions.
Definition: Article.php:1492
getParserOptions()
Get parser options suitable for rendering the primary article wikitext.
Definition: Article.php:1972
clear()
Definition: Article.php:237
IContextSource null $mContext
The context this Article is executed in.
Definition: Article.php:56
getParserOutput( $oldid=null, UserIdentity $user=null)
#-
Definition: Article.php:1958
showViewError(string $errortext)
Show error text for errors generated in Article::view().
Definition: Article.php:1477
static getRedirectHeaderHtml(Language $lang, $target, $forceKnown=false)
Return the HTML for the top of a redirect page.
Definition: Article.php:1740
protect()
action=protect handler
Definition: Article.php:1814
generateContentOutput(Authority $performer, ParserOptions $parserOptions, int $oldid, OutputPage $outputPage, array $textOptions)
Determines the desired ParserOutput and passes it to $outputPage.
Definition: Article.php:566
isCurrent()
Returns true if the currently-referenced revision is the current edit to this page (and it exists).
Definition: Article.php:395
showMissingArticle()
Show the error text for a missing article.
Definition: Article.php:1312
__set( $fname, $fvalue)
Definition: Article.php:2030
unprotect()
action=unprotect handler (alias)
Definition: Article.php:1822
newPage(Title $title)
Definition: Article.php:140
getPage()
Get the WikiPage object of this instance.
Definition: Article.php:233
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition: Article.php:1786
string bool $mRedirectUrl
URL to redirect to or false if none.
Definition: Article.php:71
getTimestamp()
Definition: Article.php:2058
static newFromID( $id)
Constructor from a page id.
Definition: Article.php:149
int null $mOldId
The oldid of the article that was requested to be shown, 0 for the current revision.
Definition: Article.php:65
static formatRobotPolicy( $policy)
Converts a String robot policy into an associative array, to allow merging of several policies using ...
Definition: Article.php:992
fetchRevisionRecord()
Fetches the revision to work on.
Definition: Article.php:337
viewRedirect( $target, $appendSubtitle=true, $forceKnown=false)
Return the HTML for the top of a redirect page.
Definition: Article.php:1718
showPatrolFooter()
If patrol is possible, output a patrol UI box.
Definition: Article.php:1130
setOldSubtitle( $oldid=0)
Generate the navigation links when browsing through an article revisions It shows the information as:...
Definition: Article.php:1553
showViewFooter()
Show the footer section of an ordinary page view.
Definition: Article.php:1106
Status null $fetchResult
represents the outcome of fetchRevisionRecord().
Definition: Article.php:77
WikiPage $mPage
The WikiPage object of this instance.
Definition: Article.php:59
setRedirectedFrom(Title $from)
Tell the page view functions that this view was redirected from another page on the wiki.
Definition: Article.php:214
isFileCacheable( $mode=HTMLFileCache::MODE_NORMAL)
Check if the page can be cached.
Definition: Article.php:1930
doOutputMetaData(?ParserOutput $pOutput, OutputPage $outputPage)
Definition: Article.php:728
tryFileCache()
checkLastModified returns true if it has taken care of all output to the client that is necessary for...
Definition: Article.php:1899
getRevIdFetched()
Use this to fetch the rev ID used on page views.
Definition: Article.php:414
showNamespaceHeader()
Show a header specific to the namespace currently being viewed, like [[MediaWiki:Talkpagetext]].
Definition: Article.php:1094
__get( $fname)
Definition: Article.php:2011
WatchlistManager $watchlistManager
Definition: Article.php:106
getRevisionRedirectTarget(RevisionRecord $revision)
Definition: Article.php:819
static newFromTitle( $title, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition: Article.php:161
doOutputFromRenderStatus(?RevisionRecord $rev, Status $renderStatus, OutputPage $outputPage, array $textOptions)
Definition: Article.php:774
showDiffPage()
Show a diff page according to current request variables.
Definition: Article.php:846
render()
Handle action=render.
Definition: Article.php:1803
showRedirectedFromHeader()
If this request is a redirect view, send "redirected from" subtitle to the output.
Definition: Article.php:1019
doOutputFromParserCache(ParserOutput $pOutput, OutputPage $outputPage, array $textOptions)
Definition: Article.php:747
setContext( $context)
Sets the context this Article is executed in.
Definition: Article.php:1982
getCacheRevisionId()
Definition: CacheTime.php:98
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:120
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:31
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:45
static revComment(RevisionRecord $revRecord, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition: Linker.php:1582
static getRevDeleteLink(Authority $performer, RevisionRecord $revRecord, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition: Linker.php:2134
static revUserTools(RevisionRecord $revRecord, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition: Linker.php:1346
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition: Linker.php:1038
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Class to simplify the use of log pages.
Definition: LogPage.php:39
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
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=[])
A class containing constants representing the names of configuration variables.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Service for getting rendered output of a given page.
A StatusValue for permission errors.
Page revision base class.
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.
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:52
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:918
setIndexPolicy( $policy)
Set the index policy for the page, but leave the follow policy un- touched.
Definition: OutputPage.php:997
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:410
setLastModified( $timestamp)
Override the last modified timestamp.
Definition: OutputPage.php:907
setSections(array $sections)
Adds sections to OutputPage from ParserOutput.
adaptCdnTTL( $mtime, $minTTL=0, $maxTTL=0)
Get TTL in [$minTTL,$maxTTL] and pass it to lowerCdnMaxage()
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:633
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:493
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:607
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?
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:70
isOK()
Returns whether the operation completed.
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition: Status.php:189
Represents a title within MediaWiki.
Definition: Title.php:48
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:517
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:369
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:637
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
static newFromName( $name, $validate='valid')
Definition: User.php:599
Base representation for an editable wiki page.
Definition: WikiPage.php:62
getTitle()
Get the title object of the article.
Definition: WikiPage.php:307
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
$cache
Definition: mcc.php:33
const DB_REPLICA
Definition: defines.php:25
$content
Definition: router.php:76
return true
Definition: router.php:90
if(!isset( $args[0])) $lang