Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
1.79% covered (danger)
1.79%
2 / 112
CRAP
11.09% covered (danger)
11.09%
133 / 1199
Article
0.00% covered (danger)
0.00%
0 / 1
1.79% covered (danger)
1.79%
2 / 112
87924.64
11.09% covered (danger)
11.09%
133 / 1199
 __construct
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 7
 newPage
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 newFromID
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 2
 newFromTitle
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 13
 newFromWikiPage
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 getRedirectedFrom
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setRedirectedFrom
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getTitle
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getPage
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 clear
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 9
 getContentObject
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 getSubstituteContent
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 8
 getEmptyPageParserOutput
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getOldID
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
3 / 3
 getOldIDFromRequest
0.00% covered (danger)
0.00%
0 / 1
240
0.00% covered (danger)
0.00%
0 / 36
 fetchContentObject
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 fetchRevisionRecord
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 37
 makeFetchErrorContent
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 3
 applyContentOverride
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 5
 isCurrent
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 5
 getRevisionFetched
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 getRevIdFetched
0.00% covered (danger)
0.00%
0 / 1
3.33
66.67% covered (warning)
66.67%
2 / 3
 view
0.00% covered (danger)
0.00%
0 / 1
99.49
71.25% covered (warning)
71.25%
114 / 160
 getRevisionRedirectTarget
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 2
 adjustDisplayTitle
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
 showDiffPage
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 35
 getRobotPolicy
0.00% covered (danger)
0.00%
0 / 1
240
0.00% covered (danger)
0.00%
0 / 31
 formatRobotPolicy
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 13
 showRedirectedFromHeader
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 38
 showNamespaceHeader
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 5
 showViewFooter
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 6
 showPatrolFooter
0.00% covered (danger)
0.00%
0 / 1
702
0.00% covered (danger)
0.00%
0 / 78
 purgePatrolFooterCache
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 showMissingArticle
0.00% covered (danger)
0.00%
0 / 1
812
0.00% covered (danger)
0.00%
0 / 87
 showDeletedRevisionHeader
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 24
 setOldSubtitle
0.00% covered (danger)
0.00%
0 / 1
240
0.00% covered (danger)
0.00%
0 / 102
 viewRedirect
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
 getRedirectHeaderHtml
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 22
 addHelpLink
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 8
 render
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 5
 protect
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 unprotect
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 delete
0.00% covered (danger)
0.00%
0 / 1
210
0.00% covered (danger)
0.00%
0 / 69
 confirmDelete
0.00% covered (danger)
0.00%
0 / 1
110
0.00% covered (danger)
0.00%
0 / 109
 doDelete
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 32
 tryFileCache
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 15
 isFileCacheable
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 7
 getParserOutput
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 setParserOptions
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 getParserOptions
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 setContext
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getContext
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 __get
0.00% covered (danger)
0.00%
0 / 1
5.40
55.56% covered (warning)
55.56%
5 / 9
 __set
0.00% covered (danger)
0.00%
0 / 1
6.42
61.54% covered (warning)
61.54%
8 / 13
 checkFlags
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 checkTouched
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 clearPreparedEdit
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 doDeleteUpdates
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 doEditUpdates
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 doPurge
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 doViewUpdates
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 exists
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 followRedirect
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getActionOverrides
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getAutoDeleteReason
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getCategories
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getContentHandler
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getContentModel
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getContributors
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getDeletionUpdates
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getHiddenCategories
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getId
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getLatest
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getLinksTimestamp
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getMinorEdit
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getOldestRevision
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getRedirectTarget
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getRedirectURL
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getRevision
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getTimestamp
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getTouched
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getUndoContent
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 hasViewableContent
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 insertOn
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 insertRedirect
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 insertRedirectEntry
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 isCountable
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 isRedirect
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 loadFromRow
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 loadPageData
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 lockAndGetLatest
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 makeParserOptions
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 pageDataFromId
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 pageDataFromTitle
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 prepareContentForEdit
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 4
 protectDescription
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 protectDescriptionLog
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 replaceSectionAtRev
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 replaceSectionContent
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 setTimestamp
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 shouldCheckParserCache
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 supportsSections
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 triggerOpportunisticLinksUpdate
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 updateCategoryCounts
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 updateIfNewerOn
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 updateRedirectOn
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 updateRevisionOn
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 doUpdateRestrictions
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 updateRestrictions
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 7
 doRollback
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 commitRollback
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 generateReason
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
<?php
/**
 * User interface for page actions.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 */
use MediaWiki\Block\DatabaseBlock;
use MediaWiki\Edit\PreparedEdit;
use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\MediaWikiServices;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
use MediaWiki\Revision\SlotRecord;
use Wikimedia\IPUtils;
use Wikimedia\Rdbms\IDatabase;
/**
 * Class for viewing MediaWiki article and history.
 *
 * This maintains WikiPage functions for backwards compatibility.
 *
 * @todo Move and rewrite code to an Action class
 *
 * Note: edit user interface and cache support functions have been
 * moved to separate EditPage and HTMLFileCache classes.
 */
