From: Yuri Astrakhan Date: Sat, 14 Oct 2006 07:18:08 +0000 (+0000) Subject: * API: Restructured to allow internal usage. Error handling cleanup. X-Git-Tag: 1.31.0-rc.0~55495 X-Git-Url: http://git.cyclocoop.org/%7B%24www_url%7Dadmin/compta/operations/?a=commitdiff_plain;h=b56d23ed4691c9bd1711c334efb94fe7156ccb75;p=lhc%2Fweb%2Fwiklou.git * API: Restructured to allow internal usage. Error handling cleanup. * API: Added opensearch module, added apprefix param for list=allpages --- diff --git a/api.php b/api.php index 9adfeb03df..d3274dc4cf 100644 --- a/api.php +++ b/api.php @@ -25,71 +25,6 @@ // Initialise common code require (dirname(__FILE__) . '/includes/WebStart.php'); -/** - * When no format parameter is given, this format will be used - */ -define('API_DEFAULT_FORMAT', 'xmlfm'); - -/** - * Location of all api-related files (must end with a slash '/') - */ -define('API_DIR', 'includes/api/'); - -/** - * List of classes and containing files. - */ -$wgApiAutoloadClasses = array ( - - 'ApiMain' => API_DIR . 'ApiMain.php', - - // Utility classes - 'ApiBase' => API_DIR . 'ApiBase.php', - 'ApiQueryBase' => API_DIR . 'ApiQueryBase.php', - 'ApiResult' => API_DIR . 'ApiResult.php', - 'ApiPageSet' => API_DIR . 'ApiPageSet.php', - - // Formats - 'ApiFormatBase' => API_DIR . 'ApiFormatBase.php', - 'ApiFormatYaml' => API_DIR . 'ApiFormatYaml.php', - 'ApiFormatXml' => API_DIR . 'ApiFormatXml.php', - 'ApiFormatJson' => API_DIR . 'ApiFormatJson.php', - - // Modules (action=...) - should match the $apiModules list - 'ApiHelp' => API_DIR . 'ApiHelp.php', - 'ApiLogin' => API_DIR . 'ApiLogin.php', - 'ApiQuery' => API_DIR . 'ApiQuery.php', - - // Query items (meta/prop/list=...) - 'ApiQuerySiteinfo' => API_DIR . 'ApiQuerySiteinfo.php', - 'ApiQueryInfo' => API_DIR . 'ApiQueryInfo.php', - 'ApiQueryRevisions' => API_DIR . 'ApiQueryRevisions.php', - 'ApiQueryAllpages' => API_DIR . 'ApiQueryAllpages.php', - 'ApiQueryWatchlist' => API_DIR . 'ApiQueryWatchlist.php' -); - -/** - * List of available modules: action name => module class - * The class must also be listed in the $wgApiAutoloadClasses array. - */ -$wgApiModules = array ( - 'help' => 'ApiHelp', - 'login' => 'ApiLogin', - 'query' => 'ApiQuery' -); - -/** - * List of available formats: format name => format class - * The class must also be listed in the $wgApiAutoloadClasses array. - */ -$wgApiFormats = array ( - 'json' => 'ApiFormatJson', - 'jsonfm' => 'ApiFormatJson', - 'xml' => 'ApiFormatXml', - 'xmlfm' => 'ApiFormatXml', - 'yaml' => 'ApiFormatYaml', - 'yamlfm' => 'ApiFormatYaml' -); - wfProfileIn('api.php'); // Verify that the API has not been disabled @@ -99,9 +34,7 @@ if (!$wgEnableAPI) { die(-1); } -$wgAutoloadClasses = array_merge($wgAutoloadClasses, $wgApiAutoloadClasses); - -$processor = new ApiMain($wgRequestTime, $wgApiModules, $wgApiFormats, $wgEnableWriteAPI); +$processor = new ApiMain($wgRequest, $wgEnableWriteAPI); $processor->execute(); wfProfileOut('api.php'); diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index fb8a394f78..6107282fde 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -230,7 +230,30 @@ function __autoload($className) { 'UsercreateTemplate' => 'includes/templates/Userlogin.php', 'UserloginTemplate' => 'includes/templates/Userlogin.php', 'Language' => 'languages/Language.php', + + // API classes + 'ApiBase' => 'includes/api/ApiBase.php', + 'ApiFormatBase' => 'includes/api/ApiFormatBase.php', + 'Services_JSON' => 'includes/api/ApiFormatJson_json.php', + 'ApiFormatJson' => 'includes/api/ApiFormatJson.php', + 'ApiFormatXml' => 'includes/api/ApiFormatXml.php', + 'Spyc' => 'includes/api/ApiFormatYaml_spyc.php', + 'ApiFormatYaml' => 'includes/api/ApiFormatYaml.php', + 'ApiHelp' => 'includes/api/ApiHelp.php', + 'ApiLogin' => 'includes/api/ApiLogin.php', + 'ApiMain' => 'includes/api/ApiMain.php', + 'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php', + 'ApiPageSet' => 'includes/api/ApiPageSet.php', + 'ApiQuery' => 'includes/api/ApiQuery.php', + 'ApiQueryAllpages' => 'includes/api/ApiQueryAllpages.php', + 'ApiQueryBase' => 'includes/api/ApiQueryBase.php', + 'ApiQueryInfo' => 'includes/api/ApiQueryInfo.php', + 'ApiQueryRevisions' => 'includes/api/ApiQueryRevisions.php', + 'ApiQuerySiteinfo' => 'includes/api/ApiQuerySiteinfo.php', + 'ApiQueryWatchlist' => 'includes/api/ApiQueryWatchlist.php', + 'ApiResult' => 'includes/api/ApiResult.php', ); + if ( isset( $localClasses[$className] ) ) { $filename = $localClasses[$className]; } elseif ( isset( $wgAutoloadClasses[$className] ) ) { diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 8ad5ab5f1f..b4277c06b2 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -219,7 +219,6 @@ abstract class ApiBase { * @param $paramSettings Mixed: default value or an array of settings using PARAM_* constants. */ protected function getParameterFromSettings($paramName, $paramSettings) { - global $wgRequest; // Some classes may decide to change parameter names $paramName = $this->encodeParamName($paramName); @@ -248,10 +247,11 @@ abstract class ApiBase { ApiBase :: dieDebug(__METHOD__, "Boolean param $paramName's default is set to '$default'"); } - $value = $wgRequest->getCheck($paramName); - } else - $value = $wgRequest->getVal($paramName, $default); - + $value = $this->getMain()->getRequest()->getCheck($paramName); + } else { + $value = $this->getMain()->getRequest()->getVal($paramName, $default); + } + if (isset ($value) && ($multi || is_array($type))) $value = $this->parseMultiValue($paramName, $value, $multi, is_array($type) ? $type : null); @@ -349,7 +349,7 @@ abstract class ApiBase { * Call main module's error handler */ public function dieUsage($description, $errorCode, $httpRespCode = 0) { - $this->getMain()->mainDieUsage($description, $this->encodeParamName($errorCode), $httpRespCode); + throw new UsageException($description, $this->encodeParamName($errorCode), $httpRespCode); } /** diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index c886a15acb..f87cd81620 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -54,6 +54,15 @@ abstract class ApiFormatBase extends ApiBase { */ public abstract function getMimeType(); + public function execute() { + ApiBase :: dieDebug(__METHOD__, 'This is not an executable module'); + } + + /** + * Format modules must override this method to implement actual printing + */ + public abstract function executePrinter(); + public function getNeedsRawData() { return false; } diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index b13551e66a..8942f1c083 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -39,8 +39,7 @@ class ApiFormatJson extends ApiFormatBase { return 'application/json'; } - public function execute() { - require ('ApiFormatJson_json.php'); + public function executePrinter() { $json = new Services_JSON(); $this->printText($json->encode($this->getResultData(), true)); } diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php index 11efcf948c..f72c0d4f71 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -43,7 +43,7 @@ class ApiFormatXml extends ApiFormatBase { return true; } - public function execute() { + public function executePrinter() { $xmlindent = null; extract($this->extractRequestParams()); diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php index ca952e20f6..f0318967a7 100644 --- a/includes/api/ApiFormatYaml.php +++ b/includes/api/ApiFormatYaml.php @@ -39,8 +39,7 @@ class ApiFormatYaml extends ApiFormatBase { return 'application/yaml'; } - public function execute() { - require ('ApiFormatYaml_spyc.php'); + public function executePrinter() { $this->printText(Spyc :: YAMLDump($this->getResultData())); } diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index a12a1597ab..05eff56292 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -29,39 +29,71 @@ if (!defined('MEDIAWIKI')) { require_once ('ApiBase.php'); } + +/** + * This is the main API class, used for both external and internal processing. + */ class ApiMain extends ApiBase { + /** + * When no format parameter is given, this format will be used + */ + const API_DEFAULT_FORMAT = 'xmlfm'; + + /** + * List of available modules: action name => module class + */ + private static $Modules = array ( + 'help' => 'ApiHelp', + 'login' => 'ApiLogin', + 'query' => 'ApiQuery', + 'opensearch' => 'ApiOpenSearch' + ); + + /** + * List of available formats: format name => format class + */ + private static $Formats = array ( + 'json' => 'ApiFormatJson', + 'jsonfm' => 'ApiFormatJson', + 'xml' => 'ApiFormatXml', + 'xmlfm' => 'ApiFormatXml', + 'yaml' => 'ApiFormatYaml', + 'yamlfm' => 'ApiFormatYaml' + ); + private $mPrinter, $mModules, $mModuleNames, $mFormats, $mFormatNames; - private $mApiStartTime, $mResult, $mShowVersions, $mEnableWrite; + private $mResult, $mShowVersions, $mEnableWrite, $mRequest, $mInternalMode; /** * Constructor - * $apiStartTime - time of the originating call for profiling purposes - * $modules - an array of actions (keys) and classes that handle them (values) + * @param $request object - if this is an instance of FauxRequest, errors are thrown and no printing occurs + * @param $enableWrite bool should be set to true if the api may modify data */ - public function __construct($apiStartTime, $modules, $formats, $enableWrite) { + public function __construct($request, $enableWrite = false) { // Special handling for the main module: $parent === $this parent :: __construct($this, 'main'); - $this->mModules = $modules; - $this->mModuleNames = array_keys($modules); - $this->mFormats = $formats; - $this->mFormatNames = array_keys($formats); - $this->mApiStartTime = $apiStartTime; + $this->mModules =& self::$Modules; + $this->mModuleNames = array_keys($this->mModules); // todo: optimize + $this->mFormats =& self::$Formats; + $this->mFormatNames = array_keys($this->mFormats); // todo: optimize + $this->mResult = new ApiResult($this); $this->mShowVersions = false; $this->mEnableWrite = $enableWrite; - // Initialize Error handler - set_exception_handler( array($this, 'exceptionHandler') ); + $this->mRequest =& $request; + + $this->mInternalMode = ($request instanceof FauxRequest); } - public function & getResult() { - return $this->mResult; + public function & getRequest() { + return $this->mRequest; } - public function getShowVersions() { - return $this->mShowVersions; + public function & getResult() { + return $this->mResult; } public function requestWriteMode() { @@ -70,81 +102,141 @@ class ApiMain extends ApiBase { 'statement is included in the site\'s LocalSettings.php file', 'readonly'); } - protected function getAllowedParams() { - return array ( - 'format' => array ( - ApiBase :: PARAM_DFLT => API_DEFAULT_FORMAT, - ApiBase :: PARAM_TYPE => $this->mFormatNames - ), - 'action' => array ( - ApiBase :: PARAM_DFLT => 'help', - ApiBase :: PARAM_TYPE => $this->mModuleNames - ), - 'version' => false - ); - } - - protected function getParamDescription() { - return array ( - 'format' => 'The format of the output', - 'action' => 'What action you would like to perform', - 'version' => 'When showing help, include version for each module' - ); - } - public function execute() { $this->profileIn(); + if($this->mInternalMode) + $this->executeAction(); + else + $this->executeActionWithErrorHandling(); + $this->profileOut(); + } + + protected function executeActionWithErrorHandling() { - // Experimental -- in case an error occurs during data output, + // In case an error occurs during data output, // this clear the output buffer and print just the error information ob_start(); try { - $action = $format = $version = null; - extract($this->extractRequestParams()); - $this->mShowVersions = $version; - - // Create an appropriate printer - $this->mPrinter = new $this->mFormats[$format] ($this, $format); - - // Instantiate and execute module requested by the user - $module = new $this->mModules[$action] ($this, $action); - $module->profileIn(); - $module->execute(); - $module->profileOut(); - $this->printResult(false); - - } catch (UsageException $e) { - $this->printError(); + $this->executeAction(); + } catch (Exception $e) { + // + // Handle any kind of exception by outputing properly formatted error message. + // If this fails, an unhandled exception should be thrown so that global error + // handler will process and log it. + // + + // Printer may not be initialized if the extractRequestParams() fails for the main module + if (!isset ($this->mPrinter)) { + $format = self :: API_DEFAULT_FORMAT; + $this->mPrinter = new $this->mFormats[$format] ($this, $format); + } + + if ($e instanceof UsageException) { + // + // User entered incorrect parameters - print usage screen + // + $httpRespCode = $e->getCode(); + $errMessage = array ( + 'code' => $e->getCodeString(), + 'info' => $e->getMessage() + ); + ApiResult :: setContent($errMessage, $this->makeHelpMsg()); + + } else { + // + // Something is seriously wrong + // + $httpRespCode = 0; + $errMessage = array ( + 'code' => 'internal_api_error', + 'info' => "Exception Caught: {$e->getMessage()}" + ); + ApiResult :: setContent($errMessage, "\n\n{$e->getTraceAsString()}\n\n"); + } + + $headerStr = 'MediaWiki-API-Error: ' . $errMessage['code']; + if ($e->getCode() === 0) + header($headerStr, true); + else + header($headerStr, true, $e->getCode()); + + // Reset and print just the error message + ob_clean(); + $this->mResult->Reset(); + $this->mResult->addValue(null, 'error', $errMessage); + $this->printResult(true); } ob_end_flush(); - $this->profileOut(); } + /** + * Execute the actual module, without any error handling + */ + protected function executeAction() { + $action = $format = $version = null; + extract($this->extractRequestParams()); + $this->mShowVersions = $version; + + // Instantiate the module requested by the user + $module = new $this->mModules[$action] ($this, $action); + + if (!$this->mInternalMode) { + if ($module instanceof ApiFormatBase) { + // The requested module will print data in its own format + $this->mPrinter = $module; + } else { + // Create an appropriate printer + $this->mPrinter = new $this->mFormats[$format] ($this, $format); + } + } + + // Execute + $module->profileIn(); + $module->execute(); + $module->profileOut(); + + if (!$this->mInternalMode) { + // Print result data + $this->printResult(false); + } + } + /** * Internal printer */ - private function printResult($isError) { + protected function printResult($isError) { $printer = $this->mPrinter; $printer->profileIn(); $printer->initPrinter($isError); if (!$printer->getNeedsRawData()) $this->getResult()->SanitizeData(); - $printer->execute(); + $printer->executePrinter(); $printer->closePrinter(); $printer->profileOut(); } - - private function printError() { - // Printer may not be initialized if the extractRequestParams() fails for the main module - if (!isset ($this->mPrinter)) - $this->mPrinter = new $this->mFormats[API_DEFAULT_FORMAT] ($this, API_DEFAULT_FORMAT); - - // In case of an error, reset anythnig that was printed before - ob_clean(); - - $this->printResult(true); + + protected function getAllowedParams() { + return array ( + 'format' => array ( + ApiBase :: PARAM_DFLT => ApiMain :: API_DEFAULT_FORMAT, + ApiBase :: PARAM_TYPE => $this->mFormatNames + ), + 'action' => array ( + ApiBase :: PARAM_DFLT => 'help', + ApiBase :: PARAM_TYPE => $this->mModuleNames + ), + 'version' => false + ); + } + + protected function getParamDescription() { + return array ( + 'format' => 'The format of the output', + 'action' => 'What action you would like to perform', + 'version' => 'When showing help, include version for each module' + ); } protected function getDescription() { @@ -156,30 +248,6 @@ class ApiMain extends ApiBase { ); } - public function mainDieUsage($description, $errorCode, $httpRespCode = 0) { - if ($httpRespCode === 0) - header($errorCode, true); - else - header($errorCode, true, $httpRespCode); - - $this->makeErrorMessage($description, $errorCode); - - throw new UsageException($description, $errorCode); - } - - public function makeErrorMessage($description, $errorCode, $customContent = null) { - $this->mResult->Reset(); - $data = array ( - 'code' => $errorCode, - 'info' => $description - ); - - ApiResult :: setContent($data, - is_null($customContent) ? $this->makeHelpMsg() : $customContent); - - $this->mResult->addValue(null, 'error', $data); - } - /** * Override the parent to generate help messages for all available modules. */ @@ -211,53 +279,6 @@ class ApiMain extends ApiBase { return $msg; } - - /** - * Exception handler which simulates the appropriate catch() handling: - * - * try { - * ... - * } catch ( MWException $e ) { - * dieUsage() - * } catch ( Exception $e ) { - * echo $e->__toString(); - * } - * - * - * - * - * !!!!!!!!!!!!! REVIEW needed !!!!!!!!!!!!!!!!!! - * - * this method needs to be reviewed/cleaned up - * - * - * - */ - public function exceptionHandler( $e ) { - global $wgFullyInitialised; - if ( is_a( $e, 'MWException' ) ) { - try { - $msg = "Exception Caught: {$e->getMessage()}"; - $this->makeErrorMessage($msg, 'internal_api_error', "\n\n{$e->getTraceAsString()}\n\n"); - $this->printError(); - } catch (Exception $e2) { - echo $e->__toString(); - } - } else { - echo $e->__toString(); - } - - // Final cleanup, similar to wfErrorExit() - if ( $wgFullyInitialised ) { - try { - wfLogProfilingData(); // uses $wgRequest, hence the $wgFullyInitialised condition - } catch ( Exception $e ) {} - } - - // Exit value should be nonzero for the benefit of shell jobs - exit( 1 ); - } - private $mIsBot = null; public function isBot() { @@ -268,6 +289,10 @@ class ApiMain extends ApiBase { return $this->mIsBot; } + public function getShowVersions() { + return $this->mShowVersions; + } + public function getVersion() { $vers = array (); $vers[] = __CLASS__ . ': $Id$'; @@ -283,14 +308,17 @@ class ApiMain extends ApiBase { */ class UsageException extends Exception { - private $codestr; + private $mCodestr; - public function __construct($message, $codestr) { - parent :: __construct($message); - $this->codestr = $codestr; + public function __construct($message, $codestr, $code = 0) { + parent :: __construct($message, $code); + $this->mCodestr = $codestr; + } + public function getCodeString() { + return $this->mCodestr; } public function __toString() { - return "{$this->codestr}: {$this->message}"; + return "{$this->getCodeString()}: {$this->getMessage()}"; } } ?> \ No newline at end of file diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php new file mode 100644 index 0000000000..8c24861df8 --- /dev/null +++ b/includes/api/ApiOpenSearch.php @@ -0,0 +1,106 @@ + + * + * 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., + * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * http://www.gnu.org/copyleft/gpl.html + */ + +if (!defined('MEDIAWIKI')) { + // Eclipse helper - will be ignored in production + require_once ("ApiFormatBase.php"); +} + +class ApiOpenSearch extends ApiFormatBase { + + private $mResult = array(); + + public function __construct($main, $action) { + parent :: __construct($main, 'opensearch'); + } + + public function getMimeType() { + return 'application/json'; + } + + public function execute() { + $command = null; + extract($this->ExtractRequestParams()); + + // Prepare nested request + $params = new FauxRequest(array ( + 'action' => 'query', + 'list' => 'allpages', + 'apnamespace' => 0, + 'aplimit' => 10, + 'apprefix' => $command + )); + + // Execute + $module = new ApiMain($params); + $module->execute(); + + // Get clean data + $result =& $module->getResult(); + $result->SanitizeData(); + $data = $result->GetData(); + + // Reformat useful data for future printing + $result = array(); + foreach ($data['query']['allpages'] as $pageid => &$pageinfo) { + $result[] = $pageinfo['title']; + } + + $this->mResult = array($command, $result); + } + + public function executePrinter() { + $json = new Services_JSON(); + $this->printText($json->encode($this->mResult, true)); + } + + protected function GetAllowedParams() { + return array ( + 'command' => null + ); + } + + protected function GetParamDescription() { + return array ( + 'command' => 'Search string' + ); + } + + protected function GetDescription() { + return 'This module implements OpenSearch protocol'; + } + + protected function GetExamples() { + return array ( + 'api.php?action=opensearch&command=Te' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php index e02b459cc9..7c6f01b4d5 100644 --- a/includes/api/ApiQueryAllpages.php +++ b/includes/api/ApiQueryAllpages.php @@ -59,6 +59,10 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { if (isset ($from)) { $where[] = 'page_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($from)); } + + if (isset ($prefix)) { + $where[] = "page_title LIKE '{$db->strencode(ApiQueryBase :: titleToKey($prefix))}%'"; + } if ($filterredir === 'redirects') { $where['page_is_redirect'] = 1; @@ -126,6 +130,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { return array ( 'from' => null, + 'prefix' => null, 'namespace' => array ( ApiBase :: PARAM_DFLT => 0, ApiBase :: PARAM_TYPE => $this->getQuery()->getValidNamespaces()), @@ -148,6 +153,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { protected function getParamDescription() { return array ( 'from' => 'The page title to start enumerating from.', + 'prefix' => 'Search for all page titles that begin with this value.', 'namespace' => 'The namespace to enumerate. Default 0 (Main).', 'filterredir' => 'Which pages to list: "all" (default), "redirects", or "nonredirects"', 'limit' => 'How many total pages to return'