MediaWiki master
Article.php
Go to the documentation of this file.
1<?php
22
23use LogicException;
33use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
63use Wikimedia\IPUtils;
64use Wikimedia\NonSerializable\NonSerializableTrait;
66
76class Article implements Page {
77 use ProtectedHookAccessorTrait;
78 use NonSerializableTrait;
79
85 protected $mContext;
86
88 protected $mPage;
89
94 public $mOldId;
95
97 public $mRedirectedFrom = null;
98
100 public $mRedirectUrl = false;
101
106 private $fetchResult = null;
107
113 public $mParserOutput = null;
114
120 protected $viewIsRenderAction = false;
121
123 private RevisionStore $revisionStore;
124 private UserNameUtils $userNameUtils;
125 private UserOptionsLookup $userOptionsLookup;
126 private CommentFormatter $commentFormatter;
127 private WikiPageFactory $wikiPageFactory;
128 private JobQueueGroup $jobQueueGroup;
129 private ArchivedRevisionLookup $archivedRevisionLookup;
132
134
141 private $mRevisionRecord = null;
142
147 public function __construct( Title $title, $oldId = null ) {
148 $this->mOldId = $oldId;
149 $this->mPage = $this->newPage( $title );
150
151 $services = MediaWikiServices::getInstance();
152 $this->linkRenderer = $services->getLinkRenderer();
153 $this->revisionStore = $services->getRevisionStore();
154 $this->userNameUtils = $services->getUserNameUtils();
155 $this->userOptionsLookup = $services->getUserOptionsLookup();
156 $this->commentFormatter = $services->getCommentFormatter();
157 $this->wikiPageFactory = $services->getWikiPageFactory();
158 $this->jobQueueGroup = $services->getJobQueueGroup();
159 $this->archivedRevisionLookup = $services->getArchivedRevisionLookup();
160 $this->dbProvider = $services->getConnectionProvider();
161 $this->blockStore = $services->getDatabaseBlockStore();
162 $this->restrictionStore = $services->getRestrictionStore();
163 }
164
169 protected function newPage( Title $title ) {
170 return new WikiPage( $title );
171 }
172
178 public static function newFromID( $id ) {
179 $t = Title::newFromID( $id );
180 return $t === null ? null : new static( $t );
181 }
182
190 public static function newFromTitle( $title, IContextSource $context ): self {
191 if ( $title->getNamespace() === NS_MEDIA ) {
192 // XXX: This should not be here, but where should it go?
193 $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
194 }
195
196 $page = null;
197 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
198 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
199 ->onArticleFromTitle( $title, $page, $context );
200 if ( !$page ) {
201 switch ( $title->getNamespace() ) {
202 case NS_FILE:
203 $page = new ImagePage( $title );
204 break;
205 case NS_CATEGORY:
206 $page = new CategoryPage( $title );
207 break;
208 default:
209 $page = new Article( $title );
210 }
211 }
212 $page->setContext( $context );
213
214 return $page;
215 }
216
224 public static function newFromWikiPage( WikiPage $page, IContextSource $context ) {
225 $article = self::newFromTitle( $page->getTitle(), $context );
226 $article->mPage = $page; // override to keep process cached vars
227 return $article;
228 }
229
235 public function getRedirectedFrom() {
236 return $this->mRedirectedFrom;
237 }
238
243 public function setRedirectedFrom( Title $from ) {
244 $this->mRedirectedFrom = $from;
245 }
246
252 public function getTitle() {
253 return $this->mPage->getTitle();
254 }
255
262 public function getPage() {
263 return $this->mPage;
264 }
265
266 public function clear() {
267 $this->mRedirectedFrom = null; # Title object if set
268 $this->mRedirectUrl = false;
269 $this->mRevisionRecord = null;
270 $this->fetchResult = null;
271
272 // TODO hard-deprecate direct access to public fields
273
274 $this->mPage->clear();
275 }
276
284 public function getOldID() {
285 if ( $this->mOldId === null ) {
286 $this->mOldId = $this->getOldIDFromRequest();
287 }
288
289 return $this->mOldId;
290 }
291
297 public function getOldIDFromRequest() {
298 $this->mRedirectUrl = false;
299
300 $request = $this->getContext()->getRequest();
301 $oldid = $request->getIntOrNull( 'oldid' );
302
303 if ( $oldid === null ) {
304 return 0;
305 }
306
307 if ( $oldid !== 0 ) {
308 # Load the given revision and check whether the page is another one.
309 # In that case, update this instance to reflect the change.
310 if ( $oldid === $this->mPage->getLatest() ) {
311 $this->mRevisionRecord = $this->mPage->getRevisionRecord();
312 } else {
313 $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
314 if ( $this->mRevisionRecord !== null ) {
315 $revPageId = $this->mRevisionRecord->getPageId();
316 // Revision title doesn't match the page title given?
317 if ( $this->mPage->getId() !== $revPageId ) {
318 $this->mPage = $this->wikiPageFactory->newFromID( $revPageId );
319 }
320 }
321 }
322 }
323
324 $oldRev = $this->mRevisionRecord;
325 if ( $request->getRawVal( 'direction' ) === 'next' ) {
326 $nextid = 0;
327 if ( $oldRev ) {
328 $nextRev = $this->revisionStore->getNextRevision( $oldRev );
329 if ( $nextRev ) {
330 $nextid = $nextRev->getId();
331 }
332 }
333 if ( $nextid ) {
334 $oldid = $nextid;
335 $this->mRevisionRecord = null;
336 } else {
337 $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' );
338 }
339 } elseif ( $request->getRawVal( 'direction' ) === 'prev' ) {
340 $previd = 0;
341 if ( $oldRev ) {
342 $prevRev = $this->revisionStore->getPreviousRevision( $oldRev );
343 if ( $prevRev ) {
344 $previd = $prevRev->getId();
345 }
346 }
347 if ( $previd ) {
348 $oldid = $previd;
349 $this->mRevisionRecord = null;
350 }
351 }
352
353 return $oldid;
354 }
355
365 public function fetchRevisionRecord() {
366 if ( $this->fetchResult ) {
367 return $this->mRevisionRecord;
368 }
369
370 $oldid = $this->getOldID();
371
372 // $this->mRevisionRecord might already be fetched by getOldIDFromRequest()
373 if ( !$this->mRevisionRecord ) {
374 if ( !$oldid ) {
375 $this->mRevisionRecord = $this->mPage->getRevisionRecord();
376
377 if ( !$this->mRevisionRecord ) {
378 wfDebug( __METHOD__ . " failed to find page data for title " .
379 $this->getTitle()->getPrefixedText() );
380
381 // Output for this case is done by showMissingArticle().
382 $this->fetchResult = Status::newFatal( 'noarticletext' );
383 return null;
384 }
385 } else {
386 $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
387
388 if ( !$this->mRevisionRecord ) {
389 wfDebug( __METHOD__ . " failed to load revision, rev_id $oldid" );
390
391 $this->fetchResult = Status::newFatal( $this->getMissingRevisionMsg( $oldid ) );
392 return null;
393 }
394 }
395 }
396
397 if ( !$this->mRevisionRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getContext()->getAuthority() ) ) {
398 wfDebug( __METHOD__ . " failed to retrieve content of revision " . $this->mRevisionRecord->getId() );
399
400 // Output for this case is done by showDeletedRevisionHeader().
401 // title used in wikilinks, should not contain whitespaces
402 $this->fetchResult = new Status;
403 $title = $this->getTitle()->getPrefixedDBkey();
404
405 if ( $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
406 $this->fetchResult->fatal( 'rev-suppressed-text' );
407 } else {
408 $this->fetchResult->fatal( 'rev-deleted-text-permission', $title );
409 }
410
411 return null;
412 }
413
414 $this->fetchResult = Status::newGood( $this->mRevisionRecord );
415 return $this->mRevisionRecord;
416 }
417
423 public function isCurrent() {
424 # If no oldid, this is the current version.
425 if ( $this->getOldID() == 0 ) {
426 return true;
427 }
428
429 return $this->mPage->exists() &&
430 $this->mRevisionRecord &&
431 $this->mRevisionRecord->isCurrent();
432 }
433
442 public function getRevIdFetched() {
443 if ( $this->fetchResult && $this->fetchResult->isOK() ) {
445 $rev = $this->fetchResult->getValue();
446 return $rev->getId();
447 } else {
448 return $this->mPage->getLatest();
449 }
450 }
451
456 public function view() {
457 $context = $this->getContext();
458 $useFileCache = $context->getConfig()->get( MainConfigNames::UseFileCache );
459
460 # Get variables from query string
461 # As side effect this will load the revision and update the title
462 # in a revision ID is passed in the request, so this should remain
463 # the first call of this method even if $oldid is used way below.
464 $oldid = $this->getOldID();
465
466 $authority = $context->getAuthority();
467 # Another check in case getOldID() is altering the title
468 $permissionStatus = PermissionStatus::newEmpty();
469 if ( !$authority
470 ->authorizeRead( 'read', $this->getTitle(), $permissionStatus )
471 ) {
472 wfDebug( __METHOD__ . ": denied on secondary read check" );
473 throw new PermissionsError( 'read', $permissionStatus );
474 }
475
476 $outputPage = $context->getOutput();
477 # getOldID() may as well want us to redirect somewhere else
478 if ( $this->mRedirectUrl ) {
479 $outputPage->redirect( $this->mRedirectUrl );
480 wfDebug( __METHOD__ . ": redirecting due to oldid" );
481
482 return;
483 }
484
485 # If we got diff in the query, we want to see a diff page instead of the article.
486 if ( $context->getRequest()->getCheck( 'diff' ) ) {
487 wfDebug( __METHOD__ . ": showing diff page" );
488 $this->showDiffPage();
489
490 return;
491 }
492
493 $this->showProtectionIndicator();
494
495 # Set page title (may be overridden from ParserOutput if title conversion is enabled or DISPLAYTITLE is used)
496 $outputPage->setPageTitle( Parser::formatPageTitle(
497 str_replace( '_', ' ', $this->getTitle()->getNsText() ),
498 ':',
499 $this->getTitle()->getText()
500 ) );
501
502 $outputPage->setArticleFlag( true );
503 # Allow frames by default
504 $outputPage->getMetadata()->setPreventClickjacking( false );
505
506 $parserOptions = $this->getParserOptions();
507
508 $poOptions = [];
509 # Allow extensions to vary parser options used for article rendering
510 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
511 ->onArticleParserOptions( $this, $parserOptions );
512 # Render printable version, use printable version cache
513 if ( $outputPage->isPrintable() ) {
514 $parserOptions->setIsPrintable( true );
515 $poOptions['enableSectionEditLinks'] = false;
516 $this->addMessageBoxStyles( $outputPage );
517 $outputPage->prependHTML(
518 Html::warningBox(
519 $outputPage->msg( 'printableversion-deprecated-warning' )->escaped()
520 )
521 );
522 } elseif ( $this->viewIsRenderAction || !$this->isCurrent() ||
523 !$authority->probablyCan( 'edit', $this->getTitle() )
524 ) {
525 $poOptions['enableSectionEditLinks'] = false;
526 }
527
528 # Try client and file cache
529 if ( $oldid === 0 && $this->mPage->checkTouched() ) {
530 # Try to stream the output from file cache
531 if ( $useFileCache && $this->tryFileCache() ) {
532 wfDebug( __METHOD__ . ": done file cache" );
533 # tell wgOut that output is taken care of
534 $outputPage->disable();
535 $this->mPage->doViewUpdates( $authority, $oldid );
536
537 return;
538 }
539 }
540
541 $this->showRedirectedFromHeader();
542 $this->showNamespaceHeader();
543
544 if ( $this->viewIsRenderAction ) {
545 $poOptions += [ 'absoluteURLs' => true ];
546 }
547 $poOptions += [ 'includeDebugInfo' => true ];
548
549 try {
550 $continue =
551 $this->generateContentOutput( $authority, $parserOptions, $oldid, $outputPage, $poOptions );
552 } catch ( BadRevisionException $e ) {
553 $continue = false;
554 $this->showViewError( wfMessage( 'badrevision' )->text() );
555 }
556
557 if ( !$continue ) {
558 return;
559 }
560
561 # For the main page, overwrite the <title> element with the con-
562 # tents of 'pagetitle-view-mainpage' instead of the default (if
563 # that's not empty).
564 # This message always exists because it is in the i18n files
565 if ( $this->getTitle()->isMainPage() ) {
566 $msg = $context->msg( 'pagetitle-view-mainpage' )->inContentLanguage();
567 if ( !$msg->isDisabled() ) {
568 $outputPage->setHTMLTitle( $msg->text() );
569 }
570 }
571
572 // Enable 1-day CDN cache on this response
573 //
574 // To reduce impact of lost or delayed HTTP purges, the adaptive TTL will
575 // raise the TTL for pages not recently edited, upto $wgCdnMaxAge.
576 // This could use getTouched(), but that could be scary for major template edits.
577 $outputPage->adaptCdnTTL( $this->mPage->getTimestamp(), 86_400 );
578
579 $this->showViewFooter();
580 $this->mPage->doViewUpdates( $authority, $oldid, $this->fetchRevisionRecord() );
581
582 # Load the postEdit module if the user just saved this revision
583 # See also EditPage::setPostEditCookie
584 $request = $context->getRequest();
585 $cookieKey = EditPage::POST_EDIT_COOKIE_KEY_PREFIX . $this->getRevIdFetched();
586 $postEdit = $request->getCookie( $cookieKey );
587 if ( $postEdit ) {
588 # Clear the cookie. This also prevents caching of the response.
589 $request->response()->clearCookie( $cookieKey );
590 $outputPage->addJsConfigVars( 'wgPostEdit', $postEdit );
591 $outputPage->addModules( 'mediawiki.action.view.postEdit' ); // FIXME: test this
592 if ( $this->getContext()->getConfig()->get( MainConfigNames::EnableEditRecovery )
593 && $this->userOptionsLookup->getOption( $this->getContext()->getUser(), 'editrecovery' )
594 ) {
595 $outputPage->addModules( 'mediawiki.editRecovery.postEdit' );
596 }
597 }
598 }
599
603 public function showProtectionIndicator(): void {
604 $title = $this->getTitle();
605 $context = $this->getContext();
606 $outputPage = $context->getOutput();
607
608 $protectionIndicatorsAreEnabled = $context->getConfig()
610
611 if ( !$protectionIndicatorsAreEnabled || $title->isMainPage() ) {
612 return;
613 }
614
615 $protection = $this->restrictionStore->getRestrictions( $title, 'edit' );
616
617 $cascadeProtection = $this->restrictionStore->getCascadeProtectionSources( $title )[1];
618
619 $isCascadeProtected = array_key_exists( 'edit', $cascadeProtection );
620
621 if ( !$protection && !$isCascadeProtected ) {
622 return;
623 }
624
625 if ( $isCascadeProtected ) {
626 // Cascade-protected pages are protected at the sysop level. So it
627 // should not matter if we take the protection level of the first
628 // or last page that is being cascaded to the current page.
629 $protectionLevel = $cascadeProtection['edit'][0];
630 } else {
631 $protectionLevel = $protection[0];
632 }
633
634 // Protection levels are stored in the database as plain text, but
635 // they are expected to be valid protection levels. So we should be able to
636 // safely use them. However phan thinks this could be a XSS problem so we
637 // are being paranoid and escaping them once more.
638 $protectionLevel = htmlspecialchars( $protectionLevel );
639
640 $protectionExpiry = $this->restrictionStore->getRestrictionExpiry( $title, 'edit' );
641 $formattedProtectionExpiry = $context->getLanguage()
642 ->formatExpiry( $protectionExpiry ?? '' );
643
644 $protectionMsg = 'protection-indicator-title';
645 if ( $protectionExpiry === 'infinity' || !$protectionExpiry ) {
646 $protectionMsg .= '-infinity';
647 }
648
649 // Potential values: 'protection-sysop', 'protection-autoconfirmed',
650 // 'protection-sysop-cascade' etc.
651 // If the wiki has more protection levels, the additional ids that get
652 // added take the form 'protection-<protectionLevel>' and
653 // 'protection-<protectionLevel>-cascade'.
654 $protectionIndicatorId = 'protection-' . $protectionLevel;
655 $protectionIndicatorId .= ( $isCascadeProtected ? '-cascade' : '' );
656
657 // Messages 'protection-indicator-title', 'protection-indicator-title-infinity'
658 $protectionMsg = $outputPage->msg( $protectionMsg, $protectionLevel, $formattedProtectionExpiry )->text();
659
660 // Use a trick similar to the one used in Action::addHelpLink() to allow wikis
661 // to customize where the help link points to.
662 $protectionHelpLink = $outputPage->msg( $protectionIndicatorId . '-helppage' );
663 if ( $protectionHelpLink->isDisabled() ) {
664 $protectionHelpLink = 'https://mediawiki.org/wiki/Special:MyLanguage/Help:Protection';
665 } else {
666 $protectionHelpLink = $protectionHelpLink->text();
667 }
668
669 $outputPage->setIndicators( [
670 $protectionIndicatorId => Html::rawElement( 'a', [
671 'class' => 'mw-protection-indicator-icon--lock',
672 'title' => $protectionMsg,
673 'href' => $protectionHelpLink
674 ],
675 // Screen reader-only text describing the same thing as
676 // was mentioned in the title attribute.
677 Html::element( 'span', [], $protectionMsg ) )
678 ] );
679
680 $outputPage->addModuleStyles( 'mediawiki.protectionIndicators.styles' );
681 }
682
695 private function generateContentOutput(
696 Authority $performer,
697 ParserOptions $parserOptions,
698 int $oldid,
699 OutputPage $outputPage,
700 array $textOptions
701 ): bool {
702 # Should the parser cache be used?
703 $useParserCache = true;
704 $pOutput = null;
705 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
706
707 // NOTE: $outputDone and $useParserCache may be changed by the hook
708 $this->getHookRunner()->onArticleViewHeader( $this, $outputDone, $useParserCache );
709 if ( $outputDone ) {
710 if ( $outputDone instanceof ParserOutput ) {
711 $pOutput = $outputDone;
712 }
713
714 if ( $pOutput ) {
715 $this->doOutputMetaData( $pOutput, $outputPage );
716 }
717 return true;
718 }
719
720 // Early abort if the page doesn't exist
721 if ( !$this->mPage->exists() ) {
722 wfDebug( __METHOD__ . ": showing missing article" );
723 $this->showMissingArticle();
724 $this->mPage->doViewUpdates( $performer );
725 return false; // skip all further output to OutputPage
726 }
727
728 // Try the latest parser cache
729 // NOTE: try latest-revision cache first to avoid loading revision.
730 if ( $useParserCache && !$oldid ) {
731 $pOutput = $parserOutputAccess->getCachedParserOutput(
732 $this->getPage(),
733 $parserOptions,
734 null,
735 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked
736 );
737
738 if ( $pOutput ) {
739 $this->doOutputFromParserCache( $pOutput, $parserOptions, $outputPage, $textOptions );
740 $this->doOutputMetaData( $pOutput, $outputPage );
741 return true;
742 }
743 }
744
745 $rev = $this->fetchRevisionRecord();
746 if ( !$this->fetchResult->isOK() ) {
747 $this->showViewError( $this->fetchResult->getWikiText(
748 false, false, $this->getContext()->getLanguage()
749 ) );
750 return true;
751 }
752
753 # Are we looking at an old revision
754 if ( $oldid ) {
755 $this->setOldSubtitle( $oldid );
756
757 if ( !$this->showDeletedRevisionHeader() ) {
758 wfDebug( __METHOD__ . ": cannot view deleted revision" );
759 return false; // skip all further output to OutputPage
760 }
761
762 // Try the old revision parser cache
763 // NOTE: Repeating cache check for old revision to avoid fetching $rev
764 // before it's absolutely necessary.
765 if ( $useParserCache ) {
766 $pOutput = $parserOutputAccess->getCachedParserOutput(
767 $this->getPage(),
768 $parserOptions,
769 $rev,
770 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked in fetchRevisionRecord
771 );
772
773 if ( $pOutput ) {
774 $this->doOutputFromParserCache( $pOutput, $parserOptions, $outputPage, $textOptions );
775 $this->doOutputMetaData( $pOutput, $outputPage );
776 return true;
777 }
778 }
779 }
780
781 # Ensure that UI elements requiring revision ID have
782 # the correct version information. (This may be overwritten after creation of ParserOutput)
783 $outputPage->setRevisionId( $this->getRevIdFetched() );
784 $outputPage->setRevisionIsCurrent( $rev->isCurrent() );
785 # Preload timestamp to avoid a DB hit
786 $outputPage->getMetadata()->setRevisionTimestamp( $rev->getTimestamp() );
787
788 # Pages containing custom CSS or JavaScript get special treatment
789 if ( $this->getTitle()->isSiteConfigPage() || $this->getTitle()->isUserConfigPage() ) {
790 $dir = $this->getContext()->getLanguage()->getDir();
791 $lang = $this->getContext()->getLanguage()->getHtmlCode();
792
793 $outputPage->wrapWikiMsg(
794 "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
795 'clearyourcache'
796 );
797 $outputPage->addModuleStyles( 'mediawiki.action.styles' );
798 } elseif ( !$this->getHookRunner()->onArticleRevisionViewCustom(
799 $rev,
800 $this->getTitle(),
801 $oldid,
802 $outputPage )
803 ) {
804 // NOTE: sync with hooks called in DifferenceEngine::renderNewRevision()
805 // Allow extensions do their own custom view for certain pages
806 $this->doOutputMetaData( $pOutput, $outputPage );
807 return true;
808 }
809
810 # Run the parse, protected by a pool counter
811 wfDebug( __METHOD__ . ": doing uncached parse" );
812
813 $opt = 0;
814
815 // we already checked the cache in case 2, don't check again.
816 $opt |= ParserOutputAccess::OPT_NO_CHECK_CACHE;
817
818 // we already checked in fetchRevisionRecord()
819 $opt |= ParserOutputAccess::OPT_NO_AUDIENCE_CHECK;
820
821 // enable stampede protection and allow stale content
822 $opt |= ParserOutputAccess::OPT_FOR_ARTICLE_VIEW;
823
824 // Attempt to trigger WikiPage::triggerOpportunisticLinksUpdate
825 // Ideally this should not be the responsibility of the ParserCache to control this.
826 // See https://phabricator.wikimedia.org/T329842#8816557 for more context.
827 $opt |= ParserOutputAccess::OPT_LINKS_UPDATE;
828
829 if ( !$rev->getId() || !$useParserCache ) {
830 // fake revision or uncacheable options
831 $opt |= ParserOutputAccess::OPT_NO_CACHE;
832 }
833
834 $renderStatus = $parserOutputAccess->getParserOutput(
835 $this->getPage(),
836 $parserOptions,
837 $rev,
838 $opt
839 );
840
841 // T327164: If parsoid cache warming is enabled, we want to ensure that the page
842 // the user is currently looking at has a cached parsoid rendering, in case they
843 // open visual editor. The cache entry would typically be missing if it has expired
844 // from the cache or it was invalidated by RefreshLinksJob. When "traditional"
845 // parser output has been invalidated by RefreshLinksJob, we will render it on
846 // the fly when a user requests the page, and thereby populate the cache again,
847 // per the code above.
848 // The code below is intended to do the same for parsoid output, but asynchronously
849 // in a job, so the user does not have to wait.
850 // Note that we get here if the traditional parser output was missing from the cache.
851 // We do not check if the parsoid output is present in the cache, because that check
852 // takes time. The assumption is that if we have traditional parser output
853 // cached, we probably also have parsoid output cached.
854 // So we leave it to ParsoidCachePrewarmJob to determine whether or not parsing is
855 // needed.
856 if ( $oldid === 0 || $oldid === $this->getPage()->getLatest() ) {
857 $parsoidCacheWarmingEnabled = $this->getContext()->getConfig()
858 ->get( MainConfigNames::ParsoidCacheConfig )['WarmParsoidParserCache'];
859
860 if ( $parsoidCacheWarmingEnabled ) {
861 $parsoidJobSpec = ParsoidCachePrewarmJob::newSpec(
862 $rev->getId(),
863 $this->getPage()->toPageRecord(),
864 [ 'causeAction' => 'view' ]
865 );
866 $this->jobQueueGroup->lazyPush( $parsoidJobSpec );
867 }
868 }
869
870 $this->doOutputFromRenderStatus(
871 $rev,
872 $renderStatus,
873 $outputPage,
874 $parserOptions,
875 $textOptions
876 );
877
878 if ( !$renderStatus->isOK() ) {
879 return true;
880 }
881
882 $pOutput = $renderStatus->getValue();
883 $this->doOutputMetaData( $pOutput, $outputPage );
884 return true;
885 }
886
887 private function doOutputMetaData( ?ParserOutput $pOutput, OutputPage $outputPage ) {
888 # Adjust title for main page & pages with displaytitle
889 if ( $pOutput ) {
890 $this->adjustDisplayTitle( $pOutput );
891
892 // It would be nice to automatically set this during the first call
893 // to OutputPage::addParserOutputMetadata, but we can't because doing
894 // so would break non-pageview actions where OutputPage::getContLangForJS
895 // has different requirements.
896 $pageLang = $pOutput->getLanguage();
897 if ( $pageLang ) {
898 $outputPage->setContentLangForJS( $pageLang );
899 }
900 }
901
902 # Check for any __NOINDEX__ tags on the page using $pOutput
903 $policy = $this->getRobotPolicy( 'view', $pOutput ?: null );
904 $outputPage->getMetadata()->setIndexPolicy( $policy['index'] );
905 $outputPage->setFollowPolicy( $policy['follow'] ); // FIXME: test this
906
907 $this->mParserOutput = $pOutput;
908 }
909
916 private function doOutputFromParserCache(
917 ParserOutput $pOutput,
918 ParserOptions $pOptions,
919 OutputPage $outputPage,
920 array $textOptions
921 ) {
922 # Ensure that UI elements requiring revision ID have
923 # the correct version information.
924 $oldid = $pOutput->getCacheRevisionId() ?? $this->getRevIdFetched();
925 $outputPage->setRevisionId( $oldid );
926 $outputPage->setRevisionIsCurrent( $oldid === $this->mPage->getLatest() );
927 $outputPage->addParserOutput( $pOutput, $pOptions, $textOptions );
928 # Preload timestamp to avoid a DB hit
929 $cachedTimestamp = $pOutput->getRevisionTimestamp();
930 if ( $cachedTimestamp !== null ) {
931 $outputPage->getMetadata()->setRevisionTimestamp( $cachedTimestamp );
932 $this->mPage->setTimestamp( $cachedTimestamp );
933 }
934 }
935
943 private function doOutputFromRenderStatus(
944 RevisionRecord $rev,
945 Status $renderStatus,
946 OutputPage $outputPage,
947 ParserOptions $parserOptions,
948 array $textOptions
949 ) {
950 $context = $this->getContext();
951 if ( !$renderStatus->isOK() ) {
952 $this->showViewError( $renderStatus->getWikiText(
953 false, 'view-pool-error', $context->getLanguage()
954 ) );
955 return;
956 }
957
958 $pOutput = $renderStatus->getValue();
959
960 // Cache stale ParserOutput object with a short expiry
961 if ( $renderStatus->hasMessage( 'view-pool-dirty-output' ) ) {
962 $outputPage->lowerCdnMaxage( $context->getConfig()->get( MainConfigNames::CdnMaxageStale ) );
963 $outputPage->setLastModified( $pOutput->getCacheTime() );
964 $staleReason = $renderStatus->hasMessage( 'view-pool-contention' )
965 ? $context->msg( 'view-pool-contention' )->escaped()
966 : $context->msg( 'view-pool-timeout' )->escaped();
967 $outputPage->addHTML( "<!-- parser cache is expired, " .
968 "sending anyway due to $staleReason-->\n" );
969
970 // Ensure OutputPage knowns the id from the dirty cache, but keep the current flag (T341013)
971 $cachedId = $pOutput->getCacheRevisionId();
972 if ( $cachedId !== null ) {
973 $outputPage->setRevisionId( $cachedId );
974 $outputPage->getMetadata()->setRevisionTimestamp( $pOutput->getTimestamp() );
975 }
976 }
977
978 $outputPage->addParserOutput( $pOutput, $parserOptions, $textOptions );
979
980 if ( $this->getRevisionRedirectTarget( $rev ) ) {
981 $outputPage->addSubtitle( "<span id=\"redirectsub\">" .
982 $context->msg( 'redirectpagesub' )->parse() . "</span>" );
983 }
984 }
985
990 private function getRevisionRedirectTarget( RevisionRecord $revision ) {
991 // TODO: find a *good* place for the code that determines the redirect target for
992 // a given revision!
993 // NOTE: Use main slot content. Compare code in DerivedPageDataUpdater::revisionIsRedirect.
994 $content = $revision->getContent( SlotRecord::MAIN );
995 return $content ? $content->getRedirectTarget() : null;
996 }
997
1001 public function adjustDisplayTitle( ParserOutput $pOutput ) {
1002 $out = $this->getContext()->getOutput();
1003
1004 # Adjust the title if it was set by displaytitle, -{T|}- or language conversion
1005 $titleText = $pOutput->getTitleText();
1006 if ( strval( $titleText ) !== '' ) {
1007 $out->setPageTitle( $titleText );
1008 $out->setDisplayTitle( $titleText );
1009 }
1010 }
1011
1016 protected function showDiffPage() {
1017 $context = $this->getContext();
1018 $outputPage = $context->getOutput();
1019 $outputPage->addBodyClasses( 'mw-article-diff' );
1020 $request = $context->getRequest();
1021 $diff = $request->getVal( 'diff' );
1022 $rcid = $request->getInt( 'rcid' );
1023 $purge = $request->getRawVal( 'action' ) === 'purge';
1024 $unhide = $request->getInt( 'unhide' ) === 1;
1025 $oldid = $this->getOldID();
1026
1027 $rev = $this->fetchRevisionRecord();
1028
1029 if ( !$rev ) {
1030 // T213621: $rev maybe null due to either lack of permission to view the
1031 // revision or actually not existing. So let's try loading it from the id
1032 $rev = $this->revisionStore->getRevisionById( $oldid );
1033 if ( $rev ) {
1034 // Revision exists but $user lacks permission to diff it.
1035 // Do nothing here.
1036 // The $rev will later be used to create standard diff elements however.
1037 } else {
1038 $outputPage->setPageTitleMsg( $context->msg( 'errorpagetitle' ) );
1039 $msg = $context->msg( 'difference-missing-revision' )
1040 ->params( $oldid )
1041 ->numParams( 1 )
1042 ->parseAsBlock();
1043 $outputPage->addHTML( $msg );
1044 return;
1045 }
1046 }
1047
1048 $services = MediaWikiServices::getInstance();
1049
1050 $contentHandler = $services
1051 ->getContentHandlerFactory()
1052 ->getContentHandler(
1053 $rev->getMainContentModel()
1054 );
1055 $de = $contentHandler->createDifferenceEngine(
1056 $context,
1057 $oldid,
1058 $diff,
1059 $rcid,
1060 $purge,
1061 $unhide
1062 );
1063
1064 $diffType = $request->getVal( 'diff-type' );
1065
1066 if ( $diffType === null ) {
1067 $diffType = $this->userOptionsLookup
1068 ->getOption( $context->getUser(), 'diff-type' );
1069 } else {
1070 $de->setExtraQueryParams( [ 'diff-type' => $diffType ] );
1071 }
1072
1073 $de->setSlotDiffOptions( [
1074 'diff-type' => $diffType,
1075 'expand-url' => $this->viewIsRenderAction,
1076 'inline-toggle' => true,
1077 ] );
1078 $de->showDiffPage( $this->isDiffOnlyView() );
1079
1080 // Run view updates for the newer revision being diffed (and shown
1081 // below the diff if not diffOnly).
1082 [ , $new ] = $de->mapDiffPrevNext( $oldid, $diff );
1083 // New can be false, convert it to 0 - this conveniently means the latest revision
1084 $this->mPage->doViewUpdates( $context->getAuthority(), (int)$new );
1085
1086 // Add link to help page; see T321569
1087 $context->getOutput()->addHelpLink( 'Help:Diff' );
1088 }
1089
1090 protected function isDiffOnlyView() {
1091 return $this->getContext()->getRequest()->getBool(
1092 'diffonly',
1093 $this->userOptionsLookup->getBoolOption( $this->getContext()->getUser(), 'diffonly' )
1094 );
1095 }
1096
1104 public function getRobotPolicy( $action, ?ParserOutput $pOutput = null ) {
1105 $context = $this->getContext();
1106 $mainConfig = $context->getConfig();
1107 $articleRobotPolicies = $mainConfig->get( MainConfigNames::ArticleRobotPolicies );
1108 $namespaceRobotPolicies = $mainConfig->get( MainConfigNames::NamespaceRobotPolicies );
1109 $defaultRobotPolicy = $mainConfig->get( MainConfigNames::DefaultRobotPolicy );
1110 $title = $this->getTitle();
1111 $ns = $title->getNamespace();
1112
1113 # Don't index user and user talk pages for blocked users (T13443)
1114 if ( $ns === NS_USER || $ns === NS_USER_TALK ) {
1115 $specificTarget = null;
1116 $vagueTarget = null;
1117 $titleText = $title->getText();
1118 if ( IPUtils::isValid( $titleText ) ) {
1119 $vagueTarget = $titleText;
1120 } else {
1121 $specificTarget = $title->getRootText();
1122 }
1123 $block = $this->blockStore->newFromTarget(
1124 $specificTarget, $vagueTarget, false, DatabaseBlockStore::AUTO_NONE );
1125 if ( $block instanceof DatabaseBlock ) {
1126 return [
1127 'index' => 'noindex',
1128 'follow' => 'nofollow'
1129 ];
1130 }
1131 }
1132
1133 if ( $this->mPage->getId() === 0 || $this->getOldID() ) {
1134 # Non-articles (special pages etc), and old revisions
1135 return [
1136 'index' => 'noindex',
1137 'follow' => 'nofollow'
1138 ];
1139 } elseif ( $context->getOutput()->isPrintable() ) {
1140 # Discourage indexing of printable versions, but encourage following
1141 return [
1142 'index' => 'noindex',
1143 'follow' => 'follow'
1144 ];
1145 } elseif ( $context->getRequest()->getInt( 'curid' ) ) {
1146 # For ?curid=x urls, disallow indexing
1147 return [
1148 'index' => 'noindex',
1149 'follow' => 'follow'
1150 ];
1151 }
1152
1153 # Otherwise, construct the policy based on the various config variables.
1154 $policy = self::formatRobotPolicy( $defaultRobotPolicy );
1155
1156 if ( isset( $namespaceRobotPolicies[$ns] ) ) {
1157 # Honour customised robot policies for this namespace
1158 $policy = array_merge(
1159 $policy,
1160 self::formatRobotPolicy( $namespaceRobotPolicies[$ns] )
1161 );
1162 }
1163 if ( $title->canUseNoindex() && $pOutput && $pOutput->getIndexPolicy() ) {
1164 # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates
1165 # a final check that we have really got the parser output.
1166 $policy = array_merge(
1167 $policy,
1168 [ 'index' => $pOutput->getIndexPolicy() ]
1169 );
1170 }
1171
1172 if ( isset( $articleRobotPolicies[$title->getPrefixedText()] ) ) {
1173 # (T16900) site config can override user-defined __INDEX__ or __NOINDEX__
1174 $policy = array_merge(
1175 $policy,
1176 self::formatRobotPolicy( $articleRobotPolicies[$title->getPrefixedText()] )
1177 );
1178 }
1179
1180 return $policy;
1181 }
1182
1190 public static function formatRobotPolicy( $policy ) {
1191 if ( is_array( $policy ) ) {
1192 return $policy;
1193 } elseif ( !$policy ) {
1194 return [];
1195 }
1196
1197 $arr = [];
1198 foreach ( explode( ',', $policy ) as $var ) {
1199 $var = trim( $var );
1200 if ( $var === 'index' || $var === 'noindex' ) {
1201 $arr['index'] = $var;
1202 } elseif ( $var === 'follow' || $var === 'nofollow' ) {
1203 $arr['follow'] = $var;
1204 }
1205 }
1206
1207 return $arr;
1208 }
1209
1217 public function showRedirectedFromHeader() {
1218 $context = $this->getContext();
1219 $redirectSources = $context->getConfig()->get( MainConfigNames::RedirectSources );
1220 $outputPage = $context->getOutput();
1221 $request = $context->getRequest();
1222 $rdfrom = $request->getVal( 'rdfrom' );
1223
1224 // Construct a URL for the current page view, but with the target title
1225 $query = $request->getQueryValues();
1226 unset( $query['rdfrom'] );
1227 unset( $query['title'] );
1228 if ( $this->getTitle()->isRedirect() ) {
1229 // Prevent double redirects
1230 $query['redirect'] = 'no';
1231 }
1232 $redirectTargetUrl = $this->getTitle()->getLinkURL( $query );
1233
1234 if ( $this->mRedirectedFrom ) {
1235 // This is an internally redirected page view.
1236 // We'll need a backlink to the source page for navigation.
1237 if ( $this->getHookRunner()->onArticleViewRedirect( $this ) ) {
1238 $redir = $this->linkRenderer->makeKnownLink(
1239 $this->mRedirectedFrom,
1240 null,
1241 [],
1242 [ 'redirect' => 'no' ]
1243 );
1244
1245 $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1246 $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1247 . "</span>" );
1248
1249 // Add the script to update the displayed URL and
1250 // set the fragment if one was specified in the redirect
1251 $outputPage->addJsConfigVars( [
1252 'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1253 ] );
1254 $outputPage->addModules( 'mediawiki.action.view.redirect' );
1255
1256 // Add a <link rel="canonical"> tag
1257 $outputPage->setCanonicalUrl( $this->getTitle()->getCanonicalURL() );
1258
1259 // Tell the output object that the user arrived at this article through a redirect
1260 $outputPage->setRedirectedFrom( $this->mRedirectedFrom );
1261
1262 return true;
1263 }
1264 } elseif ( $rdfrom ) {
1265 // This is an externally redirected view, from some other wiki.
1266 // If it was reported from a trusted site, supply a backlink.
1267 if ( $redirectSources && preg_match( $redirectSources, $rdfrom ) ) {
1268 $redir = $this->linkRenderer->makeExternalLink( $rdfrom, $rdfrom, $this->getTitle() );
1269 $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1270 $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1271 . "</span>" );
1272
1273 // Add the script to update the displayed URL
1274 $outputPage->addJsConfigVars( [
1275 'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1276 ] );
1277 $outputPage->addModules( 'mediawiki.action.view.redirect' );
1278
1279 return true;
1280 }
1281 }
1282
1283 return false;
1284 }
1285
1290 public function showNamespaceHeader() {
1291 if ( $this->getTitle()->isTalkPage() && !$this->getContext()->msg( 'talkpageheader' )->isDisabled() ) {
1292 $this->getContext()->getOutput()->wrapWikiMsg(
1293 "<div class=\"mw-talkpageheader\">\n$1\n</div>",
1294 [ 'talkpageheader' ]
1295 );
1296 }
1297 }
1298
1302 public function showViewFooter() {
1303 # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
1304 if ( $this->getTitle()->getNamespace() === NS_USER_TALK
1305 && IPUtils::isValid( $this->getTitle()->getText() )
1306 ) {
1307 $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' );
1308 }
1309
1310 // Show a footer allowing the user to patrol the shown revision or page if possible
1311 $patrolFooterShown = $this->showPatrolFooter();
1312
1313 $this->getHookRunner()->onArticleViewFooter( $this, $patrolFooterShown );
1314 }
1315
1326 public function showPatrolFooter() {
1327 $context = $this->getContext();
1328 $mainConfig = $context->getConfig();
1329 $useNPPatrol = $mainConfig->get( MainConfigNames::UseNPPatrol );
1330 $useRCPatrol = $mainConfig->get( MainConfigNames::UseRCPatrol );
1331 $useFilePatrol = $mainConfig->get( MainConfigNames::UseFilePatrol );
1332 $fileMigrationStage = $mainConfig->get( MainConfigNames::FileSchemaMigrationStage );
1333 // Allow hooks to decide whether to not output this at all
1334 if ( !$this->getHookRunner()->onArticleShowPatrolFooter( $this ) ) {
1335 return false;
1336 }
1337
1338 $outputPage = $context->getOutput();
1339 $user = $context->getUser();
1340 $title = $this->getTitle();
1341 $rc = false;
1342
1343 if ( !$context->getAuthority()->probablyCan( 'patrol', $title )
1344 || !( $useRCPatrol || $useNPPatrol
1345 || ( $useFilePatrol && $title->inNamespace( NS_FILE ) ) )
1346 ) {
1347 // Patrolling is disabled or the user isn't allowed to
1348 return false;
1349 }
1350
1351 if ( $this->mRevisionRecord
1352 && !RecentChange::isInRCLifespan( $this->mRevisionRecord->getTimestamp(), 21600 )
1353 ) {
1354 // The current revision is already older than what could be in the RC table
1355 // 6h tolerance because the RC might not be cleaned out regularly
1356 return false;
1357 }
1358
1359 // Check for cached results
1360 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1361 $key = $cache->makeKey( 'unpatrollable-page', $title->getArticleID() );
1362 if ( $cache->get( $key ) ) {
1363 return false;
1364 }
1365
1366 $dbr = $this->dbProvider->getReplicaDatabase();
1367 $oldestRevisionRow = $dbr->newSelectQueryBuilder()
1368 ->select( [ 'rev_id', 'rev_timestamp' ] )
1369 ->from( 'revision' )
1370 ->where( [ 'rev_page' => $title->getArticleID() ] )
1371 ->orderBy( [ 'rev_timestamp', 'rev_id' ] )
1372 ->caller( __METHOD__ )->fetchRow();
1373 $oldestRevisionTimestamp = $oldestRevisionRow ? $oldestRevisionRow->rev_timestamp : false;
1374
1375 // New page patrol: Get the timestamp of the oldest revision which
1376 // the revision table holds for the given page. Then we look
1377 // whether it's within the RC lifespan and if it is, we try
1378 // to get the recentchanges row belonging to that entry.
1379 $recentPageCreation = false;
1380 if ( $oldestRevisionTimestamp
1381 && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 )
1382 ) {
1383 // 6h tolerance because the RC might not be cleaned out regularly
1384 $recentPageCreation = true;
1385 $rc = RecentChange::newFromConds(
1386 [
1387 'rc_this_oldid' => intval( $oldestRevisionRow->rev_id ),
1388 // Avoid selecting a categorization entry
1389 'rc_type' => RC_NEW,
1390 ],
1391 __METHOD__
1392 );
1393 if ( $rc ) {
1394 // Use generic patrol message for new pages
1395 $markPatrolledMsg = $context->msg( 'markaspatrolledtext' );
1396 }
1397 }
1398
1399 // File patrol: Get the timestamp of the latest upload for this page,
1400 // check whether it is within the RC lifespan and if it is, we try
1401 // to get the recentchanges row belonging to that entry
1402 // (with rc_type = RC_LOG, rc_log_type = upload).
1403 $recentFileUpload = false;
1404 if ( ( !$rc || $rc->getAttribute( 'rc_patrolled' ) ) && $useFilePatrol
1405 && $title->getNamespace() === NS_FILE ) {
1406 // Retrieve timestamp from the current file (latest upload)
1407 if ( $fileMigrationStage & SCHEMA_COMPAT_READ_OLD ) {
1408 $newestUploadTimestamp = $dbr->newSelectQueryBuilder()
1409 ->select( 'img_timestamp' )
1410 ->from( 'image' )
1411 ->where( [ 'img_name' => $title->getDBkey() ] )
1412 ->caller( __METHOD__ )->fetchField();
1413 } else {
1414 $newestUploadTimestamp = $dbr->newSelectQueryBuilder()
1415 ->select( 'fr_timestamp' )
1416 ->from( 'file' )
1417 ->join( 'filerevision', null, 'file_latest = fr_id' )
1418 ->where( [ 'file_name' => $title->getDBkey() ] )
1419 ->caller( __METHOD__ )->fetchField();
1420 }
1421
1422 if ( $newestUploadTimestamp
1423 && RecentChange::isInRCLifespan( $newestUploadTimestamp, 21600 )
1424 ) {
1425 // 6h tolerance because the RC might not be cleaned out regularly
1426 $recentFileUpload = true;
1427 $rc = RecentChange::newFromConds(
1428 [
1429 'rc_type' => RC_LOG,
1430 'rc_log_type' => 'upload',
1431 'rc_timestamp' => $newestUploadTimestamp,
1432 'rc_namespace' => NS_FILE,
1433 'rc_cur_id' => $title->getArticleID()
1434 ],
1435 __METHOD__
1436 );
1437 if ( $rc ) {
1438 // Use patrol message specific to files
1439 $markPatrolledMsg = $context->msg( 'markaspatrolledtext-file' );
1440 }
1441 }
1442 }
1443
1444 if ( !$recentPageCreation && !$recentFileUpload ) {
1445 // Page creation and latest upload (for files) is too old to be in RC
1446
1447 // We definitely can't patrol so cache the information
1448 // When a new file version is uploaded, the cache is cleared
1449 $cache->set( $key, '1' );
1450
1451 return false;
1452 }
1453
1454 if ( !$rc ) {
1455 // Don't cache: This can be hit if the page gets accessed very fast after
1456 // its creation / latest upload or in case we have high replica DB lag. In case
1457 // the revision is too old, we will already return above.
1458 return false;
1459 }
1460
1461 if ( $rc->getAttribute( 'rc_patrolled' ) ) {
1462 // Patrolled RC entry around
1463
1464 // Cache the information we gathered above in case we can't patrol
1465 // Don't cache in case we can patrol as this could change
1466 $cache->set( $key, '1' );
1467
1468 return false;
1469 }
1470
1471 if ( $rc->getPerformerIdentity()->equals( $user ) ) {
1472 // Don't show a patrol link for own creations/uploads. If the user could
1473 // patrol them, they already would be patrolled
1474 return false;
1475 }
1476
1477 $outputPage->getMetadata()->setPreventClickjacking( true );
1478 $outputPage->addModules( 'mediawiki.misc-authed-curate' );
1479
1480 $link = $this->linkRenderer->makeKnownLink(
1481 $title,
1482 new HtmlArmor( '<button class="cdx-button cdx-button--action-progressive">'
1483 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $markPatrolledMsg is always set
1484 . $markPatrolledMsg->escaped() . '</button>' ),
1485 [],
1486 [
1487 'action' => 'markpatrolled',
1488 'rcid' => $rc->getAttribute( 'rc_id' ),
1489 ]
1490 );
1491
1492 $outputPage->addModuleStyles( 'mediawiki.action.styles' );
1493 $outputPage->addHTML( "<div class='patrollink' data-mw='interface'>$link</div>" );
1494
1495 return true;
1496 }
1497
1504 public static function purgePatrolFooterCache( $articleID ) {
1505 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1506 $cache->delete( $cache->makeKey( 'unpatrollable-page', $articleID ) );
1507 }
1508
1513 public function showMissingArticle() {
1514 $context = $this->getContext();
1515 $send404Code = $context->getConfig()->get( MainConfigNames::Send404Code );
1516
1517 $outputPage = $context->getOutput();
1518 // Whether the page is a root user page of an existing user (but not a subpage)
1519 $validUserPage = false;
1520
1521 $title = $this->getTitle();
1522
1523 $services = MediaWikiServices::getInstance();
1524
1525 $contextUser = $context->getUser();
1526
1527 # Show info in user (talk) namespace. Does the user exist? Are they blocked?
1528 if ( $title->getNamespace() === NS_USER
1529 || $title->getNamespace() === NS_USER_TALK
1530 ) {
1531 $rootPart = $title->getRootText();
1532 $userFactory = $services->getUserFactory();
1533 $user = $userFactory->newFromNameOrIp( $rootPart );
1534
1535 if ( $user && $user->isRegistered() && $user->isHidden() &&
1536 !$context->getAuthority()->isAllowed( 'hideuser' )
1537 ) {
1538 // T120883 if the user is hidden and the viewer cannot see hidden
1539 // users, pretend like it does not exist at all.
1540 $user = false;
1541 }
1542
1543 if ( !( $user && $user->isRegistered() ) && !$this->userNameUtils->isIP( $rootPart ) ) {
1544 $this->addMessageBoxStyles( $outputPage );
1545 // User does not exist
1546 $outputPage->addHTML( Html::warningBox(
1547 $context->msg( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) )->parse(),
1548 'mw-userpage-userdoesnotexist'
1549 ) );
1550
1551 // Show renameuser log extract
1552 LogEventsList::showLogExtract(
1553 $outputPage,
1554 'renameuser',
1555 Title::makeTitleSafe( NS_USER, $rootPart ),
1556 '',
1557 [
1558 'lim' => 10,
1559 'showIfEmpty' => false,
1560 'msgKey' => [ 'renameuser-renamed-notice', $title->getBaseText() ]
1561 ]
1562 );
1563 } else {
1564 $validUserPage = !$title->isSubpage();
1565
1566 $blockLogBox = LogEventsList::getBlockLogWarningBox(
1567 $this->blockStore,
1568 $services->getNamespaceInfo(),
1569 $this->getContext(),
1570 $this->linkRenderer,
1571 $user,
1572 $title
1573 );
1574 if ( $blockLogBox !== null ) {
1575 $outputPage->addHTML( $blockLogBox );
1576 }
1577 }
1578 }
1579
1580 $this->getHookRunner()->onShowMissingArticle( $this );
1581
1582 # Show delete and move logs if there were any such events.
1583 # The logging query can DOS the site when bots/crawlers cause 404 floods,
1584 # so be careful showing this. 404 pages must be cheap as they are hard to cache.
1585 $dbCache = MediaWikiServices::getInstance()->getMainObjectStash();
1586 $key = $dbCache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
1587 $isRegistered = $contextUser->isRegistered();
1588 $sessionExists = $context->getRequest()->getSession()->isPersistent();
1589
1590 if ( $isRegistered || $dbCache->get( $key ) || $sessionExists ) {
1591 $logTypes = [ 'delete', 'move', 'protect', 'merge' ];
1592
1593 $dbr = $this->dbProvider->getReplicaDatabase();
1594
1595 $conds = [ $dbr->expr( 'log_action', '!=', 'revision' ) ];
1596 // Give extensions a chance to hide their (unrelated) log entries
1597 $this->getHookRunner()->onArticle__MissingArticleConditions( $conds, $logTypes );
1598 LogEventsList::showLogExtract(
1599 $outputPage,
1600 $logTypes,
1601 $title,
1602 '',
1603 [
1604 'lim' => 10,
1605 'conds' => $conds,
1606 'showIfEmpty' => false,
1607 'msgKey' => [ $isRegistered || $sessionExists
1608 ? 'moveddeleted-notice'
1609 : 'moveddeleted-notice-recent'
1610 ]
1611 ]
1612 );
1613 }
1614
1615 if ( !$this->mPage->hasViewableContent() && $send404Code && !$validUserPage ) {
1616 // If there's no backing content, send a 404 Not Found
1617 // for better machine handling of broken links.
1618 $context->getRequest()->response()->statusHeader( 404 );
1619 }
1620
1621 // Also apply the robot policy for nonexisting pages (even if a 404 was used)
1622 $policy = $this->getRobotPolicy( 'view' );
1623 $outputPage->getMetadata()->setIndexPolicy( $policy['index'] );
1624 $outputPage->setFollowPolicy( $policy['follow'] );
1625
1626 $hookResult = $this->getHookRunner()->onBeforeDisplayNoArticleText( $this );
1627
1628 if ( !$hookResult ) {
1629 return;
1630 }
1631
1632 # Show error message
1633 $oldid = $this->getOldID();
1634 if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) {
1635 $text = $this->getTitle()->getDefaultMessageText() ?? '';
1636 $outputPage->addWikiTextAsContent( $text );
1637 } else {
1638 if ( $oldid ) {
1639 $text = $this->getMissingRevisionMsg( $oldid )->plain();
1640 } elseif ( $context->getAuthority()->probablyCan( 'edit', $title ) ) {
1641 $message = $isRegistered ? 'noarticletext' : 'noarticletextanon';
1642 $text = $context->msg( $message )->plain();
1643 } else {
1644 $text = $context->msg( 'noarticletext-nopermission' )->plain();
1645 }
1646
1647 $dir = $context->getLanguage()->getDir();
1648 $lang = $context->getLanguage()->getHtmlCode();
1649 $outputPage->addWikiTextAsInterface( Html::openElement( 'div', [
1650 'class' => "noarticletext mw-content-$dir",
1651 'dir' => $dir,
1652 'lang' => $lang,
1653 ] ) . "\n$text\n</div>" );
1654 }
1655 }
1656
1661 private function showViewError( string $errortext ) {
1662 $outputPage = $this->getContext()->getOutput();
1663 $outputPage->setPageTitleMsg( $this->getContext()->msg( 'errorpagetitle' ) );
1664 $outputPage->disableClientCache();
1665 $outputPage->setRobotPolicy( 'noindex,nofollow' );
1666 $outputPage->clearHTML();
1667 $this->addMessageBoxStyles( $outputPage );
1668 $outputPage->addHTML( Html::errorBox( $outputPage->parseAsContent( $errortext ) ) );
1669 }
1670
1677 public function showDeletedRevisionHeader() {
1678 if ( !$this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1679 // Not deleted
1680 return true;
1681 }
1682 $outputPage = $this->getContext()->getOutput();
1683 // Used in wikilinks, should not contain whitespaces
1684 $titleText = $this->getTitle()->getPrefixedDBkey();
1685 $this->addMessageBoxStyles( $outputPage );
1686 // If the user is not allowed to see it...
1687 if ( !$this->mRevisionRecord->userCan(
1688 RevisionRecord::DELETED_TEXT,
1689 $this->getContext()->getAuthority()
1690 ) ) {
1691 $outputPage->addHTML(
1692 Html::warningBox(
1693 $outputPage->msg( 'rev-deleted-text-permission', $titleText )->parse(),
1694 'plainlinks'
1695 )
1696 );
1697
1698 return false;
1699 // If the user needs to confirm that they want to see it...
1700 } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) !== 1 ) {
1701 # Give explanation and add a link to view the revision...
1702 $oldid = intval( $this->getOldID() );
1703 $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" );
1704 $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ?
1705 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
1706 $outputPage->addHTML(
1707 Html::warningBox(
1708 $outputPage->msg( $msg, $link )->parse(),
1709 'plainlinks'
1710 )
1711 );
1712
1713 return false;
1714 // We are allowed to see...
1715 } else {
1716 $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
1717 ? [ 'rev-suppressed-text-view', $titleText ]
1718 : [ 'rev-deleted-text-view', $titleText ];
1719 $outputPage->addHTML(
1720 Html::warningBox(
1721 $outputPage->msg( $msg[0], $msg[1] )->parse(),
1722 'plainlinks'
1723 )
1724 );
1725
1726 return true;
1727 }
1728 }
1729
1730 private function addMessageBoxStyles( OutputPage $outputPage ) {
1731 $outputPage->addModuleStyles( [
1732 'mediawiki.codex.messagebox.styles',
1733 ] );
1734 }
1735
1744 public function setOldSubtitle( $oldid = 0 ) {
1745 if ( !$this->getHookRunner()->onDisplayOldSubtitle( $this, $oldid ) ) {
1746 return;
1747 }
1748
1749 $context = $this->getContext();
1750 $unhide = $context->getRequest()->getInt( 'unhide' ) === 1;
1751
1752 # Cascade unhide param in links for easy deletion browsing
1753 $extraParams = [];
1754 if ( $unhide ) {
1755 $extraParams['unhide'] = 1;
1756 }
1757
1758 if ( $this->mRevisionRecord && $this->mRevisionRecord->getId() === $oldid ) {
1759 $revisionRecord = $this->mRevisionRecord;
1760 } else {
1761 $revisionRecord = $this->revisionStore->getRevisionById( $oldid );
1762 }
1763 if ( !$revisionRecord ) {
1764 throw new LogicException( 'There should be a revision record at this point.' );
1765 }
1766
1767 $timestamp = $revisionRecord->getTimestamp();
1768
1769 $current = ( $oldid == $this->mPage->getLatest() );
1770 $language = $context->getLanguage();
1771 $user = $context->getUser();
1772
1773 $td = $language->userTimeAndDate( $timestamp, $user );
1774 $tddate = $language->userDate( $timestamp, $user );
1775 $tdtime = $language->userTime( $timestamp, $user );
1776
1777 # Show user links if allowed to see them. If hidden, then show them only if requested...
1778 $userlinks = Linker::revUserTools( $revisionRecord, !$unhide );
1779
1780 $infomsg = $current && !$context->msg( 'revision-info-current' )->isDisabled()
1781 ? 'revision-info-current'
1782 : 'revision-info';
1783
1784 $outputPage = $context->getOutput();
1785 $outputPage->addModuleStyles( [
1786 'mediawiki.action.styles',
1787 'mediawiki.interface.helpers.styles'
1788 ] );
1789
1790 $revisionUser = $revisionRecord->getUser();
1791 $revisionInfo = "<div id=\"mw-{$infomsg}\">" .
1792 $context->msg( $infomsg, $td )
1793 ->rawParams( $userlinks )
1794 ->params(
1795 $revisionRecord->getId(),
1796 $tddate,
1797 $tdtime,
1798 $revisionUser ? $revisionUser->getName() : ''
1799 )
1800 ->rawParams( $this->commentFormatter->formatRevision(
1801 $revisionRecord,
1802 $user,
1803 true,
1804 !$unhide
1805 ) )
1806 ->parse() .
1807 "</div>";
1808
1809 $lnk = $current
1810 ? $context->msg( 'currentrevisionlink' )->escaped()
1811 : $this->linkRenderer->makeKnownLink(
1812 $this->getTitle(),
1813 $context->msg( 'currentrevisionlink' )->text(),
1814 [],
1815 $extraParams
1816 );
1817 $curdiff = $current
1818 ? $context->msg( 'diff' )->escaped()
1819 : $this->linkRenderer->makeKnownLink(
1820 $this->getTitle(),
1821 $context->msg( 'diff' )->text(),
1822 [],
1823 [
1824 'diff' => 'cur',
1825 'oldid' => $oldid
1826 ] + $extraParams
1827 );
1828 $prevExist = (bool)$this->revisionStore->getPreviousRevision( $revisionRecord );
1829 $prevlink = $prevExist
1830 ? $this->linkRenderer->makeKnownLink(
1831 $this->getTitle(),
1832 $context->msg( 'previousrevision' )->text(),
1833 [],
1834 [
1835 'direction' => 'prev',
1836 'oldid' => $oldid
1837 ] + $extraParams
1838 )
1839 : $context->msg( 'previousrevision' )->escaped();
1840 $prevdiff = $prevExist
1841 ? $this->linkRenderer->makeKnownLink(
1842 $this->getTitle(),
1843 $context->msg( 'diff' )->text(),
1844 [],
1845 [
1846 'diff' => 'prev',
1847 'oldid' => $oldid
1848 ] + $extraParams
1849 )
1850 : $context->msg( 'diff' )->escaped();
1851 $nextlink = $current
1852 ? $context->msg( 'nextrevision' )->escaped()
1853 : $this->linkRenderer->makeKnownLink(
1854 $this->getTitle(),
1855 $context->msg( 'nextrevision' )->text(),
1856 [],
1857 [
1858 'direction' => 'next',
1859 'oldid' => $oldid
1860 ] + $extraParams
1861 );
1862 $nextdiff = $current
1863 ? $context->msg( 'diff' )->escaped()
1864 : $this->linkRenderer->makeKnownLink(
1865 $this->getTitle(),
1866 $context->msg( 'diff' )->text(),
1867 [],
1868 [
1869 'diff' => 'next',
1870 'oldid' => $oldid
1871 ] + $extraParams
1872 );
1873
1874 $cdel = Linker::getRevDeleteLink(
1875 $context->getAuthority(),
1876 $revisionRecord,
1877 $this->getTitle()
1878 );
1879 if ( $cdel !== '' ) {
1880 $cdel .= ' ';
1881 }
1882
1883 // the outer div is need for styling the revision info and nav in MobileFrontend
1884 $this->addMessageBoxStyles( $outputPage );
1885 $outputPage->addSubtitle(
1886 Html::warningBox(
1887 $revisionInfo .
1888 "<div id=\"mw-revision-nav\">" . $cdel .
1889 $context->msg( 'revision-nav' )->rawParams(
1890 $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff
1891 )->escaped() . "</div>",
1892 'mw-revision'
1893 )
1894 );
1895 }
1896
1910 public static function getRedirectHeaderHtml( Language $lang, Title $target, $forceKnown = false ) {
1911 wfDeprecated( __METHOD__, '1.41' );
1912 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1913 return $linkRenderer->makeRedirectHeader( $lang, $target, $forceKnown );
1914 }
1915
1924 public function addHelpLink( $to, $overrideBaseUrl = false ) {
1925 $out = $this->getContext()->getOutput();
1926 $msg = $out->msg( 'namespace-' . $this->getTitle()->getNamespace() . '-helppage' );
1927
1928 if ( !$msg->isDisabled() ) {
1929 $title = Title::newFromText( $msg->plain() );
1930 if ( $title instanceof Title ) {
1931 $out->addHelpLink( $title->getLocalURL(), true );
1932 }
1933 } else {
1934 $out->addHelpLink( $to, $overrideBaseUrl );
1935 }
1936 }
1937
1941 public function render() {
1942 $this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' );
1943 $this->getContext()->getOutput()->setArticleBodyOnly( true );
1944 // We later set 'enableSectionEditLinks=false' based on this; also used by ImagePage
1945 $this->viewIsRenderAction = true;
1946 $this->view();
1947 }
1948
1952 public function protect() {
1953 $form = new ProtectionForm( $this );
1954 $form->execute();
1955 }
1956
1960 public function unprotect() {
1961 $this->protect();
1962 }
1963
1964 /* Caching functions */
1965
1973 protected function tryFileCache() {
1974 static $called = false;
1975
1976 if ( $called ) {
1977 wfDebug( "Article::tryFileCache(): called twice!?" );
1978 return false;
1979 }
1980
1981 $called = true;
1982 if ( $this->isFileCacheable() ) {
1983 $cache = new HTMLFileCache( $this->getTitle(), 'view' );
1984 if ( $cache->isCacheGood( $this->mPage->getTouched() ) ) {
1985 wfDebug( "Article::tryFileCache(): about to load file" );
1986 $cache->loadFromFileCache( $this->getContext() );
1987 return true;
1988 } else {
1989 wfDebug( "Article::tryFileCache(): starting buffer" );
1990 ob_start( [ &$cache, 'saveToFileCache' ] );
1991 }
1992 } else {
1993 wfDebug( "Article::tryFileCache(): not cacheable" );
1994 }
1995
1996 return false;
1997 }
1998
2004 public function isFileCacheable( $mode = HTMLFileCache::MODE_NORMAL ) {
2005 $cacheable = false;
2006
2007 if ( HTMLFileCache::useFileCache( $this->getContext(), $mode ) ) {
2008 $cacheable = $this->mPage->getId()
2009 && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect();
2010 // Extension may have reason to disable file caching on some pages.
2011 if ( $cacheable ) {
2012 $cacheable = $this->getHookRunner()->onIsFileCacheable( $this ) ?? false;
2013 }
2014 }
2015
2016 return $cacheable;
2017 }
2018
2030 public function getParserOutput( $oldid = null, ?UserIdentity $user = null ) {
2031 if ( $user === null ) {
2032 $parserOptions = $this->getParserOptions();
2033 } else {
2034 $parserOptions = $this->mPage->makeParserOptions( $user );
2035 $parserOptions->setRenderReason( 'page-view' );
2036 }
2037
2038 return $this->mPage->getParserOutput( $parserOptions, $oldid );
2039 }
2040
2045 public function getParserOptions() {
2046 $parserOptions = $this->mPage->makeParserOptions( $this->getContext() );
2047 $parserOptions->setRenderReason( 'page-view' );
2048 return $parserOptions;
2049 }
2050
2057 public function setContext( $context ) {
2058 $this->mContext = $context;
2059 }
2060
2067 public function getContext(): IContextSource {
2068 if ( $this->mContext instanceof IContextSource ) {
2069 return $this->mContext;
2070 } else {
2071 wfDebug( __METHOD__ . " called and \$mContext is null. " .
2072 "Return RequestContext::getMain()" );
2073 return RequestContext::getMain();
2074 }
2075 }
2076
2082 public function getActionOverrides() {
2083 return $this->mPage->getActionOverrides();
2084 }
2085
2086 private function getMissingRevisionMsg( int $oldid ): Message {
2087 // T251066: Try loading the revision from the archive table.
2088 // Show link to view it if it exists and the user has permission to view it.
2089 // (Ignore the given title, if any; look it up from the revision instead.)
2090 $context = $this->getContext();
2091 $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $oldid );
2092 if (
2093 $revRecord &&
2094 $revRecord->userCan(
2095 RevisionRecord::DELETED_TEXT,
2096 $context->getAuthority()
2097 ) &&
2098 $context->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' )
2099 ) {
2100 return $context->msg(
2101 'missing-revision-permission',
2102 $oldid,
2103 $revRecord->getTimestamp(),
2104 Title::newFromPageIdentity( $revRecord->getPage() )->getPrefixedDBkey()
2105 );
2106 }
2107 return $context->msg( 'missing-revision', $oldid );
2108 }
2109}
2110
2112class_alias( Article::class, 'Article' );
const NS_USER
Definition Defines.php:67
const NS_FILE
Definition Defines.php:71
const RC_NEW
Definition Defines.php:118
const NS_MEDIAWIKI
Definition Defines.php:73
const SCHEMA_COMPAT_READ_OLD
Definition Defines.php:304
const RC_LOG
Definition Defines.php:119
const NS_MEDIA
Definition Defines.php:53
const NS_USER_TALK
Definition Defines.php:68
const NS_CATEGORY
Definition Defines.php:79
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:82
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
Page view caching in the file system.
This is the main service interface for converting single-line comments from various DB comment fields...
Group all the pieces relevant to the context of a request into one instance.
The HTML user interface for page editing.
Definition EditPage.php:151
Show an error when a user tries to do something they do not have the necessary permissions for.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:57
Handle enqueueing of background jobs.
Base class for language-specific code.
Definition Language.php:81
Class that generates HTML for internal links.
makeRedirectHeader(Language $lang, Title $target, bool $forceKnown=false, bool $addLinkTag=false)
Return the HTML for the top of a redirect page.
Some internal bits split of from Skin.php.
Definition Linker.php:61
A class containing constants representing the names of configuration variables.
const UseFileCache
Name constant for the UseFileCache setting, for use with Config::get()
const EnableEditRecovery
Name constant for the EnableEditRecovery setting, for use with Config::get()
const EnableProtectionIndicators
Name constant for the EnableProtectionIndicators setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:157
This is one of the Core classes and should be read at least once by any new developers.
addSubtitle( $str)
Add $str to the subtitle.
addModuleStyles( $modules)
Load the styles of one or more style-only ResourceLoader modules on this page.
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:76
render()
Handle action=render.
Definition Article.php:1941
static formatRobotPolicy( $policy)
Converts a String robot policy into an associative array, to allow merging of several policies using ...
Definition Article.php:1190
static purgePatrolFooterCache( $articleID)
Purge the cache used to check if it is worth showing the patrol footer For example,...
Definition Article.php:1504
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition Article.php:224
showNamespaceHeader()
Show a header specific to the namespace currently being viewed, like [[MediaWiki:Talkpagetext]].
Definition Article.php:1290
showViewFooter()
Show the footer section of an ordinary page view.
Definition Article.php:1302
IConnectionProvider $dbProvider
Definition Article.php:130
int null $mOldId
The oldid of the article that was requested to be shown, 0 for the current revision.
Definition Article.php:94
showPatrolFooter()
If patrol is possible, output a patrol UI box.
Definition Article.php:1326
setOldSubtitle( $oldid=0)
Generate the navigation links when browsing through an article revisions It shows the information as:...
Definition Article.php:1744
Title null $mRedirectedFrom
Title from which we were redirected here, if any.
Definition Article.php:97
setContext( $context)
Sets the context this Article is executed in.
Definition Article.php:2057
getParserOutput( $oldid=null, ?UserIdentity $user=null)
Lightweight method to get the parser output for a page, checking the parser cache and so on.
Definition Article.php:2030
bool $viewIsRenderAction
Whether render() was called.
Definition Article.php:120
getRedirectedFrom()
Get the page this view was redirected from.
Definition Article.php:235
showProtectionIndicator()
Show a lock icon above the article body if the page is protected.
Definition Article.php:603
view()
This is the default action of the index.php entry point: just view the page of the given title.
Definition Article.php:456
getRevIdFetched()
Use this to fetch the rev ID used on page views.
Definition Article.php:442
string false $mRedirectUrl
URL to redirect to or false if none.
Definition Article.php:100
isCurrent()
Returns true if the currently-referenced revision is the current edit to this page (and it exists).
Definition Article.php:423
static newFromID( $id)
Constructor from a page id.
Definition Article.php:178
tryFileCache()
checkLastModified returns true if it has taken care of all output to the client that is necessary for...
Definition Article.php:1973
showRedirectedFromHeader()
If this request is a redirect view, send "redirected from" subtitle to the output.
Definition Article.php:1217
getPage()
Get the WikiPage object of this instance.
Definition Article.php:262
protect()
action=protect handler
Definition Article.php:1952
ParserOutput null false $mParserOutput
The ParserOutput generated for viewing the page, initialized by view().
Definition Article.php:113
fetchRevisionRecord()
Fetches the revision to work on.
Definition Article.php:365
DatabaseBlockStore $blockStore
Definition Article.php:131
newPage(Title $title)
Definition Article.php:169
IContextSource null $mContext
The context this Article is executed in.
Definition Article.php:85
RestrictionStore $restrictionStore
Definition Article.php:133
LinkRenderer $linkRenderer
Definition Article.php:122
showDeletedRevisionHeader()
If the revision requested for view is deleted, check permissions.
Definition Article.php:1677
getTitle()
Get the title object of the article.
Definition Article.php:252
showDiffPage()
Show a diff page according to current request variables.
Definition Article.php:1016
getActionOverrides()
Call to WikiPage function for backwards compatibility.
Definition Article.php:2082
WikiPage $mPage
The WikiPage object of this instance.
Definition Article.php:88
isFileCacheable( $mode=HTMLFileCache::MODE_NORMAL)
Check if the page can be cached.
Definition Article.php:2004
setRedirectedFrom(Title $from)
Tell the page view functions that this view was redirected from another page on the wiki.
Definition Article.php:243
adjustDisplayTitle(ParserOutput $pOutput)
Adjust title for pages with displaytitle, -{T|}- or language conversion.
Definition Article.php:1001
showMissingArticle()
Show the error text for a missing article.
Definition Article.php:1513
getRobotPolicy( $action, ?ParserOutput $pOutput=null)
Get the robot policy to be used for the current view.
Definition Article.php:1104
unprotect()
action=unprotect handler (alias)
Definition Article.php:1960
getContext()
Gets the context this Article is executed in.
Definition Article.php:2067
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition Article.php:1924
__construct(Title $title, $oldId=null)
Definition Article.php:147
getOldIDFromRequest()
Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect.
Definition Article.php:297
static getRedirectHeaderHtml(Language $lang, Title $target, $forceKnown=false)
Return the HTML for the top of a redirect page.
Definition Article.php:1910
getParserOptions()
Get parser options suitable for rendering the primary article wikitext.
Definition Article.php:2045
static newFromTitle( $title, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition Article.php:190
Special handling for category description pages.
Rendering of file description pages.
Definition ImagePage.php:45
Handles the page protection UI and backend.
Service for creating WikiPage objects.
Base representation for an editable wiki page.
Definition WikiPage.php:93
getTitle()
Get the title object of the article.
Definition WikiPage.php:261
Set options of the Parser.
ParserOutput is a rendering of a Content object or a message.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:147
A StatusValue for permission errors.
Utility class for creating and reading rows in the recentchanges table.
Exception raised when the text of a revision is permanently missing or corrupt.
Page revision base class.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Represents a title within MediaWiki.
Definition Title.php:78
Provides access to user options.
UserNameUtils service.
fatal( $message,... $parameters)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:32
Interface for objects which can provide a MediaWiki context on request.
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition Page.php:32
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
Interface for objects representing user identity.
Provide primary and replica IDatabase connections.
element(SerializerNode $parent, SerializerNode $node, $contents)