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