MediaWiki master
Article.php
Go to the documentation of this file.
1<?php
28use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
56use Wikimedia\IPUtils;
58use Wikimedia\NonSerializable\NonSerializableTrait;
60
70class Article implements Page {
71 use ProtectedHookAccessorTrait;
72 use NonSerializableTrait;
73
79 protected $mContext;
80
82 protected $mPage;
83
88 public $mOldId;
89
91 public $mRedirectedFrom = null;
92
94 public $mRedirectUrl = false;
95
100 private $fetchResult = null;
101
107 public $mParserOutput = null;
108
114 protected $viewIsRenderAction = false;
115
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;
126
128
135 private $mRevisionRecord = null;
136
141 public function __construct( Title $title, $oldId = null ) {
142 $this->mOldId = $oldId;
143 $this->mPage = $this->newPage( $title );
144
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();
157 }
158
163 protected function newPage( Title $title ) {
164 return new WikiPage( $title );
165 }
166
172 public static function newFromID( $id ) {
173 $t = Title::newFromID( $id );
174 return $t === null ? null : new static( $t );
175 }
176
184 public static function newFromTitle( $title, IContextSource $context ): 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() );
188 }
189
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 );
204 }
205 }
206 $page->setContext( $context );
207
208 return $page;
209 }
210
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;
222 }
223
229 public function getRedirectedFrom() {
230 return $this->mRedirectedFrom;
231 }
232
238 public function setRedirectedFrom( Title $from ) {
239 $this->mRedirectedFrom = $from;
240 }
241
247 public function getTitle() {
248 return $this->mPage->getTitle();
249 }
250
257 public function getPage() {
258 return $this->mPage;
259 }
260
261 public function clear() {
262 $this->mRedirectedFrom = null; # Title object if set
263 $this->mRedirectUrl = false;
264 $this->mRevisionRecord = null;
265 $this->fetchResult = null;
266
267 // TODO hard-deprecate direct access to public fields
268
269 $this->mPage->clear();
270 }
271
279 public function getOldID() {
280 if ( $this->mOldId === null ) {
281 $this->mOldId = $this->getOldIDFromRequest();
282 }
283
284 return $this->mOldId;
285 }
286
292 public function getOldIDFromRequest() {
293 $this->mRedirectUrl = false;
294
295 $request = $this->getContext()->getRequest();
296 $oldid = $request->getIntOrNull( 'oldid' );
297
298 if ( $oldid === null ) {
299 return 0;
300 }
301
302 if ( $oldid !== 0 ) {
303 # Load the given revision and check whether the page is another one.
304 # In that case, update this instance to reflect the change.
305 if ( $oldid === $this->mPage->getLatest() ) {
306 $this->mRevisionRecord = $this->mPage->getRevisionRecord();
307 } else {
308 $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
309 if ( $this->mRevisionRecord !== null ) {
310 $revPageId = $this->mRevisionRecord->getPageId();
311 // Revision title doesn't match the page title given?
312 if ( $this->mPage->getId() !== $revPageId ) {
313 $this->mPage = $this->wikiPageFactory->newFromID( $revPageId );
314 }
315 }
316 }
317 }
318
319 $oldRev = $this->mRevisionRecord;
320 if ( $request->getRawVal( 'direction' ) === 'next' ) {
321 $nextid = 0;
322 if ( $oldRev ) {
323 $nextRev = $this->revisionStore->getNextRevision( $oldRev );
324 if ( $nextRev ) {
325 $nextid = $nextRev->getId();
326 }
327 }
328 if ( $nextid ) {
329 $oldid = $nextid;
330 $this->mRevisionRecord = null;
331 } else {
332 $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' );
333 }
334 } elseif ( $request->getRawVal( 'direction' ) === 'prev' ) {
335 $previd = 0;
336 if ( $oldRev ) {
337 $prevRev = $this->revisionStore->getPreviousRevision( $oldRev );
338 if ( $prevRev ) {
339 $previd = $prevRev->getId();
340 }
341 }
342 if ( $previd ) {
343 $oldid = $previd;
344 $this->mRevisionRecord = null;
345 }
346 }
347
348 return $oldid;
349 }
350
360 public function fetchRevisionRecord() {
361 if ( $this->fetchResult ) {
362 return $this->mRevisionRecord;
363 }
364
365 $oldid = $this->getOldID();
366
367 // $this->mRevisionRecord might already be fetched by getOldIDFromRequest()
368 if ( !$this->mRevisionRecord ) {
369 if ( !$oldid ) {
370 $this->mRevisionRecord = $this->mPage->getRevisionRecord();
371
372 if ( !$this->mRevisionRecord ) {
373 wfDebug( __METHOD__ . " failed to find page data for title " .
374 $this->getTitle()->getPrefixedText() );
375
376 // Output for this case is done by showMissingArticle().
377 $this->fetchResult = Status::newFatal( 'noarticletext' );
378 return null;
379 }
380 } else {
381 $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
382
383 if ( !$this->mRevisionRecord ) {
384 wfDebug( __METHOD__ . " failed to load revision, rev_id $oldid" );
385
386 $this->fetchResult = Status::newFatal( $this->getMissingRevisionMsg( $oldid ) );
387 return null;
388 }
389 }
390 }
391
392 if ( !$this->mRevisionRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getContext()->getAuthority() ) ) {
393 wfDebug( __METHOD__ . " failed to retrieve content of revision " . $this->mRevisionRecord->getId() );
394
395 // Output for this case is done by showDeletedRevisionHeader().
396 // title used in wikilinks, should not contain whitespaces
397 $this->fetchResult = new Status;
398 $title = $this->getTitle()->getPrefixedDBkey();
399
400 if ( $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
401 $this->fetchResult->fatal( 'rev-suppressed-text' );
402 } else {
403 $this->fetchResult->fatal( 'rev-deleted-text-permission', $title );
404 }
405
406 return null;
407 }
408
409 $this->fetchResult = Status::newGood( $this->mRevisionRecord );
410 return $this->mRevisionRecord;
411 }
412
418 public function isCurrent() {
419 # If no oldid, this is the current version.
420 if ( $this->getOldID() == 0 ) {
421 return true;
422 }
423
424 return $this->mPage->exists() &&
425 $this->mRevisionRecord &&
426 $this->mRevisionRecord->isCurrent();
427 }
428
437 public function getRevIdFetched() {
438 if ( $this->fetchResult && $this->fetchResult->isOK() ) {
440 $rev = $this->fetchResult->getValue();
441 return $rev->getId();
442 } else {
443 return $this->mPage->getLatest();
444 }
445 }
446
451 public function view() {
452 $context = $this->getContext();
453 $useFileCache = $context->getConfig()->get( MainConfigNames::UseFileCache );
454
455 # Get variables from query string
456 # As side effect this will load the revision and update the title
457 # in a revision ID is passed in the request, so this should remain
458 # the first call of this method even if $oldid is used way below.
459 $oldid = $this->getOldID();
460
461 $authority = $context->getAuthority();
462 # Another check in case getOldID() is altering the title
463 $permissionStatus = PermissionStatus::newEmpty();
464 if ( !$authority
465 ->authorizeRead( 'read', $this->getTitle(), $permissionStatus )
466 ) {
467 wfDebug( __METHOD__ . ": denied on secondary read check" );
468 throw new PermissionsError( 'read', $permissionStatus );
469 }
470
471 $outputPage = $context->getOutput();
472 # getOldID() may as well want us to redirect somewhere else
473 if ( $this->mRedirectUrl ) {
474 $outputPage->redirect( $this->mRedirectUrl );
475 wfDebug( __METHOD__ . ": redirecting due to oldid" );
476
477 return;
478 }
479
480 # If we got diff in the query, we want to see a diff page instead of the article.
481 if ( $context->getRequest()->getCheck( 'diff' ) ) {
482 wfDebug( __METHOD__ . ": showing diff page" );
483 $this->showDiffPage();
484
485 return;
486 }
487
488 $this->showProtectionIndicator();
489
490 # Set page title (may be overridden from ParserOutput if title conversion is enabled or DISPLAYTITLE is used)
491 $outputPage->setPageTitle( Parser::formatPageTitle(
492 str_replace( '_', ' ', $this->getTitle()->getNsText() ),
493 ':',
494 $this->getTitle()->getText()
495 ) );
496
497 $outputPage->setArticleFlag( true );
498 # Allow frames by default
499 $outputPage->setPreventClickjacking( false );
500
501 $parserOptions = $this->getParserOptions();
502
503 $poOptions = [];
504 # Allow extensions to vary parser options used for article rendering
505 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
506 ->onArticleParserOptions( $this, $parserOptions );
507 # Render printable version, use printable version cache
508 if ( $outputPage->isPrintable() ) {
509 $parserOptions->setIsPrintable( true );
510 $poOptions['enableSectionEditLinks'] = false;
511 $outputPage->prependHTML(
512 Html::warningBox(
513 $outputPage->msg( 'printableversion-deprecated-warning' )->escaped()
514 )
515 );
516 } elseif ( $this->viewIsRenderAction || !$this->isCurrent() ||
517 !$authority->probablyCan( 'edit', $this->getTitle() )
518 ) {
519 $poOptions['enableSectionEditLinks'] = false;
520 }
521
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 );
530
531 return;
532 }
533 }
534
535 $this->showRedirectedFromHeader();
536 $this->showNamespaceHeader();
537
538 if ( $this->viewIsRenderAction ) {
539 $poOptions += [ 'absoluteURLs' => true ];
540 }
541 $poOptions += [ 'includeDebugInfo' => true ];
542
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() );
549 }
550
551 if ( !$continue ) {
552 return;
553 }
554
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() );
563 }
564 }
565
566 # Use adaptive TTLs for CDN so delayed/failed purges are noticed less often.
567 # This could use getTouched(), but that could be scary for major template edits.
568 $outputPage->adaptCdnTTL( $this->mPage->getTimestamp(), ExpirationAwareness::TTL_DAY );
569
570 $this->showViewFooter();
571 $this->mPage->doViewUpdates( $authority, $oldid, $this->fetchRevisionRecord() );
572
573 # Load the postEdit module if the user just saved this revision
574 # See also EditPage::setPostEditCookie
575 $request = $context->getRequest();
576 $cookieKey = EditPage::POST_EDIT_COOKIE_KEY_PREFIX . $this->getRevIdFetched();
577 $postEdit = $request->getCookie( $cookieKey );
578 if ( $postEdit ) {
579 # Clear the cookie. This also prevents caching of the response.
580 $request->response()->clearCookie( $cookieKey );
581 $outputPage->addJsConfigVars( 'wgPostEdit', $postEdit );
582 $outputPage->addModules( 'mediawiki.action.view.postEdit' ); // FIXME: test this
583 if ( $this->getContext()->getConfig()->get( MainConfigNames::EnableEditRecovery )
584 && $this->userOptionsLookup->getOption( $this->getContext()->getUser(), 'editrecovery' )
585 ) {
586 $outputPage->addModules( 'mediawiki.editRecovery.postEdit' );
587 }
588 }
589 }
590
594 public function showProtectionIndicator(): void {
595 $title = $this->getTitle();
596 $context = $this->getContext();
597 $outputPage = $context->getOutput();
598
599 $protectionIndicatorsAreEnabled = $context->getConfig()
600 ->get( MainConfigNames::EnableProtectionIndicators );
601
602 if ( !$protectionIndicatorsAreEnabled || $title->isMainPage() ) {
603 return;
604 }
605
606 $protection = $this->restrictionStore->getRestrictions( $title, 'edit' );
607
608 $cascadeProtection = $this->restrictionStore->getCascadeProtectionSources( $title )[1];
609
610 $isCascadeProtected = array_key_exists( 'edit', $cascadeProtection );
611
612 if ( !$protection && !$isCascadeProtected ) {
613 return;
614 }
615
616 if ( $isCascadeProtected ) {
617 // Cascade-protected pages are protected at the sysop level. So it
618 // should not matter if we take the protection level of the first
619 // or last page that is being cascaded to the current page.
620 $protectionLevel = $cascadeProtection['edit'][0];
621 } else {
622 $protectionLevel = $protection[0];
623 }
624
625 // Protection levels are stored in the database as plain text, but
626 // they are expected to be valid protection levels. So we should be able to
627 // safely use them. However phan thinks this could be a XSS problem so we
628 // are being paranoid and escaping them once more.
629 $protectionLevel = htmlspecialchars( $protectionLevel );
630
631 $protectionExpiry = $this->restrictionStore->getRestrictionExpiry( $title, 'edit' );
632 $formattedProtectionExpiry = $context->getLanguage()
633 ->formatExpiry( $protectionExpiry ?? '' );
634
635 $protectionMsg = 'protection-indicator-title';
636 if ( $protectionExpiry === 'infinity' || !$protectionExpiry ) {
637 $protectionMsg .= '-infinity';
638 }
639
640 // Potential values: 'protection-sysop', 'protection-autoconfirmed',
641 // 'protection-sysop-cascade' etc.
642 // If the wiki has more protection levels, the additional ids that get
643 // added take the form 'protection-<protectionLevel>' and
644 // 'protection-<protectionLevel>-cascade'.
645 $protectionIndicatorId = 'protection-' . $protectionLevel;
646 $protectionIndicatorId .= ( $isCascadeProtected ? '-cascade' : '' );
647
648 // Messages 'protection-indicator-title', 'protection-indicator-title-infinity'
649 $protectionMsg = $outputPage->msg( $protectionMsg, $protectionLevel, $formattedProtectionExpiry )->text();
650
651 // Use a trick similar to the one used in Action::addHelpLink() to allow wikis
652 // to customize where the help link points to.
653 $protectionHelpLink = $outputPage->msg( $protectionIndicatorId . '-helppage' );
654 if ( $protectionHelpLink->isDisabled() ) {
655 $protectionHelpLink = 'https://mediawiki.org/wiki/Special:MyLanguage/Help:Protection';
656 } else {
657 $protectionHelpLink = $protectionHelpLink->text();
658 }
659
660 $outputPage->setIndicators( [
661 $protectionIndicatorId => Html::rawElement( 'a', [
662 'class' => 'mw-protection-indicator-icon--lock',
663 'title' => $protectionMsg,
664 'href' => $protectionHelpLink
665 ],
666 // Screen reader-only text describing the same thing as
667 // was mentioned in the title attribute.
668 Html::element( 'span', [], $protectionMsg ) )
669 ] );
670
671 $outputPage->addModuleStyles( 'mediawiki.protectionIndicators.styles' );
672 }
673
686 private function generateContentOutput(
687 Authority $performer,
688 ParserOptions $parserOptions,
689 int $oldid,
690 OutputPage $outputPage,
691 array $textOptions
692 ): bool {
693 # Should the parser cache be used?
694 $useParserCache = true;
695 $pOutput = null;
696 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
697
698 // NOTE: $outputDone and $useParserCache may be changed by the hook
699 $this->getHookRunner()->onArticleViewHeader( $this, $outputDone, $useParserCache );
700 if ( $outputDone ) {
701 if ( $outputDone instanceof ParserOutput ) {
702 $pOutput = $outputDone;
703 }
704
705 if ( $pOutput ) {
706 $this->doOutputMetaData( $pOutput, $outputPage );
707 }
708 return true;
709 }
710
711 // Early abort if the page doesn't exist
712 if ( !$this->mPage->exists() ) {
713 wfDebug( __METHOD__ . ": showing missing article" );
714 $this->showMissingArticle();
715 $this->mPage->doViewUpdates( $performer );
716 return false; // skip all further output to OutputPage
717 }
718
719 // Try the latest parser cache
720 // NOTE: try latest-revision cache first to avoid loading revision.
721 if ( $useParserCache && !$oldid ) {
722 $pOutput = $parserOutputAccess->getCachedParserOutput(
723 $this->getPage(),
724 $parserOptions,
725 null,
726 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked
727 );
728
729 if ( $pOutput ) {
730 $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
731 $this->doOutputMetaData( $pOutput, $outputPage );
732 return true;
733 }
734 }
735
736 $rev = $this->fetchRevisionRecord();
737 if ( !$this->fetchResult->isOK() ) {
738 $this->showViewError( $this->fetchResult->getWikiText(
739 false, false, $this->getContext()->getLanguage()
740 ) );
741 return true;
742 }
743
744 # Are we looking at an old revision
745 if ( $oldid ) {
746 $this->setOldSubtitle( $oldid );
747
748 if ( !$this->showDeletedRevisionHeader() ) {
749 wfDebug( __METHOD__ . ": cannot view deleted revision" );
750 return false; // skip all further output to OutputPage
751 }
752
753 // Try the old revision parser cache
754 // NOTE: Repeating cache check for old revision to avoid fetching $rev
755 // before it's absolutely necessary.
756 if ( $useParserCache ) {
757 $pOutput = $parserOutputAccess->getCachedParserOutput(
758 $this->getPage(),
759 $parserOptions,
760 $rev,
761 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked in fetchRevisionRecord
762 );
763
764 if ( $pOutput ) {
765 $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
766 $this->doOutputMetaData( $pOutput, $outputPage );
767 return true;
768 }
769 }
770 }
771
772 # Ensure that UI elements requiring revision ID have
773 # the correct version information. (This may be overwritten after creation of ParserOutput)
774 $outputPage->setRevisionId( $this->getRevIdFetched() );
775 $outputPage->setRevisionIsCurrent( $rev->isCurrent() );
776 # Preload timestamp to avoid a DB hit
777 $outputPage->setRevisionTimestamp( $rev->getTimestamp() );
778
779 # Pages containing custom CSS or JavaScript get special treatment
780 if ( $this->getTitle()->isSiteConfigPage() || $this->getTitle()->isUserConfigPage() ) {
781 $dir = $this->getContext()->getLanguage()->getDir();
782 $lang = $this->getContext()->getLanguage()->getHtmlCode();
783
784 $outputPage->wrapWikiMsg(
785 "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
786 'clearyourcache'
787 );
788 $outputPage->addModuleStyles( 'mediawiki.action.styles' );
789 } elseif ( !$this->getHookRunner()->onArticleRevisionViewCustom(
790 $rev,
791 $this->getTitle(),
792 $oldid,
793 $outputPage )
794 ) {
795 // NOTE: sync with hooks called in DifferenceEngine::renderNewRevision()
796 // Allow extensions do their own custom view for certain pages
797 $this->doOutputMetaData( $pOutput, $outputPage );
798 return true;
799 }
800
801 # Run the parse, protected by a pool counter
802 wfDebug( __METHOD__ . ": doing uncached parse" );
803
804 $opt = 0;
805
806 // we already checked the cache in case 2, don't check again.
807 $opt |= ParserOutputAccess::OPT_NO_CHECK_CACHE;
808
809 // we already checked in fetchRevisionRecord()
810 $opt |= ParserOutputAccess::OPT_NO_AUDIENCE_CHECK;
811
812 // enable stampede protection and allow stale content
813 $opt |= ParserOutputAccess::OPT_FOR_ARTICLE_VIEW;
814
815 // Attempt to trigger WikiPage::triggerOpportunisticLinksUpdate
816 // Ideally this should not be the responsibility of the ParserCache to control this.
817 // See https://phabricator.wikimedia.org/T329842#8816557 for more context.
818 $opt |= ParserOutputAccess::OPT_LINKS_UPDATE;
819
820 if ( !$rev->getId() || !$useParserCache ) {
821 // fake revision or uncacheable options
822 $opt |= ParserOutputAccess::OPT_NO_CACHE;
823 }
824
825 $renderStatus = $parserOutputAccess->getParserOutput(
826 $this->getPage(),
827 $parserOptions,
828 $rev,
829 $opt
830 );
831
832 // T327164: If parsoid cache warming is enabled, we want to ensure that the page
833 // the user is currently looking at has a cached parsoid rendering, in case they
834 // open visual editor. The cache entry would typically be missing if it has expired
835 // from the cache or it was invalidated by RefreshLinksJob. When "traditional"
836 // parser output has been invalidated by RefreshLinksJob, we will render it on
837 // the fly when a user requests the page, and thereby populate the cache again,
838 // per the code above.
839 // The code below is intended to do the same for parsoid output, but asynchronously
840 // in a job, so the user does not have to wait.
841 // Note that we get here if the traditional parser output was missing from the cache.
842 // We do not check if the parsoid output is present in the cache, because that check
843 // takes time. The assumption is that if we have traditional parser output
844 // cached, we probably also have parsoid output cached.
845 // So we leave it to ParsoidCachePrewarmJob to determine whether or not parsing is
846 // needed.
847 if ( $oldid === 0 || $oldid === $this->getPage()->getLatest() ) {
848 $parsoidCacheWarmingEnabled = $this->getContext()->getConfig()
849 ->get( MainConfigNames::ParsoidCacheConfig )['WarmParsoidParserCache'];
850
851 if ( $parsoidCacheWarmingEnabled ) {
852 $parsoidJobSpec = ParsoidCachePrewarmJob::newSpec(
853 $rev->getId(),
854 $this->getPage()->toPageRecord(),
855 [ 'causeAction' => 'view' ]
856 );
857 $this->jobQueueGroup->lazyPush( $parsoidJobSpec );
858 }
859 }
860
861 $this->doOutputFromRenderStatus(
862 $rev,
863 $renderStatus,
864 $outputPage,
865 $textOptions
866 );
867
868 if ( !$renderStatus->isOK() ) {
869 return true;
870 }
871
872 $pOutput = $renderStatus->getValue();
873 $this->doOutputMetaData( $pOutput, $outputPage );
874 return true;
875 }
876
881 private function doOutputMetaData( ?ParserOutput $pOutput, OutputPage $outputPage ) {
882 # Adjust title for main page & pages with displaytitle
883 if ( $pOutput ) {
884 $this->adjustDisplayTitle( $pOutput );
885
886 // It would be nice to automatically set this during the first call
887 // to OutputPage::addParserOutputMetadata, but we can't because doing
888 // so would break non-pageview actions where OutputPage::getContLangForJS
889 // has different requirements.
890 $pageLang = $pOutput->getLanguage();
891 if ( $pageLang ) {
892 $outputPage->setContentLangForJS( $pageLang );
893 }
894 }
895
896 # Check for any __NOINDEX__ tags on the page using $pOutput
897 $policy = $this->getRobotPolicy( 'view', $pOutput ?: null );
898 $outputPage->setIndexPolicy( $policy['index'] );
899 $outputPage->setFollowPolicy( $policy['follow'] ); // FIXME: test this
900
901 $this->mParserOutput = $pOutput;
902 }
903
909 private function doOutputFromParserCache(
910 ParserOutput $pOutput,
911 OutputPage $outputPage,
912 array $textOptions
913 ) {
914 # Ensure that UI elements requiring revision ID have
915 # the correct version information.
916 $oldid = $pOutput->getCacheRevisionId() ?? $this->getRevIdFetched();
917 $outputPage->setRevisionId( $oldid );
918 $outputPage->setRevisionIsCurrent( $oldid === $this->mPage->getLatest() );
919 $outputPage->addParserOutput( $pOutput, $textOptions );
920 # Preload timestamp to avoid a DB hit
921 $cachedTimestamp = $pOutput->getRevisionTimestamp();
922 if ( $cachedTimestamp !== null ) {
923 $outputPage->setRevisionTimestamp( $cachedTimestamp );
924 $this->mPage->setTimestamp( $cachedTimestamp );
925 }
926 }
927
934 private function doOutputFromRenderStatus(
935 RevisionRecord $rev,
936 Status $renderStatus,
937 OutputPage $outputPage,
938 array $textOptions
939 ) {
940 $context = $this->getContext();
941 if ( !$renderStatus->isOK() ) {
942 $this->showViewError( $renderStatus->getWikiText(
943 false, 'view-pool-error', $context->getLanguage()
944 ) );
945 return;
946 }
947
948 $pOutput = $renderStatus->getValue();
949
950 // Cache stale ParserOutput object with a short expiry
951 if ( $renderStatus->hasMessage( 'view-pool-dirty-output' ) ) {
952 $outputPage->lowerCdnMaxage( $context->getConfig()->get( MainConfigNames::CdnMaxageStale ) );
953 $outputPage->setLastModified( $pOutput->getCacheTime() );
954 $staleReason = $renderStatus->hasMessage( 'view-pool-contention' )
955 ? $context->msg( 'view-pool-contention' )->escaped()
956 : $context->msg( 'view-pool-timeout' )->escaped();
957 $outputPage->addHTML( "<!-- parser cache is expired, " .
958 "sending anyway due to $staleReason-->\n" );
959
960 // Ensure OutputPage knowns the id from the dirty cache, but keep the current flag (T341013)
961 $cachedId = $pOutput->getCacheRevisionId();
962 if ( $cachedId !== null ) {
963 $outputPage->setRevisionId( $cachedId );
964 $outputPage->setRevisionTimestamp( $pOutput->getTimestamp() );
965 }
966 }
967
968 $outputPage->addParserOutput( $pOutput, $textOptions );
969
970 if ( $this->getRevisionRedirectTarget( $rev ) ) {
971 $outputPage->addSubtitle( "<span id=\"redirectsub\">" .
972 $context->msg( 'redirectpagesub' )->parse() . "</span>" );
973 }
974 }
975
980 private function getRevisionRedirectTarget( RevisionRecord $revision ) {
981 // TODO: find a *good* place for the code that determines the redirect target for
982 // a given revision!
983 // NOTE: Use main slot content. Compare code in DerivedPageDataUpdater::revisionIsRedirect.
984 $content = $revision->getContent( SlotRecord::MAIN );
985 return $content ? $content->getRedirectTarget() : null;
986 }
987
992 public function adjustDisplayTitle( ParserOutput $pOutput ) {
993 $out = $this->getContext()->getOutput();
994
995 # Adjust the title if it was set by displaytitle, -{T|}- or language conversion
996 $titleText = $pOutput->getTitleText();
997 if ( strval( $titleText ) !== '' ) {
998 $out->setPageTitle( $titleText );
999 $out->setDisplayTitle( $titleText );
1000 }
1001 }
1002
1007 protected function showDiffPage() {
1008 $context = $this->getContext();
1009 $outputPage = $context->getOutput();
1010 $outputPage->addBodyClasses( 'mw-article-diff' );
1011 $request = $context->getRequest();
1012 $diff = $request->getVal( 'diff' );
1013 $rcid = $request->getInt( 'rcid' );
1014 $purge = $request->getRawVal( 'action' ) === 'purge';
1015 $unhide = $request->getInt( 'unhide' ) === 1;
1016 $oldid = $this->getOldID();
1017
1018 $rev = $this->fetchRevisionRecord();
1019
1020 if ( !$rev ) {
1021 // T213621: $rev maybe null due to either lack of permission to view the
1022 // revision or actually not existing. So let's try loading it from the id
1023 $rev = $this->revisionStore->getRevisionById( $oldid );
1024 if ( $rev ) {
1025 // Revision exists but $user lacks permission to diff it.
1026 // Do nothing here.
1027 // The $rev will later be used to create standard diff elements however.
1028 } else {
1029 $outputPage->setPageTitleMsg( $context->msg( 'errorpagetitle' ) );
1030 $msg = $context->msg( 'difference-missing-revision' )
1031 ->params( $oldid )
1032 ->numParams( 1 )
1033 ->parseAsBlock();
1034 $outputPage->addHTML( $msg );
1035 return;
1036 }
1037 }
1038
1039 $services = MediaWikiServices::getInstance();
1040
1041 $contentHandler = $services
1042 ->getContentHandlerFactory()
1043 ->getContentHandler(
1044 $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
1045 );
1046 $de = $contentHandler->createDifferenceEngine(
1047 $context,
1048 $oldid,
1049 $diff,
1050 $rcid,
1051 $purge,
1052 $unhide
1053 );
1054
1055 $diffType = $request->getVal( 'diff-type' );
1056
1057 if ( $diffType === null ) {
1058 $diffType = $this->userOptionsLookup
1059 ->getOption( $context->getUser(), 'diff-type' );
1060 } else {
1061 $de->setExtraQueryParams( [ 'diff-type' => $diffType ] );
1062 }
1063
1064 $de->setSlotDiffOptions( [
1065 'diff-type' => $diffType,
1066 'expand-url' => $this->viewIsRenderAction,
1067 'inline-toggle' => true,
1068 ] );
1069 $de->showDiffPage( $this->isDiffOnlyView() );
1070
1071 // Run view updates for the newer revision being diffed (and shown
1072 // below the diff if not diffOnly).
1073 [ , $new ] = $de->mapDiffPrevNext( $oldid, $diff );
1074 // New can be false, convert it to 0 - this conveniently means the latest revision
1075 $this->mPage->doViewUpdates( $context->getAuthority(), (int)$new );
1076
1077 // Add link to help page; see T321569
1078 $context->getOutput()->addHelpLink( 'Help:Diff' );
1079 }
1080
1081 protected function isDiffOnlyView() {
1082 return $this->getContext()->getRequest()->getBool(
1083 'diffonly',
1084 $this->userOptionsLookup->getBoolOption( $this->getContext()->getUser(), 'diffonly' )
1085 );
1086 }
1087
1095 public function getRobotPolicy( $action, ParserOutput $pOutput = null ) {
1096 $context = $this->getContext();
1097 $mainConfig = $context->getConfig();
1098 $articleRobotPolicies = $mainConfig->get( MainConfigNames::ArticleRobotPolicies );
1099 $namespaceRobotPolicies = $mainConfig->get( MainConfigNames::NamespaceRobotPolicies );
1100 $defaultRobotPolicy = $mainConfig->get( MainConfigNames::DefaultRobotPolicy );
1101 $title = $this->getTitle();
1102 $ns = $title->getNamespace();
1103
1104 # Don't index user and user talk pages for blocked users (T13443)
1105 if ( $ns === NS_USER || $ns === NS_USER_TALK ) {
1106 $specificTarget = null;
1107 $vagueTarget = null;
1108 $titleText = $title->getText();
1109 if ( IPUtils::isValid( $titleText ) ) {
1110 $vagueTarget = $titleText;
1111 } else {
1112 $specificTarget = $title->getRootText();
1113 }
1114 if ( $this->blockStore->newFromTarget( $specificTarget, $vagueTarget ) instanceof DatabaseBlock ) {
1115 return [
1116 'index' => 'noindex',
1117 'follow' => 'nofollow'
1118 ];
1119 }
1120 }
1121
1122 if ( $this->mPage->getId() === 0 || $this->getOldID() ) {
1123 # Non-articles (special pages etc), and old revisions
1124 return [
1125 'index' => 'noindex',
1126 'follow' => 'nofollow'
1127 ];
1128 } elseif ( $context->getOutput()->isPrintable() ) {
1129 # Discourage indexing of printable versions, but encourage following
1130 return [
1131 'index' => 'noindex',
1132 'follow' => 'follow'
1133 ];
1134 } elseif ( $context->getRequest()->getInt( 'curid' ) ) {
1135 # For ?curid=x urls, disallow indexing
1136 return [
1137 'index' => 'noindex',
1138 'follow' => 'follow'
1139 ];
1140 }
1141
1142 # Otherwise, construct the policy based on the various config variables.
1143 $policy = self::formatRobotPolicy( $defaultRobotPolicy );
1144
1145 if ( isset( $namespaceRobotPolicies[$ns] ) ) {
1146 # Honour customised robot policies for this namespace
1147 $policy = array_merge(
1148 $policy,
1149 self::formatRobotPolicy( $namespaceRobotPolicies[$ns] )
1150 );
1151 }
1152 if ( $title->canUseNoindex() && $pOutput && $pOutput->getIndexPolicy() ) {
1153 # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates
1154 # a final check that we have really got the parser output.
1155 $policy = array_merge(
1156 $policy,
1157 [ 'index' => $pOutput->getIndexPolicy() ]
1158 );
1159 }
1160
1161 if ( isset( $articleRobotPolicies[$title->getPrefixedText()] ) ) {
1162 # (T16900) site config can override user-defined __INDEX__ or __NOINDEX__
1163 $policy = array_merge(
1164 $policy,
1165 self::formatRobotPolicy( $articleRobotPolicies[$title->getPrefixedText()] )
1166 );
1167 }
1168
1169 return $policy;
1170 }
1171
1179 public static function formatRobotPolicy( $policy ) {
1180 if ( is_array( $policy ) ) {
1181 return $policy;
1182 } elseif ( !$policy ) {
1183 return [];
1184 }
1185
1186 $arr = [];
1187 foreach ( explode( ',', $policy ) as $var ) {
1188 $var = trim( $var );
1189 if ( $var === 'index' || $var === 'noindex' ) {
1190 $arr['index'] = $var;
1191 } elseif ( $var === 'follow' || $var === 'nofollow' ) {
1192 $arr['follow'] = $var;
1193 }
1194 }
1195
1196 return $arr;
1197 }
1198
1206 public function showRedirectedFromHeader() {
1207 $context = $this->getContext();
1208 $redirectSources = $context->getConfig()->get( MainConfigNames::RedirectSources );
1209 $outputPage = $context->getOutput();
1210 $request = $context->getRequest();
1211 $rdfrom = $request->getVal( 'rdfrom' );
1212
1213 // Construct a URL for the current page view, but with the target title
1214 $query = $request->getValues();
1215 unset( $query['rdfrom'] );
1216 unset( $query['title'] );
1217 if ( $this->getTitle()->isRedirect() ) {
1218 // Prevent double redirects
1219 $query['redirect'] = 'no';
1220 }
1221 $redirectTargetUrl = $this->getTitle()->getLinkURL( $query );
1222
1223 if ( isset( $this->mRedirectedFrom ) ) {
1224 // This is an internally redirected page view.
1225 // We'll need a backlink to the source page for navigation.
1226 if ( $this->getHookRunner()->onArticleViewRedirect( $this ) ) {
1227 $redir = $this->linkRenderer->makeKnownLink(
1228 $this->mRedirectedFrom,
1229 null,
1230 [],
1231 [ 'redirect' => 'no' ]
1232 );
1233
1234 $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1235 $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1236 . "</span>" );
1237
1238 // Add the script to update the displayed URL and
1239 // set the fragment if one was specified in the redirect
1240 $outputPage->addJsConfigVars( [
1241 'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1242 ] );
1243 $outputPage->addModules( 'mediawiki.action.view.redirect' );
1244
1245 // Add a <link rel="canonical"> tag
1246 $outputPage->setCanonicalUrl( $this->getTitle()->getCanonicalURL() );
1247
1248 // Tell the output object that the user arrived at this article through a redirect
1249 $outputPage->setRedirectedFrom( $this->mRedirectedFrom );
1250
1251 return true;
1252 }
1253 } elseif ( $rdfrom ) {
1254 // This is an externally redirected view, from some other wiki.
1255 // If it was reported from a trusted site, supply a backlink.
1256 if ( $redirectSources && preg_match( $redirectSources, $rdfrom ) ) {
1257 $redir = $this->linkRenderer->makeExternalLink( $rdfrom, $rdfrom, $this->getTitle() );
1258 $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1259 $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1260 . "</span>" );
1261
1262 // Add the script to update the displayed URL
1263 $outputPage->addJsConfigVars( [
1264 'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1265 ] );
1266 $outputPage->addModules( 'mediawiki.action.view.redirect' );
1267
1268 return true;
1269 }
1270 }
1271
1272 return false;
1273 }
1274
1279 public function showNamespaceHeader() {
1280 if ( $this->getTitle()->isTalkPage() && !$this->getContext()->msg( 'talkpageheader' )->isDisabled() ) {
1281 $this->getContext()->getOutput()->wrapWikiMsg(
1282 "<div class=\"mw-talkpageheader\">\n$1\n</div>",
1283 [ 'talkpageheader' ]
1284 );
1285 }
1286 }
1287
1291 public function showViewFooter() {
1292 # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
1293 if ( $this->getTitle()->getNamespace() === NS_USER_TALK
1294 && IPUtils::isValid( $this->getTitle()->getText() )
1295 ) {
1296 $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' );
1297 }
1298
1299 // Show a footer allowing the user to patrol the shown revision or page if possible
1300 $patrolFooterShown = $this->showPatrolFooter();
1301
1302 $this->getHookRunner()->onArticleViewFooter( $this, $patrolFooterShown );
1303 }
1304
1315 public function showPatrolFooter() {
1316 $context = $this->getContext();
1317 $mainConfig = $context->getConfig();
1318 $useNPPatrol = $mainConfig->get( MainConfigNames::UseNPPatrol );
1319 $useRCPatrol = $mainConfig->get( MainConfigNames::UseRCPatrol );
1320 $useFilePatrol = $mainConfig->get( MainConfigNames::UseFilePatrol );
1321 // Allow hooks to decide whether to not output this at all
1322 if ( !$this->getHookRunner()->onArticleShowPatrolFooter( $this ) ) {
1323 return false;
1324 }
1325
1326 $outputPage = $context->getOutput();
1327 $user = $context->getUser();
1328 $title = $this->getTitle();
1329 $rc = false;
1330
1331 if ( !$context->getAuthority()->probablyCan( 'patrol', $title )
1332 || !( $useRCPatrol || $useNPPatrol
1333 || ( $useFilePatrol && $title->inNamespace( NS_FILE ) ) )
1334 ) {
1335 // Patrolling is disabled or the user isn't allowed to
1336 return false;
1337 }
1338
1339 if ( $this->mRevisionRecord
1340 && !RecentChange::isInRCLifespan( $this->mRevisionRecord->getTimestamp(), 21600 )
1341 ) {
1342 // The current revision is already older than what could be in the RC table
1343 // 6h tolerance because the RC might not be cleaned out regularly
1344 return false;
1345 }
1346
1347 // Check for cached results
1348 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1349 $key = $cache->makeKey( 'unpatrollable-page', $title->getArticleID() );
1350 if ( $cache->get( $key ) ) {
1351 return false;
1352 }
1353
1354 $dbr = $this->dbProvider->getReplicaDatabase();
1355 $oldestRevisionRow = $dbr->newSelectQueryBuilder()
1356 ->select( [ 'rev_id', 'rev_timestamp' ] )
1357 ->from( 'revision' )
1358 ->where( [ 'rev_page' => $title->getArticleID() ] )
1359 ->orderBy( [ 'rev_timestamp', 'rev_id' ] )
1360 ->caller( __METHOD__ )->fetchRow();
1361 $oldestRevisionTimestamp = $oldestRevisionRow ? $oldestRevisionRow->rev_timestamp : false;
1362
1363 // New page patrol: Get the timestamp of the oldest revision which
1364 // the revision table holds for the given page. Then we look
1365 // whether it's within the RC lifespan and if it is, we try
1366 // to get the recentchanges row belonging to that entry.
1367 $recentPageCreation = false;
1368 if ( $oldestRevisionTimestamp
1369 && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 )
1370 ) {
1371 // 6h tolerance because the RC might not be cleaned out regularly
1372 $recentPageCreation = true;
1373 $rc = RecentChange::newFromConds(
1374 [
1375 'rc_this_oldid' => intval( $oldestRevisionRow->rev_id ),
1376 // Avoid selecting a categorization entry
1377 'rc_type' => RC_NEW,
1378 ],
1379 __METHOD__
1380 );
1381 if ( $rc ) {
1382 // Use generic patrol message for new pages
1383 $markPatrolledMsg = $context->msg( 'markaspatrolledtext' );
1384 }
1385 }
1386
1387 // File patrol: Get the timestamp of the latest upload for this page,
1388 // check whether it is within the RC lifespan and if it is, we try
1389 // to get the recentchanges row belonging to that entry
1390 // (with rc_type = RC_LOG, rc_log_type = upload).
1391 $recentFileUpload = false;
1392 if ( ( !$rc || $rc->getAttribute( 'rc_patrolled' ) ) && $useFilePatrol
1393 && $title->getNamespace() === NS_FILE ) {
1394 // Retrieve timestamp from the current file (lastest upload)
1395 $newestUploadTimestamp = $dbr->newSelectQueryBuilder()
1396 ->select( 'img_timestamp' )
1397 ->from( 'image' )
1398 ->where( [ 'img_name' => $title->getDBkey() ] )
1399 ->caller( __METHOD__ )->fetchField();
1400 if ( $newestUploadTimestamp
1401 && RecentChange::isInRCLifespan( $newestUploadTimestamp, 21600 )
1402 ) {
1403 // 6h tolerance because the RC might not be cleaned out regularly
1404 $recentFileUpload = true;
1405 $rc = RecentChange::newFromConds(
1406 [
1407 'rc_type' => RC_LOG,
1408 'rc_log_type' => 'upload',
1409 'rc_timestamp' => $newestUploadTimestamp,
1410 'rc_namespace' => NS_FILE,
1411 'rc_cur_id' => $title->getArticleID()
1412 ],
1413 __METHOD__
1414 );
1415 if ( $rc ) {
1416 // Use patrol message specific to files
1417 $markPatrolledMsg = $context->msg( 'markaspatrolledtext-file' );
1418 }
1419 }
1420 }
1421
1422 if ( !$recentPageCreation && !$recentFileUpload ) {
1423 // Page creation and latest upload (for files) is too old to be in RC
1424
1425 // We definitely can't patrol so cache the information
1426 // When a new file version is uploaded, the cache is cleared
1427 $cache->set( $key, '1' );
1428
1429 return false;
1430 }
1431
1432 if ( !$rc ) {
1433 // Don't cache: This can be hit if the page gets accessed very fast after
1434 // its creation / latest upload or in case we have high replica DB lag. In case
1435 // the revision is too old, we will already return above.
1436 return false;
1437 }
1438
1439 if ( $rc->getAttribute( 'rc_patrolled' ) ) {
1440 // Patrolled RC entry around
1441
1442 // Cache the information we gathered above in case we can't patrol
1443 // Don't cache in case we can patrol as this could change
1444 $cache->set( $key, '1' );
1445
1446 return false;
1447 }
1448
1449 if ( $rc->getPerformerIdentity()->equals( $user ) ) {
1450 // Don't show a patrol link for own creations/uploads. If the user could
1451 // patrol them, they already would be patrolled
1452 return false;
1453 }
1454
1455 $outputPage->setPreventClickjacking( true );
1456 if ( $context->getAuthority()->isAllowed( 'writeapi' ) ) {
1457 $outputPage->addModules( 'mediawiki.misc-authed-curate' );
1458 }
1459
1460 $link = $this->linkRenderer->makeKnownLink(
1461 $title,
1462 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $markPatrolledMsg is always set when $rc is set
1463 $markPatrolledMsg->text(),
1464 [],
1465 [
1466 'action' => 'markpatrolled',
1467 'rcid' => $rc->getAttribute( 'rc_id' ),
1468 ]
1469 );
1470
1471 $outputPage->addModuleStyles( 'mediawiki.action.styles' );
1472 $outputPage->addHTML(
1473 "<div class='patrollink' data-mw='interface'>" .
1474 $context->msg( 'markaspatrolledlink' )->rawParams( $link )->escaped() .
1475 '</div>'
1476 );
1477
1478 return true;
1479 }
1480
1487 public static function purgePatrolFooterCache( $articleID ) {
1488 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1489 $cache->delete( $cache->makeKey( 'unpatrollable-page', $articleID ) );
1490 }
1491
1496 public function showMissingArticle() {
1497 $context = $this->getContext();
1498 $send404Code = $context->getConfig()->get( MainConfigNames::Send404Code );
1499
1500 $outputPage = $context->getOutput();
1501 // Whether the page is a root user page of an existing user (but not a subpage)
1502 $validUserPage = false;
1503
1504 $title = $this->getTitle();
1505
1506 $services = MediaWikiServices::getInstance();
1507
1508 $contextUser = $context->getUser();
1509
1510 # Show info in user (talk) namespace. Does the user exist? Is he blocked?
1511 if ( $title->getNamespace() === NS_USER
1512 || $title->getNamespace() === NS_USER_TALK
1513 ) {
1514 $rootPart = $title->getRootText();
1515 $user = User::newFromName( $rootPart, false /* allow IP users */ );
1516 $ip = $this->userNameUtils->isIP( $rootPart );
1517 $block = $this->blockStore->newFromTarget( $user, $user );
1518
1519 if ( $user && $user->isRegistered() && $user->isHidden() &&
1520 !$context->getAuthority()->isAllowed( 'hideuser' )
1521 ) {
1522 // T120883 if the user is hidden and the viewer cannot see hidden
1523 // users, pretend like it does not exist at all.
1524 $user = false;
1525 }
1526
1527 if ( !( $user && $user->isRegistered() ) && !$ip ) {
1528 // User does not exist
1529 $outputPage->addHTML( Html::warningBox(
1530 $context->msg( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) )->parse(),
1531 'mw-userpage-userdoesnotexist'
1532 ) );
1533
1534 // Show renameuser log extract
1535 LogEventsList::showLogExtract(
1536 $outputPage,
1537 'renameuser',
1538 Title::makeTitleSafe( NS_USER, $rootPart ),
1539 '',
1540 [
1541 'lim' => 10,
1542 'showIfEmpty' => false,
1543 'msgKey' => [ 'renameuser-renamed-notice', $title->getBaseText() ]
1544 ]
1545 );
1546 } elseif (
1547 $block !== null &&
1548 $block->getType() != DatabaseBlock::TYPE_AUTO &&
1549 (
1550 $block->isSitewide() ||
1551 $services->getPermissionManager()->isBlockedFrom( $user, $title, true )
1552 )
1553 ) {
1554 // Show log extract if the user is sitewide blocked or is partially
1555 // blocked and not allowed to edit their user page or user talk page
1556 LogEventsList::showLogExtract(
1557 $outputPage,
1558 'block',
1559 $services->getNamespaceInfo()->getCanonicalName( NS_USER ) . ':' .
1560 $block->getTargetName(),
1561 '',
1562 [
1563 'lim' => 1,
1564 'showIfEmpty' => false,
1565 'msgKey' => [
1566 'blocked-notice-logextract',
1567 $user->getName() # Support GENDER in notice
1568 ]
1569 ]
1570 );
1571 $validUserPage = !$title->isSubpage();
1572 } else {
1573 $validUserPage = !$title->isSubpage();
1574 }
1575 }
1576
1577 $this->getHookRunner()->onShowMissingArticle( $this );
1578
1579 # Show delete and move logs if there were any such events.
1580 # The logging query can DOS the site when bots/crawlers cause 404 floods,
1581 # so be careful showing this. 404 pages must be cheap as they are hard to cache.
1582 $dbCache = MediaWikiServices::getInstance()->getMainObjectStash();
1583 $key = $dbCache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
1584 $isRegistered = $contextUser->isRegistered();
1585 $sessionExists = $context->getRequest()->getSession()->isPersistent();
1586
1587 if ( $isRegistered || $dbCache->get( $key ) || $sessionExists ) {
1588 $logTypes = [ 'delete', 'move', 'protect' ];
1589
1590 $dbr = $this->dbProvider->getReplicaDatabase();
1591
1592 $conds = [ $dbr->expr( 'log_action', '!=', 'revision' ) ];
1593 // Give extensions a chance to hide their (unrelated) log entries
1594 $this->getHookRunner()->onArticle__MissingArticleConditions( $conds, $logTypes );
1595 LogEventsList::showLogExtract(
1596 $outputPage,
1597 $logTypes,
1598 $title,
1599 '',
1600 [
1601 'lim' => 10,
1602 'conds' => $conds,
1603 'showIfEmpty' => false,
1604 'msgKey' => [ $isRegistered || $sessionExists
1605 ? 'moveddeleted-notice'
1606 : 'moveddeleted-notice-recent'
1607 ]
1608 ]
1609 );
1610 }
1611
1612 if ( !$this->mPage->hasViewableContent() && $send404Code && !$validUserPage ) {
1613 // If there's no backing content, send a 404 Not Found
1614 // for better machine handling of broken links.
1615 $context->getRequest()->response()->statusHeader( 404 );
1616 }
1617
1618 // Also apply the robot policy for nonexisting pages (even if a 404 was used)
1619 $policy = $this->getRobotPolicy( 'view' );
1620 $outputPage->setIndexPolicy( $policy['index'] );
1621 $outputPage->setFollowPolicy( $policy['follow'] );
1622
1623 $hookResult = $this->getHookRunner()->onBeforeDisplayNoArticleText( $this );
1624
1625 if ( !$hookResult ) {
1626 return;
1627 }
1628
1629 # Show error message
1630 $oldid = $this->getOldID();
1631 if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) {
1632 $text = $this->getTitle()->getDefaultMessageText() ?? '';
1633 $outputPage->addWikiTextAsContent( $text );
1634 } else {
1635 if ( $oldid ) {
1636 $text = $this->getMissingRevisionMsg( $oldid )->plain();
1637 } elseif ( $context->getAuthority()->probablyCan( 'edit', $title ) ) {
1638 $message = $isRegistered ? 'noarticletext' : 'noarticletextanon';
1639 $text = $context->msg( $message )->plain();
1640 } else {
1641 $text = $context->msg( 'noarticletext-nopermission' )->plain();
1642 }
1643
1644 $dir = $context->getLanguage()->getDir();
1645 $lang = $context->getLanguage()->getHtmlCode();
1646 $outputPage->addWikiTextAsInterface( Xml::openElement( 'div', [
1647 'class' => "noarticletext mw-content-$dir",
1648 'dir' => $dir,
1649 'lang' => $lang,
1650 ] ) . "\n$text\n</div>" );
1651 }
1652 }
1653
1658 private function showViewError( string $errortext ) {
1659 $outputPage = $this->getContext()->getOutput();
1660 $outputPage->setPageTitleMsg( $this->getContext()->msg( 'errorpagetitle' ) );
1661 $outputPage->disableClientCache();
1662 $outputPage->setRobotPolicy( 'noindex,nofollow' );
1663 $outputPage->clearHTML();
1664 $outputPage->addHTML( Html::errorBox( $outputPage->parseAsContent( $errortext ) ) );
1665 }
1666
1673 public function showDeletedRevisionHeader() {
1674 if ( !$this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1675 // Not deleted
1676 return true;
1677 }
1678 $outputPage = $this->getContext()->getOutput();
1679 // Used in wikilinks, should not contain whitespaces
1680 $titleText = $this->getTitle()->getPrefixedDBkey();
1681 // If the user is not allowed to see it...
1682 if ( !$this->mRevisionRecord->userCan(
1683 RevisionRecord::DELETED_TEXT,
1684 $this->getContext()->getAuthority()
1685 ) ) {
1686 $outputPage->addHTML(
1687 Html::warningBox(
1688 $outputPage->msg( 'rev-deleted-text-permission', $titleText )->parse(),
1689 'plainlinks'
1690 )
1691 );
1692
1693 return false;
1694 // If the user needs to confirm that they want to see it...
1695 } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) !== 1 ) {
1696 # Give explanation and add a link to view the revision...
1697 $oldid = intval( $this->getOldID() );
1698 $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" );
1699 $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ?
1700 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
1701 $outputPage->addHTML(
1702 Html::warningBox(
1703 $outputPage->msg( $msg, $link )->parse(),
1704 'plainlinks'
1705 )
1706 );
1707
1708 return false;
1709 // We are allowed to see...
1710 } else {
1711 $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
1712 ? [ 'rev-suppressed-text-view', $titleText ]
1713 : [ 'rev-deleted-text-view', $titleText ];
1714 $outputPage->addHTML(
1715 Html::warningBox(
1716 $outputPage->msg( $msg[0], $msg[1] )->parse(),
1717 'plainlinks'
1718 )
1719 );
1720
1721 return true;
1722 }
1723 }
1724
1733 public function setOldSubtitle( $oldid = 0 ) {
1734 if ( !$this->getHookRunner()->onDisplayOldSubtitle( $this, $oldid ) ) {
1735 return;
1736 }
1737
1738 $context = $this->getContext();
1739 $unhide = $context->getRequest()->getInt( 'unhide' ) === 1;
1740
1741 # Cascade unhide param in links for easy deletion browsing
1742 $extraParams = [];
1743 if ( $unhide ) {
1744 $extraParams['unhide'] = 1;
1745 }
1746
1747 if ( $this->mRevisionRecord && $this->mRevisionRecord->getId() === $oldid ) {
1748 $revisionRecord = $this->mRevisionRecord;
1749 } else {
1750 $revisionRecord = $this->revisionStore->getRevisionById( $oldid );
1751 }
1752 if ( !$revisionRecord ) {
1753 throw new LogicException( 'There should be a revision record at this point.' );
1754 }
1755
1756 $timestamp = $revisionRecord->getTimestamp();
1757
1758 $current = ( $oldid == $this->mPage->getLatest() );
1759 $language = $context->getLanguage();
1760 $user = $context->getUser();
1761
1762 $td = $language->userTimeAndDate( $timestamp, $user );
1763 $tddate = $language->userDate( $timestamp, $user );
1764 $tdtime = $language->userTime( $timestamp, $user );
1765
1766 # Show user links if allowed to see them. If hidden, then show them only if requested...
1767 $userlinks = Linker::revUserTools( $revisionRecord, !$unhide );
1768
1769 $infomsg = $current && !$context->msg( 'revision-info-current' )->isDisabled()
1770 ? 'revision-info-current'
1771 : 'revision-info';
1772
1773 $outputPage = $context->getOutput();
1774 $outputPage->addModuleStyles( [
1775 'mediawiki.action.styles',
1776 'mediawiki.interface.helpers.styles'
1777 ] );
1778
1779 $revisionUser = $revisionRecord->getUser();
1780 $revisionInfo = "<div id=\"mw-{$infomsg}\">" .
1781 $context->msg( $infomsg, $td )
1782 ->rawParams( $userlinks )
1783 ->params(
1784 $revisionRecord->getId(),
1785 $tddate,
1786 $tdtime,
1787 $revisionUser ? $revisionUser->getName() : ''
1788 )
1789 ->rawParams( $this->commentFormatter->formatRevision(
1790 $revisionRecord,
1791 $user,
1792 true,
1793 !$unhide
1794 ) )
1795 ->parse() .
1796 "</div>";
1797
1798 $lnk = $current
1799 ? $context->msg( 'currentrevisionlink' )->escaped()
1800 : $this->linkRenderer->makeKnownLink(
1801 $this->getTitle(),
1802 $context->msg( 'currentrevisionlink' )->text(),
1803 [],
1804 $extraParams
1805 );
1806 $curdiff = $current
1807 ? $context->msg( 'diff' )->escaped()
1808 : $this->linkRenderer->makeKnownLink(
1809 $this->getTitle(),
1810 $context->msg( 'diff' )->text(),
1811 [],
1812 [
1813 'diff' => 'cur',
1814 'oldid' => $oldid
1815 ] + $extraParams
1816 );
1817 $prevExist = (bool)$this->revisionStore->getPreviousRevision( $revisionRecord );
1818 $prevlink = $prevExist
1819 ? $this->linkRenderer->makeKnownLink(
1820 $this->getTitle(),
1821 $context->msg( 'previousrevision' )->text(),
1822 [],
1823 [
1824 'direction' => 'prev',
1825 'oldid' => $oldid
1826 ] + $extraParams
1827 )
1828 : $context->msg( 'previousrevision' )->escaped();
1829 $prevdiff = $prevExist
1830 ? $this->linkRenderer->makeKnownLink(
1831 $this->getTitle(),
1832 $context->msg( 'diff' )->text(),
1833 [],
1834 [
1835 'diff' => 'prev',
1836 'oldid' => $oldid
1837 ] + $extraParams
1838 )
1839 : $context->msg( 'diff' )->escaped();
1840 $nextlink = $current
1841 ? $context->msg( 'nextrevision' )->escaped()
1842 : $this->linkRenderer->makeKnownLink(
1843 $this->getTitle(),
1844 $context->msg( 'nextrevision' )->text(),
1845 [],
1846 [
1847 'direction' => 'next',
1848 'oldid' => $oldid
1849 ] + $extraParams
1850 );
1851 $nextdiff = $current
1852 ? $context->msg( 'diff' )->escaped()
1853 : $this->linkRenderer->makeKnownLink(
1854 $this->getTitle(),
1855 $context->msg( 'diff' )->text(),
1856 [],
1857 [
1858 'diff' => 'next',
1859 'oldid' => $oldid
1860 ] + $extraParams
1861 );
1862
1863 $cdel = Linker::getRevDeleteLink(
1864 $context->getAuthority(),
1865 $revisionRecord,
1866 $this->getTitle()
1867 );
1868 if ( $cdel !== '' ) {
1869 $cdel .= ' ';
1870 }
1871
1872 // the outer div is need for styling the revision info and nav in MobileFrontend
1873 $outputPage->addSubtitle(
1874 Html::warningBox(
1875 $revisionInfo .
1876 "<div id=\"mw-revision-nav\">" . $cdel .
1877 $context->msg( 'revision-nav' )->rawParams(
1878 $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff
1879 )->escaped() . "</div>",
1880 'mw-revision'
1881 )
1882 );
1883 }
1884
1898 public static function getRedirectHeaderHtml( Language $lang, Title $target, $forceKnown = false ) {
1899 wfDeprecated( __METHOD__, '1.41' );
1900 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1901 return $linkRenderer->makeRedirectHeader( $lang, $target, $forceKnown );
1902 }
1903
1912 public function addHelpLink( $to, $overrideBaseUrl = false ) {
1913 $out = $this->getContext()->getOutput();
1914 $msg = $out->msg( 'namespace-' . $this->getTitle()->getNamespace() . '-helppage' );
1915
1916 if ( !$msg->isDisabled() ) {
1917 $title = Title::newFromText( $msg->plain() );
1918 if ( $title instanceof Title ) {
1919 $out->addHelpLink( $title->getLocalURL(), true );
1920 }
1921 } else {
1922 $out->addHelpLink( $to, $overrideBaseUrl );
1923 }
1924 }
1925
1929 public function render() {
1930 $this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' );
1931 $this->getContext()->getOutput()->setArticleBodyOnly( true );
1932 // We later set 'enableSectionEditLinks=false' based on this; also used by ImagePage
1933 $this->viewIsRenderAction = true;
1934 $this->view();
1935 }
1936
1940 public function protect() {
1941 $form = new ProtectionForm( $this );
1942 $form->execute();
1943 }
1944
1948 public function unprotect() {
1949 $this->protect();
1950 }
1951
1952 /* Caching functions */
1953
1961 protected function tryFileCache() {
1962 static $called = false;
1963
1964 if ( $called ) {
1965 wfDebug( "Article::tryFileCache(): called twice!?" );
1966 return false;
1967 }
1968
1969 $called = true;
1970 if ( $this->isFileCacheable() ) {
1971 $cache = new HTMLFileCache( $this->getTitle(), 'view' );
1972 if ( $cache->isCacheGood( $this->mPage->getTouched() ) ) {
1973 wfDebug( "Article::tryFileCache(): about to load file" );
1974 $cache->loadFromFileCache( $this->getContext() );
1975 return true;
1976 } else {
1977 wfDebug( "Article::tryFileCache(): starting buffer" );
1978 ob_start( [ &$cache, 'saveToFileCache' ] );
1979 }
1980 } else {
1981 wfDebug( "Article::tryFileCache(): not cacheable" );
1982 }
1983
1984 return false;
1985 }
1986
1992 public function isFileCacheable( $mode = HTMLFileCache::MODE_NORMAL ) {
1993 $cacheable = false;
1994
1995 if ( HTMLFileCache::useFileCache( $this->getContext(), $mode ) ) {
1996 $cacheable = $this->mPage->getId()
1997 && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect();
1998 // Extension may have reason to disable file caching on some pages.
1999 if ( $cacheable ) {
2000 $cacheable = $this->getHookRunner()->onIsFileCacheable( $this ) ?? false;
2001 }
2002 }
2003
2004 return $cacheable;
2005 }
2006
2018 public function getParserOutput( $oldid = null, UserIdentity $user = null ) {
2019 if ( $user === null ) {
2020 $parserOptions = $this->getParserOptions();
2021 } else {
2022 $parserOptions = $this->mPage->makeParserOptions( $user );
2023 $parserOptions->setRenderReason( 'page-view' );
2024 }
2025
2026 return $this->mPage->getParserOutput( $parserOptions, $oldid );
2027 }
2028
2033 public function getParserOptions() {
2034 $parserOptions = $this->mPage->makeParserOptions( $this->getContext() );
2035 $parserOptions->setRenderReason( 'page-view' );
2036 return $parserOptions;
2037 }
2038
2045 public function setContext( $context ) {
2046 $this->mContext = $context;
2047 }
2048
2055 public function getContext(): IContextSource {
2056 if ( $this->mContext instanceof IContextSource ) {
2057 return $this->mContext;
2058 } else {
2059 wfDebug( __METHOD__ . " called and \$mContext is null. " .
2060 "Return RequestContext::getMain()" );
2061 return RequestContext::getMain();
2062 }
2063 }
2064
2070 public function getActionOverrides() {
2071 return $this->mPage->getActionOverrides();
2072 }
2073
2074 private function getMissingRevisionMsg( int $oldid ): Message {
2075 // T251066: Try loading the revision from the archive table.
2076 // Show link to view it if it exists and the user has permission to view it.
2077 // (Ignore the given title, if any; look it up from the revision instead.)
2078 $context = $this->getContext();
2079 $revRecord = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $oldid );
2080 if (
2081 $revRecord &&
2082 $revRecord->userCan(
2083 RevisionRecord::DELETED_TEXT,
2084 $context->getAuthority()
2085 ) &&
2086 $context->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' )
2087 ) {
2088 return $context->msg(
2089 'missing-revision-permission',
2090 $oldid,
2091 $revRecord->getTimestamp(),
2092 Title::newFromPageIdentity( $revRecord->getPage() )->getPrefixedDBkey()
2093 );
2094 }
2095 return $context->msg( 'missing-revision', $oldid );
2096 }
2097}
getRequest()
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 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.
getContext()
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:70
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition Article.php:218
getContext()
Gets the context this Article is executed in.
Definition Article.php:2055
getOldIDFromRequest()
Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect.
Definition Article.php:292
getRedirectedFrom()
Get the page this view was redirected from.
Definition Article.php:229
Title null $mRedirectedFrom
Title from which we were redirected here, if any.
Definition Article.php:91
bool $viewIsRenderAction
Whether render() was called.
Definition Article.php:114
view()
This is the default action of the index.php entry point: just view the page of the given title.
Definition Article.php:451
showProtectionIndicator()
Show a lock icon above the article body if the page is protected.
Definition Article.php:594
__construct(Title $title, $oldId=null)
Definition Article.php:141
getRobotPolicy( $action, ParserOutput $pOutput=null)
Get the robot policy to be used for the current view.
Definition Article.php:1095
static purgePatrolFooterCache( $articleID)
Purge the cache used to check if it is worth showing the patrol footer For example,...
Definition Article.php:1487
ParserOutput null false $mParserOutput
The ParserOutput generated for viewing the page, initialized by view().
Definition Article.php:107
getOldID()
Definition Article.php:279
LinkRenderer $linkRenderer
Definition Article.php:116
getTitle()
Get the title object of the article.
Definition Article.php:247
getActionOverrides()
Call to WikiPage function for backwards compatibility.
Definition Article.php:2070
isDiffOnlyView()
Definition Article.php:1081
adjustDisplayTitle(ParserOutput $pOutput)
Adjust title for pages with displaytitle, -{T|}- or language conversion.
Definition Article.php:992
DatabaseBlockStore $blockStore
Definition Article.php:125
showDeletedRevisionHeader()
If the revision requested for view is deleted, check permissions.
Definition Article.php:1673
getParserOptions()
Get parser options suitable for rendering the primary article wikitext.
Definition Article.php:2033
IContextSource null $mContext
The context this Article is executed in.
Definition Article.php:79
static getRedirectHeaderHtml(Language $lang, Title $target, $forceKnown=false)
Return the HTML for the top of a redirect page.
Definition Article.php:1898
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:2018
protect()
action=protect handler
Definition Article.php:1940
string false $mRedirectUrl
URL to redirect to or false if none.
Definition Article.php:94
isCurrent()
Returns true if the currently-referenced revision is the current edit to this page (and it exists).
Definition Article.php:418
showMissingArticle()
Show the error text for a missing article.
Definition Article.php:1496
IConnectionProvider $dbProvider
Definition Article.php:124
unprotect()
action=unprotect handler (alias)
Definition Article.php:1948
newPage(Title $title)
Definition Article.php:163
getPage()
Get the WikiPage object of this instance.
Definition Article.php:257
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition Article.php:1912
static newFromID( $id)
Constructor from a page id.
Definition Article.php:172
int null $mOldId
The oldid of the article that was requested to be shown, 0 for the current revision.
Definition Article.php:88
static formatRobotPolicy( $policy)
Converts a String robot policy into an associative array, to allow merging of several policies using ...
Definition Article.php:1179
fetchRevisionRecord()
Fetches the revision to work on.
Definition Article.php:360
showPatrolFooter()
If patrol is possible, output a patrol UI box.
Definition Article.php:1315
setOldSubtitle( $oldid=0)
Generate the navigation links when browsing through an article revisions It shows the information as:...
Definition Article.php:1733
showViewFooter()
Show the footer section of an ordinary page view.
Definition Article.php:1291
WikiPage $mPage
The WikiPage object of this instance.
Definition Article.php:82
setRedirectedFrom(Title $from)
Tell the page view functions that this view was redirected from another page on the wiki.
Definition Article.php:238
isFileCacheable( $mode=HTMLFileCache::MODE_NORMAL)
Check if the page can be cached.
Definition Article.php:1992
tryFileCache()
checkLastModified returns true if it has taken care of all output to the client that is necessary for...
Definition Article.php:1961
getRevIdFetched()
Use this to fetch the rev ID used on page views.
Definition Article.php:437
showNamespaceHeader()
Show a header specific to the namespace currently being viewed, like [[MediaWiki:Talkpagetext]].
Definition Article.php:1279
static newFromTitle( $title, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition Article.php:184
showDiffPage()
Show a diff page according to current request variables.
Definition Article.php:1007
RestrictionStore $restrictionStore
Definition Article.php:127
render()
Handle action=render.
Definition Article.php:1929
showRedirectedFromHeader()
If this request is a redirect view, send "redirected from" subtitle to the output.
Definition Article.php:1206
setContext( $context)
Sets the context this Article is executed in.
Definition Article.php:2045
getCacheRevisionId()
Special handling for category description pages.
Page view caching in the file system.
Rendering of file description pages.
Definition ImagePage.php:38
Handle enqueueing of background jobs.
Base class for language-specific code.
Definition Language.php:66
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
This is the main service interface for converting single-line comments from various DB comment fields...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Group all the pieces relevant to the context of a request into one instance.
The HTML user interface for page editing.
Definition EditPage.php:146
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
This class is a collection of static functions that serve two purposes:
Definition Html.php:56
Class that generates HTML for internal links.
makeRedirectHeader(Language $lang, Title $target, bool $forceKnown=false)
Return the HTML for the top of a redirect page.
Some internal bits split of from Skin.php.
Definition Linker.php:63
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:158
This is one of the Core classes and should be read at least once by any new developers.
parseAsContent( $text, $linestart=true)
Parse wikitext in the page content language and return the HTML.
disableClientCache()
Force the page to send nocache headers.
addJsConfigVars( $keys, $value=null)
Add one or more variables to be set in mw.config in JavaScript.
wrapWikiMsg( $wrap,... $msgSpecs)
This function takes a number of message/argument specifications, wraps them in some overall structure...
setIndexPolicy( $policy)
Set the index policy for the page, but leave the follow policy un- touched.
setPreventClickjacking(bool $enable)
Set the prevent-clickjacking flag.
disable()
Disable output completely, i.e.
setRedirectedFrom(PageReference $t)
Set $mRedirectedFrom, the page which redirected us to the current page.
setRevisionTimestamp( $timestamp)
Set the timestamp of the revision which will be displayed.
adaptCdnTTL( $mtime, $minTTL=0, $maxTTL=0)
Get TTL in [$minTTL,$maxTTL] and pass it to lowerCdnMaxage()
addBodyClasses( $classes)
Add a class to the <body> element.
lowerCdnMaxage( $maxage)
Set the value of the "s-maxage" part of the "Cache-control" HTTP header to $maxage if that is lower t...
setContentLangForJS(Bcp47Code $lang)
setIndicators(array $indicators)
Add an array of indicators, with their identifiers as array keys and HTML contents as values.
setLastModified( $timestamp)
Override the last modified timestamp.
setRevisionIsCurrent(bool $isCurrent)
Set whether the revision displayed (as set in ::setRevisionId()) is the latest revision of the page.
setFollowPolicy( $policy)
Set the follow policy for the page, but leave the index policy un- touched.
clearHTML()
Clear the body HTML.
setPageTitle( $name)
"Page title" means the contents of <h1>.
setPageTitleMsg(Message $msg)
"Page title" means the contents of <h1>.
addModules( $modules)
Load one or more ResourceLoader modules on this page.
redirect( $url, $responsecode='302')
Redirect to $url rather than displaying the normal page.
setHTMLTitle( $name)
"HTML title" means the contents of "<title>".
prependHTML( $text)
Prepend $text to the body HTML.
setRobotPolicy( $policy)
Set the robot policy for the page: http://www.robotstxt.org/meta.html
setCanonicalUrl( $url)
Set the URL to be used for the <link rel=canonical>>.
addWikiTextAsContent( $text, $linestart=true, PageReference $title=null)
Convert wikitext in the page content language to HTML and add it to the buffer.
addHTML( $text)
Append $text to the body HTML.
setRevisionId( $revid)
Set the revision ID which will be seen by the wiki text parser for things such as embedded {{REVISION...
addSubtitle( $str)
Add $str to the subtitle.
addModuleStyles( $modules)
Load the styles of one or more style-only ResourceLoader modules on this page.
addParserOutput(ParserOutput $parserOutput, $poOptions=[])
Add everything from a ParserOutput object.
isPrintable()
Return whether the page is "printable".
setArticleFlag( $newVal)
Set whether the displayed content is related to the source of the corresponding article on the wiki S...
addWikiTextAsInterface( $text, $linestart=true, PageReference $title=null)
Convert wikitext in the user interface language to HTML and add it to the buffer.
Service for getting rendered output of a given page.
Handles the page protection UI and backend.
Service for creating WikiPage objects.
ParserOutput is a rendering of a Content object or a message.
getLanguage()
Get the primary language code of the output.
PHP Parser - Processes wiki markup (which uses a more user-friendly syntax, such as "[[link]]" for ma...
Definition Parser.php:155
A StatusValue for permission errors.
Exception raised when the text of a revision is permanently missing or corrupt.
Page revision base class.
isCurrent()
Checks whether the revision record is a stored current revision.
getTimestamp()
MCR migration note: this replaced Revision::getTimestamp.
getSlot( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns meta-data for the given slot.
getContent( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns the Content of the given slot of this revision.
getId( $wikiId=self::LOCAL)
Get revision ID.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition Status.php:211
Represents a title within MediaWiki.
Definition Title.php:79
Provides access to user options.
UserNameUtils service.
internal since 1.36
Definition User.php:93
Module of static functions for generating XML.
Definition Xml.php:37
Set options of the Parser.
setIsPrintable( $x)
Parsing the printable version of the page?
setRenderReason(string $renderReason)
Sets reason for rendering the content.
static newSpec(int $revisionId, PageRecord $page, array $params=[])
Show an error when a user tries to do something they do not have the necessary permissions for.
hasMessage( $message)
Returns true if the specified message is present as a warning or error.
isOK()
Returns whether the operation completed.
fatal( $message,... $parameters)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
Base representation for an editable wiki page.
Definition WikiPage.php:81
getTitle()
Get the title object of the article.
Definition WikiPage.php:255
Interface for objects which can provide a MediaWiki context on request.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
Interface for objects representing user identity.
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition Page.php:30
Generic interface providing Time-To-Live constants for expirable object storage.
Provide primary and replica IDatabase connections.