5 * Created on Sep 4, 2006
7 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
19 * You should have received a copy of the GNU General Public License along
20 * with this program; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 * http://www.gnu.org/copyleft/gpl.html
29 * This is the main API class, used for both external and internal processing.
30 * When executed, it will create the requested formatter object,
31 * instantiate and execute an object associated with the needed action,
32 * and use formatter to print results.
33 * In case of an exception, an error message will be printed using the same formatter.
35 * To use API from another application, run it using FauxRequest object, in which
36 * case any internal exceptions will not be handled but passed up to the caller.
37 * After successful execution, use getResult() for the resulting data.
41 class ApiMain
extends ApiBase
{
43 * When no format parameter is given, this format will be used
45 const API_DEFAULT_FORMAT
= 'jsonfm';
48 * List of available modules: action name => module class
50 private static $Modules = array(
51 'login' => 'ApiLogin',
52 'logout' => 'ApiLogout',
53 'createaccount' => 'ApiCreateAccount',
54 'query' => 'ApiQuery',
55 'expandtemplates' => 'ApiExpandTemplates',
56 'parse' => 'ApiParse',
57 'opensearch' => 'ApiOpenSearch',
58 'feedcontributions' => 'ApiFeedContributions',
59 'feedrecentchanges' => 'ApiFeedRecentChanges',
60 'feedwatchlist' => 'ApiFeedWatchlist',
62 'paraminfo' => 'ApiParamInfo',
64 'compare' => 'ApiComparePages',
65 'tokens' => 'ApiTokens',
68 'purge' => 'ApiPurge',
69 'setnotificationtimestamp' => 'ApiSetNotificationTimestamp',
70 'rollback' => 'ApiRollback',
71 'delete' => 'ApiDelete',
72 'undelete' => 'ApiUndelete',
73 'protect' => 'ApiProtect',
74 'block' => 'ApiBlock',
75 'unblock' => 'ApiUnblock',
77 'edit' => 'ApiEditPage',
78 'upload' => 'ApiUpload',
79 'filerevert' => 'ApiFileRevert',
80 'emailuser' => 'ApiEmailUser',
81 'watch' => 'ApiWatch',
82 'patrol' => 'ApiPatrol',
83 'import' => 'ApiImport',
84 'clearhasmsg' => 'ApiClearHasMsg',
85 'userrights' => 'ApiUserrights',
86 'options' => 'ApiOptions',
87 'imagerotate' => 'ApiImageRotate',
88 'revisiondelete' => 'ApiRevisionDelete',
92 * List of available formats: format name => format class
94 private static $Formats = array(
95 'json' => 'ApiFormatJson',
96 'jsonfm' => 'ApiFormatJson',
97 'php' => 'ApiFormatPhp',
98 'phpfm' => 'ApiFormatPhp',
99 'wddx' => 'ApiFormatWddx',
100 'wddxfm' => 'ApiFormatWddx',
101 'xml' => 'ApiFormatXml',
102 'xmlfm' => 'ApiFormatXml',
103 'yaml' => 'ApiFormatYaml',
104 'yamlfm' => 'ApiFormatYaml',
105 'rawfm' => 'ApiFormatJson',
106 'txt' => 'ApiFormatTxt',
107 'txtfm' => 'ApiFormatTxt',
108 'dbg' => 'ApiFormatDbg',
109 'dbgfm' => 'ApiFormatDbg',
110 'dump' => 'ApiFormatDump',
111 'dumpfm' => 'ApiFormatDump',
112 'none' => 'ApiFormatNone',
115 // @codingStandardsIgnoreStart String contenation on "msg" not allowed to break long line
117 * List of user roles that are specifically relevant to the API.
118 * array( 'right' => array ( 'msg' => 'Some message with a $1',
119 * 'params' => array ( $someVarToSubst ) ),
122 private static $mRights = array(
124 'msg' => 'right-writeapi',
127 'apihighlimits' => array(
128 'msg' => 'api-help-right-apihighlimits',
129 'params' => array( ApiBase
::LIMIT_SML2
, ApiBase
::LIMIT_BIG2
)
132 // @codingStandardsIgnoreEnd
139 private $mModuleMgr, $mResult;
141 private $mEnableWrite;
142 private $mInternalMode, $mSquidMaxage, $mModule;
144 private $mCacheMode = 'private';
145 private $mCacheControl = array();
146 private $mParamsUsed = array();
149 * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
151 * @param IContextSource|WebRequest $context If this is an instance of
152 * FauxRequest, errors are thrown and no printing occurs
153 * @param bool $enableWrite Should be set to true if the api may modify data
155 public function __construct( $context = null, $enableWrite = false ) {
156 if ( $context === null ) {
157 $context = RequestContext
::getMain();
158 } elseif ( $context instanceof WebRequest
) {
161 $context = RequestContext
::getMain();
163 // We set a derivative context so we can change stuff later
164 $this->setContext( new DerivativeContext( $context ) );
166 if ( isset( $request ) ) {
167 $this->getContext()->setRequest( $request );
170 $this->mInternalMode
= ( $this->getRequest() instanceof FauxRequest
);
172 // Special handling for the main module: $parent === $this
173 parent
::__construct( $this, $this->mInternalMode ?
'main_int' : 'main' );
175 if ( !$this->mInternalMode
) {
176 // Impose module restrictions.
177 // If the current user cannot read,
178 // Remove all modules other than login
181 if ( $this->getVal( 'callback' ) !== null ) {
182 // JSON callback allows cross-site reads.
183 // For safety, strip user credentials.
184 wfDebug( "API: stripping user credentials for JSON callback\n" );
185 $wgUser = new User();
186 $this->getContext()->setUser( $wgUser );
190 $uselang = $this->getParameter( 'uselang' );
191 if ( $uselang === 'user' ) {
192 $uselang = $this->getUser()->getOption( 'language' );
193 $uselang = RequestContext
::sanitizeLangCode( $uselang );
194 wfRunHooks( 'UserGetLanguageObject', array( $this->getUser(), &$uselang, $this ) );
196 $code = RequestContext
::sanitizeLangCode( $uselang );
197 $this->getContext()->setLanguage( $code );
198 if ( !$this->mInternalMode
) {
200 $wgLang = RequestContext
::getMain()->getLanguage();
203 $config = $this->getConfig();
204 $this->mModuleMgr
= new ApiModuleManager( $this );
205 $this->mModuleMgr
->addModules( self
::$Modules, 'action' );
206 $this->mModuleMgr
->addModules( $config->get( 'APIModules' ), 'action' );
207 $this->mModuleMgr
->addModules( self
::$Formats, 'format' );
208 $this->mModuleMgr
->addModules( $config->get( 'APIFormatModules' ), 'format' );
210 $this->mResult
= new ApiResult( $this );
211 $this->mEnableWrite
= $enableWrite;
213 $this->mSquidMaxage
= -1; // flag for executeActionWithErrorHandling()
214 $this->mCommit
= false;
218 * Return true if the API was started by other PHP code using FauxRequest
221 public function isInternalMode() {
222 return $this->mInternalMode
;
226 * Get the ApiResult object associated with current request
230 public function getResult() {
231 return $this->mResult
;
235 * Get the API module object. Only works after executeAction()
239 public function getModule() {
240 return $this->mModule
;
244 * Get the result formatter object. Only works after setupExecuteAction()
246 * @return ApiFormatBase
248 public function getPrinter() {
249 return $this->mPrinter
;
253 * Set how long the response should be cached.
257 public function setCacheMaxAge( $maxage ) {
258 $this->setCacheControl( array(
259 'max-age' => $maxage,
260 's-maxage' => $maxage
265 * Set the type of caching headers which will be sent.
267 * @param string $mode One of:
268 * - 'public': Cache this object in public caches, if the maxage or smaxage
269 * parameter is set, or if setCacheMaxAge() was called. If a maximum age is
270 * not provided by any of these means, the object will be private.
271 * - 'private': Cache this object only in private client-side caches.
272 * - 'anon-public-user-private': Make this object cacheable for logged-out
273 * users, but private for logged-in users. IMPORTANT: If this is set, it must be
274 * set consistently for a given URL, it cannot be set differently depending on
275 * things like the contents of the database, or whether the user is logged in.
277 * If the wiki does not allow anonymous users to read it, the mode set here
278 * will be ignored, and private caching headers will always be sent. In other words,
279 * the "public" mode is equivalent to saying that the data sent is as public as a page
282 * For user-dependent data, the private mode should generally be used. The
283 * anon-public-user-private mode should only be used where there is a particularly
284 * good performance reason for caching the anonymous response, but where the
285 * response to logged-in users may differ, or may contain private data.
287 * If this function is never called, then the default will be the private mode.
289 public function setCacheMode( $mode ) {
290 if ( !in_array( $mode, array( 'private', 'public', 'anon-public-user-private' ) ) ) {
291 wfDebug( __METHOD__
. ": unrecognised cache mode \"$mode\"\n" );
293 // Ignore for forwards-compatibility
297 if ( !User
::isEveryoneAllowed( 'read' ) ) {
298 // Private wiki, only private headers
299 if ( $mode !== 'private' ) {
300 wfDebug( __METHOD__
. ": ignoring request for $mode cache mode, private wiki\n" );
306 if ( $mode === 'public' && $this->getParameter( 'uselang' ) === 'user' ) {
307 // User language is used for i18n, so we don't want to publicly
308 // cache. Anons are ok, because if they have non-default language
309 // then there's an appropriate Vary header set by whatever set
310 // their non-default language.
311 wfDebug( __METHOD__
. ": downgrading cache mode 'public' to " .
312 "'anon-public-user-private' due to uselang=user\n" );
313 $mode = 'anon-public-user-private';
316 wfDebug( __METHOD__
. ": setting cache mode $mode\n" );
317 $this->mCacheMode
= $mode;
321 * Set directives (key/value pairs) for the Cache-Control header.
322 * Boolean values will be formatted as such, by including or omitting
323 * without an equals sign.
325 * Cache control values set here will only be used if the cache mode is not
326 * private, see setCacheMode().
328 * @param array $directives
330 public function setCacheControl( $directives ) {
331 $this->mCacheControl
= $directives +
$this->mCacheControl
;
335 * Create an instance of an output formatter by its name
337 * @param string $format
339 * @return ApiFormatBase
341 public function createPrinterByName( $format ) {
342 $printer = $this->mModuleMgr
->getModule( $format, 'format' );
343 if ( $printer === null ) {
344 $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
351 * Execute api request. Any errors will be handled if the API was called by the remote client.
353 public function execute() {
355 if ( $this->mInternalMode
) {
356 $this->executeAction();
358 $this->executeActionWithErrorHandling();
365 * Execute an action, and in case of an error, erase whatever partial results
366 * have been accumulated, and replace it with an error message and a help screen.
368 protected function executeActionWithErrorHandling() {
369 // Verify the CORS header before executing the action
370 if ( !$this->handleCORS() ) {
371 // handleCORS() has sent a 403, abort
375 // Exit here if the request method was OPTIONS
376 // (assume there will be a followup GET or POST)
377 if ( $this->getRequest()->getMethod() === 'OPTIONS' ) {
381 // In case an error occurs during data output,
382 // clear the output buffer and print just the error information
385 $t = microtime( true );
387 $this->executeAction();
388 } catch ( Exception
$e ) {
389 $this->handleException( $e );
392 // Log the request whether or not there was an error
393 $this->logRequest( microtime( true ) - $t );
395 // Send cache headers after any code which might generate an error, to
396 // avoid sending public cache headers for errors.
397 $this->sendCacheHeaders();
403 * Handle an exception as an API response
406 * @param Exception $e
408 protected function handleException( Exception
$e ) {
409 // Bug 63145: Rollback any open database transactions
410 if ( !( $e instanceof UsageException
) ) {
411 // UsageExceptions are intentional, so don't rollback if that's the case
412 MWExceptionHandler
::rollbackMasterChangesAndLog( $e );
415 // Allow extra cleanup and logging
416 wfRunHooks( 'ApiMain::onException', array( $this, $e ) );
419 if ( !( $e instanceof UsageException
) ) {
420 MWExceptionHandler
::logException( $e );
423 // Handle any kind of exception by outputting properly formatted error message.
424 // If this fails, an unhandled exception should be thrown so that global error
425 // handler will process and log it.
427 $errCode = $this->substituteResultWithError( $e );
429 // Error results should not be cached
430 $this->setCacheMode( 'private' );
432 $response = $this->getRequest()->response();
433 $headerStr = 'MediaWiki-API-Error: ' . $errCode;
434 if ( $e->getCode() === 0 ) {
435 $response->header( $headerStr );
437 $response->header( $headerStr, true, $e->getCode() );
440 // Reset and print just the error message
443 // If the error occurred during printing, do a printer->profileOut()
444 $this->mPrinter
->safeProfileOut();
445 $this->printResult( true );
449 * Handle an exception from the ApiBeforeMain hook.
451 * This tries to print the exception as an API response, to be more
452 * friendly to clients. If it fails, it will rethrow the exception.
455 * @param Exception $e
457 public static function handleApiBeforeMainException( Exception
$e ) {
461 $main = new self( RequestContext
::getMain(), false );
462 $main->handleException( $e );
463 } catch ( Exception
$e2 ) {
464 // Nope, even that didn't work. Punt.
468 // Log the request and reset cache headers
469 $main->logRequest( 0 );
470 $main->sendCacheHeaders();
476 * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately.
478 * If no origin parameter is present, nothing happens.
479 * If an origin parameter is present but doesn't match the Origin header, a 403 status code
480 * is set and false is returned.
481 * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains
482 * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS
485 * @return bool False if the caller should abort (403 case), true otherwise (all other cases)
487 protected function handleCORS() {
488 $originParam = $this->getParameter( 'origin' ); // defaults to null
489 if ( $originParam === null ) {
490 // No origin parameter, nothing to do
494 $request = $this->getRequest();
495 $response = $request->response();
496 // Origin: header is a space-separated list of origins, check all of them
497 $originHeader = $request->getHeader( 'Origin' );
498 if ( $originHeader === false ) {
501 $origins = explode( ' ', $originHeader );
504 if ( !in_array( $originParam, $origins ) ) {
505 // origin parameter set but incorrect
506 // Send a 403 response
507 $message = HttpStatus
::getMessage( 403 );
508 $response->header( "HTTP/1.1 403 $message", true, 403 );
509 $response->header( 'Cache-Control: no-cache' );
510 echo "'origin' parameter does not match Origin header\n";
515 $config = $this->getConfig();
516 $matchOrigin = self
::matchOrigin(
518 $config->get( 'CrossSiteAJAXdomains' ),
519 $config->get( 'CrossSiteAJAXdomainExceptions' )
522 if ( $matchOrigin ) {
523 $response->header( "Access-Control-Allow-Origin: $originParam" );
524 $response->header( 'Access-Control-Allow-Credentials: true' );
525 $this->getOutput()->addVaryHeader( 'Origin' );
532 * Attempt to match an Origin header against a set of rules and a set of exceptions
533 * @param string $value Origin header
534 * @param array $rules Set of wildcard rules
535 * @param array $exceptions Set of wildcard rules
536 * @return bool True if $value matches a rule in $rules and doesn't match
537 * any rules in $exceptions, false otherwise
539 protected static function matchOrigin( $value, $rules, $exceptions ) {
540 foreach ( $rules as $rule ) {
541 if ( preg_match( self
::wildcardToRegex( $rule ), $value ) ) {
542 // Rule matches, check exceptions
543 foreach ( $exceptions as $exc ) {
544 if ( preg_match( self
::wildcardToRegex( $exc ), $value ) ) {
557 * Helper function to convert wildcard string into a regex
561 * @param string $wildcard String with wildcards
562 * @return string Regular expression
564 protected static function wildcardToRegex( $wildcard ) {
565 $wildcard = preg_quote( $wildcard, '/' );
566 $wildcard = str_replace(
572 return "/https?:\/\/$wildcard/";
575 protected function sendCacheHeaders() {
576 $response = $this->getRequest()->response();
577 $out = $this->getOutput();
579 $config = $this->getConfig();
581 if ( $config->get( 'VaryOnXFP' ) ) {
582 $out->addVaryHeader( 'X-Forwarded-Proto' );
585 if ( $this->mCacheMode
== 'private' ) {
586 $response->header( 'Cache-Control: private' );
590 $useXVO = $config->get( 'UseXVO' );
591 if ( $this->mCacheMode
== 'anon-public-user-private' ) {
592 $out->addVaryHeader( 'Cookie' );
593 $response->header( $out->getVaryHeader() );
595 $response->header( $out->getXVO() );
596 if ( $out->haveCacheVaryCookies() ) {
597 // Logged in, mark this request private
598 $response->header( 'Cache-Control: private' );
601 // Logged out, send normal public headers below
602 } elseif ( session_id() != '' ) {
603 // Logged in or otherwise has session (e.g. anonymous users who have edited)
604 // Mark request private
605 $response->header( 'Cache-Control: private' );
608 } // else no XVO and anonymous, send public headers below
611 // Send public headers
612 $response->header( $out->getVaryHeader() );
614 $response->header( $out->getXVO() );
617 // If nobody called setCacheMaxAge(), use the (s)maxage parameters
618 if ( !isset( $this->mCacheControl
['s-maxage'] ) ) {
619 $this->mCacheControl
['s-maxage'] = $this->getParameter( 'smaxage' );
621 if ( !isset( $this->mCacheControl
['max-age'] ) ) {
622 $this->mCacheControl
['max-age'] = $this->getParameter( 'maxage' );
625 if ( !$this->mCacheControl
['s-maxage'] && !$this->mCacheControl
['max-age'] ) {
626 // Public cache not requested
627 // Sending a Vary header in this case is harmless, and protects us
628 // against conditional calls of setCacheMaxAge().
629 $response->header( 'Cache-Control: private' );
634 $this->mCacheControl
['public'] = true;
636 // Send an Expires header
637 $maxAge = min( $this->mCacheControl
['s-maxage'], $this->mCacheControl
['max-age'] );
638 $expiryUnixTime = ( $maxAge == 0 ?
1 : time() +
$maxAge );
639 $response->header( 'Expires: ' . wfTimestamp( TS_RFC2822
, $expiryUnixTime ) );
641 // Construct the Cache-Control header
644 foreach ( $this->mCacheControl
as $name => $value ) {
645 if ( is_bool( $value ) ) {
647 $ccHeader .= $separator . $name;
651 $ccHeader .= $separator . "$name=$value";
656 $response->header( "Cache-Control: $ccHeader" );
660 * Replace the result data with the information about an exception.
661 * Returns the error code
662 * @param Exception $e
665 protected function substituteResultWithError( $e ) {
666 $result = $this->getResult();
668 // Printer may not be initialized if the extractRequestParams() fails for the main module
669 if ( !isset( $this->mPrinter
) ) {
670 // The printer has not been created yet. Try to manually get formatter value.
671 $value = $this->getRequest()->getVal( 'format', self
::API_DEFAULT_FORMAT
);
672 if ( !$this->mModuleMgr
->isDefined( $value, 'format' ) ) {
673 $value = self
::API_DEFAULT_FORMAT
;
676 $this->mPrinter
= $this->createPrinterByName( $value );
679 // Printer may not be able to handle errors. This is particularly
680 // likely if the module returns something for getCustomPrinter().
681 if ( !$this->mPrinter
->canPrintErrors() ) {
682 $this->mPrinter
->safeProfileOut();
683 $this->mPrinter
= $this->createPrinterByName( self
::API_DEFAULT_FORMAT
);
686 // Update raw mode flag for the selected printer.
687 $result->setRawMode( $this->mPrinter
->getNeedsRawData() );
689 $config = $this->getConfig();
691 if ( $e instanceof UsageException
) {
692 // User entered incorrect parameters - generate error response
693 $errMessage = $e->getMessageArray();
694 $link = wfExpandUrl( wfScript( 'api' ) );
695 ApiResult
::setContent( $errMessage, "See $link for API usage" );
697 // Something is seriously wrong
698 if ( ( $e instanceof DBQueryError
) && !$config->get( 'ShowSQLErrors' ) ) {
699 $info = 'Database query error';
701 $info = "Exception Caught: {$e->getMessage()}";
705 'code' => 'internal_api_error_' . get_class( $e ),
708 ApiResult
::setContent(
710 $config->get( 'ShowExceptionDetails' ) ?
"\n\n{$e->getTraceAsString()}\n\n" : ''
714 // Remember all the warnings to re-add them later
715 $oldResult = $result->getData();
716 $warnings = isset( $oldResult['warnings'] ) ?
$oldResult['warnings'] : null;
720 $requestid = $this->getParameter( 'requestid' );
721 if ( !is_null( $requestid ) ) {
722 $result->addValue( null, 'requestid', $requestid, ApiResult
::NO_SIZE_CHECK
);
724 if ( $config->get( 'ShowHostnames' ) ) {
725 // servedby is especially useful when debugging errors
726 $result->addValue( null, 'servedby', wfHostName(), ApiResult
::NO_SIZE_CHECK
);
728 if ( $warnings !== null ) {
729 $result->addValue( null, 'warnings', $warnings, ApiResult
::NO_SIZE_CHECK
);
732 $result->addValue( null, 'error', $errMessage, ApiResult
::NO_SIZE_CHECK
);
734 return $errMessage['code'];
738 * Set up for the execution.
741 protected function setupExecuteAction() {
742 // First add the id to the top element
743 $result = $this->getResult();
744 $requestid = $this->getParameter( 'requestid' );
745 if ( !is_null( $requestid ) ) {
746 $result->addValue( null, 'requestid', $requestid );
749 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
750 $servedby = $this->getParameter( 'servedby' );
752 $result->addValue( null, 'servedby', wfHostName() );
756 if ( $this->getParameter( 'curtimestamp' ) ) {
757 $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601
, time() ),
758 ApiResult
::NO_SIZE_CHECK
);
761 $params = $this->extractRequestParams();
763 $this->mAction
= $params['action'];
765 if ( !is_string( $this->mAction
) ) {
766 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
773 * Set up the module for response
774 * @return ApiBase The module that will handle this action
776 protected function setupModule() {
777 // Instantiate the module requested by the user
778 $module = $this->mModuleMgr
->getModule( $this->mAction
, 'action' );
779 if ( $module === null ) {
780 $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
782 $moduleParams = $module->extractRequestParams();
784 // Check token, if necessary
785 if ( $module->needsToken() === true ) {
786 throw new MWException(
787 "Module '{$module->getModuleName()}' must be updated for the new token handling. " .
788 "See documentation for ApiBase::needsToken for details."
791 if ( $module->needsToken() ) {
792 if ( !$module->mustBePosted() ) {
793 throw new MWException(
794 "Module '{$module->getModuleName()}' must require POST to use tokens."
798 if ( !isset( $moduleParams['token'] ) ) {
799 $this->dieUsageMsg( array( 'missingparam', 'token' ) );
802 if ( !$this->getConfig()->get( 'DebugAPI' ) &&
804 $module->encodeParamName( 'token' ),
805 $this->getRequest()->getQueryValues()
809 "The '{$module->encodeParamName( 'token' )}' parameter was found in the query string, but must be in the POST body",
814 if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
815 $this->dieUsageMsg( 'sessionfailure' );
823 * Check the max lag if necessary
824 * @param ApiBase $module Api module being used
825 * @param array $params Array an array containing the request parameters.
826 * @return bool True on success, false should exit immediately
828 protected function checkMaxLag( $module, $params ) {
829 if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) {
831 $maxLag = $params['maxlag'];
832 list( $host, $lag ) = wfGetLB()->getMaxLag();
833 if ( $lag > $maxLag ) {
834 $response = $this->getRequest()->response();
836 $response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) );
837 $response->header( 'X-Database-Lag: ' . intval( $lag ) );
839 if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
840 $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
843 $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
851 * Check for sufficient permissions to execute
852 * @param ApiBase $module An Api module
854 protected function checkExecutePermissions( $module ) {
855 $user = $this->getUser();
856 if ( $module->isReadMode() && !User
::isEveryoneAllowed( 'read' ) &&
857 !$user->isAllowed( 'read' )
859 $this->dieUsageMsg( 'readrequired' );
861 if ( $module->isWriteMode() ) {
862 if ( !$this->mEnableWrite
) {
863 $this->dieUsageMsg( 'writedisabled' );
865 if ( !$user->isAllowed( 'writeapi' ) ) {
866 $this->dieUsageMsg( 'writerequired' );
868 if ( wfReadOnly() ) {
869 $this->dieReadOnly();
873 // Allow extensions to stop execution for arbitrary reasons.
875 if ( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) {
876 $this->dieUsageMsg( $message );
881 * Check asserts of the user's rights
882 * @param array $params
884 protected function checkAsserts( $params ) {
885 if ( isset( $params['assert'] ) ) {
886 $user = $this->getUser();
887 switch ( $params['assert'] ) {
889 if ( $user->isAnon() ) {
890 $this->dieUsage( 'Assertion that the user is logged in failed', 'assertuserfailed' );
894 if ( !$user->isAllowed( 'bot' ) ) {
895 $this->dieUsage( 'Assertion that the user has the bot right failed', 'assertbotfailed' );
903 * Check POST for external response and setup result printer
904 * @param ApiBase $module An Api module
905 * @param array $params An array with the request parameters
907 protected function setupExternalResponse( $module, $params ) {
908 if ( !$this->getRequest()->wasPosted() && $module->mustBePosted() ) {
909 // Module requires POST. GET request might still be allowed
910 // if $wgDebugApi is true, otherwise fail.
911 $this->dieUsageMsgOrDebug( array( 'mustbeposted', $this->mAction
) );
914 // See if custom printer is used
915 $this->mPrinter
= $module->getCustomPrinter();
916 if ( is_null( $this->mPrinter
) ) {
917 // Create an appropriate printer
918 $this->mPrinter
= $this->createPrinterByName( $params['format'] );
921 if ( $this->mPrinter
->getNeedsRawData() ) {
922 $this->getResult()->setRawMode();
927 * Execute the actual module, without any error handling
929 protected function executeAction() {
930 $params = $this->setupExecuteAction();
931 $module = $this->setupModule();
932 $this->mModule
= $module;
934 $this->checkExecutePermissions( $module );
936 if ( !$this->checkMaxLag( $module, $params ) ) {
940 if ( !$this->mInternalMode
) {
941 $this->setupExternalResponse( $module, $params );
944 $this->checkAsserts( $params );
947 $module->profileIn();
949 wfRunHooks( 'APIAfterExecute', array( &$module ) );
950 $module->profileOut();
952 $this->reportUnusedParams();
954 if ( !$this->mInternalMode
) {
955 //append Debug information
956 MWDebug
::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
959 $this->printResult( false );
964 * Log the preceding request
965 * @param int $time Time in seconds
967 protected function logRequest( $time ) {
968 $request = $this->getRequest();
969 $milliseconds = $time === null ?
'?' : round( $time * 1000 );
971 ' ' . $request->getMethod() .
972 ' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
973 ' ' . $request->getIP() .
974 ' T=' . $milliseconds . 'ms';
975 foreach ( $this->getParamsUsed() as $name ) {
976 $value = $request->getVal( $name );
977 if ( $value === null ) {
980 $s .= ' ' . $name . '=';
981 if ( strlen( $value ) > 256 ) {
982 $encValue = $this->encodeRequestLogValue( substr( $value, 0, 256 ) );
983 $s .= $encValue . '[...]';
985 $s .= $this->encodeRequestLogValue( $value );
989 wfDebugLog( 'api', $s, 'private' );
993 * Encode a value in a format suitable for a space-separated log line.
997 protected function encodeRequestLogValue( $s ) {
1000 $chars = ';@$!*(),/:';
1001 $numChars = strlen( $chars );
1002 for ( $i = 0; $i < $numChars; $i++
) {
1003 $table[rawurlencode( $chars[$i] )] = $chars[$i];
1007 return strtr( rawurlencode( $s ), $table );
1011 * Get the request parameters used in the course of the preceding execute() request
1014 protected function getParamsUsed() {
1015 return array_keys( $this->mParamsUsed
);
1019 * Get a request value, and register the fact that it was used, for logging.
1020 * @param string $name
1021 * @param mixed $default
1024 public function getVal( $name, $default = null ) {
1025 $this->mParamsUsed
[$name] = true;
1027 $ret = $this->getRequest()->getVal( $name );
1028 if ( $ret === null ) {
1029 if ( $this->getRequest()->getArray( $name ) !== null ) {
1030 // See bug 10262 for why we don't just join( '|', ... ) the
1033 "Parameter '$name' uses unsupported PHP array syntax"
1042 * Get a boolean request value, and register the fact that the parameter
1043 * was used, for logging.
1044 * @param string $name
1047 public function getCheck( $name ) {
1048 return $this->getVal( $name, null ) !== null;
1052 * Get a request upload, and register the fact that it was used, for logging.
1055 * @param string $name Parameter name
1056 * @return WebRequestUpload
1058 public function getUpload( $name ) {
1059 $this->mParamsUsed
[$name] = true;
1061 return $this->getRequest()->getUpload( $name );
1065 * Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,
1066 * for example in case of spelling mistakes or a missing 'g' prefix for generators.
1068 protected function reportUnusedParams() {
1069 $paramsUsed = $this->getParamsUsed();
1070 $allParams = $this->getRequest()->getValueNames();
1072 if ( !$this->mInternalMode
) {
1073 // Printer has not yet executed; don't warn that its parameters are unused
1074 $printerParams = array_map(
1075 array( $this->mPrinter
, 'encodeParamName' ),
1076 array_keys( $this->mPrinter
->getFinalParams() ?
: array() )
1078 $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
1080 $unusedParams = array_diff( $allParams, $paramsUsed );
1083 if ( count( $unusedParams ) ) {
1084 $s = count( $unusedParams ) > 1 ?
's' : '';
1085 $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
1090 * Print results using the current printer
1092 * @param bool $isError
1094 protected function printResult( $isError ) {
1095 if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
1096 $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
1099 $this->getResult()->cleanUpUTF8();
1100 $printer = $this->mPrinter
;
1101 $printer->profileIn();
1103 $printer->initPrinter( false );
1105 $printer->execute();
1106 $printer->closePrinter();
1107 $printer->profileOut();
1113 public function isReadMode() {
1118 * See ApiBase for description.
1122 public function getAllowedParams() {
1127 ApiBase
::PARAM_DFLT
=> 'help',
1128 ApiBase
::PARAM_TYPE
=> 'submodule',
1131 ApiBase
::PARAM_DFLT
=> ApiMain
::API_DEFAULT_FORMAT
,
1132 ApiBase
::PARAM_TYPE
=> 'submodule',
1135 ApiBase
::PARAM_TYPE
=> 'integer'
1138 ApiBase
::PARAM_TYPE
=> 'integer',
1139 ApiBase
::PARAM_DFLT
=> 0
1142 ApiBase
::PARAM_TYPE
=> 'integer',
1143 ApiBase
::PARAM_DFLT
=> 0
1146 ApiBase
::PARAM_TYPE
=> array( 'user', 'bot' )
1148 'requestid' => null,
1149 'servedby' => false,
1150 'curtimestamp' => false,
1153 ApiBase
::PARAM_DFLT
=> $wgContLang->getCode(),
1158 /** @see ApiBase::getExamplesMessages() */
1159 public function getExamplesMessages() {
1161 'action=help' => 'apihelp-help-example-main',
1162 'action=help&recursivesubmodules=1' => 'apihelp-help-example-recursive',
1166 public function modifyHelp( array &$help, array $options ) {
1167 // Wish PHP had an "array_insert_before". Instead, we have to manually
1168 // reindex the array to get 'permissions' in the right place.
1171 foreach ( $oldHelp as $k => $v ) {
1172 if ( $k === 'submodules' ) {
1173 $help['permissions'] = '';
1177 $help['credits'] = '';
1179 // Fill 'permissions'
1180 $help['permissions'] .= Html
::openElement( 'div',
1181 array( 'class' => 'apihelp-block apihelp-permissions' ) );
1182 $m = $this->msg( 'api-help-permissions' );
1183 if ( !$m->isDisabled() ) {
1184 $help['permissions'] .= Html
::rawElement( 'div', array( 'class' => 'apihelp-block-head' ),
1185 $m->numParams( count( self
::$mRights ) )->parse()
1188 $help['permissions'] .= Html
::openElement( 'dl' );
1189 foreach ( self
::$mRights as $right => $rightMsg ) {
1190 $help['permissions'] .= Html
::element( 'dt', null, $right );
1192 $rightMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )->parse();
1193 $help['permissions'] .= Html
::rawElement( 'dd', null, $rightMsg );
1195 $groups = array_map( function ( $group ) {
1196 return $group == '*' ?
'all' : $group;
1197 }, User
::getGroupsWithPermission( $right ) );
1199 $help['permissions'] .= Html
::rawElement( 'dd', null,
1200 $this->msg( 'api-help-permissions-granted-to' )
1201 ->numParams( count( $groups ) )
1202 ->params( $this->getLanguage()->commaList( $groups ) )
1206 $help['permissions'] .= Html
::closeElement( 'dl' );
1207 $help['permissions'] .= Html
::closeElement( 'div' );
1209 // Fill 'credits', if applicable
1210 if ( empty( $options['nolead'] ) ) {
1211 $help['credits'] .= Html
::element( 'h' . min( 6, $options['headerlevel'] +
1 ),
1212 array( 'id' => '+credits', 'class' => 'apihelp-header' ),
1213 $this->msg( 'api-credits-header' )->parse()
1215 $help['credits'] .= $this->msg( 'api-credits' )->useDatabase( false )->parseAsBlock();
1219 private $mCanApiHighLimits = null;
1222 * Check whether the current user is allowed to use high limits
1225 public function canApiHighLimits() {
1226 if ( !isset( $this->mCanApiHighLimits
) ) {
1227 $this->mCanApiHighLimits
= $this->getUser()->isAllowed( 'apihighlimits' );
1230 return $this->mCanApiHighLimits
;
1234 * Overrides to return this instance's module manager.
1235 * @return ApiModuleManager
1237 public function getModuleManager() {
1238 return $this->mModuleMgr
;
1241 /************************************************************************//**
1247 * @deprecated since 1.25
1250 public function getParamDescription() {
1252 'format' => 'The format of the output',
1253 'action' => 'What action you would like to perform. See below for module help',
1255 'Maximum lag can be used when MediaWiki is installed on a database replicated cluster.',
1256 'To save actions causing any more site replication lag, this parameter can make the client',
1257 'wait until the replication lag is less than the specified value.',
1258 'In case of a replag error, error code "maxlag" is returned, with the message like',
1259 '"Waiting for $host: $lag seconds lagged\n".',
1260 'See https://www.mediawiki.org/wiki/Manual:Maxlag_parameter for more information',
1262 'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached',
1263 'maxage' => 'Set the max-age header to this many seconds. Errors are never cached',
1264 'assert' => 'Verify the user is logged in if set to "user", or has the bot userright if "bot"',
1265 'requestid' => 'Request ID to distinguish requests. This will just be output back to you',
1266 'servedby' => 'Include the hostname that served the request in the ' .
1267 'results. Unconditionally shown on error',
1268 'curtimestamp' => 'Include the current timestamp in the result.',
1270 'When accessing the API using a cross-domain AJAX request (CORS), set this to the',
1271 'originating domain. This must be included in any pre-flight request, and',
1272 'therefore must be part of the request URI (not the POST body). This must match',
1273 'one of the origins in the Origin: header exactly, so it has to be set to ',
1274 'something like http://en.wikipedia.org or https://meta.wikimedia.org . If this',
1275 'parameter does not match the Origin: header, a 403 response will be returned. If',
1276 'this parameter matches the Origin: header and the origin is whitelisted, an',
1277 'Access-Control-Allow-Origin header will be set.',
1283 * @deprecated since 1.25
1286 public function getDescription() {
1290 '**********************************************************************************************',
1292 '** This is an auto-generated MediaWiki API documentation page **',
1294 '** Documentation and Examples: **',
1295 '** https://www.mediawiki.org/wiki/API **',
1297 '**********************************************************************************************',
1299 'Status: All features shown on this page should be working, but the API',
1300 ' is still in active development, and may change at any time.',
1301 ' Make sure to monitor our mailing list for any updates.',
1303 'Erroneous requests: When erroneous requests are sent to the API, a HTTP header will be sent',
1304 ' with the key "MediaWiki-API-Error" and then both the value of the',
1305 ' header and the error code sent back will be set to the same value.',
1307 ' In the case of an invalid action being passed, these will have a value',
1308 ' of "unknown_action".',
1310 ' For more information see https://www.mediawiki.org' .
1311 '/wiki/API:Errors_and_warnings',
1313 'Documentation: https://www.mediawiki.org/wiki/API:Main_page',
1314 'FAQ https://www.mediawiki.org/wiki/API:FAQ',
1315 'Mailing list: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api',
1316 'Api Announcements: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce',
1317 'Bugs & Requests: https://bugzilla.wikimedia.org/buglist.cgi?component=API&' .
1318 'bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts',
1328 * Sets whether the pretty-printer should format *bold* and $italics$
1330 * @deprecated since 1.25
1333 public function setHelp( $help = true ) {
1334 wfDeprecated( __METHOD__
, '1.25' );
1335 $this->mPrinter
->setHelp( $help );
1339 * Override the parent to generate help messages for all available modules.
1341 * @deprecated since 1.25
1344 public function makeHelpMsg() {
1345 wfDeprecated( __METHOD__
, '1.25' );
1348 // Get help text from cache if present
1349 $key = wfMemcKey( 'apihelp', $this->getModuleName(),
1350 str_replace( ' ', '_', SpecialVersion
::getVersion( 'nodb' ) ) );
1352 $cacheHelpTimeout = $this->getConfig()->get( 'APICacheHelpTimeout' );
1353 if ( $cacheHelpTimeout > 0 ) {
1354 $cached = $wgMemc->get( $key );
1359 $retval = $this->reallyMakeHelpMsg();
1360 if ( $cacheHelpTimeout > 0 ) {
1361 $wgMemc->set( $key, $retval, $cacheHelpTimeout );
1368 * @deprecated since 1.25
1369 * @return mixed|string
1371 public function reallyMakeHelpMsg() {
1372 wfDeprecated( __METHOD__
, '1.25' );
1375 // Use parent to make default message for the main module
1376 $msg = parent
::makeHelpMsg();
1378 $astriks = str_repeat( '*** ', 14 );
1379 $msg .= "\n\n$astriks Modules $astriks\n\n";
1381 foreach ( $this->mModuleMgr
->getNames( 'action' ) as $name ) {
1382 $module = $this->mModuleMgr
->getModule( $name );
1383 $msg .= self
::makeHelpMsgHeader( $module, 'action' );
1385 $msg2 = $module->makeHelpMsg();
1386 if ( $msg2 !== false ) {
1392 $msg .= "\n$astriks Permissions $astriks\n\n";
1393 foreach ( self
::$mRights as $right => $rightMsg ) {
1394 $rightsMsg = $this->msg( $rightMsg['msg'], $rightMsg['params'] )
1395 ->useDatabase( false )
1396 ->inLanguage( 'en' )
1398 $groups = User
::getGroupsWithPermission( $right );
1399 $msg .= "* " . $right . " *\n $rightsMsg" .
1400 "\nGranted to:\n " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n";
1403 $msg .= "\n$astriks Formats $astriks\n\n";
1404 foreach ( $this->mModuleMgr
->getNames( 'format' ) as $name ) {
1405 $module = $this->mModuleMgr
->getModule( $name );
1406 $msg .= self
::makeHelpMsgHeader( $module, 'format' );
1407 $msg2 = $module->makeHelpMsg();
1408 if ( $msg2 !== false ) {
1414 $credits = $this->msg( 'api-credits' )->useDatabase( 'false' )->inLanguage( 'en' )->text();
1415 $credits = str_replace( "\n", "\n ", $credits );
1416 $msg .= "\n*** Credits: ***\n $credits\n";
1422 * @deprecated since 1.25
1423 * @param ApiBase $module
1424 * @param string $paramName What type of request is this? e.g. action,
1425 * query, list, prop, meta, format
1428 public static function makeHelpMsgHeader( $module, $paramName ) {
1429 wfDeprecated( __METHOD__
, '1.25' );
1430 $modulePrefix = $module->getModulePrefix();
1431 if ( strval( $modulePrefix ) !== '' ) {
1432 $modulePrefix = "($modulePrefix) ";
1435 return "* $paramName={$module->getModuleName()} $modulePrefix*";
1439 * Check whether the user wants us to show version information in the API help
1441 * @deprecated since 1.21, always returns false
1443 public function getShowVersions() {
1444 wfDeprecated( __METHOD__
, '1.21' );
1450 * Add or overwrite a module in this ApiMain instance. Intended for use by extending
1451 * classes who wish to add their own modules to their lexicon or override the
1452 * behavior of inherent ones.
1454 * @deprecated since 1.21, Use getModuleManager()->addModule() instead.
1455 * @param string $name The identifier for this module.
1456 * @param ApiBase $class The class where this module is implemented.
1458 protected function addModule( $name, $class ) {
1459 $this->getModuleManager()->addModule( $name, 'action', $class );
1463 * Add or overwrite an output format for this ApiMain. Intended for use by extending
1464 * classes who wish to add to or modify current formatters.
1466 * @deprecated since 1.21, Use getModuleManager()->addModule() instead.
1467 * @param string $name The identifier for this format.
1468 * @param ApiFormatBase $class The class implementing this format.
1470 protected function addFormat( $name, $class ) {
1471 $this->getModuleManager()->addModule( $name, 'format', $class );
1475 * Get the array mapping module names to class names
1476 * @deprecated since 1.21, Use getModuleManager()'s methods instead.
1479 function getModules() {
1480 return $this->getModuleManager()->getNamesWithClasses( 'action' );
1484 * Returns the list of supported formats in form ( 'format' => 'ClassName' )
1487 * @deprecated since 1.21, Use getModuleManager()'s methods instead.
1490 public function getFormats() {
1491 return $this->getModuleManager()->getNamesWithClasses( 'format' );
1499 * This exception will be thrown when dieUsage is called to stop module execution.
1503 class UsageException
extends MWException
{
1510 private $mExtraData;
1513 * @param string $message
1514 * @param string $codestr
1516 * @param array|null $extradata
1518 public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
1519 parent
::__construct( $message, $code );
1520 $this->mCodestr
= $codestr;
1521 $this->mExtraData
= $extradata;
1527 public function getCodeString() {
1528 return $this->mCodestr
;
1534 public function getMessageArray() {
1536 'code' => $this->mCodestr
,
1537 'info' => $this->getMessage()
1539 if ( is_array( $this->mExtraData
) ) {
1540 $result = array_merge( $result, $this->mExtraData
);
1549 public function __toString() {
1550 return "{$this->getCodeString()}: {$this->getMessage()}";
1555 * For really cool vim folding this needs to be at the end:
1556 * vim: foldmarker=@{,@} foldmethod=marker