Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
52.63% covered (warning)
52.63%
30 / 57
CRAP
81.87% covered (warning)
81.87%
682 / 833
ApiMain
0.00% covered (danger)
0.00%
0 / 1
52.63% covered (warning)
52.63%
30 / 57
760.06
81.87% covered (warning)
81.87%
682 / 833
 __construct
100.00% covered (success)
100.00%
1 / 1
13
100.00% covered (success)
100.00%
61 / 61
 isInternalMode
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getResult
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 lacksSameOriginSecurity
0.00% covered (danger)
0.00%
0 / 1
5.01
93.33% covered (success)
93.33%
14 / 15
 getErrorFormatter
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getContinuationManager
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setContinuationManager
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
6 / 6
 getParamValidator
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getModule
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getPrinter
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 setCacheMaxAge
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 4
 setCacheMode
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
14 / 14
 setCacheControl
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 createPrinterByName
0.00% covered (danger)
0.00%
0 / 1
2.26
60.00% covered (warning)
60.00%
3 / 5
 execute
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 executeActionWithErrorHandling
0.00% covered (danger)
0.00%
0 / 1
8.03
92.59% covered (success)
92.59%
25 / 27
 handleException
0.00% covered (danger)
0.00%
0 / 1
8.51
80.00% covered (warning)
80.00%
24 / 30
 handleApiBeforeMainException
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 9
 handleCORS
0.00% covered (danger)
0.00%
0 / 1
82.31
41.67% covered (danger)
41.67%
25 / 60
 matchOrigin
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 7
 matchRequestedHeaders
0.00% covered (danger)
0.00%
0 / 1
4.01
91.67% covered (success)
91.67%
11 / 12
 wildcardToRegex
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 6
 sendCacheHeaders
0.00% covered (danger)
0.00%
0 / 1
47.37
64.15% covered (warning)
64.15%
34 / 53
 createErrorPrinter
0.00% covered (danger)
0.00%
0 / 1
4.25
75.00% covered (warning)
75.00%
6 / 8
 errorMessagesFromException
0.00% covered (danger)
0.00%
0 / 1
7.06
89.47% covered (warning)
89.47%
17 / 19
 substituteResultWithError
100.00% covered (success)
100.00%
1 / 1
12
100.00% covered (success)
100.00%
49 / 49
 addRequestedFields
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
15 / 15
 setupExecuteAction
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 setupModule
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
16 / 16
 getMaxLag
0.00% covered (danger)
0.00%
0 / 1
3.91
53.33% covered (warning)
53.33%
8 / 15
 checkMaxLag
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
14 / 14
 checkConditionalRequestHeaders
100.00% covered (success)
100.00%
1 / 1
21
100.00% covered (success)
100.00%
48 / 48
 checkExecutePermissions
100.00% covered (success)
100.00%
1 / 1
9
100.00% covered (success)
100.00%
16 / 16
 checkReadOnly
0.00% covered (danger)
0.00%
0 / 1
5.58
71.43% covered (warning)
71.43%
5 / 7
 checkBotReadOnly
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 22
 checkAsserts
100.00% covered (success)
100.00%
1 / 1
11
100.00% covered (success)
100.00%
21 / 21
 setupExternalResponse
0.00% covered (danger)
0.00%
0 / 1
12.57
84.21% covered (warning)
84.21%
16 / 19
 executeAction
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
20 / 20
 setRequestExpectations
0.00% covered (danger)
0.00%
0 / 1
4.02
90.00% covered (success)
90.00%
9 / 10
 logRequest
0.00% covered (danger)
0.00%
0 / 1
9.15
87.80% covered (warning)
87.80%
36 / 41
 encodeRequestLogValue
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
7 / 7
 getParamsUsed
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 markParamsUsed
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getSensitiveParams
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 markParamsSensitive
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getVal
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
7 / 7
 getCheck
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getUpload
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 reportUnusedParams
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
13 / 13
 printResult
0.00% covered (danger)
0.00%
0 / 1
3.10
77.78% covered (warning)
77.78%
7 / 9
 isReadMode
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getAllowedParams
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getExamplesMessages
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 modifyHelp
0.00% covered (danger)
0.00%
0 / 1
21
96.36% covered (success)
96.36%
106 / 110
 canApiHighLimits
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 getModuleManager
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getUserAgent
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
<?php
/**
 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
 *
 * 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
 * @defgroup API API
 */
use MediaWiki\Api\Validator\ApiParamValidator;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use MediaWiki\ParamValidator\TypeDef\UserDef;
use MediaWiki\Session\SessionManager;
use Wikimedia\Timestamp\TimestampException;
/**
 * This is the main API class, used for both external and internal processing.
 * When executed, it will create the requested formatter object,
 * instantiate and execute an object associated with the needed action,
 * and use formatter to print results.
 * In case of an exception, an error message will be printed using the same formatter.
 *
 * To use API from another application, run it using FauxRequest object, in which
 * case any internal exceptions will not be handled but passed up to the caller.
 * After successful execution, use getResult() for the resulting data.
 *
 * @newable
 * @note marked as newable in 1.35 for lack of a better alternative,
 *       but should use a factory in the future.
 * @ingroup API
 */
