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