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