MediaWiki REL1_40
Article.php
Go to the documentation of this file.
1<?php
24use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
44use Wikimedia\IPUtils;
45use Wikimedia\NonSerializable\NonSerializableTrait;
46
56class Article implements Page {
57 use ProtectedHookAccessorTrait;
58 use NonSerializableTrait;
59
65 protected $mContext;
66
68 protected $mPage;
69
74 public $mOldId;
75
77 public $mRedirectedFrom = null;
78
80 public $mRedirectUrl = false;
81
86 private $fetchResult = null;
87
93 public $mParserOutput = null;
94
100 protected $viewIsRenderAction = false;
101
105 protected $linkRenderer;
106
110 private $revisionStore;
111
115 private $watchlistManager;
116
120 private $userNameUtils;
121
125 private $userOptionsLookup;
126
128 private $commentFormatter;
129
131 private $wikiPageFactory;
132
134 private $jobQueueGroup;
135
142 private $mRevisionRecord = null;
143
148 public function __construct( Title $title, $oldId = null ) {
149 $this->mOldId = $oldId;
150 $this->mPage = $this->newPage( $title );
151
152 $services = MediaWikiServices::getInstance();
153 $this->linkRenderer = $services->getLinkRenderer();
154 $this->revisionStore = $services->getRevisionStore();
155 $this->watchlistManager = $services->getWatchlistManager();
156 $this->userNameUtils = $services->getUserNameUtils();
157 $this->userOptionsLookup = $services->getUserOptionsLookup();
158 $this->commentFormatter = $services->getCommentFormatter();
159 $this->wikiPageFactory = $services->getWikiPageFactory();
160 $this->jobQueueGroup = $services->getJobQueueGroup();
161 }
162
167 protected function newPage( Title $title ) {
168 return new WikiPage( $title );
169 }
170
176 public static function newFromID( $id ) {
177 $t = Title::newFromID( $id );
178 return $t == null ? null : new static( $t );
179 }
180
188 public static function newFromTitle( $title, IContextSource $context ) {
189 if ( $title->getNamespace() === NS_MEDIA ) {
190 // XXX: This should not be here, but where should it go?
191 $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
192 }
193
194 $page = null;
195 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
196 Hooks::runner()->onArticleFromTitle( $title, $page, $context );
197 if ( !$page ) {
198 switch ( $title->getNamespace() ) {
199 case NS_FILE:
200 $page = new ImagePage( $title );
201 break;
202 case NS_CATEGORY:
203 $page = new CategoryPage( $title );
204 break;
205 default:
206 $page = new Article( $title );
207 }
208 }
209 $page->setContext( $context );
210
211 return $page;
212 }
213
221 public static function newFromWikiPage( WikiPage $page, IContextSource $context ) {
222 $article = self::newFromTitle( $page->getTitle(), $context );
223 $article->mPage = $page; // override to keep process cached vars
224 return $article;
225 }
226
232 public function getRedirectedFrom() {
233 return $this->mRedirectedFrom;
234 }
235
241 public function setRedirectedFrom( Title $from ) {
242 $this->mRedirectedFrom = $from;
243 }
244
250 public function getTitle() {
251 return $this->mPage->getTitle();
252 }
253
260 public function getPage() {
261 return $this->mPage;
262 }
263
264 public function clear() {
265 $this->mRedirectedFrom = null; # Title object if set
266 $this->mRedirectUrl = false;
267 $this->mRevisionRecord = null;
268 $this->fetchResult = null;
269
270 // TODO hard-deprecate direct access to public fields
271
272 $this->mPage->clear();
273 }
274
282 public function getOldID() {
283 if ( $this->mOldId === null ) {
284 $this->mOldId = $this->getOldIDFromRequest();
285 }
286
287 return $this->mOldId;
288 }
289
295 public function getOldIDFromRequest() {
296 $this->mRedirectUrl = false;
297
298 $request = $this->getContext()->getRequest();
299 $oldid = $request->getIntOrNull( 'oldid' );
300
301 if ( $oldid === null ) {
302 return 0;
303 }
304
305 if ( $oldid !== 0 ) {
306 # Load the given revision and check whether the page is another one.
307 # In that case, update this instance to reflect the change.
308 if ( $oldid === $this->mPage->getLatest() ) {
309 $this->mRevisionRecord = $this->mPage->getRevisionRecord();
310 } else {
311 $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
312 if ( $this->mRevisionRecord !== null ) {
313 $revPageId = $this->mRevisionRecord->getPageId();
314 // Revision title doesn't match the page title given?
315 if ( $this->mPage->getId() != $revPageId ) {
316 $this->mPage = $this->wikiPageFactory->newFromID( $revPageId );
317 }
318 }
319 }
320 }
321
322 $oldRev = $this->mRevisionRecord;
323 if ( $request->getRawVal( 'direction' ) === 'next' ) {
324 $nextid = 0;
325 if ( $oldRev ) {
326 $nextRev = $this->revisionStore->getNextRevision( $oldRev );
327 if ( $nextRev ) {
328 $nextid = $nextRev->getId();
329 }
330 }
331 if ( $nextid ) {
332 $oldid = $nextid;
333 $this->mRevisionRecord = null;
334 } else {
335 $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' );
336 }
337 } elseif ( $request->getRawVal( 'direction' ) === 'prev' ) {
338 $previd = 0;
339 if ( $oldRev ) {
340 $prevRev = $this->revisionStore->getPreviousRevision( $oldRev );
341 if ( $prevRev ) {
342 $previd = $prevRev->getId();
343 }
344 }
345 if ( $previd ) {
346 $oldid = $previd;
347 $this->mRevisionRecord = null;
348 }
349 }
350
351 return $oldid;
352 }
353
363 public function fetchRevisionRecord() {
364 if ( $this->fetchResult ) {
365 return $this->mRevisionRecord;
366 }
367
368 $oldid = $this->getOldID();
369
370 // $this->mRevisionRecord might already be fetched by getOldIDFromRequest()
371 if ( !$this->mRevisionRecord ) {
372 if ( !$oldid ) {
373 $this->mRevisionRecord = $this->mPage->getRevisionRecord();
374
375 if ( !$this->mRevisionRecord ) {
376 wfDebug( __METHOD__ . " failed to find page data for title " .
377 $this->getTitle()->getPrefixedText() );
378
379 // Output for this case is done by showMissingArticle().
380 $this->fetchResult = Status::newFatal( 'noarticletext' );
381 return null;
382 }
383 } else {
384 $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
385
386 if ( !$this->mRevisionRecord ) {
387 wfDebug( __METHOD__ . " failed to load revision, rev_id $oldid" );
388
389 $this->fetchResult = Status::newFatal( 'missing-revision', $oldid );
390 return null;
391 }
392 }
393 }
394
395 if ( !$this->mRevisionRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getContext()->getAuthority() ) ) {
396 wfDebug( __METHOD__ . " failed to retrieve content of revision " . $this->mRevisionRecord->getId() );
397
398 // Output for this case is done by showDeletedRevisionHeader().
399 // title used in wikilinks, should not contain whitespaces
400 $this->fetchResult = new Status;
401 $title = $this->getTitle()->getPrefixedDBkey();
402
403 if ( $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
404 $this->fetchResult->fatal( 'rev-suppressed-text' );
405 } else {
406 $this->fetchResult->fatal( 'rev-deleted-text-permission', $title );
407 }
408
409 return null;
410 }
411
412 $this->fetchResult = Status::newGood( $this->mRevisionRecord );
413 return $this->mRevisionRecord;
414 }
415
421 public function isCurrent() {
422 # If no oldid, this is the current version.
423 if ( $this->getOldID() == 0 ) {
424 return true;
425 }
426
427 return $this->mPage->exists() &&
428 $this->mRevisionRecord &&
429 $this->mRevisionRecord->isCurrent();
430 }
431
440 public function getRevIdFetched() {
441 if ( $this->fetchResult && $this->fetchResult->isOK() ) {
443 $rev = $this->fetchResult->getValue();
444 return $rev->getId();
445 } else {
446 return $this->mPage->getLatest();
447 }
448 }
449
454 public function view() {
455 $context = $this->getContext();
456 $useFileCache = $context->getConfig()->get( MainConfigNames::UseFileCache );
457
458 # Get variables from query string
459 # As side effect this will load the revision and update the title
460 # in a revision ID is passed in the request, so this should remain
461 # the first call of this method even if $oldid is used way below.
462 $oldid = $this->getOldID();
463
464 $authority = $context->getAuthority();
465 # Another check in case getOldID() is altering the title
466 $permissionStatus = PermissionStatus::newEmpty();
467 if ( !$authority
468 ->authorizeRead( 'read', $this->getTitle(), $permissionStatus )
469 ) {
470 wfDebug( __METHOD__ . ": denied on secondary read check" );
471 throw new PermissionsError( 'read', $permissionStatus );
472 }
473
474 $outputPage = $context->getOutput();
475 # getOldID() may as well want us to redirect somewhere else
476 if ( $this->mRedirectUrl ) {
477 $outputPage->redirect( $this->mRedirectUrl );
478 wfDebug( __METHOD__ . ": redirecting due to oldid" );
479
480 return;
481 }
482
483 # If we got diff in the query, we want to see a diff page instead of the article.
484 if ( $context->getRequest()->getCheck( 'diff' ) ) {
485 wfDebug( __METHOD__ . ": showing diff page" );
486 $this->showDiffPage();
487
488 return;
489 }
490
491 # Set page title (may be overridden from ParserOutput if title conversion is enabled or DISPLAYTITLE is used)
493 str_replace( '_', ' ', $this->getTitle()->getNsText() ),
494 ':',
495 $this->getTitle()->getText()
496 ) );
497
498 $outputPage->setArticleFlag( true );
499 # Allow frames by default
500 $outputPage->setPreventClickjacking( false );
501
502 $parserOptions = $this->getParserOptions();
503
504 $poOptions = [];
505 # Allow extensions to vary parser options used for article rendering
506 Hooks::runner()->onArticleParserOptions( $this, $parserOptions );
507 # Render printable version, use printable version cache
508 if ( $outputPage->isPrintable() ) {
509 $parserOptions->setIsPrintable( true );
510 $poOptions['enableSectionEditLinks'] = false;
511 $outputPage->prependHTML(
512 Html::warningBox(
513 $outputPage->msg( 'printableversion-deprecated-warning' )->escaped()
514 )
515 );
516 } elseif ( $this->viewIsRenderAction || !$this->isCurrent() ||
517 !$authority->probablyCan( 'edit', $this->getTitle() )
518 ) {
519 $poOptions['enableSectionEditLinks'] = false;
520 }
521
522 # Try client and file cache
523 if ( $oldid === 0 && $this->mPage->checkTouched() ) {
524 # Try to stream the output from file cache
525 if ( $useFileCache && $this->tryFileCache() ) {
526 wfDebug( __METHOD__ . ": done file cache" );
527 # tell wgOut that output is taken care of
528 $outputPage->disable();
529 $this->mPage->doViewUpdates( $authority, $oldid );
530
531 return;
532 }
533 }
534
536 $this->showNamespaceHeader();
537
538 if ( $this->viewIsRenderAction ) {
539 $poOptions += [ 'absoluteURLs' => true ];
540 }
541 $poOptions += [ 'includeDebugInfo' => true ];
542
543 try {
544 $continue =
545 $this->generateContentOutput( $authority, $parserOptions, $oldid, $outputPage, $poOptions );
546 } catch ( BadRevisionException $e ) {
547 $continue = false;
548 $this->showViewError( wfMessage( 'badrevision' )->text() );
549 }
550
551 if ( !$continue ) {
552 return;
553 }
554
555 # For the main page, overwrite the <title> element with the con-
556 # tents of 'pagetitle-view-mainpage' instead of the default (if
557 # that's not empty).
558 # This message always exists because it is in the i18n files
559 if ( $this->getTitle()->isMainPage() ) {
560 $msg = $context->msg( 'pagetitle-view-mainpage' )->inContentLanguage();
561 if ( !$msg->isDisabled() ) {
562 $outputPage->setHTMLTitle( $msg->text() );
563 }
564 }
565
566 # Use adaptive TTLs for CDN so delayed/failed purges are noticed less often.
567 # This could use getTouched(), but that could be scary for major template edits.
568 $outputPage->adaptCdnTTL( $this->mPage->getTimestamp(), IExpiringStore::TTL_DAY );
569
570 $this->showViewFooter();
571 $this->mPage->doViewUpdates( $authority, $oldid, $this->fetchRevisionRecord() );
572
573 # Load the postEdit module if the user just saved this revision
574 # See also EditPage::setPostEditCookie
575 $request = $context->getRequest();
576 $cookieKey = EditPage::POST_EDIT_COOKIE_KEY_PREFIX . $this->getRevIdFetched();
577 $postEdit = $request->getCookie( $cookieKey );
578 if ( $postEdit ) {
579 # Clear the cookie. This also prevents caching of the response.
580 $request->response()->clearCookie( $cookieKey );
581 $outputPage->addJsConfigVars( 'wgPostEdit', $postEdit );
582 $outputPage->addModules( 'mediawiki.action.view.postEdit' ); // FIXME: test this
583 }
584 }
585
598 private function generateContentOutput(
599 Authority $performer,
600 ParserOptions $parserOptions,
601 int $oldid,
602 OutputPage $outputPage,
603 array $textOptions
604 ): bool {
605 # Should the parser cache be used?
606 $useParserCache = true;
607 $pOutput = null;
608 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
609
610 // NOTE: $outputDone and $useParserCache may be changed by the hook
611 $this->getHookRunner()->onArticleViewHeader( $this, $outputDone, $useParserCache );
612 if ( $outputDone ) {
613 if ( $outputDone instanceof ParserOutput ) {
614 $pOutput = $outputDone;
615 }
616
617 if ( $pOutput ) {
618 $this->doOutputMetaData( $pOutput, $outputPage );
619 }
620 return true;
621 }
622
623 // Early abort if the page doesn't exist
624 if ( !$this->mPage->exists() ) {
625 wfDebug( __METHOD__ . ": showing missing article" );
626 $this->showMissingArticle();
627 $this->mPage->doViewUpdates( $performer );
628 return false; // skip all further output to OutputPage
629 }
630
631 // Try the latest parser cache
632 // NOTE: try latest-revision cache first to avoid loading revision.
633 if ( $useParserCache && !$oldid ) {
634 $pOutput = $parserOutputAccess->getCachedParserOutput(
635 $this->getPage(),
636 $parserOptions,
637 null,
638 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked
639 );
640
641 if ( $pOutput ) {
642 $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
643 $this->doOutputMetaData( $pOutput, $outputPage );
644 return true;
645 }
646 }
647
648 $rev = $this->fetchRevisionRecord();
649 if ( !$this->fetchResult->isOK() ) {
650 $this->showViewError( $this->fetchResult->getWikiText(
651 false, false, $this->getContext()->getLanguage()
652 ) );
653 return true;
654 }
655
656 # Are we looking at an old revision
657 if ( $oldid ) {
658 $this->setOldSubtitle( $oldid );
659
660 if ( !$this->showDeletedRevisionHeader() ) {
661 wfDebug( __METHOD__ . ": cannot view deleted revision" );
662 return false; // skip all further output to OutputPage
663 }
664
665 // Try the old revision parser cache
666 // NOTE: Repeating cache check for old revision to avoid fetching $rev
667 // before it's absolutely necessary.
668 if ( $useParserCache ) {
669 $pOutput = $parserOutputAccess->getCachedParserOutput(
670 $this->getPage(),
671 $parserOptions,
672 $rev,
673 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked in fetchRevisionRecord
674 );
675
676 if ( $pOutput ) {
677 $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
678 $this->doOutputMetaData( $pOutput, $outputPage );
679 return true;
680 }
681 }
682 }
683
684 # Ensure that UI elements requiring revision ID have
685 # the correct version information.
686 $outputPage->setRevisionId( $this->getRevIdFetched() );
687 $outputPage->setRevisionIsCurrent( $rev->isCurrent() );
688 # Preload timestamp to avoid a DB hit
689 $outputPage->setRevisionTimestamp( $rev->getTimestamp() );
690
691 # Pages containing custom CSS or JavaScript get special treatment
692 if ( $this->getTitle()->isSiteConfigPage() || $this->getTitle()->isUserConfigPage() ) {
693 $dir = $this->getContext()->getLanguage()->getDir();
694 $lang = $this->getContext()->getLanguage()->getHtmlCode();
695
696 $outputPage->wrapWikiMsg(
697 "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
698 'clearyourcache'
699 );
700 $outputPage->addModuleStyles( 'mediawiki.action.styles' );
701 } elseif ( !$this->getHookRunner()->onArticleRevisionViewCustom(
702 $rev,
703 $this->getTitle(),
704 $oldid,
705 $outputPage )
706 ) {
707 // NOTE: sync with hooks called in DifferenceEngine::renderNewRevision()
708 // Allow extensions do their own custom view for certain pages
709 $this->doOutputMetaData( $pOutput, $outputPage );
710 return true;
711 }
712
713 # Run the parse, protected by a pool counter
714 wfDebug( __METHOD__ . ": doing uncached parse" );
715
716 if ( !$rev ) {
717 // No revision, abort! Shouldn't happen.
718 return false;
719 }
720
721 $opt = 0;
722
723 // we already checked the cache in case 2, don't check again.
724 $opt |= ParserOutputAccess::OPT_NO_CHECK_CACHE;
725
726 // we already checked in fetchRevisionRecord()
727 $opt |= ParserOutputAccess::OPT_NO_AUDIENCE_CHECK;
728
729 if ( !$rev->getId() || !$useParserCache ) {
730 // fake revision or uncacheable options
731 $opt |= ParserOutputAccess::OPT_NO_CACHE;
732 }
733
734 $renderStatus = $parserOutputAccess->getParserOutput(
735 $this->getPage(),
736 $parserOptions,
737 $rev,
738 $opt
739 );
740
741 // T327164: If parsoid cache warming is enabled, we want to ensure that the page
742 // the user is currently looking at has a cached parsoid rendering, in case they
743 // open visual editor. The cache entry would typically be missing if it has expired
744 // from the cache or it was invalidated by RefreshLinksJob. When "traditional"
745 // parser output has been invalidated by RefreshLinksJob, we will render it on
746 // the fly when a user requests the page, and thereby populate the cache again,
747 // per the code above.
748 // The code below is intended to do the same for parsoid output, but asynchronously
749 // in a job, so the user does not have to wait.
750 // Note that we get here if the traditional parser output was missing from the cache.
751 // We do not check if the parsoid output is present in the cache, because that check
752 // takes time. The assumption is that if we have traditional parser output
753 // cached, we probably also have parsoid output cached.
754 // So we leave it to ParsoidCachePrewarmJob to determine whether or not parsing is
755 // needed.
756 if ( $oldid === 0 || $oldid === $this->getPage()->getLatest() ) {
757 $parsoidCacheWarmingEnabled = $this->getContext()->getConfig()
758 ->get( MainConfigNames::ParsoidCacheConfig )['WarmParsoidParserCache'];
759
760 if ( $parsoidCacheWarmingEnabled ) {
761 $parsoidJobSpec = ParsoidCachePrewarmJob::newSpec(
762 $rev->getId(),
763 $this->getPage()->toPageRecord(),
764 [ 'causeAction' => 'view' ]
765 );
766 $this->jobQueueGroup->lazyPush( $parsoidJobSpec );
767 }
768 }
769
770 $this->doOutputFromRenderStatus(
771 $rev,
772 $renderStatus,
773 $outputPage,
774 $textOptions
775 );
776
777 if ( !$renderStatus->isOK() ) {
778 return true;
779 }
780
781 $pOutput = $renderStatus->getValue();
782 $this->doOutputMetaData( $pOutput, $outputPage );
783 return true;
784 }
785
790 private function doOutputMetaData( ?ParserOutput $pOutput, OutputPage $outputPage ) {
791 # Adjust title for main page & pages with displaytitle
792 if ( $pOutput ) {
793 $this->adjustDisplayTitle( $pOutput );
794 }
795
796 # Check for any __NOINDEX__ tags on the page using $pOutput
797 $policy = $this->getRobotPolicy( 'view', $pOutput ?: null );
798 $outputPage->setIndexPolicy( $policy['index'] );
799 $outputPage->setFollowPolicy( $policy['follow'] ); // FIXME: test this
800
801 $this->mParserOutput = $pOutput;
802 }
803
809 private function doOutputFromParserCache(
810 ParserOutput $pOutput,
811 OutputPage $outputPage,
812 array $textOptions
813 ) {
814 # Ensure that UI elements requiring revision ID have
815 # the correct version information.
816 $oldid = $pOutput->getCacheRevisionId() ?? $this->getRevIdFetched();
817 $outputPage->setRevisionId( $oldid );
818 $outputPage->setRevisionIsCurrent( $oldid === $this->mPage->getLatest() );
819 $outputPage->addParserOutput( $pOutput, $textOptions );
820 # Preload timestamp to avoid a DB hit
821 $cachedTimestamp = $pOutput->getTimestamp();
822 if ( $cachedTimestamp !== null ) {
823 $outputPage->setRevisionTimestamp( $cachedTimestamp );
824 $this->mPage->setTimestamp( $cachedTimestamp );
825 }
826 }
827
834 private function doOutputFromRenderStatus(
835 ?RevisionRecord $rev,
836 Status $renderStatus,
837 OutputPage $outputPage,
838 array $textOptions
839 ) {
840 $context = $this->getContext();
841 $cdnMaxageStale = $context->getConfig()->get( MainConfigNames::CdnMaxageStale );
842 $ok = $renderStatus->isOK();
843
844 $pOutput = $ok ? $renderStatus->getValue() : null;
845
846 // Cache stale ParserOutput object with a short expiry
847 if ( $ok && $renderStatus->hasMessage( 'view-pool-dirty-output' ) ) {
848 $outputPage->setCdnMaxage( $cdnMaxageStale );
849 $outputPage->setLastModified( $pOutput->getCacheTime() );
850 $staleReason = $renderStatus->hasMessage( 'view-pool-contention' )
851 ? $context->msg( 'view-pool-contention' )
852 : $context->msg( 'view-pool-timeout' );
853 $outputPage->addHTML( "<!-- parser cache is expired, " .
854 "sending anyway due to $staleReason-->\n" );
855 }
856
857 if ( !$renderStatus->isOK() ) {
858 $this->showViewError( $renderStatus->getWikiText(
859 false, 'view-pool-error', $context->getLanguage()
860 ) );
861 return;
862 }
863
864 if ( $pOutput ) {
865 $outputPage->addParserOutput( $pOutput, $textOptions );
866 }
867
868 if ( $this->getRevisionRedirectTarget( $rev ) ) {
869 $outputPage->addSubtitle( "<span id=\"redirectsub\">" .
870 $context->msg( 'redirectpagesub' )->parse() . "</span>" );
871 }
872 }
873
878 private function getRevisionRedirectTarget( RevisionRecord $revision ) {
879 // TODO: find a *good* place for the code that determines the redirect target for
880 // a given revision!
881 // NOTE: Use main slot content. Compare code in DerivedPageDataUpdater::revisionIsRedirect.
882 $content = $revision->getContent( SlotRecord::MAIN );
883 return $content ? $content->getRedirectTarget() : null;
884 }
885
890 public function adjustDisplayTitle( ParserOutput $pOutput ) {
891 $out = $this->getContext()->getOutput();
892
893 # Adjust the title if it was set by displaytitle, -{T|}- or language conversion
894 $titleText = $pOutput->getTitleText();
895 if ( strval( $titleText ) !== '' ) {
896 $out->setPageTitle( $titleText );
897 $out->setDisplayTitle( $titleText );
898 }
899 }
900
905 protected function showDiffPage() {
906 $context = $this->getContext();
907 $request = $context->getRequest();
908 $diff = $request->getVal( 'diff' );
909 $rcid = $request->getInt( 'rcid' );
910 $purge = $request->getRawVal( 'action' ) === 'purge';
911 $unhide = $request->getInt( 'unhide' ) == 1;
912 $oldid = $this->getOldID();
913
914 $rev = $this->fetchRevisionRecord();
915
916 if ( !$rev ) {
917 // T213621: $rev maybe null due to either lack of permission to view the
918 // revision or actually not existing. So let's try loading it from the id
919 $rev = $this->revisionStore->getRevisionById( $oldid );
920 if ( $rev ) {
921 // Revision exists but $user lacks permission to diff it.
922 // Do nothing here.
923 // The $rev will later be used to create standard diff elements however.
924 } else {
925 $context->getOutput()->setPageTitle( $context->msg( 'errorpagetitle' ) );
926 $msg = $context->msg( 'difference-missing-revision' )
927 ->params( $oldid )
928 ->numParams( 1 )
929 ->parseAsBlock();
930 $context->getOutput()->addHTML( $msg );
931 return;
932 }
933 }
934
935 $contentHandler = MediaWikiServices::getInstance()
936 ->getContentHandlerFactory()
937 ->getContentHandler(
938 $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
939 );
940 $de = $contentHandler->createDifferenceEngine(
941 $context,
942 $oldid,
943 $diff,
944 $rcid,
945 $purge,
946 $unhide
947 );
948 $de->setSlotDiffOptions( [
949 'diff-type' => $request->getVal( 'diff-type' ),
950 'expand-url' => $this->viewIsRenderAction
951 ] );
952 $de->showDiffPage( $this->isDiffOnlyView() );
953
954 // Run view updates for the newer revision being diffed (and shown
955 // below the diff if not diffOnly).
956 [ , $new ] = $de->mapDiffPrevNext( $oldid, $diff );
957 // New can be false, convert it to 0 - this conveniently means the latest revision
958 $this->mPage->doViewUpdates( $context->getAuthority(), (int)$new );
959
960 // Add link to help page; see T321569
961 $context->getOutput()->addHelpLink( 'Help:Diff' );
962 }
963
964 protected function isDiffOnlyView() {
965 return $this->getContext()->getRequest()->getBool(
966 'diffonly',
967 $this->userOptionsLookup->getBoolOption( $this->getContext()->getUser(), 'diffonly' )
968 );
969 }
970
978 public function getRobotPolicy( $action, ParserOutput $pOutput = null ) {
979 $context = $this->getContext();
980 $mainConfig = $context->getConfig();
981 $articleRobotPolicies = $mainConfig->get( MainConfigNames::ArticleRobotPolicies );
982 $namespaceRobotPolicies = $mainConfig->get( MainConfigNames::NamespaceRobotPolicies );
983 $defaultRobotPolicy = $mainConfig->get( MainConfigNames::DefaultRobotPolicy );
984 $title = $this->getTitle();
985 $ns = $title->getNamespace();
986
987 # Don't index user and user talk pages for blocked users (T13443)
988 if ( ( $ns === NS_USER || $ns === NS_USER_TALK ) && !$title->isSubpage() ) {
989 $specificTarget = null;
990 $vagueTarget = null;
991 $titleText = $title->getText();
992 if ( IPUtils::isValid( $titleText ) ) {
993 $vagueTarget = $titleText;
994 } else {
995 $specificTarget = $titleText;
996 }
997 if ( DatabaseBlock::newFromTarget( $specificTarget, $vagueTarget ) instanceof DatabaseBlock ) {
998 return [
999 'index' => 'noindex',
1000 'follow' => 'nofollow'
1001 ];
1002 }
1003 }
1004
1005 if ( $this->mPage->getId() === 0 || $this->getOldID() ) {
1006 # Non-articles (special pages etc), and old revisions
1007 return [
1008 'index' => 'noindex',
1009 'follow' => 'nofollow'
1010 ];
1011 } elseif ( $context->getOutput()->isPrintable() ) {
1012 # Discourage indexing of printable versions, but encourage following
1013 return [
1014 'index' => 'noindex',
1015 'follow' => 'follow'
1016 ];
1017 } elseif ( $context->getRequest()->getInt( 'curid' ) ) {
1018 # For ?curid=x urls, disallow indexing
1019 return [
1020 'index' => 'noindex',
1021 'follow' => 'follow'
1022 ];
1023 }
1024
1025 # Otherwise, construct the policy based on the various config variables.
1026 $policy = self::formatRobotPolicy( $defaultRobotPolicy );
1027
1028 if ( isset( $namespaceRobotPolicies[$ns] ) ) {
1029 # Honour customised robot policies for this namespace
1030 $policy = array_merge(
1031 $policy,
1032 self::formatRobotPolicy( $namespaceRobotPolicies[$ns] )
1033 );
1034 }
1035 if ( $title->canUseNoindex() && is_object( $pOutput ) && $pOutput->getIndexPolicy() ) {
1036 # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates
1037 # a final check that we have really got the parser output.
1038 $policy = array_merge(
1039 $policy,
1040 [ 'index' => $pOutput->getIndexPolicy() ]
1041 );
1042 }
1043
1044 if ( isset( $articleRobotPolicies[$title->getPrefixedText()] ) ) {
1045 # (T16900) site config can override user-defined __INDEX__ or __NOINDEX__
1046 $policy = array_merge(
1047 $policy,
1048 self::formatRobotPolicy( $articleRobotPolicies[$title->getPrefixedText()] )
1049 );
1050 }
1051
1052 return $policy;
1053 }
1054
1062 public static function formatRobotPolicy( $policy ) {
1063 if ( is_array( $policy ) ) {
1064 return $policy;
1065 } elseif ( !$policy ) {
1066 return [];
1067 }
1068
1069 $arr = [];
1070 foreach ( explode( ',', $policy ) as $var ) {
1071 $var = trim( $var );
1072 if ( $var === 'index' || $var === 'noindex' ) {
1073 $arr['index'] = $var;
1074 } elseif ( $var === 'follow' || $var === 'nofollow' ) {
1075 $arr['follow'] = $var;
1076 }
1077 }
1078
1079 return $arr;
1080 }
1081
1089 public function showRedirectedFromHeader() {
1090 $context = $this->getContext();
1091 $redirectSources = $context->getConfig()->get( MainConfigNames::RedirectSources );
1092 $outputPage = $context->getOutput();
1093 $request = $context->getRequest();
1094 $rdfrom = $request->getVal( 'rdfrom' );
1095
1096 // Construct a URL for the current page view, but with the target title
1097 $query = $request->getValues();
1098 unset( $query['rdfrom'] );
1099 unset( $query['title'] );
1100 if ( $this->getTitle()->isRedirect() ) {
1101 // Prevent double redirects
1102 $query['redirect'] = 'no';
1103 }
1104 $redirectTargetUrl = $this->getTitle()->getLinkURL( $query );
1105
1106 if ( isset( $this->mRedirectedFrom ) ) {
1107 // This is an internally redirected page view.
1108 // We'll need a backlink to the source page for navigation.
1109 if ( $this->getHookRunner()->onArticleViewRedirect( $this ) ) {
1110 $redir = $this->linkRenderer->makeKnownLink(
1111 $this->mRedirectedFrom,
1112 null,
1113 [],
1114 [ 'redirect' => 'no' ]
1115 );
1116
1117 $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1118 $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1119 . "</span>" );
1120
1121 // Add the script to update the displayed URL and
1122 // set the fragment if one was specified in the redirect
1123 $outputPage->addJsConfigVars( [
1124 'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1125 ] );
1126 $outputPage->addModules( 'mediawiki.action.view.redirect' );
1127
1128 // Add a <link rel="canonical"> tag
1129 $outputPage->setCanonicalUrl( $this->getTitle()->getCanonicalURL() );
1130
1131 // Tell the output object that the user arrived at this article through a redirect
1132 $outputPage->setRedirectedFrom( $this->mRedirectedFrom );
1133
1134 return true;
1135 }
1136 } elseif ( $rdfrom ) {
1137 // This is an externally redirected view, from some other wiki.
1138 // If it was reported from a trusted site, supply a backlink.
1139 if ( $redirectSources && preg_match( $redirectSources, $rdfrom ) ) {
1140 $redir = Linker::makeExternalLink( $rdfrom, $rdfrom );
1141 $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1142 $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1143 . "</span>" );
1144
1145 // Add the script to update the displayed URL
1146 $outputPage->addJsConfigVars( [
1147 'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1148 ] );
1149 $outputPage->addModules( 'mediawiki.action.view.redirect' );
1150
1151 return true;
1152 }
1153 }
1154
1155 return false;
1156 }
1157
1162 public function showNamespaceHeader() {
1163 if ( $this->getTitle()->isTalkPage() && !$this->getContext()->msg( 'talkpageheader' )->isDisabled() ) {
1164 $this->getContext()->getOutput()->wrapWikiMsg(
1165 "<div class=\"mw-talkpageheader\">\n$1\n</div>",
1166 [ 'talkpageheader' ]
1167 );
1168 }
1169 }
1170
1174 public function showViewFooter() {
1175 # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
1176 if ( $this->getTitle()->getNamespace() === NS_USER_TALK
1177 && IPUtils::isValid( $this->getTitle()->getText() )
1178 ) {
1179 $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' );
1180 }
1181
1182 // Show a footer allowing the user to patrol the shown revision or page if possible
1183 $patrolFooterShown = $this->showPatrolFooter();
1184
1185 $this->getHookRunner()->onArticleViewFooter( $this, $patrolFooterShown );
1186 }
1187
1198 public function showPatrolFooter() {
1199 $context = $this->getContext();
1200 $mainConfig = $context->getConfig();
1201 $useNPPatrol = $mainConfig->get( MainConfigNames::UseNPPatrol );
1202 $useRCPatrol = $mainConfig->get( MainConfigNames::UseRCPatrol );
1203 $useFilePatrol = $mainConfig->get( MainConfigNames::UseFilePatrol );
1204 // Allow hooks to decide whether to not output this at all
1205 if ( !$this->getHookRunner()->onArticleShowPatrolFooter( $this ) ) {
1206 return false;
1207 }
1208
1209 $outputPage = $context->getOutput();
1210 $user = $context->getUser();
1211 $title = $this->getTitle();
1212 $rc = false;
1213
1214 if ( !$context->getAuthority()->probablyCan( 'patrol', $title )
1215 || !( $useRCPatrol || $useNPPatrol
1216 || ( $useFilePatrol && $title->inNamespace( NS_FILE ) ) )
1217 ) {
1218 // Patrolling is disabled or the user isn't allowed to
1219 return false;
1220 }
1221
1222 if ( $this->mRevisionRecord
1223 && !RecentChange::isInRCLifespan( $this->mRevisionRecord->getTimestamp(), 21600 )
1224 ) {
1225 // The current revision is already older than what could be in the RC table
1226 // 6h tolerance because the RC might not be cleaned out regularly
1227 return false;
1228 }
1229
1230 // Check for cached results
1231 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1232 $key = $cache->makeKey( 'unpatrollable-page', $title->getArticleID() );
1233 if ( $cache->get( $key ) ) {
1234 return false;
1235 }
1236
1237 $dbr = wfGetDB( DB_REPLICA );
1238 $oldestRevisionRow = $dbr->selectRow(
1239 'revision',
1240 [ 'rev_id', 'rev_timestamp' ],
1241 [ 'rev_page' => $title->getArticleID() ],
1242 __METHOD__,
1243 [ 'ORDER BY' => [ 'rev_timestamp', 'rev_id' ] ]
1244 );
1245 $oldestRevisionTimestamp = $oldestRevisionRow ? $oldestRevisionRow->rev_timestamp : false;
1246
1247 // New page patrol: Get the timestamp of the oldest revision which
1248 // the revision table holds for the given page. Then we look
1249 // whether it's within the RC lifespan and if it is, we try
1250 // to get the recentchanges row belonging to that entry.
1251 $recentPageCreation = false;
1252 if ( $oldestRevisionTimestamp
1253 && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 )
1254 ) {
1255 // 6h tolerance because the RC might not be cleaned out regularly
1256 $recentPageCreation = true;
1257 $rc = RecentChange::newFromConds(
1258 [
1259 'rc_this_oldid' => intval( $oldestRevisionRow->rev_id ),
1260 // Avoid selecting a categorization entry
1261 'rc_type' => RC_NEW,
1262 ],
1263 __METHOD__
1264 );
1265 if ( $rc ) {
1266 // Use generic patrol message for new pages
1267 $markPatrolledMsg = $context->msg( 'markaspatrolledtext' );
1268 }
1269 }
1270
1271 // File patrol: Get the timestamp of the latest upload for this page,
1272 // check whether it is within the RC lifespan and if it is, we try
1273 // to get the recentchanges row belonging to that entry
1274 // (with rc_type = RC_LOG, rc_log_type = upload).
1275 $recentFileUpload = false;
1276 if ( ( !$rc || $rc->getAttribute( 'rc_patrolled' ) ) && $useFilePatrol
1277 && $title->getNamespace() === NS_FILE ) {
1278 // Retrieve timestamp from the current file (lastest upload)
1279 $newestUploadTimestamp = $dbr->selectField(
1280 'image',
1281 'img_timestamp',
1282 [ 'img_name' => $title->getDBkey() ],
1283 __METHOD__
1284 );
1285 if ( $newestUploadTimestamp
1286 && RecentChange::isInRCLifespan( $newestUploadTimestamp, 21600 )
1287 ) {
1288 // 6h tolerance because the RC might not be cleaned out regularly
1289 $recentFileUpload = true;
1290 $rc = RecentChange::newFromConds(
1291 [
1292 'rc_type' => RC_LOG,
1293 'rc_log_type' => 'upload',
1294 'rc_timestamp' => $newestUploadTimestamp,
1295 'rc_namespace' => NS_FILE,
1296 'rc_cur_id' => $title->getArticleID()
1297 ],
1298 __METHOD__
1299 );
1300 if ( $rc ) {
1301 // Use patrol message specific to files
1302 $markPatrolledMsg = $context->msg( 'markaspatrolledtext-file' );
1303 }
1304 }
1305 }
1306
1307 if ( !$recentPageCreation && !$recentFileUpload ) {
1308 // Page creation and latest upload (for files) is too old to be in RC
1309
1310 // We definitely can't patrol so cache the information
1311 // When a new file version is uploaded, the cache is cleared
1312 $cache->set( $key, '1' );
1313
1314 return false;
1315 }
1316
1317 if ( !$rc ) {
1318 // Don't cache: This can be hit if the page gets accessed very fast after
1319 // its creation / latest upload or in case we have high replica DB lag. In case
1320 // the revision is too old, we will already return above.
1321 return false;
1322 }
1323
1324 if ( $rc->getAttribute( 'rc_patrolled' ) ) {
1325 // Patrolled RC entry around
1326
1327 // Cache the information we gathered above in case we can't patrol
1328 // Don't cache in case we can patrol as this could change
1329 $cache->set( $key, '1' );
1330
1331 return false;
1332 }
1333
1334 if ( $rc->getPerformerIdentity()->equals( $user ) ) {
1335 // Don't show a patrol link for own creations/uploads. If the user could
1336 // patrol them, they already would be patrolled
1337 return false;
1338 }
1339
1340 $outputPage->setPreventClickjacking( true );
1341 if ( $context->getAuthority()->isAllowed( 'writeapi' ) ) {
1342 $outputPage->addModules( 'mediawiki.misc-authed-curate' );
1343 }
1344
1345 $link = $this->linkRenderer->makeKnownLink(
1346 $title,
1347 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $markPatrolledMsg is always set when $rc is set
1348 $markPatrolledMsg->text(),
1349 [],
1350 [
1351 'action' => 'markpatrolled',
1352 'rcid' => $rc->getAttribute( 'rc_id' ),
1353 ]
1354 );
1355
1356 $outputPage->addModuleStyles( 'mediawiki.action.styles' );
1357 $outputPage->addHTML(
1358 "<div class='patrollink' data-mw='interface'>" .
1359 $context->msg( 'markaspatrolledlink' )->rawParams( $link )->escaped() .
1360 '</div>'
1361 );
1362
1363 return true;
1364 }
1365
1372 public static function purgePatrolFooterCache( $articleID ) {
1373 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1374 $cache->delete( $cache->makeKey( 'unpatrollable-page', $articleID ) );
1375 }
1376
1381 public function showMissingArticle() {
1382 $context = $this->getContext();
1383 $send404Code = $context->getConfig()->get( MainConfigNames::Send404Code );
1384
1385 $outputPage = $context->getOutput();
1386 // Whether the page is a root user page of an existing user (but not a subpage)
1387 $validUserPage = false;
1388
1389 $title = $this->getTitle();
1390
1391 $services = MediaWikiServices::getInstance();
1392
1393 $contextUser = $context->getUser();
1394
1395 # Show info in user (talk) namespace. Does the user exist? Is he blocked?
1396 if ( $title->getNamespace() === NS_USER
1397 || $title->getNamespace() === NS_USER_TALK
1398 ) {
1399 $rootPart = explode( '/', $title->getText() )[0];
1400 $user = User::newFromName( $rootPart, false /* allow IP users */ );
1401 $ip = $this->userNameUtils->isIP( $rootPart );
1402 $block = DatabaseBlock::newFromTarget( $user, $user );
1403
1404 if ( $user && $user->isRegistered() && $user->isHidden() &&
1405 !$context->getAuthority()->isAllowed( 'hideuser' )
1406 ) {
1407 // T120883 if the user is hidden and the viewer cannot see hidden
1408 // users, pretend like it does not exist at all.
1409 $user = false;
1410 }
1411
1412 if ( !( $user && $user->isRegistered() ) && !$ip ) {
1413 // User does not exist
1414 $outputPage->addHTML( Html::warningBox(
1415 $context->msg( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) )->parse(),
1416 'mw-userpage-userdoesnotexist'
1417 ) );
1418
1419 // Show renameuser log extract
1420 LogEventsList::showLogExtract(
1421 $outputPage,
1422 'renameuser',
1423 Title::makeTitleSafe( NS_USER, $rootPart ),
1424 '',
1425 [
1426 'lim' => 10,
1427 'showIfEmpty' => false,
1428 'msgKey' => [ 'renameuser-renamed-notice', $title->getBaseText() ]
1429 ]
1430 );
1431 } elseif (
1432 $block !== null &&
1433 $block->getType() != DatabaseBlock::TYPE_AUTO &&
1434 (
1435 $block->isSitewide() ||
1436 $services->getPermissionManager()->isBlockedFrom( $user, $title, true )
1437 )
1438 ) {
1439 // Show log extract if the user is sitewide blocked or is partially
1440 // blocked and not allowed to edit their user page or user talk page
1441 LogEventsList::showLogExtract(
1442 $outputPage,
1443 'block',
1444 $services->getNamespaceInfo()->getCanonicalName( NS_USER ) . ':' .
1445 $block->getTargetName(),
1446 '',
1447 [
1448 'lim' => 1,
1449 'showIfEmpty' => false,
1450 'msgKey' => [
1451 'blocked-notice-logextract',
1452 $user->getName() # Support GENDER in notice
1453 ]
1454 ]
1455 );
1456 $validUserPage = !$title->isSubpage();
1457 } else {
1458 $validUserPage = !$title->isSubpage();
1459 }
1460 }
1461
1462 $this->getHookRunner()->onShowMissingArticle( $this );
1463
1464 # Show delete and move logs if there were any such events.
1465 # The logging query can DOS the site when bots/crawlers cause 404 floods,
1466 # so be careful showing this. 404 pages must be cheap as they are hard to cache.
1467 $dbCache = MediaWikiServices::getInstance()->getMainObjectStash();
1468 $key = $dbCache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
1469 $isRegistered = $contextUser->isRegistered();
1470 $sessionExists = $context->getRequest()->getSession()->isPersistent();
1471
1472 if ( $isRegistered || $dbCache->get( $key ) || $sessionExists ) {
1473 $logTypes = [ 'delete', 'move', 'protect' ];
1474
1475 $dbr = wfGetDB( DB_REPLICA );
1476
1477 $conds = [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ];
1478 // Give extensions a chance to hide their (unrelated) log entries
1479 $this->getHookRunner()->onArticle__MissingArticleConditions( $conds, $logTypes );
1480 LogEventsList::showLogExtract(
1481 $outputPage,
1482 $logTypes,
1483 $title,
1484 '',
1485 [
1486 'lim' => 10,
1487 'conds' => $conds,
1488 'showIfEmpty' => false,
1489 'msgKey' => [ $isRegistered || $sessionExists
1490 ? 'moveddeleted-notice'
1491 : 'moveddeleted-notice-recent'
1492 ]
1493 ]
1494 );
1495 }
1496
1497 if ( !$this->mPage->hasViewableContent() && $send404Code && !$validUserPage ) {
1498 // If there's no backing content, send a 404 Not Found
1499 // for better machine handling of broken links.
1500 $context->getRequest()->response()->statusHeader( 404 );
1501 }
1502
1503 // Also apply the robot policy for nonexisting pages (even if a 404 was used)
1504 $policy = $this->getRobotPolicy( 'view' );
1505 $outputPage->setIndexPolicy( $policy['index'] );
1506 $outputPage->setFollowPolicy( $policy['follow'] );
1507
1508 $hookResult = $this->getHookRunner()->onBeforeDisplayNoArticleText( $this );
1509
1510 if ( !$hookResult ) {
1511 return;
1512 }
1513
1514 # Show error message
1515 $oldid = $this->getOldID();
1516 if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) {
1517 $text = $this->getTitle()->getDefaultMessageText() ?? '';
1518 $outputPage->addWikiTextAsContent( $text );
1519 } else {
1520 if ( $oldid ) {
1521 // T251066: Try loading the revision from the archive table.
1522 // Show link to view it if it exists and the user has permission to view it.
1523 $pa = new PageArchive( $title );
1524 $revRecord = $pa->getArchivedRevisionRecord( $oldid );
1525 if ( $revRecord && $revRecord->userCan(
1526 RevisionRecord::DELETED_TEXT,
1527 $context->getAuthority()
1528 ) && $context->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
1529 $text = $context->msg(
1530 'missing-revision-permission', $oldid,
1531 $revRecord->getTimestamp(),
1532 $title->getPrefixedDBkey()
1533 )->plain();
1534 } else {
1535 $text = $context->msg( 'missing-revision', $oldid )->plain();
1536 }
1537
1538 } elseif ( $context->getAuthority()->probablyCan( 'edit', $title ) ) {
1539 $message = $isRegistered ? 'noarticletext' : 'noarticletextanon';
1540 $text = $context->msg( $message )->plain();
1541 } else {
1542 $text = $context->msg( 'noarticletext-nopermission' )->plain();
1543 }
1544
1545 $dir = $context->getLanguage()->getDir();
1546 $lang = $context->getLanguage()->getHtmlCode();
1547 $outputPage->addWikiTextAsInterface( Xml::openElement( 'div', [
1548 'class' => "noarticletext mw-content-$dir",
1549 'dir' => $dir,
1550 'lang' => $lang,
1551 ] ) . "\n$text\n</div>" );
1552 }
1553 }
1554
1559 private function showViewError( string $errortext ) {
1560 $outputPage = $this->getContext()->getOutput();
1561 $outputPage->setPageTitle( $this->getContext()->msg( 'errorpagetitle' ) );
1562 $outputPage->disableClientCache();
1563 $outputPage->setRobotPolicy( 'noindex,nofollow' );
1564 $outputPage->clearHTML();
1565 $outputPage->addHTML( Html::errorBox( $outputPage->parseAsContent( $errortext ) ) );
1566 }
1567
1574 public function showDeletedRevisionHeader() {
1575 if ( !$this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1576 // Not deleted
1577 return true;
1578 }
1579 $outputPage = $this->getContext()->getOutput();
1580 // Used in wikilinks, should not contain whitespaces
1581 $titleText = $this->getTitle()->getPrefixedDBkey();
1582 // If the user is not allowed to see it...
1583 if ( !$this->mRevisionRecord->userCan(
1584 RevisionRecord::DELETED_TEXT,
1585 $this->getContext()->getAuthority()
1586 ) ) {
1587 $outputPage->addHTML(
1588 Html::warningBox(
1589 $outputPage->msg( 'rev-deleted-text-permission', $titleText )->parse(),
1590 'plainlinks'
1591 )
1592 );
1593
1594 return false;
1595 // If the user needs to confirm that they want to see it...
1596 } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) != 1 ) {
1597 # Give explanation and add a link to view the revision...
1598 $oldid = intval( $this->getOldID() );
1599 $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" );
1600 $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ?
1601 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
1602 $outputPage->addHTML(
1603 Html::warningBox(
1604 $outputPage->msg( $msg, $link )->parse(),
1605 'plainlinks'
1606 )
1607 );
1608
1609 return false;
1610 // We are allowed to see...
1611 } else {
1612 $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
1613 ? [ 'rev-suppressed-text-view', $titleText ]
1614 : [ 'rev-deleted-text-view', $titleText ];
1615 $outputPage->addHTML(
1616 Html::warningBox(
1617 $outputPage->msg( $msg[0], $msg[1] )->parse(),
1618 'plainlinks'
1619 )
1620 );
1621
1622 return true;
1623 }
1624 }
1625
1634 public function setOldSubtitle( $oldid = 0 ) {
1635 if ( !$this->getHookRunner()->onDisplayOldSubtitle( $this, $oldid ) ) {
1636 return;
1637 }
1638
1639 $context = $this->getContext();
1640 $unhide = $context->getRequest()->getInt( 'unhide' ) == 1;
1641
1642 # Cascade unhide param in links for easy deletion browsing
1643 $extraParams = [];
1644 if ( $unhide ) {
1645 $extraParams['unhide'] = 1;
1646 }
1647
1648 if ( $this->mRevisionRecord && $this->mRevisionRecord->getId() === $oldid ) {
1649 $revisionRecord = $this->mRevisionRecord;
1650 } else {
1651 $revisionRecord = $this->revisionStore->getRevisionById( $oldid );
1652 }
1653
1654 $timestamp = $revisionRecord->getTimestamp();
1655
1656 $current = ( $oldid == $this->mPage->getLatest() );
1657 $language = $context->getLanguage();
1658 $user = $context->getUser();
1659
1660 $td = $language->userTimeAndDate( $timestamp, $user );
1661 $tddate = $language->userDate( $timestamp, $user );
1662 $tdtime = $language->userTime( $timestamp, $user );
1663
1664 # Show user links if allowed to see them. If hidden, then show them only if requested...
1665 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable revisionRecord known to exists
1666 $userlinks = Linker::revUserTools( $revisionRecord, !$unhide );
1667
1668 $infomsg = $current && !$context->msg( 'revision-info-current' )->isDisabled()
1669 ? 'revision-info-current'
1670 : 'revision-info';
1671
1672 $outputPage = $context->getOutput();
1673 $outputPage->addModuleStyles( [
1674 'mediawiki.action.styles',
1675 'mediawiki.interface.helpers.styles'
1676 ] );
1677
1678 $revisionUser = $revisionRecord->getUser();
1679 $revisionInfo = "<div id=\"mw-{$infomsg}\">" .
1680 $context->msg( $infomsg, $td )
1681 ->rawParams( $userlinks )
1682 ->params(
1683 $revisionRecord->getId(),
1684 $tddate,
1685 $tdtime,
1686 $revisionUser ? $revisionUser->getName() : ''
1687 )
1688 ->rawParams( $this->commentFormatter->formatRevision(
1689 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable revisionRecord known to exists
1690 $revisionRecord,
1691 $user,
1692 true,
1693 !$unhide
1694 ) )
1695 ->parse() .
1696 "</div>";
1697
1698 $lnk = $current
1699 ? $context->msg( 'currentrevisionlink' )->escaped()
1700 : $this->linkRenderer->makeKnownLink(
1701 $this->getTitle(),
1702 $context->msg( 'currentrevisionlink' )->text(),
1703 [],
1704 $extraParams
1705 );
1706 $curdiff = $current
1707 ? $context->msg( 'diff' )->escaped()
1708 : $this->linkRenderer->makeKnownLink(
1709 $this->getTitle(),
1710 $context->msg( 'diff' )->text(),
1711 [],
1712 [
1713 'diff' => 'cur',
1714 'oldid' => $oldid
1715 ] + $extraParams
1716 );
1717 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable revisionRecord known to exists
1718 $prevExist = (bool)$this->revisionStore->getPreviousRevision( $revisionRecord );
1719 $prevlink = $prevExist
1720 ? $this->linkRenderer->makeKnownLink(
1721 $this->getTitle(),
1722 $context->msg( 'previousrevision' )->text(),
1723 [],
1724 [
1725 'direction' => 'prev',
1726 'oldid' => $oldid
1727 ] + $extraParams
1728 )
1729 : $context->msg( 'previousrevision' )->escaped();
1730 $prevdiff = $prevExist
1731 ? $this->linkRenderer->makeKnownLink(
1732 $this->getTitle(),
1733 $context->msg( 'diff' )->text(),
1734 [],
1735 [
1736 'diff' => 'prev',
1737 'oldid' => $oldid
1738 ] + $extraParams
1739 )
1740 : $context->msg( 'diff' )->escaped();
1741 $nextlink = $current
1742 ? $context->msg( 'nextrevision' )->escaped()
1743 : $this->linkRenderer->makeKnownLink(
1744 $this->getTitle(),
1745 $context->msg( 'nextrevision' )->text(),
1746 [],
1747 [
1748 'direction' => 'next',
1749 'oldid' => $oldid
1750 ] + $extraParams
1751 );
1752 $nextdiff = $current
1753 ? $context->msg( 'diff' )->escaped()
1754 : $this->linkRenderer->makeKnownLink(
1755 $this->getTitle(),
1756 $context->msg( 'diff' )->text(),
1757 [],
1758 [
1759 'diff' => 'next',
1760 'oldid' => $oldid
1761 ] + $extraParams
1762 );
1763
1764 $cdel = Linker::getRevDeleteLink(
1765 $context->getAuthority(),
1766 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable revisionRecord known to exists
1767 $revisionRecord,
1768 $this->getTitle()
1769 );
1770 if ( $cdel !== '' ) {
1771 $cdel .= ' ';
1772 }
1773
1774 // the outer div is need for styling the revision info and nav in MobileFrontend
1775 $outputPage->addSubtitle(
1776 Html::warningBox(
1777 $revisionInfo .
1778 "<div id=\"mw-revision-nav\">" . $cdel .
1779 $context->msg( 'revision-nav' )->rawParams(
1780 $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff
1781 )->escaped() . "</div>",
1782 'mw-revision'
1783 )
1784 );
1785 }
1786
1799 public static function getRedirectHeaderHtml( Language $lang, $target, $forceKnown = false ) {
1800 if ( is_array( $target ) ) {
1801 // Up until 1.39, $target was allowed to be an array.
1802 wfDeprecatedMsg( 'The $target parameter can no longer be an array', '1.39' );
1803 $target = reset( $target ); // There really can only be one element (T296430)
1804 }
1805
1806 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1807
1808 $html = '<ul class="redirectText">';
1809 if ( $forceKnown ) {
1810 $link = $linkRenderer->makeKnownLink(
1811 $target,
1812 $target->getFullText(),
1813 [],
1814 // Make sure wiki page redirects are not followed
1815 $target->isRedirect() ? [ 'redirect' => 'no' ] : []
1816 );
1817 } else {
1818 $link = $linkRenderer->makeLink(
1819 $target,
1820 $target->getFullText(),
1821 [],
1822 // Make sure wiki page redirects are not followed
1823 $target->isRedirect() ? [ 'redirect' => 'no' ] : []
1824 );
1825 }
1826 $html .= '<li>' . $link . '</li>';
1827 $html .= '</ul>';
1828
1829 $redirectToText = wfMessage( 'redirectto' )->inLanguage( $lang )->escaped();
1830
1831 return '<div class="redirectMsg">' .
1832 '<p>' . $redirectToText . '</p>' .
1833 $html .
1834 '</div>';
1835 }
1836
1845 public function addHelpLink( $to, $overrideBaseUrl = false ) {
1846 $out = $this->getContext()->getOutput();
1847 $msg = $out->msg( 'namespace-' . $this->getTitle()->getNamespace() . '-helppage' );
1848
1849 if ( !$msg->isDisabled() ) {
1850 $title = Title::newFromText( $msg->plain() );
1851 if ( $title instanceof Title ) {
1852 $out->addHelpLink( $title->getLocalURL(), true );
1853 }
1854 } else {
1855 $out->addHelpLink( $to, $overrideBaseUrl );
1856 }
1857 }
1858
1862 public function render() {
1863 $this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' );
1864 $this->getContext()->getOutput()->setArticleBodyOnly( true );
1865 // We later set 'enableSectionEditLinks=false' based on this; also used by ImagePage
1866 $this->viewIsRenderAction = true;
1867 $this->view();
1868 }
1869
1873 public function protect() {
1874 $form = new ProtectionForm( $this );
1875 $form->execute();
1876 }
1877
1881 public function unprotect() {
1882 $this->protect();
1883 }
1884
1898 public function doDelete( $reason, $suppress = false, $immediate = false ) {
1899 wfDeprecated( __METHOD__, '1.37' );
1900 $error = '';
1901 $context = $this->getContext();
1902 $outputPage = $context->getOutput();
1903 $user = $context->getUser();
1904 $status = $this->mPage->doDeleteArticleReal(
1905 $reason, $user, $suppress, null, $error,
1906 null, [], 'delete', $immediate
1907 );
1908
1909 if ( $status->isOK() ) {
1910 $deleted = $this->getTitle()->getPrefixedText();
1911
1912 $outputPage->setPageTitle( $context->msg( 'actioncomplete' ) );
1913 $outputPage->setRobotPolicy( 'noindex,nofollow' );
1914
1915 if ( $status->isGood() ) {
1916 $loglink = '[[Special:Log/delete|' . $context->msg( 'deletionlog' )->text() . ']]';
1917 $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
1918 $this->getHookRunner()->onArticleDeleteAfterSuccess( $this->getTitle(), $outputPage );
1919 } else {
1920 $outputPage->addWikiMsg( 'delete-scheduled', wfEscapeWikiText( $deleted ) );
1921 }
1922
1923 $outputPage->returnToMain( false );
1924 } else {
1925 $outputPage->setPageTitle(
1926 $context->msg( 'cannotdelete-title',
1927 $this->getTitle()->getPrefixedText() )
1928 );
1929
1930 if ( $error == '' ) {
1931 $outputPage->wrapWikiTextAsInterface(
1932 'error mw-error-cannotdelete',
1933 $status->getWikiText( false, false, $context->getLanguage() )
1934 );
1935 $deleteLogPage = new LogPage( 'delete' );
1936 $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) );
1937
1938 LogEventsList::showLogExtract(
1939 $outputPage,
1940 'delete',
1941 $this->getTitle()
1942 );
1943 } else {
1944 $outputPage->addHTML( $error );
1945 }
1946 }
1947 }
1948
1949 /* Caching functions */
1950
1958 protected function tryFileCache() {
1959 static $called = false;
1960
1961 if ( $called ) {
1962 wfDebug( "Article::tryFileCache(): called twice!?" );
1963 return false;
1964 }
1965
1966 $called = true;
1967 if ( $this->isFileCacheable() ) {
1968 $cache = new HTMLFileCache( $this->getTitle(), 'view' );
1969 if ( $cache->isCacheGood( $this->mPage->getTouched() ) ) {
1970 wfDebug( "Article::tryFileCache(): about to load file" );
1971 $cache->loadFromFileCache( $this->getContext() );
1972 return true;
1973 } else {
1974 wfDebug( "Article::tryFileCache(): starting buffer" );
1975 ob_start( [ &$cache, 'saveToFileCache' ] );
1976 }
1977 } else {
1978 wfDebug( "Article::tryFileCache(): not cacheable" );
1979 }
1980
1981 return false;
1982 }
1983
1989 public function isFileCacheable( $mode = HTMLFileCache::MODE_NORMAL ) {
1990 $cacheable = false;
1991
1992 if ( HTMLFileCache::useFileCache( $this->getContext(), $mode ) ) {
1993 $cacheable = $this->mPage->getId()
1994 && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect();
1995 // Extension may have reason to disable file caching on some pages.
1996 if ( $cacheable ) {
1997 $cacheable = $this->getHookRunner()->onIsFileCacheable( $this ) ?? false;
1998 }
1999 }
2000
2001 return $cacheable;
2002 }
2003
2017 public function getParserOutput( $oldid = null, UserIdentity $user = null ) {
2018 if ( $user === null ) {
2019 $parserOptions = $this->getParserOptions();
2020 } else {
2021 $parserOptions = $this->mPage->makeParserOptions( $user );
2022 $parserOptions->setRenderReason( 'page-view' );
2023 }
2024
2025 return $this->mPage->getParserOutput( $parserOptions, $oldid );
2026 }
2027
2032 public function getParserOptions() {
2033 $parserOptions = $this->mPage->makeParserOptions( $this->getContext() );
2034 $parserOptions->setRenderReason( 'page-view' );
2035 return $parserOptions;
2036 }
2037
2044 public function setContext( $context ) {
2045 $this->mContext = $context;
2046 }
2047
2054 public function getContext() {
2055 if ( $this->mContext instanceof IContextSource ) {
2056 return $this->mContext;
2057 } else {
2058 wfDebug( __METHOD__ . " called and \$mContext is null. " .
2059 "Return RequestContext::getMain()" );
2060 return RequestContext::getMain();
2061 }
2062 }
2063
2073 public function __get( $fname ) {
2074 wfDeprecatedMsg( "Accessing Article::\$$fname is deprecated since MediaWiki 1.35",
2075 '1.35' );
2076
2077 if ( property_exists( $this->mPage, $fname ) ) {
2078 return $this->mPage->$fname;
2079 }
2080 trigger_error( 'Inaccessible property via __get(): ' . $fname, E_USER_NOTICE );
2081 }
2082
2092 public function __set( $fname, $fvalue ) {
2093 wfDeprecatedMsg( "Setting Article::\$$fname is deprecated since MediaWiki 1.35",
2094 '1.35' );
2095
2096 if ( property_exists( $this->mPage, $fname ) ) {
2097 $this->mPage->$fname = $fvalue;
2098 // Note: extensions may want to toss on new fields
2099 } elseif ( !in_array( $fname, [ 'mContext', 'mPage' ] ) ) {
2100 $this->mPage->$fname = $fvalue;
2101 } else {
2102 trigger_error( 'Inaccessible property via __set(): ' . $fname, E_USER_NOTICE );
2103 }
2104 }
2105
2111 public function getActionOverrides() {
2112 return $this->mPage->getActionOverrides();
2113 }
2114
2120 public function getTimestamp() {
2121 wfDeprecated( __METHOD__, '1.35' );
2122 return $this->mPage->getTimestamp();
2123 }
2124}
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.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
getContext()
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:56
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition Article.php:221
getContext()
Gets the context this Article is executed in.
Definition Article.php:2054
getOldIDFromRequest()
Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect.
Definition Article.php:295
getRedirectedFrom()
Get the page this view was redirected from.
Definition Article.php:232
Title null $mRedirectedFrom
Title from which we were redirected here, if any.
Definition Article.php:77
bool $viewIsRenderAction
Whether render() was called.
Definition Article.php:100
view()
This is the default action of the index.php entry point: just view the page of the given title.
Definition Article.php:454
__construct(Title $title, $oldId=null)
Definition Article.php:148
getRobotPolicy( $action, ParserOutput $pOutput=null)
Get the robot policy to be used for the current view.
Definition Article.php:978
static purgePatrolFooterCache( $articleID)
Purge the cache used to check if it is worth showing the patrol footer For example,...
Definition Article.php:1372
doDelete( $reason, $suppress=false, $immediate=false)
Perform a deletion and output success or failure messages.
Definition Article.php:1898
ParserOutput null false $mParserOutput
The ParserOutput generated for viewing the page, initialized by view().
Definition Article.php:93
getOldID()
Definition Article.php:282
LinkRenderer $linkRenderer
Definition Article.php:105
getTitle()
Get the title object of the article.
Definition Article.php:250
getActionOverrides()
Call to WikiPage function for backwards compatibility.
Definition Article.php:2111
isDiffOnlyView()
Definition Article.php:964
adjustDisplayTitle(ParserOutput $pOutput)
Adjust title for pages with displaytitle, -{T|}- or language conversion.
Definition Article.php:890
showDeletedRevisionHeader()
If the revision requested for view is deleted, check permissions.
Definition Article.php:1574
getParserOptions()
Get parser options suitable for rendering the primary article wikitext.
Definition Article.php:2032
IContextSource null $mContext
The context this Article is executed in.
Definition Article.php:65
getParserOutput( $oldid=null, UserIdentity $user=null)
#-
Definition Article.php:2017
static getRedirectHeaderHtml(Language $lang, $target, $forceKnown=false)
Return the HTML for the top of a redirect page.
Definition Article.php:1799
protect()
action=protect handler
Definition Article.php:1873
string false $mRedirectUrl
URL to redirect to or false if none.
Definition Article.php:80
isCurrent()
Returns true if the currently-referenced revision is the current edit to this page (and it exists).
Definition Article.php:421
showMissingArticle()
Show the error text for a missing article.
Definition Article.php:1381
__set( $fname, $fvalue)
Definition Article.php:2092
unprotect()
action=unprotect handler (alias)
Definition Article.php:1881
newPage(Title $title)
Definition Article.php:167
getPage()
Get the WikiPage object of this instance.
Definition Article.php:260
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition Article.php:1845
getTimestamp()
Definition Article.php:2120
static newFromID( $id)
Constructor from a page id.
Definition Article.php:176
int null $mOldId
The oldid of the article that was requested to be shown, 0 for the current revision.
Definition Article.php:74
static formatRobotPolicy( $policy)
Converts a String robot policy into an associative array, to allow merging of several policies using ...
Definition Article.php:1062
fetchRevisionRecord()
Fetches the revision to work on.
Definition Article.php:363
showPatrolFooter()
If patrol is possible, output a patrol UI box.
Definition Article.php:1198
setOldSubtitle( $oldid=0)
Generate the navigation links when browsing through an article revisions It shows the information as:...
Definition Article.php:1634
showViewFooter()
Show the footer section of an ordinary page view.
Definition Article.php:1174
WikiPage $mPage
The WikiPage object of this instance.
Definition Article.php:68
setRedirectedFrom(Title $from)
Tell the page view functions that this view was redirected from another page on the wiki.
Definition Article.php:241
isFileCacheable( $mode=HTMLFileCache::MODE_NORMAL)
Check if the page can be cached.
Definition Article.php:1989
tryFileCache()
checkLastModified returns true if it has taken care of all output to the client that is necessary for...
Definition Article.php:1958
getRevIdFetched()
Use this to fetch the rev ID used on page views.
Definition Article.php:440
showNamespaceHeader()
Show a header specific to the namespace currently being viewed, like [[MediaWiki:Talkpagetext]].
Definition Article.php:1162
__get( $fname)
Definition Article.php:2073
static newFromTitle( $title, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition Article.php:188
showDiffPage()
Show a diff page according to current request variables.
Definition Article.php:905
render()
Handle action=render.
Definition Article.php:1862
showRedirectedFromHeader()
If this request is a redirect view, send "redirected from" subtitle to the output.
Definition Article.php:1089
setContext( $context)
Sets the context this Article is executed in.
Definition Article.php:2044
getCacheRevisionId()
Definition CacheTime.php:98
Special handling for category description pages.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
Page view caching in the file system.
static useFileCache(IContextSource $context, $mode=self::MODE_NORMAL)
Check if pages can be cached for this request/user.
Rendering of file description pages.
Definition ImagePage.php:35
Handle enqueueing of background jobs.
Base class for language-specific code.
Definition Language.php:56
Class to simplify the use of log pages.
Definition LogPage.php:41
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...
The HTML user interface for page editing.
Definition EditPage.php:149
This class is a collection of static functions that serve two purposes:
Definition Html.php:55
Class that generates HTML for internal links.
makeKnownLink( $target, $text=null, array $extraAttribs=[], array $query=[])
Make a link that's styled as if the target page exists (usually a "blue link", although the styling m...
makeLink( $target, $text=null, array $extraAttribs=[], array $query=[])
Render a wikilink.
Some internal bits split of from Skin.php.
Definition Linker.php:67
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Service for getting rendered output of a given page.
Handles the page protection UI and backend.
Service for creating WikiPage objects.
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.
Represents a title within MediaWiki.
Definition Title.php:82
UserNameUtils service.
Provides access to user options.
This is one of the Core classes and should be read at least once by any new developers.
disable()
Disable output completely, i.e.
addWikiMsg(... $args)
Add a wikitext-formatted message to the output.
wrapWikiTextAsInterface( $wrapperClass, $text)
Convert wikitext in the user interface language to HTML and add it to the buffer with a <div class="$...
setArticleFlag( $newVal)
Set whether the displayed content is related to the source of the corresponding article on the wiki S...
setRobotPolicy( $policy)
Set the robot policy for the page: http://www.robotstxt.org/meta.html
setIndexPolicy( $policy)
Set the index policy for the page, but leave the follow policy un- touched.
setPageTitle( $name)
"Page title" means the contents of <h1>.
redirect( $url, $responsecode='302')
Redirect to $url rather than displaying the normal page.
setLastModified( $timestamp)
Override the last modified timestamp.
adaptCdnTTL( $mtime, $minTTL=0, $maxTTL=0)
Get TTL in [$minTTL,$maxTTL] and pass it to lowerCdnMaxage()
setRevisionIsCurrent(bool $isCurrent)
Set whether the revision displayed (as set in ::setRevisionId()) is the latest revision of the page.
wrapWikiMsg( $wrap,... $msgSpecs)
This function takes a number of message/argument specifications, wraps them in some overall structure...
setCdnMaxage( $maxage)
Set the value of the "s-maxage" part of the "Cache-control" HTTP header.
parseAsContent( $text, $linestart=true)
Parse wikitext in the page content language and return the HTML.
addParserOutput(ParserOutput $parserOutput, $poOptions=[])
Add everything from a ParserOutput object.
addWikiTextAsInterface( $text, $linestart=true, PageReference $title=null)
Convert wikitext in the user interface language to HTML and add it to the buffer.
setFollowPolicy( $policy)
Set the follow policy for the page, but leave the index policy un- touched.
isPrintable()
Return whether the page is "printable".
addModuleStyles( $modules)
Load the styles of one or more style-only ResourceLoader modules on this page.
setHTMLTitle( $name)
"HTML title" means the contents of "<title>".
returnToMain( $unused=null, $returnto=null, $returntoquery=null)
Add a "return to" link pointing to a specified title, or the title indicated in the request,...
disableClientCache()
Force the page to send nocache headers.
addSubtitle( $str)
Add $str to the subtitle.
setRevisionId( $revid)
Set the revision ID which will be seen by the wiki text parser for things such as embedded {{REVISION...
clearHTML()
Clear the body HTML.
setPreventClickjacking(bool $enable)
Set the prevent-clickjacking flag.
addHTML( $text)
Append $text to the body HTML.
addJsConfigVars( $keys, $value=null)
Add one or more variables to be set in mw.config in JavaScript.
setCanonicalUrl( $url)
Set the URL to be used for the <link rel=canonical>>.
setRevisionTimestamp( $timestamp)
Set the timestamp of the revision which will be displayed.
setRedirectedFrom(PageReference $t)
Set $mRedirectedFrom, the page which redirected us to the current page.
prependHTML( $text)
Prepend $text to the body HTML.
addModules( $modules)
Load one or more ResourceLoader modules on this page.
addWikiTextAsContent( $text, $linestart=true, PageReference $title=null)
Convert wikitext in the page content language to HTML and add it to the buffer.
Used to show archived pages and eventually restore them.
Set options of the Parser.
setIsPrintable( $x)
Parsing the printable version of the page?
setRenderReason(string $renderReason)
Sets reason for rendering the content.
static formatPageTitle( $nsText, $nsSeparator, $mainText)
Add HTML tags marking the parts of a page title, to be displayed in the first heading of the page.
Definition Parser.php:6474
static newSpec(int $revisionId, $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.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:46
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition Status.php:192
static newFromName( $name, $validate='valid')
Definition User.php:592
Base representation for an editable wiki page.
Definition WikiPage.php:75
getTitle()
Get the title object of the article.
Definition WikiPage.php:318
Interface for objects which can provide a MediaWiki context on request.
This interface represents the authority associated the current execution context, such as a web reque...
Definition Authority.php:37
Interface for objects representing user identity.
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition Page.php:31
const DB_REPLICA
Definition defines.php:26
$content
Definition router.php:76
return true
Definition router.php:92
if(!isset( $args[0])) $lang