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