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