From d81f562b712f2387fa02290bf2ca86392ab356f2 Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Wed, 11 Oct 2006 20:21:25 +0000 Subject: Aktualisierung auf Version 1.8.1 --- includes/api/ApiBase.php | 441 +++++++++++++++++++ includes/api/ApiFormatBase.php | 161 +++++++ includes/api/ApiFormatJson.php | 56 +++ includes/api/ApiFormatJson_json.php | 841 +++++++++++++++++++++++++++++++++++ includes/api/ApiFormatXml.php | 161 +++++++ includes/api/ApiFormatYaml.php | 55 +++ includes/api/ApiFormatYaml_spyc.php | 854 ++++++++++++++++++++++++++++++++++++ includes/api/ApiHelp.php | 55 +++ includes/api/ApiLogin.php | 122 ++++++ includes/api/ApiMain.php | 226 ++++++++++ includes/api/ApiPageSet.php | 514 ++++++++++++++++++++++ includes/api/ApiQuery.php | 354 +++++++++++++++ includes/api/ApiQueryAllpages.php | 183 ++++++++ includes/api/ApiQueryBase.php | 112 +++++ includes/api/ApiQueryInfo.php | 82 ++++ includes/api/ApiQueryRevisions.php | 320 ++++++++++++++ includes/api/ApiQuerySiteinfo.php | 113 +++++ includes/api/ApiResult.php | 153 +++++++ 18 files changed, 4803 insertions(+) create mode 100644 includes/api/ApiBase.php create mode 100644 includes/api/ApiFormatBase.php create mode 100644 includes/api/ApiFormatJson.php create mode 100644 includes/api/ApiFormatJson_json.php create mode 100644 includes/api/ApiFormatXml.php create mode 100644 includes/api/ApiFormatYaml.php create mode 100644 includes/api/ApiFormatYaml_spyc.php create mode 100644 includes/api/ApiHelp.php create mode 100644 includes/api/ApiLogin.php create mode 100644 includes/api/ApiMain.php create mode 100644 includes/api/ApiPageSet.php create mode 100644 includes/api/ApiQuery.php create mode 100644 includes/api/ApiQueryAllpages.php create mode 100644 includes/api/ApiQueryBase.php create mode 100644 includes/api/ApiQueryInfo.php create mode 100644 includes/api/ApiQueryRevisions.php create mode 100644 includes/api/ApiQuerySiteinfo.php create mode 100644 includes/api/ApiResult.php (limited to 'includes/api') diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php new file mode 100644 index 00000000..f578f41b --- /dev/null +++ b/includes/api/ApiBase.php @@ -0,0 +1,441 @@ + + * + * 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 + */ + +abstract class ApiBase { + + // These constants allow modules to specify exactly how to treat incomming parameters. + + const PARAM_DFLT = 0; + const PARAM_ISMULTI = 1; + const PARAM_TYPE = 2; + const PARAM_MAX1 = 3; + const PARAM_MAX2 = 4; + const PARAM_MIN = 5; + + private $mMainModule, $mModuleName, $mParamPrefix; + + /** + * Constructor + */ + public function __construct($mainModule, $moduleName, $paramPrefix = '') { + $this->mMainModule = $mainModule; + $this->mModuleName = $moduleName; + $this->mParamPrefix = $paramPrefix; + } + + /** + * Executes this module + */ + public abstract function execute(); + + /** + * Get the name of the query being executed by this instance + */ + public function getModuleName() { + return $this->mModuleName; + } + + /** + * Get main module + */ + public function getMain() { + return $this->mMainModule; + } + + /** + * If this module's $this is the same as $this->mMainModule, its the root, otherwise no + */ + public function isMain() { + return $this === $this->mMainModule; + } + + /** + * Get result object + */ + public function getResult() { + // Main module has getResult() method overriden + // Safety - avoid infinite loop: + if ($this->isMain()) + ApiBase :: dieDebug(__METHOD__, 'base method was called on main module. '); + return $this->getMain()->getResult(); + } + + /** + * Get the result data array + */ + public function & getResultData() { + return $this->getResult()->getData(); + } + + /** + * Generates help message for this module, or false if there is no description + */ + public function makeHelpMsg() { + + static $lnPrfx = "\n "; + + $msg = $this->getDescription(); + + if ($msg !== false) { + + if (!is_array($msg)) + $msg = array ( + $msg + ); + $msg = $lnPrfx . implode($lnPrfx, $msg) . "\n"; + + // Parameters + $paramsMsg = $this->makeHelpMsgParameters(); + if ($paramsMsg !== false) { + $msg .= "Parameters:\n$paramsMsg"; + } + + // Examples + $examples = $this->getExamples(); + if ($examples !== false) { + if (!is_array($examples)) + $examples = array ( + $examples + ); + $msg .= 'Example' . (count($examples) > 1 ? 's' : '') . ":\n "; + $msg .= implode($lnPrfx, $examples) . "\n"; + } + + if ($this->getMain()->getShowVersions()) { + $versions = $this->getVersion(); + if (is_array($versions)) + $versions = implode("\n ", $versions); + $msg .= "Version:\n $versions\n"; + } + } + + return $msg; + } + + public function makeHelpMsgParameters() { + $params = $this->getAllowedParams(); + if ($params !== false) { + + $paramsDescription = $this->getParamDescription(); + $msg = ''; + foreach (array_keys($params) as $paramName) { + $desc = isset ($paramsDescription[$paramName]) ? $paramsDescription[$paramName] : ''; + if (is_array($desc)) + $desc = implode("\n" . str_repeat(' ', 19), $desc); + $msg .= sprintf(" %-14s - %s\n", $this->encodeParamName($paramName), $desc); + } + return $msg; + + } else + return false; + } + + /** + * Returns the description string for this module + */ + protected function getDescription() { + return false; + } + + /** + * Returns usage examples for this module. Return null if no examples are available. + */ + protected function getExamples() { + return false; + } + + /** + * Returns an array of allowed parameters (keys) => default value for that parameter + */ + protected function getAllowedParams() { + return false; + } + + /** + * Returns the description string for the given parameter. + */ + protected function getParamDescription() { + return false; + } + + /** + * This method mangles parameter name based on the prefix supplied to the constructor. + * Override this method to change parameter name during runtime + */ + public function encodeParamName($paramName) { + return $this->mParamPrefix . $paramName; + } + + /** + * Using getAllowedParams(), makes an array of the values provided by the user, + * with key being the name of the variable, and value - validated value from user or default. + * This method can be used to generate local variables using extract(). + */ + public function extractRequestParams() { + $params = $this->getAllowedParams(); + $results = array (); + + foreach ($params as $paramName => $paramSettings) + $results[$paramName] = $this->getParameterFromSettings($paramName, $paramSettings); + + return $results; + } + + /** + * Get a value for the given parameter + */ + protected function getParameter($paramName) { + $params = $this->getAllowedParams(); + $paramSettings = $params[$paramName]; + return $this->getParameterFromSettings($paramName, $paramSettings); + } + + /** + * Using the settings determine the value for the given parameter + * @param $paramName String: parameter name + * @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); + + if (!is_array($paramSettings)) { + $default = $paramSettings; + $multi = false; + $type = gettype($paramSettings); + } else { + $default = isset ($paramSettings[self :: PARAM_DFLT]) ? $paramSettings[self :: PARAM_DFLT] : null; + $multi = isset ($paramSettings[self :: PARAM_ISMULTI]) ? $paramSettings[self :: PARAM_ISMULTI] : false; + $type = isset ($paramSettings[self :: PARAM_TYPE]) ? $paramSettings[self :: PARAM_TYPE] : null; + + // When type is not given, and no choices, the type is the same as $default + if (!isset ($type)) { + if (isset ($default)) + $type = gettype($default); + else + $type = 'NULL'; // allow everything + } + } + + if ($type == 'boolean') { + if (isset ($default) && $default !== false) { + // Having a default value of anything other than 'false' is pointless + ApiBase :: dieDebug(__METHOD__, "Boolean param $paramName's default is set to '$default'"); + } + + $value = $wgRequest->getCheck($paramName); + } else + $value = $wgRequest->getVal($paramName, $default); + + if (isset ($value) && ($multi || is_array($type))) + $value = $this->parseMultiValue($paramName, $value, $multi, is_array($type) ? $type : null); + + // More validation only when choices were not given + // choices were validated in parseMultiValue() + if (!is_array($type) && isset ($value)) { + + switch ($type) { + case 'NULL' : // nothing to do + break; + case 'string' : // nothing to do + break; + case 'integer' : // Force everything using intval() + $value = is_array($value) ? array_map('intval', $value) : intval($value); + break; + case 'limit' : + if (!isset ($paramSettings[self :: PARAM_MAX1]) || !isset ($paramSettings[self :: PARAM_MAX2])) + ApiBase :: dieDebug(__METHOD__, "MAX1 or MAX2 are not defined for the limit $paramName"); + if ($multi) + ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); + $min = isset ($paramSettings[self :: PARAM_MIN]) ? $paramSettings[self :: PARAM_MIN] : 0; + $value = intval($value); + $this->validateLimit($paramName, $value, $min, $paramSettings[self :: PARAM_MAX1], $paramSettings[self :: PARAM_MAX2]); + break; + case 'boolean' : + if ($multi) + ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); + break; + case 'timestamp' : + if ($multi) + ApiBase :: dieDebug(__METHOD__, "Multi-values not supported for $paramName"); + if (!preg_match('/^[0-9]{14}$/', $value)) + $this->dieUsage("Invalid value '$value' for timestamp parameter $paramName", "badtimestamp_{$valueName}"); + break; + default : + ApiBase :: dieDebug(__METHOD__, "Param $paramName's type is unknown - $type"); + + } + } + + return $value; + } + + /** + * Return an array of values that were given in a 'a|b|c' notation, + * after it optionally validates them against the list allowed values. + * + * @param valueName - The name of the parameter (for error reporting) + * @param value - The value being parsed + * @param allowMultiple - Can $value contain more than one value separated by '|'? + * @param allowedValues - An array of values to check against. If null, all values are accepted. + * @return (allowMultiple ? an_array_of_values : a_single_value) + */ + protected function parseMultiValue($valueName, $value, $allowMultiple, $allowedValues) { + $valuesList = explode('|', $value); + if (!$allowMultiple && count($valuesList) != 1) { + $possibleValues = is_array($allowedValues) ? "of '" . implode("', '", $allowedValues) . "'" : ''; + $this->dieUsage("Only one $possibleValues is allowed for parameter '$valueName'", "multival_$valueName"); + } + if (is_array($allowedValues)) { + $unknownValues = array_diff($valuesList, $allowedValues); + if ($unknownValues) { + $this->dieUsage('Unrecognised value' . (count($unknownValues) > 1 ? "s '" : " '") . implode("', '", $unknownValues) . "' for parameter '$valueName'", "unknown_$valueName"); + } + } + + return $allowMultiple ? $valuesList : $valuesList[0]; + } + + /** + * Validate the value against the minimum and user/bot maximum limits. Prints usage info on failure. + */ + function validateLimit($varname, $value, $min, $max, $botMax) { + global $wgUser; + + if ($value < $min) { + $this->dieUsage("$varname may not be less than $min (set to $value)", $varname); + } + + if ($this->getMain()->isBot()) { + if ($value > $botMax) { + $this->dieUsage("$varname may not be over $botMax (set to $value) for bots", $varname); + } + } + elseif ($value > $max) { + $this->dieUsage("$varname may not be over $max (set to $value) for users", $varname); + } + } + + /** + * Call main module's error handler + */ + public function dieUsage($description, $errorCode, $httpRespCode = 0) { + $this->getMain()->mainDieUsage($description, $this->encodeParamName($errorCode), $httpRespCode); + } + + /** + * Internal code errors should be reported with this method + */ + protected static function dieDebug($method, $message) { + wfDebugDieBacktrace("Internal error in $method: $message"); + } + + /** + * Profiling: total module execution time + */ + private $mTimeIn = 0, $mModuleTime = 0; + + /** + * Start module profiling + */ + public function profileIn() { + if ($this->mTimeIn !== 0) + ApiBase :: dieDebug(__METHOD__, 'called twice without calling profileOut()'); + $this->mTimeIn = microtime(true); + } + + /** + * End module profiling + */ + public function profileOut() { + if ($this->mTimeIn === 0) + ApiBase :: dieDebug(__METHOD__, 'called without calling profileIn() first'); + if ($this->mDBTimeIn !== 0) + ApiBase :: dieDebug(__METHOD__, 'must be called after database profiling is done with profileDBOut()'); + + $this->mModuleTime += microtime(true) - $this->mTimeIn; + $this->mTimeIn = 0; + } + + /** + * Total time the module was executed + */ + public function getProfileTime() { + if ($this->mTimeIn !== 0) + ApiBase :: dieDebug(__METHOD__, 'called without calling profileOut() first'); + return $this->mModuleTime; + } + + /** + * Profiling: database execution time + */ + private $mDBTimeIn = 0, $mDBTime = 0; + + /** + * Start module profiling + */ + public function profileDBIn() { + if ($this->mTimeIn === 0) + ApiBase :: dieDebug(__METHOD__, 'must be called while profiling the entire module with profileIn()'); + if ($this->mDBTimeIn !== 0) + ApiBase :: dieDebug(__METHOD__, 'called twice without calling profileDBOut()'); + $this->mDBTimeIn = microtime(true); + } + + /** + * End database profiling + */ + public function profileDBOut() { + if ($this->mTimeIn === 0) + ApiBase :: dieDebug(__METHOD__, 'must be called while profiling the entire module with profileIn()'); + if ($this->mDBTimeIn === 0) + ApiBase :: dieDebug(__METHOD__, 'called without calling profileDBIn() first'); + + $time = microtime(true) - $this->mDBTimeIn; + $this->mDBTimeIn = 0; + + $this->mDBTime += $time; + $this->getMain()->mDBTime += $time; + } + + /** + * Total time the module used the database + */ + public function getProfileDBTime() { + if ($this->mDBTimeIn !== 0) + ApiBase :: dieDebug(__METHOD__, 'called without calling profileDBOut() first'); + return $this->mDBTime; + } + + public abstract function getVersion(); + + public static function getBaseVersion() { + return __CLASS__ . ': $Id: ApiBase.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php new file mode 100644 index 00000000..6f5b4aca --- /dev/null +++ b/includes/api/ApiFormatBase.php @@ -0,0 +1,161 @@ + + * + * 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 ('ApiBase.php'); +} + +abstract class ApiFormatBase extends ApiBase { + + private $mIsHtml, $mFormat; + + /** + * Constructor + */ + public function __construct($main, $format) { + parent :: __construct($main, $format); + + $this->mIsHtml = (substr($format, -2, 2) === 'fm'); // ends with 'fm' + if ($this->mIsHtml) + $this->mFormat = substr($format, 0, -2); // remove ending 'fm' + else + $this->mFormat = $format; + $this->mFormat = strtoupper($this->mFormat); + } + + /** + * Overriding class returns the mime type that should be sent to the client. + * This method is not called if getIsHtml() returns true. + * @return string + */ + public abstract function getMimeType(); + + public function getNeedsRawData() { + return false; + } + + /** + * Returns true when an HTML filtering printer should be used. + * The default implementation assumes that formats ending with 'fm' + * should be formatted in HTML. + */ + public function getIsHtml() { + return $this->mIsHtml; + } + + /** + * Initialize the printer function and prepares the output headers, etc. + * This method must be the first outputing method during execution. + * A help screen's header is printed for the HTML-based output + */ + function initPrinter($isError) { + $isHtml = $this->getIsHtml(); + $mime = $isHtml ? 'text/html' : $this->getMimeType(); + header("Content-Type: $mime; charset=utf-8;"); + + if ($isHtml) { +?> + + + MediaWiki API + + + +
+ + This result is being shown in mFormat?> format, + which might not be suitable for your application.
+ See API help for more information.
+
+ +
+getIsHtml()) {
+?>
+		
+ +getIsHtml()) + echo $this->formatHTML($text); + else + echo $text; + } + + /** + * Prety-print various elements in HTML format, such as xml tags and URLs. + * This method also replaces any '<' with < + */ + protected function formatHTML($text) { + // encode all tags as safe blue strings + $text = ereg_replace('\<([^>]+)\>', '<\1>', $text); + // identify URLs + $text = ereg_replace("[a-zA-Z]+://[^ '()<\n]+", '\\0', $text); + // identify requests to api.php + $text = ereg_replace("api\\.php\\?[^ ()<\n\t]+", '\\0', $text); + // make strings inside * bold + $text = ereg_replace("\\*[^<>\n]+\\*", '\\0', $text); + // make strings inside $ italic + $text = ereg_replace("\\$[^<>\n]+\\$", '\\0', $text); + + return $text; + } + + /** + * Returns usage examples for this format. + */ + protected function getExamples() { + return 'api.php?action=query&meta=siteinfo&si=namespaces&format=' . $this->getModuleName(); + } + + public static function getBaseVersion() { + return __CLASS__ . ': $Id: ApiFormatBase.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php new file mode 100644 index 00000000..fdc29cf2 --- /dev/null +++ b/includes/api/ApiFormatJson.php @@ -0,0 +1,56 @@ + + * + * 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 ApiFormatJson extends ApiFormatBase { + + public function __construct($main, $format) { + parent :: __construct($main, $format); + } + + public function getMimeType() { + return 'application/json'; + } + + public function execute() { + require ('ApiFormatJson_json.php'); + $json = new Services_JSON(); + $this->printText($json->encode($this->getResultData(), true)); + } + + protected function getDescription() { + return 'Output data in JSON format'; + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFormatJson.php 16725 2006-10-01 21:20:55Z yurik $'; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiFormatJson_json.php b/includes/api/ApiFormatJson_json.php new file mode 100644 index 00000000..375de7eb --- /dev/null +++ b/includes/api/ApiFormatJson_json.php @@ -0,0 +1,841 @@ + +* @author Matt Knapp +* @author Brett Stimmerman +* @copyright 2005 Michal Migurski +* @version CVS: $Id: JSON.php,v 1.30 2006/03/08 16:10:20 migurski Exp $ +* @license http://www.opensource.org/licenses/bsd-license.php +* @link http://pear.php.net/pepr/pepr-proposal-show.php?id=198 +*/ + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_SLICE', 1); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_STR', 2); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_ARR', 3); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_OBJ', 4); + +/** +* Marker constant for Services_JSON::decode(), used to flag stack state +*/ +define('SERVICES_JSON_IN_CMT', 5); + +/** +* Behavior switch for Services_JSON::decode() +*/ +define('SERVICES_JSON_LOOSE_TYPE', 16); + +/** +* Behavior switch for Services_JSON::decode() +*/ +define('SERVICES_JSON_SUPPRESS_ERRORS', 32); + +/** +* Converts to and from JSON format. +* +* Brief example of use: +* +* +* // create a new instance of Services_JSON +* $json = new Services_JSON(); +* +* // convert a complexe value to JSON notation, and send it to the browser +* $value = array('foo', 'bar', array(1, 2, 'baz'), array(3, array(4))); +* $output = $json->encode($value); +* +* print($output); +* // prints: ["foo","bar",[1,2,"baz"],[3,[4]]] +* +* // accept incoming POST data, assumed to be in JSON notation +* $input = file_get_contents('php://input', 1000000); +* $value = $json->decode($input); +* +*/ +class Services_JSON +{ + /** + * constructs a new JSON instance + * + * @param int $use object behavior flags; combine with boolean-OR + * + * possible values: + * - SERVICES_JSON_LOOSE_TYPE: loose typing. + * "{...}" syntax creates associative arrays + * instead of objects in decode(). + * - SERVICES_JSON_SUPPRESS_ERRORS: error suppression. + * Values which can't be encoded (e.g. resources) + * appear as NULL instead of throwing errors. + * By default, a deeply-nested resource will + * bubble up with an error, so all return values + * from encode() should be checked with isError() + */ + function Services_JSON($use = 0) + { + $this->use = $use; + } + + /** + * convert a string from one UTF-16 char to one UTF-8 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf16 UTF-16 character + * @return string UTF-8 character + * @access private + */ + function utf162utf8($utf16) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16'); + } + + $bytes = (ord($utf16{0}) << 8) | ord($utf16{1}); + + switch(true) { + case ((0x7F & $bytes) == $bytes): + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x7F & $bytes); + + case (0x07FF & $bytes) == $bytes: + // return a 2-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xC0 | (($bytes >> 6) & 0x1F)) + . chr(0x80 | ($bytes & 0x3F)); + + case (0xFFFF & $bytes) == $bytes: + // return a 3-byte UTF-8 character + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0xE0 | (($bytes >> 12) & 0x0F)) + . chr(0x80 | (($bytes >> 6) & 0x3F)) + . chr(0x80 | ($bytes & 0x3F)); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * convert a string from one UTF-8 char to one UTF-16 char + * + * Normally should be handled by mb_convert_encoding, but + * provides a slower PHP-only method for installations + * that lack the multibye string extension. + * + * @param string $utf8 UTF-8 character + * @return string UTF-16 character + * @access private + */ + function utf82utf16($utf8) + { + // oh please oh please oh please oh please oh please + if(function_exists('mb_convert_encoding')) { + return mb_convert_encoding($utf8, 'UTF-16', 'UTF-8'); + } + + switch(strlen($utf8)) { + case 1: + // this case should never be reached, because we are in ASCII range + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return $utf8; + + case 2: + // return a UTF-16 character from a 2-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr(0x07 & (ord($utf8{0}) >> 2)) + . chr((0xC0 & (ord($utf8{0}) << 6)) + | (0x3F & ord($utf8{1}))); + + case 3: + // return a UTF-16 character from a 3-byte UTF-8 char + // see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + return chr((0xF0 & (ord($utf8{0}) << 4)) + | (0x0F & (ord($utf8{1}) >> 2))) + . chr((0xC0 & (ord($utf8{1}) << 6)) + | (0x7F & ord($utf8{2}))); + } + + // ignoring UTF-32 for now, sorry + return ''; + } + + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * @param bool $pretty pretty-print output with indents and newlines + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access public + */ + function encode($var, $pretty=false) + { + $this->indent = 0; + $this->pretty = $pretty; + $this->nameValSeparator = $pretty ? ': ' : ':'; + return $this->encode2($var); + } + + /** + * encodes an arbitrary variable into JSON format + * + * @param mixed $var any number, boolean, string, array, or object to be encoded. + * see argument 1 to Services_JSON() above for array-parsing behavior. + * if var is a strng, note that encode() always expects it + * to be in ASCII or UTF-8 format! + * + * @return mixed JSON string representation of input var or an error if a problem occurs + * @access private + */ + function encode2($var) + { + if ($this->pretty) { + $close = "\n" . str_repeat("\t", $this->indent); + $open = $close . "\t"; + $mid = ',' . $open; + } + else { + $open = $close = ''; + $mid = ','; + } + + switch (gettype($var)) { + case 'boolean': + return $var ? 'true' : 'false'; + + case 'NULL': + return 'null'; + + case 'integer': + return (int) $var; + + case 'double': + case 'float': + return (float) $var; + + case 'string': + // STRINGS ARE EXPECTED TO BE IN ASCII OR UTF-8 FORMAT + $ascii = ''; + $strlen_var = strlen($var); + + /* + * Iterate over every character in the string, + * escaping with a slash or encoding to UTF-8 where necessary + */ + for ($c = 0; $c < $strlen_var; ++$c) { + + $ord_var_c = ord($var{$c}); + + switch (true) { + case $ord_var_c == 0x08: + $ascii .= '\b'; + break; + case $ord_var_c == 0x09: + $ascii .= '\t'; + break; + case $ord_var_c == 0x0A: + $ascii .= '\n'; + break; + case $ord_var_c == 0x0C: + $ascii .= '\f'; + break; + case $ord_var_c == 0x0D: + $ascii .= '\r'; + break; + + case $ord_var_c == 0x22: + case $ord_var_c == 0x2F: + case $ord_var_c == 0x5C: + // double quote, slash, slosh + $ascii .= '\\'.$var{$c}; + break; + + case (($ord_var_c >= 0x20) && ($ord_var_c <= 0x7F)): + // characters U-00000000 - U-0000007F (same as ASCII) + $ascii .= $var{$c}; + break; + + case (($ord_var_c & 0xE0) == 0xC0): + // characters U-00000080 - U-000007FF, mask 110XXXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, ord($var{$c + 1})); + $c += 1; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF0) == 0xE0): + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2})); + $c += 2; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xF8) == 0xF0): + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3})); + $c += 3; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFC) == 0xF8): + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4})); + $c += 4; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + + case (($ord_var_c & 0xFE) == 0xFC): + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3}), + ord($var{$c + 4}), + ord($var{$c + 5})); + $c += 5; + $utf16 = $this->utf82utf16($char); + $ascii .= sprintf('\u%04s', bin2hex($utf16)); + break; + } + } + + return '"'.$ascii.'"'; + + case 'array': + /* + * As per JSON spec if any array key is not an integer + * we must treat the the whole array as an object. We + * also try to catch a sparsely populated associative + * array with numeric keys here because some JS engines + * will create an array with empty indexes up to + * max_index which can cause memory issues and because + * the keys, which may be relevant, will be remapped + * otherwise. + * + * As per the ECMA and JSON specification an object may + * have any string as a property. Unfortunately due to + * a hole in the ECMA specification if the key is a + * ECMA reserved word or starts with a digit the + * parameter is only accessible using ECMAScript's + * bracket notation. + */ + + // treat as a JSON object + if (is_array($var) && count($var) && (array_keys($var) !== range(0, sizeof($var) - 1))) { + $this->indent++; + $properties = array_map(array($this, 'name_value'), + array_keys($var), + array_values($var)); + $this->indent--; + + foreach($properties as $property) { + if(Services_JSON::isError($property)) { + return $property; + } + } + + return '{' . $open . join($mid, $properties) . $close . '}'; + } + + // treat it like a regular array + $this->indent++; + $elements = array_map(array($this, 'encode2'), $var); + $this->indent--; + + foreach($elements as $element) { + if(Services_JSON::isError($element)) { + return $element; + } + } + + return '[' . $open . join($mid, $elements) . $close . ']'; + + case 'object': + $vars = get_object_vars($var); + + $this->indent++; + $properties = array_map(array($this, 'name_value'), + array_keys($vars), + array_values($vars)); + $this->indent--; + + foreach($properties as $property) { + if(Services_JSON::isError($property)) { + return $property; + } + } + + return '{' . $open . join($mid, $properties) . $close . '}'; + + default: + return ($this->use & SERVICES_JSON_SUPPRESS_ERRORS) + ? 'null' + : new Services_JSON_Error(gettype($var)." can not be encoded as JSON string"); + } + } + + /** + * array-walking function for use in generating JSON-formatted name-value pairs + * + * @param string $name name of key to use + * @param mixed $value reference to an array element to be encoded + * + * @return string JSON-formatted name-value pair, like '"name":value' + * @access private + */ + function name_value($name, $value) + { + $encoded_value = $this->encode2($value); + + if(Services_JSON::isError($encoded_value)) { + return $encoded_value; + } + + return $this->encode2(strval($name)) . $this->nameValSeparator . $encoded_value; + } + + /** + * reduce a string by removing leading and trailing comments and whitespace + * + * @param $str string string value to strip of comments and whitespace + * + * @return string string value stripped of comments and whitespace + * @access private + */ + function reduce_string($str) + { + $str = preg_replace(array( + + // eliminate single line comments in '// ...' form + '#^\s*//(.+)$#m', + + // eliminate multi-line comments in '/* ... */' form, at start of string + '#^\s*/\*(.+)\*/#Us', + + // eliminate multi-line comments in '/* ... */' form, at end of string + '#/\*(.+)\*/\s*$#Us' + + ), '', $str); + + // eliminate extraneous space + return trim($str); + } + + /** + * decodes a JSON string into appropriate variable + * + * @param string $str JSON-formatted string + * + * @return mixed number, boolean, string, array, or object + * corresponding to given JSON input string. + * See argument 1 to Services_JSON() above for object-output behavior. + * Note that decode() always returns strings + * in ASCII or UTF-8 format! + * @access public + */ + function decode($str) + { + $str = $this->reduce_string($str); + + switch (strtolower($str)) { + case 'true': + return true; + + case 'false': + return false; + + case 'null': + return null; + + default: + $m = array(); + + if (is_numeric($str)) { + // Lookie-loo, it's a number + + // This would work on its own, but I'm trying to be + // good about returning integers where appropriate: + // return (float)$str; + + // Return float or int, as appropriate + return ((float)$str == (integer)$str) + ? (integer)$str + : (float)$str; + + } elseif (preg_match('/^("|\').*(\1)$/s', $str, $m) && $m[1] == $m[2]) { + // STRINGS RETURNED IN UTF-8 FORMAT + $delim = substr($str, 0, 1); + $chrs = substr($str, 1, -1); + $utf8 = ''; + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c < $strlen_chrs; ++$c) { + + $substr_chrs_c_2 = substr($chrs, $c, 2); + $ord_chrs_c = ord($chrs{$c}); + + switch (true) { + case $substr_chrs_c_2 == '\b': + $utf8 .= chr(0x08); + ++$c; + break; + case $substr_chrs_c_2 == '\t': + $utf8 .= chr(0x09); + ++$c; + break; + case $substr_chrs_c_2 == '\n': + $utf8 .= chr(0x0A); + ++$c; + break; + case $substr_chrs_c_2 == '\f': + $utf8 .= chr(0x0C); + ++$c; + break; + case $substr_chrs_c_2 == '\r': + $utf8 .= chr(0x0D); + ++$c; + break; + + case $substr_chrs_c_2 == '\\"': + case $substr_chrs_c_2 == '\\\'': + case $substr_chrs_c_2 == '\\\\': + case $substr_chrs_c_2 == '\\/': + if (($delim == '"' && $substr_chrs_c_2 != '\\\'') || + ($delim == "'" && $substr_chrs_c_2 != '\\"')) { + $utf8 .= $chrs{++$c}; + } + break; + + case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $c, 6)): + // single, escaped unicode character + $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) + . chr(hexdec(substr($chrs, ($c + 4), 2))); + $utf8 .= $this->utf162utf8($utf16); + $c += 5; + break; + + case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F): + $utf8 .= $chrs{$c}; + break; + + case ($ord_chrs_c & 0xE0) == 0xC0: + // characters U-00000080 - U-000007FF, mask 110XXXXX + //see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 2); + ++$c; + break; + + case ($ord_chrs_c & 0xF0) == 0xE0: + // characters U-00000800 - U-0000FFFF, mask 1110XXXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 3); + $c += 2; + break; + + case ($ord_chrs_c & 0xF8) == 0xF0: + // characters U-00010000 - U-001FFFFF, mask 11110XXX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 4); + $c += 3; + break; + + case ($ord_chrs_c & 0xFC) == 0xF8: + // characters U-00200000 - U-03FFFFFF, mask 111110XX + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 5); + $c += 4; + break; + + case ($ord_chrs_c & 0xFE) == 0xFC: + // characters U-04000000 - U-7FFFFFFF, mask 1111110X + // see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8 + $utf8 .= substr($chrs, $c, 6); + $c += 5; + break; + + } + + } + + return $utf8; + + } elseif (preg_match('/^\[.*\]$/s', $str) || preg_match('/^\{.*\}$/s', $str)) { + // array, or object notation + + if ($str{0} == '[') { + $stk = array(SERVICES_JSON_IN_ARR); + $arr = array(); + } else { + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = array(); + } else { + $stk = array(SERVICES_JSON_IN_OBJ); + $obj = new stdClass(); + } + } + + array_push($stk, array('what' => SERVICES_JSON_SLICE, + 'where' => 0, + 'delim' => false)); + + $chrs = substr($str, 1, -1); + $chrs = $this->reduce_string($chrs); + + if ($chrs == '') { + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } else { + return $obj; + + } + } + + //print("\nparsing {$chrs}\n"); + + $strlen_chrs = strlen($chrs); + + for ($c = 0; $c <= $strlen_chrs; ++$c) { + + $top = end($stk); + $substr_chrs_c_2 = substr($chrs, $c, 2); + + if (($c == $strlen_chrs) || (($chrs{$c} == ',') && ($top['what'] == SERVICES_JSON_SLICE))) { + // found a comma that is not inside a string, array, etc., + // OR we've reached the end of the character list + $slice = substr($chrs, $top['where'], ($c - $top['where'])); + array_push($stk, array('what' => SERVICES_JSON_SLICE, 'where' => ($c + 1), 'delim' => false)); + //print("Found split at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + // we are in an array, so just push an element onto the stack + array_push($arr, $this->decode($slice)); + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + // we are in an object, so figure + // out the property name and set an + // element in an associative array, + // for now + $parts = array(); + + if (preg_match('/^\s*(["\'].*[^\\\]["\'])\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // "name":value pair + $key = $this->decode($parts[1]); + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } elseif (preg_match('/^\s*(\w+)\s*:\s*(\S.*),?$/Uis', $slice, $parts)) { + // name:value pair, where name is unquoted + $key = $parts[1]; + $val = $this->decode($parts[2]); + + if ($this->use & SERVICES_JSON_LOOSE_TYPE) { + $obj[$key] = $val; + } else { + $obj->$key = $val; + } + } + + } + + } elseif ((($chrs{$c} == '"') || ($chrs{$c} == "'")) && ($top['what'] != SERVICES_JSON_IN_STR)) { + // found a quote, and we are not inside a string + array_push($stk, array('what' => SERVICES_JSON_IN_STR, 'where' => $c, 'delim' => $chrs{$c})); + //print("Found start of string at {$c}\n"); + + } elseif (($chrs{$c} == $top['delim']) && + ($top['what'] == SERVICES_JSON_IN_STR) && + (($chrs{$c - 1} != '\\') || + ($chrs{$c - 1} == '\\' && $chrs{$c - 2} == '\\'))) { + // found a quote, we're in a string, and it's not escaped + array_pop($stk); + //print("Found end of string at {$c}: ".substr($chrs, $top['where'], (1 + 1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '[') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-bracket, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_ARR, 'where' => $c, 'delim' => false)); + //print("Found start of array at {$c}\n"); + + } elseif (($chrs{$c} == ']') && ($top['what'] == SERVICES_JSON_IN_ARR)) { + // found a right-bracket, and we're in an array + array_pop($stk); + //print("Found end of array at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($chrs{$c} == '{') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a left-brace, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_OBJ, 'where' => $c, 'delim' => false)); + //print("Found start of object at {$c}\n"); + + } elseif (($chrs{$c} == '}') && ($top['what'] == SERVICES_JSON_IN_OBJ)) { + // found a right-brace, and we're in an object + array_pop($stk); + //print("Found end of object at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } elseif (($substr_chrs_c_2 == '/*') && + in_array($top['what'], array(SERVICES_JSON_SLICE, SERVICES_JSON_IN_ARR, SERVICES_JSON_IN_OBJ))) { + // found a comment start, and we are in an array, object, or slice + array_push($stk, array('what' => SERVICES_JSON_IN_CMT, 'where' => $c, 'delim' => false)); + $c++; + //print("Found start of comment at {$c}\n"); + + } elseif (($substr_chrs_c_2 == '*/') && ($top['what'] == SERVICES_JSON_IN_CMT)) { + // found a comment end, and we're in one now + array_pop($stk); + $c++; + + for ($i = $top['where']; $i <= $c; ++$i) + $chrs = substr_replace($chrs, ' ', $i, 1); + + //print("Found end of comment at {$c}: ".substr($chrs, $top['where'], (1 + $c - $top['where']))."\n"); + + } + + } + + if (reset($stk) == SERVICES_JSON_IN_ARR) { + return $arr; + + } elseif (reset($stk) == SERVICES_JSON_IN_OBJ) { + return $obj; + + } + + } + } + } + + /** + * @todo Ultimately, this should just call PEAR::isError() + */ + function isError($data, $code = null) + { + if (class_exists('pear')) { + return PEAR::isError($data, $code); + } elseif (is_object($data) && (get_class($data) == 'services_json_error' || + is_subclass_of($data, 'services_json_error'))) { + return true; + } + + return false; + } +} + +if (class_exists('PEAR_Error')) { + + class Services_JSON_Error extends PEAR_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + parent::PEAR_Error($message, $code, $mode, $options, $userinfo); + } + } + +} else { + + /** + * @todo Ultimately, this class shall be descended from PEAR_Error + */ + class Services_JSON_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + + } + } + +} + +?> diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php new file mode 100644 index 00000000..6aa08e00 --- /dev/null +++ b/includes/api/ApiFormatXml.php @@ -0,0 +1,161 @@ + + * + * 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 ApiFormatXml extends ApiFormatBase { + + public function __construct($main, $format) { + parent :: __construct($main, $format); + } + + public function getMimeType() { + return 'text/xml'; + } + + public function getNeedsRawData() { + return true; + } + + public function execute() { + $xmlindent = null; + extract($this->extractRequestParams()); + + if ($xmlindent || $this->getIsHtml()) + $xmlindent = -2; + else + $xmlindent = null; + + $this->printText(''); + $this->recXmlPrint('api', $this->getResultData(), $xmlindent); + } + + /** + * This method takes an array and converts it into an xml. + * There are several noteworthy cases: + * + * If array contains a key '_element', then the code assumes that ALL other keys are not important and replaces them with the value['_element']. + * Example: name='root', value = array( '_element'=>'page', 'x', 'y', 'z') creates x y z + * + * If any of the array's element key is '*', then the code treats all other key->value pairs as attributes, and the value['*'] as the element's content. + * Example: name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10) creates text + * + * If neither key is found, all keys become element names, and values become element content. + * The method is recursive, so the same rules apply to any sub-arrays. + */ + function recXmlPrint($elemName, $elemValue, $indent) { + if (!is_null($indent)) { + $indent += 2; + $indstr = "\n" . str_repeat(" ", $indent); + } else { + $indstr = ''; + } + + switch (gettype($elemValue)) { + case 'array' : + + if (isset ($elemValue['*'])) { + $subElemContent = $elemValue['*']; + unset ($elemValue['*']); + } else { + $subElemContent = null; + } + + if (isset ($elemValue['_element'])) { + $subElemIndName = $elemValue['_element']; + unset ($elemValue['_element']); + } else { + $subElemIndName = null; + } + + $indElements = array (); + $subElements = array (); + foreach ($elemValue as $subElemId => & $subElemValue) { + if (gettype($subElemId) === 'integer') { + if (!is_array($subElemValue)) + ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has a scalar indexed value."); + $indElements[] = $subElemValue; + unset ($elemValue[$subElemId]); + } elseif (is_array($subElemValue)) { + $subElements[$subElemId] = $subElemValue; + unset ($elemValue[$subElemId]); + } + } + + if (is_null($subElemIndName) && !empty ($indElements)) + ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has integer keys without _element value"); + + if (!empty ($subElements) && !empty ($indElements) && !is_null($subElemContent)) + ApiBase :: dieDebug(__METHOD__, "($elemName, ...) has content and subelements"); + + if (!is_null($subElemContent)) { + $this->printText($indstr . wfElement($elemName, $elemValue, $subElemContent)); + } elseif (empty ($indElements) && empty ($subElements)) { + $this->printText($indstr . wfElement($elemName, $elemValue)); + } else { + $this->printText($indstr . wfElement($elemName, $elemValue, null)); + + foreach ($subElements as $subElemId => & $subElemValue) + $this->recXmlPrint($subElemId, $subElemValue, $indent); + + foreach ($indElements as $subElemId => & $subElemValue) + $this->recXmlPrint($subElemIndName, $subElemValue, $indent); + + $this->printText($indstr . wfCloseElement($elemName)); + } + break; + case 'object' : + // ignore + break; + default : + $this->printText($indstr . wfElement($elemName, null, $elemValue)); + break; + } + } + protected function getDescription() { + return 'Output data in XML format'; + } + + protected function getAllowedParams() { + return array ( + 'xmlindent' => false + ); + } + + protected function getParamDescription() { + return array ( + 'xmlindent' => 'Enable XML indentation' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFormatXml.php 16725 2006-10-01 21:20:55Z yurik $'; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php new file mode 100644 index 00000000..bd74f01a --- /dev/null +++ b/includes/api/ApiFormatYaml.php @@ -0,0 +1,55 @@ + + * + * 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 ApiFormatYaml extends ApiFormatBase { + + public function __construct($main, $format) { + parent :: __construct($main, $format); + } + + public function getMimeType() { + return 'application/yaml'; + } + + public function execute() { + require ('ApiFormatYaml_spyc.php'); + $this->printText(Spyc :: YAMLDump($this->getResultData())); + } + + protected function getDescription() { + return 'Output data in YAML format'; + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFormatYaml.php 16725 2006-10-01 21:20:55Z yurik $'; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiFormatYaml_spyc.php b/includes/api/ApiFormatYaml_spyc.php new file mode 100644 index 00000000..05a39e23 --- /dev/null +++ b/includes/api/ApiFormatYaml_spyc.php @@ -0,0 +1,854 @@ + + * @link http://spyc.sourceforge.net/ + * @copyright Copyright 2005-2006 Chris Wanstrath + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @package Spyc + */ + + /** + * A node, used by Spyc for parsing YAML. + * @package Spyc + */ + class YAMLNode { + /**#@+ + * @access public + * @var string + */ + var $parent; + var $id; + /**#@+*/ + /** + * @access public + * @var mixed + */ + var $data; + /** + * @access public + * @var int + */ + var $indent; + /** + * @access public + * @var bool + */ + var $children = false; + + /** + * The constructor assigns the node a unique ID. + * @access public + * @return void + */ + function YAMLNode() { + $this->id = uniqid(''); + } + } + + /** + * The Simple PHP YAML Class. + * + * This class can be used to read a YAML file and convert its contents + * into a PHP array. It currently supports a very limited subsection of + * the YAML spec. + * + * Usage: + * + * $parser = new Spyc; + * $array = $parser->load($file); + * + * @package Spyc + */ + class Spyc { + + /** + * Load YAML into a PHP array statically + * + * The load method, when supplied with a YAML stream (string or file), + * will do its best to convert YAML in a file into a PHP array. Pretty + * simple. + * Usage: + * + * $array = Spyc::YAMLLoad('lucky.yml'); + * print_r($array); + * + * @access public + * @return array + * @param string $input Path of YAML file or string containing YAML + */ + function YAMLLoad($input) { + $spyc = new Spyc; + return $spyc->load($input); + } + + /** + * Dump YAML from PHP array statically + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as nothing.yml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @access public + * @static + * @return string + * @param array $array PHP array + * @param int $indent Pass in false to use the default, which is 2 + * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) + */ + public static function YAMLDump($array,$indent = false,$wordwrap = false) { + $spyc = new Spyc; + return $spyc->dump($array,$indent,$wordwrap); + } + + /** + * Load YAML into a PHP array from an instantiated object + * + * The load method, when supplied with a YAML stream (string or file path), + * will do its best to convert the YAML into a PHP array. Pretty simple. + * Usage: + * + * $parser = new Spyc; + * $array = $parser->load('lucky.yml'); + * print_r($array); + * + * @access public + * @return array + * @param string $input Path of YAML file or string containing YAML + */ + function load($input) { + // See what type of input we're talking about + // If it's not a file, assume it's a string + if (!empty($input) && (strpos($input, "\n") === false) + && file_exists($input)) { + $yaml = file($input); + } else { + $yaml = explode("\n",$input); + } + // Initiate some objects and values + $base = new YAMLNode; + $base->indent = 0; + $this->_lastIndent = 0; + $this->_lastNode = $base->id; + $this->_inBlock = false; + $this->_isInline = false; + + foreach ($yaml as $linenum => $line) { + $ifchk = trim($line); + + // If the line starts with a tab (instead of a space), throw a fit. + if (preg_match('/^(\t)+(\w+)/', $line)) { + $err = 'ERROR: Line '. ($linenum + 1) .' in your input YAML begins'. + ' with a tab. YAML only recognizes spaces. Please reformat.'; + die($err); + } + + if ($this->_inBlock === false && empty($ifchk)) { + continue; + } elseif ($this->_inBlock == true && empty($ifchk)) { + $last =& $this->_allNodes[$this->_lastNode]; + $last->data[key($last->data)] .= "\n"; + } elseif ($ifchk{0} != '#' && substr($ifchk,0,3) != '---') { + // Create a new node and get its indent + $node = new YAMLNode; + $node->indent = $this->_getIndent($line); + + // Check where the node lies in the hierarchy + if ($this->_lastIndent == $node->indent) { + // If we're in a block, add the text to the parent's data + if ($this->_inBlock === true) { + $parent =& $this->_allNodes[$this->_lastNode]; + $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd; + } else { + // The current node's parent is the same as the previous node's + if (isset($this->_allNodes[$this->_lastNode])) { + $node->parent = $this->_allNodes[$this->_lastNode]->parent; + } + } + } elseif ($this->_lastIndent < $node->indent) { + if ($this->_inBlock === true) { + $parent =& $this->_allNodes[$this->_lastNode]; + $parent->data[key($parent->data)] .= trim($line).$this->_blockEnd; + } elseif ($this->_inBlock === false) { + // The current node's parent is the previous node + $node->parent = $this->_lastNode; + + // If the value of the last node's data was > or | we need to + // start blocking i.e. taking in all lines as a text value until + // we drop our indent. + $parent =& $this->_allNodes[$node->parent]; + $this->_allNodes[$node->parent]->children = true; + if (is_array($parent->data)) { + $chk = $parent->data[key($parent->data)]; + if ($chk === '>') { + $this->_inBlock = true; + $this->_blockEnd = ' '; + $parent->data[key($parent->data)] = + str_replace('>','',$parent->data[key($parent->data)]); + $parent->data[key($parent->data)] .= trim($line).' '; + $this->_allNodes[$node->parent]->children = false; + $this->_lastIndent = $node->indent; + } elseif ($chk === '|') { + $this->_inBlock = true; + $this->_blockEnd = "\n"; + $parent->data[key($parent->data)] = + str_replace('|','',$parent->data[key($parent->data)]); + $parent->data[key($parent->data)] .= trim($line)."\n"; + $this->_allNodes[$node->parent]->children = false; + $this->_lastIndent = $node->indent; + } + } + } + } elseif ($this->_lastIndent > $node->indent) { + // Any block we had going is dead now + if ($this->_inBlock === true) { + $this->_inBlock = false; + if ($this->_blockEnd = "\n") { + $last =& $this->_allNodes[$this->_lastNode]; + $last->data[key($last->data)] = + trim($last->data[key($last->data)]); + } + } + + // We don't know the parent of the node so we have to find it + // foreach ($this->_allNodes as $n) { + foreach ($this->_indentSort[$node->indent] as $n) { + if ($n->indent == $node->indent) { + $node->parent = $n->parent; + } + } + } + + if ($this->_inBlock === false) { + // Set these properties with information from our current node + $this->_lastIndent = $node->indent; + // Set the last node + $this->_lastNode = $node->id; + // Parse the YAML line and return its data + $node->data = $this->_parseLine($line); + // Add the node to the master list + $this->_allNodes[$node->id] = $node; + // Add a reference to the node in an indent array + $this->_indentSort[$node->indent][] =& $this->_allNodes[$node->id]; + // Add a reference to the node in a References array if this node + // has a YAML reference in it. + if ( + ( (is_array($node->data)) && + isset($node->data[key($node->data)]) && + (!is_array($node->data[key($node->data)])) ) + && + ( (preg_match('/^&([^ ]+)/',$node->data[key($node->data)])) + || + (preg_match('/^\*([^ ]+)/',$node->data[key($node->data)])) ) + ) { + $this->_haveRefs[] =& $this->_allNodes[$node->id]; + } elseif ( + ( (is_array($node->data)) && + isset($node->data[key($node->data)]) && + (is_array($node->data[key($node->data)])) ) + ) { + // Incomplete reference making code. Ugly, needs cleaned up. + foreach ($node->data[key($node->data)] as $d) { + if ( !is_array($d) && + ( (preg_match('/^&([^ ]+)/',$d)) + || + (preg_match('/^\*([^ ]+)/',$d)) ) + ) { + $this->_haveRefs[] =& $this->_allNodes[$node->id]; + } + } + } + } + } + } + unset($node); + + // Here we travel through node-space and pick out references (& and *) + $this->_linkReferences(); + + // Build the PHP array out of node-space + $trunk = $this->_buildArray(); + return $trunk; + } + + /** + * Dump PHP array to YAML + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. Pretty simple. Feel free to + * save the returned string as tasteful.yml and pass it around. + * + * Oh, and you can decide how big the indent is and what the wordwrap + * for folding is. Pretty cool -- just pass in 'false' for either if + * you want to use the default. + * + * Indent's default is 2 spaces, wordwrap's default is 40 characters. And + * you can turn off wordwrap by passing in 0. + * + * @access public + * @return string + * @param array $array PHP array + * @param int $indent Pass in false to use the default, which is 2 + * @param int $wordwrap Pass in 0 for no wordwrap, false for default (40) + */ + function dump($array,$indent = false,$wordwrap = false) { + // Dumps to some very clean YAML. We'll have to add some more features + // and options soon. And better support for folding. + + // New features and options. + if ($indent === false or !is_numeric($indent)) { + $this->_dumpIndent = 2; + } else { + $this->_dumpIndent = $indent; + } + + if ($wordwrap === false or !is_numeric($wordwrap)) { + $this->_dumpWordWrap = 40; + } else { + $this->_dumpWordWrap = $wordwrap; + } + + // New YAML document + $string = "---\n"; + + // Start at the base of the array and move through it. + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key,$value,0); + } + return $string; + } + + /**** Private Properties ****/ + + /**#@+ + * @access private + * @var mixed + */ + var $_haveRefs; + var $_allNodes; + var $_lastIndent; + var $_lastNode; + var $_inBlock; + var $_isInline; + var $_dumpIndent; + var $_dumpWordWrap; + /**#@+*/ + + /**** Private Methods ****/ + + /** + * Attempts to convert a key / value array item to YAML + * @access private + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + function _yamlize($key,$value,$indent) { + if (is_array($value)) { + // It has children. What to do? + // Make it the right kind of item + $string = $this->_dumpNode($key,NULL,$indent); + // Add the indent + $indent += $this->_dumpIndent; + // Yamlize the array + $string .= $this->_yamlizeArray($value,$indent); + } elseif (!is_array($value)) { + // It doesn't have children. Yip. + $string = $this->_dumpNode($key,$value,$indent); + } + return $string; + } + + /** + * Attempts to convert an array to YAML + * @access private + * @return string + * @param $array The array you want to convert + * @param $indent The indent of the current level + */ + function _yamlizeArray($array,$indent) { + if (is_array($array)) { + $string = ''; + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key,$value,$indent); + } + return $string; + } else { + return false; + } + } + + /** + * Returns YAML from a key and a value + * @access private + * @return string + * @param $key The name of the key + * @param $value The value of the item + * @param $indent The indent of the current node + */ + function _dumpNode($key,$value,$indent) { + // do some folding here, for blocks + if (strpos($value,"\n")) { + $value = $this->_doLiteralBlock($value,$indent); + } else { + $value = $this->_doFolding($value,$indent); + } + + $spaces = str_repeat(' ',$indent); + + if (is_int($key)) { + // It's a sequence + $string = $spaces.'- '.$value."\n"; + } else { + // It's mapped + $string = $spaces.$key.': '.$value."\n"; + } + return $string; + } + + /** + * Creates a literal block for dumping + * @access private + * @return string + * @param $value + * @param $indent int The value of the indent + */ + function _doLiteralBlock($value,$indent) { + $exploded = explode("\n",$value); + $newValue = '|'; + $indent += $this->_dumpIndent; + $spaces = str_repeat(' ',$indent); + foreach ($exploded as $line) { + $newValue .= "\n" . $spaces . trim($line); + } + return $newValue; + } + + /** + * Folds a string of text, if necessary + * @access private + * @return string + * @param $value The string you wish to fold + */ + function _doFolding($value,$indent) { + // Don't do anything if wordwrap is set to 0 + if ($this->_dumpWordWrap === 0) { + return $value; + } + + if (strlen($value) > $this->_dumpWordWrap) { + $indent += $this->_dumpIndent; + $indent = str_repeat(' ',$indent); + $wrapped = wordwrap($value,$this->_dumpWordWrap,"\n$indent"); + $value = ">\n".$indent.$wrapped; + } + return $value; + } + + /* Methods used in loading */ + + /** + * Finds and returns the indentation of a YAML line + * @access private + * @return int + * @param string $line A line from the YAML file + */ + function _getIndent($line) { + preg_match('/^\s{1,}/',$line,$match); + if (!empty($match[0])) { + $indent = substr_count($match[0],' '); + } else { + $indent = 0; + } + return $indent; + } + + /** + * Parses YAML code and returns an array for a node + * @access private + * @return array + * @param string $line A line from the YAML file + */ + function _parseLine($line) { + $line = trim($line); + + $array = array(); + + if (preg_match('/^-(.*):$/',$line)) { + // It's a mapped sequence + $key = trim(substr(substr($line,1),0,-1)); + $array[$key] = ''; + } elseif ($line[0] == '-' && substr($line,0,3) != '---') { + // It's a list item but not a new stream + if (strlen($line) > 1) { + $value = trim(substr($line,1)); + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + $array[] = $value; + } else { + $array[] = array(); + } + } elseif (preg_match('/^(.+):/',$line,$key)) { + // It's a key/value pair most likely + // If the key is in double quotes pull it out + if (preg_match('/^(["\'](.*)["\'](\s)*:)/',$line,$matches)) { + $value = trim(str_replace($matches[1],'',$line)); + $key = $matches[2]; + } else { + // Do some guesswork as to the key and the value + $explode = explode(':',$line); + $key = trim($explode[0]); + array_shift($explode); + $value = trim(implode(':',$explode)); + } + + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + if (empty($key)) { + $array[] = $value; + } else { + $array[$key] = $value; + } + } + return $array; + } + + /** + * Finds the type of the passed value, returns the value as the new type. + * @access private + * @param string $value + * @return mixed + */ + function _toType($value) { + if (preg_match('/^("(.*)"|\'(.*)\')/',$value,$matches)) { + $value = (string)preg_replace('/(\'\'|\\\\\')/',"'",end($matches)); + $value = preg_replace('/\\\\"/','"',$value); + } elseif (preg_match('/^\\[(.+)\\]$/',$value,$matches)) { + // Inline Sequence + + // Take out strings sequences and mappings + $explode = $this->_inlineEscape($matches[1]); + + // Propogate value array + $value = array(); + foreach ($explode as $v) { + $value[] = $this->_toType($v); + } + } elseif (strpos($value,': ')!==false && !preg_match('/^{(.+)/',$value)) { + // It's a map + $array = explode(': ',$value); + $key = trim($array[0]); + array_shift($array); + $value = trim(implode(': ',$array)); + $value = $this->_toType($value); + $value = array($key => $value); + } elseif (preg_match("/{(.+)}$/",$value,$matches)) { + // Inline Mapping + + // Take out strings sequences and mappings + $explode = $this->_inlineEscape($matches[1]); + + // Propogate value array + $array = array(); + foreach ($explode as $v) { + $array = $array + $this->_toType($v); + } + $value = $array; + } elseif (strtolower($value) == 'null' or $value == '' or $value == '~') { + $value = NULL; + } elseif (ctype_digit($value)) { + $value = (int)$value; + } elseif (in_array(strtolower($value), + array('true', 'on', '+', 'yes', 'y'))) { + $value = TRUE; + } elseif (in_array(strtolower($value), + array('false', 'off', '-', 'no', 'n'))) { + $value = FALSE; + } elseif (is_numeric($value)) { + $value = (float)$value; + } else { + // Just a normal string, right? + $value = trim(preg_replace('/#(.+)$/','',$value)); + } + + return $value; + } + + /** + * Used in inlines to check for more inlines or quoted strings + * @access private + * @return array + */ + function _inlineEscape($inline) { + // There's gotta be a cleaner way to do this... + // While pure sequences seem to be nesting just fine, + // pure mappings and mappings with sequences inside can't go very + // deep. This needs to be fixed. + + // Check for strings + $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; + if (preg_match_all($regex,$inline,$strings)) { + $saved_strings[] = $strings[0][0]; + $inline = preg_replace($regex,'YAMLString',$inline); + } + unset($regex); + + // Check for sequences + if (preg_match_all('/\[(.+)\]/U',$inline,$seqs)) { + $inline = preg_replace('/\[(.+)\]/U','YAMLSeq',$inline); + $seqs = $seqs[0]; + } + + // Check for mappings + if (preg_match_all('/{(.+)}/U',$inline,$maps)) { + $inline = preg_replace('/{(.+)}/U','YAMLMap',$inline); + $maps = $maps[0]; + } + + $explode = explode(', ',$inline); + + // Re-add the strings + if (!empty($saved_strings)) { + $i = 0; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLString')) { + $explode[$key] = str_replace('YAMLString',$saved_strings[$i],$value); + ++$i; + } + } + } + + // Re-add the sequences + if (!empty($seqs)) { + $i = 0; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLSeq') !== false) { + $explode[$key] = str_replace('YAMLSeq',$seqs[$i],$value); + ++$i; + } + } + } + + // Re-add the mappings + if (!empty($maps)) { + $i = 0; + foreach ($explode as $key => $value) { + if (strpos($value,'YAMLMap') !== false) { + $explode[$key] = str_replace('YAMLMap',$maps[$i],$value); + ++$i; + } + } + } + + return $explode; + } + + /** + * Builds the PHP array from all the YAML nodes we've gathered + * @access private + * @return array + */ + function _buildArray() { + $trunk = array(); + + if (!isset($this->_indentSort[0])) { + return $trunk; + } + + foreach ($this->_indentSort[0] as $n) { + if (empty($n->parent)) { + $this->_nodeArrayizeData($n); + // Check for references and copy the needed data to complete them. + $this->_makeReferences($n); + // Merge our data with the big array we're building + $trunk = $this->_array_kmerge($trunk,$n->data); + } + } + + return $trunk; + } + + /** + * Traverses node-space and sets references (& and *) accordingly + * @access private + * @return bool + */ + function _linkReferences() { + if (is_array($this->_haveRefs)) { + foreach ($this->_haveRefs as $node) { + if (!empty($node->data)) { + $key = key($node->data); + // If it's an array, don't check. + if (is_array($node->data[$key])) { + foreach ($node->data[$key] as $k => $v) { + $this->_linkRef($node,$key,$k,$v); + } + } else { + $this->_linkRef($node,$key); + } + } + } + } + return true; + } + + function _linkRef(&$n,$key,$k = NULL,$v = NULL) { + if (empty($k) && empty($v)) { + // Look for &refs + if (preg_match('/^&([^ ]+)/',$n->data[$key],$matches)) { + // Flag the node so we know it's a reference + $this->_allNodes[$n->id]->ref = substr($matches[0],1); + $this->_allNodes[$n->id]->data[$key] = + substr($n->data[$key],strlen($matches[0])+1); + // Look for *refs + } elseif (preg_match('/^\*([^ ]+)/',$n->data[$key],$matches)) { + $ref = substr($matches[0],1); + // Flag the node as having a reference + $this->_allNodes[$n->id]->refKey = $ref; + } + } elseif (!empty($k) && !empty($v)) { + if (preg_match('/^&([^ ]+)/',$v,$matches)) { + // Flag the node so we know it's a reference + $this->_allNodes[$n->id]->ref = substr($matches[0],1); + $this->_allNodes[$n->id]->data[$key][$k] = + substr($v,strlen($matches[0])+1); + // Look for *refs + } elseif (preg_match('/^\*([^ ]+)/',$v,$matches)) { + $ref = substr($matches[0],1); + // Flag the node as having a reference + $this->_allNodes[$n->id]->refKey = $ref; + } + } + } + + /** + * Finds the children of a node and aids in the building of the PHP array + * @access private + * @param int $nid The id of the node whose children we're gathering + * @return array + */ + function _gatherChildren($nid) { + $return = array(); + $node =& $this->_allNodes[$nid]; + foreach ($this->_allNodes as $z) { + if ($z->parent == $node->id) { + // We found a child + $this->_nodeArrayizeData($z); + // Check for references + $this->_makeReferences($z); + // Merge with the big array we're returning + // The big array being all the data of the children of our parent node + $return = $this->_array_kmerge($return,$z->data); + } + } + return $return; + } + + /** + * Turns a node's data and its children's data into a PHP array + * + * @access private + * @param array $node The node which you want to arrayize + * @return boolean + */ + function _nodeArrayizeData(&$node) { + if (is_array($node->data) && $node->children == true) { + // This node has children, so we need to find them + $childs = $this->_gatherChildren($node->id); + // We've gathered all our children's data and are ready to use it + $key = key($node->data); + $key = empty($key) ? 0 : $key; + // If it's an array, add to it of course + if (is_array($node->data[$key])) { + $node->data[$key] = $this->_array_kmerge($node->data[$key],$childs); + } else { + $node->data[$key] = $childs; + } + } elseif (!is_array($node->data) && $node->children == true) { + // Same as above, find the children of this node + $childs = $this->_gatherChildren($node->id); + $node->data = array(); + $node->data[] = $childs; + } + + // We edited $node by reference, so just return true + return true; + } + + /** + * Traverses node-space and copies references to / from this object. + * @access private + * @param object $z A node whose references we wish to make real + * @return bool + */ + function _makeReferences(&$z) { + // It is a reference + if (isset($z->ref)) { + $key = key($z->data); + // Copy the data to this object for easy retrieval later + $this->ref[$z->ref] =& $z->data[$key]; + // It has a reference + } elseif (isset($z->refKey)) { + if (isset($this->ref[$z->refKey])) { + $key = key($z->data); + // Copy the data from this object to make the node a real reference + $z->data[$key] =& $this->ref[$z->refKey]; + } + } + return true; + } + + + /** + * Merges arrays and maintains numeric keys. + * + * An ever-so-slightly modified version of the array_kmerge() function posted + * to php.net by mail at nospam dot iaindooley dot com on 2004-04-08. + * + * http://us3.php.net/manual/en/function.array-merge.php#41394 + * + * @access private + * @param array $arr1 + * @param array $arr2 + * @return array + */ + function _array_kmerge($arr1,$arr2) { + if(!is_array($arr1)) + $arr1 = array(); + + if(!is_array($arr2)) + $arr2 = array(); + + $keys1 = array_keys($arr1); + $keys2 = array_keys($arr2); + $keys = array_merge($keys1,$keys2); + $vals1 = array_values($arr1); + $vals2 = array_values($arr2); + $vals = array_merge($vals1,$vals2); + $ret = array(); + + foreach($keys as $key) { + list($unused,$val) = each($vals); + // This is the good part! If a key already exists, but it's part of a + // sequence (an int), just keep addin numbers until we find a fresh one. + if (isset($ret[$key]) and is_int($key)) { + while (array_key_exists($key, $ret)) { + $key++; + } + } + $ret[$key] = $val; + } + + return $ret; + } + } +?> \ No newline at end of file diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php new file mode 100644 index 00000000..33fb67fd --- /dev/null +++ b/includes/api/ApiHelp.php @@ -0,0 +1,55 @@ + + * + * 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 ('ApiBase.php'); +} + +class ApiHelp extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + /** + * Stub module for displaying help when no parameters are given + */ + public function execute() { + $this->dieUsage('', 'help'); + } + + protected function getDescription() { + return array ( + 'Display this help screen.' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiHelp.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php new file mode 100644 index 00000000..2aa571c1 --- /dev/null +++ b/includes/api/ApiLogin.php @@ -0,0 +1,122 @@ + + * + * 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 ('ApiBase.php'); +} + +class ApiLogin extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action, 'lg'); + } + + public function execute() { + $name = $password = $domain = null; + extract($this->extractRequestParams()); + + $params = new FauxRequest(array ( + 'wpName' => $name, + 'wpPassword' => $password, + 'wpDomain' => $domain, + 'wpRemember' => '' + )); + + $result = array (); + + $loginForm = new LoginForm($params); + switch ($loginForm->authenticateUserData()) { + case LoginForm :: SUCCESS : + global $wgUser; + + $wgUser->setOption('rememberpassword', 1); + $wgUser->setCookies(); + + $result['result'] = 'Success'; + $result['lguserid'] = $_SESSION['wsUserID']; + $result['lgusername'] = $_SESSION['wsUserName']; + $result['lgtoken'] = $_SESSION['wsToken']; + break; + + case LoginForm :: NO_NAME : + $result['result'] = 'NoName'; + break; + case LoginForm :: ILLEGAL : + $result['result'] = 'Illegal'; + break; + case LoginForm :: WRONG_PLUGIN_PASS : + $result['result'] = 'WrongPluginPass'; + break; + case LoginForm :: NOT_EXISTS : + $result['result'] = 'NotExists'; + break; + case LoginForm :: WRONG_PASS : + $result['result'] = 'WrongPass'; + break; + case LoginForm :: EMPTY_PASS : + $result['result'] = 'EmptyPass'; + break; + default : + ApiBase :: dieDebug(__METHOD__, 'Unhandled case value'); + } + + $this->getResult()->addValue(null, 'login', $result); + } + + protected function getAllowedParams() { + return array ( + 'name' => '', + 'password' => '', + 'domain' => null + ); + } + + protected function getParamDescription() { + return array ( + 'name' => 'User Name', + 'password' => 'Password', + 'domain' => 'Domain (optional)' + ); + } + + protected function getDescription() { + return array ( + 'This module is used to login and get the authentication tokens.' + ); + } + + protected function getExamples() { + return array( + 'api.php?action=login&lgname=user&lgpassword=password' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiLogin.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?> diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php new file mode 100644 index 00000000..046d7d7c --- /dev/null +++ b/includes/api/ApiMain.php @@ -0,0 +1,226 @@ + + * + * 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 ('ApiBase.php'); +} + +class ApiMain extends ApiBase { + + private $mPrinter, $mModules, $mModuleNames, $mFormats, $mFormatNames; + private $mApiStartTime, $mResult, $mShowVersions, $mEnableWrite; + + /** + * Constructor + * $apiStartTime - time of the originating call for profiling purposes + * $modules - an array of actions (keys) and classes that handle them (values) + */ + public function __construct($apiStartTime, $modules, $formats, $enableWrite) { + // 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->mResult = new ApiResult($this); + $this->mShowVersions = false; + $this->mEnableWrite = $enableWrite; + } + + public function & getResult() { + return $this->mResult; + } + + public function getShowVersions() { + return $this->mShowVersions; + } + + public function requestWriteMode() { + if (!$this->mEnableWrite) + $this->dieUsage('Editing of this site is disabled. Make sure the $wgEnableWriteAPI=true; ' . + '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(); + $action = $format = $version = null; + try { + 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) { + + // 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); + $this->printResult(true); + + } + $this->profileOut(); + } + + /** + * Internal printer + */ + private function printResult($isError) { + $printer = $this->mPrinter; + $printer->profileIn(); + $printer->initPrinter($isError); + if (!$printer->getNeedsRawData()) + $this->getResult()->SanitizeData(); + $printer->execute(); + $printer->closePrinter(); + $printer->profileOut(); + } + + protected function getDescription() { + return array ( + '', + 'This API allows programs to access various functions of MediaWiki software.', + 'For more details see API Home Page @ http://meta.wikimedia.org/wiki/API', + '' + ); + } + + public function mainDieUsage($description, $errorCode, $httpRespCode = 0) { + $this->mResult->Reset(); + if ($httpRespCode === 0) + header($errorCode, true); + else + header($errorCode, true, $httpRespCode); + + $data = array ( + 'code' => $errorCode, + 'info' => $description + ); + ApiResult :: setContent($data, $this->makeHelpMsg()); + $this->mResult->addValue(null, 'error', $data); + + throw new UsageException($description, $errorCode); + } + + /** + * Override the parent to generate help messages for all available modules. + */ + public function makeHelpMsg() { + + // Use parent to make default message for the main module + $msg = parent :: makeHelpMsg(); + + $astriks = str_repeat('*** ', 10); + $msg .= "\n\n$astriks Modules $astriks\n\n"; + foreach ($this->mModules as $moduleName => $moduleClass) { + $msg .= "* action=$moduleName *"; + $module = new $this->mModules[$moduleName] ($this, $moduleName); + $msg2 = $module->makeHelpMsg(); + if ($msg2 !== false) + $msg .= $msg2; + $msg .= "\n"; + } + + $msg .= "\n$astriks Formats $astriks\n\n"; + foreach ($this->mFormats as $moduleName => $moduleClass) { + $msg .= "* format=$moduleName *"; + $module = new $this->mFormats[$moduleName] ($this, $moduleName); + $msg2 = $module->makeHelpMsg(); + if ($msg2 !== false) + $msg .= $msg2; + $msg .= "\n"; + } + + return $msg; + } + + private $mIsBot = null; + public function isBot() { + if (!isset ($this->mIsBot)) { + global $wgUser; + $this->mIsBot = $wgUser->isAllowed('bot'); + } + return $this->mIsBot; + } + + public function getVersion() { + $vers = array (); + $vers[] = __CLASS__ . ': $Id: ApiMain.php 16820 2006-10-06 01:02:14Z yurik $'; + $vers[] = ApiBase :: getBaseVersion(); + $vers[] = ApiFormatBase :: getBaseVersion(); + $vers[] = ApiQueryBase :: getBaseVersion(); + return $vers; + } +} + +/** +* @desc This exception will be thrown when dieUsage is called to stop module execution. +*/ +class UsageException extends Exception { + + private $codestr; + + public function __construct($message, $codestr) { + parent :: __construct($message); + $this->codestr = $codestr; + } + public function __toString() { + return "{$this->codestr}: {$this->message}"; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php new file mode 100644 index 00000000..d2384b39 --- /dev/null +++ b/includes/api/ApiPageSet.php @@ -0,0 +1,514 @@ + + * + * 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 ('ApiQueryBase.php'); +} + +class ApiPageSet extends ApiQueryBase { + + private $mAllPages; // [ns][dbkey] => page_id or 0 when missing + private $mGoodTitles, $mMissingTitles, $mMissingPageIDs, $mRedirectTitles, $mNormalizedTitles; + private $mResolveRedirects, $mPendingRedirectIDs; + + private $mRequestedPageFields; + + public function __construct($query, $resolveRedirects = false) { + parent :: __construct($query, __CLASS__); + + $this->mAllPages = array (); + $this->mGoodTitles = array (); + $this->mMissingTitles = array (); + $this->mMissingPageIDs = array (); + $this->mRedirectTitles = array (); + $this->mNormalizedTitles = array (); + + $this->mRequestedPageFields = array (); + $this->mResolveRedirects = $resolveRedirects; + if($resolveRedirects) + $this->mPendingRedirectIDs = array(); + } + + public function isResolvingRedirects() { + return $this->mResolveRedirects; + } + + public function requestField($fieldName) { + $this->mRequestedPageFields[$fieldName] = null; + } + + public function getCustomField($fieldName) { + return $this->mRequestedPageFields[$fieldName]; + } + + /** + * Get fields that modules have requested from the page table + */ + public function getPageTableFields() { + // Ensure we get minimum required fields + $pageFlds = array ( + 'page_id' => null, + 'page_namespace' => null, + 'page_title' => null + ); + + // only store non-default fields + $this->mRequestedPageFields = array_diff_key($this->mRequestedPageFields, $pageFlds); + + if ($this->mResolveRedirects) + $pageFlds['page_is_redirect'] = null; + + return array_keys(array_merge($pageFlds, $this->mRequestedPageFields)); + } + + /** + * Title objects that were found in the database. + * @return array page_id (int) => Title (obj) + */ + public function getGoodTitles() { + return $this->mGoodTitles; + } + + /** + * Returns the number of unique pages (not revisions) in the set. + */ + public function getGoodTitleCount() { + return count($this->getGoodTitles()); + } + + /** + * Title objects that were NOT found in the database. + * @return array of Title objects + */ + public function getMissingTitles() { + return $this->mMissingTitles; + } + + /** + * Page IDs that were not found in the database + * @return array of page IDs + */ + public function getMissingPageIDs() { + return $this->mMissingPageIDs; + } + + /** + * Get a list of redirects when doing redirect resolution + * @return array prefixed_title (string) => prefixed_title (string) + */ + public function getRedirectTitles() { + return $this->mRedirectTitles; + } + + /** + * Get a list of title normalizations - maps the title given + * with its normalized version. + * @return array raw_prefixed_title (string) => prefixed_title (string) + */ + public function getNormalizedTitles() { + return $this->mNormalizedTitles; + } + + /** + * Get the list of revision IDs (requested with revids= parameter) + */ + public function getRevisionIDs() { + $this->dieUsage(__METHOD__ . ' is not implemented', 'notimplemented'); + } + + /** + * Returns the number of revisions (requested with revids= parameter) + */ + public function getRevisionCount() { + return 0; // TODO: implement + } + + /** + * Populate from the request parameters + */ + public function execute() { + $this->profileIn(); + $titles = $pageids = $revids = null; + extract($this->extractRequestParams()); + + // Only one of the titles/pageids/revids is allowed at the same time + $dataSource = null; + if (isset ($titles)) + $dataSource = 'titles'; + if (isset ($pageids)) { + if (isset ($dataSource)) + $this->dieUsage("Cannot use 'pageids' at the same time as '$dataSource'", 'multisource'); + $dataSource = 'pageids'; + } + if (isset ($revids)) { + if (isset ($dataSource)) + $this->dieUsage("Cannot use 'revids' at the same time as '$dataSource'", 'multisource'); + $dataSource = 'revids'; + } + + switch ($dataSource) { + case 'titles' : + $this->initFromTitles($titles); + break; + case 'pageids' : + $this->initFromPageIds($pageids); + break; + case 'revids' : + $this->initFromRevIDs($revids); + break; + default : + // Do nothing - some queries do not need any of the data sources. + break; + } + $this->profileOut(); + } + + /** + * Initialize PageSet from a list of Titles + */ + public function populateFromTitles($titles) { + $this->profileIn(); + $this->initFromTitles($titles); + $this->profileOut(); + } + + /** + * Initialize PageSet from a list of Page IDs + */ + public function populateFromPageIDs($pageIDs) { + $this->profileIn(); + $pageIDs = array_map('intval', $pageIDs); // paranoia + $this->initFromPageIds($pageIDs); + $this->profileOut(); + } + + /** + * Initialize PageSet from a rowset returned from the database + */ + public function populateFromQueryResult($db, $queryResult) { + $this->profileIn(); + $this->initFromQueryResult($db, $queryResult); + $this->profileOut(); + } + + /** + * Extract all requested fields from the row received from the database + */ + public function processDbRow($row) { + $pageId = intval($row->page_id); + + // Store Title object in various data structures + $title = Title :: makeTitle($row->page_namespace, $row->page_title); + $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId; + + if ($this->mResolveRedirects && $row->page_is_redirect == '1') { + $this->mPendingRedirectIDs[$pageId] = $title; + } else { + $this->mGoodTitles[$pageId] = $title; + } + + foreach ($this->mRequestedPageFields as $fieldName => & $fieldValues) + $fieldValues[$pageId] = $row-> $fieldName; + } + + public function finishPageSetGeneration() { + $this->profileIn(); + $this->resolvePendingRedirects(); + $this->profileOut(); + } + + /** + * This method populates internal variables with page information + * based on the given array of title strings. + * + * Steps: + * #1 For each title, get data from `page` table + * #2 If page was not found in the DB, store it as missing + * + * Additionally, when resolving redirects: + * #3 If no more redirects left, stop. + * #4 For each redirect, get its links from `pagelinks` table. + * #5 Substitute the original LinkBatch object with the new list + * #6 Repeat from step #1 + */ + private function initFromTitles($titles) { + $db = $this->getDB(); + + // Get validated and normalized title objects + $linkBatch = $this->processTitlesStrArray($titles); + $set = $linkBatch->constructSet('page', $db); + + // Get pageIDs data from the `page` table + $this->profileDBIn(); + $res = $db->select('page', $this->getPageTableFields(), $set, __METHOD__); + $this->profileDBOut(); + + // Hack: get the ns:titles stored in array(ns => array(titles)) format + $this->initFromQueryResult($db, $res, $linkBatch->data, true); // process Titles + + // Resolve any found redirects + $this->resolvePendingRedirects(); + } + + private function initFromPageIds($pageids) { + $db = $this->getDB(); + + $set = array ( + 'page_id' => $pageids + ); + + // Get pageIDs data from the `page` table + $this->profileDBIn(); + $res = $db->select('page', $this->getPageTableFields(), $set, __METHOD__); + $this->profileDBOut(); + + $this->initFromQueryResult($db, $res, array_flip($pageids), false); // process PageIDs + + // Resolve any found redirects + $this->resolvePendingRedirects(); + } + + /** + * Iterate through the result of the query on 'page' table, + * and for each row create and store title object and save any extra fields requested. + * @param $db Database + * @param $res DB Query result + * @param $remaining Array of either pageID or ns/title elements (optional). + * If given, any missing items will go to $mMissingPageIDs and $mMissingTitles + * @param $processTitles bool Must be provided together with $remaining. + * If true, treat $remaining as an array of [ns][title] + * If false, treat it as an array of [pageIDs] + * @return Array of redirect IDs (only when resolving redirects) + */ + private function initFromQueryResult($db, $res, &$remaining = null, $processTitles = null) { + if (!is_null($remaining) && is_null($processTitles)) + $this->dieDebug('Missing $processTitles parameter when $remaining is provided'); + + while ($row = $db->fetchObject($res)) { + + $pageId = intval($row->page_id); + + // Remove found page from the list of remaining items + if (isset($remaining)) { + if ($processTitles) + unset ($remaining[$row->page_namespace][$row->page_title]); + else + unset ($remaining[$pageId]); + } + + // Store any extra fields requested by modules + $this->processDbRow($row); + } + $db->freeResult($res); + + if(isset($remaining)) { + // Any items left in the $remaining list are added as missing + if($processTitles) { + // The remaining titles in $remaining are non-existant pages + foreach ($remaining as $ns => $dbkeys) { + foreach ($dbkeys as $dbkey => $nothing) { + $this->mMissingTitles[] = Title :: makeTitle($ns, $dbkey); + $this->mAllPages[$ns][$dbkey] = 0; + } + } + } + else + { + // The remaining pageids do not exist + if(empty($this->mMissingPageIDs)) + $this->mMissingPageIDs = array_keys($remaining); + else + $this->mMissingPageIDs = array_merge($this->mMissingPageIDs, array_keys($remaining)); + } + } + } + + private function initFromRevIDs($revids) { + $this->dieUsage(__METHOD__ . ' is not implemented', 'notimplemented'); + } + + private function resolvePendingRedirects() { + + if($this->mResolveRedirects) { + $db = $this->getDB(); + $pageFlds = $this->getPageTableFields(); + + // Repeat until all redirects have been resolved + // The infinite loop is prevented by keeping all known pages in $this->mAllPages + while (!empty ($this->mPendingRedirectIDs)) { + + // Resolve redirects by querying the pagelinks table, and repeat the process + // Create a new linkBatch object for the next pass + $linkBatch = $this->getRedirectTargets(); + + if ($linkBatch->isEmpty()) + break; + + $set = $linkBatch->constructSet('page', $db); + if(false === $set) + break; + + // Get pageIDs data from the `page` table + $this->profileDBIn(); + $res = $db->select('page', $pageFlds, $set, __METHOD__); + $this->profileDBOut(); + + // Hack: get the ns:titles stored in array(ns => array(titles)) format + $this->initFromQueryResult($db, $res, $linkBatch->data, true); + } + } + } + + private function getRedirectTargets() { + + $linkBatch = new LinkBatch(); + $db = $this->getDB(); + + // find redirect targets for all redirect pages + $this->profileDBIn(); + $res = $db->select('pagelinks', array ( + 'pl_from', + 'pl_namespace', + 'pl_title' + ), array ( + 'pl_from' => array_keys($this->mPendingRedirectIDs + )), __METHOD__); + $this->profileDBOut(); + + while ($row = $db->fetchObject($res)) { + + $plfrom = intval($row->pl_from); + + // Bug 7304 workaround + // ( http://bugzilla.wikipedia.org/show_bug.cgi?id=7304 ) + // A redirect page may have more than one link. + // This code will only use the first link returned. + if (isset ($this->mPendingRedirectIDs[$plfrom])) { // remove line when bug 7304 is fixed + + $titleStrFrom = $this->mPendingRedirectIDs[$plfrom]->getPrefixedText(); + $titleStrTo = Title :: makeTitle($row->pl_namespace, $row->pl_title)->getPrefixedText(); + unset ($this->mPendingRedirectIDs[$plfrom]); // remove line when bug 7304 is fixed + + // Avoid an infinite loop by checking if we have already processed this target + if (!isset ($this->mAllPages[$row->pl_namespace][$row->pl_title])) { + $linkBatch->add($row->pl_namespace, $row->pl_title); + } + } else { + // This redirect page has more than one link. + // This is very slow, but safer until bug 7304 is resolved + $title = Title :: newFromID($plfrom); + $titleStrFrom = $title->getPrefixedText(); + + $article = new Article($title); + $text = $article->getContent(); + $titleTo = Title :: newFromRedirect($text); + $titleStrTo = $titleTo->getPrefixedText(); + + if (is_null($titleStrTo)) + ApiBase :: dieDebug(__METHOD__, 'Bug7304 workaround: redir target from {$title->getPrefixedText()} not found'); + + // Avoid an infinite loop by checking if we have already processed this target + if (!isset ($this->mAllPages[$titleTo->getNamespace()][$titleTo->getDBkey()])) { + $linkBatch->addObj($titleTo); + } + } + + $this->mRedirectTitles[$titleStrFrom] = $titleStrTo; + } + $db->freeResult($res); + + // All IDs must exist in the page table + if (!empty($this->mPendingRedirectIDs[$plfrom])) + $this->dieDebug('Invalid redirect IDs were found'); + + return $linkBatch; + } + + /** + * Given an array of title strings, convert them into Title objects. + * This method validates access rights for the title, + * and appends normalization values to the output. + * + * @return LinkBatch of title objects. + */ + private function processTitlesStrArray($titles) { + + $linkBatch = new LinkBatch(); + + foreach ($titles as $titleString) { + $titleObj = Title :: newFromText($titleString); + + // Validation + if (!$titleObj) + $this->dieUsage("bad title $titleString", 'invalidtitle'); + if ($titleObj->getNamespace() < 0) + $this->dieUsage("No support for special page $titleString has been implemented", 'unsupportednamespace'); + if (!$titleObj->userCanRead()) + $this->dieUsage("No read permission for $titleString", 'titleaccessdenied'); + + $linkBatch->addObj($titleObj); + + // Make sure we remember the original title that was given to us + // This way the caller can correlate new titles with the originally requested, + // i.e. namespace is localized or capitalization is different + if ($titleString !== $titleObj->getPrefixedText()) { + $this->mNormalizedTitles[$titleString] = $titleObj->getPrefixedText(); + } + } + + return $linkBatch; + } + + protected function getAllowedParams() { + return array ( + 'titles' => array ( + ApiBase :: PARAM_ISMULTI => true + ), + 'pageids' => array ( + ApiBase :: PARAM_TYPE => 'integer', + ApiBase :: PARAM_ISMULTI => true + ), + 'revids' => array ( + ApiBase :: PARAM_TYPE => 'integer', + ApiBase :: PARAM_ISMULTI => true + ) + ); + } + + protected function getParamDescription() { + return array ( + 'titles' => 'A list of titles to work on', + 'pageids' => 'A list of page IDs to work on', + 'revids' => 'A list of revision IDs to work on' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiPageSet.php 16820 2006-10-06 01:02:14Z yurik $'; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php new file mode 100644 index 00000000..985bde63 --- /dev/null +++ b/includes/api/ApiQuery.php @@ -0,0 +1,354 @@ + + * + * 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 ('ApiBase.php'); +} + +class ApiQuery extends ApiBase { + + private $mPropModuleNames, $mListModuleNames, $mMetaModuleNames; + private $mPageSet; + + private $mQueryPropModules = array ( + 'info' => 'ApiQueryInfo', + 'revisions' => 'ApiQueryRevisions' + ); + // 'categories' => 'ApiQueryCategories', + // 'imageinfo' => 'ApiQueryImageinfo', + // 'langlinks' => 'ApiQueryLanglinks', + // 'links' => 'ApiQueryLinks', + // 'templates' => 'ApiQueryTemplates', + + private $mQueryListModules = array ( + 'allpages' => 'ApiQueryAllpages' + ); + // 'backlinks' => 'ApiQueryBacklinks', + // 'categorymembers' => 'ApiQueryCategorymembers', + // 'embeddedin' => 'ApiQueryEmbeddedin', + // 'imagelinks' => 'ApiQueryImagelinks', + // 'logevents' => 'ApiQueryLogevents', + // 'recentchanges' => 'ApiQueryRecentchanges', + // 'usercontribs' => 'ApiQueryUsercontribs', + // 'users' => 'ApiQueryUsers', + // 'watchlist' => 'ApiQueryWatchlist', + + private $mQueryMetaModules = array ( + 'siteinfo' => 'ApiQuerySiteinfo' + ); + // 'userinfo' => 'ApiQueryUserinfo', + + private $mSlaveDB = null; + + public function __construct($main, $action) { + parent :: __construct($main, $action); + $this->mPropModuleNames = array_keys($this->mQueryPropModules); + $this->mListModuleNames = array_keys($this->mQueryListModules); + $this->mMetaModuleNames = array_keys($this->mQueryMetaModules); + + // Allow the entire list of modules at first, + // but during module instantiation check if it can be used as a generator. + $this->mAllowedGenerators = array_merge($this->mListModuleNames, $this->mPropModuleNames); + } + + public function getDB() { + if (!isset ($this->mSlaveDB)) + $this->mSlaveDB = & wfGetDB(DB_SLAVE); + return $this->mSlaveDB; + } + + public function getPageSet() { + return $this->mPageSet; + } + + /** + * Query execution happens in the following steps: + * #1 Create a PageSet object with any pages requested by the user + * #2 If using generator, execute it to get a new PageSet object + * #3 Instantiate all requested modules. + * This way the PageSet object will know what shared data is required, + * and minimize DB calls. + * #4 Output all normalization and redirect resolution information + * #5 Execute all requested modules + */ + public function execute() { + $prop = $list = $meta = $generator = $redirects = null; + extract($this->extractRequestParams()); + + // + // Create PageSet + // + $this->mPageSet = new ApiPageSet($this, $redirects); + + // Instantiate required modules + $modules = array (); + if (isset ($prop)) + foreach ($prop as $moduleName) + $modules[] = new $this->mQueryPropModules[$moduleName] ($this, $moduleName); + if (isset ($list)) + foreach ($list as $moduleName) + $modules[] = new $this->mQueryListModules[$moduleName] ($this, $moduleName); + if (isset ($meta)) + foreach ($meta as $moduleName) + $modules[] = new $this->mQueryMetaModules[$moduleName] ($this, $moduleName); + + // Modules may optimize data requests through the $this->getPageSet() object + // Execute all requested modules. + foreach ($modules as $module) { + $module->requestExtraData(); + } + + // + // If given, execute generator to substitute user supplied data with generated data. + // + if (isset ($generator)) + $this->executeGeneratorModule($generator, $redirects); + + // + // Populate page information for the given pageSet + // + $this->mPageSet->execute(); + + // + // Record page information (title, namespace, if exists, etc) + // + $this->outputGeneralPageInfo(); + + // + // Execute all requested modules. + // + foreach ($modules as $module) { + $module->profileIn(); + $module->execute(); + $module->profileOut(); + } + } + + private function outputGeneralPageInfo() { + + $pageSet = $this->getPageSet(); + + // Title normalizations + $normValues = array (); + foreach ($pageSet->getNormalizedTitles() as $rawTitleStr => $titleStr) { + $normValues[] = array ( + 'from' => $rawTitleStr, + 'to' => $titleStr + ); + } + + if (!empty ($normValues)) { + ApiResult :: setIndexedTagName($normValues, 'n'); + $this->getResult()->addValue('query', 'normalized', $normValues); + } + + // Show redirect information + $redirValues = array (); + foreach ($pageSet->getRedirectTitles() as $titleStrFrom => $titleStrTo) { + $redirValues[] = array ( + 'from' => $titleStrFrom, + 'to' => $titleStrTo + ); + } + + if (!empty ($redirValues)) { + ApiResult :: setIndexedTagName($redirValues, 'r'); + $this->getResult()->addValue('query', 'redirects', $redirValues); + } + + // + // Page elements + // + $pages = array (); + + // Report any missing titles + $fakepageid = -1; + foreach ($pageSet->getMissingTitles() as $title) { + $pages[$fakepageid--] = array ( + 'ns' => $title->getNamespace(), 'title' => $title->getPrefixedText(), 'missing' => ''); + } + + // Report any missing page ids + foreach ($pageSet->getMissingPageIDs() as $pageid) { + $pages[$pageid] = array ( + 'id' => $pageid, + 'missing' => '' + ); + } + + // Output general page information for found titles + foreach ($pageSet->getGoodTitles() as $pageid => $title) { + $pages[$pageid] = array ( + 'ns' => $title->getNamespace(), 'title' => $title->getPrefixedText(), 'id' => $pageid); + } + + if (!empty ($pages)) { + ApiResult :: setIndexedTagName($pages, 'page'); + $this->getResult()->addValue('query', 'pages', $pages); + } + } + + protected function executeGeneratorModule($generatorName, $redirects) { + + // Find class that implements requested generator + if (isset ($this->mQueryListModules[$generatorName])) { + $className = $this->mQueryListModules[$generatorName]; + } + elseif (isset ($this->mQueryPropModules[$generatorName])) { + $className = $this->mQueryPropModules[$generatorName]; + } else { + ApiBase :: dieDebug(__METHOD__, "Unknown generator=$generatorName"); + } + + // Use current pageset as the result, and create a new one just for the generator + $resultPageSet = $this->mPageSet; + $this->mPageSet = new ApiPageSet($this, $redirects); + + // Create and execute the generator + $generator = new $className ($this, $generatorName); + if (!$generator instanceof ApiQueryGeneratorBase) + $this->dieUsage("Module $generatorName cannot be used as a generator", "badgenerator"); + + $generator->setGeneratorMode(); + $generator->requestExtraData(); + + // execute current pageSet to get the data for the generator module + $this->mPageSet->execute(); + + // populate resultPageSet with the generator output + $generator->profileIn(); + $generator->executeGenerator($resultPageSet); + $resultPageSet->finishPageSetGeneration(); + $generator->profileOut(); + + // Swap the resulting pageset back in + $this->mPageSet = $resultPageSet; + } + + protected function getAllowedParams() { + return array ( + 'prop' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => $this->mPropModuleNames + ), + 'list' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => $this->mListModuleNames + ), + 'meta' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => $this->mMetaModuleNames + ), + 'generator' => array ( + ApiBase :: PARAM_TYPE => $this->mAllowedGenerators + ), + 'redirects' => false + ); + } + + /** + * Override the parent to generate help messages for all available query modules. + */ + public function makeHelpMsg() { + + // Use parent to make default message for the query module + $msg = parent :: makeHelpMsg(); + + // Make sure the internal object is empty + // (just in case a sub-module decides to optimize during instantiation) + $this->mPageSet = null; + + $astriks = str_repeat('--- ', 8); + $msg .= "\n$astriks Query: Prop $astriks\n\n"; + $msg .= $this->makeHelpMsgHelper($this->mQueryPropModules, 'prop'); + $msg .= "\n$astriks Query: List $astriks\n\n"; + $msg .= $this->makeHelpMsgHelper($this->mQueryListModules, 'list'); + $msg .= "\n$astriks Query: Meta $astriks\n\n"; + $msg .= $this->makeHelpMsgHelper($this->mQueryMetaModules, 'meta'); + + return $msg; + } + + private function makeHelpMsgHelper($moduleList, $paramName) { + + $moduleDscriptions = array (); + + foreach ($moduleList as $moduleName => $moduleClass) { + $msg = "* $paramName=$moduleName *"; + $module = new $moduleClass ($this, $moduleName, null); + $msg2 = $module->makeHelpMsg(); + if ($msg2 !== false) + $msg .= $msg2; + if ($module instanceof ApiQueryGeneratorBase) + $msg .= "Generator:\n This module may be used as a generator\n"; + $moduleDscriptions[] = $msg; + } + + return implode("\n", $moduleDscriptions); + } + + /** + * Override to add extra parameters from PageSet + */ + public function makeHelpMsgParameters() { + $psModule = new ApiPageSet($this); + return $psModule->makeHelpMsgParameters() . parent :: makeHelpMsgParameters(); + } + + protected function getParamDescription() { + return array ( + 'prop' => 'Which properties to get for the titles/revisions/pageids', + 'list' => 'Which lists to get', + 'meta' => 'Which meta data to get about the site', + 'generator' => 'Use the output of a list as the input for other prop/list/meta items', + 'redirects' => 'Automatically resolve redirects' + ); + } + + protected function getDescription() { + return array ( + 'Query API module allows applications to get needed pieces of data from the MediaWiki databases,', + 'and is loosely based on the Query API interface currently available on all MediaWiki servers.', + 'All data modifications will first have to use query to acquire a token to prevent abuse from malicious sites.' + ); + } + + protected function getExamples() { + return array ( + 'api.php?action=query&prop=revisions&meta=siteinfo&titles=Main%20Page&rvprop=user|comment' + ); + } + + public function getVersion() { + $psModule = new ApiPageSet($this); + $vers = array (); + $vers[] = __CLASS__ . ': $Id: ApiQuery.php 16820 2006-10-06 01:02:14Z yurik $'; + $vers[] = $psModule->getVersion(); + return $vers; + } +} +?> diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php new file mode 100644 index 00000000..51330d62 --- /dev/null +++ b/includes/api/ApiQueryAllpages.php @@ -0,0 +1,183 @@ + + * + * 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 ('ApiQueryBase.php'); +} + +class ApiQueryAllpages extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'ap'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + if ($resultPageSet->isResolvingRedirects()) + $this->dieUsage('Use "gapfilterredir=nonredirects" option instead of "redirects" when using allpages as a generator', 'params'); + + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + $limit = $from = $namespace = $filterredir = null; + extract($this->extractRequestParams()); + + $db = $this->getDB(); + + $where = array ( + 'page_namespace' => $namespace + ); + + if (isset ($from)) { + $where[] = 'page_title>=' . $db->addQuotes(ApiQueryBase :: titleToKey($from)); + } + + if ($filterredir === 'redirects') { + $where['page_is_redirect'] = 1; + } + elseif ($filterredir === 'nonredirects') { + $where['page_is_redirect'] = 0; + } + + if (is_null($resultPageSet)) { + $fields = array ( + 'page_id', + 'page_namespace', + 'page_title' + ); + } else { + $fields = $resultPageSet->getPageTableFields(); + } + + $this->profileDBIn(); + $res = $db->select('page', $fields, $where, __CLASS__ . '::' . __METHOD__, array ( + 'USE INDEX' => 'name_title', + 'LIMIT' => $limit +1, + 'ORDER BY' => 'page_namespace, page_title' + )); + $this->profileDBOut(); + + $data = array (); + $count = 0; + while ($row = $db->fetchObject($res)) { + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $msg = array ( + 'continue' => $this->encodeParamName('from' + ) . '=' . ApiQueryBase :: keyToTitle($row->page_title)); + $this->getResult()->addValue('query-status', 'allpages', $msg); + break; + } + + $title = Title :: makeTitle($row->page_namespace, $row->page_title); + // skip any pages that user has no rights to read + if ($title->userCanRead()) { + + if (is_null($resultPageSet)) { + $id = intval($row->page_id); + $data[] = $id; // in generator mode, just assemble a list of page IDs. + } else { + $resultPageSet->processDbRow($row); + } + } + } + $db->freeResult($res); + + if (is_null($resultPageSet)) { + ApiResult :: setIndexedTagName($data, 'p'); + $this->getResult()->addValue('query', 'allpages', $data); + } + } + + protected function getAllowedParams() { + + global $wgContLang; + $validNamespaces = array (); + foreach (array_keys($wgContLang->getNamespaces()) as $ns) { + if ($ns >= 0) + $validNamespaces[] = $ns; // strval($ns); + } + + return array ( + 'from' => null, + 'namespace' => array ( + ApiBase :: PARAM_DFLT => 0, + ApiBase :: PARAM_TYPE => $validNamespaces + ), + 'filterredir' => array ( + ApiBase :: PARAM_DFLT => 'all', + ApiBase :: PARAM_TYPE => array ( + 'all', + 'redirects', + 'nonredirects' + ) + ), + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX1 => 500, + ApiBase :: PARAM_MAX2 => 5000 + ) + ); + } + + protected function getParamDescription() { + return array ( + 'from' => 'The page title to start enumerating from.', + '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' + ); + } + + protected function getDescription() { + return 'Enumerate all pages sequentially in a given namespace'; + } + + protected function getExamples() { + return array ( + 'Simple Use', + ' api.php?action=query&list=allpages', + ' api.php?action=query&list=allpages&apfrom=B&aplimit=5', + 'Using as Generator', + ' Show info about 4 pages starting at the letter "T"', + ' api.php?action=query&generator=allpages&gaplimit=4&gapfrom=T&prop=info', + ' Show content of first 2 non-redirect pages begining at "Re"', + ' api.php?action=query&generator=allpages&gaplimit=2&gapfilterredir=nonredirects&gapfrom=Re&prop=revisions&rvprop=content' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryAllpages.php 16820 2006-10-06 01:02:14Z yurik $'; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php new file mode 100644 index 00000000..574f742e --- /dev/null +++ b/includes/api/ApiQueryBase.php @@ -0,0 +1,112 @@ + + * + * 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 ('ApiBase.php'); +} + +abstract class ApiQueryBase extends ApiBase { + + private $mQueryModule; + + public function __construct($query, $moduleName, $paramPrefix = '') { + parent :: __construct($query->getMain(), $moduleName, $paramPrefix); + $this->mQueryModule = $query; + } + + /** + * Override this method to request extra fields from the pageSet + * using $this->getPageSet()->requestField('fieldName') + */ + public function requestExtraData() { + } + + /** + * Get the main Query module + */ + public function getQuery() { + return $this->mQueryModule; + } + + /** + * Get the Query database connection (readonly) + */ + protected function getDB() { + return $this->getQuery()->getDB(); + } + + /** + * Get the PageSet object to work on + * @return ApiPageSet data + */ + protected function getPageSet() { + return $this->mQueryModule->getPageSet(); + } + + public static function titleToKey($title) { + return str_replace(' ', '_', $title); + } + + public static function keyToTitle($key) { + return str_replace('_', ' ', $key); + } + + public static function getBaseVersion() { + return __CLASS__ . ': $Id: ApiQueryBase.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} + +abstract class ApiQueryGeneratorBase extends ApiQueryBase { + + private $mIsGenerator; + + public function __construct($query, $moduleName, $paramPrefix = '') { + parent :: __construct($query, $moduleName, $paramPrefix); + $mIsGenerator = false; + } + + public function setGeneratorMode() { + $this->mIsGenerator = true; + } + + /** + * Overrides base class to prepend 'g' to every generator parameter + */ + public function encodeParamName($paramName) { + if ($this->mIsGenerator) + return 'g' . parent :: encodeParamName($paramName); + else + return parent :: encodeParamName($paramName); + } + + /** + * Execute this module as a generator + * @param $resultPageSet PageSet: All output should be appended to this object + */ + public abstract function executeGenerator($resultPageSet); +} +?> \ No newline at end of file diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php new file mode 100644 index 00000000..de651b00 --- /dev/null +++ b/includes/api/ApiQueryInfo.php @@ -0,0 +1,82 @@ + + * + * 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 ('ApiQueryBase.php'); +} + +class ApiQueryInfo extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName); + } + + public function requestExtraData() { + $pageSet = $this->getPageSet(); + $pageSet->requestField('page_is_redirect'); + $pageSet->requestField('page_touched'); + $pageSet->requestField('page_latest'); + } + + public function execute() { + + $pageSet = $this->getPageSet(); + $titles = $pageSet->getGoodTitles(); + $result = & $this->getResult(); + + $pageIsRedir = $pageSet->getCustomField('page_is_redirect'); + $pageTouched = $pageSet->getCustomField('page_touched'); + $pageLatest = $pageSet->getCustomField('page_latest'); + + foreach ($titles as $pageid => $title) { + $pageInfo = array ('touched' => $pageTouched[$pageid], 'lastrevid' => $pageLatest[$pageid]); + + if ($pageIsRedir[$pageid]) + $pageInfo['redirect'] = ''; + + $result->addValue(array ( + 'query', + 'pages' + ), $pageid, $pageInfo); + } + } + + protected function getDescription() { + return 'Get basic page information such as namespace, title, last touched date, ...'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&prop=info&titles=Main%20Page' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryInfo.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php new file mode 100644 index 00000000..f6097bad --- /dev/null +++ b/includes/api/ApiQueryRevisions.php @@ -0,0 +1,320 @@ + + * + * 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 ('ApiQueryBase.php'); +} + +class ApiQueryRevisions extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'rv'); + } + + public function execute() { + $limit = $startid = $endid = $start = $end = $dir = $prop = null; + extract($this->extractRequestParams()); + + $db = $this->getDB(); + + // true when ordered by timestamp from older to newer, false otherwise + $dirNewer = ($dir === 'newer'); + + // If any of those parameters are used, work in 'enumeration' mode. + // Enum mode can only be used when exactly one page is provided. + // Enumerating revisions on multiple pages make it extremelly + // difficult to manage continuations and require additional sql indexes + $enumRevMode = ($limit !== 0 || $startid !== 0 || $endid !== 0 || $dirNewer || isset ($start) || isset ($end)); + + $pageSet = $this->getPageSet(); + $pageCount = $pageSet->getGoodTitleCount(); + $revCount = $pageSet->getRevisionCount(); + + // Optimization -- nothing to do + if ($revCount === 0 && $pageCount === 0) + return; + + if ($revCount > 0 && $pageCount > 0) + $this->dieUsage('The revids= parameter may not be used with titles, pageids, or generator options.', 'revids'); + + if ($revCount > 0 && $enumRevMode) + $this->dieUsage('The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).', 'revids'); + + if ($revCount === 0 && $pageCount > 1 && $enumRevMode) + $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, start, and end parameters may only be used on a single page.', 'multpages'); + + $tables = array ( + 'revision' + ); + $fields = array ( + 'rev_id', + 'rev_page', + 'rev_text_id', + 'rev_minor_edit' + ); + $conds = array ( + 'rev_deleted' => 0 + ); + $options = array (); + + $showTimestamp = $showUser = $showComment = $showContent = false; + if (isset ($prop)) { + foreach ($prop as $p) { + switch ($p) { + case 'timestamp' : + $fields[] = 'rev_timestamp'; + $showTimestamp = true; + break; + case 'user' : + $fields[] = 'rev_user'; + $fields[] = 'rev_user_text'; + $showUser = true; + break; + case 'comment' : + $fields[] = 'rev_comment'; + $showComment = true; + break; + case 'content' : + $tables[] = 'text'; + $conds[] = 'rev_text_id=old_id'; + $fields[] = 'old_id'; + $fields[] = 'old_text'; + $fields[] = 'old_flags'; + $showContent = true; + break; + default : + ApiBase :: dieDebug(__METHOD__, "unknown prop $p"); + } + } + } + + $userMax = ($showContent ? 50 : 500); + $botMax = ($showContent ? 200 : 10000); + + if ($enumRevMode) { + + // This is mostly to prevent parameter errors (and optimize sql?) + if ($startid !== 0 && isset ($start)) + $this->dieUsage('start and startid cannot be used together', 'badparams'); + + if ($endid !== 0 && isset ($end)) + $this->dieUsage('end and endid cannot be used together', 'badparams'); + + // This code makes an assumption that sorting by rev_id and rev_timestamp produces + // the same result. This way users may request revisions starting at a given time, + // but to page through results use the rev_id returned after each page. + // Switching to rev_id removes the potential problem of having more than + // one row with the same timestamp for the same page. + // The order needs to be the same as start parameter to avoid SQL filesort. + $options['ORDER BY'] = ($startid !== 0 ? 'rev_id' : 'rev_timestamp') . ($dirNewer ? '' : ' DESC'); + + $before = ($dirNewer ? '<=' : '>='); + $after = ($dirNewer ? '>=' : '<='); + + if ($startid !== 0) + $conds[] = 'rev_id' . $after . intval($startid); + if ($endid !== 0) + $conds[] = 'rev_id' . $before . intval($endid); + if (isset ($start)) + $conds[] = 'rev_timestamp' . $after . $db->addQuotes($start); + if (isset ($end)) + $conds[] = 'rev_timestamp' . $before . $db->addQuotes($end); + + // must manually initialize unset limit + if (!isset ($limit)) + $limit = 10; + + $this->validateLimit($this->encodeParamName('limit'), $limit, 1, $userMax, $botMax); + + // There is only one ID, use it + $conds['rev_page'] = array_pop(array_keys($pageSet->getGoodTitles())); + + } + elseif ($pageCount > 0) { + // When working in multi-page non-enumeration mode, + // limit to the latest revision only + $tables[] = 'page'; + $conds[] = 'page_id=rev_page'; + $conds[] = 'page_latest=rev_id'; + $this->validateLimit('page_count', $pageCount, 1, $userMax, $botMax); + + // Get all page IDs + $conds['page_id'] = array_keys($pageSet->getGoodTitles()); + + $limit = $pageCount; // assumption testing -- we should never get more then $pageCount rows. + } + elseif ($revCount > 0) { + $this->validateLimit('rev_count', $revCount, 1, $userMax, $botMax); + + // Get all revision IDs + $conds['rev_id'] = array_keys($pageSet->getRevisionIDs()); + + $limit = $revCount; // assumption testing -- we should never get more then $revCount rows. + } else + ApiBase :: dieDebug(__METHOD__, 'param validation?'); + + $options['LIMIT'] = $limit +1; + + $this->profileDBIn(); + $res = $db->select($tables, $fields, $conds, __METHOD__, $options); + $this->profileDBOut(); + + $data = array (); + $count = 0; + while ($row = $db->fetchObject($res)) { + + if (++ $count > $limit) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + if (!$enumRevMode) + ApiBase :: dieDebug(__METHOD__, 'Got more rows then expected'); // bug report + + $startStr = 'startid=' . $row->rev_id; + $msg = array ( + 'continue' => $startStr + ); + $this->getResult()->addValue('query-status', 'revisions', $msg); + break; + } + + $vals = array ( + 'revid' => intval($row->rev_id + ), 'oldid' => intval($row->rev_text_id)); + + if ($row->rev_minor_edit) { + $vals['minor'] = ''; + } + + if ($showTimestamp) + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rev_timestamp); + + if ($showUser) { + $vals['user'] = $row->rev_user_text; + if (!$row->rev_user) + $vals['anon'] = ''; + } + + if ($showComment) + $vals['comment'] = $row->rev_comment; + + if ($showContent) { + ApiResult :: setContent($vals, Revision :: getRevisionText($row)); + } + + $this->getResult()->addValue(array ( + 'query', + 'pages', + intval($row->rev_page + ), 'revisions'), intval($row->rev_id), $vals); + } + $db->freeResult($res); + + // Ensure that all revisions are shown as '' elements + $data = & $this->getResultData(); + foreach ($data['query']['pages'] as & $page) { + if (is_array($page) && array_key_exists('revisions', $page)) { + ApiResult :: setIndexedTagName($page['revisions'], 'rev'); + } + } + } + + protected function getAllowedParams() { + return array ( + 'prop' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'timestamp', + 'user', + 'comment', + 'content' + ) + ), + 'limit' => array ( + ApiBase :: PARAM_DFLT => 0, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 0, + ApiBase :: PARAM_MAX1 => 50, + ApiBase :: PARAM_MAX2 => 500 + ), + 'startid' => 0, + 'endid' => 0, + 'start' => array ( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'end' => array ( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'dir' => array ( + ApiBase :: PARAM_DFLT => 'older', + ApiBase :: PARAM_TYPE => array ( + 'newer', + 'older' + ) + ) + ); + } + + protected function getParamDescription() { + return array ( + 'prop' => 'Which properties to get for each revision: user|timestamp|comment|content', + 'limit' => 'limit how many revisions will be returned (enum)', + 'startid' => 'from which revision id to start enumeration (enum)', + 'endid' => 'stop revision enumeration on this revid (enum)', + 'start' => 'from which revision timestamp to start enumeration (enum)', + 'end' => 'enumerate up to this timestamp (enum)', + 'dir' => 'direction of enumeration - towards "newer" or "older" revisions (enum)' + ); + } + + protected function getDescription() { + return array ( + 'Get revision information.', + 'This module may be used in several ways:', + ' 1) Get data about a set of pages (last revision), by setting titles or pageids parameter.', + ' 2) Get revisions for one given page, by using titles/pageids with start/end/limit params.', + ' 3) Get data about a set of revisions by setting their IDs with revids parameter.', + 'All parameters marked as (enum) may only be used with a single page (#2).' + ); + } + + protected function getExamples() { + return array ( + 'Get data with content for the last revision of titles "API" and "Main Page":', + ' api.php?action=query&prop=revisions&titles=API|Main%20Page&rvprop=timestamp|user|comment|content', + 'Get last 5 revisions of the "Main Page":', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment', + 'Get first 5 revisions of the "Main Page":', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvdir=newer', + 'Get first 5 revisions of the "Main Page" made after 2006-05-01:', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvdir=newer&rvstart=20060501000000' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryRevisions.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php new file mode 100644 index 00000000..27c3f187 --- /dev/null +++ b/includes/api/ApiQuerySiteinfo.php @@ -0,0 +1,113 @@ + + * + * 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 ('ApiQueryBase.php'); +} + +class ApiQuerySiteinfo extends ApiQueryBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'si'); + } + + public function execute() { + $prop = null; + extract($this->extractRequestParams()); + + foreach ($prop as $p) { + switch ($p) { + + case 'general' : + + global $wgSitename, $wgVersion, $wgCapitalLinks; + $data = array (); + $mainPage = Title :: newFromText(wfMsgForContent('mainpage')); + $data['mainpage'] = $mainPage->getText(); + $data['base'] = $mainPage->getFullUrl(); + $data['sitename'] = $wgSitename; + $data['generator'] = "MediaWiki $wgVersion"; + $data['case'] = $wgCapitalLinks ? 'first-letter' : 'case-sensitive'; // 'case-insensitive' option is reserved for future + $this->getResult()->addValue('query', $p, $data); + break; + + case 'namespaces' : + + global $wgContLang; + $data = array (); + foreach ($wgContLang->getFormattedNamespaces() as $ns => $title) { + $data[$ns] = array ( + 'id' => $ns + ); + ApiResult :: setContent($data[$ns], $title); + } + ApiResult :: setIndexedTagName($data, 'ns'); + $this->getResult()->addValue('query', $p, $data); + break; + + default : + ApiBase :: dieDebug(__METHOD__, "Unknown prop=$p"); + } + } + } + + protected function getAllowedParams() { + return array ( + 'prop' => array ( + ApiBase :: PARAM_DFLT => 'general', + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array ( + 'general', + 'namespaces' + ) + ) + ); + } + + protected function getParamDescription() { + return array ( + 'prop' => array ( + 'Which sysinfo properties to get:', + ' "general" - Overall system information', + ' "namespaces" - List of registered namespaces (localized)' + ) + ); + } + + protected function getDescription() { + return 'Return general information about the site.'; + } + + protected function getExamples() { + return 'api.php?action=query&meta=siteinfo&siprop=general|namespaces'; + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?> \ No newline at end of file diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php new file mode 100644 index 00000000..67fbf41e --- /dev/null +++ b/includes/api/ApiResult.php @@ -0,0 +1,153 @@ + + * + * 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 ('ApiBase.php'); +} + +class ApiResult extends ApiBase { + + private $mData; + + /** + * Constructor + */ + public function __construct($main) { + parent :: __construct($main, 'result'); + $this->Reset(); + } + + public function Reset() { + $this->mData = array (); + } + + function & getData() { + return $this->mData; + } + + /** + * Add an output value to the array by name. + * Verifies that value with the same name has not been added before. + */ + public static function setElement(& $arr, $name, $value) { + if ($arr === null || $name === null || $value === null || !is_array($arr) || is_array($name)) + ApiBase :: dieDebug(__METHOD__, 'Bad parameter'); + + if (!isset ($arr[$name])) { + $arr[$name] = $value; + } + elseif (is_array($arr[$name]) && is_array($value)) { + $merged = array_intersect_key($arr[$name], $value); + if (empty ($merged)) + $arr[$name] += $value; + else + ApiBase :: dieDebug(__METHOD__, "Attempting to merge element $name"); + } else + ApiBase :: dieDebug(__METHOD__, "Attempting to add element $name=$value, existing value is {$arr[$name]}"); + } + + /** + * Adds the content element to the array. + * Use this function instead of hardcoding the '*' element. + */ + public static function setContent(& $arr, $value) { + if (is_array($value)) + ApiBase :: dieDebug(__METHOD__, 'Bad parameter'); + ApiResult :: setElement($arr, '*', $value); + } + + // public static function makeContentElement($tag, $value) { + // $result = array(); + // ApiResult::setContent($result, ) + // } + // + /** + * In case the array contains indexed values (in addition to named), + * all indexed values will have the given tag name. + */ + public static function setIndexedTagName(& $arr, $tag) { + // Do not use setElement() as it is ok to call this more than once + if ($arr === null || $tag === null || !is_array($arr) || is_array($tag)) + ApiBase :: dieDebug(__METHOD__, 'Bad parameter'); + $arr['_element'] = $tag; + } + + /** + * Add value to the output data at the given path. + * Path is an indexed array, each element specifing the branch at which to add the new value + * Setting $path to array('a','b','c') is equivalent to data['a']['b']['c'] = $value + */ + public function addValue($path, $name, $value) { + + $data = & $this->getData(); + + if (isset ($path)) { + if (is_array($path)) { + foreach ($path as $p) { + if (!isset ($data[$p])) + $data[$p] = array (); + $data = & $data[$p]; + } + } else { + if (!isset ($data[$path])) + $data[$path] = array (); + $data = & $data[$path]; + } + } + + ApiResult :: setElement($data, $name, $value); + } + + /** + * Recursivelly removes any elements from the array that begin with an '_'. + * The content element '*' is the only special element that is left. + * Use this method when the entire data object gets sent to the user. + */ + public function SanitizeData() { + ApiResult :: SanitizeDataInt($this->mData); + } + + private static function SanitizeDataInt(& $data) { + foreach ($data as $key => & $value) { + if ($key[0] === '_') { + unset ($data[$key]); + } + elseif (is_array($value)) { + ApiResult :: SanitizeDataInt($value); + } + } + } + + public function execute() { + ApiBase :: dieDebug(__METHOD__, 'execute() is not supported on Result object'); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiResult.php 16757 2006-10-03 05:41:55Z yurik $'; + } +} +?> \ No newline at end of file -- cgit v1.2.3-54-g00ecf