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