class ApiMain extends ApiBase {
    /**
     * When no format parameter is given, this format will be used
     */
    private const API_DEFAULT_FORMAT = 'jsonfm';
    /**
     * When no uselang parameter is given, this language will be used
     */
    private const API_DEFAULT_USELANG = 'user';
    /**
     * List of available modules: action name => module class
     */
    private static $Modules = [
        'login' => ApiLogin::class,
        'clientlogin' => ApiClientLogin::class,
        'logout' => ApiLogout::class,
        'createaccount' => ApiAMCreateAccount::class,
        'linkaccount' => ApiLinkAccount::class,
        'unlinkaccount' => ApiRemoveAuthenticationData::class,
        'changeauthenticationdata' => ApiChangeAuthenticationData::class,
        'removeauthenticationdata' => ApiRemoveAuthenticationData::class,
        'resetpassword' => ApiResetPassword::class,
        'query' => ApiQuery::class,
        'expandtemplates' => ApiExpandTemplates::class,
        'parse' => ApiParse::class,
        'stashedit' => ApiStashEdit::class,
        'opensearch' => ApiOpenSearch::class,
        'feedcontributions' => ApiFeedContributions::class,
        'feedrecentchanges' => ApiFeedRecentChanges::class,
        'feedwatchlist' => ApiFeedWatchlist::class,
        'help' => ApiHelp::class,
        'paraminfo' => ApiParamInfo::class,
        'rsd' => ApiRsd::class,
        'compare' => ApiComparePages::class,
        'tokens' => ApiTokens::class,
        'checktoken' => ApiCheckToken::class,
        'cspreport' => ApiCSPReport::class,
        'validatepassword' => ApiValidatePassword::class,
        // Write modules
        'purge' => ApiPurge::class,
        'setnotificationtimestamp' => ApiSetNotificationTimestamp::class,
        'rollback' => ApiRollback::class,
        'delete' => ApiDelete::class,
        'undelete' => ApiUndelete::class,
        'protect' => ApiProtect::class,
        'block' => ApiBlock::class,
        'unblock' => [
            'class' => ApiUnblock::class,
            'services' => [
                'BlockPermissionCheckerFactory',
                'UnblockUserFactory',
                'PermissionManager'
            ]
        ],
        'move' => ApiMove::class,
        'edit' => ApiEditPage::class,
        'upload' => ApiUpload::class,
        'filerevert' => ApiFileRevert::class,
        'emailuser' => ApiEmailUser::class,
        'watch' => ApiWatch::class,
        'patrol' => ApiPatrol::class,
        'import' => ApiImport::class,
        'clearhasmsg' => ApiClearHasMsg::class,
        'userrights' => ApiUserrights::class,
        'options' => ApiOptions::class,
        'imagerotate' => ApiImageRotate::class,
        'revisiondelete' => ApiRevisionDelete::class,
        'managetags' => ApiManageTags::class,
        'tag' => ApiTag::class,
        'mergehistory' => ApiMergeHistory::class,
        'setpagelanguage' => ApiSetPageLanguage::class,
        'changecontentmodel' => ApiChangeContentModel::class,
    ];
    /**
     * List of available formats: format name => format class
     */
    private static $Formats = [
        'json' => ApiFormatJson::class,
        'jsonfm' => ApiFormatJson::class,
        'php' => ApiFormatPhp::class,
        'phpfm' => ApiFormatPhp::class,
        'xml' => ApiFormatXml::class,
        'xmlfm' => ApiFormatXml::class,
        'rawfm' => ApiFormatJson::class,
        'none' => ApiFormatNone::class,
    ];
    /**
     * List of user roles that are specifically relevant to the API.
     * [ 'right' => [ 'msg'    => 'Some message with a $1',
     *                'params' => [ $someVarToSubst ] ],
     * ];
     */
    private static $mRights = [
        'writeapi' => [
            'msg' => 'right-writeapi',
            'params' => []
        ],
        'apihighlimits' => [
            'msg' => 'api-help-right-apihighlimits',
            'params' => [ ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ]
        ]
    ];
    /**
     * @var ApiFormatBase
     */
    private $mPrinter;
    private $mModuleMgr, $mResult, $mErrorFormatter = null, $mParamValidator;
    /** @var ApiContinuationManager|null */
    private $mContinuationManager;
    private $mAction;
    private $mEnableWrite;
    private $mInternalMode, $mCdnMaxAge;
    /** @var ApiBase */
    private $mModule;
    private $mCacheMode = 'private';
    /** @var array */
    private $mCacheControl = [];
    private $mParamsUsed = [];
    private $mParamsSensitive = [];
    /** @var bool|null Cached return value from self::lacksSameOriginSecurity() */
    private $lacksSameOriginSecurity = null;
    /**
     * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
     *
     * @stable to call
     * @param IContextSource|WebRequest|null $context If this is an instance of
     *    FauxRequest, errors are thrown and no printing occurs
     * @param bool $enableWrite Should be set to true if the api may modify data
     */
    public function __construct( $context = null, $enableWrite = false ) {
        if ( $context === null ) {
            $context = RequestContext::getMain();
        } elseif ( $context instanceof WebRequest ) {
            // BC for pre-1.19
            $request = $context;
            $context = RequestContext::getMain();
        }
        // We set a derivative context so we can change stuff later
        $derivativeContext = new DerivativeContext( $context );
        $this->setContext( $derivativeContext );
        if ( isset( $request ) ) {
            $derivativeContext->setRequest( $request );
        } else {
            $request = $this->getRequest();
        }
        $this->mInternalMode = ( $request instanceof FauxRequest );
        // Special handling for the main module: $parent === $this
        parent::__construct( $this, $this->mInternalMode ? 'main_int' : 'main' );
        $config = $this->getConfig();
        if ( !$this->mInternalMode ) {
            // If we're in a mode that breaks the same-origin policy, strip
            // user credentials for security.
            if ( $this->lacksSameOriginSecurity() ) {
                global $wgUser;
                wfDebug( "API: stripping user credentials when the same-origin policy is not applied" );
                $user = new User();
                $wgUser = $user;
                $derivativeContext->setUser( $user );
                $request->response()->header( 'MediaWiki-Login-Suppressed: true' );
            }
        }
        $this->mParamValidator = new ApiParamValidator(
            $this, MediaWikiServices::getInstance()->getObjectFactory()
        );
        $this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
        // Setup uselang. This doesn't use $this->getParameter()
        // because we're not ready to handle errors yet.
        // Optimisation: Avoid slow getVal(), this isn't user-generated content.
        $uselang = $request->getRawVal( 'uselang', self::API_DEFAULT_USELANG );
        if ( $uselang === 'user' ) {
            // Assume the parent context is going to return the user language
            // for uselang=user (see T85635).
        } else {
            if ( $uselang === 'content' ) {
                $uselang = MediaWikiServices::getInstance()->getContentLanguage()->getCode();
            }
            $code = RequestContext::sanitizeLangCode( $uselang );
            $derivativeContext->setLanguage( $code );
            if ( !$this->mInternalMode ) {
                global $wgLang;
                $wgLang = $derivativeContext->getLanguage();
                RequestContext::getMain()->setLanguage( $wgLang );
            }
        }
        // Set up the error formatter. This doesn't use $this->getParameter()
        // because we're not ready to handle errors yet.
        // Optimisation: Avoid slow getVal(), this isn't user-generated content.
        $errorFormat = $request->getRawVal( 'errorformat', 'bc' );
        $errorLangCode = $request->getRawVal( 'errorlang', 'uselang' );
        $errorsUseDB = $request->getCheck( 'errorsuselocal' );
        if ( in_array( $errorFormat, [ 'plaintext', 'wikitext', 'html', 'raw', 'none' ], true ) ) {
            if ( $errorLangCode === 'uselang' ) {
                $errorLang = $this->getLanguage();
            } elseif ( $errorLangCode === 'content' ) {
                $errorLang = MediaWikiServices::getInstance()->getContentLanguage();
            } else {
                $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode );
                $errorLang = MediaWikiServices::getInstance()->getLanguageFactory()
                    ->getLanguage( $errorLangCode );
            }
            $this->mErrorFormatter = new ApiErrorFormatter(
                $this->mResult, $errorLang, $errorFormat, $errorsUseDB
            );
        } else {
            $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
        }
        $this->mResult->setErrorFormatter( $this->getErrorFormatter() );
        $this->mModuleMgr = new ApiModuleManager(
            $this,
            MediaWikiServices::getInstance()->getObjectFactory()
        );
        $this->mModuleMgr->addModules( self::$Modules, 'action' );
        $this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
        $this->mModuleMgr->addModules( self::$Formats, 'format' );
        $this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' );
        $this->getHookRunner()->onApiMain__moduleManager( $this->mModuleMgr );
        $this->mContinuationManager = null;
        $this->mEnableWrite = $enableWrite;
        $this->mCdnMaxAge = -1; // flag for executeActionWithErrorHandling()
    }
    /**
     * Return true if the API was started by other PHP code using FauxRequest
     * @return bool
     */
    public function isInternalMode() {
        return $this->mInternalMode;
    }
    /**
     * Get the ApiResult object associated with current request
     *
     * @return ApiResult
     */
    public function getResult() {
        return $this->mResult;
    }
    /**
     * Get the security flag for the current request
     * @return bool
     */
    public function lacksSameOriginSecurity() {
        if ( $this->lacksSameOriginSecurity !== null ) {
            return $this->lacksSameOriginSecurity;
        }
        $request = $this->getRequest();
        // JSONP mode
        if ( $request->getCheck( 'callback' ) ) {
            $this->lacksSameOriginSecurity = true;
            return true;
        }
        // Anonymous CORS
        if ( $request->getVal( 'origin' ) === '*' ) {
            $this->lacksSameOriginSecurity = true;
            return true;
        }
        // Header to be used from XMLHTTPRequest when the request might
        // otherwise be used for XSS.
        if ( $request->getHeader( 'Treat-as-Untrusted' ) !== false ) {
            $this->lacksSameOriginSecurity = true;
            return true;
        }
        // Allow extensions to override.
        $this->lacksSameOriginSecurity = !$this->getHookRunner()
            ->onRequestHasSameOriginSecurity( $request );
        return $this->lacksSameOriginSecurity;
    }
    /**
     * Get the ApiErrorFormatter object associated with current request
     * @return ApiErrorFormatter
     */
    public function getErrorFormatter() {
        return $this->mErrorFormatter;
    }
    /**
     * Get the continuation manager
     * @return ApiContinuationManager|null
     */
    public function getContinuationManager() {
        return $this->mContinuationManager;
    }
    /**
     * Set the continuation manager
     * @param ApiContinuationManager|null $manager
     */
    public function setContinuationManager( ApiContinuationManager $manager = null ) {
        if ( $manager !== null && $this->mContinuationManager !== null ) {
            throw new UnexpectedValueException(
                __METHOD__ . ': tried to set manager from ' . $manager->getSource() .
                ' when a manager is already set from ' . $this->mContinuationManager->getSource()
            );
        }
        $this->mContinuationManager = $manager;
    }
    /**
     * Get the parameter validator
     * @return ApiParamValidator
     */
    public function getParamValidator() : ApiParamValidator {
        return $this->mParamValidator;
    }
    /**
     * Get the API module object. Only works after executeAction()
     *
     * @return ApiBase
     */
    public function getModule() {
        return $this->mModule;
    }
    /**
     * Get the result formatter object. Only works after setupExecuteAction()
     *
     * @return ApiFormatBase
     */
    public function getPrinter() {
        return $this->mPrinter;
    }
    /**
     * Set how long the response should be cached.
     *
     * @param int $maxage
     */
    public function setCacheMaxAge( $maxage ) {
        $this->setCacheControl( [
            'max-age' => $maxage,
            's-maxage' => $maxage
        ] );
    }
    /**
     * Set the type of caching headers which will be sent.
     *
     * @param string $mode One of:
     *    - 'public':     Cache this object in public caches, if the maxage or smaxage
     *         parameter is set, or if setCacheMaxAge() was called. If a maximum age is
     *         not provided by any of these means, the object will be private.
     *    - 'private':    Cache this object only in private client-side caches.
     *    - 'anon-public-user-private': Make this object cacheable for logged-out
     *         users, but private for logged-in users. IMPORTANT: If this is set, it must be
     *         set consistently for a given URL, it cannot be set differently depending on
     *         things like the contents of the database, or whether the user is logged in.
     *
     *  If the wiki does not allow anonymous users to read it, the mode set here
     *  will be ignored, and private caching headers will always be sent. In other words,
     *  the "public" mode is equivalent to saying that the data sent is as public as a page
     *  view.
     *
     *  For user-dependent data, the private mode should generally be used. The
     *  anon-public-user-private mode should only be used where there is a particularly
     *  good performance reason for caching the anonymous response, but where the
     *  response to logged-in users may differ, or may contain private data.
     *
     *  If this function is never called, then the default will be the private mode.
     */
    public function setCacheMode( $mode ) {
        if ( !in_array( $mode, [ 'private', 'public', 'anon-public-user-private' ] ) ) {
            wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"" );
            // Ignore for forwards-compatibility
            return;
        }
        if ( !$this->getPermissionManager()->isEveryoneAllowed( 'read' ) ) {
            // Private wiki, only private headers
            if ( $mode !== 'private' ) {
                wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki" );
                return;
            }
        }
        if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
            // User language is used for i18n, so we don't want to publicly
            // cache. Anons are ok, because if they have non-default language
            // then there's an appropriate Vary header set by whatever set
            // their non-default language.
            wfDebug( __METHOD__ . ": downgrading cache mode 'public' to " .
                "'anon-public-user-private' due to uselang=user" );
            $mode = 'anon-public-user-private';
        }
        wfDebug( __METHOD__ . ": setting cache mode $mode" );
        $this->mCacheMode = $mode;
    }
    /**
     * Set directives (key/value pairs) for the Cache-Control header.
     * Boolean values will be formatted as such, by including or omitting
     * without an equals sign.
     *
     * Cache control values set here will only be used if the cache mode is not
     * private, see setCacheMode().
     *
     * @param array $directives
     */
    public function setCacheControl( $directives ) {
        $this->mCacheControl = $directives + $this->mCacheControl;
    }
    /**
     * Create an instance of an output formatter by its name
     *
     * @param string $format
     *
     * @return ApiFormatBase
     */
    public function createPrinterByName( $format ) {
        $printer = $this->mModuleMgr->getModule( $format, 'format', /* $ignoreCache */ true );
        if ( $printer === null ) {
            $this->dieWithError(
                [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
            );
        }
        return $printer;
    }
    /**
     * Execute api request. Any errors will be handled if the API was called by the remote client.
     */
    public function execute() {
        if ( $this->mInternalMode ) {
            $this->executeAction();
        } else {
            $this->executeActionWithErrorHandling();
        }
    }
    /**
     * Execute an action, and in case of an error, erase whatever partial results
     * have been accumulated, and replace it with an error message and a help screen.
     */
    protected function executeActionWithErrorHandling() {
        // Verify the CORS header before executing the action
        if ( !$this->handleCORS() ) {
            // handleCORS() has sent a 403, abort
            return;
        }
        // Exit here if the request method was OPTIONS
        // (assume there will be a followup GET or POST)
        if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
            return;
        }
        // In case an error occurs during data output,
        // clear the output buffer and print just the error information
        $obLevel = ob_get_level();
        ob_start();
        $t = microtime( true );
        $isError = false;
        try {
            $this->executeAction();
            $runTime = microtime( true ) - $t;
            $this->logRequest( $runTime );
            MediaWikiServices::getInstance()->getStatsdDataFactory()->timing(
                'api.' . $this->mModule->getModuleName() . '.executeTiming', 1000 * $runTime
            );
        } catch ( Throwable $e ) {
            $this->handleException( $e );
            $this->logRequest( microtime( true ) - $t, $e );
            $isError = true;
        }
        // Disable the client cache on the output so that BlockManager::trackBlockWithCookie is executed
        // as part of MediaWiki::preOutputCommit().
        if (
            $this->mCacheMode === 'private'
            || (
                $this->mCacheMode === 'anon-public-user-private'
                && SessionManager::getGlobalSession()->isPersistent()
            )
        ) {
            $this->getContext()->getOutput()->enableClientCache( false );
            $this->getContext()->getOutput()->considerCacheSettingsFinal();
        }
        // Commit DBs and send any related cookies and headers
        MediaWiki::preOutputCommit( $this->getContext() );
        // Send cache headers after any code which might generate an error, to
        // avoid sending public cache headers for errors.
        $this->sendCacheHeaders( $isError );
        // Executing the action might have already messed with the output
        // buffers.
        while ( ob_get_level() > $obLevel ) {
            ob_end_flush();
        }
    }
    /**
     * Handle a throwable as an API response
     *
     * @since 1.23
     * @param Throwable $e
     */
    protected function handleException( Throwable $e ) {
        // T65145: Rollback any open database transactions
        if ( !$e instanceof ApiUsageException ) {
            // ApiUsageExceptions are intentional, so don't rollback if that's the case
            MWExceptionHandler::rollbackMasterChangesAndLog(
                $e,
                MWExceptionHandler::CAUGHT_BY_ENTRYPOINT
            );
        }
        // Allow extra cleanup and logging
        $this->getHookRunner()->onApiMain__onException( $this, $e );
        // Handle any kind of exception by outputting properly formatted error message.
        // If this fails, an unhandled exception should be thrown so that global error
        // handler will process and log it.
        $errCodes = $this->substituteResultWithError( $e );
        // Error results should not be cached
        $this->setCacheMode( 'private' );
        $response = $this->getRequest()->response();
        $headerStr = 'MediaWiki-API-Error: ' . implode( ', ', $errCodes );
        $response->header( $headerStr );
        // Reset and print just the error message
        ob_clean();
        // Printer may not be initialized if the extractRequestParams() fails for the main module
        $this->createErrorPrinter();
        // Get desired HTTP code from an ApiUsageException. Don't use codes from other
        // exception types, as they are unlikely to be intended as an HTTP code.
        $httpCode = $e instanceof ApiUsageException ? $e->getCode() : 0;
        $failed = false;
        try {
            $this->printResult( $httpCode );
        } catch ( ApiUsageException $ex ) {
            // The error printer itself is failing. Try suppressing its request
            // parameters and redo.
            $failed = true;
            $this->addWarning( 'apiwarn-errorprinterfailed' );
            foreach ( $ex->getStatusValue()->getErrors() as $error ) {
                try {
                    $this->mPrinter->addWarning( $error );
                } catch ( Throwable $ex2 ) {
                    // WTF?
                    $this->addWarning( $error );
                }
            }
        }
        if ( $failed ) {
            $this->mPrinter = null;
            $this->createErrorPrinter();
            $this->mPrinter->forceDefaultParams();
            if ( $httpCode ) {
                $response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200
            }
            $this->printResult( $httpCode );
        }
    }
    /**
     * Handle a throwable from the ApiBeforeMain hook.
     *
     * This tries to print the throwable as an API response, to be more
     * friendly to clients. If it fails, it will rethrow the throwable.
     *
     * @since 1.23
     * @param Throwable $e
     * @throws Throwable
     */
    public static function handleApiBeforeMainException( Throwable $e ) {
        ob_start();
        try {
            $main = new self( RequestContext::getMain(), false );
            $main->handleException( $e );
            $main->logRequest( 0, $e );
        } catch ( Throwable $e2 ) {
            // Nope, even that didn't work. Punt.
            throw $e;
        }
        // Reset cache headers
        $main->sendCacheHeaders( true );
        ob_end_flush();
    }
    /**
     * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
     *
     * If no origin parameter is present, nothing happens.
     * If an origin parameter is present but doesn't match the Origin header, a 403 status code
     * is set and false is returned.
     * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
     * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
     * headers are set.
     * https://www.w3.org/TR/cors/#resource-requests
     * https://www.w3.org/TR/cors/#resource-preflight-requests
     *
     * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
     */
    protected function handleCORS() {
        $originParam = $this->getParameter( 'origin' ); // defaults to null
        if ( $originParam === null ) {
            // No origin parameter, nothing to do
            return true;
        }
        $request = $this->getRequest();
        $response = $request->response();
        $allowTiming = false;
        $varyOrigin = true;
        if ( $originParam === '*' ) {
            // Request for anonymous CORS
            // Technically we should check for the presence of an Origin header
            // and not process it as CORS if it's not set, but that would
            // require us to vary on Origin for all 'origin=*' requests which
            // we don't want to do.
            $matchedOrigin = true;
            $allowOrigin = '*';
            $allowCredentials = 'false';
            $varyOrigin = false; // No need to vary
        } else {
            // Non-anonymous CORS, check we allow the domain
            // Origin: header is a space-separated list of origins, check all of them
            $originHeader = $request->getHeader( 'Origin' );
            if ( $originHeader === false ) {
                $origins = [];
            } else {
                $originHeader = trim( $originHeader );
                $origins = preg_split( '/\s+/', $originHeader );
            }
            if ( !in_array( $originParam, $origins ) ) {
                // origin parameter set but incorrect
                // Send a 403 response
                $response->statusHeader( 403 );
                $response->header( 'Cache-Control: no-cache' );
                echo "'origin' parameter does not match Origin header\n";
                return false;
            }
            $config = $this->getConfig();
            $matchedOrigin = count( $origins ) === 1 && self::matchOrigin(
                $originParam,
                $config->get( 'CrossSiteAJAXdomains' ),
                $config->get( 'CrossSiteAJAXdomainExceptions' )
            );
            $allowOrigin = $originHeader;
            $allowCredentials = 'true';
            $allowTiming = $originHeader;
        }
        if ( $matchedOrigin ) {
            $requestedMethod = $request->getHeader( 'Access-Control-Request-Method' );
            $preflight = $request->getMethod() === 'OPTIONS' && $requestedMethod !== false;
            if ( $preflight ) {
                // This is a CORS preflight request
                if ( $requestedMethod !== 'POST' && $requestedMethod !== 'GET' ) {
                    // If method is not a case-sensitive match, do not set any additional headers and terminate.
                    $response->header( 'MediaWiki-CORS-Rejection: Unsupported method requested in preflight' );
                    return true;
                }
                // We allow the actual request to send the following headers
                $requestedHeaders = $request->getHeader( 'Access-Control-Request-Headers' );
                $allowedHeaders = $this->getConfig()->get( 'AllowedCorsHeaders' );
                if ( $requestedHeaders !== false ) {
                    if ( !self::matchRequestedHeaders( $requestedHeaders, $allowedHeaders ) ) {
                        $response->header( 'MediaWiki-CORS-Rejection: Unsupported header requested in preflight' );
                        return true;
                    }
                    $response->header( 'Access-Control-Allow-Headers: ' . $requestedHeaders );
                }
                // We only allow the actual request to be GET or POST
                $response->header( 'Access-Control-Allow-Methods: POST, GET' );
            } elseif ( $request->getMethod() !== 'POST' && $request->getMethod() !== 'GET' ) {
                // Unsupported non-preflight method, don't handle it as CORS
                $response->header(
                    'MediaWiki-CORS-Rejection: Unsupported method for simple request or actual request'
                );
                return true;
            }
            $response->header( "Access-Control-Allow-Origin: $allowOrigin" );
            $response->header( "Access-Control-Allow-Credentials: $allowCredentials" );
            // https://www.w3.org/TR/resource-timing/#timing-allow-origin
            if ( $allowTiming !== false ) {
                $response->header( "Timing-Allow-Origin: $allowTiming" );
            }
            if ( !$preflight ) {
                $response->header(
                    'Access-Control-Expose-Headers: MediaWiki-API-Error, Retry-After, X-Database-Lag, '
                    . 'MediaWiki-Login-Suppressed'
                );
            }
        } else {
            $response->header( 'MediaWiki-CORS-Rejection: Origin mismatch' );
        }
        if ( $varyOrigin ) {
            $this->getOutput()->addVaryHeader( 'Origin' );
        }
        return true;
    }
    /**
     * Attempt to match an Origin header against a set of rules and a set of exceptions
     * @param string $value Origin header
     * @param array $rules Set of wildcard rules
     * @param array $exceptions Set of wildcard rules
     * @return bool True if $value matches a rule in $rules and doesn't match
     *    any rules in $exceptions, false otherwise
     */
    protected static function matchOrigin( $value, $rules, $exceptions ) {
        foreach ( $rules as $rule ) {
            if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) {
                // Rule matches, check exceptions
                foreach ( $exceptions as $exc ) {
                    if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) {
                        return false;
                    }
                }
                return true;
            }
        }
        return false;
    }
    /**
     * Attempt to validate the value of Access-Control-Request-Headers against a list
     * of headers that we allow the follow up request to send.
     *
     * @param string $requestedHeaders Comma separated list of HTTP headers
     * @param string[] $allowedHeaders List of allowed HTTP headers
     * @return bool True if all requested headers are in the list of allowed headers
     */
    protected static function matchRequestedHeaders( $requestedHeaders, $allowedHeaders ) {
        if ( trim( $requestedHeaders ) === '' ) {
            return true;
        }
        $requestedHeaders = explode( ',', $requestedHeaders );
        $allowedHeaders = array_change_key_case( array_flip( $allowedHeaders ), CASE_LOWER );
        foreach ( $requestedHeaders as $rHeader ) {
            $rHeader = strtolower( trim( $rHeader ) );
            if ( !isset( $allowedHeaders[$rHeader] ) ) {
                LoggerFactory::getInstance( 'api-warning' )->warning(
                    'CORS preflight failed on requested header: {header}', [
                        'header' => $rHeader
                    ]
                );
                return false;
            }
        }
        return true;
    }
    /**
     * Helper function to convert wildcard string into a regex
     * '*' => '.*?'
     * '?' => '.'
     *
     * @param string $wildcard String with wildcards
     * @return string Regular expression
     */
    protected static function wildcardToRegex( $wildcard ) {
        $wildcard = preg_quote( $wildcard, '/' );
        $wildcard = str_replace(
            [ '\*', '\?' ],
            [ '.*?', '.' ],
            $wildcard
        );
        return "/^https?:\/\/$wildcard$/";
    }
    /**
     * Send caching headers
     * @param bool $isError Whether an error response is being output
     * @since 1.26 added $isError parameter
     */
    protected function sendCacheHeaders( $isError ) {
        $response = $this->getRequest()->response();
        $out = $this->getOutput();
        $out->addVaryHeader( 'Treat-as-Untrusted' );
        $config = $this->getConfig();
        if ( $config->get( 'VaryOnXFP' ) ) {
            $out->addVaryHeader( 'X-Forwarded-Proto' );
        }
        if ( !$isError && $this->mModule &&
            ( $this->getRequest()->getMethod() === 'GET' || $this->getRequest()->getMethod() === 'HEAD' )
        ) {
            $etag = $this->mModule->getConditionalRequestData( 'etag' );
            if ( $etag !== null ) {
                $response->header( "ETag: $etag" );
            }
            $lastMod = $this->mModule->getConditionalRequestData( 'last-modified' );
            if ( $lastMod !== null ) {
                $response->header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $lastMod ) );
            }
        }
        // The logic should be:
        // $this->mCacheControl['max-age'] is set?
        //    Use it, the module knows better than our guess.
        // !$this->mModule || $this->mModule->isWriteMode(), and mCacheMode is private?
        //    Use 0 because we can guess caching is probably the wrong thing to do.
        // Use $this->getParameter( 'maxage' ), which already defaults to 0.
        $maxage = 0;
        if ( isset( $this->mCacheControl['max-age'] ) ) {
            $maxage = $this->mCacheControl['max-age'];
        } elseif ( ( $this->mModule && !$this->mModule->isWriteMode() ) ||
            $this->mCacheMode !== 'private'
        ) {
            $maxage = $this->getParameter( 'maxage' );
        }
        $privateCache = 'private, must-revalidate, max-age=' . $maxage;
        if ( $this->mCacheMode == 'private' ) {
            $response->header( "Cache-Control: $privateCache" );
            return;
        }
        if ( $this->mCacheMode == 'anon-public-user-private' ) {
            $out->addVaryHeader( 'Cookie' );
            $response->header( $out->getVaryHeader() );
            if ( SessionManager::getGlobalSession()->isPersistent() ) {
                // Logged in or otherwise has session (e.g. anonymous users who have edited)
                // Mark request private
                $response->header( "Cache-Control: $privateCache" );
                return;
            } // else anonymous, send public headers below
        }
        // Send public headers
        $response->header( $out->getVaryHeader() );
        // If nobody called setCacheMaxAge(), use the (s)maxage parameters
        if ( !isset( $this->mCacheControl['s-maxage'] ) ) {
            $this->mCacheControl['s-maxage'] = $this->getParameter( 'smaxage' );
        }
        if ( !isset( $this->mCacheControl['max-age'] ) ) {
            $this->mCacheControl['max-age'] = $this->getParameter( 'maxage' );
        }
        if ( !$this->mCacheControl['s-maxage'] && !$this->mCacheControl['max-age'] ) {
            // Public cache not requested
            // Sending a Vary header in this case is harmless, and protects us
            // against conditional calls of setCacheMaxAge().
            $response->header( "Cache-Control: $privateCache" );
            return;
        }
        $this->mCacheControl['public'] = true;
        // Send an Expires header
        $maxAge = min( $this->mCacheControl['s-maxage'], $this->mCacheControl['max-age'] );
        $expiryUnixTime = ( $maxAge == 0 ? 1 : time() + $maxAge );
        $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822, $expiryUnixTime ) );
        // Construct the Cache-Control header
        $ccHeader = '';
        $separator = '';
        foreach ( $this->mCacheControl as $name => $value ) {
            if ( is_bool( $value ) ) {
                if ( $value ) {
                    $ccHeader .= $separator . $name;
                    $separator = ', ';
                }
            } else {
                $ccHeader .= $separator . "$name=$value";
                $separator = ', ';
            }
        }
        $response->header( "Cache-Control: $ccHeader" );
    }
    /**
     * Create the printer for error output
     */
    private function createErrorPrinter() {
        if ( !isset( $this->mPrinter ) ) {
            $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT );
            if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
                $value = self::API_DEFAULT_FORMAT;
            }
            $this->mPrinter = $this->createPrinterByName( $value );
        }
        // Printer may not be able to handle errors. This is particularly
        // likely if the module returns something for getCustomPrinter().
        if ( !$this->mPrinter->canPrintErrors() ) {
            $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT );
        }
    }
    /**
     * Create an error message for the given throwable.
     *
     * If an ApiUsageException, errors/warnings will be extracted from the
     * embedded StatusValue.
     *
     * Any other throwable will be returned with a generic code and wrapper
     * text around the throwable's (presumably English) message as a single
     * error (no warnings).
     *
     * @param Throwable $e
     * @param string $type 'error' or 'warning'
     * @return ApiMessage[]
     * @since 1.27
     */
    protected function errorMessagesFromException( Throwable $e, $type = 'error' ) {
        $messages = [];
        if ( $e instanceof ApiUsageException ) {
            foreach ( $e->getStatusValue()->getErrorsByType( $type ) as $error ) {
                $messages[] = ApiMessage::create( $error );
            }
        } elseif ( $type !== 'error' ) {
            // None of the rest have any messages for non-error types
        } else {
            // Something is seriously wrong
            $config = $this->getConfig();
            // TODO: Avoid embedding arbitrary class names in the error code.
            $class = preg_replace( '#^Wikimedia\\\Rdbms\\\#', '', get_class( $e ) );
            $code = 'internal_api_error_' . $class;
            $data = [ 'errorclass' => get_class( $e ) ];
            if ( $config->get( 'ShowExceptionDetails' ) ) {
                if ( $e instanceof ILocalizedException ) {
                    $msg = $e->getMessageObject();
                } elseif ( $e instanceof MessageSpecifier ) {
                    $msg = Message::newFromSpecifier( $e );
                } else {
                    $msg = wfEscapeWikiText( $e->getMessage() );
                }
                $params = [ 'apierror-exceptioncaught', WebRequest::getRequestId(), $msg ];
            } else {
                $params = [ 'apierror-exceptioncaughttype', WebRequest::getRequestId(), get_class( $e ) ];
            }
            $messages[] = ApiMessage::create( $params, $code, $data );
        }
        return $messages;
    }
    /**
     * Replace the result data with the information about a throwable.
     * @param Throwable $e
     * @return string[] Error codes
     */
    protected function substituteResultWithError( Throwable $e ) {
        $result = $this->getResult();
        $formatter = $this->getErrorFormatter();
        $config = $this->getConfig();
        $errorCodes = [];
        // Remember existing warnings and errors across the reset
        $errors = $result->getResultData( [ 'errors' ] );
        $warnings = $result->getResultData( [ 'warnings' ] );
        $result->reset();
        if ( $warnings !== null ) {
            $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
        }
        if ( $errors !== null ) {
            $result->addValue( null, 'errors', $errors, ApiResult::NO_SIZE_CHECK );
            // Collect the copied error codes for the return value
            foreach ( $errors as $error ) {
                if ( isset( $error['code'] ) ) {
                    $errorCodes[$error['code']] = true;
                }
            }
        }
        // Add errors from the exception
        $modulePath = $e instanceof ApiUsageException ? $e->getModulePath() : null;
        foreach ( $this->errorMessagesFromException( $e, 'error' ) as $msg ) {
            if ( ApiErrorFormatter::isValidApiCode( $msg->getApiCode() ) ) {
                $errorCodes[$msg->getApiCode()] = true;
            } else {
                LoggerFactory::getInstance( 'api-warning' )->error( 'Invalid API error code "{code}"', [
                    'code' => $msg->getApiCode(),
                    'exception' => $e,
                ] );
                $errorCodes['<invalid-code>'] = true;
            }
            $formatter->addError( $modulePath, $msg );
        }
        foreach ( $this->errorMessagesFromException( $e, 'warning' ) as $msg ) {
            $formatter->addWarning( $modulePath, $msg );
        }
        // Add additional data. Path depends on whether we're in BC mode or not.
        // Data depends on the type of exception.
        if ( $formatter instanceof ApiErrorFormatter_BackCompat ) {
            $path = [ 'error' ];
        } else {
            $path = null;
        }
        if ( $e instanceof ApiUsageException ) {
            $link