MediaWiki REL1_36
Article.php
Go to the documentation of this file.
1<?php
24use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
32use Wikimedia\IPUtils;
33use Wikimedia\NonSerializable\NonSerializableTrait;
34
45class Article implements Page {
46 use ProtectedHookAccessorTrait;
47 use NonSerializableTrait;
48
54 protected $mContext;
55
57 protected $mPage;
58
63 public $mOldId;
64
66 public $mRedirectedFrom = null;
67
69 public $mRedirectUrl = false;
70
75 private $fetchResult = null;
76
82 public $mParserOutput = null;
83
89 protected $viewIsRenderAction = false;
90
94 protected $linkRenderer;
95
100
110 private $mRevisionRecord = null;
111
116 public function __construct( Title $title, $oldId = null ) {
117 $this->mOldId = $oldId;
118 $this->mPage = $this->newPage( $title );
119
120 $services = MediaWikiServices::getInstance();
121 $this->linkRenderer = $services->getLinkRenderer();
122 $this->revisionStore = $services->getRevisionStore();
123 }
124
129 protected function newPage( Title $title ) {
130 return new WikiPage( $title );
131 }
132
138 public static function newFromID( $id ) {
139 $t = Title::newFromID( $id );
140 return $t == null ? null : new static( $t );
141 }
142
150 public static function newFromTitle( $title, IContextSource $context ) {
151 if ( $title->getNamespace() === NS_MEDIA ) {
152 // XXX: This should not be here, but where should it go?
153 $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
154 }
155
156 $page = null;
157 Hooks::runner()->onArticleFromTitle( $title, $page, $context );
158 if ( !$page ) {
159 switch ( $title->getNamespace() ) {
160 case NS_FILE:
161 $page = new ImagePage( $title );
162 break;
163 case NS_CATEGORY:
164 $page = new CategoryPage( $title );
165 break;
166 default:
167 $page = new Article( $title );
168 }
169 }
170 $page->setContext( $context );
171
172 return $page;
173 }
174
182 public static function newFromWikiPage( WikiPage $page, IContextSource $context ) {
183 $article = self::newFromTitle( $page->getTitle(), $context );
184 $article->mPage = $page; // override to keep process cached vars
185 return $article;
186 }
187
193 public function getRedirectedFrom() {
194 return $this->mRedirectedFrom;
195 }
196
202 public function setRedirectedFrom( Title $from ) {
203 $this->mRedirectedFrom = $from;
204 }
205
211 public function getTitle() {
212 return $this->mPage->getTitle();
213 }
214
221 public function getPage() {
222 return $this->mPage;
223 }
224
225 public function clear() {
226 $this->mRedirectedFrom = null; # Title object if set
227 $this->mRedirectUrl = false;
228 $this->mRevisionRecord = null;
229 $this->fetchResult = null;
230
231 // TODO hard-deprecate direct access to public fields
232
233 $this->mPage->clear();
234 }
235
253 protected function getContentObject() {
254 wfDeprecated( __METHOD__, '1.32' );
255 $content = null;
256 if ( $this->mPage->getId() === 0 ) {
258 } else {
259 $revision = $this->fetchRevisionRecord();
260 if ( $revision ) {
261 $content = $revision->getContent(
262 SlotRecord::MAIN,
263 RevisionRecord::FOR_THIS_USER,
264 $this->getContext()->getUser()
265 );
266 }
267 }
268 return $content;
269 }
270
276 private function getSubstituteContent() {
277 # If this is a MediaWiki:x message, then load the messages
278 # and return the message value for x.
279 if ( $this->getTitle()->getNamespace() === NS_MEDIAWIKI ) {
280 $text = $this->getTitle()->getDefaultMessageText();
281 if ( $text === false ) {
282 $text = '';
283 }
284
285 $content = ContentHandler::makeContent( $text, $this->getTitle() );
286 } else {
287 $message = $this->getContext()->getUser()->isRegistered() ? 'noarticletext' : 'noarticletextanon';
288 $content = new MessageContent( $message, null );
289 }
290
291 return $content;
292 }
293
301 public function getOldID() {
302 if ( $this->mOldId === null ) {
303 $this->mOldId = $this->getOldIDFromRequest();
304 }
305
306 return $this->mOldId;
307 }
308
314 public function getOldIDFromRequest() {
315 $this->mRedirectUrl = false;
316
317 $request = $this->getContext()->getRequest();
318 $oldid = $request->getIntOrNull( 'oldid' );
319
320 if ( $oldid === null ) {
321 return 0;
322 }
323
324 if ( $oldid !== 0 ) {
325 # Load the given revision and check whether the page is another one.
326 # In that case, update this instance to reflect the change.
327 if ( $oldid === $this->mPage->getLatest() ) {
328 $this->mRevisionRecord = $this->mPage->getRevisionRecord();
329 } else {
330 $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
331 if ( $this->mRevisionRecord !== null ) {
332 $revPageId = $this->mRevisionRecord->getPageId();
333 // Revision title doesn't match the page title given?
334 if ( $this->mPage->getId() != $revPageId ) {
335 $function = get_class( $this->mPage ) . '::newFromID';
336 $this->mPage = $function( $revPageId );
337 }
338 }
339 }
340 }
341
342 $oldRev = $this->mRevisionRecord;
343 if ( $request->getVal( 'direction' ) == 'next' ) {
344 $nextid = 0;
345 if ( $oldRev ) {
346 $nextRev = $this->revisionStore->getNextRevision( $oldRev );
347 if ( $nextRev ) {
348 $nextid = $nextRev->getId();
349 }
350 }
351 if ( $nextid ) {
352 $oldid = $nextid;
353 $this->mRevisionRecord = null;
354 } else {
355 $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' );
356 }
357 } elseif ( $request->getVal( 'direction' ) == 'prev' ) {
358 $previd = 0;
359 if ( $oldRev ) {
360 $prevRev = $this->revisionStore->getPreviousRevision( $oldRev );
361 if ( $prevRev ) {
362 $previd = $prevRev->getId();
363 }
364 }
365 if ( $previd ) {
366 $oldid = $previd;
367 $this->mRevisionRecord = null;
368 }
369 }
370
371 return $oldid;
372 }
373
383 public function fetchRevisionRecord() {
384 if ( $this->fetchResult ) {
385 return $this->mRevisionRecord;
386 }
387
388 $oldid = $this->getOldID();
389
390 // $this->mRevisionRecord might already be fetched by getOldIDFromRequest()
391 if ( !$this->mRevisionRecord ) {
392 if ( !$oldid ) {
393 $this->mRevisionRecord = $this->mPage->getRevisionRecord();
394
395 if ( !$this->mRevisionRecord ) {
396 wfDebug( __METHOD__ . " failed to find page data for title " .
397 $this->getTitle()->getPrefixedText() );
398
399 // Just for sanity, output for this case is done by showMissingArticle().
400 $this->fetchResult = Status::newFatal( 'noarticletext' );
401 return null;
402 }
403 } else {
404 $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
405
406 if ( !$this->mRevisionRecord ) {
407 wfDebug( __METHOD__ . " failed to load revision, rev_id $oldid" );
408
409 $this->fetchResult = Status::newFatal( 'missing-revision', $oldid );
410 return null;
411 }
412 }
413 }
414
415 if ( !$this->mRevisionRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getContext()->getAuthority() ) ) {
416 wfDebug( __METHOD__ . " failed to retrieve content of revision " .
417 $this->mRevisionRecord->getId() );
418
419 // Just for sanity, output for this case is done by showDeletedRevisionHeader().
420 // title used in wikilinks, should not contain whitespaces
421 $this->fetchResult = Status::newFatal(
422 'rev-deleted-text-permission', $this->getTitle()->getPrefixedDBkey() );
423 return null;
424 }
425
426 $this->fetchResult = Status::newGood( $this->mRevisionRecord );
427 return $this->mRevisionRecord;
428 }
429
435 public function isCurrent() {
436 # If no oldid, this is the current version.
437 if ( $this->getOldID() == 0 ) {
438 return true;
439 }
440
441 return $this->mPage->exists() &&
442 $this->mRevisionRecord &&
443 $this->mRevisionRecord->isCurrent();
444 }
445
455 public function getRevisionFetched() {
456 wfDeprecated( __METHOD__, '1.35' );
457 $revRecord = $this->fetchRevisionRecord();
458
459 return $revRecord ? new Revision( $revRecord ) : null;
460 }
461
470 public function getRevIdFetched() {
471 if ( $this->fetchResult && $this->fetchResult->isOK() ) {
472 return $this->fetchResult->value->getId();
473 } else {
474 return $this->mPage->getLatest();
475 }
476 }
477
482 public function view() {
483 global $wgUseFileCache;
484
485 # Get variables from query string
486 # As side effect this will load the revision and update the title
487 # in a revision ID is passed in the request, so this should remain
488 # the first call of this method even if $oldid is used way below.
489 $oldid = $this->getOldID();
490
491 $user = $this->getContext()->getUser();
492 # Another whitelist check in case getOldID() is altering the title
493 $permissionStatus = PermissionStatus::newEmpty();
494 if ( !$this->getContext()->getAuthority()
495 ->authorizeRead( 'read', $this->getTitle(), $permissionStatus )
496 ) {
497 wfDebug( __METHOD__ . ": denied on secondary read check" );
498 throw new PermissionsError( 'read', $permissionStatus );
499 }
500
501 $outputPage = $this->getContext()->getOutput();
502 # getOldID() may as well want us to redirect somewhere else
503 if ( $this->mRedirectUrl ) {
504 $outputPage->redirect( $this->mRedirectUrl );
505 wfDebug( __METHOD__ . ": redirecting due to oldid" );
506
507 return;
508 }
509
510 # If we got diff in the query, we want to see a diff page instead of the article.
511 if ( $this->getContext()->getRequest()->getCheck( 'diff' ) ) {
512 wfDebug( __METHOD__ . ": showing diff page" );
513 $this->showDiffPage();
514
515 return;
516 }
517
518 # Set page title (may be overridden by DISPLAYTITLE)
519 $outputPage->setPageTitle( $this->getTitle()->getPrefixedText() );
520
521 $outputPage->setArticleFlag( true );
522 # Allow frames by default
523 $outputPage->allowClickjacking();
524
525 $parserOptions = $this->getParserOptions();
526 $poOptions = [];
527 # Allow extensions to vary parser options used for article rendering
528 Hooks::runner()->onArticleParserOptions( $this, $parserOptions );
529 # Render printable version, use printable version cache
530 if ( $outputPage->isPrintable() ) {
531 $parserOptions->setIsPrintable( true );
532 $poOptions['enableSectionEditLinks'] = false;
533 $outputPage->prependHTML(
534 Html::warningBox(
535 $outputPage->msg( 'printableversion-deprecated-warning' )->escaped()
536 )
537 );
538 } elseif ( $this->viewIsRenderAction || !$this->isCurrent() ||
539 !$this->getContext()->getAuthority()->probablyCan( 'edit', $this->getTitle() )
540 ) {
541 $poOptions['enableSectionEditLinks'] = false;
542 }
543
544 # Try client and file cache
545 if ( $oldid === 0 && $this->mPage->checkTouched() ) {
546 # Try to stream the output from file cache
547 if ( $wgUseFileCache && $this->tryFileCache() ) {
548 wfDebug( __METHOD__ . ": done file cache" );
549 # tell wgOut that output is taken care of
550 $outputPage->disable();
551 $this->mPage->doViewUpdates( $user, $oldid );
552
553 return;
554 }
555 }
556
558 $this->showNamespaceHeader();
559
560 $continue =
561 $this->generateContentOutput( $user, $parserOptions, $oldid, $outputPage, $poOptions );
562
563 if ( !$continue ) {
564 return;
565 }
566
567 # For the main page, overwrite the <title> element with the con-
568 # tents of 'pagetitle-view-mainpage' instead of the default (if
569 # that's not empty).
570 # This message always exists because it is in the i18n files
571 if ( $this->getTitle()->isMainPage() ) {
572 $msg = wfMessage( 'pagetitle-view-mainpage' )->inContentLanguage();
573 if ( !$msg->isDisabled() ) {
574 $outputPage->setHTMLTitle( $msg->title( $this->getTitle() )->text() );
575 }
576 }
577
578 # Use adaptive TTLs for CDN so delayed/failed purges are noticed less often.
579 # This could use getTouched(), but that could be scary for major template edits.
580 $outputPage->adaptCdnTTL( $this->mPage->getTimestamp(), IExpiringStore::TTL_DAY );
581
582 $this->showViewFooter();
583 $this->mPage->doViewUpdates( $user, $oldid ); // FIXME: test this
584
585 # Load the postEdit module if the user just saved this revision
586 # See also EditPage::setPostEditCookie
587 $request = $this->getContext()->getRequest();
589 $postEdit = $request->getCookie( $cookieKey );
590 if ( $postEdit ) {
591 # Clear the cookie. This also prevents caching of the response.
592 $request->response()->clearCookie( $cookieKey );
593 $outputPage->addJsConfigVars( 'wgPostEdit', $postEdit );
594 $outputPage->addModules( 'mediawiki.action.view.postEdit' ); // FIXME: test this
595 }
596 }
597
610 private function generateContentOutput(
611 User $user,
612 ParserOptions $parserOptions,
613 int $oldid,
614 OutputPage $outputPage,
615 array $textOptions
616 ): bool {
617 # Should the parser cache be used?
618 $useParserCache = true;
619 $pOutput = null;
620 $parserOutputAccess = MediaWikiServices::getInstance()->getParserOutputAccess();
621
622 // NOTE: $outputDone and $useParserCache may be changed by the hook
623 $this->getHookRunner()->onArticleViewHeader( $this, $outputDone, $useParserCache );
624 if ( $outputDone ) {
625 if ( $outputDone instanceof ParserOutput ) {
626 $pOutput = $outputDone;
627 }
628
629 if ( $pOutput ) {
630 $this->doOutputMetaData( $pOutput, $outputPage );
631 }
632 return true;
633 }
634
635 // Early abort if the page doesn't exist
636 if ( !$this->mPage->exists() ) {
637 wfDebug( __METHOD__ . ": showing missing article" );
638 $this->showMissingArticle();
639 $this->mPage->doViewUpdates( $user );
640 return false; // skip all further output to OutputPage
641 }
642
643 // Try the latest parser cache
644 // NOTE: try latest-revision cache first to avoid loading revision.
645 if ( $useParserCache && !$oldid ) {
646 $pOutput = $parserOutputAccess->getCachedParserOutput(
647 $this->getPage(),
648 $parserOptions,
649 null,
650 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked
651 );
652
653 if ( $pOutput ) {
654 $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
655 $this->doOutputMetaData( $pOutput, $outputPage );
656 return true;
657 }
658 }
659
660 $rev = $this->fetchRevisionRecord();
661 if ( !$this->fetchResult->isOK() ) {
662 $this->showViewError( $this->fetchResult->getWikiText(
663 false, false, $this->getContext()->getLanguage()
664 ) );
665 return true;
666 }
667
668 # Are we looking at an old revision
669 if ( $oldid ) {
670 $this->setOldSubtitle( $oldid );
671
672 if ( !$this->showDeletedRevisionHeader() ) {
673 wfDebug( __METHOD__ . ": cannot view deleted revision" );
674 return false; // skip all further output to OutputPage
675 }
676
677 // Try the old revision parser cache
678 // NOTE: Repeating cache check for old revision to avoid fetching $rev
679 // before it's absolutely necessary.
680 if ( $useParserCache ) {
681 $pOutput = $parserOutputAccess->getCachedParserOutput(
682 $this->getPage(),
683 $parserOptions,
684 $rev,
685 ParserOutputAccess::OPT_NO_AUDIENCE_CHECK // we already checked in fetchRevisionRevord
686 );
687
688 if ( $pOutput ) {
689 $this->doOutputFromParserCache( $pOutput, $outputPage, $textOptions );
690 $this->doOutputMetaData( $pOutput, $outputPage );
691 return true;
692 }
693 }
694 }
695
696 # Ensure that UI elements requiring revision ID have
697 # the correct version information.
698 $outputPage->setRevisionId( $this->getRevIdFetched() );
699 # Preload timestamp to avoid a DB hit
700 $outputPage->setRevisionTimestamp( $this->mPage->getTimestamp() );
701
702 # Pages containing custom CSS or JavaScript get special treatment
703 if ( $this->getTitle()->isSiteConfigPage() || $this->getTitle()->isUserConfigPage() ) {
704 $dir = $this->getContext()->getLanguage()->getDir();
705 $lang = $this->getContext()->getLanguage()->getHtmlCode();
706
707 $outputPage->wrapWikiMsg(
708 "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
709 'clearyourcache'
710 );
711 $outputPage->addModuleStyles( 'mediawiki.action.styles' );
712 } elseif ( !$this->getHookRunner()->onArticleRevisionViewCustom(
713 $rev,
714 $this->getTitle(),
715 $oldid,
716 $outputPage )
717 ) {
718 // NOTE: sync with hooks called in DifferenceEngine::renderNewRevision()
719 // Allow extensions do their own custom view for certain pages
720 $this->doOutputMetaData( $pOutput, $outputPage );
721 return true;
722 }
723
724 # Run the parse, protected by a pool counter
725 wfDebug( __METHOD__ . ": doing uncached parse" );
726
727 if ( !$rev ) {
728 // No revision, abort! Shouldn't happen.
729 return false;
730 }
731
732 $opt = 0;
733
734 // we already checked the cache in case 2, don't check again.
735 $opt |= ParserOutputAccess::OPT_NO_CHECK_CACHE;
736
737 // we already checked in fetchRevisionRecord()
738 $opt |= ParserOutputAccess::OPT_NO_AUDIENCE_CHECK;
739
740 if ( !$rev->getId() || !$useParserCache ) {
741 // fake revision or uncacheable options
742 $opt |= ParserOutputAccess::OPT_NO_CACHE;
743 }
744
745 $renderStatus = $parserOutputAccess->getParserOutput(
746 $this->getPage(),
747 $parserOptions,
748 $rev,
749 $opt
750 );
751
753 $rev,
754 $renderStatus,
755 $outputPage,
756 $textOptions
757 );
758
759 if ( !$renderStatus->isOK() ) {
760 return true;
761 }
762
763 $pOutput = $renderStatus->getValue();
764 $this->doOutputMetaData( $pOutput, $outputPage );
765 return true;
766 }
767
772 private function doOutputMetaData( ?ParserOutput $pOutput, OutputPage $outputPage ) {
773 # Adjust title for main page & pages with displaytitle
774 if ( $pOutput ) {
775 $this->adjustDisplayTitle( $pOutput );
776 }
777
778 # Check for any __NOINDEX__ tags on the page using $pOutput
779 $policy = $this->getRobotPolicy( 'view', $pOutput ?: null );
780 $outputPage->setIndexPolicy( $policy['index'] );
781 $outputPage->setFollowPolicy( $policy['follow'] ); // FIXME: test this
782
783 $this->mParserOutput = $pOutput;
784 }
785
791 private function doOutputFromParserCache(
792 ParserOutput $pOutput,
793 OutputPage $outputPage,
794 array $textOptions
795 ) {
796 $outputPage->addParserOutput( $pOutput, $textOptions );
797 # Ensure that UI elements requiring revision ID have
798 # the correct version information.
799 $outputPage->setRevisionId( $pOutput->getCacheRevisionId() ?? $this->mPage->getLatest() );
800 # Preload timestamp to avoid a DB hit
801 $cachedTimestamp = $pOutput->getTimestamp();
802 if ( $cachedTimestamp !== null ) {
803 $outputPage->setRevisionTimestamp( $cachedTimestamp );
804 $this->mPage->setTimestamp( $cachedTimestamp );
805 }
806 }
807
814 private function doOutputFromRenderStatus(
815 ?RevisionRecord $rev,
816 Status $renderStatus,
817 OutputPage $outputPage,
818 array $textOptions
819 ) {
820 global $wgCdnMaxageStale;
821 $ok = $renderStatus->isOK();
822
823 $pOutput = $ok ? $renderStatus->getValue() : null;
824
825 // Cache stale ParserOutput object with a short expiry
826 if ( $ok && $renderStatus->hasMessage( 'view-pool-dirty-output' ) ) {
827 $outputPage->setCdnMaxage( $wgCdnMaxageStale );
828 $outputPage->setLastModified( $pOutput->getCacheTime() );
829 $staleReason = $renderStatus->hasMessage( 'view-pool-contention' )
830 ? $this->getContext()->msg( 'view-pool-contention' )
831 : $this->getContext()->msg( 'view-pool-timeout' );
832 $outputPage->addHTML( "<!-- parser cache is expired, " .
833 "sending anyway due to $staleReason-->\n" );
834 }
835
836 if ( !$renderStatus->isOK() ) {
837 $this->showViewError( $renderStatus->getWikiText(
838 false, 'view-pool-error', $this->getContext()->getLanguage()
839 ) );
840 return;
841 }
842
843 if ( $pOutput ) {
844 $outputPage->addParserOutput( $pOutput, $textOptions );
845 }
846
847 if ( $this->getRevisionRedirectTarget( $rev ) ) {
848 $outputPage->addSubtitle( "<span id=\"redirectsub\">" .
849 $this->getContext()->msg( 'redirectpagesub' )->parse() . "</span>" );
850 }
851 }
852
857 private function getRevisionRedirectTarget( RevisionRecord $revision ) {
858 // TODO: find a *good* place for the code that determines the redirect target for
859 // a given revision!
860 // NOTE: Use main slot content. Compare code in DerivedPageDataUpdater::revisionIsRedirect.
861 $content = $revision->getContent( SlotRecord::MAIN );
862 return $content ? $content->getRedirectTarget() : null;
863 }
864
869 public function adjustDisplayTitle( ParserOutput $pOutput ) {
870 $out = $this->getContext()->getOutput();
871
872 # Adjust the title if it was set by displaytitle, -{T|}- or language conversion
873 $titleText = $pOutput->getTitleText();
874 if ( strval( $titleText ) !== '' ) {
875 $out->setPageTitle( $titleText );
876 $out->setDisplayTitle( $titleText );
877 }
878 }
879
884 protected function showDiffPage() {
885 $request = $this->getContext()->getRequest();
886 $user = $this->getContext()->getUser();
887 $diff = $request->getVal( 'diff' );
888 $rcid = $request->getVal( 'rcid' );
889 $diffOnly = $request->getBool( 'diffonly', $user->getOption( 'diffonly' ) );
890 $purge = $request->getVal( 'action' ) == 'purge';
891 $unhide = $request->getInt( 'unhide' ) == 1;
892 $oldid = $this->getOldID();
893
894 $rev = $this->fetchRevisionRecord();
895
896 if ( !$rev ) {
897 // T213621: $rev maybe null due to either lack of permission to view the
898 // revision or actually not existing. So let's try loading it from the id
899 $rev = $this->revisionStore->getRevisionById( $oldid );
900 if ( $rev ) {
901 // Revision exists but $user lacks permission to diff it.
902 // Do nothing here.
903 // The $rev will later be used to create standard diff elements however.
904 } else {
905 $this->getContext()->getOutput()->setPageTitle( wfMessage( 'errorpagetitle' ) );
906 $msg = $this->getContext()->msg( 'difference-missing-revision' )
907 ->params( $oldid )
908 ->numParams( 1 )
909 ->parseAsBlock();
910 $this->getContext()->getOutput()->addHTML( $msg );
911 return;
912 }
913 }
914
915 $contentHandler = MediaWikiServices::getInstance()
916 ->getContentHandlerFactory()
917 ->getContentHandler(
918 $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
919 );
920 $de = $contentHandler->createDifferenceEngine(
921 $this->getContext(),
922 $oldid,
923 $diff,
924 $rcid,
925 $purge,
926 $unhide
927 );
928 $de->setSlotDiffOptions( [
929 'diff-type' => $request->getVal( 'diff-type' )
930 ] );
931 $de->showDiffPage( $diffOnly );
932
933 // Run view updates for the newer revision being diffed (and shown
934 // below the diff if not $diffOnly).
935 list( $old, $new ) = $de->mapDiffPrevNext( $oldid, $diff );
936 // New can be false, convert it to 0 - this conveniently means the latest revision
937 $this->mPage->doViewUpdates( $user, (int)$new );
938 }
939
947 public function getRobotPolicy( $action, ParserOutput $pOutput = null ) {
949
950 $ns = $this->getTitle()->getNamespace();
951
952 # Don't index user and user talk pages for blocked users (T13443)
953 if ( ( $ns === NS_USER || $ns === NS_USER_TALK ) && !$this->getTitle()->isSubpage() ) {
954 $specificTarget = null;
955 $vagueTarget = null;
956 $titleText = $this->getTitle()->getText();
957 if ( IPUtils::isValid( $titleText ) ) {
958 $vagueTarget = $titleText;
959 } else {
960 $specificTarget = $titleText;
961 }
962 if ( DatabaseBlock::newFromTarget( $specificTarget, $vagueTarget ) instanceof DatabaseBlock ) {
963 return [
964 'index' => 'noindex',
965 'follow' => 'nofollow'
966 ];
967 }
968 }
969
970 if ( $this->mPage->getId() === 0 || $this->getOldID() ) {
971 # Non-articles (special pages etc), and old revisions
972 return [
973 'index' => 'noindex',
974 'follow' => 'nofollow'
975 ];
976 } elseif ( $this->getContext()->getOutput()->isPrintable() ) {
977 # Discourage indexing of printable versions, but encourage following
978 return [
979 'index' => 'noindex',
980 'follow' => 'follow'
981 ];
982 } elseif ( $this->getContext()->getRequest()->getInt( 'curid' ) ) {
983 # For ?curid=x urls, disallow indexing
984 return [
985 'index' => 'noindex',
986 'follow' => 'follow'
987 ];
988 }
989
990 # Otherwise, construct the policy based on the various config variables.
991 $policy = self::formatRobotPolicy( $wgDefaultRobotPolicy );
992
993 if ( isset( $wgNamespaceRobotPolicies[$ns] ) ) {
994 # Honour customised robot policies for this namespace
995 $policy = array_merge(
996 $policy,
997 self::formatRobotPolicy( $wgNamespaceRobotPolicies[$ns] )
998 );
999 }
1000 if ( $this->getTitle()->canUseNoindex() && is_object( $pOutput ) && $pOutput->getIndexPolicy() ) {
1001 # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates
1002 # a final sanity check that we have really got the parser output.
1003 $policy = array_merge(
1004 $policy,
1005 [ 'index' => $pOutput->getIndexPolicy() ]
1006 );
1007 }
1008
1009 if ( isset( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] ) ) {
1010 # (T16900) site config can override user-defined __INDEX__ or __NOINDEX__
1011 $policy = array_merge(
1012 $policy,
1013 self::formatRobotPolicy( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] )
1014 );
1015 }
1016
1017 return $policy;
1018 }
1019
1027 public static function formatRobotPolicy( $policy ) {
1028 if ( is_array( $policy ) ) {
1029 return $policy;
1030 } elseif ( !$policy ) {
1031 return [];
1032 }
1033
1034 $policy = explode( ',', $policy );
1035 $policy = array_map( 'trim', $policy );
1036
1037 $arr = [];
1038 foreach ( $policy as $var ) {
1039 if ( in_array( $var, [ 'index', 'noindex' ] ) ) {
1040 $arr['index'] = $var;
1041 } elseif ( in_array( $var, [ 'follow', 'nofollow' ] ) ) {
1042 $arr['follow'] = $var;
1043 }
1044 }
1045
1046 return $arr;
1047 }
1048
1056 public function showRedirectedFromHeader() {
1057 global $wgRedirectSources;
1058
1059 $context = $this->getContext();
1060 $outputPage = $context->getOutput();
1061 $request = $context->getRequest();
1062 $rdfrom = $request->getVal( 'rdfrom' );
1063
1064 // Construct a URL for the current page view, but with the target title
1065 $query = $request->getValues();
1066 unset( $query['rdfrom'] );
1067 unset( $query['title'] );
1068 if ( $this->getTitle()->isRedirect() ) {
1069 // Prevent double redirects
1070 $query['redirect'] = 'no';
1071 }
1072 $redirectTargetUrl = $this->getTitle()->getLinkURL( $query );
1073
1074 if ( isset( $this->mRedirectedFrom ) ) {
1075 // This is an internally redirected page view.
1076 // We'll need a backlink to the source page for navigation.
1077 if ( $this->getHookRunner()->onArticleViewRedirect( $this ) ) {
1078 $redir = $this->linkRenderer->makeKnownLink(
1079 $this->mRedirectedFrom,
1080 null,
1081 [],
1082 [ 'redirect' => 'no' ]
1083 );
1084
1085 $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1086 $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1087 . "</span>" );
1088
1089 // Add the script to update the displayed URL and
1090 // set the fragment if one was specified in the redirect
1091 $outputPage->addJsConfigVars( [
1092 'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1093 ] );
1094 $outputPage->addModules( 'mediawiki.action.view.redirect' );
1095
1096 // Add a <link rel="canonical"> tag
1097 $outputPage->setCanonicalUrl( $this->getTitle()->getCanonicalURL() );
1098
1099 // Tell the output object that the user arrived at this article through a redirect
1100 $outputPage->setRedirectedFrom( $this->mRedirectedFrom );
1101
1102 return true;
1103 }
1104 } elseif ( $rdfrom ) {
1105 // This is an externally redirected view, from some other wiki.
1106 // If it was reported from a trusted site, supply a backlink.
1107 if ( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) {
1108 $redir = Linker::makeExternalLink( $rdfrom, $rdfrom );
1109 $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
1110 $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
1111 . "</span>" );
1112
1113 // Add the script to update the displayed URL
1114 $outputPage->addJsConfigVars( [
1115 'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
1116 ] );
1117 $outputPage->addModules( 'mediawiki.action.view.redirect' );
1118
1119 return true;
1120 }
1121 }
1122
1123 return false;
1124 }
1125
1130 public function showNamespaceHeader() {
1131 if ( $this->getTitle()->isTalkPage() && !wfMessage( 'talkpageheader' )->isDisabled() ) {
1132 $this->getContext()->getOutput()->wrapWikiMsg(
1133 "<div class=\"mw-talkpageheader\">\n$1\n</div>",
1134 [ 'talkpageheader' ]
1135 );
1136 }
1137 }
1138
1142 public function showViewFooter() {
1143 # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
1144 if ( $this->getTitle()->getNamespace() === NS_USER_TALK
1145 && IPUtils::isValid( $this->getTitle()->getText() )
1146 ) {
1147 $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' );
1148 }
1149
1150 // Show a footer allowing the user to patrol the shown revision or page if possible
1151 $patrolFooterShown = $this->showPatrolFooter();
1152
1153 $this->getHookRunner()->onArticleViewFooter( $this, $patrolFooterShown );
1154 }
1155
1166 public function showPatrolFooter() {
1168
1169 // Allow hooks to decide whether to not output this at all
1170 if ( !$this->getHookRunner()->onArticleShowPatrolFooter( $this ) ) {
1171 return false;
1172 }
1173
1174 $outputPage = $this->getContext()->getOutput();
1175 $user = $this->getContext()->getUser();
1176 $title = $this->getTitle();
1177 $rc = false;
1178
1179 if ( !$this->getContext()->getAuthority()->probablyCan( 'patrol', $title )
1181 || ( $wgUseFilePatrol && $title->inNamespace( NS_FILE ) ) )
1182 ) {
1183 // Patrolling is disabled or the user isn't allowed to
1184 return false;
1185 }
1186
1187 if ( $this->mRevisionRecord
1188 && !RecentChange::isInRCLifespan( $this->mRevisionRecord->getTimestamp(), 21600 )
1189 ) {
1190 // The current revision is already older than what could be in the RC table
1191 // 6h tolerance because the RC might not be cleaned out regularly
1192 return false;
1193 }
1194
1195 // Check for cached results
1196 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1197 $key = $cache->makeKey( 'unpatrollable-page', $title->getArticleID() );
1198 if ( $cache->get( $key ) ) {
1199 return false;
1200 }
1201
1202 $dbr = wfGetDB( DB_REPLICA );
1203 $oldestRevisionTimestamp = $dbr->selectField(
1204 'revision',
1205 'MIN( rev_timestamp )',
1206 [ 'rev_page' => $title->getArticleID() ],
1207 __METHOD__
1208 );
1209
1210 // New page patrol: Get the timestamp of the oldest revison which
1211 // the revision table holds for the given page. Then we look
1212 // whether it's within the RC lifespan and if it is, we try
1213 // to get the recentchanges row belonging to that entry
1214 // (with rc_new = 1).
1215 $recentPageCreation = false;
1216 if ( $oldestRevisionTimestamp
1217 && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 )
1218 ) {
1219 // 6h tolerance because the RC might not be cleaned out regularly
1220 $recentPageCreation = true;
1221 $rc = RecentChange::newFromConds(
1222 [
1223 'rc_new' => 1,
1224 'rc_timestamp' => $oldestRevisionTimestamp,
1225 'rc_namespace' => $title->getNamespace(),
1226 'rc_cur_id' => $title->getArticleID()
1227 ],
1228 __METHOD__
1229 );
1230 if ( $rc ) {
1231 // Use generic patrol message for new pages
1232 $markPatrolledMsg = wfMessage( 'markaspatrolledtext' );
1233 }
1234 }
1235
1236 // File patrol: Get the timestamp of the latest upload for this page,
1237 // check whether it is within the RC lifespan and if it is, we try
1238 // to get the recentchanges row belonging to that entry
1239 // (with rc_type = RC_LOG, rc_log_type = upload).
1240 $recentFileUpload = false;
1241 if ( ( !$rc || $rc->getAttribute( 'rc_patrolled' ) ) && $wgUseFilePatrol
1242 && $title->getNamespace() === NS_FILE ) {
1243 // Retrieve timestamp of most recent upload
1244 $newestUploadTimestamp = $dbr->selectField(
1245 'image',
1246 'MAX( img_timestamp )',
1247 [ 'img_name' => $title->getDBkey() ],
1248 __METHOD__
1249 );
1250 if ( $newestUploadTimestamp
1251 && RecentChange::isInRCLifespan( $newestUploadTimestamp, 21600 )
1252 ) {
1253 // 6h tolerance because the RC might not be cleaned out regularly
1254 $recentFileUpload = true;
1255 $rc = RecentChange::newFromConds(
1256 [
1257 'rc_type' => RC_LOG,
1258 'rc_log_type' => 'upload',
1259 'rc_timestamp' => $newestUploadTimestamp,
1260 'rc_namespace' => NS_FILE,
1261 'rc_cur_id' => $title->getArticleID()
1262 ],
1263 __METHOD__
1264 );
1265 if ( $rc ) {
1266 // Use patrol message specific to files
1267 $markPatrolledMsg = wfMessage( 'markaspatrolledtext-file' );
1268 }
1269 }
1270 }
1271
1272 if ( !$recentPageCreation && !$recentFileUpload ) {
1273 // Page creation and latest upload (for files) is too old to be in RC
1274
1275 // We definitely can't patrol so cache the information
1276 // When a new file version is uploaded, the cache is cleared
1277 $cache->set( $key, '1' );
1278
1279 return false;
1280 }
1281
1282 if ( !$rc ) {
1283 // Don't cache: This can be hit if the page gets accessed very fast after
1284 // its creation / latest upload or in case we have high replica DB lag. In case
1285 // the revision is too old, we will already return above.
1286 return false;
1287 }
1288
1289 if ( $rc->getAttribute( 'rc_patrolled' ) ) {
1290 // Patrolled RC entry around
1291
1292 // Cache the information we gathered above in case we can't patrol
1293 // Don't cache in case we can patrol as this could change
1294 $cache->set( $key, '1' );
1295
1296 return false;
1297 }
1298
1299 if ( $rc->getPerformer()->equals( $user ) ) {
1300 // Don't show a patrol link for own creations/uploads. If the user could
1301 // patrol them, they already would be patrolled
1302 return false;
1303 }
1304
1305 $outputPage->preventClickjacking();
1306 if ( $this->getContext()->getAuthority()->isAllowed( 'writeapi' ) ) {
1307 $outputPage->addModules( 'mediawiki.misc-authed-curate' );
1308 }
1309
1310 $link = $this->linkRenderer->makeKnownLink(
1311 $title,
1312 $markPatrolledMsg->text(),
1313 [],
1314 [
1315 'action' => 'markpatrolled',
1316 'rcid' => $rc->getAttribute( 'rc_id' ),
1317 ]
1318 );
1319
1320 $outputPage->addHTML(
1321 "<div class='patrollink' data-mw='interface'>" .
1322 wfMessage( 'markaspatrolledlink' )->rawParams( $link )->escaped() .
1323 '</div>'
1324 );
1325
1326 return true;
1327 }
1328
1335 public static function purgePatrolFooterCache( $articleID ) {
1336 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
1337 $cache->delete( $cache->makeKey( 'unpatrollable-page', $articleID ) );
1338 }
1339
1344 public function showMissingArticle() {
1345 global $wgSend404Code;
1346
1347 $outputPage = $this->getContext()->getOutput();
1348 // Whether the page is a root user page of an existing user (but not a subpage)
1349 $validUserPage = false;
1350
1351 $title = $this->getTitle();
1352
1353 $services = MediaWikiServices::getInstance();
1354
1355 $contextUser = $this->getContext()->getUser();
1356
1357 # Show info in user (talk) namespace. Does the user exist? Is he blocked?
1358 if ( $title->getNamespace() === NS_USER
1359 || $title->getNamespace() === NS_USER_TALK
1360 ) {
1361 $rootPart = explode( '/', $title->getText() )[0];
1362 $user = User::newFromName( $rootPart, false /* allow IP users */ );
1363 $ip = User::isIP( $rootPart );
1364 $block = DatabaseBlock::newFromTarget( $user, $user );
1365
1366 if ( $user && $user->isRegistered() && $user->isHidden() &&
1367 !$this->getContext()->getAuthority()->isAllowed( 'hideuser' )
1368 ) {
1369 // T120883 if the user is hidden and the viewer cannot see hidden
1370 // users, pretend like it does not exist at all.
1371 $user = false;
1372 }
1373 if ( !( $user && $user->isRegistered() ) && !$ip ) { # User does not exist
1374 $outputPage->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
1375 [ 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) ] );
1376 } elseif (
1377 $block !== null &&
1378 $block->getType() != DatabaseBlock::TYPE_AUTO &&
1379 ( $block->isSitewide() || $user->isBlockedFrom( $title ) )
1380 ) {
1381 // Show log extract if the user is sitewide blocked or is partially
1382 // blocked and not allowed to edit their user page or user talk page
1383 LogEventsList::showLogExtract(
1384 $outputPage,
1385 'block',
1386 $services->getNamespaceInfo()->getCanonicalName( NS_USER ) . ':' .
1387 $block->getTarget(),
1388 '',
1389 [
1390 'lim' => 1,
1391 'showIfEmpty' => false,
1392 'msgKey' => [
1393 'blocked-notice-logextract',
1394 $user->getName() # Support GENDER in notice
1395 ]
1396 ]
1397 );
1398 $validUserPage = !$title->isSubpage();
1399 } else {
1400 $validUserPage = !$title->isSubpage();
1401 }
1402 }
1403
1404 $this->getHookRunner()->onShowMissingArticle( $this );
1405
1406 # Show delete and move logs if there were any such events.
1407 # The logging query can DOS the site when bots/crawlers cause 404 floods,
1408 # so be careful showing this. 404 pages must be cheap as they are hard to cache.
1409 $dbCache = ObjectCache::getInstance( 'db-replicated' );
1410 $key = $dbCache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
1411 $isRegistered = $contextUser->isRegistered();
1412 $sessionExists = $this->getContext()->getRequest()->getSession()->isPersistent();
1413
1414 if ( $isRegistered || $dbCache->get( $key ) || $sessionExists ) {
1415 $logTypes = [ 'delete', 'move', 'protect' ];
1416
1417 $dbr = wfGetDB( DB_REPLICA );
1418
1419 $conds = [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ];
1420 // Give extensions a chance to hide their (unrelated) log entries
1421 $this->getHookRunner()->onArticle__MissingArticleConditions( $conds, $logTypes );
1422 LogEventsList::showLogExtract(
1423 $outputPage,
1424 $logTypes,
1425 $title,
1426 '',
1427 [
1428 'lim' => 10,
1429 'conds' => $conds,
1430 'showIfEmpty' => false,
1431 'msgKey' => [ $isRegistered || $sessionExists
1432 ? 'moveddeleted-notice'
1433 : 'moveddeleted-notice-recent'
1434 ]
1435 ]
1436 );
1437 }
1438
1439 if ( !$this->mPage->hasViewableContent() && $wgSend404Code && !$validUserPage ) {
1440 // If there's no backing content, send a 404 Not Found
1441 // for better machine handling of broken links.
1442 $this->getContext()->getRequest()->response()->statusHeader( 404 );
1443 }
1444
1445 // Also apply the robot policy for nonexisting pages (even if a 404 was used for sanity)
1446 $policy = $this->getRobotPolicy( 'view' );
1447 $outputPage->setIndexPolicy( $policy['index'] );
1448 $outputPage->setFollowPolicy( $policy['follow'] );
1449
1450 $hookResult = $this->getHookRunner()->onBeforeDisplayNoArticleText( $this );
1451
1452 if ( !$hookResult ) {
1453 return;
1454 }
1455
1456 # Show error message
1457 $oldid = $this->getOldID();
1458 if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) {
1459 $text = $this->getTitle()->getDefaultMessageText() ?? '';
1460 $outputPage->addWikiTextAsContent( $text );
1461 } else {
1462 if ( $oldid ) {
1463 // T251066: Try loading the revision from the archive table.
1464 // Show link to view it if it exists and the user has permission to view it.
1465 $pa = new PageArchive( $title, $this->getContext()->getConfig() );
1466 $revRecord = $pa->getArchivedRevisionRecord( $oldid );
1467 if ( $revRecord && $revRecord->userCan(
1468 RevisionRecord::DELETED_TEXT,
1469 $this->getContext()->getAuthority()
1470 ) ) {
1471 $text = wfMessage(
1472 'missing-revision-permission', $oldid,
1473 $revRecord->getTimestamp(),
1474 $title->getPrefixedDBkey()
1475 )->plain();
1476 } else {
1477 $text = wfMessage( 'missing-revision', $oldid )->plain();
1478 }
1479
1480 } elseif ( $this->getContext()->getAuthority()->probablyCan( 'create', $title ) &&
1481 $this->getContext()->getAuthority()->probablyCan( 'edit', $title )
1482 ) {
1483 $message = $isRegistered ? 'noarticletext' : 'noarticletextanon';
1484 $text = wfMessage( $message )->plain();
1485 } else {
1486 $text = wfMessage( 'noarticletext-nopermission' )->plain();
1487 }
1488
1489 $dir = $this->getContext()->getLanguage()->getDir();
1490 $lang = $this->getContext()->getLanguage()->getHtmlCode();
1491 $outputPage->addWikiTextAsInterface( Xml::openElement( 'div', [
1492 'class' => "noarticletext mw-content-$dir",
1493 'dir' => $dir,
1494 'lang' => $lang,
1495 ] ) . "\n$text\n</div>" );
1496 }
1497 }
1498
1503 private function showViewError( string $errortext ) {
1504 $outputPage = $this->getContext()->getOutput();
1505 $outputPage->setPageTitle( $this->getContext()->msg( 'errorpagetitle' ) );
1506 $outputPage->enableClientCache( false );
1507 $outputPage->setRobotPolicy( 'noindex,nofollow' );
1508 $outputPage->clearHTML();
1509 $outputPage->wrapWikiTextAsInterface( 'errorbox', $errortext );
1510 }
1511
1518 public function showDeletedRevisionHeader() {
1519 if ( !$this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
1520 // Not deleted
1521 return true;
1522 }
1523 $outputPage = $this->getContext()->getOutput();
1524 $user = $this->getContext()->getUser();
1525 // Used in wikilinks, should not contain whitespaces
1526 $titleText = $this->getTitle()->getPrefixedDBkey();
1527 // If the user is not allowed to see it...
1528 if ( !$this->mRevisionRecord->userCan(
1529 RevisionRecord::DELETED_TEXT,
1530 $this->getContext()->getAuthority()
1531 ) ) {
1532 $outputPage->addHtml(
1533 Html::warningBox(
1534 $outputPage->msg( 'rev-deleted-text-permission', $titleText )->parse(),
1535 'plainlinks'
1536 )
1537 );
1538
1539 return false;
1540 // If the user needs to confirm that they want to see it...
1541 } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) != 1 ) {
1542 # Give explanation and add a link to view the revision...
1543 $oldid = intval( $this->getOldID() );
1544 $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" );
1545 $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ?
1546 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
1547 $outputPage->addHtml(
1548 Html::warningBox(
1549 $outputPage->msg( $msg, $link )->parse(),
1550 'plainlinks'
1551 )
1552 );
1553
1554 return false;
1555 // We are allowed to see...
1556 } else {
1557 $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
1558 ? [ 'rev-suppressed-text-view', $titleText ]
1559 : [ 'rev-deleted-text-view', $titleText ];
1560 $outputPage->addHtml(
1561 Html::warningBox(
1562 $outputPage->msg( $msg[0], $msg[1] )->parse(),
1563 'plainlinks'
1564 )
1565 );
1566
1567 return true;
1568 }
1569 }
1570
1579 public function setOldSubtitle( $oldid = 0 ) {
1580 if ( !$this->getHookRunner()->onDisplayOldSubtitle( $this, $oldid ) ) {
1581 return;
1582 }
1583
1584 $context = $this->getContext();
1585 $unhide = $context->getRequest()->getInt( 'unhide' ) == 1;
1586
1587 # Cascade unhide param in links for easy deletion browsing
1588 $extraParams = [];
1589 if ( $unhide ) {
1590 $extraParams['unhide'] = 1;
1591 }
1592
1593 if ( $this->mRevisionRecord && $this->mRevisionRecord->getId() === $oldid ) {
1594 $revisionRecord = $this->mRevisionRecord;
1595 } else {
1596 $revisionRecord = $this->revisionStore->getRevisionById( $oldid );
1597 }
1598
1599 $timestamp = $revisionRecord->getTimestamp();
1600
1601 $current = ( $oldid == $this->mPage->getLatest() );
1602 $language = $context->getLanguage();
1603 $user = $context->getUser();
1604
1605 $td = $language->userTimeAndDate( $timestamp, $user );
1606 $tddate = $language->userDate( $timestamp, $user );
1607 $tdtime = $language->userTime( $timestamp, $user );
1608
1609 # Show user links if allowed to see them. If hidden, then show them only if requested...
1610 $userlinks = Linker::revUserTools( $revisionRecord, !$unhide );
1611
1612 $infomsg = $current && !$context->msg( 'revision-info-current' )->isDisabled()
1613 ? 'revision-info-current'
1614 : 'revision-info';
1615
1616 $outputPage = $context->getOutput();
1617 $outputPage->addModuleStyles( [
1618 'mediawiki.action.styles',
1619 'mediawiki.interface.helpers.styles'
1620 ] );
1621
1622 $revisionUser = $revisionRecord->getUser();
1623 $revisionInfo = "<div id=\"mw-{$infomsg}\">" .
1624 $context->msg( $infomsg, $td )
1625 ->rawParams( $userlinks )
1626 ->params(
1627 $revisionRecord->getId(),
1628 $tddate,
1629 $tdtime,
1630 $revisionUser ? $revisionUser->getName() : ''
1631 )
1632 ->rawParams( Linker::revComment(
1633 $revisionRecord,
1634 true,
1635 true
1636 ) )
1637 ->parse() .
1638 "</div>";
1639
1640 $lnk = $current
1641 ? $context->msg( 'currentrevisionlink' )->escaped()
1642 : $this->linkRenderer->makeKnownLink(
1643 $this->getTitle(),
1644 $context->msg( 'currentrevisionlink' )->text(),
1645 [],
1646 $extraParams
1647 );
1648 $curdiff = $current
1649 ? $context->msg( 'diff' )->escaped()
1650 : $this->linkRenderer->makeKnownLink(
1651 $this->getTitle(),
1652 $context->msg( 'diff' )->text(),
1653 [],
1654 [
1655 'diff' => 'cur',
1656 'oldid' => $oldid
1657 ] + $extraParams
1658 );
1659 $prevExist = (bool)$this->revisionStore->getPreviousRevision( $revisionRecord );
1660 $prevlink = $prevExist
1661 ? $this->linkRenderer->makeKnownLink(
1662 $this->getTitle(),
1663 $context->msg( 'previousrevision' )->text(),
1664 [],
1665 [
1666 'direction' => 'prev',
1667 'oldid' => $oldid
1668 ] + $extraParams
1669 )
1670 : $context->msg( 'previousrevision' )->escaped();
1671 $prevdiff = $prevExist
1672 ? $this->linkRenderer->makeKnownLink(
1673 $this->getTitle(),
1674 $context->msg( 'diff' )->text(),
1675 [],
1676 [
1677 'diff' => 'prev',
1678 'oldid' => $oldid
1679 ] + $extraParams
1680 )
1681 : $context->msg( 'diff' )->escaped();
1682 $nextlink = $current
1683 ? $context->msg( 'nextrevision' )->escaped()
1684 : $this->linkRenderer->makeKnownLink(
1685 $this->getTitle(),
1686 $context->msg( 'nextrevision' )->text(),
1687 [],
1688 [
1689 'direction' => 'next',
1690 'oldid' => $oldid
1691 ] + $extraParams
1692 );
1693 $nextdiff = $current
1694 ? $context->msg( 'diff' )->escaped()
1695 : $this->linkRenderer->makeKnownLink(
1696 $this->getTitle(),
1697 $context->msg( 'diff' )->text(),
1698 [],
1699 [
1700 'diff' => 'next',
1701 'oldid' => $oldid
1702 ] + $extraParams
1703 );
1704
1706 $user,
1707 $revisionRecord,
1708 $this->getTitle()
1709 );
1710 if ( $cdel !== '' ) {
1711 $cdel .= ' ';
1712 }
1713
1714 // the outer div is need for styling the revision info and nav in MobileFrontend
1715 $outputPage->addSubtitle( "<div class=\"mw-revision warningbox\">" . $revisionInfo .
1716 "<div id=\"mw-revision-nav\">" . $cdel .
1717 $context->msg( 'revision-nav' )->rawParams(
1718 $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff
1719 )->escaped() . "</div></div>" );
1720 }
1721
1735 public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) {
1736 $lang = $this->getTitle()->getPageLanguage();
1737 $out = $this->getContext()->getOutput();
1738 if ( $appendSubtitle ) {
1739 $out->addSubtitle( wfMessage( 'redirectpagesub' ) );
1740 }
1741 $out->addModuleStyles( 'mediawiki.action.view.redirectPage' );
1742 return static::getRedirectHeaderHtml( $lang, $target, $forceKnown );
1743 }
1744
1757 public static function getRedirectHeaderHtml( Language $lang, $target, $forceKnown = false ) {
1758 if ( !is_array( $target ) ) {
1759 $target = [ $target ];
1760 }
1761
1762 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1763
1764 $html = '<ul class="redirectText">';
1766 foreach ( $target as $title ) {
1767 if ( $forceKnown ) {
1768 $link = $linkRenderer->makeKnownLink(
1769 $title,
1770 $title->getFullText(),
1771 [],
1772 // Make sure wiki page redirects are not followed
1773 $title->isRedirect() ? [ 'redirect' => 'no' ] : []
1774 );
1775 } else {
1776 $link = $linkRenderer->makeLink(
1777 $title,
1778 $title->getFullText(),
1779 [],
1780 // Make sure wiki page redirects are not followed
1781 $title->isRedirect() ? [ 'redirect' => 'no' ] : []
1782 );
1783 }
1784 $html .= '<li>' . $link . '</li>';
1785 }
1786 $html .= '</ul>';
1787
1788 $redirectToText = wfMessage( 'redirectto' )->inLanguage( $lang )->escaped();
1789
1790 return '<div class="redirectMsg">' .
1791 '<p>' . $redirectToText . '</p>' .
1792 $html .
1793 '</div>';
1794 }
1795
1804 public function addHelpLink( $to, $overrideBaseUrl = false ) {
1805 $out = $this->getContext()->getOutput();
1806 $msg = $out->msg( 'namespace-' . $this->getTitle()->getNamespace() . '-helppage' );
1807
1808 if ( !$msg->isDisabled() ) {
1809 $title = Title::newFromText( $msg->plain() );
1810 if ( $title instanceof Title ) {
1811 $out->addHelpLink( $title->getLocalURL(), true );
1812 }
1813 } else {
1814 $out->addHelpLink( $to, $overrideBaseUrl );
1815 }
1816 }
1817
1821 public function render() {
1822 $this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' );
1823 $this->getContext()->getOutput()->setArticleBodyOnly( true );
1824 // We later set 'enableSectionEditLinks=false' based on this; also used by ImagePage
1825 $this->viewIsRenderAction = true;
1826 $this->view();
1827 }
1828
1832 public function protect() {
1833 $form = new ProtectionForm( $this );
1834 $form->execute();
1835 }
1836
1840 public function unprotect() {
1841 $this->protect();
1842 }
1843
1847 public function delete() {
1848 # This code desperately needs to be totally rewritten
1849
1850 $title = $this->getTitle();
1851 $context = $this->getContext();
1852 $user = $context->getUser();
1853 $request = $context->getRequest();
1854
1855 # Check permissions
1856 $permissionStatus = PermissionStatus::newEmpty();
1857 if ( !$context->getAuthority()->authorizeWrite( 'delete', $title, $permissionStatus ) ) {
1858 throw new PermissionsError( 'delete', $permissionStatus );
1859 }
1860
1861 # Read-only check...
1862 if ( wfReadOnly() ) {
1863 throw new ReadOnlyError;
1864 }
1865
1866 # Better double-check that it hasn't been deleted yet!
1867 $this->mPage->loadPageData(
1868 $request->wasPosted() ? WikiPage::READ_LATEST : WikiPage::READ_NORMAL
1869 );
1870 if ( !$this->mPage->exists() ) {
1871 $deleteLogPage = new LogPage( 'delete' );
1872 $outputPage = $context->getOutput();
1873 $outputPage->setPageTitle( $context->msg( 'cannotdelete-title', $title->getPrefixedText() ) );
1874 $outputPage->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>",
1875 [ 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) ]
1876 );
1877 $outputPage->addHTML(
1878 Xml::element( 'h2', null, $deleteLogPage->getName()->text() )
1879 );
1880 LogEventsList::showLogExtract(
1881 $outputPage,
1882 'delete',
1883 $title
1884 );
1885
1886 return;
1887 }
1888
1889 $deleteReasonList = $request->getText( 'wpDeleteReasonList', 'other' );
1890 $deleteReason = $request->getText( 'wpReason' );
1891
1892 if ( $deleteReasonList == 'other' ) {
1893 $reason = $deleteReason;
1894 } elseif ( $deleteReason != '' ) {
1895 // Entry from drop down menu + additional comment
1896 $colonseparator = wfMessage( 'colon-separator' )->inContentLanguage()->text();
1897 $reason = $deleteReasonList . $colonseparator . $deleteReason;
1898 } else {
1899 $reason = $deleteReasonList;
1900 }
1901
1902 if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ),
1903 [ 'delete', $this->getTitle()->getPrefixedText() ] )
1904 ) {
1905 # Flag to hide all contents of the archived revisions
1906
1907 $suppress = $request->getCheck( 'wpSuppress' ) &&
1908 $context->getAuthority()->isAllowed( 'suppressrevision' );
1909
1910 $this->doDelete( $reason, $suppress );
1911
1912 WatchAction::doWatchOrUnwatch( $request->getCheck( 'wpWatch' ), $title, $context->getAuthority() );
1913
1914 return;
1915 }
1916
1917 // Generate deletion reason
1918 $hasHistory = false;
1919 if ( !$reason ) {
1920 try {
1921 $reason = $this->getPage()
1922 ->getAutoDeleteReason( $hasHistory );
1923 } catch ( Exception $e ) {
1924 # if a page is horribly broken, we still want to be able to
1925 # delete it. So be lenient about errors here.
1926 wfDebug( "Error while building auto delete summary: $e" );
1927 $reason = '';
1928 }
1929 }
1930
1931 // If the page has a history, insert a warning
1932 if ( $hasHistory ) {
1933 $title = $this->getTitle();
1934
1935 // The following can use the real revision count as this is only being shown for users
1936 // that can delete this page.
1937 // This, as a side-effect, also makes sure that the following query isn't being run for
1938 // pages with a larger history, unless the user has the 'bigdelete' right
1939 // (and is about to delete this page).
1940 $dbr = wfGetDB( DB_REPLICA );
1941 $revisions = $edits = (int)$dbr->selectField(
1942 'revision',
1943 'COUNT(rev_page)',
1944 [ 'rev_page' => $title->getArticleID() ],
1945 __METHOD__
1946 );
1947
1948 // @todo i18n issue/patchwork message
1949 $context->getOutput()->addHTML(
1950 '<strong class="mw-delete-warning-revisions">' .
1951 $context->msg( 'historywarning' )->numParams( $revisions )->parse() .
1952 $context->msg( 'word-separator' )->escaped() . $this->linkRenderer->makeKnownLink(
1953 $title,
1954 $context->msg( 'history' )->text(),
1955 [],
1956 [ 'action' => 'history' ] ) .
1957 '</strong>'
1958 );
1959
1960 if ( $title->isBigDeletion() ) {
1962 $context->getOutput()->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n",
1963 [
1964 'delete-warning-toobig',
1965 $context->getLanguage()->formatNum( $wgDeleteRevisionsLimit )
1966 ]
1967 );
1968 }
1969 }
1970
1971 $this->confirmDelete( $reason );
1972 }
1973
1979 public function confirmDelete( $reason ) {
1980 wfDebug( "Article::confirmDelete" );
1981
1982 $title = $this->getTitle();
1983 $ctx = $this->getContext();
1984 $outputPage = $ctx->getOutput();
1985 $outputPage->setPageTitle( wfMessage( 'delete-confirm', $title->getPrefixedText() ) );
1986 $outputPage->addBacklinkSubtitle( $title );
1987 $outputPage->setRobotPolicy( 'noindex,nofollow' );
1988 $outputPage->addModules( 'mediawiki.action.delete' );
1989 $outputPage->addModuleStyles( 'mediawiki.action.styles' );
1990
1991 $backlinkCache = $title->getBacklinkCache();
1992 if ( $backlinkCache->hasLinks( 'pagelinks' ) || $backlinkCache->hasLinks( 'templatelinks' ) ) {
1993 $outputPage->addHtml(
1994 Html::warningBox(
1995 $outputPage->msg( 'deleting-backlinks-warning' )->parse(),
1996 'plainlinks'
1997 )
1998 );
1999 }
2000
2001 $subpageQueryLimit = 51;
2002 $subpages = $title->getSubpages( $subpageQueryLimit );
2003 $subpageCount = count( $subpages );
2004 if ( $subpageCount > 0 ) {
2005 $outputPage->addHtml(
2006 Html::warningBox(
2007 $outputPage->msg( 'deleting-subpages-warning', Message::numParam( $subpageCount ) )->parse(),
2008 'plainlinks'
2009 )
2010 );
2011 }
2012 $outputPage->addWikiMsg( 'confirmdeletetext' );
2013
2014 $this->getHookRunner()->onArticleConfirmDelete( $this, $outputPage, $reason );
2015
2016 $user = $this->getContext()->getUser();
2017 $checkWatch = MediaWikiServices::getInstance()->getUserOptionsLookup()
2018 ->getBoolOption( $user, 'watchdeletion' ) || $user->isWatched( $title );
2019
2020 $outputPage->enableOOUI();
2021
2022 $fields = [];
2023
2024 $suppressAllowed = $this->getContext()->getAuthority()->isAllowed( 'suppressrevision' );
2025 $dropDownReason = $ctx->msg( 'deletereason-dropdown' )->inContentLanguage()->text();
2026 // Add additional specific reasons for suppress
2027 if ( $suppressAllowed ) {
2028 $dropDownReason .= "\n" . $ctx->msg( 'deletereason-dropdown-suppress' )
2029 ->inContentLanguage()->text();
2030 }
2031
2032 $options = Xml::listDropDownOptions(
2033 $dropDownReason,
2034 [ 'other' => $ctx->msg( 'deletereasonotherlist' )->inContentLanguage()->text() ]
2035 );
2036 $options = Xml::listDropDownOptionsOoui( $options );
2037
2038 $fields[] = new OOUI\FieldLayout(
2039 new OOUI\DropdownInputWidget( [
2040 'name' => 'wpDeleteReasonList',
2041 'inputId' => 'wpDeleteReasonList',
2042 'tabIndex' => 1,
2043 'infusable' => true,
2044 'value' => '',
2045 'options' => $options
2046 ] ),
2047 [
2048 'label' => $ctx->msg( 'deletecomment' )->text(),
2049 'align' => 'top',
2050 ]
2051 );
2052
2053 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
2054 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
2055 // Unicode codepoints.
2056 $fields[] = new OOUI\FieldLayout(
2057 new OOUI\TextInputWidget( [
2058 'name' => 'wpReason',
2059 'inputId' => 'wpReason',
2060 'tabIndex' => 2,
2061 'maxLength' => CommentStore::COMMENT_CHARACTER_LIMIT,
2062 'infusable' => true,
2063 'value' => $reason,
2064 'autofocus' => true,
2065 ] ),
2066 [
2067 'label' => $ctx->msg( 'deleteotherreason' )->text(),
2068 'align' => 'top',
2069 ]
2070 );
2071
2072 if ( $user->isRegistered() ) {
2073 $fields[] = new OOUI\FieldLayout(
2074 new OOUI\CheckboxInputWidget( [
2075 'name' => 'wpWatch',
2076 'inputId' => 'wpWatch',
2077 'tabIndex' => 3,
2078 'selected' => $checkWatch,
2079 ] ),
2080 [
2081 'label' => $ctx->msg( 'watchthis' )->text(),
2082 'align' => 'inline',
2083 'infusable' => true,
2084 ]
2085 );
2086 }
2087 if ( $suppressAllowed ) {
2088 $fields[] = new OOUI\FieldLayout(
2089 new OOUI\CheckboxInputWidget( [
2090 'name' => 'wpSuppress',
2091 'inputId' => 'wpSuppress',
2092 'tabIndex' => 4,
2093 ] ),
2094 [
2095 'label' => $ctx->msg( 'revdelete-suppress' )->text(),
2096 'align' => 'inline',
2097 'infusable' => true,
2098 ]
2099 );
2100 }
2101
2102 $fields[] = new OOUI\FieldLayout(
2103 new OOUI\ButtonInputWidget( [
2104 'name' => 'wpConfirmB',
2105 'inputId' => 'wpConfirmB',
2106 'tabIndex' => 5,
2107 'value' => $ctx->msg( 'deletepage' )->text(),
2108 'label' => $ctx->msg( 'deletepage' )->text(),
2109 'flags' => [ 'primary', 'destructive' ],
2110 'type' => 'submit',
2111 ] ),
2112 [
2113 'align' => 'top',
2114 ]
2115 );
2116
2117 $fieldset = new OOUI\FieldsetLayout( [
2118 'label' => $ctx->msg( 'delete-legend' )->text(),
2119 'id' => 'mw-delete-table',
2120 'items' => $fields,
2121 ] );
2122
2123 $form = new OOUI\FormLayout( [
2124 'method' => 'post',
2125 'action' => $title->getLocalURL( 'action=delete' ),
2126 'id' => 'deleteconfirm',
2127 ] );
2128 $form->appendContent(
2129 $fieldset,
2130 new OOUI\HtmlSnippet(
2131 Html::hidden( 'wpEditToken', $user->getEditToken( [ 'delete', $title->getPrefixedText() ] ) )
2132 )
2133 );
2134
2135 $outputPage->addHTML(
2136 new OOUI\PanelLayout( [
2137 'classes' => [ 'deletepage-wrapper' ],
2138 'expanded' => false,
2139 'padded' => true,
2140 'framed' => true,
2141 'content' => $form,
2142 ] )
2143 );
2144
2145 if ( $this->getContext()->getAuthority()->isAllowed( 'editinterface' ) ) {
2146 $link = '';
2147 if ( $suppressAllowed ) {
2148 $link .= $this->linkRenderer->makeKnownLink(
2149 $ctx->msg( 'deletereason-dropdown-suppress' )->inContentLanguage()->getTitle(),
2150 $ctx->msg( 'delete-edit-reasonlist-suppress' )->text(),
2151 [],
2152 [ 'action' => 'edit' ]
2153 );
2154 $link .= $ctx->msg( 'pipe-separator' )->escaped();
2155 }
2156 $link .= $this->linkRenderer->makeKnownLink(
2157 $ctx->msg( 'deletereason-dropdown' )->inContentLanguage()->getTitle(),
2158 $ctx->msg( 'delete-edit-reasonlist' )->text(),
2159 [],
2160 [ 'action' => 'edit' ]
2161 );
2162 $outputPage->addHTML( '<p class="mw-delete-editreasons">' . $link . '</p>' );
2163 }
2164
2165 $deleteLogPage = new LogPage( 'delete' );
2166 $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) );
2167 LogEventsList::showLogExtract( $outputPage, 'delete', $title );
2168 }
2169
2178 public function doDelete( $reason, $suppress = false, $immediate = false ) {
2179 $error = '';
2180 $context = $this->getContext();
2181 $outputPage = $context->getOutput();
2182 $user = $context->getUser();
2183 $status = $this->mPage->doDeleteArticleReal(
2184 $reason, $user, $suppress, null, $error,
2185 null, [], 'delete', $immediate
2186 );
2187
2188 if ( $status->isOK() ) {
2189 $deleted = $this->getTitle()->getPrefixedText();
2190
2191 $outputPage->setPageTitle( wfMessage( 'actioncomplete' ) );
2192 $outputPage->setRobotPolicy( 'noindex,nofollow' );
2193
2194 if ( $status->isGood() ) {
2195 $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]';
2196 $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
2197 $this->getHookRunner()->onArticleDeleteAfterSuccess( $this->getTitle(), $outputPage );
2198 } else {
2199 $outputPage->addWikiMsg( 'delete-scheduled', wfEscapeWikiText( $deleted ) );
2200 }
2201
2202 $outputPage->returnToMain( false );
2203 } else {
2204 $outputPage->setPageTitle(
2205 wfMessage( 'cannotdelete-title',
2206 $this->getTitle()->getPrefixedText() )
2207 );
2208
2209 if ( $error == '' ) {
2210 $outputPage->wrapWikiTextAsInterface(
2211 'error mw-error-cannotdelete',
2212 $status->getWikiText( false, false, $context->getLanguage() )
2213 );
2214 $deleteLogPage = new LogPage( 'delete' );
2215 $outputPage->addHTML( Xml::element( 'h2', null, $deleteLogPage->getName()->text() ) );
2216
2217 LogEventsList::showLogExtract(
2218 $outputPage,
2219 'delete',
2220 $this->getTitle()
2221 );
2222 } else {
2223 $outputPage->addHTML( $error );
2224 }
2225 }
2226 }
2227
2228 /* Caching functions */
2229
2237 protected function tryFileCache() {
2238 static $called = false;
2239
2240 if ( $called ) {
2241 wfDebug( "Article::tryFileCache(): called twice!?" );
2242 return false;
2243 }
2244
2245 $called = true;
2246 if ( $this->isFileCacheable() ) {
2247 $cache = new HTMLFileCache( $this->getTitle(), 'view' );
2248 if ( $cache->isCacheGood( $this->mPage->getTouched() ) ) {
2249 wfDebug( "Article::tryFileCache(): about to load file" );
2250 $cache->loadFromFileCache( $this->getContext() );
2251 return true;
2252 } else {
2253 wfDebug( "Article::tryFileCache(): starting buffer" );
2254 ob_start( [ &$cache, 'saveToFileCache' ] );
2255 }
2256 } else {
2257 wfDebug( "Article::tryFileCache(): not cacheable" );
2258 }
2259
2260 return false;
2261 }
2262
2268 public function isFileCacheable( $mode = HTMLFileCache::MODE_NORMAL ) {
2269 $cacheable = false;
2270
2271 if ( HTMLFileCache::useFileCache( $this->getContext(), $mode ) ) {
2272 $cacheable = $this->mPage->getId()
2273 && !$this->mRedirectedFrom && !$this->getTitle()->isRedirect();
2274 // Extension may have reason to disable file caching on some pages.
2275 if ( $cacheable ) {
2276 $cacheable = $this->getHookRunner()->onIsFileCacheable( $this );
2277 }
2278 }
2279
2280 return $cacheable;
2281 }
2282
2296 public function getParserOutput( $oldid = null, User $user = null ) {
2297 if ( $user === null ) {
2298 $parserOptions = $this->getParserOptions();
2299 } else {
2300 $parserOptions = $this->mPage->makeParserOptions( $user );
2301 }
2302
2303 return $this->mPage->getParserOutput( $parserOptions, $oldid );
2304 }
2305
2310 public function getParserOptions() {
2311 return $this->mPage->makeParserOptions( $this->getContext() );
2312 }
2313
2320 public function setContext( $context ) {
2321 $this->mContext = $context;
2322 }
2323
2330 public function getContext() {
2331 if ( $this->mContext instanceof IContextSource ) {
2332 return $this->mContext;
2333 } else {
2334 wfDebug( __METHOD__ . " called and \$mContext is null. " .
2335 "Return RequestContext::getMain(); for sanity" );
2336 return RequestContext::getMain();
2337 }
2338 }
2339
2349 public function __get( $fname ) {
2350 wfDeprecatedMsg( "Accessing Article::\$$fname is deprecated since MediaWiki 1.35",
2351 '1.35' );
2352
2353 if ( $fname === 'mRevision' ) {
2354 $record = $this->fetchRevisionRecord(); // Ensure that it is loaded
2355 return $record ? new Revision( $record ) : null;
2356 }
2357
2358 if ( property_exists( $this->mPage, $fname ) ) {
2359 return $this->mPage->$fname;
2360 }
2361 trigger_error( 'Inaccessible property via __get(): ' . $fname, E_USER_NOTICE );
2362 }
2363
2373 public function __set( $fname, $fvalue ) {
2374 wfDeprecatedMsg( "Setting Article::\$$fname is deprecated since MediaWiki 1.35",
2375 '1.35' );
2376
2377 if ( $fname === 'mRevision' ) {
2378 $this->mRevisionRecord = $fvalue ?
2379 $fvalue->getRevisionRecord() :
2380 null;
2381 return;
2382 }
2383
2384 if ( property_exists( $this->mPage, $fname ) ) {
2385 $this->mPage->$fname = $fvalue;
2386 // Note: extensions may want to toss on new fields
2387 } elseif ( !in_array( $fname, [ 'mContext', 'mPage' ] ) ) {
2388 $this->mPage->$fname = $fvalue;
2389 } else {
2390 trigger_error( 'Inaccessible property via __set(): ' . $fname, E_USER_NOTICE );
2391 }
2392 }
2393
2399 public function getActionOverrides() {
2400 return $this->mPage->getActionOverrides();
2401 }
2402
2408 public function getTimestamp() {
2409 wfDeprecated( __METHOD__, '1.35' );
2410 return $this->mPage->getTimestamp();
2411 }
2412}
getAuthority()
$wgArticleRobotPolicies
Robot policies per article.
$wgCdnMaxageStale
Cache timeout when delivering a stale ParserCache response due to PoolCounter contention.
$wgDefaultRobotPolicy
Default robot policy.
$wgSend404Code
Some web hosts attempt to rewrite all responses with a 404 (not found) status code,...
$wgRedirectSources
If local interwikis are set up which allow redirects, set this regexp to restrict URLs which will be ...
$wgUseFilePatrol
Use file patrolling to check new files on Special:Newfiles.
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
$wgNamespaceRobotPolicies
Robot policies per namespaces.
$wgUseNPPatrol
Use new page patrolling to check new pages on Special:Newpages.
$wgDeleteRevisionsLimit
Optional to restrict deletion of pages with higher revision counts to users with the 'bigdelete' perm...
$wgUseFileCache
This will cache static pages for non-logged-in users to reduce database traffic on public sites.
const NS_USER
Definition Defines.php:66
const NS_FILE
Definition Defines.php:70
const NS_MEDIAWIKI
Definition Defines.php:72
const RC_LOG
Definition Defines.php:128
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.
wfReadOnly()
Check whether the wiki is in read-only mode.
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 $function is deprecated.
getContext()
Class for viewing MediaWiki article and history.
Definition Article.php:45
getRevisionFetched()
Get the fetched Revision object depending on request parameters or null on failure.
Definition Article.php:455
getSubstituteContent()
Returns Content object to use when the page does not exist.
Definition Article.php:276
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition Article.php:182
generateContentOutput(User $user, ParserOptions $parserOptions, int $oldid, OutputPage $outputPage, array $textOptions)
Determines the desired ParserOutput and passes it to $outputPage.
Definition Article.php:610
getContext()
Gets the context this Article is executed in.
Definition Article.php:2330
getOldIDFromRequest()
Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect.
Definition Article.php:314
getParserOutput( $oldid=null, User $user=null)
#-
Definition Article.php:2296
getRedirectedFrom()
Get the page this view was redirected from.
Definition Article.php:193
Title null $mRedirectedFrom
Title from which we were redirected here, if any.
Definition Article.php:66
bool $viewIsRenderAction
Whether render() was called.
Definition Article.php:89
RevisionRecord null $mRevisionRecord
Revision to be shown.
Definition Article.php:110
RevisionStore $revisionStore
Definition Article.php:99
view()
This is the default action of the index.php entry point: just view the page of the given title.
Definition Article.php:482
__construct(Title $title, $oldId=null)
Definition Article.php:116
getRobotPolicy( $action, ParserOutput $pOutput=null)
Get the robot policy to be used for the current view.
Definition Article.php:947
static purgePatrolFooterCache( $articleID)
Purge the cache used to check if it is worth showing the patrol footer For example,...
Definition Article.php:1335
doDelete( $reason, $suppress=false, $immediate=false)
Perform a deletion and output success or failure messages.
Definition Article.php:2178
ParserOutput null false $mParserOutput
The ParserOutput generated for viewing the page, initialized by view().
Definition Article.php:82
getOldID()
Definition Article.php:301
LinkRenderer $linkRenderer
Definition Article.php:94
getTitle()
Get the title object of the article.
Definition Article.php:211
getActionOverrides()
Call to WikiPage function for backwards compatibility.
Definition Article.php:2399
adjustDisplayTitle(ParserOutput $pOutput)
Adjust title for pages with displaytitle, -{T|}- or language conversion.
Definition Article.php:869
showDeletedRevisionHeader()
If the revision requested for view is deleted, check permissions.
Definition Article.php:1518
getParserOptions()
Get parser options suitable for rendering the primary article wikitext.
Definition Article.php:2310
IContextSource null $mContext
The context this Article is executed in.
Definition Article.php:54
showViewError(string $errortext)
Show error text for errors generated in Article::view().
Definition Article.php:1503
static getRedirectHeaderHtml(Language $lang, $target, $forceKnown=false)
Return the HTML for the top of a redirect page.
Definition Article.php:1757
protect()
action=protect handler
Definition Article.php:1832
isCurrent()
Returns true if the currently-referenced revision is the current edit to this page (and it exists).
Definition Article.php:435
showMissingArticle()
Show the error text for a missing article.
Definition Article.php:1344
__set( $fname, $fvalue)
Definition Article.php:2373
unprotect()
action=unprotect handler (alias)
Definition Article.php:1840
newPage(Title $title)
Definition Article.php:129
confirmDelete( $reason)
Output deletion confirmation dialog.
Definition Article.php:1979
getPage()
Get the WikiPage object of this instance.
Definition Article.php:221
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Definition Article.php:1804
string bool $mRedirectUrl
URL to redirect to or false if none.
Definition Article.php:69
getTimestamp()
Definition Article.php:2408
static newFromID( $id)
Constructor from a page id.
Definition Article.php:138
int null $mOldId
The oldid of the article that was requested to be shown, 0 for the current revision.
Definition Article.php:63
static formatRobotPolicy( $policy)
Converts a String robot policy into an associative array, to allow merging of several policies using ...
Definition Article.php:1027
fetchRevisionRecord()
Fetches the revision to work on.
Definition Article.php:383
viewRedirect( $target, $appendSubtitle=true, $forceKnown=false)
Return the HTML for the top of a redirect page.
Definition Article.php:1735
showPatrolFooter()
If patrol is possible, output a patrol UI box.
Definition Article.php:1166
setOldSubtitle( $oldid=0)
Generate the navigation links when browsing through an article revisions It shows the information as:...
Definition Article.php:1579
showViewFooter()
Show the footer section of an ordinary page view.
Definition Article.php:1142
Status null $fetchResult
represents the outcome of fetchRevisionRecord().
Definition Article.php:75
WikiPage $mPage
The WikiPage object of this instance.
Definition Article.php:57
setRedirectedFrom(Title $from)
Tell the page view functions that this view was redirected from another page on the wiki.
Definition Article.php:202
isFileCacheable( $mode=HTMLFileCache::MODE_NORMAL)
Check if the page can be cached.
Definition Article.php:2268
doOutputMetaData(?ParserOutput $pOutput, OutputPage $outputPage)
Definition Article.php:772
tryFileCache()
checkLastModified returns true if it has taken care of all output to the client that is necessary for...
Definition Article.php:2237
getRevIdFetched()
Use this to fetch the rev ID used on page views.
Definition Article.php:470
showNamespaceHeader()
Show a header specific to the namespace currently being viewed, like [[MediaWiki:Talkpagetext]].
Definition Article.php:1130
__get( $fname)
Definition Article.php:2349
getRevisionRedirectTarget(RevisionRecord $revision)
Definition Article.php:857
static newFromTitle( $title, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition Article.php:150
doOutputFromRenderStatus(?RevisionRecord $rev, Status $renderStatus, OutputPage $outputPage, array $textOptions)
Definition Article.php:814
getContentObject()
Returns a Content object representing the pages effective display content, not necessarily the revisi...
Definition Article.php:253
showDiffPage()
Show a diff page according to current request variables.
Definition Article.php:884
render()
Handle action=render.
Definition Article.php:1821
showRedirectedFromHeader()
If this request is a redirect view, send "redirected from" subtitle to the output.
Definition Article.php:1056
doOutputFromParserCache(ParserOutput $pOutput, OutputPage $outputPage, array $textOptions)
Definition Article.php:791
setContext( $context)
Sets the context this Article is executed in.
Definition Article.php:2320
getCacheRevisionId()
Definition CacheTime.php:96
Special handling for category description pages, showing pages, subcategories and file that belong to...
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition EditPage.php:104
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.
Class for viewing MediaWiki file description pages.
Definition ImagePage.php:33
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition Language.php:43
static getRevDeleteLink(Authority $performer, $rev, LinkTarget $title)
Get a revision-deletion link, or disabled link, or nothing, depending on user permissions & the setti...
Definition Linker.php:2205
static revComment( $rev, $local=false, $isPublic=false, $useParentheses=true)
Wrap and format the given revision's comment block, if the current user is allowed to view it.
Definition Linker.php:1610
static revUserTools( $rev, $isPublic=false, $useParentheses=true)
Generate a user tool link cluster if the current user is allowed to view it.
Definition Linker.php:1147
static makeExternalLink( $url, $text, $escape=true, $linktype='', $attribs=[], $title=null)
Make an external link.
Definition Linker.php:847
Class to simplify the use of log pages.
Definition LogPage.php:38
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
Class that generates HTML links for pages.
makeKnownLink(LinkTarget $target, $text=null, array $extraAttribs=[], array $query=[])
makeLink(LinkTarget $target, $text=null, array $extraAttribs=[], array $query=[])
MediaWikiServices is the service locator for the application scope of MediaWiki.
Service for getting rendered output of a given page.
A StatusValue for permission errors.
Page revision base class.
getContent( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns the Content of the given slot of this revision.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
Wrapper allowing us to handle a system message as a Content object.
static numParam( $num)
Definition Message.php:1098
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.
allowClickjacking()
Turn off frame-breaking.
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()
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.
addParserOutput(ParserOutput $parserOutput, $poOptions=[])
Add everything from a ParserOutput object.
preventClickjacking( $enable=true)
Set a flag which will cause an X-Frame-Options header appropriate for edit pages to be sent.
setFollowPolicy( $policy)
Set the follow policy for the page, but leave the index policy un- touched.
isPrintable()
Return whether the page is "printable".
setRedirectedFrom( $t)
Set $mRedirectedFrom, the Title of the page which redirected us to the current page.
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,...
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...
enableOOUI()
Add ResourceLoader module styles for OOUI and set up the PHP implementation of it for use with MediaW...
clearHTML()
Clear the body HTML.
enableClientCache( $state)
Use enableClientCache(false) to force it to send nocache headers.
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.
addWikiTextAsContent( $text, $linestart=true, Title $title=null)
Convert wikitext in the page content language to HTML and add it to the buffer.
addBacklinkSubtitle(Title $title, $query=[])
Add a subtitle containing a backlink to a page.
prependHTML( $text)
Prepend $text to the body HTML.
addModules( $modules)
Load one or more ResourceLoader modules on this page.
addWikiTextAsInterface( $text, $linestart=true, Title $title=null)
Convert wikitext in the user interface 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?
Show an error when a user tries to do something they do not have the necessary permissions for.
Handles the page protection UI and backend.
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
hasMessage( $message)
Returns true if the specified message is present as a warning or error.
isOK()
Returns whether the operation completed.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition Status.php:189
Represents a title within MediaWiki.
Definition Title.php:48
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:67
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2120
static newFromName( $name, $validate='valid')
Definition User.php:587
getEditToken( $salt='', $request=null)
Initialize (if necessary) and return a session token value which can be used in edit forms to show th...
Definition User.php:3829
isRegistered()
Get whether the user is registered.
Definition User.php:3072
getOption( $oname, $defaultOverride=null, $ignoreHidden=false)
Get the user's current setting for a given option.
Definition User.php:2669
isHidden()
Check if user account is hidden.
Definition User.php:2075
isWatched( $title, $checkRights=self::CHECK_USER_RIGHTS)
Check the watched status of an article.
Definition User.php:3193
static isIP( $name)
Does the string match an anonymous IP address?
Definition User.php:1013
matchEditToken( $val, $salt='', $request=null, $maxage=null)
Check given value against the token value stored in the session.
Definition User.php:3845
getUser()
Definition User.php:4518
isBlockedFrom( $title, $fromReplica=false)
Check if user is blocked from editing a particular article.
Definition User.php:1973
static doWatchOrUnwatch( $watch, Title $title, Authority $performer, string $expiry=null)
Watch or unwatch a page.
Class representing a MediaWiki article and history.
Definition WikiPage.php:64
getTitle()
Get the title object of the article.
Definition WikiPage.php:335
Interface for objects which can provide a MediaWiki context on request.
Interface for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
Definition Page.php:29
$cache
Definition mcc.php:33
const DB_REPLICA
Definition defines.php:25
$content
Definition router.php:76
return true
Definition router.php:92
if(!isset( $args[0])) $lang