Merge "docs: Fix typo"
[mediawiki.git] / includes / page / Article.php
blob234d3620971f8b6b87d56eeb5024edf97f80e1b1
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
18 * @file
21 use MediaWiki\Block\DatabaseBlock;
22 use MediaWiki\Block\DatabaseBlockStore;
23 use MediaWiki\CommentFormatter\CommentFormatter;
24 use MediaWiki\Context\IContextSource;
25 use MediaWiki\Context\RequestContext;
26 use MediaWiki\EditPage\EditPage;
27 use MediaWiki\HookContainer\HookRunner;
28 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
29 use MediaWiki\Html\Html;
30 use MediaWiki\Language\Language;
31 use MediaWiki\Linker\Linker;
32 use MediaWiki\Linker\LinkRenderer;
33 use MediaWiki\MainConfigNames;
34 use MediaWiki\MediaWikiServices;
35 use MediaWiki\Message\Message;
36 use MediaWiki\Output\OutputPage;
37 use MediaWiki\Page\ParserOutputAccess;
38 use MediaWiki\Page\ProtectionForm;
39 use MediaWiki\Page\WikiPageFactory;
40 use MediaWiki\Parser\Parser;
41 use MediaWiki\Parser\ParserOptions;
42 use MediaWiki\Parser\ParserOutput;
43 use MediaWiki\Permissions\Authority;
44 use MediaWiki\Permissions\PermissionStatus;
45 use MediaWiki\Permissions\RestrictionStore;
46 use MediaWiki\Revision\ArchivedRevisionLookup;
47 use MediaWiki\Revision\BadRevisionException;
48 use MediaWiki\Revision\RevisionRecord;
49 use MediaWiki\Revision\RevisionStore;
50 use MediaWiki\Revision\SlotRecord;
51 use MediaWiki\Status\Status;
52 use MediaWiki\Title\Title;
53 use MediaWiki\User\Options\UserOptionsLookup;
54 use MediaWiki\User\UserIdentity;
55 use MediaWiki\User\UserNameUtils;
56 use MediaWiki\Xml\Xml;
57 use Wikimedia\IPUtils;
58 use Wikimedia\NonSerializable\NonSerializableTrait;
59 use Wikimedia\Rdbms\IConnectionProvider;
61 /**
62 * Legacy class representing an editable page and handling UI for some page actions.
64 * This has largely been superseded by WikiPage, with Action subclasses for the
65 * user interface of page actions, and service classes for their backend logic.
67 * @todo Move and refactor remaining code
68 * @todo Deprecate
70 class Article implements Page {
71 use ProtectedHookAccessorTrait;
72 use NonSerializableTrait;
74 /**
75 * @var IContextSource|null The context this Article is executed in.
76 * If null, RequestContext::getMain() is used.
77 * @deprecated since 1.35, must be private, use {@link getContext}
79 protected $mContext;
81 /** @var WikiPage The WikiPage object of this instance */
82 protected $mPage;
84 /**
85 * @var int|null The oldid of the article that was requested to be shown,
86 * 0 for the current revision.
88 public $mOldId;
90 /** @var Title|null Title from which we were redirected here, if any. */
91 public $mRedirectedFrom = null;
93 /** @var string|false URL to redirect to or false if none */
94 public $mRedirectUrl = false;
96 /**
97 * @var Status|null represents the outcome of fetchRevisionRecord().
98 * $fetchResult->value is the RevisionRecord object, if the operation was successful.
100 private $fetchResult = null;
103 * @var ParserOutput|null|false The ParserOutput generated for viewing the page,
104 * initialized by view(). If no ParserOutput could be generated, this is set to false.
105 * @deprecated since 1.32
107 public $mParserOutput = null;
110 * @var bool Whether render() was called. With the way subclasses work
111 * here, there doesn't seem to be any other way to stop calling
112 * OutputPage::enableSectionEditLinks() and still have it work as it did before.
114 protected $viewIsRenderAction = false;
116 protected LinkRenderer $linkRenderer;
117 private RevisionStore $revisionStore;
118 private UserNameUtils $userNameUtils;
119 private UserOptionsLookup $userOptionsLookup;
120 private CommentFormatter $commentFormatter;
121 private WikiPageFactory $wikiPageFactory;
122 private JobQueueGroup $jobQueueGroup;
123 private ArchivedRevisionLookup $archivedRevisionLookup;
124 protected IConnectionProvider $dbProvider;
125 protected DatabaseBlockStore $blockStore;
127 protected RestrictionStore $restrictionStore;
130 * @var RevisionRecord|null Revision to be shown
132 * Initialized by getOldIDFromRequest() or fetchRevisionRecord(). While the output of
133 * Article::view is typically based on this revision, it may be replaced by extensions.
135 private $mRevisionRecord = null;
138 * @param Title $title
139 * @param int|null $oldId Revision ID, null to fetch from request, zero for current
141 public function __construct( Title $title, $oldId = null ) {
142 $this->mOldId = $oldId;
143 $this->mPage = $this->newPage( $title );
145 $services = MediaWikiServices::getInstance();
146 $this->linkRenderer = $services->getLinkRenderer();
147 $this->revisionStore = $services->getRevisionStore();
148 $this->userNameUtils = $services->getUserNameUtils();
149 $this->userOptionsLookup = $services->getUserOptionsLookup();
150 $this->commentFormatter = $services->getCommentFormatter();
151 $this->wikiPageFactory = $services->getWikiPageFactory();
152 $this->jobQueueGroup = $services->getJobQueueGroup();
153 $this->archivedRevisionLookup = $services->getArchivedRevisionLookup();
154 $this->dbProvider = $services->getConnectionProvider();
155 $this->blockStore = $services->getDatabaseBlockStore();
156 $this->restrictionStore = $services->getRestrictionStore();
160 * @param Title $title
161 * @return WikiPage
163 protected function newPage( Title $title ) {
164 return new WikiPage( $title );
168 * Constructor from a page id
169 * @param int $id Article ID to load
170 * @return Article|null
172 public static function newFromID( $id ) {
173 $t = Title::newFromID( $id );
174 return $t === null ? null : new static( $t );
178 * Create an Article object of the appropriate class for the given page.
180 * @param Title $title
181 * @param IContextSource $context
182 * @return Article
184 public static function newFromTitle( $title, IContextSource $context ): self {
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() );
190 $page = null;
191 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
192 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
193 ->onArticleFromTitle( $title, $page, $context );
194 if ( !$page ) {
195 switch ( $title->getNamespace() ) {
196 case NS_FILE:
197 $page = new ImagePage( $title );
198 break;
199 case NS_CATEGORY:
200 $page = new CategoryPage( $title );
201 break;
202 default:
203 $page = new Article( $title );
206 $page->setContext( $context );
208 return $page;
212 * Create an Article object of the appropriate class for the given page.
214 * @param WikiPage $page
215 * @param IContextSource $context
216 * @return Article
218 public static function newFromWikiPage( WikiPage $page, IContextSource $context ) {
219 $article = self::newFromTitle( $page->getTitle(), $context );
220 $article->mPage = $page; // override to keep process cached vars
221 return $article;
225 * Get the page this view was redirected from
226 * @return Title|null
227 * @since 1.28
229 public function getRedirectedFrom() {
230 return $this->mRedirectedFrom;
234 * Tell the page view functions that this view was redirected
235 * from another page on the wiki.
237 public function setRedirectedFrom( Title $from ) {
238 $this->mRedirectedFrom = $from;
242 * Get the title object of the article
244 * @return Title Title object of this page
246 public function getTitle() {
247 return $this->mPage->getTitle();
251 * Get the WikiPage object of this instance
253 * @since 1.19
254 * @return WikiPage
256 public function getPage() {
257 return $this->mPage;
260 public function clear() {
261 $this->mRedirectedFrom = null; # Title object if set
262 $this->mRedirectUrl = false;
263 $this->mRevisionRecord = null;
264 $this->fetchResult = null;
266 // TODO hard-deprecate direct access to public fields
268 $this->mPage->clear();
272 * @see getOldIDFromRequest()
273 * @see getRevIdFetched()
275 * @return int The oldid of the article that is was requested in the constructor or via the
276 * context's WebRequest.
278 public function getOldID() {
279 if ( $this->mOldId === null ) {
280 $this->mOldId = $this->getOldIDFromRequest();
283 return $this->mOldId;
287 * Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect
289 * @return int The old id for the request
291 public function getOldIDFromRequest() {
292 $this->mRedirectUrl = false;
294 $request = $this->getContext()->getRequest();
295 $oldid = $request->getIntOrNull( 'oldid' );
297 if ( $oldid === null ) {
298 return 0;
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 );
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();
327 if ( $nextid ) {
328 $oldid = $nextid;
329 $this->mRevisionRecord = null;
330 } else {
331 $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' );
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();
341 if ( $previd ) {
342 $oldid = $previd;
343 $this->mRevisionRecord = null;
347 return $oldid;
351 * Fetches the revision to work on.
352 * The revision is loaded from the database. Refer to $this->fetchResult for the revision
353 * or any errors encountered while loading it.
355 * Public since 1.35
357 * @return RevisionRecord|null
359 public function fetchRevisionRecord() {
360 if ( $this->fetchResult ) {
361 return $this->mRevisionRecord;
364 $oldid = $this->getOldID();
366 // $this->mRevisionRecord might already be fetched by getOldIDFromRequest()
367 if ( !$this->mRevisionRecord ) {
368 if ( !$oldid ) {
369 $this->mRevisionRecord = $this->mPage->getRevisionRecord();
371 if ( !$this->mRevisionRecord ) {
372 wfDebug( __METHOD__ . " failed to find page data for title " .
373 $this->getTitle()->getPrefixedText() );
375 // Output for this case is done by showMissingArticle().
376 $this->fetchResult = Status::newFatal( 'noarticletext' );
377 return null;
379 } else {
380 $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
382 if ( !$this->mRevisionRecord ) {
383 wfDebug( __METHOD__ . " failed to load revision, rev_id $oldid" );
385 $this->fetchResult = Status::newFatal( $this->getMissingRevisionMsg( $oldid ) );
386 return null;
391 if ( !$this->mRevisionRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getContext()->getAuthority() ) ) {
392 wfDebug( __METHOD__ . " failed to retrieve content of revision " . $this->mRevisionRecord->getId() );
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();
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 );
405 return null;
408 $this->fetchResult = Status::newGood( $this->mRevisionRecord );
409 return $this->mRevisionRecord;
413 * Returns true if the currently-referenced revision is the current edit
414 * to this page (and it exists).
415 * @return bool
417 public function isCurrent() {
418 # If no oldid, this is the current version.
419 if ( $this->getOldID() == 0 ) {
420 return true;
423 return $this->mPage->exists() &&
424 $this->mRevisionRecord &&
425 $this->mRevisionRecord->isCurrent();
429 * Use this to fetch the rev ID used on page views
431 * Before fetchRevisionRecord was called, this returns the page's latest revision,
432 * regardless of what getOldID() returns.
434 * @return int Revision ID of last article revision
436 public function getRevIdFetched() {
437 if ( $this->fetchResult && $this->fetchResult->isOK() ) {
438 /** @var RevisionRecord $rev */
439 $rev = $this->fetchResult->getValue();
440 return $rev->getId();
441 } else {
442 return $this->mPage->getLatest();
447 * This is the default action of the index.php entry point: just view the
448 * page of the given title.
450 public function view() {
451 $context = $this->getContext();
452 $useFileCache = $context->getConfig()->get( MainConfigNames::UseFileCache );
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();
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 )
466 wfDebug( __METHOD__ . ": denied on secondary read check" );
467 throw new PermissionsError( 'read', $permissionStatus );
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" );
476 return;
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();
484 return;
487 $this->showProtectionIndicator();
489 # Set page title (may be overridden from ParserOutput if title conversion is enabled or DISPLAYTITLE is used)
490 $outputPage->setPageTitle( Parser::formatPageTitle(
491 str_replace( '_', ' ', $this->getTitle()->getNsText() ),
492 ':',
493 $this->getTitle()->getText()
494 ) );
496 $outputPage->setArticleFlag( true );
497 # Allow frames by default
498 $outputPage->getMetadata()->setPreventClickjacking( false );
500 $parserOptions = $this->getParserOptions();
502 $poOptions = [];
503 # Allow extensions to vary parser options used for article rendering
504 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
505 ->onArticleParserOptions( $this, $parserOptions );
506 # Render printable version, use printable version cache
507 if ( $outputPage->isPrintable() ) {
508 $parserOptions->setIsPrintable( true );
509 $poOptions['enableSectionEditLinks'] = false;
510 $this->addMessageBoxStyles( $outputPage );
511 $outputPage->prependHTML(
512 Html::warningBox(
513 $outputPage->msg( 'printableversion-deprecated-warning' )->escaped()
516 } elseif ( $this->viewIsRenderAction || !$this->isCurrent() ||
517 !$authority->probablyCan( 'edit', $this->getTitle() )
519 $poOptions['enableSectionEditLinks'] = false;
522 # Try client and file cache
523 if ( $oldid === 0 && $this->mPage->checkTouched() ) {
524 # Try to stream the output from file cache
525 if ( $useFileCache && $this->tryFileCache() ) {
526 wfDebug( __METHOD__ . ": done file cache" );
527 # tell wgOut that output is taken care of
528 $outputPage->disable();
529 $this->mPage->doViewUpdates( $authority, $oldid );
531 return;
535 $this->showRedirectedFromHeader();
536 $this->showNamespaceHeader();
538 if ( $this->viewIsRenderAction ) {
539 $poOptions += [ 'absoluteURLs' => true ];
541 $poOptions += [ 'includeDebugInfo' => true ];
543 try {
544 $continue =
545 $this->generateContentOutput( $authority, $parserOptions, $oldid, $outputPage, $poOptions );
546 } catch ( BadRevisionException $e ) {
547 $continue = false;
548 $this->showViewError( wfMessage( 'badrevision' )->text() );
551 if ( !$continue ) {
552 return;
555 # For the main page, overwrite the <title> element with the con-
556 # tents of 'pagetitle-view-mainpage' instead of the default (if
557 # that's not empty).
558 # This message always exists because it is in the i18n files
559 if ( $this->getTitle()->isMainPage() ) {
560 $msg = $context->msg( 'pagetitle-view-mainpage' )->inContentLanguage();
561 if ( !$msg->isDisabled() ) {
562 $outputPage->setHTMLTitle( $msg->text() );
566 // Enable 1-day CDN cache on this response
568 // To reduce impact of lost or delayed HTTP purges, the adaptive TTL will
569 // raise the TTL for pages not recently edited, upto $wgCdnMaxAge.
570 // This could use getTouched(), but that could be scary for major template edits.
571 $outputPage->adaptCdnTTL( $this->mPage->getTimestamp(), 86_400 );
573 $this->showViewFooter();
574 $this->mPage->doViewUpdates( $authority, $oldid, $this->fetchRevisionRecord() );
576 # Load the postEdit module if the user just saved this revision
577 # See also EditPage::setPostEditCookie
578 $request = $context->getRequest();
579 $cookieKey = EditPage::POST_EDIT_COOKIE_KEY_PREFIX . $this->getRevIdFetched();
580 $postEdit = $request->getCookie( $cookieKey );
581 if ( $postEdit ) {
582 # Clear the cookie. This also prevents caching of the response.
583 $request->response()->clearCookie( $cookieKey );
584 $outputPage->addJsConfigVars( 'wgPostEdit', $postEdit );
585 $outputPage->addModules( 'mediawiki.action.view.postEdit' ); // FIXME: test this
586 if ( $this->getContext()->getConfig()->get( MainConfigNames::EnableEditRecovery )
587 && $this->userOptionsLookup->getOption( $this->getContext()->getUser(), 'editrecovery' )
589 $outputPage->addModules( 'mediawiki.editRecovery.postEdit' );
595 * Show a lock icon above the article body if the page is protected.
597 public function showProtectionIndicator(): void {
598 $title = $this->getTitle();
599 $context = $this->getContext();
600 $outputPage = $context->getOutput();
602 $protectionIndicatorsAreEnabled = $context->getConfig()
603 ->get( MainConfigNames::EnableProtectionIndicators );
605 if ( !$protectionIndicatorsAreEnabled || $title->isMainPage() ) {
606 return;
609 $protection = $this->restrictionStore->getRestrictions( $title, 'edit' );
611 $cascadeProtection = $this->restrictionStore->getCascadeProtectionSources( $title )[1];
613 $isCascadeProtected = array_key_exists( 'edit', $cascadeProtection );
615 if ( !$protection && !$isCascadeProtected ) {
616 return;
619 if ( $isCascadeProtected ) {
620 // Cascade-protected pages are protected at the sysop level. So it
621 // should not matter if we take the protection level of the first
622 // or last page that is being cascaded to the current page.
623 $protectionLevel = $cascadeProtection['edit'][0];
624 } else {
625 $protectionLevel = $protection[0];
628 // Protection levels are stored in the database as plain text, but
629 // they are expected to be valid protection levels. So we should be able to
630 // safely use them. However phan thinks this could be a XSS problem so we
631 // are being paranoid and escaping them once more.
632 $protectionLevel = htmlspecialchars( $protectionLevel );
634 $protectionExpiry = $this->restrictionStore->getRestrictionExpiry( $title, 'edit' );
635 $formattedProtectionExpiry = $context->getLanguage()
636 ->formatExpiry( $protectionExpiry ?? '' );
638 $protectionMsg = 'protection-indicator-title';
639 if ( $protectionExpiry === 'infinity' || !$protectionExpiry ) {
640 $protectionMsg .= '-infinity';
643 // Potential values: 'protection-sysop', 'protection-autoconfirmed',
644 // 'protection-sysop-cascade' etc.
645 // If the wiki has more protection levels, the additional ids that get
646 // added take the form 'protection-<protectionLevel>' and
647 // 'protection-<protectionLevel>-cascade'.
648 $protectionIndicatorId = 'protection-' . $protectionLevel;
649 $protectionIndicatorId .= ( $isCascadeProtected ? '-cascade' : '' );
651 // Messages 'protection-indicator-title', 'protection-indicator-title-infinity'
652 $protectionMsg = $outputPage->msg( $protectionMsg, $protectionLevel, $formattedProtectionExpiry )->text();
654 // Use a trick similar to the one used in Action::addHelpLink() to allow wikis
655 // to customize where the help link points to.
656 $protectionHelpLink = $outputPage->msg( $protectionIndicatorId . '-helppage' );
657 if ( $protectionHelpLink->isDisabled() ) {
658 $protectionHelpLink = 'https://mediawiki.org/wiki/Special:MyLanguage/Help:Protection';
659 } else {
660 $protectionHelpLink = $protectionHelpLink->text();
663 $outputPage->setIndicators( [
664 $protectionIndicatorId => Html::rawElement( 'a', [
665 'class' => 'mw-protection-indicator-icon--lock',
666 'title' => $protectionMsg,
667 'href' => $protectionHelpLink
669 // Screen reader-only text describing the same thing as
670 // was mentioned in the title attribute.
671 Html::element( 'span', [], $protectionMsg ) )
672 ] );
674 $outputPage->addModuleStyles( 'mediawiki.protectionIndicators.styles' );
678 * Determines the desired ParserOutput and passes it to $outputPage.
680 * @param Authority $performer
681 * @param ParserOptions $parserOptions
682 * @param int $oldid
683 * @param OutputPage $outputPage
684 * @param array $textOptions
686 * @return bool True if further processing like footer generation should be applied,
687 * false to skip further processing.
689 private function generateContentOutput(
690 Authority $performer,
691 ParserOptions $parserOptions,
692 int $oldid,
693 OutputPage $outputPage,
694 array $textOptions
695 ): bool {
696 # Should the parser cache be used?
697 $useParserCache = true;
698 $pOutput = null;
699 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
701 // NOTE: $outputDone and $useParserCache may be changed by the hook
702 $this->getHookRunner()->onArticleViewHeader( $this, $outputDone, $useParserCache );
703 if ( $outputDone ) {
704 if ( $outputDone instanceof ParserOutput ) {
705 $pOutput = $outputDone;
708 if ( $pOutput ) {
709 $this->doOutputMetaData( $pOutput, $outputPage );
711 return true;
714 // Early abort if the page doesn't exist
715 if ( !$this->mPage->exists() ) {
716 wfDebug( __METHOD__ . ": showing missing article" );
717 $this->showMissingArticle();
718 $this->mPage->doViewUpdates( $performer );
719 return false; // skip all further output to OutputPage
722 // Try the latest parser cache
723 // NOTE: try latest-revision cache first to avoid loading revision.
724 if ( $useParserCache && !$oldid ) {
725 $pOutput = $parserOutputAccess->getCachedParserOutput(
726 $this->getPage(),
727 $parserOptions,
728 null,
729 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked
732 if ( $pOutput ) {
733 $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
734 $this->doOutputMetaData( $pOutput, $outputPage );
735 return true;
739 $rev = $this->fetchRevisionRecord();
740 if ( !$this->fetchResult->isOK() ) {
741 $this->showViewError( $this->fetchResult->getWikiText(
742 false, false, $this->getContext()->getLanguage()
743 ) );
744 return true;
747 # Are we looking at an old revision
748 if ( $oldid ) {
749 $this->setOldSubtitle( $oldid );
751 if ( !$this->showDeletedRevisionHeader() ) {
752 wfDebug( __METHOD__ . ": cannot view deleted revision" );
753 return false; // skip all further output to OutputPage
756 // Try the old revision parser cache
757 // NOTE: Repeating cache check for old revision to avoid fetching $rev
758 // before it's absolutely necessary.
759 if ( $useParserCache ) {
760 $pOutput = $parserOutputAccess->getCachedParserOutput(
761 $this->getPage(),
762 $parserOptions,
763 $rev,
764 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked in fetchRevisionRecord
767 if ( $pOutput ) {
768 $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
769 $this->doOutputMetaData( $pOutput, $outputPage );
770 return true;
775 # Ensure that UI elements requiring revision ID have
776 # the correct version information. (This may be overwritten after creation of ParserOutput)
777 $outputPage->setRevisionId( $this->getRevIdFetched() );
778 $outputPage->setRevisionIsCurrent( $rev->isCurrent() );
779 # Preload timestamp to avoid a DB hit
780 $outputPage->setRevisionTimestamp( $rev->getTimestamp() );
782 # Pages containing custom CSS or JavaScript get special treatment
783 if ( $this->getTitle()->isSiteConfigPage() || $this->getTitle()->isUserConfigPage() ) {
784 $dir = $this->getContext()->getLanguage()->getDir();
785 $lang = $this->getContext()->getLanguage()->getHtmlCode();
787 $outputPage->wrapWikiMsg(
788 "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
789 'clearyourcache'
791 $outputPage->addModuleStyles( 'mediawiki.action.styles' );
792 } elseif ( !$this->getHookRunner()->onArticleRevisionViewCustom(
793 $rev,
794 $this->getTitle(),
795 $oldid,
796 $outputPage )
798 // NOTE: sync with hooks called in DifferenceEngine::renderNewRevision()
799 // Allow extensions do their own custom view for certain pages
800 $this->doOutputMetaData( $pOutput, $outputPage );
801 return true;
804 # Run the parse, protected by a pool counter
805 wfDebug( __METHOD__ . ": doing uncached parse" );
807 $opt = 0;
809 // we already checked the cache in case 2, don't check again.
810 $opt |= ParserOutputAccess::OPT_NO_CHECK_CACHE;
812 // we already checked in fetchRevisionRecord()
813 $opt |= ParserOutputAccess::OPT_NO_AUDIENCE_CHECK;
815 // enable stampede protection and allow stale content
816 $opt |= ParserOutputAccess::OPT_FOR_ARTICLE_VIEW;
818 // Attempt to trigger WikiPage::triggerOpportunisticLinksUpdate
819 // Ideally this should not be the responsibility of the ParserCache to control this.
820 // See https://phabricator.wikimedia.org/T329842#8816557 for more context.
821 $opt |= ParserOutputAccess::OPT_LINKS_UPDATE;
823 if ( !$rev->getId() || !$useParserCache ) {
824 // fake revision or uncacheable options
825 $opt |= ParserOutputAccess::OPT_NO_CACHE;
828 $renderStatus = $parserOutputAccess->getParserOutput(
829 $this->getPage(),
830 $parserOptions,
831 $rev,
832 $opt
835 // T327164: If parsoid cache warming is enabled, we want to ensure that the page
836 // the user is currently looking at has a cached parsoid rendering, in case they
837 // open visual editor. The cache entry would typically be missing if it has expired
838 // from the cache or it was invalidated by RefreshLinksJob. When "traditional"
839 // parser output has been invalidated by RefreshLinksJob, we will render it on
840 // the fly when a user requests the page, and thereby populate the cache again,
841 // per the code above.
842 // The code below is intended to do the same for parsoid output, but asynchronously
843 // in a job, so the user does not have to wait.
844 // Note that we get here if the traditional parser output was missing from the cache.
845 // We do not check if the parsoid output is present in the cache, because that check
846 // takes time. The assumption is that if we have traditional parser output
847 // cached, we probably also have parsoid output cached.
848 // So we leave it to ParsoidCachePrewarmJob to determine whether or not parsing is
849 // needed.
850 if ( $oldid === 0 || $oldid === $this->getPage()->getLatest() ) {
851 $parsoidCacheWarmingEnabled = $this->getContext()->getConfig()
852 ->get( MainConfigNames::ParsoidCacheConfig )['WarmParsoidParserCache'];
854 if ( $parsoidCacheWarmingEnabled ) {
855 $parsoidJobSpec = ParsoidCachePrewarmJob::newSpec(
856 $rev->getId(),
857 $this->getPage()->toPageRecord(),
858 [ 'causeAction' => 'view' ]
860 $this->jobQueueGroup->lazyPush( $parsoidJobSpec );
864 $this->doOutputFromRenderStatus(
865 $rev,
866 $renderStatus,
867 $outputPage,
868 $textOptions
871 if ( !$renderStatus->isOK() ) {
872 return true;
875 $pOutput = $renderStatus->getValue();
876 $this->doOutputMetaData( $pOutput, $outputPage );
877 return true;
880 private function doOutputMetaData( ?ParserOutput $pOutput, OutputPage $outputPage ) {
881 # Adjust title for main page & pages with displaytitle
882 if ( $pOutput ) {
883 $this->adjustDisplayTitle( $pOutput );
885 // It would be nice to automatically set this during the first call
886 // to OutputPage::addParserOutputMetadata, but we can't because doing
887 // so would break non-pageview actions where OutputPage::getContLangForJS
888 // has different requirements.
889 $pageLang = $pOutput->getLanguage();
890 if ( $pageLang ) {
891 $outputPage->setContentLangForJS( $pageLang );
895 # Check for any __NOINDEX__ tags on the page using $pOutput
896 $policy = $this->getRobotPolicy( 'view', $pOutput ?: null );
897 $outputPage->getMetadata()->setIndexPolicy( $policy['index'] );
898 $outputPage->setFollowPolicy( $policy['follow'] ); // FIXME: test this
900 $this->mParserOutput = $pOutput;
904 * @param ParserOutput $pOutput
905 * @param OutputPage $outputPage
906 * @param array $textOptions
908 private function doOutputFromParserCache(
909 ParserOutput $pOutput,
910 OutputPage $outputPage,
911 array $textOptions
913 # Ensure that UI elements requiring revision ID have
914 # the correct version information.
915 $oldid = $pOutput->getCacheRevisionId() ?? $this->getRevIdFetched();
916 $outputPage->setRevisionId( $oldid );
917 $outputPage->setRevisionIsCurrent( $oldid === $this->mPage->getLatest() );
918 $outputPage->addParserOutput( $pOutput, $textOptions );
919 # Preload timestamp to avoid a DB hit
920 $cachedTimestamp = $pOutput->getRevisionTimestamp();
921 if ( $cachedTimestamp !== null ) {
922 $outputPage->setRevisionTimestamp( $cachedTimestamp );
923 $this->mPage->setTimestamp( $cachedTimestamp );
928 * @param RevisionRecord $rev
929 * @param Status $renderStatus
930 * @param OutputPage $outputPage
931 * @param array $textOptions
933 private function doOutputFromRenderStatus(
934 RevisionRecord $rev,
935 Status $renderStatus,
936 OutputPage $outputPage,
937 array $textOptions
939 $context = $this->getContext();
940 if ( !$renderStatus->isOK() ) {
941 $this->showViewError( $renderStatus->getWikiText(
942 false, 'view-pool-error', $context->getLanguage()
943 ) );
944 return;
947 $pOutput = $renderStatus->getValue();
949 // Cache stale ParserOutput object with a short expiry
950 if ( $renderStatus->hasMessage( 'view-pool-dirty-output' ) ) {
951 $outputPage->lowerCdnMaxage( $context->getConfig()->get( MainConfigNames::CdnMaxageStale ) );
952 $outputPage->setLastModified( $pOutput->getCacheTime() );
953 $staleReason = $renderStatus->hasMessage( 'view-pool-contention' )
954 ? $context->msg( 'view-pool-contention' )->escaped()
955 : $context->msg( 'view-pool-timeout' )->escaped();
956 $outputPage->addHTML( "<!-- parser cache is expired, " .
957 "sending anyway due to $staleReason-->\n" );
959 // Ensure OutputPage knowns the id from the dirty cache, but keep the current flag (T341013)
960 $cachedId = $pOutput->getCacheRevisionId();
961 if ( $cachedId !== null ) {
962 $outputPage->setRevisionId( $cachedId );
963 $outputPage->setRevisionTimestamp( $pOutput->getTimestamp() );
967 $outputPage->addParserOutput( $pOutput, $textOptions );
969 if ( $this->getRevisionRedirectTarget( $rev ) ) {
970 $outputPage->addSubtitle( "<span id=\"redirectsub\">" .
971 $context->msg( 'redirectpagesub' )->parse() . "</span>" );
976 * @param RevisionRecord $revision
977 * @return null|Title
979 private function getRevisionRedirectTarget( RevisionRecord $revision ) {
980 // TODO: find a *good* place for the code that determines the redirect target for
981 // a given revision!
982 // NOTE: Use main slot content. Compare code in DerivedPageDataUpdater::revisionIsRedirect.
983 $content = $revision->getContent( SlotRecord::MAIN );
984 return $content ? $content->getRedirectTarget() : null;
988 * Adjust title for pages with displaytitle, -{T|}- or language conversion
990 public function adjustDisplayTitle( ParserOutput $pOutput ) {
991 $out = $this->getContext()->getOutput();
993 # Adjust the title if it was set by displaytitle, -{T|}- or language conversion
994 $titleText = $pOutput->getTitleText();
995 if ( strval( $titleText ) !== '' ) {
996 $out->setPageTitle( $titleText );
997 $out->setDisplayTitle( $titleText );
1002 * Show a diff page according to current request variables. For use within
1003 * Article::view() only, other callers should use the DifferenceEngine class.
1005 protected function showDiffPage() {
1006 $context = $this->getContext();
1007 $outputPage = $context->getOutput();
1008 $outputPage->addBodyClasses( 'mw-article-diff' );
1009 $request = $context->getRequest();
1010 $diff = $request->getVal( 'diff' );
1011 $rcid = $request->getInt( 'rcid' );
1012 $purge = $request->getRawVal( 'action' ) === 'purge';
1013 $unhide = $request->getInt( 'unhide' ) === 1;
1014 $oldid = $this->getOldID();
1016 $rev = $this->fetchRevisionRecord();
1018 if ( !$rev ) {
1019 // T213621: $rev maybe null due to either lack of permission to view the
1020 // revision or actually not existing. So let's try loading it from the id
1021 $rev = $this->revisionStore->getRevisionById( $oldid );
1022 if ( $rev ) {
1023 // Revision exists but $user lacks permission to diff it.
1024 // Do nothing here.
1025 // The $rev will later be used to create standard diff elements however.
1026 } else {
1027 $outputPage->setPageTitleMsg( $context->msg( 'errorpagetitle' ) );
1028 $msg = $context->msg( 'difference-missing-revision' )
1029 ->params( $oldid )
1030 ->numParams( 1 )
1031 ->parseAsBlock();
1032 $outputPage->addHTML( $msg );
1033 return;
1037 $services = MediaWikiServices::getInstance();
1039 $contentHandler = $services
1040 ->getContentHandlerFactory()
1041 ->getContentHandler(
1042 $rev->getMainContentModel()
1044 $de = $contentHandler->createDifferenceEngine(
1045 $context,
1046 $oldid,
1047 $diff,
1048 $rcid,
1049 $purge,
1050 $unhide
1053 $diffType = $request->getVal( 'diff-type' );
1055 if ( $diffType === null ) {
1056 $diffType = $this->userOptionsLookup
1057 ->getOption( $context->getUser(), 'diff-type' );
1058 } else {
1059 $de->setExtraQueryParams( [ 'diff-type' => $diffType ] );
1062 $de->setSlotDiffOptions( [
1063 'diff-type' => $diffType,
1064 'expand-url' => $this->viewIsRenderAction,
1065 'inline-toggle' => true,
1066 ] );
1067 $de->showDiffPage( $this->isDiffOnlyView() );
1069 // Run view updates for the newer revision being diffed (and shown
1070 // below the diff if not diffOnly).
1071 [ , $new ] = $de->mapDiffPrevNext( $oldid, $diff );
1072 // New can be false, convert it to 0 - this conveniently means the latest revision
1073 $this->mPage->doViewUpdates( $context->getAuthority(), (int)$new );
1075 // Add link to help page; see T321569
1076 $context->getOutput()->addHelpLink( 'Help:Diff' );
1079 protected function isDiffOnlyView() {
1080 return $this->getContext()->getRequest()->getBool(
1081 'diffonly',
1082 $this->userOptionsLookup->getBoolOption( $this->getContext()->getUser(), 'diffonly' )
1087 * Get the robot policy to be used for the current view
1088 * @param string $action The action= GET parameter
1089 * @param ParserOutput|null $pOutput
1090 * @return string[] The policy that should be set
1091 * @todo actions other than 'view'
1093 public function getRobotPolicy( $action, ?ParserOutput $pOutput = null ) {
1094 $context = $this->getContext();
1095 $mainConfig = $context->getConfig();
1096 $articleRobotPolicies = $mainConfig->get( MainConfigNames::ArticleRobotPolicies );
1097 $namespaceRobotPolicies = $mainConfig->get( MainConfigNames::NamespaceRobotPolicies );
1098 $defaultRobotPolicy = $mainConfig->get( MainConfigNames::DefaultRobotPolicy );
1099 $title = $this->getTitle();
1100 $ns = $title->getNamespace();
1102 # Don't index user and user talk pages for blocked users (T13443)
1103 if ( $ns === NS_USER || $ns === NS_USER_TALK ) {
1104 $specificTarget = null;
1105 $vagueTarget = null;
1106 $titleText = $title->getText();
1107 if ( IPUtils::isValid( $titleText ) ) {
1108 $vagueTarget = $titleText;
1109 } else {
1110 $specificTarget = $title->getRootText();
1112 if ( $this->blockStore->newFromTarget( $specificTarget, $vagueTarget ) instanceof DatabaseBlock ) {
1113 return [
1114 'index' => 'noindex',
1115 'follow' => 'nofollow'
1120 if ( $this->mPage->getId() === 0 || $this->getOldID() ) {
1121 # Non-articles (special pages etc), and old revisions
1122 return [
1123 'index' => 'noindex',
1124 'follow' => 'nofollow'
1126 } elseif ( $context->getOutput()->isPrintable() ) {
1127 # Discourage indexing of printable versions, but encourage following
1128 return [
1129 'index' => 'noindex',
1130 'follow' => 'follow'
1132 } elseif ( $context->getRequest()->getInt( 'curid' ) ) {
1133 # For ?curid=x urls, disallow indexing
1134 return [
1135 'index' => 'noindex',
1136 'follow' => 'follow'
1140 # Otherwise, construct the policy based on the various config variables.
1141 $policy = self::formatRobotPolicy( $defaultRobotPolicy );
1143 if ( isset( $namespaceRobotPolicies[$ns] ) ) {
1144 # Honour customised robot policies for this namespace
1145 $policy = array_merge(
1146 $policy,
1147 self::formatRobotPolicy( $namespaceRobotPolicies[$ns] )
1150 if ( $title->canUseNoindex() && $pOutput && $pOutput->getIndexPolicy() ) {
1151 # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates
1152 # a final check that we have really got the parser output.
1153 $policy = array_merge(
1154 $policy,
1155 [ 'index' => $pOutput->getIndexPolicy() ]
1159 if ( isset( $articleRobotPolicies[$title->getPrefixedText()] ) ) {
1160 # (T16900) site config can override user-defined __INDEX__ or __NOINDEX__
1161 $policy = array_merge(
1162 $policy,
1163 self::formatRobotPolicy( $articleRobotPolicies[$title->getPrefixedText()] )
1167 return $policy;
1171 * Converts a String robot policy into an associative array, to allow
1172 * merging of several policies using array_merge().
1173 * @param array|string $policy Returns empty array on null/false/'', transparent
1174 * to already-converted arrays, converts string.
1175 * @return array 'index' => \<indexpolicy\>, 'follow' => \<followpolicy\>
1177 public static function formatRobotPolicy( $policy ) {
1178 if ( is_array( $policy ) ) {
1179 return $policy;
1180 } elseif ( !$policy ) {
1181 return [];
1184 $arr = [];
1185 foreach ( explode( ',', $policy ) as $var ) {
1186 $var = trim( $var );
1187 if ( $var === 'index' || $var === 'noindex' ) {
1188 $arr['index'] = $var;
1189 } elseif ( $var === 'follow' || $var === 'nofollow' ) {
1190 $arr['follow'] = $var;
1194 return $arr;
1198 * If this request is a redirect view, send "redirected from" subtitle to
1199 * the output. Returns true if the header was needed, false if this is not
1200 * a redirect view. Handles both local and remote redirects.
1202 * @return bool
1204 public function showRedirectedFromHeader() {
1205 $context = $this->getContext();
1206 $redirectSources = $context->getConfig()->get( MainConfigNames::RedirectSources );
1207 $outputPage = $context->getOutput();
1208 $request = $context->getRequest();
1209 $rdfrom = $request->getVal( 'rdfrom' );
1211 // Construct a URL for the current page view, but with the target title
1212 $query = $request->getQueryValues();
1213 unset( $query['rdfrom'] );
1214 unset( $query['title'] );
1215 if ( $this->getTitle()->isRedirect() ) {
1216 // Prevent double redirects
1217 $query['redirect'] = 'no';
1219 $redirectTargetUrl = $this->getTitle()->getLinkURL( $query );
1221 if ( $this->mRedirectedFrom ) {
1222 // This is an internally redirected page view.
1223 // We'll need a backlink to the source page for navigation.
1224 if ( $this->getHookRunner()->onArticleViewRedirect( $this ) ) {
1225 $redir = $this->linkRenderer->makeKnownLink(
1226 $this->mRedirectedFrom,
1227 null,
1229 [ 'redirect' => 'no' ]
1232 $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1233 $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1234 . "</span>" );
1236 // Add the script to update the displayed URL and
1237 // set the fragment if one was specified in the redirect
1238 $outputPage->addJsConfigVars( [
1239 'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1240 ] );
1241 $outputPage->addModules( 'mediawiki.action.view.redirect' );
1243 // Add a <link rel="canonical"> tag
1244 $outputPage->setCanonicalUrl( $this->getTitle()->getCanonicalURL() );
1246 // Tell the output object that the user arrived at this article through a redirect
1247 $outputPage->setRedirectedFrom( $this->mRedirectedFrom );
1249 return true;
1251 } elseif ( $rdfrom ) {
1252 // This is an externally redirected view, from some other wiki.
1253 // If it was reported from a trusted site, supply a backlink.
1254 if ( $redirectSources && preg_match( $redirectSources, $rdfrom ) ) {
1255 $redir = $this->linkRenderer->makeExternalLink( $rdfrom, $rdfrom, $this->getTitle() );
1256 $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1257 $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1258 . "</span>" );
1260 // Add the script to update the displayed URL
1261 $outputPage->addJsConfigVars( [
1262 'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1263 ] );
1264 $outputPage->addModules( 'mediawiki.action.view.redirect' );
1266 return true;
1270 return false;
1274 * Show a header specific to the namespace currently being viewed, like
1275 * [[MediaWiki:Talkpagetext]]. For Article::view().
1277 public function showNamespaceHeader() {
1278 if ( $this->getTitle()->isTalkPage() && !$this->getContext()->msg( 'talkpageheader' )->isDisabled() ) {
1279 $this->getContext()->getOutput()->wrapWikiMsg(
1280 "<div class=\"mw-talkpageheader\">\n$1\n</div>",
1281 [ 'talkpageheader' ]
1287 * Show the footer section of an ordinary page view
1289 public function showViewFooter() {
1290 # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
1291 if ( $this->getTitle()->getNamespace() === NS_USER_TALK
1292 && IPUtils::isValid( $this->getTitle()->getText() )
1294 $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' );
1297 // Show a footer allowing the user to patrol the shown revision or page if possible
1298 $patrolFooterShown = $this->showPatrolFooter();
1300 $this->getHookRunner()->onArticleViewFooter( $this, $patrolFooterShown );
1304 * If patrol is possible, output a patrol UI box. This is called from the
1305 * footer section of ordinary page views. If patrol is not possible or not
1306 * desired, does nothing.
1308 * Side effect: When the patrol link is build, this method will call
1309 * OutputPage::setPreventClickjacking(true) and load a JS module.
1311 * @return bool
1313 public function showPatrolFooter() {
1314 $context = $this->getContext();
1315 $mainConfig = $context->getConfig();
1316 $useNPPatrol = $mainConfig->get( MainConfigNames::UseNPPatrol );
1317 $useRCPatrol = $mainConfig->get( MainConfigNames::UseRCPatrol );
1318 $useFilePatrol = $mainConfig->get( MainConfigNames::UseFilePatrol );
1319 // Allow hooks to decide whether to not output this at all
1320 if ( !$this->getHookRunner()->onArticleShowPatrolFooter( $this ) ) {
1321 return false;
1324 $outputPage = $context->getOutput();
1325 $user = $context->getUser();
1326 $title = $this->getTitle();
1327 $rc = false;
1329 if ( !$context->getAuthority()->probablyCan( 'patrol', $title )
1330 || !( $useRCPatrol || $useNPPatrol
1331 || ( $useFilePatrol && $title->inNamespace( NS_FILE ) ) )
1333 // Patrolling is disabled or the user isn't allowed to
1334 return false;
1337 if ( $this->mRevisionRecord
1338 && !RecentChange::isInRCLifespan( $this->mRevisionRecord->getTimestamp(), 21600 )
1340 // The current revision is already older than what could be in the RC table
1341 // 6h tolerance because the RC might not be cleaned out regularly
1342 return false;
1345 // Check for cached results
1346 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1347 $key = $cache->makeKey( 'unpatrollable-page', $title->getArticleID() );
1348 if ( $cache->get( $key ) ) {
1349 return false;
1352 $dbr = $this->dbProvider->getReplicaDatabase();
1353 $oldestRevisionRow = $dbr->newSelectQueryBuilder()
1354 ->select( [ 'rev_id', 'rev_timestamp' ] )
1355 ->from( 'revision' )
1356 ->where( [ 'rev_page' => $title->getArticleID() ] )
1357 ->orderBy( [ 'rev_timestamp', 'rev_id' ] )
1358 ->caller( __METHOD__ )->fetchRow();
1359 $oldestRevisionTimestamp = $oldestRevisionRow ? $oldestRevisionRow->rev_timestamp : false;
1361 // New page patrol: Get the timestamp of the oldest revision which
1362 // the revision table holds for the given page. Then we look
1363 // whether it's within the RC lifespan and if it is, we try
1364 // to get the recentchanges row belonging to that entry.
1365 $recentPageCreation = false;
1366 if ( $oldestRevisionTimestamp
1367 && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 )
1369 // 6h tolerance because the RC might not be cleaned out regularly
1370 $recentPageCreation = true;
1371 $rc = RecentChange::newFromConds(
1373 'rc_this_oldid' => intval( $oldestRevisionRow->rev_id ),
1374 // Avoid selecting a categorization entry
1375 'rc_type' => RC_NEW,
1377 __METHOD__
1379 if ( $rc ) {
1380 // Use generic patrol message for new pages
1381 $markPatrolledMsg = $context->msg( 'markaspatrolledtext' );
1385 // File patrol: Get the timestamp of the latest upload for this page,
1386 // check whether it is within the RC lifespan and if it is, we try
1387 // to get the recentchanges row belonging to that entry
1388 // (with rc_type = RC_LOG, rc_log_type = upload).
1389 $recentFileUpload = false;
1390 if ( ( !$rc || $rc->getAttribute( 'rc_patrolled' ) ) && $useFilePatrol
1391 && $title->getNamespace() === NS_FILE ) {
1392 // Retrieve timestamp from the current file (lastest upload)
1393 $newestUploadTimestamp = $dbr->newSelectQueryBuilder()
1394 ->select( 'img_timestamp' )
1395 ->from( 'image' )
1396 ->where( [ 'img_name' => $title->getDBkey() ] )
1397 ->caller( __METHOD__ )->fetchField();
1398 if ( $newestUploadTimestamp
1399 && RecentChange::isInRCLifespan( $newestUploadTimestamp, 21600 )
1401 // 6h tolerance because the RC might not be cleaned out regularly
1402 $recentFileUpload = true;
1403 $rc = RecentChange::newFromConds(
1405 'rc_type' => RC_LOG,
1406 'rc_log_type' => 'upload',
1407 'rc_timestamp' => $newestUploadTimestamp,
1408 'rc_namespace' => NS_FILE,
1409 'rc_cur_id' => $title->getArticleID()
1411 __METHOD__
1413 if ( $rc ) {
1414 // Use patrol message specific to files
1415 $markPatrolledMsg = $context->msg( 'markaspatrolledtext-file' );
1420 if ( !$recentPageCreation && !$recentFileUpload ) {
1421 // Page creation and latest upload (for files) is too old to be in RC
1423 // We definitely can't patrol so cache the information
1424 // When a new file version is uploaded, the cache is cleared
1425 $cache->set( $key, '1' );
1427 return false;
1430 if ( !$rc ) {
1431 // Don't cache: This can be hit if the page gets accessed very fast after
1432 // its creation / latest upload or in case we have high replica DB lag. In case
1433 // the revision is too old, we will already return above.
1434 return false;
1437 if ( $rc->getAttribute( 'rc_patrolled' ) ) {
1438 // Patrolled RC entry around
1440 // Cache the information we gathered above in case we can't patrol
1441 // Don't cache in case we can patrol as this could change
1442 $cache->set( $key, '1' );
1444 return false;
1447 if ( $rc->getPerformerIdentity()->equals( $user ) ) {
1448 // Don't show a patrol link for own creations/uploads. If the user could
1449 // patrol them, they already would be patrolled
1450 return false;
1453 $outputPage->getMetadata()->setPreventClickjacking( true );
1454 $outputPage->addModules( 'mediawiki.misc-authed-curate' );
1456 $link = $this->linkRenderer->makeKnownLink(
1457 $title,
1458 new HtmlArmor( '<button class="cdx-button cdx-button--action-progressive">'
1459 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $markPatrolledMsg is always set
1460 . $markPatrolledMsg->escaped() . '</button>' ),
1463 'action' => 'markpatrolled',
1464 'rcid' => $rc->getAttribute( 'rc_id' ),
1468 $outputPage->addModuleStyles( 'mediawiki.action.styles' );
1469 $outputPage->addHTML( "<div class='patrollink' data-mw='interface'>$link</div>" );
1471 return true;
1475 * Purge the cache used to check if it is worth showing the patrol footer
1476 * For example, it is done during re-uploads when file patrol is used.
1477 * @param int $articleID ID of the article to purge
1478 * @since 1.27
1480 public static function purgePatrolFooterCache( $articleID ) {
1481 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1482 $cache->delete( $cache->makeKey( 'unpatrollable-page', $articleID ) );
1486 * Show the error text for a missing article. For articles in the MediaWiki
1487 * namespace, show the default message text. To be called from Article::view().
1489 public function showMissingArticle() {
1490 $context = $this->getContext();
1491 $send404Code = $context->getConfig()->get( MainConfigNames::Send404Code );
1493 $outputPage = $context->getOutput();
1494 // Whether the page is a root user page of an existing user (but not a subpage)
1495 $validUserPage = false;
1497 $title = $this->getTitle();
1499 $services = MediaWikiServices::getInstance();
1501 $contextUser = $context->getUser();
1503 # Show info in user (talk) namespace. Does the user exist? Is he blocked?
1504 if ( $title->getNamespace() === NS_USER
1505 || $title->getNamespace() === NS_USER_TALK
1507 $rootPart = $title->getRootText();
1508 $userFactory = $services->getUserFactory();
1509 $user = $userFactory->newFromNameOrIp( $rootPart );
1511 $block = $this->blockStore->newFromTarget( $user, $user );
1513 if ( $user && $user->isRegistered() && $user->isHidden() &&
1514 !$context->getAuthority()->isAllowed( 'hideuser' )
1516 // T120883 if the user is hidden and the viewer cannot see hidden
1517 // users, pretend like it does not exist at all.
1518 $user = false;
1521 if ( !( $user && $user->isRegistered() ) && !$this->userNameUtils->isIP( $rootPart ) ) {
1522 $this->addMessageBoxStyles( $outputPage );
1523 // User does not exist
1524 $outputPage->addHTML( Html::warningBox(
1525 $context->msg( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) )->parse(),
1526 'mw-userpage-userdoesnotexist'
1527 ) );
1529 // Show renameuser log extract
1530 LogEventsList::showLogExtract(
1531 $outputPage,
1532 'renameuser',
1533 Title::makeTitleSafe( NS_USER, $rootPart ),
1536 'lim' => 10,
1537 'showIfEmpty' => false,
1538 'msgKey' => [ 'renameuser-renamed-notice', $title->getBaseText() ]
1541 } elseif (
1542 $user && $block !== null &&
1543 $block->getType() != DatabaseBlock::TYPE_AUTO &&
1545 $block->isSitewide() ||
1546 $services->getPermissionManager()->isBlockedFrom( $user, $title, true )
1549 // Show log extract if the user is sitewide blocked or is partially
1550 // blocked and not allowed to edit their user page or user talk page
1551 LogEventsList::showLogExtract(
1552 $outputPage,
1553 'block',
1554 $services->getNamespaceInfo()->getCanonicalName( NS_USER ) . ':' .
1555 $block->getTargetName(),
1558 'lim' => 1,
1559 'showIfEmpty' => false,
1560 'msgKey' => [
1561 'blocked-notice-logextract',
1562 $user->getName() # Support GENDER in notice
1566 $validUserPage = !$title->isSubpage();
1567 } else {
1568 $validUserPage = !$title->isSubpage();
1572 $this->getHookRunner()->onShowMissingArticle( $this );
1574 # Show delete and move logs if there were any such events.
1575 # The logging query can DOS the site when bots/crawlers cause 404 floods,
1576 # so be careful showing this. 404 pages must be cheap as they are hard to cache.
1577 $dbCache = MediaWikiServices::getInstance()->getMainObjectStash();
1578 $key = $dbCache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
1579 $isRegistered = $contextUser->isRegistered();
1580 $sessionExists = $context->getRequest()->getSession()->isPersistent();
1582 if ( $isRegistered || $dbCache->get( $key ) || $sessionExists ) {
1583 $logTypes = [ 'delete', 'move', 'protect', 'merge' ];
1585 $dbr = $this->dbProvider->getReplicaDatabase();
1587 $conds = [ $dbr->expr( 'log_action', '!=', 'revision' ) ];
1588 // Give extensions a chance to hide their (unrelated) log entries
1589 $this->getHookRunner()->onArticle__MissingArticleConditions( $conds, $logTypes );
1590 LogEventsList::showLogExtract(
1591 $outputPage,
1592 $logTypes,
1593 $title,
1596 'lim' => 10,
1597 'conds' => $conds,
1598 'showIfEmpty' => false,
1599 'msgKey' => [ $isRegistered || $sessionExists
1600 ? 'moveddeleted-notice'
1601 : 'moveddeleted-notice-recent'
1607 if ( !$this->mPage->hasViewableContent() && $send404Code && !$validUserPage ) {
1608 // If there's no backing content, send a 404 Not Found
1609 // for better machine handling of broken links.
1610 $context->getRequest()->response()->statusHeader( 404 );
1613 // Also apply the robot policy for nonexisting pages (even if a 404 was used)
1614 $policy = $this->getRobotPolicy( 'view' );
1615 $outputPage->getMetadata()->setIndexPolicy( $policy['index'] );
1616 $outputPage->setFollowPolicy( $policy['follow'] );
1618 $hookResult = $this->getHookRunner()->onBeforeDisplayNoArticleText( $this );
1620 if ( !$hookResult ) {
1621 return;
1624 # Show error message
1625 $oldid = $this->getOldID();
1626 if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) {
1627 $text = $this->getTitle()->getDefaultMessageText() ?? '';
1628 $outputPage->addWikiTextAsContent( $text );
1629 } else {
1630 if ( $oldid ) {
1631 $text = $this->getMissingRevisionMsg( $oldid )->plain();
1632 } elseif ( $context->getAuthority()->probablyCan( 'edit', $title ) ) {
1633 $message = $isRegistered ? 'noarticletext' : 'noarticletextanon';
1634 $text = $context->msg( $message )->plain();
1635 } else {
1636 $text = $context->msg( 'noarticletext-nopermission' )->plain();
1639 $dir = $context->getLanguage()->getDir();
1640 $lang = $context->getLanguage()->getHtmlCode();
1641 $outputPage->addWikiTextAsInterface( Xml::openElement( 'div', [
1642 'class' => "noarticletext mw-content-$dir",
1643 'dir' => $dir,
1644 'lang' => $lang,
1645 ] ) . "\n$text\n</div>" );
1650 * Show error text for errors generated in Article::view().
1651 * @param string $errortext localized wikitext error message
1653 private function showViewError( string $errortext ) {
1654 $outputPage = $this->getContext()->getOutput();
1655 $outputPage->setPageTitleMsg( $this->getContext()->msg( 'errorpagetitle' ) );
1656 $outputPage->disableClientCache();
1657 $outputPage->setRobotPolicy( 'noindex,nofollow' );
1658 $outputPage->clearHTML();
1659 $this->addMessageBoxStyles( $outputPage );
1660 $outputPage->addHTML( Html::errorBox( $outputPage->parseAsContent( $errortext ) ) );
1664 * If the revision requested for view is deleted, check permissions.
1665 * Send either an error message or a warning header to the output.
1667 * @return bool True if the view is allowed, false if not.
1669 public function showDeletedRevisionHeader() {
1670 if ( !$this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1671 // Not deleted
1672 return true;
1674 $outputPage = $this->getContext()->getOutput();
1675 // Used in wikilinks, should not contain whitespaces
1676 $titleText = $this->getTitle()->getPrefixedDBkey();
1677 $this->addMessageBoxStyles( $outputPage );
1678 // If the user is not allowed to see it...
1679 if ( !$this->mRevisionRecord->userCan(
1680 RevisionRecord::DELETED_TEXT,
1681 $this->getContext()->getAuthority()
1682 ) ) {
1683 $outputPage->addHTML(
1684 Html::warningBox(
1685 $outputPage->msg( 'rev-deleted-text-permission', $titleText )->parse(),
1686 'plainlinks'
1690 return false;
1691 // If the user needs to confirm that they want to see it...
1692 } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) !== 1 ) {
1693 # Give explanation and add a link to view the revision...
1694 $oldid = intval( $this->getOldID() );
1695 $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" );
1696 $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ?
1697 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
1698 $outputPage->addHTML(
1699 Html::warningBox(
1700 $outputPage->msg( $msg, $link )->parse(),
1701 'plainlinks'
1705 return false;
1706 // We are allowed to see...
1707 } else {
1708 $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
1709 ? [ 'rev-suppressed-text-view', $titleText ]
1710 : [ 'rev-deleted-text-view', $titleText ];
1711 $outputPage->addHTML(
1712 Html::warningBox(
1713 $outputPage->msg( $msg[0], $msg[1] )->parse(),
1714 'plainlinks'
1718 return true;
1722 private function addMessageBoxStyles( OutputPage $outputPage ) {
1723 $outputPage->addModuleStyles( [
1724 'mediawiki.codex.messagebox.styles',
1725 ] );
1729 * Generate the navigation links when browsing through an article revisions
1730 * It shows the information as:
1731 * Revision as of \<date\>; view current revision
1732 * \<- Previous version | Next Version -\>
1734 * @param int $oldid Revision ID of this article revision
1736 public function setOldSubtitle( $oldid = 0 ) {
1737 if ( !$this->getHookRunner()->onDisplayOldSubtitle( $this, $oldid ) ) {
1738 return;
1741 $context = $this->getContext();
1742 $unhide = $context->getRequest()->getInt( 'unhide' ) === 1;
1744 # Cascade unhide param in links for easy deletion browsing
1745 $extraParams = [];
1746 if ( $unhide ) {
1747 $extraParams['unhide'] = 1;
1750 if ( $this->mRevisionRecord && $this->mRevisionRecord->getId() === $oldid ) {
1751 $revisionRecord = $this->mRevisionRecord;
1752 } else {
1753 $revisionRecord = $this->revisionStore->getRevisionById( $oldid );
1755 if ( !$revisionRecord ) {
1756 throw new LogicException( 'There should be a revision record at this point.' );
1759 $timestamp = $revisionRecord->getTimestamp();
1761 $current = ( $oldid == $this->mPage->getLatest() );
1762 $language = $context->getLanguage();
1763 $user = $context->getUser();
1765 $td = $language->userTimeAndDate( $timestamp, $user );
1766 $tddate = $language->userDate( $timestamp, $user );
1767 $tdtime = $language->userTime( $timestamp, $user );
1769 # Show user links if allowed to see them. If hidden, then show them only if requested...
1770 $userlinks = Linker::revUserTools( $revisionRecord, !$unhide );
1772 $infomsg = $current && !$context->msg( 'revision-info-current' )->isDisabled()
1773 ? 'revision-info-current'
1774 : 'revision-info';
1776 $outputPage = $context->getOutput();
1777 $outputPage->addModuleStyles( [
1778 'mediawiki.action.styles',
1779 'mediawiki.interface.helpers.styles'
1780 ] );
1782 $revisionUser = $revisionRecord->getUser();
1783 $revisionInfo = "<div id=\"mw-{$infomsg}\">" .
1784 $context->msg( $infomsg, $td )
1785 ->rawParams( $userlinks )
1786 ->params(
1787 $revisionRecord->getId(),
1788 $tddate,
1789 $tdtime,
1790 $revisionUser ? $revisionUser->getName() : ''
1792 ->rawParams( $this->commentFormatter->formatRevision(
1793 $revisionRecord,
1794 $user,
1795 true,
1796 !$unhide
1798 ->parse() .
1799 "</div>";
1801 $lnk = $current
1802 ? $context->msg( 'currentrevisionlink' )->escaped()
1803 : $this->linkRenderer->makeKnownLink(
1804 $this->getTitle(),
1805 $context->msg( 'currentrevisionlink' )->text(),
1807 $extraParams
1809 $curdiff = $current
1810 ? $context->msg( 'diff' )->escaped()
1811 : $this->linkRenderer->makeKnownLink(
1812 $this->getTitle(),
1813 $context->msg( 'diff' )->text(),
1816 'diff' => 'cur',
1817 'oldid' => $oldid
1818 ] + $extraParams
1820 $prevExist = (bool)$this->revisionStore->getPreviousRevision( $revisionRecord );
1821 $prevlink = $prevExist
1822 ? $this->linkRenderer->makeKnownLink(
1823 $this->getTitle(),
1824 $context->msg( 'previousrevision' )->text(),
1827 'direction' => 'prev',
1828 'oldid' => $oldid
1829 ] + $extraParams
1831 : $context->msg( 'previousrevision' )->escaped();
1832 $prevdiff = $prevExist
1833 ? $this->linkRenderer->makeKnownLink(
1834 $this->getTitle(),
1835 $context->msg( 'diff' )->text(),
1838 'diff' => 'prev',
1839 'oldid' => $oldid
1840 ] + $extraParams
1842 : $context->msg( 'diff' )->escaped();
1843 $nextlink = $current
1844 ? $context->msg( 'nextrevision' )->escaped()
1845 : $this->linkRenderer->makeKnownLink(
1846 $this->getTitle(),
1847 $context->msg( 'nextrevision' )->text(),
1850 'direction' => 'next',
1851 'oldid' => $oldid
1852 ] + $extraParams
1854 $nextdiff = $current
1855 ? $context->msg( 'diff' )->escaped()
1856 : $this->linkRenderer->makeKnownLink(
1857 $this->getTitle(),
1858 $context->msg( 'diff' )->text(),
1861 'diff' => 'next',
1862 'oldid' => $oldid
1863 ] + $extraParams
1866 $cdel = Linker::getRevDeleteLink(
1867 $context->getAuthority(),
1868 $revisionRecord,
1869 $this->getTitle()
1871 if ( $cdel !== '' ) {
1872 $cdel .= ' ';
1875 // the outer div is need for styling the revision info and nav in MobileFrontend
1876 $this->addMessageBoxStyles( $outputPage );
1877 $outputPage->addSubtitle(
1878 Html::warningBox(
1879 $revisionInfo .
1880 "<div id=\"mw-revision-nav\">" . $cdel .
1881 $context->msg( 'revision-nav' )->rawParams(
1882 $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff
1883 )->escaped() . "</div>",
1884 'mw-revision'
1890 * Return the HTML for the top of a redirect page
1892 * Chances are you should just be using the ParserOutput from
1893 * WikitextContent::getParserOutput instead of calling this for redirects.
1895 * @since 1.23
1896 * @param Language $lang
1897 * @param Title $target Destination to redirect
1898 * @param bool $forceKnown Should the image be shown as a bluelink regardless of existence?
1899 * @return string Containing HTML with redirect link
1900 * @deprecated since 1.41, use LinkRenderer::makeRedirectHeader() instead
1902 public static function getRedirectHeaderHtml( Language $lang, Title $target, $forceKnown = false ) {
1903 wfDeprecated( __METHOD__, '1.41' );
1904 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1905 return $linkRenderer->makeRedirectHeader( $lang, $target, $forceKnown );
1909 * Adds help link with an icon via page indicators.
1910 * Link target can be overridden by a local message containing a wikilink:
1911 * the message key is: 'namespace-' + namespace number + '-helppage'.
1912 * @param string $to Target MediaWiki.org page title or encoded URL.
1913 * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
1914 * @since 1.25
1916 public function addHelpLink( $to, $overrideBaseUrl = false ) {
1917 $out = $this->getContext()->getOutput();
1918 $msg = $out->msg( 'namespace-' . $this->getTitle()->getNamespace() . '-helppage' );
1920 if ( !$msg->isDisabled() ) {
1921 $title = Title::newFromText( $msg->plain() );
1922 if ( $title instanceof Title ) {
1923 $out->addHelpLink( $title->getLocalURL(), true );
1925 } else {
1926 $out->addHelpLink( $to, $overrideBaseUrl );
1931 * Handle action=render
1933 public function render() {
1934 $this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' );
1935 $this->getContext()->getOutput()->setArticleBodyOnly( true );
1936 // We later set 'enableSectionEditLinks=false' based on this; also used by ImagePage
1937 $this->viewIsRenderAction = true;
1938 $this->view();
1942 * action=protect handler
1944 public function protect() {
1945 $form = new ProtectionForm( $this );
1946 $form->execute();
1950 * action=unprotect handler (alias)
1952 public function unprotect() {
1953 $this->protect();
1956 /* Caching functions */
1959 * checkLastModified returns true if it has taken care of all
1960 * output to the client that is necessary for this request.
1961 * (that is, it has sent a cached version of the page)
1963 * @return bool True if cached version send, false otherwise
1965 protected function tryFileCache() {
1966 static $called = false;
1968 if ( $called ) {
1969 wfDebug( "Article::tryFileCache(): called twice!?" );
1970 return false;
1973 $called = true;
1974 if ( $this->isFileCacheable() ) {
1975 $cache = new HTMLFileCache( $this->getTitle(), 'view' );
1976 if ( $cache->isCacheGood( $this->mPage->getTouched() ) ) {
1977 wfDebug( "Article::tryFileCache(): about to load file" );
1978 $cache->loadFromFileCache( $this->getContext() );
1979 return true;
1980 } else {
1981 wfDebug( "Article::tryFileCache(): starting buffer" );
1982 ob_start( [ &$cache, 'saveToFileCache' ] );
1984 } else {
1985 wfDebug( "Article::tryFileCache(): not cacheable" );
1988 return false;
1992 * Check if the page can be cached
1993 * @param int $mode One of the HTMLFileCache::MODE_* constants (since 1.28)
1994 * @return bool
1996 public function isFileCacheable( $mode = HTMLFileCache::MODE_NORMAL ) {
1997 $cacheable = false;
1999 if ( HTMLFileCache::useFileCache( $this->getContext(), $mode ) ) {
2000 $cacheable = $this->mPage->getId()
2001 && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect();
2002 // Extension may have reason to disable file caching on some pages.
2003 if ( $cacheable ) {
2004 $cacheable = $this->getHookRunner()->onIsFileCacheable( $this ) ?? false;
2008 return $cacheable;
2012 * Lightweight method to get the parser output for a page, checking the parser cache
2013 * and so on. Doesn't consider most of the stuff that Article::view() is forced to
2014 * consider, so it's not appropriate to use there.
2016 * @since 1.16 (r52326) for LiquidThreads
2018 * @param int|null $oldid Revision ID or null
2019 * @param UserIdentity|null $user The relevant user
2020 * @return ParserOutput|false ParserOutput or false if the given revision ID is not found
2022 public function getParserOutput( $oldid = null, ?UserIdentity $user = null ) {
2023 if ( $user === null ) {
2024 $parserOptions = $this->getParserOptions();
2025 } else {
2026 $parserOptions = $this->mPage->makeParserOptions( $user );
2027 $parserOptions->setRenderReason( 'page-view' );
2030 return $this->mPage->getParserOutput( $parserOptions, $oldid );
2034 * Get parser options suitable for rendering the primary article wikitext
2035 * @return ParserOptions
2037 public function getParserOptions() {
2038 $parserOptions = $this->mPage->makeParserOptions( $this->getContext() );
2039 $parserOptions->setRenderReason( 'page-view' );
2040 return $parserOptions;
2044 * Sets the context this Article is executed in
2046 * @param IContextSource $context
2047 * @since 1.18
2049 public function setContext( $context ) {
2050 $this->mContext = $context;
2054 * Gets the context this Article is executed in
2056 * @return IContextSource
2057 * @since 1.18
2059 public function getContext(): IContextSource {
2060 if ( $this->mContext instanceof IContextSource ) {
2061 return $this->mContext;
2062 } else {
2063 wfDebug( __METHOD__ . " called and \$mContext is null. " .
2064 "Return RequestContext::getMain()" );
2065 return RequestContext::getMain();
2070 * Call to WikiPage function for backwards compatibility.
2071 * @see ContentHandler::getActionOverrides
2072 * @return array
2074 public function getActionOverrides() {
2075 return $this->mPage->getActionOverrides();
2078 private function getMissingRevisionMsg( int $oldid ): Message {
2079 // T251066: Try loading the revision from the archive table.
2080 // Show link to view it if it exists and the user has permission to view it.
2081 // (Ignore the given title, if any; look it up from the revision instead.)
2082 $context = $this->getContext();
2083 $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $oldid );
2084 if (
2085 $revRecord &&
2086 $revRecord->userCan(
2087 RevisionRecord::DELETED_TEXT,
2088 $context->getAuthority()
2089 ) &&
2090 $context->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' )
2092 return $context->msg(
2093 'missing-revision-permission',
2094 $oldid,
2095 $revRecord->getTimestamp(),
2096 Title::newFromPageIdentity( $revRecord->getPage() )->getPrefixedDBkey()
2099 return $context->msg( 'missing-revision', $oldid );