class Article implements Page {
    use ProtectedHookAccessorTrait;
    /**
     * @var IContextSource|null The context this Article is executed in.
     * If null, RequestContext::getMain() is used.
     * @deprecated since 1.35, must be private, use {@link getContext}
     */
    protected $mContext;
    /** @var WikiPage The WikiPage object of this instance */
    protected $mPage;
    /**
     * @var ParserOptions|null ParserOptions object for $wgUser articles.
     * Initialized by getParserOptions by calling $this->mPage->makeParserOptions().
     */
    public $mParserOptions;
    /**
     * @var Content|null Content of the main slot of $this->mRevisionRecord.
     * @note This variable is read only, setting it has no effect.
     *       Extensions that wish to override the output of Article::view should use a hook.
     * @todo MCR: Remove in 1.33
     * @deprecated since 1.32
     * @since 1.21
     */
    public $mContentObject;
    /**
     * @var bool Is the target revision loaded? Set by fetchRevisionRecord().
     *
     * @deprecated since 1.32. Whether content has been loaded should not be relevant to
     * code outside this class.
     */
    public $mContentLoaded = false;
    /**
     * @var int|null The oldid of the article that was requested to be shown,
     * 0 for the current revision.
     * @see $mRevIdFetched
     */
    public $mOldId;
    /** @var Title|null Title from which we were redirected here, if any. */
    public $mRedirectedFrom = null;
    /** @var string|bool URL to redirect to or false if none */
    public $mRedirectUrl = false;
    /**
     * @var int Revision ID of revision that was loaded.
     * @see $mOldId
     * @deprecated since 1.32, use getRevIdFetched() instead.
     */
    public $mRevIdFetched = 0;
    /**
     * @var Status|null represents the outcome of fetchRevisionRecord().
     * $fetchResult->value is the RevisionRecord object, if the operation was successful.
     *
     * The information in $fetchResult is duplicated by the following deprecated public fields:
     * $mRevIdFetched, $mContentLoaded (and $mContentObject) also typically duplicate
     * information of the loaded revision, but may be overwritten by extensions or due to errors.
     */
    private $fetchResult = null;
    /**
     * @var ParserOutput|null|false The ParserOutput generated for viewing the page,
     * initialized by view(). If no ParserOutput could be generated, this is set to false.
     * @deprecated since 1.32
     */
    public $mParserOutput = null;
    /**
     * @var bool Whether render() was called. With the way subclasses work
     * here, there doesn't seem to be any other way to stop calling
     * OutputPage::enableSectionEditLinks() and still have it work as it did before.
     */
    protected $viewIsRenderAction = false;
    /**
     * @var LinkRenderer
     */
    protected $linkRenderer;
    /**
     * @var PermissionManager
     */
    private $permManager;
    /**
     * @var RevisionStore
     */
    private $revisionStore;
    /**
     * @var RevisionRecord|null Revision to be shown
     *
     * Initialized by getOldIDFromRequest() or fetchRevisionRecord(). Normally loaded from the
     * database, but may be replaced by an extension, or be a fake representing an error message
     * or some such. While the output of Article::view is typically based on this revision,
     * it may be overwritten by error messages or replaced by extensions.
     *
     * Replaced $mRevision, which was public and is provided in a deprecated manner via
     * __get and __set
     */
    private $mRevisionRecord = null;
    /**
     * @param Title $title
     * @param int|null $oldId Revision ID, null to fetch from request, zero for current
     */
    public function __construct( Title $title, $oldId = null ) {
        $this->mOldId = $oldId;
        $this->mPage = $this->newPage( $title );
        $services = MediaWikiServices::getInstance();
        $this->linkRenderer = $services->getLinkRenderer();
        $this->permManager = $services->getPermissionManager();
        $this->revisionStore = $services->getRevisionStore();
    }
    /**
     * @param Title $title
     * @return WikiPage
     */
    protected function newPage( Title $title ) {
        return new WikiPage( $title );
    }
    /**
     * Constructor from a page id
     * @param int $id Article ID to load
     * @return Article|null
     */
    public static function newFromID( $id ) {
        $t = Title::newFromID( $id );
        return $t == null ? null : new static( $t );
    }
    /**
     * Create an Article object of the appropriate class for the given page.
     *
     * @param Title $title
     * @param IContextSource $context
     * @return Article
     */
    public static function newFromTitle( $title, IContextSource $context ) {
        if ( $title->getNamespace() === NS_MEDIA ) {
            // XXX: This should not be here, but where should it go?
            $title = Title::makeTitle( NS_FILE, $title->getDBkey() );
        }
        $page = null;
        Hooks::runner()->onArticleFromTitle( $title, $page, $context );
        if ( !$page ) {
            switch ( $title->getNamespace() ) {
                case NS_FILE:
                    $page = new ImagePage( $title );
                    break;
                case NS_CATEGORY:
                    $page = new CategoryPage( $title );
                    break;
                default:
                    $page = new Article( $title );
            }
        }
        $page->setContext( $context );
        return $page;
    }
    /**
     * Create an Article object of the appropriate class for the given page.
     *
     * @param WikiPage $page
     * @param IContextSource $context
     * @return Article
     */
    public static function newFromWikiPage( WikiPage $page, IContextSource $context ) {
        $article = self::newFromTitle( $page->getTitle(), $context );
        $article->mPage = $page; // override to keep process cached vars
        return $article;
    }
    /**
     * Get the page this view was redirected from
     * @return Title|null
     * @since 1.28
     */
    public function getRedirectedFrom() {
        return $this->mRedirectedFrom;
    }
    /**
     * Tell the page view functions that this view was redirected
     * from another page on the wiki.
     * @param Title $from
     */
    public function setRedirectedFrom( Title $from ) {
        $this->mRedirectedFrom = $from;
    }
    /**
     * Get the title object of the article
     *
     * @return Title Title object of this page
     */
    public function getTitle() {
        return $this->mPage->getTitle();
    }
    /**
     * Get the WikiPage object of this instance
     *
     * @since 1.19
     * @return WikiPage
     */
    public function getPage() {
        return $this->mPage;
    }
    /**
     * Clear the object
     */
    public function clear() {
        $this->mContentLoaded = false;
        $this->mRedirectedFrom = null; # Title object if set
        $this->mRevIdFetched = 0;
        $this->mRedirectUrl = false;
        $this->mRevisionRecord = null;
        $this->mContentObject = null;
        $this->fetchResult = null;
        // TODO hard-deprecate direct access to public fields
        $this->mPage->clear();
    }
    /**
     * Returns a Content object representing the pages effective display content,
     * not necessarily the revision's content!
     *
     * Note that getContent does not follow redirects anymore.
     * If you need to fetch redirectable content easily, try
     * the shortcut in WikiPage::getRedirectTarget()
     *
     * This function has side effects! Do not use this function if you
     * only want the real revision text if any.
     *
     * @deprecated since 1.32, use fetchRevisionRecord() instead.
     *
     * @return Content
     *
     * @since 1.21
     */
    protected function getContentObject() {
        if ( $this->mPage->getId() === 0 ) {
            $content = $this->getSubstituteContent();
        } else {
            $this->fetchContentObject();
            $content = $this->mContentObject;
        }
        return $content;
    }
    /**
     * Returns Content object to use when the page does not exist.
     *
     * @return Content
     */
    private function getSubstituteContent() {
        # If this is a MediaWiki:x message, then load the messages
        # and return the message value for x.
        if ( $this->getTitle()->getNamespace() === NS_MEDIAWIKI ) {
            $text = $this->getTitle()->getDefaultMessageText();
            if ( $text === false ) {
                $text = '';
            }
            $content = ContentHandler::makeContent( $text, $this->getTitle() );
        } else {
            $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon';
            $content = new MessageContent( $message, null );
        }
        return $content;
    }
    /**
     * Returns ParserOutput to use when a page does not exist. In some cases, we still want to show
     * "virtual" content, e.g. in the MediaWiki namespace, or in the File namespace for non-local
     * files.
     *
     * @param ParserOptions $options
     *
     * @return ParserOutput
     */
    protected function getEmptyPageParserOutput( ParserOptions $options ) {
        $content = $this->getSubstituteContent();
        return $content->getParserOutput( $this->getTitle(), 0, $options );
    }
    /**
     * @see getOldIDFromRequest()
     * @see getRevIdFetched()
     *
     * @return int The oldid of the article that is was requested in the constructor or via the
     *         context's WebRequest.
     */
    public function getOldID() {
        if ( $this->mOldId === null ) {
            $this->mOldId = $this->getOldIDFromRequest();
        }
        return $this->mOldId;
    }
    /**
     * Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect
     *
     * @return int The old id for the request
     */
    public function getOldIDFromRequest() {
        $this->mRedirectUrl = false;
        $request = $this->getContext()->getRequest();
        $oldid = $request->getIntOrNull( 'oldid' );
        if ( $oldid === null ) {
            return 0;
        }
        if ( $oldid !== 0 ) {
            # Load the given revision and check whether the page is another one.
            # In that case, update this instance to reflect the change.
            if ( $oldid === $this->mPage->getLatest() ) {
                $this->mRevisionRecord = $this->mPage->getRevisionRecord();
            } else {
                $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
                if ( $this->mRevisionRecord !== null ) {
                    $revPageId = $this->mRevisionRecord->getPageId();
                    // Revision title doesn't match the page title given?
                    if ( $this->mPage->getId() != $revPageId ) {
                        $function = get_class( $this->mPage ) . '::newFromID';
                        $this->mPage = $function( $revPageId );
                    }
                }
            }
        }
        $oldRev = $this->mRevisionRecord;
        if ( $request->getVal( 'direction' ) == 'next' ) {
            $nextid = 0;
            if ( $oldRev ) {
                $nextRev = $this->revisionStore->getNextRevision( $oldRev );
                if ( $nextRev ) {
                    $nextid = $nextRev->getId();
                }
            }
            if ( $nextid ) {
                $oldid = $nextid;
                $this->mRevisionRecord = null;
            } else {
                $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' );
            }
        } elseif ( $request->getVal( 'direction' ) == 'prev' ) {
            $previd = 0;
            if ( $oldRev ) {
                $prevRev = $this->revisionStore->getPreviousRevision( $oldRev );
                if ( $prevRev ) {
                    $previd = $prevRev->getId();
                }
            }
            if ( $previd ) {
                $oldid = $previd;
                $this->mRevisionRecord = null;
            }
        }
        $this->mRevIdFetched = $this->mRevisionRecord ? $this->mRevisionRecord->getId() : 0;
        return $oldid;
    }
    /**
     * Get text content object
     * Does *NOT* follow redirects.
     * @todo When is this null?
     * @deprecated since 1.32, use fetchRevisionRecord() instead.
     *
     * @note Code that wants to retrieve page content from the database should
     * use WikiPage::getContent().
     *
     * @return Content|null|bool
     *
     * @since 1.21
     */
    protected function fetchContentObject() {
        if ( !$this->mContentLoaded ) {
            $this->fetchRevisionRecord();
        }
        return $this->mContentObject;
    }
    /**
     * Fetches the revision to work on.
     * The revision is typically loaded from the database, but may also be a fake representing
     * an error message or content supplied by an extension. Refer to $this->fetchResult for
     * the revision actually loaded from the database, and any errors encountered while doing
     * that.
     *
     * Public since 1.35
     *
     * @return RevisionRecord|null
     */
    public function fetchRevisionRecord() {
        if ( $this->fetchResult ) {
            return $this->mRevisionRecord;
        }
        $this->mContentLoaded = true;
        $this->mContentObject = null;
        $oldid = $this->getOldID();
        // $this->mRevisionRecord might already be fetched by getOldIDFromRequest()
        if ( !$this->mRevisionRecord ) {
            if ( !$oldid ) {
                $this->mRevisionRecord = $this->mPage->getRevisionRecord();
                if ( !$this->mRevisionRecord ) {
                    wfDebug( __METHOD__ . " failed to find page data for title " .
                        $this->getTitle()->getPrefixedText() );
                    // Just for sanity, output for this case is done by showMissingArticle().
                    $this->fetchResult = Status::newFatal( 'noarticletext' );
                    $this->applyContentOverride( $this->makeFetchErrorContent() );
                    return null;
                }
            } else {
                $this->mRevisionRecord = $this->revisionStore->getRevisionById( $oldid );
                if ( !$this->mRevisionRecord ) {
                    wfDebug( __METHOD__ . " failed to load revision, rev_id $oldid" );
                    $this->fetchResult = Status::newFatal( 'missing-revision', $oldid );
                    $this->applyContentOverride( $this->makeFetchErrorContent() );
                    return null;
                }
            }
        }
        $this->mRevIdFetched = $this->mRevisionRecord->getId();
        $this->fetchResult = Status::newGood( $this->mRevisionRecord );
        if ( !RevisionRecord::userCanBitfield(
            $this->mRevisionRecord->getVisibility(),
            RevisionRecord::DELETED_TEXT,
            $this->getContext()->getUser()
        ) ) {
            wfDebug( __METHOD__ . " failed to retrieve content of revision " .
                $this->mRevisionRecord->getId() );
            // Just for sanity, output for this case is done by showDeletedRevisionHeader().
            $this->fetchResult = Status::newFatal(
                'rev-deleted-text-permission', $this->getTitle()->getPrefixedText() );
            $this->applyContentOverride( $this->makeFetchErrorContent() );
            return null;
        }
        // For B/C only
        $this->mContentObject = $this->mRevisionRecord->getContent(
            SlotRecord::MAIN,
            RevisionRecord::FOR_THIS_USER,
            $this->getContext()->getUser()
        );
        return $this->mRevisionRecord;
    }
    /**
     * Returns a Content object representing any error in $this->fetchContent, or null
     * if there is no such error.
     *
     * @return Content|null
     */
    private function makeFetchErrorContent() {
        if ( !$this->fetchResult || $this->fetchResult->isOK() ) {
            return null;
        }
        return new MessageContent( $this->fetchResult->getMessage() );
    }
    /**
     * Applies a content override by constructing a fake Revision object and assigning
     * it to mRevisionRecord. The fake revision will not have a user, timestamp or summary set.
     *
     * @todo This mechanism was created mainly to accommodate extensions that use the
     * ArticleAfterFetchContentObject. fetchRevisionRecord() presently also uses this mechanism
     * to report errors, but that could be changed to use $this->fetchResult instead.
     *
     * @todo the ArticleAfterFetchContentObject hook was removed; check if this is still needed
     *
     * @param Content $override Content to be used instead of the actual page content,
     *        coming from an extension or representing an error message.
     */
    private function applyContentOverride( Content $override ) {
        // Construct a fake revision
        $rev = new MutableRevisionRecord( $this->getTitle() );
        $rev->setContent( SlotRecord::MAIN, $override );
        $this->mRevisionRecord = $rev;
        // For B/C only
        $this->mContentObject = $override;
    }
    /**
     * Returns true if the currently-referenced revision is the current edit
     * to this page (and it exists).
     * @return bool
     */
    public function isCurrent() {
        # If no oldid, this is the current version.
        if ( $this->getOldID() == 0 ) {
            return true;
        }
        return $this->mPage->exists() &&
            $this->mRevisionRecord &&
            $this->mRevisionRecord->isCurrent();
    }
    /**
     * Get the fetched Revision object depending on request parameters or null
     * on failure. The revision returned may be a fake representing an error message or
     * wrapping content supplied by an extension. Refer to $this->fetchResult for the
     * revision actually loaded from the database.
     *
     * @deprecated since 1.35
     *
     * @since 1.19
     * @return Revision|null
     */
    public function getRevisionFetched() {
        wfDeprecated( __METHOD__, '1.35' );
        $revRecord = $this->fetchRevisionRecord();
        return $revRecord ? new Revision( $revRecord ) : null;
    }
    /**
     * Use this to fetch the rev ID used on page views
     *
     * Before fetchRevisionRecord was called, this returns the page's latest revision,
     * regardless of what getOldID() returns.
     *
     * @return int Revision ID of last article revision
     */
    public function getRevIdFetched() {
        if ( $this->fetchResult && $this->fetchResult->isOK() ) {
            return $this->fetchResult->value->getId();
        } else {
            return $this->mPage->getLatest();
        }
    }
    /**
     * This is the default action of the index.php entry point: just view the
     * page of the given title.
     */
    public function view() {
        global $wgUseFileCache, $wgCdnMaxageStale;
        # Get variables from query string
        # As side effect this will load the revision and update the title
        # in a revision ID is passed in the request, so this should remain
        # the first call of this method even if $oldid is used way below.
        $oldid = $this->getOldID();
        $user = $this->getContext()->getUser();
        # Another whitelist check in case getOldID() is altering the title
        $permErrors = $this->permManager->getPermissionErrors(
            'read',
            $user,
            $this->getTitle()
        );
        if ( count( $permErrors ) ) {
            wfDebug( __METHOD__ . ": denied on secondary read check" );
            throw new PermissionsError( 'read', $permErrors );
        }
        $outputPage = $this->getContext()->getOutput();
        # getOldID() may as well want us to redirect somewhere else
        if ( $this->mRedirectUrl ) {
            $outputPage->redirect( $this->mRedirectUrl );
            wfDebug( __METHOD__ . ": redirecting due to oldid" );
            return;
        }
        # If we got diff in the query, we want to see a diff page instead of the article.
        if ( $this->getContext()->getRequest()->getCheck( 'diff' ) ) {
            wfDebug( __METHOD__ . ": showing diff page" );
            $this->showDiffPage();
            return;
        }
        # Set page title (may be overridden by DISPLAYTITLE)
        $outputPage->setPageTitle( $this->getTitle()->getPrefixedText() );
        $outputPage->setArticleFlag( true );
        # Allow frames by default
        $outputPage->allowClickjacking();
        $parserCache = MediaWikiServices::getInstance()->getParserCache();
        $parserOptions = $this->getParserOptions();
        $poOptions = [];
        # Render printable version, use printable version cache
        if ( $outputPage->isPrintable() ) {
            $parserOptions->setIsPrintable( true );
            $poOptions['enableSectionEditLinks'] = false;
            $outputPage->prependHTML(
                Html::warningBox(
                    $outputPage->msg( 'printableversion-deprecated-warning' )->escaped()
                )
            );
        } elseif ( $this->viewIsRenderAction || !$this->isCurrent() ||
            !$this->permManager->quickUserCan( 'edit', $user, $this->getTitle() )
        ) {
            $poOptions['enableSectionEditLinks'] = false;
        }
        # Try client and file cache
        if ( $oldid === 0 && $this->mPage->checkTouched() ) {
            # Try to stream the output from file cache
            if ( $wgUseFileCache && $this->tryFileCache() ) {
                wfDebug( __METHOD__ . ": done file cache" );
                # tell wgOut that output is taken care of
                $outputPage->disable();
                $this->mPage->doViewUpdates( $user, $oldid );
                return;
            }
        }
        # Should the parser cache be used?
        $useParserCache = $this->mPage->shouldCheckParserCache( $parserOptions, $oldid );
        wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) );
        if ( $user->getStubThreshold() ) {
            MediaWikiServices::getInstance()->getStatsdDataFactory()->increment( 'pcache_miss_stub' );
        }
        $this->showRedirectedFromHeader();
        $this->showNamespaceHeader();
        # Iterate through the possible ways of constructing the output text.
        # Keep going until $outputDone is set, or we run out of things to do.
        $pass = 0;
        $outputDone = false;
        $this->mParserOutput = false;
        while ( !$outputDone && ++$pass ) {
            switch ( $pass ) {
                case 1:
                    $this->getHookRunner()->onArticleViewHeader( $this, $outputDone, $useParserCache );
                    break;
                case 2:
                    # Early abort if the page doesn't exist
                    if ( !$this->mPage->exists() ) {
                        wfDebug( __METHOD__ . ": showing missing article" );
                        $this->showMissingArticle();
                        $this->mPage->doViewUpdates( $user );
                        return;
                    }
                    # Try the parser cache
                    if ( $useParserCache ) {
                        $this->mParserOutput = $parserCache->get( $this->getPage(), $parserOptions );
                        if ( $this->mParserOutput !== false ) {
                            if ( $oldid ) {
                                wfDebug( __METHOD__ . ": showing parser cache contents for current rev permalink" );
                                $this->setOldSubtitle( $oldid );
                            } else {
                                wfDebug( __METHOD__ . ": showing parser cache contents" );
                            }
                            $outputPage->addParserOutput( $this->mParserOutput, $poOptions );
                            # Ensure that UI elements requiring revision ID have
                            # the correct version information.
                            $outputPage->setRevisionId( $this->mPage->getLatest() );
                            # Preload timestamp to avoid a DB hit
                            $cachedTimestamp = $this->mParserOutput->getTimestamp();
                            if ( $cachedTimestamp !== null ) {
                                $outputPage->setRevisionTimestamp( $cachedTimestamp );
                                $this->mPage->setTimestamp( $cachedTimestamp );
                            }
                            $outputDone = true;
                        }
                    }
                    break;
                case 3:
                    # Are we looking at an old revision
                    $rev = $this->fetchRevisionRecord();
                    if ( $oldid && $this->fetchResult->isOK() ) {
                        $this->setOldSubtitle( $oldid );
                        if ( !$this->showDeletedRevisionHeader() ) {
                            wfDebug( __METHOD__ . ": cannot view deleted revision" );
                            return;
                        }
                    }
                    # Ensure that UI elements requiring revision ID have
                    # the correct version information.
                    $outputPage->setRevisionId( $this->getRevIdFetched() );
                    # Preload timestamp to avoid a DB hit
                    $outputPage->setRevisionTimestamp( $this->mPage->getTimestamp() );
                    # Pages containing custom CSS or JavaScript get special treatment
                    if ( $this->getTitle()->isSiteConfigPage() || $this->getTitle()->isUserConfigPage() ) {
                        $dir = $this->getContext()->getLanguage()->getDir();
                        $lang = $this->getContext()->getLanguage()->getHtmlCode();
                        $outputPage->wrapWikiMsg(
                            "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
                            'clearyourcache'
                        );
                    } elseif ( !$this->getHookRunner()->onArticleRevisionViewCustom(
                        $rev,
                        $this->getTitle(),
                        $oldid,
                        $outputPage )
                    ) {
                        // NOTE: sync with hooks called in DifferenceEngine::renderNewRevision()
                        // Allow extensions do their own custom view for certain pages
                        $outputDone = true;
                    }
                    break;
                case 4:
                    # Run the parse, protected by a pool counter
                    wfDebug( __METHOD__ . ": doing uncached parse" );
                    $rev = $this->fetchRevisionRecord();
                    $error = null;
                    if ( $rev ) {
                        $poolArticleView = new PoolWorkArticleView(
                            $this->getPage(),
                            $parserOptions,
                            $this->getRevIdFetched(),
                            $useParserCache,
                            $rev,
                            // permission checking was done earlier via showDeletedRevisionHeader()
                            RevisionRecord::RAW
                        );
                        $ok = $poolArticleView->execute();
                        $error = $poolArticleView->getError();
                        $this->mParserOutput = $poolArticleView->getParserOutput() ?: null;
                        # Cache stale ParserOutput object with a short expiry
                        if ( $poolArticleView->getIsDirty() ) {
                            $outputPage->setCdnMaxage( $wgCdnMaxageStale );
                            $outputPage->setLastModified( $this->mParserOutput->getCacheTime() );
                            $staleReason = $poolArticleView->getIsFastStale()
                                ? 'pool contention' : 'pool overload';
                            $outputPage->addHTML( "<!-- parser cache is expired, " .
                                "sending anyway due to $staleReason-->\n" );
                        }
                    } else {
                        $ok = false;
                    }
                    if ( !$ok ) {
                        if ( $error ) {
                            $outputPage->clearHTML(); // for release() errors
                            $outputPage->enableClientCache( false );
                            $outputPage->setRobotPolicy( 'noindex,nofollow' );
                            $errortext = $error->getWikiText(
                                false, 'view-pool-error', $this->getContext()->getLanguage()
                            );
                            $outputPage->wrapWikiTextAsInterface( 'errorbox', $errortext );
                        }
                        # Connection or timeout error
                        return;
                    }
                    if ( $this->mParserOutput ) {
                        $outputPage->addParserOutput( $this->mParserOutput, $poOptions );
                    }
                    if ( $rev && $this->getRevisionRedirectTarget( $rev ) ) {
                        $outputPage->addSubtitle( "<span id=\"redirectsub\">" .
                            $this->getContext()->msg( 'redirectpagesub' )->parse() . "</span>" );
                    }
                    $outputDone = true;
                    break;
                # Should be unreachable, but just in case...
                default:
                    break 2;
            }
        }
        // Get the ParserOutput actually *displayed* here.
        // Note that $this->mParserOutput is the *current*/oldid version output.
        // Note that the ArticleViewHeader hook is allowed to set $outputDone to a
        // ParserOutput instance.
        $pOutput = ( $outputDone instanceof ParserOutput )
            ? $outputDone // object fetched by hook
            : ( $this->mParserOutput ?: null ); // ParserOutput or null, avoid false
        # Adjust title for main page & pages with displaytitle
        if ( $pOutput ) {
            $this->adjustDisplayTitle( $pOutput );
        }
        # For the main page, overwrite the <title> element with the con-
        # tents of 'pagetitle-view-mainpage' instead of the default (if
        # that's not empty).
        # This message always exists because it is in the i18n files
        if ( $this->getTitle()->isMainPage() ) {
            $msg = wfMessage( 'pagetitle-view-mainpage' )->inContentLanguage();
            if ( !$msg->isDisabled() ) {
                $outputPage->setHTMLTitle( $msg->title( $this->getTitle() )->text() );
            }
        }
        # Use adaptive TTLs for CDN so delayed/failed purges are noticed less often.
        # This could use getTouched(), but that could be scary for major template edits.
        $outputPage->adaptCdnTTL( $this->mPage->getTimestamp(), IExpiringStore::TTL_DAY );
        # Check for any __NOINDEX__ tags on the page using $pOutput
        $policy = $this->getRobotPolicy( 'view', $pOutput ?: null );
        $outputPage->setIndexPolicy( $policy['index'] );
        $outputPage->setFollowPolicy( $policy['follow'] ); // FIXME: test this
        $this->showViewFooter();
        $this->mPage->doViewUpdates( $user, $oldid ); // FIXME: test this
        # Load the postEdit module if the user just saved this revision
        # See also EditPage::setPostEditCookie
        $request = $this->getContext()->getRequest();
        $cookieKey = EditPage::POST_EDIT_COOKIE_KEY_PREFIX . $this->getRevIdFetched();
        $postEdit = $request->getCookie( $cookieKey );
        if ( $postEdit ) {
            # Clear the cookie. This also prevents caching of the response.
            $request->response()->clearCookie( $cookieKey );
            $outputPage->addJsConfigVars( 'wgPostEdit', $postEdit );
            $outputPage->addModules( 'mediawiki.action.view.postEdit' ); // FIXME: test this
        }
    }
    /**
     * @param RevisionRecord $revision
     * @return null|Title
     */
    private function getRevisionRedirectTarget( RevisionRecord $revision ) {
        // TODO: find a *good* place for the code that determines the redirect target for
        // a given revision!
        // NOTE: Use main slot content. Compare code in DerivedPageDataUpdater::revisionIsRedirect.
        $content = $revision->getContent( SlotRecord::MAIN );
        return $content ? $content->getRedirectTarget() : null;
    }
    /**
     * Adjust title for pages with displaytitle, -{T|}- or language conversion
     * @param ParserOutput $pOutput
     */
    public function adjustDisplayTitle( ParserOutput $pOutput ) {
        $out = $this->getContext()->getOutput();
        # Adjust the title if it was set by displaytitle, -{T|}- or language conversion
        $titleText = $pOutput->getTitleText();
        if ( strval( $titleText ) !== '' ) {
            $out->setPageTitle( $titleText );
            $out->setDisplayTitle( $titleText );
        }
    }
    /**
     * Show a diff page according to current request variables. For use within
     * Article::view() only, other callers should use the DifferenceEngine class.
     */
    protected function showDiffPage() {
        $request = $this->getContext()->getRequest();
        $user = $this->getContext()->getUser();
        $diff = $request->getVal( 'diff' );
        $rcid = $request->getVal( 'rcid' );
        $diffOnly = $request->getBool( 'diffonly', $user->getOption( 'diffonly' ) );
        $purge = $request->getVal( 'action' ) == 'purge';
        $unhide = $request->getInt( 'unhide' ) == 1;
        $oldid = $this->getOldID();
        $rev = $this->fetchRevisionRecord();
        if ( !$rev ) {
            $this->getContext()->getOutput()->setPageTitle( wfMessage( 'errorpagetitle' ) );
            $msg = $this->getContext()->msg( 'difference-missing-revision' )
                ->params( $oldid )
                ->numParams( 1 )
                ->parseAsBlock();
            $this->getContext()->getOutput()->addHTML( $msg );
            return;
        }
        $contentHandler = MediaWikiServices::getInstance()
            ->getContentHandlerFactory()
            ->getContentHandler(
                $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
            );
        $de = $contentHandler->createDifferenceEngine(
            $this->getContext(),
            $oldid,
            $diff,
            $rcid,
            $purge,
            $unhide
        );
        $de->setSlotDiffOptions( [
            'diff-type' => $request->getVal( 'diff-type' )
        ] );
        // DifferenceEngine directly fetched the revision:
        $this->mRevIdFetched = $de->getNewid();
        $de->showDiffPage( $diffOnly );
        // Run view updates for the newer revision being diffed (and shown
        // below the diff if not $diffOnly).
        list( $old, $new ) = $de->mapDiffPrevNext( $oldid, $diff );
        // New can be false, convert it to 0 - this conveniently means the latest revision
        $this->mPage->doViewUpdates( $user, (int)$new );
    }
    /**
     * Get the robot policy to be used for the current view
     * @param string $action The action= GET parameter
     * @param ParserOutput|null $pOutput
     * @return array The policy that should be set
     * @todo actions other than 'view'
     */
    public function getRobotPolicy( $action, ParserOutput $pOutput = null ) {
        global $wgArticleRobotPolicies, $wgNamespaceRobotPolicies, $wgDefaultRobotPolicy;
        $ns = $this->getTitle()->getNamespace();
        # Don't index user and user talk pages for blocked users (T13443)
        if ( ( $ns === NS_USER || $ns === NS_USER_TALK ) && !$this->getTitle()->isSubpage() ) {
            $specificTarget = null;
            $vagueTarget = null;
            $titleText = $this->getTitle()->getText();
            if ( IPUtils::isValid( $titleText ) ) {
                $vagueTarget = $titleText;
            } else {
                $specificTarget = $titleText;
            }
            if ( DatabaseBlock::newFromTarget( $specificTarget, $vagueTarget ) instanceof DatabaseBlock ) {
                return [
                    'index' => 'noindex',
                    'follow' => 'nofollow'
                ];
            }
        }
        if ( $this->mPage->getId() === 0 || $this->getOldID() ) {
            # Non-articles (special pages etc), and old revisions
            return [
                'index' => 'noindex',
                'follow' => 'nofollow'
            ];
        } elseif ( $this->getContext()->getOutput()->isPrintable() ) {
            # Discourage indexing of printable versions, but encourage following
            return [
                'index' => 'noindex',
                'follow' => 'follow'
            ];
        } elseif ( $this->getContext()->getRequest()->getInt( 'curid' ) ) {
            # For ?curid=x urls, disallow indexing
            return [
                'index' => 'noindex',
                'follow' => 'follow'
            ];
        }
        # Otherwise, construct the policy based on the various config variables.
        $policy = self::formatRobotPolicy( $wgDefaultRobotPolicy );
        if ( isset( $wgNamespaceRobotPolicies[$ns] ) ) {
            # Honour customised robot policies for this namespace
            $policy = array_merge(
                $policy,
                self::formatRobotPolicy( $wgNamespaceRobotPolicies[$ns] )
            );
        }
        if ( $this->getTitle()->canUseNoindex() && is_object( $pOutput ) && $pOutput->getIndexPolicy() ) {
            # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates
            # a final sanity check that we have really got the parser output.
            $policy = array_merge(
                $policy,
                [ 'index' => $pOutput->getIndexPolicy() ]
            );
        }
        if ( isset( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] ) ) {
            # (T16900) site config can override user-defined __INDEX__ or __NOINDEX__
            $policy = array_merge(
                $policy,
                self::formatRobotPolicy( $wgArticleRobotPolicies[$this->getTitle()->getPrefixedText()] )
            );
        }
        return $policy;
    }
    /**
     * Converts a String robot policy into an associative array, to allow
     * merging of several policies using array_merge().
     * @param array|string $policy Returns empty array on null/false/'', transparent
     *   to already-converted arrays, converts string.
     * @return array 'index' => \<indexpolicy\>, 'follow' => \<followpolicy\>
     */
    public static function formatRobotPolicy( $policy ) {
        if ( is_array( $policy ) ) {
            return $policy;
        } elseif ( !$policy ) {
            return [];
        }
        $policy = explode( ',', $policy );
        $policy = array_map( 'trim', $policy );
        $arr = [];
        foreach ( $policy as $var ) {
            if ( in_array( $var, [ 'index', 'noindex' ] ) ) {
                $arr['index'] = $var;
            } elseif ( in_array( $var, [ 'follow', 'nofollow' ] ) ) {
                $arr['follow'] = $var;
            }
        }
        return $arr;
    }
    /**
     * If this request is a redirect view, send "redirected from" subtitle to
     * the output. Returns true if the header was needed, false if this is not
     * a redirect view. Handles both local and remote redirects.
     *
     * @return bool
     */
    public function showRedirectedFromHeader() {
        global $wgRedirectSources;
        $context = $this->getContext();
        $outputPage = $context->getOutput();
        $request = $context->getRequest();
        $rdfrom = $request->getVal( 'rdfrom' );
        // Construct a URL for the current page view, but with the target title
        $query = $request->getValues();
        unset( $query['rdfrom'] );
        unset( $query['title'] );
        if ( $this->getTitle()->isRedirect() ) {
            // Prevent double redirects
            $query['redirect'] = 'no';
        }
        $redirectTargetUrl = $this->getTitle()->getLinkURL( $query );
        if ( isset( $this->mRedirectedFrom ) ) {
            // This is an internally redirected page view.
            // We'll need a backlink to the source page for navigation.
            if ( $this->getHookRunner()->onArticleViewRedirect( $this ) ) {
                $redir = $this->linkRenderer->makeKnownLink(
                    $this->mRedirectedFrom,
                    null,
                    [],
                    [ 'redirect' => 'no' ]
                );
                $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
                    $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
                . "</span>" );
                // Add the script to update the displayed URL and
                // set the fragment if one was specified in the redirect
                $outputPage->addJsConfigVars( [
                    'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
                ] );
                $outputPage->addModules( 'mediawiki.action.view.redirect' );
                // Add a <link rel="canonical"> tag
                $outputPage->setCanonicalUrl( $this->getTitle()->getCanonicalURL() );
                // Tell the output object that the user arrived at this article through a redirect
                $outputPage->setRedirectedFrom( $this->mRedirectedFrom );
                return true;
            }
        } elseif ( $rdfrom ) {
            // This is an externally redirected view, from some other wiki.
            // If it was reported from a trusted site, supply a backlink.
            if ( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) {
                $redir = Linker::makeExternalLink( $rdfrom, $rdfrom );
                $outputPage->addSubtitle( "<span class=\"mw-redirectedfrom\">" .
                    $context->msg( 'redirectedfrom' )->rawParams( $redir )->parse()
                . "</span>" );
                // Add the script to update the displayed URL
                $outputPage->addJsConfigVars( [
                    'wgInternalRedirectTargetUrl' => $redirectTargetUrl,
                ] );
                $outputPage->addModules( 'mediawiki.action.view.redirect' );
                return true;
            }
        }
        return false;
    }
    /**
     * Show a header specific to the namespace currently being viewed, like
     * [[MediaWiki:Talkpagetext]]. For Article::view().
     */
    public function showNamespaceHeader() {
        if ( $this->getTitle()->isTalkPage() && !wfMessage( 'talkpageheader' )->isDisabled() ) {
            $this->getContext()->getOutput()->wrapWikiMsg(
                "<div class=\"mw-talkpageheader\">\n$1\n</div>",
                [ 'talkpageheader' ]
            );
        }
    }
    /**
     * Show the footer section of an ordinary page view
     */
    public function showViewFooter() {
        # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
        if ( $this->getTitle()->getNamespace() === NS_USER_TALK
            && IPUtils::isValid( $this->getTitle()->getText() )
        ) {
            $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' );
        }
        // Show a footer allowing the user to patrol the shown revision or page if possible
        $patrolFooterShown = $this->showPatrolFooter();
        $this->getHookRunner()->onArticleViewFooter( $this, $patrolFooterShown );
    }
    /**
     * If patrol is possible, output a patrol UI box. This is called from the
     * footer section of ordinary page views. If patrol is not possible or not
     * desired, does nothing.
     *
     * Side effect: When the patrol link is build, this method will call
     * OutputPage::preventClickjacking() and load a JS module.
     *
     * @return bool
     */
    public function showPatrolFooter() {
        global $wgUseNPPatrol, $wgUseRCPatrol, $wgUseFilePatrol;
        // Allow hooks to decide whether to not output this at all
        if ( !$this->getHookRunner()->onArticleShowPatrolFooter( $this ) ) {
            return false;
        }
        $outputPage = $this->getContext()->getOutput();
        $user = $this->getContext()->getUser();
        $title = $this->getTitle();
        $rc = false;
        if ( !$this->permManager->quickUserCan( 'patrol', $user, $title )
            || !( $wgUseRCPatrol || $wgUseNPPatrol
                || ( $wgUseFilePatrol && $title->inNamespace( NS_FILE ) ) )
        ) {
            // Patrolling is disabled or the user isn't allowed to
            return false;
        }
        if ( $this->mRevisionRecord
            && !RecentChange::isInRCLifespan( $this->mRevisionRecord->getTimestamp(), 21600 )
        ) {
            // The current revision is already older than what could be in the RC table
            // 6h tolerance because the RC might not be cleaned out regularly
            return false;
        }
        // Check for cached results
        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
        $key = $cache->makeKey( 'unpatrollable-page', $title->getArticleID() );
        if ( $cache->get( $key ) ) {
            return false;
        }
        $dbr = wfGetDB( DB_REPLICA );
        $oldestRevisionTimestamp = $dbr->selectField(
            'revision',
            'MIN( rev_timestamp )',
            [ 'rev_page' => $title->getArticleID() ],
            __METHOD__
        );
        // New page patrol: Get the timestamp of the oldest revison which
        // the revision table holds for the given page. Then we look
        // whether it's within the RC lifespan and if it is, we try
        // to get the recentchanges row belonging to that entry
        // (with rc_new = 1).
        $recentPageCreation = false;
        if ( $oldestRevisionTimestamp
            && RecentChange::isInRCLifespan( $oldestRevisionTimestamp, 21600 )
        ) {
            // 6h tolerance because the RC might not be cleaned out regularly
            $recentPageCreation = true;
            $rc = RecentChange::newFromConds(
                [
                    'rc_new' => 1,
                    'rc_timestamp' => $oldestRevisionTimestamp,
                    'rc_namespace' => $title->getNamespace(),
                    'rc_cur_id' => $title->getArticleID()
                ],
                __METHOD__
            );
            if ( $rc ) {
                // Use generic patrol message for new pages
                $markPatrolledMsg = wfMessage( 'markaspatrolledtext' );
            }
        }
        // File patrol: Get the timestamp of the latest upload for this page,
        // check whether it is within the RC lifespan and if it is, we try
        // to get the recentchanges row belonging to that entry
        // (with rc_type = RC_LOG, rc_log_type = upload).
        $recentFileUpload = false;
        if ( ( !$rc || $rc->getAttribute( 'rc_patrolled' ) ) && $wgUseFilePatrol
            && $title->getNamespace() === NS_FILE ) {
            // Retrieve timestamp of most recent upload
            $newestUploadTimestamp = $dbr->selectField(
                'image',
                'MAX( img_timestamp )',
                [ 'img_name' => $title->getDBkey() ],
                __METHOD__
            );
            if ( $newestUploadTimestamp
                && RecentChange::isInRCLifespan( $newestUploadTimestamp, 21600 )
            ) {
                // 6h tolerance because the RC might not be cleaned out regularly
                $recentFileUpload = true;
                $rc = RecentChange::newFromConds(
                    [
                        'rc_type' => RC_LOG,
                        'rc_log_type' => 'upload',
                        'rc_timestamp' => $newestUploadTimestamp,
                        'rc_namespace' => NS_FILE,
                        'rc_cur_id' => $title->getArticleID()
                    ],
                    __METHOD__
                );
                if ( $rc ) {
                    // Use patrol message specific to files
                    $markPatrolledMsg = wfMessage( 'markaspatrolledtext-file' );
                }
            }
        }
        if ( !$recentPageCreation && !$recentFileUpload ) {
            // Page creation and latest upload (for files) is too old to be in RC
            // We definitely can't patrol so cache the information
            // When a new file version is uploaded, the cache is cleared
            $cache->set( $key, '1' );
            return false;
        }
        if ( !$rc ) {
            // Don't cache: This can be hit if the page gets accessed very fast after
            // its creation / latest upload or in case we have high replica DB lag. In case
            // the revision is too old, we will already return above.
            return false;
        }
        if ( $rc->getAttribute( 'rc_patrolled' ) ) {
            // Patrolled RC entry around
            // Cache the information we gathered above in case we can't patrol
            // Don't cache in case we can patrol as this could change
            $cache->set( $key, '1' );
            return false;
        }
        if ( $rc->getPerformer()->equals( $user ) ) {
            // Don't show a patrol link for own creations/uploads. If the user could
            // patrol them, they already would be patrolled
            return false;
        }
        $outputPage->preventClickjacking();
        if ( $this->permManager->userHasRight( $user, 'writeapi' ) ) {
            $outputPage->addModules( 'mediawiki.misc-authed-curate' );
        }
        $link = $this->linkRenderer->makeKnownLink(
            $title,
            $markPatrolledMsg->text(),
            [],
            [
                'action' => 'markpatrolled',
                'rcid' => $rc->getAttribute( 'rc_id' ),
            ]
        );
        $outputPage->addHTML(
            "<div class='patrollink' data-mw='interface'>" .
                wfMessage( 'markaspatrolledlink' )->rawParams( $link )->escaped() .
            '</div>'
        );
        return true;
    }
    /**
     * Purge the cache used to check if it is worth showing the patrol footer
     * For example, it is done during re-uploads when file patrol is used.
     * @param int $articleID ID of the article to purge
     * @since 1.27
     */
    public static function purgePatrolFooterCache( $articleID ) {
        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
        $cache->delete( $cache->makeKey( 'unpatrollable-page', $articleID ) );
    }
    /**
     * Show the error text for a missing article. For articles in the MediaWiki
     * namespace, show the default message text. To be called from Article::view().
     */
    public function showMissingArticle() {
        global $wgSend404Code;
        $outputPage = $this->getContext()->getOutput();
        // Whether the page is a root user page of an existing user (but not a subpage)
        $validUserPage = false;
        $title = $this->getTitle();
        $services = MediaWikiServices::getInstance();
        # Show info in user (talk) namespace. Does the user exist? Is he blocked?
        if ( $title->getNamespace() === NS_USER
            || $title->getNamespace() === NS_USER_TALK
        ) {
            $rootPart = explode( '/', $title->getText() )[0];
            $user = User::newFromName( $rootPart, false /* allow IP users */ );
            $ip = User::isIP( $rootPart );
            $block = DatabaseBlock::newFromTarget( $user, $user );
            if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
                $outputPage->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
                    [ 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) ] );
            } elseif (
                $block !== null &&
                $block->getType() != DatabaseBlock::TYPE_AUTO &&
                ( $block->isSitewide() || $user->isBlockedFrom( $title ) )
            ) {
                // Show log extract if the user is sitewide blocked or is partially
                // blocked and not allowed to edit their user page or user talk page
                LogEventsList::showLogExtract(
                    $outputPage,
                    'block',
                    $services->getNamespaceInfo()->getCanonicalName( NS_USER ) . ':' .
                        $block->getTarget(),
                    '',
                    [
                        'lim' => 1,
                        'showIfEmpty' => false,
                        'msgKey' => [
                            'blocked-notice-logextract',
                            $user->getName() # Support GENDER in notice
                        ]
                    ]
                );
                $validUserPage = !$title->isSubpage();
            } else {
                $validUserPage = !$title->isSubpage();
            }
        }
        $this->getHookRunner()->onShowMissingArticle( $this );
        # Show delete and move logs if there were any such events.
        # The logging query can DOS the site when bots/crawlers cause 404 floods,
        # so be careful showing this. 404 pages must be cheap as they are hard to cache.
        $dbCache = ObjectCache::getInstance( 'db-replicated' );
        $key = $dbCache->makeKey( 'page-recent-delete', md5( $title->getPrefixedText() ) );
        $loggedIn = $this->getContext()->getUser()->isLoggedIn();
        $sessionExists = $this->getContext()->getRequest()->getSession()->isPersistent();
        if ( $loggedIn || $dbCache->get( $key ) || $sessionExists ) {
            $logTypes = [ 'delete', 'move', 'protect' ];
            $dbr = wfGetDB( DB_REPLICA );
            $conds = [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ];
            // Give extensions a chance to hide their (unrelated) log entries
            $this->getHookRunner()->onArticle__MissingArticleConditions( $conds, $logTypes );
            LogEventsList::showLogExtract(
                $outputPage,
                $logTypes,
                $title,
                '',
                [
                    'lim' => 10,
                    'conds' => $conds,
                    'showIfEmpty' => false,
                    'msgKey' => [ $loggedIn || $sessionExists
                        ? 'moveddeleted-notice'
                        : 'moveddeleted-notice-recent'
                    ]
                ]
            );
        }
        if ( !$this->mPage->hasViewableContent() && $wgSend404Code && !$validUserPage ) {
            // If there's no backing content, send a 404 Not Found
            // for better machine handling of broken links.
            $this->getContext()->getRequest()->response()->statusHeader( 404 );
        }
        // Also apply the robot policy for nonexisting pages (even if a 404 was used for sanity)
        $policy = $this->getRobotPolicy( 'view' );
        $outputPage->setIndexPolicy( $policy['index'] );
        $outputPage->setFollowPolicy( $policy['follow'] );
        $hookResult = $this->getHookRunner()->onBeforeDisplayNoArticleText( $this );
        if ( !$hookResult ) {
            return;
        }
        # Show error message
        $oldid = $this->getOldID();
        $pm = $this->permManager;
        if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) {
            // use fake Content object for system message
            $parserOptions = ParserOptions::newCanonical( 'canonical' );
            $outputPage->addParserOutput( $this->getEmptyPageParserOutput( $parserOptions ) );
        } else {
            if ( $oldid ) {
                // T251066: Try loading the revision from the archive table.
                // Show link to view it if it exists and the user has permission to view it.
                $pa = new PageArchive( $title, $this->getContext()->getConfig() );
                $revRecord = $pa->getArchivedRevisionRecord( $oldid );
                if ( $revRecord && $revRecord->audienceCan(
                    RevisionRecord::DELETED_TEXT,
                    RevisionRecord::FOR_THIS_USER,
                    $this->getContext()->getUser()
                ) ) {
                    $text = wfMessage(
                        'missing-revision-permission', $oldid,
                        $revRecord->getTimestamp(),
                        $title->getPrefixedDBkey()
                    )->plain();
                } else {
                    $text = wfMessage( 'missing-revision', $oldid )->plain();
                }
            } elseif ( $pm->quickUserCan( 'create', $this->getContext()->getUser(), $title ) &&
                $pm->quickUserCan( 'edit', $this->getContext()->getUser(), $title )
            ) {
                $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon';
                $text = wfMessage( $message )->plain();
            } else {
                $text = wfMessage( 'noarticletext-nopermission' )->plain();
            }
            $dir = $this->getContext()->getLanguage()->getDir();
            $lang = $this->getContext()->getLanguage()->getHtmlCode();
            $outputPage->addWikiTextAsInterface( Xml::openElement( 'div', [
                'class' => "noarticletext mw-content-$dir",
                'dir' => $dir,
                'lang' => $lang,
            ] ) . "\n$text\n</div>" );
        }
    }
    /**
     * If the revision requested for view is deleted, check permissions.
     * Send either an error message or a warning header to the output.
     *
     * @return bool True if the view is allowed, false if not.
     */
    public function showDeletedRevisionHeader() {
        if ( !$this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
            // Not deleted
            return true;
        }
        $outputPage = $this->getContext()->getOutput();
        $user = $this->getContext()->getUser();
        $titleText = $this->getTitle()->getPrefixedText();
        // If the user is not allowed to see it...
        if ( !RevisionRecord::userCanBitfield(
            $this->mRevisionRecord->getVisibility(),
            RevisionRecord::DELETED_TEXT,
            $user
        ) ) {
            $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
                [ 'rev-deleted-text-permission', $titleText ] );
            return false;
        // If the user needs to confirm that they want to see it...
        } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) != 1 ) {
            # Give explanation and add a link to view the revision...
            $oldid = intval( $this->getOldID() );
            $link = $this->getTitle()->getFullURL( "oldid={$oldid}&unhide=1" );
            $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ?
                'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
            $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", [ $msg, $link ] );
            return false;
        // We are allowed to see...
        } else {
            $msg = $this->mRevisionRecord->isDeleted( RevisionRecord::DELETED_RESTRICTED )
                ? [ 'rev-suppressed-text-view', $titleText ]
                : [ 'rev-deleted-text-view', $titleText ];
            $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", $msg );
            return true;
        }
    }
    /**
     * Generate the navigation links when browsing through an article revisions
     * It shows the information as:
     *   Revision as of \<date\>; view current revision
     *   \<- Previous version | Next Version -\>
     *
     * @param int $oldid Revision ID of this article revision
     */
    public function setOldSubtitle( $oldid = 0 ) {
        if ( !$this->getHookRunner()->onDisplayOldSubtitle( $this, $oldid ) ) {
            return;
        }
        $context = $this->getContext();
        $unhide = $context->getRequest()->getInt( 'unhide' ) == 1;
        # Cascade unhide param in links for easy deletion browsing
        $extraParams = [];
        if ( $unhide ) {
            $extraParams['unhide'] = 1;
        }
        if ( $this->mRevisionRecord && $this->mRevisionRecord->getId() === $oldid ) {
            $revisionRecord = $this->mRevisionRecord;
        } else {
            $revisionRecord = $this->revisionStore->getRevisionById( $oldid );
        }
        $timestamp = $revisionRecord->getTimestamp();
        $current = ( $oldid == $this->mPage->getLatest() );
        $language = $context->getLanguage();
        $user = $context->getUser();
        $td = $language->userTimeAndDate( $timestamp, $user );
        $tddate = $language->userDate( $timestamp, $user );
        $tdtime = $language->userTime( $timestamp, $user );
        # Show user links if allowed to see them. If hidden, then show them only if requested...
        $userlinks = Linker::revUserTools( $revisionRecord, !$unhide );
        $infomsg = $current && !$context->msg( 'revision-info-current' )->isDisabled()
            ? 'revision-info-current'
            : 'revision-info';
        $outputPage = $context->getOutput();
        $revisionUser = $revisionRecord->getUser();
        $revisionInfo = "<div id=\"mw-{$infomsg}\">" .
            $context->msg( $infomsg, $td )
                ->rawParams( $userlinks )
                ->params(
                    $revisionRecord->getId(),
                    $tddate,
                    $tdtime,
                    $revisionUser ? $revisionUser->getName() : ''
                )
                ->rawParams( Linker::revComment(
                    $revisionRecord,
                    true,
                    true
                ) )
                ->parse() .
            "</div>";
        $lnk = $current
            ? $context->msg( 'currentrevisionlink' )->escaped()
            : $this->linkRenderer->makeKnownLink(
                $this->getTitle(),
                $context->msg( 'currentrevisionlink' )->text(),
                [],
                $extraParams
            );
        $curdiff = $current
            ? $context->msg( 'diff' )->escaped()
            : $this->linkRenderer->makeKnownLink(
                $this->getTitle(),
                $context->msg( 'diff' )->text(),
                [],
                [
                    'diff' => 'cur',
                    'oldid' => $oldid
                ] + $extraParams
            );
        $prevExist = (bool)$this->revisionStore->getPreviousRevision( $revisionRecord );
        $prevlink = $prevExist
            ? $this->linkRenderer->makeKnownLink(
                $this->getTitle(),
                $context->msg( 'previousrevision' )->text(),
                [],
                [
                    'direction' => 'prev',
                    'oldid' => $oldid
                ] + $extraParams
            )
            : $context->msg( 'previousrevision' )->escaped();
        $prevdiff = $prevExist
            ? $this->linkRenderer->makeKnownLink(
                $this->getTitle(),
                $context->msg( 'diff' )->text(),
                [],
                [
                    'diff' => 'prev',
                    'oldid' => $oldid
                ] + $extraParams
            )
            : $context->msg( 'diff' )->escaped();
        $nextlink = $current
            ? $context->msg( 'nextrevision' )->escaped()
            : $this->linkRenderer->makeKnownLink(
                $this->getTitle(),
                $context->msg( 'nextrevision' )->text(),
                [],
                [
                    'direction' => 'next',
                    'oldid' => $oldid
                ] + $extraParams
            );
        $nextdiff = $current
            ? $context->msg( 'diff' )->escaped()
            : $this->linkRenderer->makeKnownLink(
                $this->getTitle(),
                $context->msg( 'diff' )->text(),
                [],
                [
                    'diff' => 'next',
                    'oldid' => $oldid
                ] + $extraParams
            );
        $cdel = Linker::getRevDeleteLink(
            $user,
            $revisionRecord,
            $this->getTitle()
        );
        if ( $cdel !== '' ) {
            $cdel .= ' ';
        }
        // the outer div is need for styling the revision info and nav in MobileFrontend
        $outputPage->addSubtitle( "<div class=\"mw-revision warningbox\">" . $revisionInfo .
            "<div id=\"mw-revision-nav\">" . $cdel .
            $context->msg( 'revision-nav' )->rawParams(
                $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff
            )->escaped() . "</div></div>" );
    }
    /**
     * Return the HTML for the top of a redirect page
     *
     * Chances are you should just be using the ParserOutput from
     * WikitextContent::getParserOutput instead of calling this for redirects.
     *
     * @param Title|array $target Destination(s) to redirect
     * @param bool $appendSubtitle [optional]
     * @param bool $forceKnown Should the image be shown as a bluelink regardless of existence?
     * @return string Containing HTML with redirect link
     *
     * @deprecated since 1.30
     */
    public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) {
        $lang = $this->getTitle()->getPageLanguage();
        $out = $this->getContext()->getOutput();
        if ( $appendSubtitle ) {
            $out->addSubtitle( wfMessage( 'redirectpagesub' ) );
        }
        $out->addModuleStyles( 'mediawiki.action.view.redirectPage' );
        return static::getRedirectHeaderHtml( $lang, $target, $forceKnown );
    }
    /**
     * Return the HTML for the top of a redirect page
     *
     * Chances are you should just be using the ParserOutput from
     * WikitextContent::getParserOutput instead of calling this for redirects.
     *
     * @since 1.23
     * @param Language $lang
     * @param Title|Title[] $target Destination(s) to redirect
     * @param bool $forceKnown Should the image be shown as a bluelink regardless of existence?
     * @return string Containing HTML with redirect link
     */
    public static function getRedirectHeaderHtml( Language $lang, $target, $forceKnown = false ) {
        if ( !is_array( $target ) ) {
            $target = [ $target ];
        }
        $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
        $html = '<ul class="redirectText">';
        /** @var Title $title */
        foreach ( $target as $title ) {
            if ( $forceKnown ) {
                $link = $linkRenderer->makeKnownLink(
                    $title,
                    $title->getFullText(),
                    [],
                    // Make sure wiki page redirects are not followed
                    $title->isRedirect() ? [ 'redirect' => 'no' ] : []
                );
            } else {
                $link = $linkRenderer->makeLink(
                    $title,
                    $title->getFullText(),
                    [],
                    // Make sure wiki page redirects are not followed
                    $title->isRedirect() ? [ 'redirect' => 'no' ] : []
                );
            }
            $html .= '<li>' . $link . '</li>';
        }
        $html .= '</ul>';
        $redirectToText = wfMessage( 'redirectto' )->inLanguage( $lang )->escaped();
        return '<div class="redirectMsg">' .
            '<p>' . $redirectToText . '</p>' .
            $html .
            '</div>';
    }
    /**
     * Adds help link with an icon via page indicators.
     * Link target can be overridden by a local message containing a wikilink:
     * the message key is: 'namespace-' + namespace number + '-helppage'.
     * @param string $to Target MediaWiki.org page title or encoded URL.
     * @param bool $overrideBaseUrl Whether $url is a full URL, to avoid MW.o.
     * @since 1.25
     */
    public function addHelpLink( $to, $overrideBaseUrl = false ) {
        $out = $this->getContext()->getOutput();
        $msg = $out->msg( 'namespace-' . $this->getTitle()->getNamespace() . '-helppage' );
        if ( !$msg->isDisabled() ) {
            $title = Title::newFromText( $msg->plain() );
            if ( $title instanceof Title ) {
                $out->addHelpLink( $title->getLocalURL(), true );
            }
        } else {
            $out->addHelpLink( $to, $overrideBaseUrl );
        }
    }
    /**
     * Handle action=render
     */
    public function render() {
        $this->getContext()->getRequest()->response()->header( 'X-Robots-Tag: noindex' );
        $this->getContext()->getOutput()->setArticleBodyOnly( true );
        // We later set 'enableSectionEditLinks=false' based on this; also used by ImagePage
        $this->viewIsRenderAction = true;
        $this->view();
    }
    /**
     * action=protect handler
     */
    public function protect() {
        $form = new ProtectionForm( $this );
        $form->execute();
    }
    /**
     * action=unprotect handler (alias)
     */
    public function unprotect() {
        $this->protect();
    }
    /**
     * UI entry point for page deletion
     */
    public function delete() {
        # This code desperately needs to be totally rewritten
        $title = $this->getTitle();
        $context = $this->getContext();
        $user = $context->getUser();
        $request = $context->getRequest();
        # Check permissions
        $permissionErrors = $this->permManager->getPermissionErrors( 'delete', $user, $title );
        if ( count( $permissionErrors ) ) {
            throw new PermissionsError( 'delete', $permissionErrors );
        }
        # Read-only check...
        if ( wfReadOnly() ) {
            throw new ReadOnlyError;
        }
        # Better double-check that it hasn't been deleted yet!
        $this->mPage->loadPageData(
            $request->wasPosted() ? WikiPage::READ_LATEST : WikiPage::READ_NORMAL
        );
        if ( !$this->mPage->exists() ) {
            $deleteLogPage = new LogPage( 'delete' );
            $outputPage = $context->getOutput();
            $outputPage->setPageTitle( $context->msg( 'cannotdelete-title', $title->getPrefixedText() ) );
            $outputPage->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>",
                    [ 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) ]
                );
            $outputPage->addHTML(
                Xml::element( 'h2', null, $deleteLogPage->getName()->text() )
            );
            LogEventsList::showLogExtract(
                $outputPage,
                'delete',
                $title
            );
            return;
        }
        $deleteReasonList = $request->getText( 'wpDeleteReasonList', 'other' );
        $deleteReason = $request->getText( 'wpReason' );
        if ( $deleteReasonList == 'other' ) {
            $reason = $deleteReason;
        } elseif ( $deleteReason != '' ) {
            // Entry from drop down menu + additional comment
            $colonseparator = wfMessage( 'colon-separator' )->inContentLanguage()->text();
            $reason = $deleteReasonList . $colonseparator . $deleteReason;
        } else {
            $reason = $deleteReasonList;
        }
        if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ),
            [ 'delete', $this->getTitle()->getPrefixedText() ] )
        ) {
            # Flag to hide all contents of the archived revisions
            $suppress = $request->getCheck( 'wpSuppress' ) &&
                $this->permManager->userHasRight( $user, 'suppressrevision' );
            $this->doDelete( $reason, $suppress );
            WatchAction::doWatchOrUnwatch( $request->getCheck( 'wpWatch' ), $title, $user );
            return;
        }
        // Generate deletion reason
        $hasHistory = false;
        if ( !$reason ) {
            try {
                $reason = $this->getPage()
                    ->getAutoDeleteReason( $hasHistory );
            } catch ( Exception $e ) {
                # if a page is horribly broken, we still want to be able to
                # delete it. So be lenient about errors here.
                wfDebug( "Error while building auto delete summary: $e" );
                $reason = '';
            }
        }
        // If the page has a history, insert a warning
        if ( $hasHistory ) {
            $title = $this->getTitle();
            // The following can use the real revision count as this is only being shown for users
            // that can delete this page.
            // This, as a side-effect, also makes sure that the following query isn't being run for
            // pages with a larger history, unless the user has the 'bigdelete' right
            // (and is about to delete this page).
            $dbr = wfGetDB( DB_REPLICA );
            $revisions = $edits = (int)$dbr->selectField(
                'revision',
                'COUNT(rev_page)',
                [ 'rev_page' => $title->getArticleID() ],
                __METHOD__
            );
            // @todo i18n issue/patchwork message
            $context->getOutput()->addHTML(
                '<strong class="mw-delete-warning-revisions">' .
                $context->msg( 'historywarning' )->numParams( $revisions )->parse() .
                $context->msg( 'word-separator' )->escaped() . $this->linkRenderer->makeKnownLink(
                    $title,
                    $context->msg( 'history' )->text(),
                    [],
                    [ 'action' => 'history' ] ) .
                '</strong>'
            );
            if ( $title->isBigDeletion() ) {
                global $wgDeleteRevisionsLimit;