*/
const API_DEFAULT_FORMAT = 'jsonfm';
+ /**
+ * When no uselang parameter is given, this language will be used
+ */
+ const API_DEFAULT_USELANG = 'user';
+
/**
* List of available modules: action name => module class
*/
'tokens' => 'ApiTokens',
'checktoken' => 'ApiCheckToken',
'cspreport' => 'ApiCSPReport',
+ 'validatepassword' => 'ApiValidatePassword',
// Write modules
'purge' => 'ApiPurge',
'managetags' => 'ApiManageTags',
'tag' => 'ApiTag',
'mergehistory' => 'ApiMergeHistory',
+ 'setpagelanguage' => 'ApiSetPageLanguage',
];
/**
*/
private $mPrinter;
- private $mModuleMgr, $mResult, $mErrorFormatter;
+ private $mModuleMgr, $mResult, $mErrorFormatter = null;
/** @var ApiContinuationManager|null */
private $mContinuationManager;
private $mAction;
}
}
- $uselang = $this->getParameter( 'uselang' );
+ $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.
+ $uselang = $request->getVal( 'uselang', self::API_DEFAULT_USELANG );
if ( $uselang === 'user' ) {
// Assume the parent context is going to return the user language
// for uselang=user (see T85635).
}
}
+ // Set up the error formatter. This doesn't use $this->getParameter()
+ // because we're not ready to handle errors yet.
+ $errorFormat = $request->getVal( 'errorformat', 'bc' );
+ $errorLangCode = $request->getVal( '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' ) {
+ global $wgContLang;
+ $errorLang = $wgContLang;
+ } else {
+ $errorLangCode = RequestContext::sanitizeLangCode( $errorLangCode );
+ $errorLang = Language::factory( $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 );
$this->mModuleMgr->addModules( self::$Modules, 'action' );
$this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' );
Hooks::run( 'ApiMain::moduleManager', [ $this->mModuleMgr ] );
- $this->mResult = new ApiResult( $this->getConfig()->get( 'APIMaxResultSize' ) );
- $this->mErrorFormatter = new ApiErrorFormatter_BackCompat( $this->mResult );
- $this->mResult->setErrorFormatter( $this->mErrorFormatter );
$this->mContinuationManager = null;
$this->mEnableWrite = $enableWrite;
public function createPrinterByName( $format ) {
$printer = $this->mModuleMgr->getModule( $format, 'format' );
if ( $printer === null ) {
- $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' );
+ $this->dieWithError(
+ [ 'apierror-unknownformat', wfEscapeWikiText( $format ) ], 'unknown_format'
+ );
}
return $printer;
*/
protected function handleException( Exception $e ) {
// Bug 63145: Rollback any open database transactions
- if ( !( $e instanceof UsageException ) ) {
+ if ( !( $e instanceof ApiUsageException || $e instanceof UsageException ) ) {
// UsageExceptions are intentional, so don't rollback if that's the case
try {
MWExceptionHandler::rollbackMasterChangesAndLog( $e );
Hooks::run( 'ApiMain::onException', [ $this, $e ] );
// Log it
- if ( !( $e instanceof UsageException ) ) {
+ if ( !( $e instanceof ApiUsageException || $e instanceof UsageException ) ) {
MWExceptionHandler::logException( $e );
}
// If this fails, an unhandled exception should be thrown so that global error
// handler will process and log it.
- $errCode = $this->substituteResultWithError( $e );
+ $errCodes = $this->substituteResultWithError( $e );
// Error results should not be cached
$this->setCacheMode( 'private' );
$response = $this->getRequest()->response();
- $headerStr = 'MediaWiki-API-Error: ' . $errCode;
- if ( $e->getCode() === 0 ) {
- $response->header( $headerStr );
- } else {
- $response->header( $headerStr, true, $e->getCode() );
- }
+ $headerStr = 'MediaWiki-API-Error: ' . join( ', ', $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();
+ $failed = false;
try {
- $this->printResult( true );
+ $this->printResult( $e->getCode() );
+ } 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 ( Exception $ex2 ) {
+ // WTF?
+ $this->addWarning( $error );
+ }
+ }
} catch ( UsageException $ex ) {
// The error printer itself is failing. Try suppressing its request
// parameters and redo.
- $this->setWarning(
- 'Error printer failed (will retry without params): ' . $ex->getMessage()
+ $failed = true;
+ $this->addWarning(
+ [ 'apiwarn-errorprinterfailed-ex', $ex->getMessage() ], 'errorprinterfailed'
);
+ }
+ if ( $failed ) {
$this->mPrinter = null;
$this->createErrorPrinter();
$this->mPrinter->forceDefaultParams();
- $this->printResult( true );
+ if ( $e->getCode() ) {
+ $response->statusHeader( 200 ); // Reset in case the fallback doesn't want a non-200
+ }
+ $this->printResult( $e->getCode() );
}
}
/**
* Create an error message for the given exception.
*
- * If the exception is a UsageException then
- * UsageException::getMessageArray() will be called to create the message.
+ * If an ApiUsageException, errors/warnings will be extracted from the
+ * embedded StatusValue.
+ *
+ * If a base UsageException, the getMessageArray() method will be used to
+ * extract the code and English message for a single error (no warnings).
+ *
+ * Any other exception will be returned with a generic code and wrapper
+ * text around the exception's (presumably English) message as a single
+ * error (no warnings).
*
* @param Exception $e
- * @return array ['code' => 'some string', 'info' => 'some other string']
+ * @param string $type 'error' or 'warning'
+ * @return ApiMessage[]
* @since 1.27
*/
- protected function errorMessageFromException( $e ) {
- if ( $e instanceof UsageException ) {
+ protected function errorMessagesFromException( $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
+ } elseif ( $e instanceof UsageException ) {
// User entered incorrect parameters - generate error response
- $errMessage = $e->getMessageArray();
+ $data = $e->getMessageArray();
+ $code = $data['code'];
+ $info = $data['info'];
+ unset( $data['code'], $data['info'] );
+ $messages[] = new ApiRawMessage( [ '$1', $info ], $code, $data );
} else {
- $config = $this->getConfig();
// Something is seriously wrong
+ $config = $this->getConfig();
+ $code = 'internal_api_error_' . get_class( $e );
if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) {
- $info = 'Database query error';
+ $params = [ 'apierror-databaseerror', WebRequest::getRequestId() ];
} else {
- $info = "Exception Caught: {$e->getMessage()}";
+ $params = [
+ 'apierror-exceptioncaught',
+ WebRequest::getRequestId(),
+ $e instanceof ILocalizedException
+ ? $e->getMessageObject()
+ : wfEscapeWikiText( $e->getMessage() )
+ ];
}
-
- $errMessage = [
- 'code' => 'internal_api_error_' . get_class( $e ),
- 'info' => '[' . WebRequest::getRequestId() . '] ' . $info,
- ];
+ $messages[] = ApiMessage::create( $params, $code );
}
- return $errMessage;
+ return $messages;
}
/**
* Replace the result data with the information about an exception.
- * Returns the error code
* @param Exception $e
- * @return string
+ * @return string[] Error codes
*/
protected function substituteResultWithError( $e ) {
$result = $this->getResult();
+ $formatter = $this->getErrorFormatter();
$config = $this->getConfig();
+ $errorCodes = [];
- $errMessage = $this->errorMessageFromException( $e );
- if ( $e instanceof UsageException ) {
- // User entered incorrect parameters - generate error response
+ // 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 ) {
+ $errorCodes[$msg->getApiCode()] = 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 || $e instanceof UsageException ) {
$link = wfExpandUrl( wfScript( 'api' ) );
- ApiResult::setContentValue( $errMessage, 'docref', "See $link for API usage" );
+ $result->addContentValue(
+ $path,
+ 'docref',
+ $this->msg( 'api-usage-docref', $link )->inLanguage( $formatter->getLanguage() )->text()
+ );
} else {
- // Something is seriously wrong
if ( $config->get( 'ShowExceptionDetails' ) ) {
- ApiResult::setContentValue(
- $errMessage,
+ $result->addContentValue(
+ $path,
'trace',
- MWExceptionHandler::getRedactedTraceAsString( $e )
+ $this->msg( 'api-exception-trace',
+ get_class( $e ),
+ $e->getFile(),
+ $e->getLine(),
+ MWExceptionHandler::getRedactedTraceAsString( $e )
+ )->inLanguage( $formatter->getLanguage() )->text()
);
}
}
- // Remember all the warnings to re-add them later
- $warnings = $result->getResultData( [ 'warnings' ] );
+ // Add the id and such
+ $this->addRequestedFields( [ 'servedby' ] );
- $result->reset();
- // Re-add the id
- $requestid = $this->getParameter( 'requestid' );
- if ( !is_null( $requestid ) ) {
- $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
- }
- if ( $config->get( 'ShowHostnames' ) ) {
- // servedby is especially useful when debugging errors
- $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
- }
- if ( $warnings !== null ) {
- $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK );
- }
-
- $result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK );
-
- return $errMessage['code'];
+ return array_keys( $errorCodes );
}
/**
- * Set up for the execution.
- * @return array
+ * Add requested fields to the result
+ * @param string[] $force Which fields to force even if not requested. Accepted values are:
+ * - servedby
*/
- protected function setupExecuteAction() {
- // First add the id to the top element
+ protected function addRequestedFields( $force = [] ) {
$result = $this->getResult();
+
$requestid = $this->getParameter( 'requestid' );
- if ( !is_null( $requestid ) ) {
- $result->addValue( null, 'requestid', $requestid );
+ if ( $requestid !== null ) {
+ $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK );
}
- if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
- $servedby = $this->getParameter( 'servedby' );
- if ( $servedby ) {
- $result->addValue( null, 'servedby', wfHostname() );
- }
+ if ( $this->getConfig()->get( 'ShowHostnames' ) && (
+ in_array( 'servedby', $force, true ) || $this->getParameter( 'servedby' )
+ ) ) {
+ $result->addValue( null, 'servedby', wfHostname(), ApiResult::NO_SIZE_CHECK );
}
if ( $this->getParameter( 'curtimestamp' ) ) {
ApiResult::NO_SIZE_CHECK );
}
- $params = $this->extractRequestParams();
+ if ( $this->getParameter( 'responselanginfo' ) ) {
+ $result->addValue( null, 'uselang', $this->getLanguage()->getCode(),
+ ApiResult::NO_SIZE_CHECK );
+ $result->addValue( null, 'errorlang', $this->getErrorFormatter()->getLanguage()->getCode(),
+ ApiResult::NO_SIZE_CHECK );
+ }
+ }
- $this->mAction = $params['action'];
+ /**
+ * Set up for the execution.
+ * @return array
+ */
+ protected function setupExecuteAction() {
+ $this->addRequestedFields();
- if ( !is_string( $this->mAction ) ) {
- $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
- }
+ $params = $this->extractRequestParams();
+ $this->mAction = $params['action'];
return $params;
}
* Set up the module for response
* @return ApiBase The module that will handle this action
* @throws MWException
- * @throws UsageException
+ * @throws ApiUsageException
*/
protected function setupModule() {
// Instantiate the module requested by the user
$module = $this->mModuleMgr->getModule( $this->mAction, 'action' );
if ( $module === null ) {
- $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' );
+ $this->dieWithError(
+ [ 'apierror-unknownaction', wfEscapeWikiText( $this->mAction ) ], 'unknown_action'
+ );
}
$moduleParams = $module->extractRequestParams();
}
if ( !isset( $moduleParams['token'] ) ) {
- $this->dieUsageMsg( [ 'missingparam', 'token' ] );
+ $module->dieWithError( [ 'apierror-missingparam', 'token' ] );
}
$module->requirePostedParameters( [ 'token' ] );
if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) {
- $this->dieUsageMsg( 'sessionfailure' );
+ $module->dieWithError( 'apierror-badtoken' );
}
}
$response->header( 'X-Database-Lag: ' . intval( $lag ) );
if ( $this->getConfig()->get( 'ShowHostnames' ) ) {
- $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' );
+ $this->dieWithError( [ 'apierror-maxlag', $lag, $host ] );
}
- $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' );
+ $this->dieWithError( [ 'apierror-maxlag-generic', $lag ], 'maxlag' );
}
}
if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) &&
!$user->isAllowed( 'read' )
) {
- $this->dieUsageMsg( 'readrequired' );
+ $this->dieWithError( 'apierror-readapidenied' );
}
if ( $module->isWriteMode() ) {
if ( !$this->mEnableWrite ) {
- $this->dieUsageMsg( 'writedisabled' );
+ $this->dieWithError( 'apierror-noapiwrite' );
} elseif ( !$user->isAllowed( 'writeapi' ) ) {
- $this->dieUsageMsg( 'writerequired' );
+ $this->dieWithError( 'apierror-writeapidenied' );
} elseif ( $this->getRequest()->getHeader( 'Promise-Non-Write-API-Action' ) ) {
- $this->dieUsage(
- 'Promise-Non-Write-API-Action HTTP header cannot be sent to write API modules',
- 'promised-nonwrite-api'
- );
+ $this->dieWithError( 'apierror-promised-nonwrite-api' );
}
$this->checkReadOnly( $module );
// Allow extensions to stop execution for arbitrary reasons.
$message = false;
if ( !Hooks::run( 'ApiCheckCanExecute', [ $module, $user, &$message ] ) ) {
- $this->dieUsageMsg( $message );
+ $this->dieWithError( $message );
}
}
"Api request failed as read only because the following DBs are lagged: $laggedServers"
);
- $parsed = $this->parseMsg( [ 'readonlytext' ] );
- $this->dieUsage(
- $parsed['info'],
- $parsed['code'],
- /* http error */
- 0,
+ $this->dieWithError(
+ 'readonly_lag',
+ 'readonly',
[ 'readonlyreason' => "Waiting for $numLagged lagged database(s)" ]
);
}
switch ( $params['assert'] ) {
case 'user':
if ( $user->isAnon() ) {
- $this->dieUsage( 'Assertion that the user is logged in failed', 'assertuserfailed' );
+ $this->dieWithError( 'apierror-assertuserfailed' );
}
break;
case 'bot':
if ( !$user->isAllowed( 'bot' ) ) {
- $this->dieUsage( 'Assertion that the user has the bot right failed', 'assertbotfailed' );
+ $this->dieWithError( 'apierror-assertbotfailed' );
}
break;
}
if ( isset( $params['assertuser'] ) ) {
$assertUser = User::newFromName( $params['assertuser'], false );
if ( !$assertUser || !$this->getUser()->equals( $assertUser ) ) {
- $this->dieUsage(
- 'Assertion that the user is "' . $params['assertuser'] . '" failed',
- 'assertnameduserfailed'
+ $this->dieWithError(
+ [ 'apierror-assertnameduserfailed', wfEscapeWikiText( $params['assertuser'] ) ]
);
}
}
if ( !$request->wasPosted() && $module->mustBePosted() ) {
// Module requires POST. GET request might still be allowed
// if $wgDebugApi is true, otherwise fail.
- $this->dieUsageMsgOrDebug( [ 'mustbeposted', $this->mAction ] );
+ $this->dieWithErrorOrDebug( [ 'apierror-mustbeposted', $this->mAction ] );
}
// See if custom printer is used
( $this->getUser()->isLoggedIn() &&
$this->getUser()->requiresHTTPS() )
) ) {
- $this->logFeatureUsage( 'https-expected' );
- $this->setWarning( 'HTTP used when HTTPS was expected' );
+ $this->addDeprecation( 'apiwarn-deprecation-httpsexpected', 'https-expected' );
}
}
MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
// Print result data
- $this->printResult( false );
+ $this->printResult();
}
}
];
if ( $e ) {
- $logCtx['errorCodes'][] = $this->errorMessageFromException( $e )['code'];
+ foreach ( $this->errorMessagesFromException( $e ) as $msg ) {
+ $logCtx['errorCodes'][] = $msg->getApiCode();
+ }
}
// Construct space separated message for 'api' log channel
if ( $this->getRequest()->getArray( $name ) !== null ) {
// See bug 10262 for why we don't just implode( '|', ... ) the
// array.
- $this->setWarning(
- "Parameter '$name' uses unsupported PHP array syntax"
- );
+ $this->addWarning( [ 'apiwarn-unsupportedarray', $name ] );
}
$ret = $default;
}
if ( !$this->mInternalMode ) {
// Printer has not yet executed; don't warn that its parameters are unused
- $printerParams = array_map(
- [ $this->mPrinter, 'encodeParamName' ],
+ $printerParams = $this->mPrinter->encodeParamName(
array_keys( $this->mPrinter->getFinalParams() ?: [] )
);
$unusedParams = array_diff( $allParams, $paramsUsed, $printerParams );
}
if ( count( $unusedParams ) ) {
- $s = count( $unusedParams ) > 1 ? 's' : '';
- $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
+ $this->addWarning( [
+ 'apierror-unrecognizedparams',
+ Message::listParam( array_map( 'wfEscapeWikiText', $unusedParams ), 'comma' ),
+ count( $unusedParams )
+ ] );
}
}
/**
* Print results using the current printer
*
- * @param bool $isError
+ * @param int $httpCode HTTP status code, or 0 to not change
*/
- protected function printResult( $isError ) {
+ protected function printResult( $httpCode = 0 ) {
if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) {
- $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
+ $this->addWarning( 'apiwarn-wgDebugAPI' );
}
$printer = $this->mPrinter;
$printer->initPrinter( false );
+ if ( $httpCode ) {
+ $printer->setHttpStatus( $httpCode );
+ }
$printer->execute();
$printer->closePrinter();
}
'requestid' => null,
'servedby' => false,
'curtimestamp' => false,
+ 'responselanginfo' => false,
'origin' => null,
'uselang' => [
- ApiBase::PARAM_DFLT => 'user',
+ ApiBase::PARAM_DFLT => self::API_DEFAULT_USELANG,
+ ],
+ 'errorformat' => [
+ ApiBase::PARAM_TYPE => [ 'plaintext', 'wikitext', 'html', 'raw', 'none', 'bc' ],
+ ApiBase::PARAM_DFLT => 'bc',
+ ],
+ 'errorlang' => [
+ ApiBase::PARAM_DFLT => 'uselang',
+ ],
+ 'errorsuselocal' => [
+ ApiBase::PARAM_DFLT => false,
],
];
}
$help['permissions'] .= Html::rawElement( 'dd', null,
$this->msg( 'api-help-permissions-granted-to' )
->numParams( count( $groups ) )
- ->params( $this->getLanguage()->commaList( $groups ) )
+ ->params( Message::listParam( $groups ) )
->parse()
);
}
}
}
-/**
- * This exception will be thrown when dieUsage is called to stop module execution.
- *
- * @ingroup API
- */
-class UsageException extends MWException {
-
- private $mCodestr;
-
- /**
- * @var null|array
- */
- private $mExtraData;
-
- /**
- * @param string $message
- * @param string $codestr
- * @param int $code
- * @param array|null $extradata
- */
- public function __construct( $message, $codestr, $code = 0, $extradata = null ) {
- parent::__construct( $message, $code );
- $this->mCodestr = $codestr;
- $this->mExtraData = $extradata;
-
- // This should never happen, so throw an exception about it that will
- // hopefully get logged with a backtrace (T138585)
- if ( !is_string( $codestr ) || $codestr === '' ) {
- throw new InvalidArgumentException( 'Invalid $codestr, was ' .
- ( $codestr === '' ? 'empty string' : gettype( $codestr ) )
- );
- }
- }
-
- /**
- * @return string
- */
- public function getCodeString() {
- return $this->mCodestr;
- }
-
- /**
- * @return array
- */
- public function getMessageArray() {
- $result = [
- 'code' => $this->mCodestr,
- 'info' => $this->getMessage()
- ];
- if ( is_array( $this->mExtraData ) ) {
- $result = array_merge( $result, $this->mExtraData );
- }
-
- return $result;
- }
-
- /**
- * @return string
- */
- public function __toString() {
- return "{$this->getCodeString()}: {$this->getMessage()}";
- }
-}
-
/**
* For really cool vim folding this needs to be at the end:
* vim: foldmarker=@{,@} foldmethod=marker