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