diff options
Diffstat (limited to 'includes/api')
65 files changed, 3470 insertions, 1963 deletions
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 22144333..8cf8c096 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -24,15 +24,17 @@ */ /** - * This abstract class implements many basic API functions, and is the base of all API classes. + * This abstract class implements many basic API functions, and is the base of + * all API classes. * The class functions are divided into several areas of functionality: * - * Module parameters: Derived classes can define getAllowedParams() to specify which parameters to expect, - * how to parse and validate them. + * Module parameters: Derived classes can define getAllowedParams() to specify + * which parameters to expect,h ow to parse and validate them. * - * Profiling: various methods to allow keeping tabs on various tasks and their time costs + * Profiling: various methods to allow keeping tabs on various tasks and their + * time costs * - * Self-documentation: code to allow api to document its own state. + * Self-documentation: code to allow the API to document its own state * * @ingroup API */ @@ -56,8 +58,11 @@ abstract class ApiBase { private $mMainModule, $mModuleName, $mModulePrefix; /** - * Constructor - */ + * Constructor + * @param $mainModule ApiMain object + * @param $moduleName string Name of this module + * @param $modulePrefix string Prefix to use for parameter names + */ public function __construct($mainModule, $moduleName, $modulePrefix = '') { $this->mMainModule = $mainModule; $this->mModuleName = $moduleName; @@ -69,32 +74,34 @@ abstract class ApiBase { *****************************************************************************/ /** - * Evaluates the parameters, performs the requested query, and sets up the - * result. Concrete implementations of ApiBase must override this method to - * provide whatever functionality their module offers. Implementations must - * not produce any output on their own and are not expected to handle any - * errors. + * Evaluates the parameters, performs the requested query, and sets up + * the result. Concrete implementations of ApiBase must override this + * method to provide whatever functionality their module offers. + * Implementations must not produce any output on their own and are not + * expected to handle any errors. * - * The execute method will be invoked directly by ApiMain immediately before - * the result of the module is output. Aside from the constructor, implementations - * should assume that no other methods will be called externally on the module - * before the result is processed. + * The execute() method will be invoked directly by ApiMain immediately + * before the result of the module is output. Aside from the + * constructor, implementations should assume that no other methods + * will be called externally on the module before the result is + * processed. * - * The result data should be stored in the result object referred to by - * "getResult()". Refer to ApiResult.php for details on populating a result - * object. + * The result data should be stored in the ApiResult object available + * through getResult(). */ public abstract function execute(); /** - * Returns a String that identifies the version of the extending class. Typically - * includes the class name, the svn revision, timestamp, and last author. May - * be severely incorrect in many implementations! + * Returns a string that identifies the version of the extending class. + * Typically includes the class name, the svn revision, timestamp, and + * last author. Usually done with SVN's Id keyword + * @return string */ public abstract function getVersion(); /** * Get the name of the module being executed by this instance + * @return string */ public function getModuleName() { return $this->mModuleName; @@ -102,6 +109,7 @@ abstract class ApiBase { /** * Get parameter prefix (usually two letters or an empty string). + * @return string */ public function getModulePrefix() { return $this->mModulePrefix; @@ -109,6 +117,7 @@ abstract class ApiBase { /** * Get the name of the module as shown in the profiler log + * @return string */ public function getModuleProfileName($db = false) { if ($db) @@ -118,7 +127,8 @@ abstract class ApiBase { } /** - * Get main module + * Get the main module + * @return ApiMain object */ public function getMain() { return $this->mMainModule; @@ -127,14 +137,15 @@ abstract class ApiBase { /** * Returns true if this module is the main module ($this === $this->mMainModule), * false otherwise. + * @return bool */ public function isMain() { return $this === $this->mMainModule; } /** - * Get the result object. Please refer to the documentation in ApiResult.php - * for details on populating and accessing data in a result object. + * Get the result object + * @return ApiResult */ public function getResult() { // Main module has getResult() method overriden @@ -145,37 +156,45 @@ abstract class ApiBase { } /** - * Get the result data array + * Get the result data array (read-only) + * @return array */ - public function & getResultData() { + public function getResultData() { return $this->getResult()->getData(); } /** - * Set warning section for this module. Users should monitor this section to - * notice any changes in API. + * Set warning section for this module. Users should monitor this + * section to notice any changes in API. Multiple calls to this + * function will result in the warning messages being separated by + * newlines + * @param $warning string Warning message */ public function setWarning($warning) { - # If there is a warning already, append it to the existing one - $data =& $this->getResult()->getData(); + $data = $this->getResult()->getData(); if(isset($data['warnings'][$this->getModuleName()])) { # Don't add duplicate warnings $warn_regex = preg_quote($warning, '/'); if(preg_match("/{$warn_regex}(\\n|$)/", $data['warnings'][$this->getModuleName()]['*'])) return; - $warning = "{$data['warnings'][$this->getModuleName()]['*']}\n$warning"; - unset($data['warnings'][$this->getModuleName()]); + $oldwarning = $data['warnings'][$this->getModuleName()]['*']; + # If there is a warning already, append it to the existing one + $warning = "$oldwarning\n$warning"; + $this->getResult()->unsetValue('warnings', $this->getModuleName()); } $msg = array(); ApiResult :: setContent($msg, $warning); + $this->getResult()->disableSizeCheck(); $this->getResult()->addValue('warnings', $this->getModuleName(), $msg); + $this->getResult()->enableSizeCheck(); } /** * If the module may only be used with a certain format module, * it should override this method to return an instance of that formatter. * A value of null means the default format will be used. + * @return mixed instance of a derived class of ApiFormatBase, or null */ public function getCustomPrinter() { return null; @@ -183,6 +202,7 @@ abstract class ApiBase { /** * Generates help message for this module, or false if there is no description + * @return mixed string or false */ public function makeHelpMsg() { @@ -198,8 +218,15 @@ abstract class ApiBase { ); $msg = $lnPrfx . implode($lnPrfx, $msg) . "\n"; + if ($this->isReadMode()) + $msg .= "\nThis module requires read rights."; + if ($this->isWriteMode()) + $msg .= "\nThis module requires write rights."; if ($this->mustBePosted()) - $msg .= "\nThis module only accepts POST requests.\n"; + $msg .= "\nThis module only accepts POST requests."; + if ($this->isReadMode() || $this->isWriteMode() || + $this->mustBePosted()) + $msg .= "\n"; // Parameters $paramsMsg = $this->makeHelpMsgParameters(); @@ -220,16 +247,16 @@ abstract class ApiBase { if ($this->getMain()->getShowVersions()) { $versions = $this->getVersion(); - $pattern = '(\$.*) ([0-9a-z_]+\.php) (.*\$)'; + $pattern = '/(\$.*) ([0-9a-z_]+\.php) (.*\$)/i'; $replacement = '\\0' . "\n " . 'http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/api/\\2'; if (is_array($versions)) { foreach ($versions as &$v) - $v = eregi_replace($pattern, $replacement, $v); + $v = preg_replace($pattern, $replacement, $v); $versions = implode("\n ", $versions); } else - $versions = eregi_replace($pattern, $replacement, $versions); + $versions = preg_replace($pattern, $replacement, $versions); $msg .= "Version:\n $versions\n"; } @@ -241,6 +268,7 @@ abstract class ApiBase { /** * Generates the parameter descriptions for this module, to be displayed in the * module's help. + * @return string */ public function makeHelpMsgParameters() { $params = $this->getFinalParams(); @@ -311,6 +339,7 @@ abstract class ApiBase { /** * Returns the description string for this module + * @return mixed string or array of strings */ protected function getDescription() { return false; @@ -318,15 +347,18 @@ abstract class ApiBase { /** * Returns usage examples for this module. Return null if no examples are available. + * @return mixed string or array of strings */ protected function getExamples() { return false; } /** - * Returns an array of allowed parameters (keys) => default value for that parameter. - * Don't call this function directly: use getFinalParams() to allow hooks - * to modify parameters as needed. + * Returns an array of allowed parameters (parameter name) => (default + * value) or (parameter name) => (array with PARAM_* constants as keys) + * Don't call this function directly: use getFinalParams() to allow + * hooks to modify parameters as needed. + * @return array */ protected function getAllowedParams() { return false; @@ -334,24 +366,30 @@ abstract class ApiBase { /** * Returns an array of parameter descriptions. - * Don't call this functon directly: use getFinalParamDescription() to allow - * hooks to modify descriptions as needed. + * Don't call this functon directly: use getFinalParamDescription() to + * allow hooks to modify descriptions as needed. + * @return array */ protected function getParamDescription() { return false; } /** - * Get final list of parameters, after hooks have had - * a chance to tweak it as needed. + * Get final list of parameters, after hooks have had a chance to + * tweak it as needed. + * @return array */ public function getFinalParams() { $params = $this->getAllowedParams(); wfRunHooks('APIGetAllowedParams', array(&$this, &$params)); return $params; } - - + + /** + * Get final description, after hooks have had a chance to tweak it as + * needed. + * @return array + */ public function getFinalParamDescription() { $desc = $this->getParamDescription(); wfRunHooks('APIGetParamDescription', array(&$this, &$desc)); @@ -361,16 +399,21 @@ abstract class ApiBase { /** * This method mangles parameter name based on the prefix supplied to the constructor. * Override this method to change parameter name during runtime + * @param $paramName string Parameter name + * @return string Prefixed parameter name */ public function encodeParamName($paramName) { return $this->mModulePrefix . $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. - * limit=max will not be parsed if $parseMaxLimit is set to false; use this - * when the max limit is not definite, e.g. when getting revisions. + * Using getAllowedParams(), this function 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. limit=max will not be + * parsed if $parseMaxLimit is set to false; use this when the max + * limit is not definitive yet, e.g. when getting revisions. + * @param $parseMaxLimit bool + * @return array */ public function extractRequestParams($parseMaxLimit = true) { $params = $this->getFinalParams(); @@ -384,6 +427,9 @@ abstract class ApiBase { /** * Get a value for the given parameter + * @param $paramName string Parameter name + * @param $parseMaxLimit bool see extractRequestParams() + * @return mixed Parameter value */ protected function getParameter($paramName, $parseMaxLimit = true) { $params = $this->getFinalParams(); @@ -393,6 +439,7 @@ abstract class ApiBase { /** * Die if none or more than one of a certain set of parameters is set + * @param $params array of parameter names */ public function requireOnlyOneParameter($params) { $required = func_get_args(); @@ -411,6 +458,7 @@ abstract class ApiBase { /** * Returns an array of the namespaces (by integer id) that exist on the * wiki. Used primarily in help documentation. + * @return array */ public static function getValidNamespaces() { static $mValidNamespaces = null; @@ -430,8 +478,10 @@ abstract class ApiBase { * 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. + * @param $paramSettings Mixed: default value or an array of settings + * using PARAM_* constants. * @param $parseMaxLimit Boolean: parse limit when max is given? + * @return mixed Parameter value */ protected function getParameterFromSettings($paramName, $paramSettings, $parseMaxLimit) { @@ -550,14 +600,17 @@ abstract class ApiBase { * 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) + * @param $valueName string The name of the parameter (for error + * reporting) + * @param $value mixed The value being parsed + * @param $allowMultiple bool Can $value contain more than one value + * separated by '|'? + * @param $allowedValues mixed An array of values to check against. If + * null, all values are accepted. + * @return mixed (allowMultiple ? an_array_of_values : a_single_value) */ protected function parseMultiValue($valueName, $value, $allowMultiple, $allowedValues) { - if( trim($value) === "" ) + if( trim($value) === "" && $allowMultiple) return array(); $sizeLimit = $this->mMainModule->canApiHighLimits() ? self::LIMIT_SML2 : self::LIMIT_SML1; $valuesList = explode('|', $value, $sizeLimit + 1); @@ -590,8 +643,14 @@ abstract class ApiBase { } /** - * Validate the value against the minimum and user/bot maximum limits. Prints usage info on failure. - */ + * Validate the value against the minimum and user/bot maximum limits. + * Prints usage info on failure. + * @param $paramName string Parameter name + * @param $value int Parameter value + * @param $min int Minimum value + * @param $max int Maximum value for users + * @param $botMax int Maximum value for sysops/bots + */ function validateLimit($paramName, $value, $min, $max, $botMax = null) { if (!is_null($min) && $value < $min) { $this->dieUsage($this->encodeParamName($paramName) . " may not be less than $min (set to $value)", $paramName); @@ -632,9 +691,13 @@ abstract class ApiBase { } /** - * Call main module's error handler + * Call the main module's error handler + * @param $description string Error text + * @param $errorCode string Error code + * @param $httpRespCode int HTTP response code */ public function dieUsage($description, $errorCode, $httpRespCode = 0) { + wfProfileClose(); throw new UsageException($description, $this->encodeParamName($errorCode), $httpRespCode); } @@ -698,11 +761,17 @@ abstract class ApiBase { 'noemail' => array('code' => 'noemail', 'info' => "The user has not specified a valid e-mail address, or has chosen not to receive e-mail from other users"), 'rcpatroldisabled' => array('code' => 'patroldisabled', 'info' => "Patrolling is disabled on this wiki"), 'markedaspatrollederror-noautopatrol' => array('code' => 'noautopatrol', 'info' => "You don't have permission to patrol your own changes"), + 'delete-toobig' => array('code' => 'bigdelete', 'info' => "You can't delete this page because it has more than \$1 revisions"), + 'movenotallowedfile' => array('code' => 'cantmovefile', 'info' => "You don't have permission to move files"), // API-specific messages + 'readrequired' => array('code' => 'readapidenied', 'info' => "You need read permission to use this module"), + 'writedisabled' => array('code' => 'noapiwrite', 'info' => "Editing of this wiki through the API is disabled. Make sure the \$wgEnableWriteAPI=true; statement is included in the wiki's LocalSettings.php file"), + 'writerequired' => array('code' => 'writeapidenied', 'info' => "You're not allowed to edit this wiki through the API"), 'missingparam' => array('code' => 'no$1', 'info' => "The \$1 parameter must be set"), 'invalidtitle' => array('code' => 'invalidtitle', 'info' => "Bad title ``\$1''"), 'nosuchpageid' => array('code' => 'nosuchpageid', 'info' => "There is no page with ID \$1"), + 'nosuchrevid' => array('code' => 'nosuchrevid', 'info' => "There is no revision with ID \$1"), 'invaliduser' => array('code' => 'invaliduser', 'info' => "Invalid username ``\$1''"), 'invalidexpiry' => array('code' => 'invalidexpiry', 'info' => "Invalid expiry time ``\$1''"), 'pastexpiry' => array('code' => 'pastexpiry', 'info' => "Expiry time ``\$1'' is in the past"), @@ -723,36 +792,47 @@ abstract class ApiBase { 'protect-invalidaction' => array('code' => 'protect-invalidaction', 'info' => "Invalid protection type ``\$1''"), 'protect-invalidlevel' => array('code' => 'protect-invalidlevel', 'info' => "Invalid protection level ``\$1''"), 'toofewexpiries' => array('code' => 'toofewexpiries', 'info' => "\$1 expiry timestamps were provided where \$2 were needed"), - + 'cantimport' => array('code' => 'cantimport', 'info' => "You don't have permission to import pages"), + 'cantimport-upload' => array('code' => 'cantimport-upload', 'info' => "You don't have permission to import uploaded pages"), + 'importnofile' => array('code' => 'nofile', 'info' => "You didn't upload a file"), + 'importuploaderrorsize' => array('code' => 'filetoobig', 'info' => 'The file you uploaded is bigger than the maximum upload size'), + 'importuploaderrorpartial' => array('code' => 'partialupload', 'info' => 'The file was only partially uploaded'), + 'importuploaderrortemp' => array('code' => 'notempdir', 'info' => 'The temporary upload directory is missing'), + 'importcantopen' => array('code' => 'cantopenfile', 'info' => "Couldn't open the uploaded file"), + 'import-noarticle' => array('code' => 'badinterwiki', 'info' => 'Invalid interwiki title specified'), + 'importbadinterwiki' => array('code' => 'badinterwiki', 'info' => 'Invalid interwiki title specified'), + 'import-unknownerror' => array('code' => 'import-unknownerror', 'info' => "Unknown error on import: ``\$1''"), // ApiEditPage messages 'noimageredirect-anon' => array('code' => 'noimageredirect-anon', 'info' => "Anonymous users can't create image redirects"), 'noimageredirect-logged' => array('code' => 'noimageredirect', 'info' => "You don't have permission to create image redirects"), 'spamdetected' => array('code' => 'spamdetected', 'info' => "Your edit was refused because it contained a spam fragment: ``\$1''"), 'filtered' => array('code' => 'filtered', 'info' => "The filter callback function refused your edit"), - 'contenttoobig' => array('code' => 'contenttoobig', 'info' => "The content you supplied exceeds the article size limit of \$1 bytes"), + 'contenttoobig' => array('code' => 'contenttoobig', 'info' => "The content you supplied exceeds the article size limit of \$1 kilobytes"), 'noedit-anon' => array('code' => 'noedit-anon', 'info' => "Anonymous users can't edit pages"), 'noedit' => array('code' => 'noedit', 'info' => "You don't have permission to edit pages"), 'wasdeleted' => array('code' => 'pagedeleted', 'info' => "The page has been deleted since you fetched its timestamp"), 'blankpage' => array('code' => 'emptypage', 'info' => "Creating new, empty pages is not allowed"), 'editconflict' => array('code' => 'editconflict', 'info' => "Edit conflict detected"), 'hashcheckfailed' => array('code' => 'badmd5', 'info' => "The supplied MD5 hash was incorrect"), - 'missingtext' => array('code' => 'notext', 'info' => "One of the text, appendtext and prependtext parameters must be set"), + 'missingtext' => array('code' => 'notext', 'info' => "One of the text, appendtext, prependtext and undo parameters must be set"), 'emptynewsection' => array('code' => 'emptynewsection', 'info' => 'Creating empty new sections is not possible.'), + 'revwrongpage' => array('code' => 'revwrongpage', 'info' => "r\$1 is not a revision of ``\$2''"), + 'undo-failure' => array('code' => 'undofailure', 'info' => 'Undo failed due to conflicting intermediate edits'), ); /** * Output the error message related to a certain array - * @param array $error Element of a getUserPermissionsErrors()-style array + * @param $error array Element of a getUserPermissionsErrors()-style array */ public function dieUsageMsg($error) { $parsed = $this->parseMsg($error); - $this->dieUsage($parsed['code'], $parsed['info']); + $this->dieUsage($parsed['info'], $parsed['code']); } /** * Return the error message related to a certain array - * @param array $error Element of a getUserPermissionsErrors()-style array + * @param $error array Element of a getUserPermissionsErrors()-style array * @return array('code' => code, 'info' => info) */ public function parseMsg($error) { @@ -769,27 +849,39 @@ abstract class ApiBase { /** * Internal code errors should be reported with this method + * @param $method string Method or function name + * @param $message string Error message */ protected static function dieDebug($method, $message) { wfDebugDieBacktrace("Internal error in $method: $message"); } /** - * Indicates if API needs to check maxlag + * Indicates if this module needs maxlag to be checked + * @return bool */ public function shouldCheckMaxlag() { return true; } /** - * Indicates if this module requires edit mode + * Indicates whether this module requires read rights + * @return bool + */ + public function isReadMode() { + return true; + } + /** + * Indicates whether this module requires write mode + * @return bool */ - public function isEditMode() { + public function isWriteMode() { return false; } /** * Indicates whether this module must be called with a POST request + * @return bool */ public function mustBePosted() { return false; @@ -839,6 +931,7 @@ abstract class ApiBase { /** * Total time the module was executed + * @return float */ public function getProfileTime() { if ($this->mTimeIn !== 0) @@ -882,6 +975,7 @@ abstract class ApiBase { /** * Total time the module used the database + * @return float */ public function getProfileDBTime() { if ($this->mDBTimeIn !== 0) @@ -889,8 +983,14 @@ abstract class ApiBase { return $this->mDBTime; } + /** + * Debugging function that prints a value and an optional backtrace + * @param $value mixed Value to print + * @param $name string Description of the printed value + * @param $backtrace bool If true, print a backtrace + */ public static function debugPrint($value, $name = 'unknown', $backtrace = false) { - print "\n\n<pre><b>Debuging value '$name':</b>\n\n"; + print "\n\n<pre><b>Debugging value '$name':</b>\n\n"; var_export($value); if ($backtrace) print "\n" . wfBacktrace(); @@ -899,9 +999,10 @@ abstract class ApiBase { /** - * Returns a String that identifies the version of this class. + * Returns a string that identifies the version of this class. + * @return string */ public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiBase.php 47041 2009-02-09 14:39:41Z catrope $'; + return __CLASS__ . ': $Id: ApiBase.php 50217 2009-05-05 13:12:16Z tstarling $'; } } diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index dfb11061..1c0bd5ac 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -50,7 +50,6 @@ class ApiBlock extends ApiBase { */ public function execute() { global $wgUser, $wgBlockAllowsUTEdit; - $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); if($params['gettoken']) @@ -94,7 +93,7 @@ class ApiBlock extends ApiBase { $this->dieUsageMsg($retval); $res['user'] = $params['user']; - $res['userID'] = $userID; + $res['userID'] = intval($userID); $res['expiry'] = ($expiry == Block::infinity() ? 'infinite' : wfTimestamp(TS_ISO_8601, $expiry)); $res['reason'] = $params['reason']; if($params['anononly']) @@ -115,6 +114,10 @@ class ApiBlock extends ApiBase { public function mustBePosted() { return true; } + public function isWriteMode() { + return true; + } + public function getAllowedParams() { return array ( 'user' => null, @@ -163,6 +166,6 @@ class ApiBlock extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiBlock.php 43677 2008-11-18 15:21:04Z catrope $'; + return __CLASS__ . ': $Id: ApiBlock.php 48091 2009-03-06 13:49:44Z catrope $'; } } diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index c0212924..9431ad78 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -49,7 +49,6 @@ class ApiDelete extends ApiBase { */ public function execute() { global $wgUser; - $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); $this->requireOnlyOneParameter($params, 'title', 'pageid'); @@ -76,14 +75,18 @@ class ApiDelete extends ApiBase { $retval = self::deleteFile($params['token'], $titleObj, $params['oldimage'], $reason, false); if(count($retval)) // We don't care about multiple errors, just report one of them - $this->dieUsageMsg(current($retval)); + $this->dieUsageMsg(reset($retval)); } else { $articleObj = new Article($titleObj); + if($articleObj->isBigDeletion() && !$wgUser->isAllowed('bigdelete')) { + global $wgDeleteRevisionsLimit; + $this->dieUsageMsg(array('delete-toobig', $wgDeleteRevisionsLimit)); + } $retval = self::delete($articleObj, $params['token'], $reason); if(count($retval)) // We don't care about multiple errors, just report one of them - $this->dieUsageMsg(current($retval)); + $this->dieUsageMsg(reset($retval)); if($params['watch'] || $wgUser->getOption('watchdeletion')) $articleObj->doWatch(); @@ -133,9 +136,10 @@ class ApiDelete extends ApiBase { if($reason === false) return array(array('cannotdelete')); } - - if (!wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason))) - $this->dieUsageMsg(array('hookaborted')); + + $error = ''; + if (!wfRunHooks('ArticleDelete', array(&$article, &$wgUser, &$reason, $error))) + $this->dieUsageMsg(array('hookaborted', $error)); // Luckily, Article.php provides a reusable delete function that does the hard work for us if($article->doDeleteArticle($reason)) { @@ -173,6 +177,10 @@ class ApiDelete extends ApiBase { public function mustBePosted() { return true; } + public function isWriteMode() { + return true; + } + public function getAllowedParams() { return array ( 'title' => null, @@ -213,6 +221,6 @@ class ApiDelete extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiDelete.php 44541 2008-12-13 21:07:18Z mrzman $'; + return __CLASS__ . ': $Id: ApiDelete.php 48122 2009-03-07 12:58:41Z catrope $'; } } diff --git a/includes/api/ApiDisabled.php b/includes/api/ApiDisabled.php index 40e38a0f..9e0bf56e 100644 --- a/includes/api/ApiDisabled.php +++ b/includes/api/ApiDisabled.php @@ -48,6 +48,10 @@ class ApiDisabled extends ApiBase { $this->dieUsage("The ``{$this->getModuleName()}'' module has been disabled.", 'moduledisabled'); } + public function isReadMode() { + return false; + } + public function getAllowedParams() { return array (); } @@ -67,6 +71,6 @@ class ApiDisabled extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiDisabled.php 41268 2008-09-25 20:50:50Z catrope $'; + return __CLASS__ . ': $Id: ApiDisabled.php 48091 2009-03-06 13:49:44Z catrope $'; } } diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index bc5dfa87..d4a57b83 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -43,12 +43,12 @@ class ApiEditPage extends ApiBase { public function execute() { global $wgUser; - $this->getMain()->requestWriteMode(); - $params = $this->extractRequestParams(); if(is_null($params['title'])) $this->dieUsageMsg(array('missingparam', 'title')); - if(is_null($params['text']) && is_null($params['appendtext']) && is_null($params['prependtext'])) + if(is_null($params['text']) && is_null($params['appendtext']) && + is_null($params['prependtext']) && + $params['undo'] == 0) $this->dieUsageMsg(array('missingtext')); if(is_null($params['token'])) $this->dieUsageMsg(array('missingparam', 'token')); @@ -58,6 +58,9 @@ class ApiEditPage extends ApiBase { $titleObj = Title::newFromText($params['title']); if(!$titleObj) $this->dieUsageMsg(array('invalidtitle', $params['title'])); + // Some functions depend on $wgTitle == $ep->mTitle + global $wgTitle; + $wgTitle = $titleObj; if($params['createonly'] && $titleObj->exists()) $this->dieUsageMsg(array('createonly-exists')); @@ -75,13 +78,50 @@ class ApiEditPage extends ApiBase { $toMD5 = $params['text']; if(!is_null($params['appendtext']) || !is_null($params['prependtext'])) { - $content = $articleObj->getContent(); + // For non-existent pages, Article::getContent() + // returns an interface message rather than '' + // We do want getContent()'s behavior for non-existent + // MediaWiki: pages, though + if($articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI) + $content = ''; + else + $content = $articleObj->getContent(); $params['text'] = $params['prependtext'] . $content . $params['appendtext']; $toMD5 = $params['prependtext'] . $params['appendtext']; } + + if($params['undo'] > 0) + { + if($params['undoafter'] > 0) + { + if($params['undo'] < $params['undoafter']) + list($params['undo'], $params['undoafter']) = + array($params['undoafter'], $params['undo']); + $undoafterRev = Revision::newFromID($params['undoafter']); + } + $undoRev = Revision::newFromID($params['undo']); + if(is_null($undoRev) || $undoRev->isDeleted(Revision::DELETED_TEXT)) + $this->dieUsageMsg(array('nosuchrevid', $params['undo'])); + if($params['undoafter'] == 0) + $undoafterRev = $undoRev->getPrevious(); + if(is_null($undoafterRev) || $undoafterRev->isDeleted(Revision::DELETED_TEXT)) + $this->dieUsageMsg(array('nosuchrevid', $params['undoafter'])); + if($undoRev->getPage() != $articleObj->getID()) + $this->dieUsageMsg(array('revwrongpage', $undoRev->getID(), $titleObj->getPrefixedText())); + if($undoafterRev->getPage() != $articleObj->getID()) + $this->dieUsageMsg(array('revwrongpage', $undoafterRev->getID(), $titleObj->getPrefixedText())); + $newtext = $articleObj->getUndoText($undoRev, $undoafterRev); + if($newtext === false) + $this->dieUsageMsg(array('undo-failure')); + $params['text'] = $newtext; + // If no summary was given and we only undid one rev, + // use an autosummary + if(is_null($params['summary']) && $titleObj->getNextRevisionID($undoafterRev->getID()) == $params['undo']) + $params['summary'] = wfMsgForContent('undo-summary', $params['undo'], $undoRev->getUserText()); + } # See if the MD5 hash checks out - if(isset($params['md5'])) + if(!is_null($params['md5'])) if(md5($toMD5) !== $params['md5']) $this->dieUsageMsg(array('hashcheckfailed')); @@ -140,9 +180,9 @@ class ApiEditPage extends ApiBase { # Run hooks # Handle CAPTCHA parameters global $wgRequest; - if(isset($params['captchaid'])) + if(!is_null($params['captchaid'])) $wgRequest->setVal( 'wpCaptchaId', $params['captchaid'] ); - if(isset($params['captchaword'])) + if(!is_null($params['captchaword'])) $wgRequest->setVal( 'wpCaptchaWord', $params['captchaword'] ); $r = array(); if(!wfRunHooks('APIEditBeforeSave', array(&$ep, $ep->textbox1, &$r))) @@ -160,10 +200,6 @@ class ApiEditPage extends ApiBase { # Do the actual save $oldRevId = $articleObj->getRevIdFetched(); $result = null; - # *Something* is setting $wgTitle to a title corresponding to "Msg", - # but that breaks API mode detection through is_null($wgTitle) - global $wgTitle; - $wgTitle = null; # Fake $wgRequest for some hooks inside EditPage # FIXME: This interface SUCKS $oldRequest = $wgRequest; @@ -217,7 +253,7 @@ class ApiEditPage extends ApiBase { $r['new'] = ''; case EditPage::AS_SUCCESS_UPDATE: $r['result'] = "Success"; - $r['pageid'] = $titleObj->getArticleID(); + $r['pageid'] = intval($titleObj->getArticleID()); $r['title'] = $titleObj->getPrefixedText(); # HACK: We create a new Article object here because getRevIdFetched() # refuses to be run twice, and because Title::getLatestRevId() @@ -229,8 +265,8 @@ class ApiEditPage extends ApiBase { $r['nochange'] = ''; else { - $r['oldrevid'] = $oldRevId; - $r['newrevid'] = $newRevId; + $r['oldrevid'] = intval($oldRevId); + $r['newrevid'] = intval($newRevId); } break; default: @@ -243,6 +279,10 @@ class ApiEditPage extends ApiBase { return true; } + public function isWriteMode() { + return true; + } + protected function getDescription() { return 'Create and edit pages.'; } @@ -269,6 +309,12 @@ class ApiEditPage extends ApiBase { 'md5' => null, 'prependtext' => null, 'appendtext' => null, + 'undo' => array( + ApiBase :: PARAM_TYPE => 'integer' + ), + 'undoafter' => array( + ApiBase :: PARAM_TYPE => 'integer' + ), ); } @@ -300,17 +346,23 @@ class ApiEditPage extends ApiBase { 'prependtext' => array( 'Add this text to the beginning of the page. Overrides text.', 'Don\'t use together with section: that won\'t do what you expect.'), 'appendtext' => 'Add this text to the end of the page. Overrides text', + 'undo' => 'Undo this revision. Overrides text, prependtext and appendtext', + 'undoafter' => 'Undo all revisions from undo to this one. If not set, just undo one revision', ); } protected function getExamples() { return array ( "Edit a page (anonymous user):", - " api.php?action=edit&title=Test&summary=test%20summary&text=article%20content&basetimestamp=20070824123454&token=%2B\\" + " api.php?action=edit&title=Test&summary=test%20summary&text=article%20content&basetimestamp=20070824123454&token=%2B\\", + "Prepend __NOTOC__ to a page (anonymous user):", + " api.php?action=edit&title=Test&summary=NOTOC&minor&prependtext=__NOTOC__%0A&basetimestamp=20070824123454&token=%2B\\", + "Undo r13579 through r13585 with autosummary(anonymous user):", + " api.php?action=edit&title=Test&undo=13585&undoafter=13579&basetimestamp=20070824123454&token=%2B\\", ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiEditPage.php 44394 2008-12-10 14:12:54Z catrope $'; + return __CLASS__ . ': $Id: ApiEditPage.php 50220 2009-05-05 14:07:59Z tstarling $'; } } diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php index fbdf495f..9bb504fb 100644 --- a/includes/api/ApiEmailUser.php +++ b/includes/api/ApiEmailUser.php @@ -39,14 +39,11 @@ class ApiEmailUser extends ApiBase { public function execute() { global $wgUser; - // Check whether email is enabled if ( !EmailUserForm::userEmailEnabled() ) $this->dieUsageMsg( array( 'usermaildisabled' ) ); - - $this->getMain()->requestWriteMode(); + $params = $this->extractRequestParams(); - // Check required parameters if ( !isset( $params['target'] ) ) $this->dieUsageMsg( array( 'missingparam', 'target' ) ); @@ -79,6 +76,10 @@ class ApiEmailUser extends ApiBase { public function mustBePosted() { return true; } + public function isWriteMode() { + return true; + } + public function getAllowedParams() { return array ( 'target' => null, @@ -112,7 +113,7 @@ class ApiEmailUser extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiEmailUser.php 41269 2008-09-25 21:39:36Z catrope $'; + return __CLASS__ . ': $Id: ApiEmailUser.php 48091 2009-03-06 13:49:44Z catrope $'; } }
\ No newline at end of file diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index 109b6552..0859232e 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -89,7 +89,7 @@ class ApiFeedWatchlist extends ApiBase { $data = $module->getResultData(); $feedItems = array (); - foreach ($data['query']['watchlist'] as $info) { + foreach ((array)$data['query']['watchlist'] as $info) { $feedItems[] = $this->createFeedItem($info); } @@ -175,6 +175,6 @@ class ApiFeedWatchlist extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFeedWatchlist.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiFeedWatchlist.php 46848 2009-02-05 15:31:06Z catrope $'; } } diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index 9efbbbe0..cc7434c6 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -38,9 +38,11 @@ abstract class ApiFormatBase extends ApiBase { private $mIsHtml, $mFormat, $mUnescapeAmps, $mHelp, $mCleared; /** - * Create a new instance of the formatter. - * If the format name ends with 'fm', wrap its output in the proper HTML. - */ + * Constructor + * If $format ends with 'fm', pretty-print the output in HTML. + * @param $main ApiMain + * @param $format string Format name + */ public function __construct($main, $format) { parent :: __construct($main, $format); @@ -61,9 +63,8 @@ abstract class ApiFormatBase extends ApiBase { public abstract function getMimeType(); /** - * If formatter outputs data results as is, the results must first be sanitized. - * An XML formatter on the other hand uses special tags, such as "_element" for special handling, - * and thus needs to override this function to return true. + * Whether this formatter needs raw data such as _element tags + * @return bool */ public function getNeedsRawData() { return false; @@ -71,36 +72,40 @@ abstract class ApiFormatBase extends ApiBase { /** * Get the internal format name + * @return string */ public function getFormat() { return $this->mFormat; } /** - * Specify whether or not ampersands should be escaped to '&' when rendering. This - * should only be set to true for the help message when rendered in the default (xmlfm) - * format. This is a temporary special-case fix that should be removed once the help - * has been reworked to use a fully html interface. + * Specify whether or not sequences like &quot; should be unescaped + * to " . This should only be set to true for the help message + * when rendered in the default (xmlfm) format. This is a temporary + * special-case fix that should be removed once the help has been + * reworked to use a fully HTML interface. * - * @param boolean Whether or not ampersands should be escaped. + * @param $b bool Whether or not ampersands should be escaped. */ public function setUnescapeAmps ( $b ) { $this->mUnescapeAmps = $b; } /** - * Returns true when an HTML filtering printer should be used. + * Returns true when the HTML pretty-printer should be used. * The default implementation assumes that formats ending with 'fm' * should be formatted in HTML. + * @return bool */ public function getIsHtml() { return $this->mIsHtml; } /** - * Initialize the printer function and prepares the output headers, etc. + * Initialize the printer function and prepare 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 + * @param $isError bool Whether an error message is printed */ function initPrinter($isError) { $isHtml = $this->getIsHtml(); @@ -167,8 +172,10 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or } /** - * The main format printing function. Call it to output the result string to the user. - * This function will automatically output HTML when format name ends in 'fm'. + * The main format printing function. Call it to output the result + * string to the user. This function will automatically output HTML + * when format name ends in 'fm'. + * @param $text string */ public function printText($text) { if ($this->getIsHtml()) @@ -188,15 +195,18 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or } /** - * Says pretty-printer that it should use *bold* and $italics$ formatting - */ + * Sets whether the pretty-printer should format *bold* and $italics$ + * @param $help bool + */ public function setHelp( $help = true ) { $this->mHelp = true; } /** - * Prety-print various elements in HTML format, such as xml tags and URLs. - * This method also replaces any '<' with < + * Prety-print various elements in HTML format, such as xml tags and + * URLs. This method also escapes characters like < + * @param $text string + * @return string */ protected function formatHTML($text) { global $wgUrlProtocols; @@ -209,14 +219,14 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or // identify URLs $protos = implode("|", $wgUrlProtocols); # This regex hacks around bug 13218 (" included in the URL) - $text = preg_replace("#(($protos).*?)(")?([ \\'\"()<\n])#", '<a href="\\1">\\1</a>\\3\\4', $text); + $text = preg_replace("#(($protos).*?)(")?([ \\'\"<>\n]|<|>|")#", '<a href="\\1">\\1</a>\\3\\4', $text); // identify requests to api.php $text = preg_replace("#api\\.php\\?[^ \\()<\n\t]+#", '<a href="\\0">\\0</a>', $text); if( $this->mHelp ) { // make strings inside * bold - $text = ereg_replace("\\*[^<>\n]+\\*", '<b>\\0</b>', $text); + $text = preg_replace("#\\*[^<>\n]+\\*#", '<b>\\0</b>', $text); // make strings inside $ italic - $text = ereg_replace("\\$[^<>\n]+\\$", '<b><i>\\0</i></b>', $text); + $text = preg_replace("#\\$[^<>\n]+\\$#", '<b><i>\\0</i></b>', $text); } /* Temporary fix for bad links in help messages. As a special case, @@ -229,9 +239,6 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or return $text; } - /** - * Returns usage examples for this format. - */ protected function getExamples() { return 'api.php?action=query&meta=siteinfo&siprop=namespaces&format=' . $this->getModuleName(); } @@ -241,7 +248,7 @@ See <a href='http://www.mediawiki.org/wiki/API'>complete documentation</a>, or } public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiFormatBase.php 43470 2008-11-14 00:30:34Z tstarling $'; + return __CLASS__ . ': $Id: ApiFormatBase.php 48521 2009-03-18 19:25:29Z ialex $'; } } @@ -256,14 +263,21 @@ class ApiFormatFeedWrapper extends ApiFormatBase { } /** - * Call this method to initialize output data. See self::execute() + * Call this method to initialize output data. See execute() + * @param $result ApiResult + * @param $feed object an instance of one of the $wgFeedClasses classes + * @param $feedItems array of FeedItem objects */ public static function setResult($result, $feed, $feedItems) { // Store output in the Result data. // This way we can check during execution if any error has occured - $data = & $result->getData(); - $data['_feed'] = $feed; - $data['_feeditems'] = $feedItems; + // Disable size checking for this because we can't continue + // cleanly; size checking would cause more problems than it'd + // solve + $result->disableSizeCheck(); + $result->addValue(null, '_feed', $feed); + $result->addValue(null, '_feeditems', $feedItems); + $result->enableSizeCheck(); } /** @@ -282,8 +296,8 @@ class ApiFormatFeedWrapper extends ApiFormatBase { /** * This class expects the result data to be in a custom format set by self::setResult() - * $result['_feed'] - an instance of one of the $wgFeedClasses classes - * $result['_feeditems'] - an array of FeedItem instances + * $result['_feed'] - an instance of one of the $wgFeedClasses classes + * $result['_feeditems'] - an array of FeedItem instances */ public function execute() { $data = $this->getResultData(); @@ -302,6 +316,6 @@ class ApiFormatFeedWrapper extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatBase.php 43470 2008-11-14 00:30:34Z tstarling $'; + return __CLASS__ . ': $Id: ApiFormatBase.php 48521 2009-03-18 19:25:29Z ialex $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index 1d89eb18..7b5a02a4 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -61,7 +61,7 @@ class ApiFormatJson extends ApiFormatBase { // Some versions of PHP have a broken json_encode, see PHP bug // 46944. Test encoding an affected character (U+20000) to // avoid this. - if (!function_exists('json_encode') || $this->getIsHtml() || strtolower(json_encode("\xf0\xa0\x80\x80")) != '\ud840\udc00') { + if (!function_exists('json_encode') || $this->getIsHtml() || strtolower(json_encode("\xf0\xa0\x80\x80")) != '"\ud840\udc00"') { $json = new Services_JSON(); $this->printText($prefix . $json->encode($this->getResultData(), $this->getIsHtml()) . $suffix); } else { @@ -89,6 +89,6 @@ class ApiFormatJson extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatJson.php 45682 2009-01-12 19:06:33Z raymond $'; + return __CLASS__ . ': $Id: ApiFormatJson.php 48713 2009-03-23 19:58:07Z catrope $'; } } diff --git a/includes/api/ApiFormatJson_json.php b/includes/api/ApiFormatJson_json.php index 4b29ff56..8cb3606d 100644 --- a/includes/api/ApiFormatJson_json.php +++ b/includes/api/ApiFormatJson_json.php @@ -45,14 +45,14 @@ * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. * -* @ingroup API -* @author Michal Migurski <mike-json@teczno.com> -* @author Matt Knapp <mdknapp[at]gmail[dot]com> -* @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com> -* @copyright 2005 Michal Migurski -* @version CVS: $Id: ApiFormatJson_json.php 45682 2009-01-12 19:06:33Z raymond $ -* @license http://www.opensource.org/licenses/bsd-license.php -* @see http://pear.php.net/pepr/pepr-proposal-show.php?id=198 +* @ingroup API +* @author Michal Migurski <mike-json@teczno.com> +* @author Matt Knapp <mdknapp[at]gmail[dot]com> +* @author Brett Stimmerman <brettstimmerman[at]gmail[dot]com> +* @copyright 2005 Michal Migurski +* @version CVS: $Id: ApiFormatJson_json.php 45765 2009-01-15 10:18:44Z catrope $ +* @license http://www.opensource.org/licenses/bsd-license.php +* @see http://pear.php.net/pepr/pepr-proposal-show.php?id=198 */ /** @@ -115,715 +115,715 @@ define('SERVICES_JSON_SUPPRESS_ERRORS', 32); */ 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 (0xFC00 & $bytes) == 0xD800 && strlen($utf16) >= 4 && (0xFC & ord($utf16{2})) == 0xDC: - // return a 4-byte UTF-8 character - $char = ((($bytes & 0x03FF) << 10) - | ((ord($utf16{2}) & 0x03) << 8) - | ord($utf16{3})); - $char += 0x10000; - return chr(0xF0 | (($char >> 18) & 0x07)) - . chr(0x80 | (($char >> 12) & 0x3F)) - . chr(0x80 | (($char >> 6) & 0x3F)) - . chr(0x80 | ($char & 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}))); - - case 4: - // return a UTF-16 surrogate pair from a 4-byte UTF-8 char - if(ord($utf8{0}) > 0xF4) return ''; # invalid - $char = ((0x1C0000 & (ord($utf8{0}) << 18)) - | (0x03F000 & (ord($utf8{1}) << 12)) - | (0x000FC0 & (ord($utf8{2}) << 6)) - | (0x00003F & ord($utf8{3}))); - if($char > 0x10FFFF) return ''; # invalid - $char -= 0x10000; - return chr(0xD8 | (($char >> 18) & 0x03)) - . chr(($char >> 10) & 0xFF) - . chr(0xDC | (($char >> 8) & 0x03)) - . chr($char & 0xFF); - } - - // 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 - // These will always return a surrogate pair - $char = pack('C*', $ord_var_c, - ord($var{$c + 1}), - ord($var{$c + 2}), - ord($var{$c + 3})); - $c += 3; - $utf16 = $this->utf82utf16($char); - if($utf16 == '') { - $ascii .= '\ufffd'; - } else { - $utf16 = str_split($utf16, 2); - $ascii .= sprintf('\u%04s\u%04s', bin2hex($utf16[0]), bin2hex($utf16[1])); - } - 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('/\\\uD[89AB][0-9A-F]{2}\\\uD[C-F][0-9A-F]{2}/i', substr($chrs, $c, 12)): - // escaped unicode surrogate pair - $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) - . chr(hexdec(substr($chrs, ($c + 4), 2))) - . chr(hexdec(substr($chrs, ($c + 8), 2))) - . chr(hexdec(substr($chrs, ($c + 10), 2))); - $utf8 .= $this->utf162utf8($utf16); - $c += 11; - 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; - } + /** + * 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 (0xFC00 & $bytes) == 0xD800 && strlen($utf16) >= 4 && (0xFC & ord($utf16{2})) == 0xDC: + // return a 4-byte UTF-8 character + $char = ((($bytes & 0x03FF) << 10) + | ((ord($utf16{2}) & 0x03) << 8) + | ord($utf16{3})); + $char += 0x10000; + return chr(0xF0 | (($char >> 18) & 0x07)) + . chr(0x80 | (($char >> 12) & 0x3F)) + . chr(0x80 | (($char >> 6) & 0x3F)) + . chr(0x80 | ($char & 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}))); + + case 4: + // return a UTF-16 surrogate pair from a 4-byte UTF-8 char + if(ord($utf8{0}) > 0xF4) return ''; # invalid + $char = ((0x1C0000 & (ord($utf8{0}) << 18)) + | (0x03F000 & (ord($utf8{1}) << 12)) + | (0x000FC0 & (ord($utf8{2}) << 6)) + | (0x00003F & ord($utf8{3}))); + if($char > 0x10FFFF) return ''; # invalid + $char -= 0x10000; + return chr(0xD8 | (($char >> 18) & 0x03)) + . chr(($char >> 10) & 0xFF) + . chr(0xDC | (($char >> 8) & 0x03)) + . chr($char & 0xFF); + } + + // 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 + // These will always return a surrogate pair + $char = pack('C*', $ord_var_c, + ord($var{$c + 1}), + ord($var{$c + 2}), + ord($var{$c + 3})); + $c += 3; + $utf16 = $this->utf82utf16($char); + if($utf16 == '') { + $ascii .= '\ufffd'; + } else { + $utf16 = str_split($utf16, 2); + $ascii .= sprintf('\u%04s\u%04s', bin2hex($utf16[0]), bin2hex($utf16[1])); + } + 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('/\\\uD[89AB][0-9A-F]{2}\\\uD[C-F][0-9A-F]{2}/i', substr($chrs, $c, 12)): + // escaped unicode surrogate pair + $utf16 = chr(hexdec(substr($chrs, ($c + 2), 2))) + . chr(hexdec(substr($chrs, ($c + 4), 2))) + . chr(hexdec(substr($chrs, ($c + 8), 2))) + . chr(hexdec(substr($chrs, ($c + 10), 2))); + $utf8 .= $this->utf162utf8($utf16); + $c += 11; + 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; + } } @@ -831,31 +831,31 @@ class Services_JSON /// @cond if (class_exists('PEAR_Error')) { - /** - * @ingroup API - */ - 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); - } - } + /** + * @ingroup API + */ + 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 { /// @endcond - /** - * @todo Ultimately, this class shall be descended from PEAR_Error - * @ingroup API - */ - class Services_JSON_Error - { - function Services_JSON_Error($message = 'unknown error', $code = null, - $mode = null, $options = null, $userinfo = null) - { - - } - } + /** + * @todo Ultimately, this class shall be descended from PEAR_Error + * @ingroup API + */ + class Services_JSON_Error + { + function Services_JSON_Error($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + + } + } } diff --git a/includes/api/ApiFormatRaw.php b/includes/api/ApiFormatRaw.php new file mode 100644 index 00000000..51025448 --- /dev/null +++ b/includes/api/ApiFormatRaw.php @@ -0,0 +1,71 @@ +<?php + +/* + * Created on Feb 2, 2009 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2009 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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'); +} + +/** + * Formatter that spits out anything you like with any desired MIME type + * @ingroup API + */ +class ApiFormatRaw extends ApiFormatBase { + + /** + * Constructor + * @param $main ApiMain object + * @param $errorFallback Formatter object to fall back on for errors + */ + public function __construct($main, $errorFallback) { + parent :: __construct($main, 'raw'); + $this->mErrorFallback = $errorFallback; + } + + public function getMimeType() { + $data = $this->getResultData(); + if(isset($data['error'])) + return $this->mErrorFallback->getMimeType(); + if(!isset($data['mime'])) + ApiBase::dieDebug(__METHOD__, "No MIME type set for raw formatter"); + return $data['mime']; + } + + public function execute() { + $data = $this->getResultData(); + if(isset($data['error'])) + { + $this->mErrorFallback->execute(); + return; + } + if(!isset($data['text'])) + ApiBase::dieDebug(__METHOD__, "No text given for raw formatter"); + $this->printText($data['text']); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiFormatRaw.php 48629 2009-03-20 11:40:54Z catrope $'; + } +} diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php index e741c16d..a716373d 100644 --- a/includes/api/ApiFormatWddx.php +++ b/includes/api/ApiFormatWddx.php @@ -42,7 +42,13 @@ class ApiFormatWddx extends ApiFormatBase { } public function execute() { - if (function_exists('wddx_serialize_value') && !$this->getIsHtml()) { + // Some versions of PHP have a broken wddx_serialize_value, see + // PHP bug 45314. Test encoding an affected character (U+00A0) + // to avoid this. + $expected = "<wddxPacket version='1.0'><header/><data><string>\xc2\xa0</string></data></wddxPacket>"; + if (function_exists('wddx_serialize_value') + && !$this->getIsHtml() + && wddx_serialize_value("\xc2\xa0") == $expected) { $this->printText(wddx_serialize_value($this->getResultData())); } else { // Don't do newlines and indentation if we weren't asked @@ -60,8 +66,8 @@ class ApiFormatWddx extends ApiFormatBase { } /** - * Recursivelly go through the object and output its data in WDDX format. - */ + * Recursively go through the object and output its data in WDDX format. + */ function slowWddxPrinter($elemValue, $indent = 0) { $indstr = ($this->getIsHtml() ? "" : str_repeat(' ', $indent)); $indstr2 = ($this->getIsHtml() ? "" : str_repeat(' ', $indent + 2)); @@ -109,6 +115,6 @@ class ApiFormatWddx extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatWddx.php 44588 2008-12-14 19:14:21Z demon $'; + return __CLASS__ . ': $Id: ApiFormatWddx.php 48716 2009-03-23 20:06:16Z catrope $'; } } diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php index 7ff57324..35b412c9 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -89,6 +89,11 @@ class ApiFormatXml extends ApiFormatBase { if ($this->mDoubleQuote) $subElemContent = $this->doubleQuote($subElemContent); unset ($elemValue['*']); + + // Add xml:space="preserve" to the + // element so XML parsers will leave + // whitespace in the content alone + $elemValue['xml:space'] = 'preserve'; } else { $subElemContent = null; } @@ -106,14 +111,6 @@ class ApiFormatXml extends ApiFormatBase { if (is_string($subElemValue) && $this->mDoubleQuote) $subElemValue = $this->doubleQuote($subElemValue); - // Replace spaces with underscores - $newSubElemId = str_replace(' ', '_', $subElemId); - if($newSubElemId != $subElemId) { - $elemValue[$newSubElemId] = $subElemValue; - unset($elemValue[$subElemId]); - $subElemId = $newSubElemId; - } - if (gettype($subElemId) === 'integer') { $indElements[] = $subElemValue; unset ($elemValue[$subElemId]); @@ -175,6 +172,6 @@ class ApiFormatXml extends ApiFormatBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiFormatXml.php 44588 2008-12-14 19:14:21Z demon $'; + return __CLASS__ . ': $Id: ApiFormatXml.php 50217 2009-05-05 13:12:16Z tstarling $'; } } diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index 4ccb5acf..c001a7dc 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -50,6 +50,10 @@ class ApiHelp extends ApiBase { return false; } + public function isReadMode() { + return false; + } + public function getDescription() { return array ( 'Display this help screen.' @@ -57,6 +61,6 @@ class ApiHelp extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiHelp.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiHelp.php 48091 2009-03-06 13:49:44Z catrope $'; } } diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php new file mode 100644 index 00000000..4b1518bb --- /dev/null +++ b/includes/api/ApiImport.php @@ -0,0 +1,179 @@ +<?php + +/* + * Created on Feb 4, 2009 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2009 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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'); +} + +/** + * API module that imports an XML file like Special:Import does + * + * @ingroup API + */ +class ApiImport extends ApiBase { + + public function __construct($main, $action) { + parent :: __construct($main, $action); + } + + public function execute() { + global $wgUser; + if(!$wgUser->isAllowed('import')) + $this->dieUsageMsg(array('cantimport')); + $params = $this->extractRequestParams(); + if(!isset($params['token'])) + $this->dieUsageMsg(array('missingparam', 'token')); + if(!$wgUser->matchEditToken($params['token'])) + $this->dieUsageMsg(array('sessionfailure')); + + $source = null; + $isUpload = false; + if(isset($params['interwikisource'])) + { + if(!isset($params['interwikipage'])) + $this->dieUsageMsg(array('missingparam', 'interwikipage')); + $source = ImportStreamSource::newFromInterwiki( + $params['interwikisource'], + $params['interwikipage'], + $params['fullhistory'], + $params['templates']); + } + else + { + $isUpload = true; + if(!$wgUser->isAllowed('importupload')) + $this->dieUsageMsg(array('cantimport-upload')); + $source = ImportStreamSource::newFromUpload('xml'); + } + if($source instanceof WikiErrorMsg) + $this->dieUsageMsg(array_merge( + array($source->getMessageKey()), + $source->getMessageArgs())); + else if(WikiError::isError($source)) + // This shouldn't happen + $this->dieUsageMsg(array('import-unknownerror', $source->getMessage())); + + $importer = new WikiImporter($source); + if(isset($params['namespace'])) + $importer->setTargetNamespace($params['namespace']); + $reporter = new ApiImportReporter($importer, $isUpload, + $params['interwikisource'], + $params['summary']); + + $result = $importer->doImport(); + if($result instanceof WikiXmlError) + $this->dieUsageMsg(array('import-xml-error', + $result->mLine, + $result->mColumn, + $result->mByte . $result->mContext, + xml_error_string($result->mXmlError))); + else if(WikiError::isError($result)) + // This shouldn't happen + $this->dieUsageMsg(array('import-unknownerror', $result->getMessage())); + $resultData = $reporter->getData(); + $this->getResult()->setIndexedTagName($resultData, 'page'); + $this->getResult()->addValue(null, $this->getModuleName(), $resultData); + } + + public function mustBePosted() { return true; } + + public function isWriteMode() { + return true; + } + + public function getAllowedParams() { + global $wgImportSources; + return array ( + 'token' => null, + 'summary' => null, + 'xml' => null, + 'interwikisource' => array( + ApiBase :: PARAM_TYPE => $wgImportSources + ), + 'interwikipage' => null, + 'fullhistory' => false, + 'templates' => false, + 'namespace' => array( + ApiBase :: PARAM_TYPE => 'namespace' + ) + ); + } + + public function getParamDescription() { + return array ( + 'token' => 'Import token obtained through prop=info', + 'summary' => 'Import summary', + 'xml' => 'Uploaded XML file', + 'interwikisource' => 'For interwiki imports: wiki to import from', + 'interwikipage' => 'For interwiki imports: page to import', + 'fullhistory' => 'For interwiki imports: import the full history, not just the current version', + 'templates' => 'For interwiki imports: import all included templates as well', + 'namespace' => 'For interwiki imports: import to this namespace', + ); + } + + public function getDescription() { + return array ( + 'Import a page from another wiki, or an XML file' + ); + } + + protected function getExamples() { + return array( + 'Import [[meta:Help:Parserfunctions]] to namespace 100 with full history:', + ' api.php?action=import&interwikisource=meta&interwikipage=Help:ParserFunctions&namespace=100&fullhistory&token=123ABC', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiImport.php 48091 2009-03-06 13:49:44Z catrope $'; + } +} + +/** + * Import reporter for the API + * @ingroup API + */ +class ApiImportReporter extends ImportReporter { + private $mResultArr = array(); + + function reportPage($title, $origTitle, $revisionCount, $successCount) + { + // Add a result entry + $r = array(); + ApiQueryBase::addTitleInfo($r, $title); + $r['revisions'] = intval($successCount); + $this->mResultArr[] = $r; + + // Piggyback on the parent to do the logging + parent::reportPage($title, $origTitle, $revisionCount, $successCount); + } + + function getData() + { + return $this->mResultArr; + } +} diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index 43b30f7c..bc477e1d 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -82,7 +82,7 @@ class ApiLogin extends ApiBase { wfRunHooks('UserLoginComplete', array(&$wgUser, &$injected_html)); $result['result'] = 'Success'; - $result['lguserid'] = $wgUser->getId(); + $result['lguserid'] = intval($wgUser->getId()); $result['lgusername'] = $wgUser->getName(); $result['lgtoken'] = $wgUser->getToken(); $result['cookieprefix'] = $wgCookiePrefix; @@ -114,7 +114,7 @@ class ApiLogin extends ApiBase { case LoginForm :: THROTTLED : global $wgPasswordAttemptThrottle; $result['result'] = 'Throttled'; - $result['wait'] = $wgPasswordAttemptThrottle['seconds']; + $result['wait'] = intval($wgPasswordAttemptThrottle['seconds']); break; default : ApiBase :: dieDebug(__METHOD__, "Unhandled case value: {$authRes}"); @@ -125,6 +125,10 @@ class ApiLogin extends ApiBase { public function mustBePosted() { return true; } + public function isReadMode() { + return false; + } + public function getAllowedParams() { return array ( 'name' => null, @@ -158,6 +162,6 @@ class ApiLogin extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiLogin.php 45275 2009-01-01 02:02:03Z simetrical $'; + return __CLASS__ . ': $Id: ApiLogin.php 48091 2009-03-06 13:49:44Z catrope $'; } } diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php index 8b178f6a..0af579ca 100644 --- a/includes/api/ApiLogout.php +++ b/includes/api/ApiLogout.php @@ -50,6 +50,10 @@ class ApiLogout extends ApiBase { wfRunHooks( 'UserLogoutComplete', array(&$wgUser, &$injected_html, $oldName) ); } + public function isReadMode() { + return false; + } + public function getAllowedParams() { return array (); } @@ -71,6 +75,6 @@ class ApiLogout extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiLogout.php 43522 2008-11-15 01:23:39Z brion $'; + return __CLASS__ . ': $Id: ApiLogout.php 48091 2009-03-06 13:49:44Z catrope $'; } } diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 60d932be..ffdeb1e8 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -65,10 +65,9 @@ class ApiMain extends ApiBase { 'feedwatchlist' => 'ApiFeedWatchlist', 'help' => 'ApiHelp', 'paraminfo' => 'ApiParamInfo', - 'purge' => 'ApiPurge', - ); - private static $WriteModules = array ( + // Write modules + 'purge' => 'ApiPurge', 'rollback' => 'ApiRollback', 'delete' => 'ApiDelete', 'undelete' => 'ApiUndelete', @@ -80,6 +79,7 @@ class ApiMain extends ApiBase { 'emailuser' => 'ApiEmailUser', 'watch' => 'ApiWatch', 'patrol' => 'ApiPatrol', + 'import' => 'ApiImport', ); /** @@ -149,20 +149,10 @@ class ApiMain extends ApiBase { wfDebug( "API: stripping user credentials for JSON callback\n" ); $wgUser = new User(); } - - if (!$wgUser->isAllowed('read')) { - self::$Modules = array( - 'login' => self::$Modules['login'], - 'logout' => self::$Modules['logout'], - 'help' => self::$Modules['help'], - ); - } } - global $wgAPIModules, $wgEnableWriteAPI; // extension modules + global $wgAPIModules; // extension modules $this->mModules = $wgAPIModules + self :: $Modules; - if($wgEnableWriteAPI) - $this->mModules += self::$WriteModules; $this->mModuleNames = array_keys($this->mModules); $this->mFormats = self :: $Formats; @@ -200,22 +190,10 @@ class ApiMain extends ApiBase { } /** - * This method will simply cause an error if the write mode was disabled - * or if the current user doesn't have the right to use it + * Only kept for backwards compatibility + * @deprecated Use isWriteMode() instead */ - public function requestWriteMode() { - global $wgUser; - if (!$this->mEnableWrite) - $this->dieUsage('Editing of this wiki through the API' . - ' is disabled. Make sure the $wgEnableWriteAPI=true; ' . - 'statement is included in the wiki\'s ' . - 'LocalSettings.php file', 'noapiwrite'); - if (!$wgUser->isAllowed('writeapi')) - $this->dieUsage('You\'re not allowed to edit this ' . - 'wiki through the API', 'writeapidenied'); - if (wfReadOnly()) - $this->dieUsageMsg(array('readonlytext')); - } + public function requestWriteMode() {} /** * Set how long the response should be cached. @@ -360,9 +338,11 @@ class ApiMain extends ApiBase { } $this->getResult()->reset(); + $this->getResult()->disableSizeCheck(); // Re-add the id - if($this->mRequest->getCheck('requestid')) - $this->getResult()->addValue(null, 'requestid', $this->mRequest->getVal('requestid')); + $requestid = $this->getParameter('requestid'); + if(!is_null($requestid)) + $this->getResult()->addValue(null, 'requestid', $requestid); $this->getResult()->addValue(null, 'error', $errMessage); return $errMessage['code']; @@ -373,8 +353,9 @@ class ApiMain extends ApiBase { */ protected function executeAction() { // First add the id to the top element - if($this->mRequest->getCheck('requestid')) - $this->getResult()->addValue(null, 'requestid', $this->mRequest->getVal('requestid')); + $requestid = $this->getParameter('requestid'); + if(!is_null($requestid)) + $this->getResult()->addValue(null, 'requestid', $requestid); $params = $this->extractRequestParams(); @@ -398,14 +379,26 @@ class ApiMain extends ApiBase { header( 'X-Database-Lag: ' . intval( $lag ) ); // XXX: should we return a 503 HTTP error code like wfMaxlagError() does? if( $wgShowHostnames ) { - ApiBase :: dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' ); + $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' ); } else { - ApiBase :: dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' ); + $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' ); } return; } } + global $wgUser; + if ($module->isReadMode() && !$wgUser->isAllowed('read')) + $this->dieUsageMsg(array('readrequired')); + if ($module->isWriteMode()) { + if (!$this->mEnableWrite) + $this->dieUsageMsg(array('writedisabled')); + if (!$wgUser->isAllowed('writeapi')) + $this->dieUsageMsg(array('writerequired')); + if (wfReadOnly()) + $this->dieUsageMsg(array('readonlytext')); + } + if (!$this->mInternalMode) { // Ignore mustBePosted() for internal calls if($module->mustBePosted() && !$this->mRequest->wasPosted()) @@ -438,7 +431,7 @@ class ApiMain extends ApiBase { * Print results using the current printer */ protected function printResult($isError) { - $this->getResult()->cleanupUTF8(); + $this->getResult()->cleanUpUTF8(); $printer = $this->mPrinter; $printer->profileIn(); @@ -454,6 +447,10 @@ class ApiMain extends ApiBase { $printer->closePrinter(); $printer->profileOut(); } + + public function isReadMode() { + return false; + } /** * See ApiBase for description. @@ -657,7 +654,7 @@ class ApiMain extends ApiBase { public function getVersion() { $vers = array (); $vers[] = 'MediaWiki: ' . SpecialVersion::getVersion() . "\n http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/"; - $vers[] = __CLASS__ . ': $Id: ApiMain.php 45752 2009-01-14 21:36:57Z catrope $'; + $vers[] = __CLASS__ . ': $Id: ApiMain.php 50834 2009-05-20 20:10:47Z catrope $'; $vers[] = ApiBase :: getBaseVersion(); $vers[] = ApiFormatBase :: getBaseVersion(); $vers[] = ApiQueryBase :: getBaseVersion(); diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index 13b058c9..e22d0294 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -39,7 +39,6 @@ class ApiMove extends ApiBase { public function execute() { global $wgUser; - $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); if(is_null($params['reason'])) $params['reason'] = ''; @@ -73,6 +72,7 @@ class ApiMove extends ApiBase { $this->dieUsageMsg(array('invalidtitle', $params['to'])); $toTalk = $toTitle->getTalkPage(); + # Move the page $hookErr = null; $retval = $fromTitle->moveTo($toTitle, true, $params['reason'], !$params['noredirect']); if($retval !== true) @@ -82,10 +82,9 @@ class ApiMove extends ApiBase { if(!$params['noredirect'] || !$wgUser->isAllowed('suppressredirect')) $r['redirectcreated'] = ''; + # Move the talk page if($params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage()) { - // We need to move the talk page as well - $toTalk = $toTitle->getTalkPage(); $retval = $fromTalk->moveTo($toTalk, true, $params['reason'], !$params['noredirect']); if($retval === true) { @@ -101,6 +100,20 @@ class ApiMove extends ApiBase { } } + # Move subpages + if($params['movesubpages']) + { + $r['subpages'] = $this->moveSubpages($fromTitle, $toTitle, + $params['reason'], $params['noredirect']); + $this->getResult()->setIndexedTagName($r['subpages'], 'subpage'); + if($params['movetalk']) + { + $r['subpages-talk'] = $this->moveSubpages($fromTalk, $toTalk, + $params['reason'], $params['noredirect']); + $this->getResult()->setIndexedTagName($r['subpages-talk'], 'subpage'); + } + } + # Watch pages if($params['watch'] || $wgUser->getOption('watchmoves')) { @@ -114,9 +127,37 @@ class ApiMove extends ApiBase { } $this->getResult()->addValue(null, $this->getModuleName(), $r); } + + public function moveSubpages($fromTitle, $toTitle, $reason, $noredirect) + { + $retval = array(); + $success = $fromTitle->moveSubpages($toTitle, true, $reason, !$noredirect); + if(isset($success[0])) + return array('error' => $this->parseMsg($success)); + else + { + // At least some pages could be moved + // Report each of them separately + foreach($success as $oldTitle => $newTitle) + { + $r = array('from' => $oldTitle); + if(is_array($newTitle)) + $r['error'] = $this->parseMsg(reset($newTitle)); + else + // Success + $r['to'] = $newTitle; + $retval[] = $r; + } + } + return $retval; + } public function mustBePosted() { return true; } + public function isWriteMode() { + return true; + } + public function getAllowedParams() { return array ( 'from' => null, @@ -127,6 +168,7 @@ class ApiMove extends ApiBase { 'token' => null, 'reason' => null, 'movetalk' => false, + 'movesubpages' => false, 'noredirect' => false, 'watch' => false, 'unwatch' => false @@ -141,6 +183,7 @@ class ApiMove extends ApiBase { 'token' => 'A move token previously retrieved through prop=info', 'reason' => 'Reason for the move (optional).', 'movetalk' => 'Move the talk page, if it exists.', + 'movesubpages' => 'Move subpages, if applicable', 'noredirect' => 'Don\'t create a redirect', 'watch' => 'Add the page and the redirect to your watchlist', 'unwatch' => 'Remove the page and the redirect from your watchlist' @@ -160,6 +203,6 @@ class ApiMove extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiMove.php 47041 2009-02-09 14:39:41Z catrope $'; + return __CLASS__ . ': $Id: ApiMove.php 48091 2009-03-06 13:49:44Z catrope $'; } } diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index 2da92059..8fc1f32b 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -42,10 +42,14 @@ class ApiOpenSearch extends ApiBase { } public function execute() { + global $wgEnableMWSuggest; $params = $this->extractRequestParams(); $search = $params['search']; $limit = $params['limit']; $namespaces = $params['namespace']; + $suggest = $params['suggest']; + # $wgEnableMWSuggest hit incoming when $wgEnableMWSuggest is disabled + if( $suggest && !$wgEnableMWSuggest ) return; // Open search results may be stored for a very long time $this->getMain()->setCacheMaxAge(1200); @@ -61,7 +65,7 @@ class ApiOpenSearch extends ApiBase { public function getAllowedParams() { return array ( 'search' => null, - 'limit' => array ( + 'limit' => array( ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', ApiBase :: PARAM_MIN => 1, @@ -73,6 +77,7 @@ class ApiOpenSearch extends ApiBase { ApiBase :: PARAM_TYPE => 'namespace', ApiBase :: PARAM_ISMULTI => true ), + 'suggest' => false, ); } @@ -81,6 +86,7 @@ class ApiOpenSearch extends ApiBase { 'search' => 'Search string', 'limit' => 'Maximum amount of results to return', 'namespace' => 'Namespaces to search', + 'suggest' => 'Do nothing if $wgEnableMWSuggest is false', ); } @@ -95,6 +101,6 @@ class ApiOpenSearch extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiOpenSearch.php 35098 2008-05-20 17:13:28Z ialex $'; + return __CLASS__ . ': $Id: ApiOpenSearch.php 47188 2009-02-12 17:27:05Z catrope $'; } } diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index 54482e4b..6b9e90b8 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -30,13 +30,14 @@ if (!defined('MEDIAWIKI')) { /** * This class contains a list of pages that the client has requested. - * Initially, when the client passes in titles=, pageids=, or revisions= parameter, - * an instance of the ApiPageSet class will normalize titles, - * determine if the pages/revisions exist, and prefetch any additional data page data requested. + * Initially, when the client passes in titles=, pageids=, or revisions= + * parameter, an instance of the ApiPageSet class will normalize titles, + * determine if the pages/revisions exist, and prefetch any additional page + * data requested. * - * When generator is used, the result of the generator will become the input for the - * second instance of this class, and all subsequent actions will go use the second instance - * for all their work. + * When a generator is used, the result of the generator will become the input + * for the second instance of this class, and all subsequent actions will use + * the second instance for all their work. * * @ingroup API */ @@ -52,6 +53,11 @@ class ApiPageSet extends ApiQueryBase { private $mRequestedPageFields; + /** + * Constructor + * @param $query ApiQuery + * @param $resolveRedirects bool Whether redirects should be resolved + */ public function __construct($query, $resolveRedirects = false) { parent :: __construct($query, 'query'); @@ -75,20 +81,38 @@ class ApiPageSet extends ApiQueryBase { $this->mFakePageId = -1; } + /** + * Check whether this PageSet is resolving redirects + * @return bool + */ public function isResolvingRedirects() { return $this->mResolveRedirects; } + /** + * Request an additional field from the page table. Must be called + * before execute() + * @param $fieldName string Field name + */ public function requestField($fieldName) { $this->mRequestedPageFields[$fieldName] = null; } + /** + * Get the value of a custom field previously requested through + * requestField() + * @param $fieldName string Field name + * @return mixed Field value + */ public function getCustomField($fieldName) { return $this->mRequestedPageFields[$fieldName]; } /** - * Get fields that modules have requested from the page table + * Get the fields that have to be queried from the page table: + * the ones requested through requestField() and a few basic ones + * we always need + * @return array of field names */ public function getPageTableFields() { // Ensure we get minimum required fields @@ -99,12 +123,12 @@ class ApiPageSet extends ApiQueryBase { 'page_id' => null, ); - // only store non-default fields - $this->mRequestedPageFields = array_diff_key($this->mRequestedPageFields, $pageFlds); - if ($this->mResolveRedirects) $pageFlds['page_is_redirect'] = null; + // only store non-default fields + $this->mRequestedPageFields = array_diff_key($this->mRequestedPageFields, $pageFlds); + $pageFlds = array_merge($pageFlds, $this->mRequestedPageFields); return array_keys($pageFlds); } @@ -113,6 +137,7 @@ class ApiPageSet extends ApiQueryBase { * Returns an array [ns][dbkey] => page_id for all requested titles. * page_id is a unique negative number in case title was not found. * Invalid titles will also have negative page IDs and will be in namespace 0 + * @return array */ public function getAllTitlesByNamespace() { return $this->mAllPages; @@ -128,6 +153,7 @@ class ApiPageSet extends ApiQueryBase { /** * Returns the number of unique pages (not revisions) in the set. + * @return int */ public function getTitleCount() { return count($this->mTitles); @@ -143,6 +169,7 @@ class ApiPageSet extends ApiQueryBase { /** * Returns the number of found unique pages (not revisions) in the set. + * @return int */ public function getGoodTitleCount() { return count($this->mGoodTitles); @@ -175,7 +202,8 @@ class ApiPageSet extends ApiQueryBase { } /** - * Get a list of redirects when doing redirect resolution + * Get a list of redirect resolutions - maps a title to its redirect + * target. * @return array prefixed_title (string) => prefixed_title (string) */ public function getRedirectTitles() { @@ -183,8 +211,8 @@ class ApiPageSet extends ApiQueryBase { } /** - * Get a list of title normalizations - maps the title given - * with its normalized version. + * Get a list of title normalizations - maps a title to its normalized + * version. * @return array raw_prefixed_title (string) => prefixed_title (string) */ public function getNormalizedTitles() { @@ -192,8 +220,8 @@ class ApiPageSet extends ApiQueryBase { } /** - * Get a list of interwiki titles - maps the title given - * with to the interwiki prefix. + * Get a list of interwiki titles - maps a title to its interwiki + * prefix. * @return array raw_prefixed_title (string) => interwiki_prefix (string) */ public function getInterwikiTitles() { @@ -201,7 +229,7 @@ class ApiPageSet extends ApiQueryBase { } /** - * Get the list of revision IDs (requested with revids= parameter) + * Get the list of revision IDs (requested with the revids= parameter) * @return array revID (int) => pageID (int) */ public function getRevisionIDs() { @@ -217,14 +245,15 @@ class ApiPageSet extends ApiQueryBase { } /** - * Returns the number of revisions (requested with revids= parameter) + * Returns the number of revisions (requested with revids= parameter)\ + * @return int */ public function getRevisionCount() { return count($this->getRevisionIDs()); } /** - * Populate from the request parameters + * Populate the PageSet from the request parameters. */ public function execute() { $this->profileIn(); @@ -267,7 +296,8 @@ class ApiPageSet extends ApiQueryBase { } /** - * Initialize PageSet from a list of Titles + * Populate this PageSet from a list of Titles + * @param $titles array of Title objects */ public function populateFromTitles($titles) { $this->profileIn(); @@ -276,7 +306,8 @@ class ApiPageSet extends ApiQueryBase { } /** - * Initialize PageSet from a list of Page IDs + * Populate this PageSet from a list of page IDs + * @param $pageIDs array of page IDs */ public function populateFromPageIDs($pageIDs) { $this->profileIn(); @@ -285,7 +316,9 @@ class ApiPageSet extends ApiQueryBase { } /** - * Initialize PageSet from a rowset returned from the database + * Populate this PageSet from a rowset returned from the database + * @param $db Database object + * @param $queryResult Query result object */ public function populateFromQueryResult($db, $queryResult) { $this->profileIn(); @@ -294,17 +327,18 @@ class ApiPageSet extends ApiQueryBase { } /** - * Initialize PageSet from a list of Revision IDs + * Populate this PageSet from a list of revision IDs + * @param $revIDs array of revision IDs */ public function populateFromRevisionIDs($revIDs) { $this->profileIn(); - $revIDs = array_map('intval', $revIDs); // paranoia $this->initFromRevIDs($revIDs); $this->profileOut(); } /** * Extract all requested fields from the row received from the database + * @param $row Result row */ public function processDbRow($row) { @@ -325,6 +359,9 @@ class ApiPageSet extends ApiQueryBase { $fieldValues[$pageId] = $row-> $fieldName; } + /** + * Resolve redirects, if applicable + */ public function finishPageSetGeneration() { $this->profileIn(); $this->resolvePendingRedirects(); @@ -341,9 +378,11 @@ class ApiPageSet extends ApiQueryBase { * * Additionally, when resolving redirects: * #3 If no more redirects left, stop. - * #4 For each redirect, get its links from `pagelinks` table. + * #4 For each redirect, get its target from the `redirect` table. * #5 Substitute the original LinkBatch object with the new list * #6 Repeat from step #1 + * + * @param $titles array of Title objects or strings */ private function initFromTitles($titles) { @@ -357,7 +396,8 @@ class ApiPageSet extends ApiQueryBase { // Get pageIDs data from the `page` table $this->profileDBIn(); - $res = $db->select('page', $this->getPageTableFields(), $set, __METHOD__); + $res = $db->select('page', $this->getPageTableFields(), $set, + __METHOD__); $this->profileDBOut(); // Hack: get the ns:titles stored in array(ns => array(titles)) format @@ -367,6 +407,10 @@ class ApiPageSet extends ApiQueryBase { $this->resolvePendingRedirects(); } + /** + * Does the same as initFromTitles(), but is based on page IDs instead + * @param $pageids array of page IDs + */ private function initFromPageIds($pageids) { if(!count($pageids)) return; @@ -375,12 +419,12 @@ class ApiPageSet extends ApiQueryBase { $set = array ( 'page_id' => $pageids ); - $db = $this->getDB(); // Get pageIDs data from the `page` table $this->profileDBIn(); - $res = $db->select('page', $this->getPageTableFields(), $set, __METHOD__); + $res = $db->select('page', $this->getPageTableFields(), $set, + __METHOD__); $this->profileDBOut(); $remaining = array_flip($pageids); @@ -395,12 +439,11 @@ class ApiPageSet extends ApiQueryBase { * 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). + * @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)) @@ -448,30 +491,36 @@ class ApiPageSet extends ApiQueryBase { } } + /** + * Does the same as initFromTitles(), but is based on revision IDs + * instead + * @param $revids array of revision IDs + */ private function initFromRevIDs($revids) { if(!count($revids)) return; + $revids = array_map('intval', $revids); // paranoia $db = $this->getDB(); $pageids = array(); $remaining = array_flip($revids); - $tables = array('revision','page'); - $fields = array('rev_id','rev_page'); - $where = array('rev_deleted' => 0, 'rev_id' => $revids,'rev_page = page_id'); + $tables = array('revision', 'page'); + $fields = array('rev_id', 'rev_page'); + $where = array('rev_id' => $revids, 'rev_page = page_id'); // Get pageIDs data from the `page` table $this->profileDBIn(); - $res = $db->select( $tables, $fields, $where, __METHOD__ ); - while ( $row = $db->fetchObject( $res ) ) { + $res = $db->select($tables, $fields, $where, __METHOD__); + while ($row = $db->fetchObject($res)) { $revid = intval($row->rev_id); $pageid = intval($row->rev_page); $this->mGoodRevIDs[$revid] = $pageid; $pageids[$pageid] = ''; unset($remaining[$revid]); } - $db->freeResult( $res ); + $db->freeResult($res); $this->profileDBOut(); $this->mMissingRevIDs = array_keys($remaining); @@ -480,6 +529,11 @@ class ApiPageSet extends ApiQueryBase { $this->initFromPageIds(array_keys($pageids)); } + /** + * Resolve any redirects in the result if redirect resolution was + * requested. This function is called repeatedly until all redirects + * have been resolved. + */ private function resolvePendingRedirects() { if($this->mResolveRedirects) { @@ -498,7 +552,7 @@ class ApiPageSet extends ApiQueryBase { break; $set = $linkBatch->constructSet('page', $db); - if(false === $set) + if($set === false) break; // Get pageIDs data from the `page` table @@ -512,6 +566,13 @@ class ApiPageSet extends ApiQueryBase { } } + /** + * Get the targets of the pending redirects from the database + * + * Also creates entries in the redirect table for redirects that don't + * have one. + * @return LinkBatch + */ private function getRedirectTargets() { $lb = new LinkBatch(); $db = $this->getDB(); @@ -562,7 +623,8 @@ class ApiPageSet extends ApiQueryBase { * This method validates access rights for the title, * and appends normalization values to the output. * - * @return LinkBatch of title objects. + * @param $titles array of Title objects or strings + * @return LinkBatch */ private function processTitlesArray($titles) { @@ -592,9 +654,11 @@ class ApiPageSet extends ApiQueryBase { $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 + // Make sure we remember the original title that was + // given to us. This way the caller can correlate new + // titles with the originally requested when e.g. the + // namespace is localized or the capitalization is + // different if (is_string($title) && $title !== $titleObj->getPrefixedText()) { $this->mNormalizedTitles[$title] = $titleObj->getPrefixedText(); } @@ -628,6 +692,6 @@ class ApiPageSet extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiPageSet.php 45275 2009-01-01 02:02:03Z simetrical $'; + return __CLASS__ . ': $Id: ApiPageSet.php 47424 2009-02-18 05:29:11Z werdna $'; } } diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index 2cf044cf..d710c206 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -41,6 +41,7 @@ class ApiParamInfo extends ApiBase { // Get parameters $params = $this->extractRequestParams(); $result = $this->getResult(); + $queryObj = new ApiQuery($this->getMain(), 'query'); $r = array(); if(is_array($params['modules'])) { @@ -61,7 +62,6 @@ class ApiParamInfo extends ApiBase { } if(is_array($params['querymodules'])) { - $queryObj = new ApiQuery($this->getMain(), 'query'); $qmodArr = $queryObj->getModules(); foreach($params['querymodules'] as $qm) { @@ -77,6 +77,13 @@ class ApiParamInfo extends ApiBase { } $result->setIndexedTagName($r['querymodules'], 'module'); } + if($params['mainmodule']) + $r['mainmodule'] = $this->getClassInfo($this->getMain()); + if($params['pagesetmodule']) + { + $pageSet = new ApiPageSet($queryObj); + $r['pagesetmodule'] = $this->getClassInfo($pageSet); + } $result->addValue(null, $this->getModuleName(), $r); } @@ -86,6 +93,12 @@ class ApiParamInfo extends ApiBase { $retval['classname'] = get_class($obj); $retval['description'] = (is_array($obj->getDescription()) ? implode("\n", $obj->getDescription()) : $obj->getDescription()); $retval['prefix'] = $obj->getModulePrefix(); + if($obj->isReadMode()) + $retval['readrights'] = ''; + if($obj->isWriteMode()) + $retval['writerights'] = ''; + if($obj->mustBePosted()) + $retval['mustbeposted'] = ''; $allowedParams = $obj->getFinalParams(); if(!is_array($allowedParams)) return $retval; @@ -140,6 +153,10 @@ class ApiParamInfo extends ApiBase { return $retval; } + public function isReadMode() { + return false; + } + public function getAllowedParams() { return array ( 'modules' => array( @@ -147,7 +164,9 @@ class ApiParamInfo extends ApiBase { ), 'querymodules' => array( ApiBase :: PARAM_ISMULTI => true - ) + ), + 'mainmodule' => false, + 'pagesetmodule' => false, ); } @@ -155,6 +174,8 @@ class ApiParamInfo extends ApiBase { return array ( 'modules' => 'List of module names (value of the action= parameter)', 'querymodules' => 'List of query module names (value of prop=, meta= or list= parameter)', + 'mainmodule' => 'Get information about the main (top-level) module as well', + 'pagesetmodule' => 'Get information about the pageset module (providing titles= and friends) as well', ); } @@ -169,6 +190,6 @@ class ApiParamInfo extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiParamInfo.php 41653 2008-10-04 15:03:03Z catrope $'; + return __CLASS__ . ': $Id: ApiParamInfo.php 48091 2009-03-06 13:49:44Z catrope $'; } } diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index e221fb1d..8f4b70bf 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -105,7 +105,7 @@ class ApiParse extends ApiBase { $p_result = $wgParser->parse($articleObj->getContent(), $titleObj, $popts); global $wgUseParserCache; if($wgUseParserCache) - $pcache->save($p_result, $articleObj, $wgUser); + $pcache->save($p_result, $articleObj, $popts); } } } @@ -151,8 +151,12 @@ class ApiParse extends ApiBase { $result_array['externallinks'] = array_keys($p_result->getExternalLinks()); if(isset($prop['sections'])) $result_array['sections'] = $p_result->getSections(); + if(isset($prop['displaytitle'])) + $result_array['displaytitle'] = $p_result->getDisplayTitle() ? + $p_result->getDisplayTitle() : + $titleObj->getPrefixedText(); if(!is_null($oldid)) - $result_array['revid'] = $oldid; + $result_array['revid'] = intval($oldid); $result_mapping = array( 'redirects' => 'r', @@ -223,7 +227,7 @@ class ApiParse extends ApiBase { 'redirects' => false, 'oldid' => null, 'prop' => array( - ApiBase :: PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid', + ApiBase :: PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid|displaytitle', ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => array( 'text', @@ -234,7 +238,8 @@ class ApiParse extends ApiBase { 'images', 'externallinks', 'sections', - 'revid' + 'revid', + 'displaytitle', ) ), 'pst' => false, @@ -272,6 +277,6 @@ class ApiParse extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiParse.php 44858 2008-12-20 20:00:07Z catrope $'; + return __CLASS__ . ': $Id: ApiParse.php 48544 2009-03-18 23:27:48Z aboostani $'; } } diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php index 08de87b0..2c9d1ecf 100644 --- a/includes/api/ApiPatrol.php +++ b/includes/api/ApiPatrol.php @@ -42,7 +42,6 @@ class ApiPatrol extends ApiBase { */ public function execute() { global $wgUser, $wgUseRCPatrol, $wgUseNPPatrol; - $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); if(!isset($params['token'])) @@ -58,13 +57,17 @@ class ApiPatrol extends ApiBase { $retval = RecentChange::markPatrolled($params['rcid']); if($retval) - $this->dieUsageMsg(current($retval)); + $this->dieUsageMsg(reset($retval)); - $result = array('rcid' => $rc->getAttribute('rc_id')); + $result = array('rcid' => intval($rc->getAttribute('rc_id'))); ApiQueryBase::addTitleInfo($result, $rc->getTitle()); $this->getResult()->addValue(null, $this->getModuleName(), $result); } + public function isWriteMode() { + return true; + } + public function getAllowedParams() { return array ( 'token' => null, @@ -94,6 +97,6 @@ class ApiPatrol extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiPatrol.php 42548 2008-10-25 14:04:43Z tstarling $'; + return __CLASS__ . ': $Id: ApiPatrol.php 48122 2009-03-07 12:58:41Z catrope $'; } } diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php index 522d02b2..aad37066 100644 --- a/includes/api/ApiProtect.php +++ b/includes/api/ApiProtect.php @@ -38,7 +38,6 @@ class ApiProtect extends ApiBase { public function execute() { global $wgUser, $wgRestrictionTypes, $wgRestrictionLevels; - $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); $titleObj = NULL; @@ -59,7 +58,7 @@ class ApiProtect extends ApiBase { $errors = $titleObj->getUserPermissionsErrors('protect', $wgUser); if($errors) // We don't care about multiple errors, just report one of them - $this->dieUsageMsg(current($errors)); + $this->dieUsageMsg(reset($errors)); $expiry = (array)$params['expiry']; if(count($expiry) != count($params['protections'])) @@ -106,10 +105,12 @@ class ApiProtect extends ApiBase { } $cascade = $params['cascade']; - if($titleObj->exists()) { - $articleObj = new Article($titleObj); + $articleObj = new Article($titleObj); + if($params['watch']) + $articleObj->doWatch(); + if($titleObj->exists()) $ok = $articleObj->updateRestrictions($protections, $params['reason'], $cascade, $expiryarray); - } else + else $ok = $titleObj->updateTitleProtection($protections['create'], $params['reason'], $expiryarray['create']); if(!$ok) // This is very weird. Maybe the article was deleted or the user was blocked/desysopped in the meantime? @@ -125,6 +126,10 @@ class ApiProtect extends ApiBase { public function mustBePosted() { return true; } + public function isWriteMode() { + return true; + } + public function getAllowedParams() { return array ( 'title' => null, @@ -138,7 +143,8 @@ class ApiProtect extends ApiBase { ApiBase :: PARAM_DFLT => 'infinite', ), 'reason' => '', - 'cascade' => false + 'cascade' => false, + 'watch' => false, ); } @@ -152,6 +158,7 @@ class ApiProtect extends ApiBase { 'reason' => 'Reason for (un)protecting (optional)', 'cascade' => array('Enable cascading protection (i.e. protect pages included in this page)', 'Ignored if not all protection levels are \'sysop\' or \'protect\''), + 'watch' => 'If set, add the page being (un)protected to your watchlist', ); } @@ -169,6 +176,6 @@ class ApiProtect extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiProtect.php 44426 2008-12-10 22:39:41Z catrope $'; + return __CLASS__ . ': $Id: ApiProtect.php 48122 2009-03-07 12:58:41Z catrope $'; } } diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php index d7202a46..27d5cac6 100644 --- a/includes/api/ApiPurge.php +++ b/includes/api/ApiPurge.php @@ -74,6 +74,15 @@ class ApiPurge extends ApiBase { $this->getResult()->addValue(null, $this->getModuleName(), $result); } + public function mustBePosted() { + global $wgUser; + return $wgUser->isAnon(); + } + + public function isWriteMode() { + return true; + } + public function getAllowedParams() { return array ( 'titles' => array( @@ -101,6 +110,6 @@ class ApiPurge extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiPurge.php 41020 2008-09-19 00:21:03Z demon $'; + return __CLASS__ . ': $Id: ApiPurge.php 48091 2009-03-06 13:49:44Z catrope $'; } } diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index 45a5667a..149e4082 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -29,13 +29,13 @@ if (!defined('MEDIAWIKI')) { } /** - * This is the main query class. It behaves similar to ApiMain: based on the parameters given, - * it will create a list of titles to work on (an instance of the ApiPageSet object) - * instantiate and execute various property/list/meta modules, - * and assemble all resulting data into a single ApiResult object. + * This is the main query class. It behaves similar to ApiMain: based on the + * parameters given, it will create a list of titles to work on (an ApiPageSet + * object), instantiate and execute various property/list/meta modules, and + * assemble all resulting data into a single ApiResult object. * - * In the generator mode, a generator will be first executed to populate a second ApiPageSet object, - * and that object will be used for all subsequent modules. + * In generator mode, a generator will be executed first to populate a second + * ApiPageSet object, and that object will be used for all subsequent modules. * * @ingroup API */ @@ -80,6 +80,7 @@ class ApiQuery extends ApiBase { 'exturlusage' => 'ApiQueryExtLinksUsage', 'users' => 'ApiQueryUsers', 'random' => 'ApiQueryRandom', + 'protectedtitles' => 'ApiQueryProtectedTitles', ); private $mQueryMetaModules = array ( @@ -111,6 +112,8 @@ class ApiQuery extends ApiBase { /** * Helper function to append any add-in modules to the list + * @param $modules array Module array + * @param $newModules array Module array to add to $modules */ private static function appendUserModules(&$modules, $newModules) { if (is_array( $newModules )) { @@ -122,6 +125,7 @@ class ApiQuery extends ApiBase { /** * Gets a default slave database connection object + * @return Database */ public function getDB() { if (!isset ($this->mSlaveDB)) { @@ -136,7 +140,11 @@ class ApiQuery extends ApiBase { * Get the query database connection with the given name. * If no such connection has been requested before, it will be created. * Subsequent calls with the same $name will return the same connection - * as the first, regardless of $db or $groups new values. + * as the first, regardless of the values of $db and $groups + * @param $name string Name to assign to the database connection + * @param $db int One of the DB_* constants + * @param $groups array Query groups + * @return Database */ public function getNamedDB($name, $db, $groups) { if (!array_key_exists($name, $this->mNamedDB)) { @@ -149,6 +157,7 @@ class ApiQuery extends ApiBase { /** * Gets the set of pages the user has requested (or generated) + * @return ApiPageSet */ public function getPageSet() { return $this->mPageSet; @@ -156,15 +165,26 @@ class ApiQuery extends ApiBase { /** * Get the array mapping module names to class names + * @return array(modulename => classname) */ function getModules() { return array_merge($this->mQueryPropModules, $this->mQueryListModules, $this->mQueryMetaModules); } + + public function getCustomPrinter() { + // If &exportnowrap is set, use the raw formatter + if ($this->getParameter('export') && + $this->getParameter('exportnowrap')) + return new ApiFormatRaw($this->getMain(), + $this->getMain()->createPrinterByName('xml')); + else + return null; + } /** * 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 + * #2 If using a generator, execute it to get a new ApiPageSet object * #3 Instantiate all requested modules. * This way the PageSet object will know what shared data is required, * and minimize DB calls. @@ -220,6 +240,8 @@ class ApiQuery extends ApiBase { * Query modules may optimize data requests through the $this->getPageSet() object * by adding extra fields from the page table. * This function will gather all the extra request fields from the modules. + * @param $modules array of module objects + * @param $pageSet ApiPageSet */ private function addCustomFldsToPageSet($modules, $pageSet) { // Query all requested modules. @@ -230,6 +252,9 @@ class ApiQuery extends ApiBase { /** * Create instances of all modules requested by the client + * @param $modules array to append instatiated modules to + * @param $param string Parameter name to read modules from + * @param $moduleList array(modulename => classname) */ private function InstantiateModules(&$modules, $param, $moduleList) { $list = @$this->params[$param]; @@ -239,14 +264,19 @@ class ApiQuery extends ApiBase { } /** - * Appends an element for each page in the current pageSet with the most general - * information (id, title), plus any title normalizations and missing or invalid title/pageids/revids. + * Appends an element for each page in the current pageSet with the + * most general information (id, title), plus any title normalizations + * and missing or invalid title/pageids/revids. */ private function outputGeneralPageInfo() { $pageSet = $this->getPageSet(); $result = $this->getResult(); + # We don't check for a full result set here because we can't be adding + # more than 380K. The maximum revision size is in the megabyte range, + # and the maximum result size must be even higher than that. + // Title normalizations $normValues = array (); foreach ($pageSet->getNormalizedTitles() as $rawTitleStr => $titleStr) { @@ -346,12 +376,43 @@ class ApiQuery extends ApiBase { } $result->setIndexedTagName($pages, 'page'); - $result->addValue('query', 'pages', $pages); + $result->addValue('query', 'pages', $pages); + } + if ($this->params['export']) { + $exporter = new WikiExporter($this->getDB()); + // WikiExporter writes to stdout, so catch its + // output with an ob + ob_start(); + $exporter->openStream(); + foreach (@$pageSet->getGoodTitles() as $title) + if ($title->userCanRead()) + $exporter->pageByTitle($title); + $exporter->closeStream(); + $exportxml = ob_get_contents(); + ob_end_clean(); + // Don't check the size of exported stuff + // It's not continuable, so it would cause more + // problems than it'd solve + $result->disableSizeCheck(); + if ($this->params['exportnowrap']) { + $result->reset(); + // Raw formatter will handle this + $result->addValue(null, 'text', $exportxml); + $result->addValue(null, 'mime', 'text/xml'); + } else { + $r = array(); + ApiResult::setContent($r, $exportxml); + $result->addValue('query', 'export', $r); + } + $result->enableSizeCheck(); } } /** - * For generator mode, execute generator, and use its output as new pageSet + * For generator mode, execute generator, and use its output as new + * ApiPageSet + * @param $generatorName string Module name + * @param $modules array of module objects */ protected function executeGeneratorModule($generatorName, $modules) { @@ -392,10 +453,6 @@ class ApiQuery extends ApiBase { $this->mPageSet = $resultPageSet; } - /** - * Returns the list of allowed parameters for this module. - * Qurey module also lists all ApiPageSet parameters as its own. - */ public function getAllowedParams() { return array ( 'prop' => array ( @@ -415,11 +472,14 @@ class ApiQuery extends ApiBase { ), 'redirects' => false, 'indexpageids' => false, + 'export' => false, + 'exportnowrap' => false, ); } /** * Override the parent to generate help messages for all available query modules. + * @return string */ public function makeHelpMsg() { @@ -450,10 +510,13 @@ class ApiQuery extends ApiBase { /** * For all modules in $moduleList, generate help messages and join them together + * @param $moduleList array(modulename => classname) + * @param $paramName string Parameter name + * @return string */ private function makeHelpMsgHelper($moduleList, $paramName) { - $moduleDscriptions = array (); + $moduleDescriptions = array (); foreach ($moduleList as $moduleName => $moduleClass) { $module = new $moduleClass ($this, $moduleName, null); @@ -466,14 +529,15 @@ class ApiQuery extends ApiBase { $this->mAllowedGenerators[] = $moduleName; $msg .= "Generator:\n This module may be used as a generator\n"; } - $moduleDscriptions[] = $msg; + $moduleDescriptions[] = $msg; } - return implode("\n", $moduleDscriptions); + return implode("\n", $moduleDescriptions); } /** * Override to add extra parameters from PageSet + * @return string */ public function makeHelpMsgParameters() { $psModule = new ApiPageSet($this); @@ -489,31 +553,35 @@ class ApiQuery extends ApiBase { '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', + 'generator' => array('Use the output of a list as the input for other prop/list/meta items', + 'NOTE: generator parameter names must be prefixed with a \'g\', see examples.'), 'redirects' => 'Automatically resolve redirects', - 'indexpageids' => 'Include an additional pageids section listing all returned page IDs.' + 'indexpageids' => 'Include an additional pageids section listing all returned page IDs.', + 'export' => 'Export the current revisions of all given or generated pages', + 'exportnowrap' => 'Return the export XML without wrapping it in an XML result (same format as Special:Export). Can only be used with export', ); } public 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.', + 'and is loosely based on the old query.php interface.', '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' + 'api.php?action=query&prop=revisions&meta=siteinfo&titles=Main%20Page&rvprop=user|comment', + 'api.php?action=query&generator=allpages&gapprefix=API/&prop=revisions', ); } public function getVersion() { $psModule = new ApiPageSet($this); $vers = array (); - $vers[] = __CLASS__ . ': $Id: ApiQuery.php 42548 2008-10-25 14:04:43Z tstarling $'; + $vers[] = __CLASS__ . ': $Id: ApiQuery.php 48629 2009-03-20 11:40:54Z catrope $'; $vers[] = $psModule->getVersion(); return $vers; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php index e6287eea..e835c042 100644 --- a/includes/api/ApiQueryAllCategories.php +++ b/includes/api/ApiQueryAllCategories.php @@ -103,21 +103,25 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { $item = array(); $result->setContent( $item, $titleObj->getText() ); if( isset( $prop['size'] ) ) { - $item['size'] = $row->cat_pages; + $item['size'] = intval($row->cat_pages); $item['pages'] = $row->cat_pages - $row->cat_subcats - $row->cat_files; - $item['files'] = $row->cat_files; - $item['subcats'] = $row->cat_subcats; + $item['files'] = intval($row->cat_files); + $item['subcats'] = intval($row->cat_subcats); } if( isset( $prop['hidden'] ) && $row->cat_hidden ) $item['hidden'] = ''; - $categories[] = $item; + $fit = $result->addValue(array('query', $this->getModuleName()), null, $item); + if(!$fit) + { + $this->setContinueEnumParameter('from', $this->keyToTitle($row->cat_title)); + break; + } } } $db->freeResult($res); if (is_null($resultPageSet)) { - $result->setIndexedTagName($categories, 'c'); - $result->addValue('query', $this->getModuleName(), $categories); + $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'c'); } else { $resultPageSet->populateFromTitles($pages); } @@ -171,6 +175,6 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllCategories.php 44590 2008-12-14 20:24:23Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllCategories.php 47865 2009-02-27 16:03:01Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index 9ad34aa2..7ae24665 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -101,8 +101,9 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $res = $this->select(__METHOD__); - $data = array (); + $pageids = array (); $count = 0; + $result = $this->getResult(); 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... @@ -120,10 +121,17 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $vals['fromid'] = intval($row->pl_from); if ($fld_title) { $title = Title :: makeTitle($params['namespace'], $row->pl_title); - $vals['ns'] = intval($title->getNamespace()); - $vals['title'] = $title->getPrefixedText(); + ApiQueryBase::addTitleInfo($vals, $title); + } + $fit = $result->addValue(array('query', $this->getModuleName()), null, $vals); + if(!$fit) + { + if($params['unique']) + $this->setContinueEnumParameter('from', $this->keyToTitle($row->pl_title)); + else + $this->setContinueEnumParameter('continue', $this->keyToTitle($row->pl_title) . "|" . $row->pl_from); + break; } - $data[] = $vals; } else { $pageids[] = $row->pl_from; } @@ -131,9 +139,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $db->freeResult($res); if (is_null($resultPageSet)) { - $result = $this->getResult(); - $result->setIndexedTagName($data, 'l'); - $result->addValue('query', $this->getModuleName(), $data); + $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'l'); } else { $resultPageSet->populateFromPageIDs($pageids); } @@ -190,6 +196,6 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllLinks.php 45850 2009-01-17 20:03:25Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllLinks.php 47865 2009-02-27 16:03:01Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index 8395808b..5f9ff064 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -57,11 +57,11 @@ class ApiQueryAllUsers extends ApiQueryBase { $limit = $params['limit']; $this->addTables('user', 'u1'); - if( !is_null( $params['from'] ) ) - $this->addWhere( 'u1.user_name >= ' . $db->addQuotes( $this->keyToTitle( $params['from'] ) ) ); + if (!is_null($params['from'])) + $this->addWhere('u1.user_name >= ' . $db->addQuotes($this->keyToTitle($params['from']))); - if( isset( $params['prefix'] ) ) - $this->addWhere( 'u1.user_name LIKE "' . $db->escapeLike( $this->keyToTitle( $params['prefix'] ) ) . '%"' ); + if (!is_null($params['prefix'])) + $this->addWhere('u1.user_name LIKE "' . $db->escapeLike($this->keyToTitle( $params['prefix'])) . '%"'); if (!is_null($params['group'])) { // Filter only users that belong to a given group @@ -70,6 +70,9 @@ class ApiQueryAllUsers extends ApiQueryBase { $this->addWhereFld('ug1.ug_group', $params['group']); } + if ($params['witheditsonly']) + $this->addWhere('user_editcount > 0'); + if ($fld_groups) { // Show the groups the given users belong to // request more than needed to avoid not getting all rows that belong to one user @@ -124,7 +127,16 @@ class ApiQueryAllUsers extends ApiQueryBase { if (!$row || $lastUser !== $row->user_name) { // Save the last pass's user data if (is_array($lastUserData)) - $data[] = $lastUserData; + { + $fit = $result->addValue(array('query', $this->getModuleName()), + null, $lastUserData); + if(!$fit) + { + $this->setContinueEnumParameter('from', + $this->keyToTitle($lastUserData['name'])); + break; + } + } // No more rows left if (!$row) @@ -166,8 +178,7 @@ class ApiQueryAllUsers extends ApiQueryBase { $db->freeResult($res); - $result->setIndexedTagName($data, 'u'); - $result->addValue('query', $this->getModuleName(), $data); + $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'u'); } public function getAllowedParams() { @@ -192,7 +203,8 @@ class ApiQueryAllUsers extends ApiQueryBase { ApiBase :: PARAM_MIN => 1, ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 - ) + ), + 'witheditsonly' => false, ); } @@ -205,6 +217,7 @@ class ApiQueryAllUsers extends ApiQueryBase { 'What pieces of information to include.', '`groups` property uses more server resources and may return fewer results than the limit.'), 'limit' => 'How many total user names to return.', + 'witheditsonly' => 'Only list users who have made edits', ); } @@ -219,6 +232,6 @@ class ApiQueryAllUsers extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllUsers.php 44472 2008-12-11 21:51:01Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllUsers.php 46845 2009-02-05 14:30:59Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryAllimages.php b/includes/api/ApiQueryAllimages.php index ea83c667..76d5d238 100644 --- a/includes/api/ApiQueryAllimages.php +++ b/includes/api/ApiQueryAllimages.php @@ -97,7 +97,7 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase { $res = $this->select(__METHOD__); - $data = array (); + $titles = array(); $count = 0; $result = $this->getResult(); while ($row = $db->fetchObject($res)) { @@ -110,20 +110,23 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase { if (is_null($resultPageSet)) { $file = $repo->newFileFromRow( $row ); - $data[] = array_merge(array('name' => $row->img_name), + $info = array_merge(array('name' => $row->img_name), ApiQueryImageInfo::getInfo($file, $prop, $result)); + $fit = $result->addValue(array('query', $this->getModuleName()), null, $info); + if( !$fit ) { + $this->setContinueEnumParameter('from', $this->keyToTitle($row->img_name)); + break; + } } else { - $data[] = Title::makeTitle(NS_FILE, $row->img_name); + $titles[] = Title::makeTitle(NS_IMAGE, $row->img_name); } } $db->freeResult($res); if (is_null($resultPageSet)) { - $result = $this->getResult(); - $result->setIndexedTagName($data, 'img'); - $result->addValue('query', $this->getModuleName(), $data); + $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'img'); } else { - $resultPageSet->populateFromTitles( $data ); + $resultPageSet->populateFromTitles($titles); } } @@ -202,6 +205,6 @@ class ApiQueryAllimages extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllimages.php 44121 2008-12-01 17:14:30Z vyznev $'; + return __CLASS__ . ': $Id: ApiQueryAllimages.php 46845 2009-02-05 14:30:59Z catrope $'; } } diff --git a/includes/api/ApiQueryAllmessages.php b/includes/api/ApiQueryAllmessages.php index 06683379..b19dc8fb 100644 --- a/includes/api/ApiQueryAllmessages.php +++ b/includes/api/ApiQueryAllmessages.php @@ -74,8 +74,13 @@ class ApiQueryAllmessages extends ApiQueryBase { //Get all requested messages $messages = array(); + $skip = !is_null($params['from']); foreach( $messages_target as $message ) { - $messages[$message] = wfMsg( $message ); + // Skip all messages up to $params['from'] + if($skip && $message === $params['from']) + $skip = false; + if(!$skip) + $messages[$message] = wfMsg( $message ); } //Print the result @@ -89,10 +94,14 @@ class ApiQueryAllmessages extends ApiQueryBase { } else { $result->setContent( $message, $value ); } - $messages_out[] = $message; + $fit = $result->addValue(array('query', $this->getModuleName()), null, $message); + if(!$fit) + { + $this->setContinueEnumParameter('from', $name); + break; + } } - $result->setIndexedTagName( $messages_out, 'message' ); - $result->addValue( 'query', $this->getModuleName(), $messages_out ); + $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'message'); } public function getAllowedParams() { @@ -102,6 +111,7 @@ class ApiQueryAllmessages extends ApiQueryBase { ), 'filter' => array(), 'lang' => null, + 'from' => null, ); } @@ -110,6 +120,7 @@ class ApiQueryAllmessages extends ApiQueryBase { 'messages' => 'Which messages to output. "*" means all messages', 'filter' => 'Return only messages that contain this string', 'lang' => 'Return messages in this language', + 'from' => 'Return messages starting at this message', ); } @@ -125,6 +136,6 @@ class ApiQueryAllmessages extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllmessages.php 37504 2008-07-10 14:28:09Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllmessages.php 47048 2009-02-09 19:24:28Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllpages.php index 531fa02a..3d30aba9 100644 --- a/includes/api/ApiQueryAllpages.php +++ b/includes/api/ApiQueryAllpages.php @@ -135,8 +135,8 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $this->addOption('LIMIT', $limit+1); $res = $this->select(__METHOD__); - $data = array (); $count = 0; + $result = $this->getResult(); 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... @@ -147,10 +147,16 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { if (is_null($resultPageSet)) { $title = Title :: makeTitle($row->page_namespace, $row->page_title); - $data[] = array( + $vals = array( 'pageid' => intval($row->page_id), 'ns' => intval($title->getNamespace()), 'title' => $title->getPrefixedText()); + $fit = $result->addValue(array('query', $this->getModuleName()), null, $vals); + if(!$fit) + { + $this->setContinueEnumParameter('from', $this->keyToTitle($row->page_title)); + break; + } } else { $resultPageSet->processDbRow($row); } @@ -158,9 +164,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $db->freeResult($res); if (is_null($resultPageSet)) { - $result = $this->getResult(); - $result->setIndexedTagName($data, 'p'); - $result->addValue('query', $this->getModuleName(), $data); + $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'p'); } } @@ -264,6 +268,6 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryAllpages.php 44863 2008-12-20 23:54:04Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryAllpages.php 46845 2009-02-05 14:30:59Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index f67e0044..95972392 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -38,7 +38,9 @@ if (!defined('MEDIAWIKI')) { */ class ApiQueryBacklinks extends ApiQueryGeneratorBase { - private $params, $rootTitle, $contRedirs, $contLevel, $contTitle, $contID, $redirID; + private $params, $rootTitle, $contRedirs, $contLevel, $contTitle, $contID, $redirID, $redirect; + private $bl_ns, $bl_from, $bl_table, $bl_code, $bl_title, $bl_sort, $bl_fields, $hasNS; + private $pageMap, $resultArr; // output element name, database column field prefix, database table private $backlinksSettings = array ( @@ -61,6 +63,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { public function __construct($query, $moduleName) { extract($this->backlinksSettings[$moduleName]); + $this->resultArr = array(); parent :: __construct($query, $moduleName, $code); $this->bl_ns = $prefix . '_namespace'; @@ -137,11 +140,11 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->addFields($this->bl_title); if($this->hasNS) $this->addFields($this->bl_ns); + // We can't use LinkBatch here because $this->hasNS may be false $titleWhere = array(); foreach($this->redirTitles as $t) - $titleWhere[] = "({$this->bl_title} = ".$db->addQuotes($t->getDBKey()). - ($this->hasNS ? " AND {$this->bl_ns} = '{$t->getNamespace()}'" : "") . - ")"; + $titleWhere[] = "{$this->bl_title} = ".$db->addQuotes($t->getDBKey()). + ($this->hasNS ? " AND {$this->bl_ns} = '{$t->getNamespace()}'" : ""); $this->addWhere($db->makeList($titleWhere, LIST_OR)); $this->addWhereFld('page_namespace', $this->params['namespace']); if(!is_null($this->redirID)) @@ -168,6 +171,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->addWhereFld('page_is_redirect', 0); $this->addOption('LIMIT', $this->params['limit'] + 1); $this->addOption('ORDER BY', $this->bl_sort); + $this->addOption('USE INDEX', array('page' => 'PRIMARY')); } private function run($resultPageSet = null) { @@ -187,7 +191,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $res = $this->select(__METHOD__.'::firstQuery'); $count = 0; - $this->data = array (); + $this->pageMap = array(); // Maps ns and title to pageid $this->continueStr = null; $this->redirTitles = array(); while ($row = $db->fetchObject($res)) { @@ -202,6 +206,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->extractRowInfo($row); else { + $this->pageMap[$row->page_namespace][$row->page_title] = $row->page_id; if($row->page_is_redirect) $this->redirTitles[] = Title::makeTitle($row->page_namespace, $row->page_title); $resultPageSet->processDbRow($row); @@ -222,10 +227,10 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { // We've reached the one extra which shows that there are additional pages to be had. Stop here... // We need to keep the parent page of this redir in if($this->hasNS) - $contTitle = Title::makeTitle($row->{$this->bl_ns}, $row->{$this->bl_title}); + $parentID = $this->pageMap[$row->{$this->bl_ns}][$row->{$this->bl_title}]; else - $contTitle = Title::makeTitle(NS_FILE, $row->{$this->bl_title}); - $this->continueStr = $this->getContinueRedirStr($contTitle->getArticleID(), $row->page_id); + $parentID = $this->pageMap[NS_IMAGE][$row->{$this->bl_title}]; + $this->continueStr = $this->getContinueRedirStr($parentID, $row->page_id); break; } @@ -236,41 +241,80 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } $db->freeResult($res); } - if(!is_null($this->continueStr)) - $this->setContinueEnumParameter('continue', $this->continueStr); - if (is_null($resultPageSet)) { - $resultData = array(); - foreach($this->data as $ns => $a) - foreach($a as $title => $arr) - $resultData[] = $arr; - $result = $this->getResult(); - $result->setIndexedTagName($resultData, $this->bl_code); - $result->addValue('query', $this->getModuleName(), $resultData); + // Try to add the result data in one go and pray that it fits + $fit = $this->getResult()->addValue('query', $this->getModuleName(), array_values($this->resultArr)); + if(!$fit) + { + // It didn't fit. Add elements one by one until the + // result is full. + foreach($this->resultArr as $pageID => $arr) + { + // Add the basic entry without redirlinks first + $fit = $this->getResult()->addValue( + array('query', $this->getModuleName()), + null, array_diff_key($arr, array('redirlinks' => ''))); + if(!$fit) + { + $this->continueStr = $this->getContinueStr($pageID); + break; + } + + $hasRedirs = false; + foreach((array)@$arr['redirlinks'] as $key => $redir) + { + $fit = $this->getResult()->addValue( + array('query', $this->getModuleName(), $pageID, 'redirlinks'), + $key, $redir); + if(!$fit) + { + $this->continueStr = $this->getContinueRedirStr($pageID, $redir['pageid']); + break; + } + $hasRedirs = true; + } + if($hasRedirs) + $this->getResult()->setIndexedTagName_internal( + array('query', $this->getModuleName(), $pageID, 'redirlinks'), + $this->bl_code); + if(!$fit) + break; + } + } + + $this->getResult()->setIndexedTagName_internal( + array('query', $this->getModuleName()), + $this->bl_code); } + if(!is_null($this->continueStr)) + $this->setContinueEnumParameter('continue', $this->continueStr); } private function extractRowInfo($row) { - if(!isset($this->data[$row->page_namespace][$row->page_title])) { - $this->data[$row->page_namespace][$row->page_title]['pageid'] = $row->page_id; - ApiQueryBase::addTitleInfo($this->data[$row->page_namespace][$row->page_title], Title::makeTitle($row->page_namespace, $row->page_title)); - if($row->page_is_redirect) - { - $this->data[$row->page_namespace][$row->page_title]['redirect'] = ''; - $this->redirTitles[] = Title::makeTitle($row->page_namespace, $row->page_title); - } + $this->pageMap[$row->page_namespace][$row->page_title] = $row->page_id; + $t = Title::makeTitle($row->page_namespace, $row->page_title); + $a = array('pageid' => intval($row->page_id)); + ApiQueryBase::addTitleInfo($a, $t); + if($row->page_is_redirect) + { + $a['redirect'] = ''; + $this->redirTitles[] = $t; } + // Put all the results in an array first + $this->resultArr[$a['pageid']] = $a; } private function extractRedirRowInfo($row) { - $a['pageid'] = $row->page_id; + $a['pageid'] = intval($row->page_id); ApiQueryBase::addTitleInfo($a, Title::makeTitle($row->page_namespace, $row->page_title)); if($row->page_is_redirect) $a['redirect'] = ''; $ns = $this->hasNS ? $row->{$this->bl_ns} : NS_FILE; - $this->data[$ns][$row->{$this->bl_title}]['redirlinks'][] = $a; - $this->getResult()->setIndexedTagName($this->data[$ns][$row->{$this->bl_title}]['redirlinks'], $this->bl_code); + $parentID = $this->pageMap[$ns][$row->{$this->bl_title}]; + // Put all the results in an array first + $this->resultArr[$parentID]['redirlinks'][] = $a; + $this->getResult()->setIndexedTagName($this->resultArr[$parentID]['redirlinks'], $this->bl_code); } protected function processContinue() { @@ -404,8 +448,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { "api.php?action=query&generator=embeddedin&geititle=Template:Stub&prop=info" ), 'imageusage' => array ( - "api.php?action=query&list=imageusage&iutitle=Image:Albert%20Einstein%20Head.jpg", - "api.php?action=query&generator=imageusage&giutitle=Image:Albert%20Einstein%20Head.jpg&prop=info" + "api.php?action=query&list=imageusage&iutitle=File:Albert%20Einstein%20Head.jpg", + "api.php?action=query&generator=imageusage&giutitle=File:Albert%20Einstein%20Head.jpg&prop=info" ) ); @@ -413,6 +457,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryBacklinks.php 46135 2009-01-24 13:03:40Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryBacklinks.php 50217 2009-05-05 13:12:16Z tstarling $'; } } diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 896dd00c..9d1cbcea 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -30,7 +30,8 @@ if (!defined('MEDIAWIKI')) { /** * This is a base class for all Query modules. - * It provides some common functionality such as constructing various SQL queries. + * It provides some common functionality such as constructing various SQL + * queries. * * @ingroup API */ @@ -58,8 +59,9 @@ abstract class ApiQueryBase extends ApiBase { /** * Add a set of tables to the internal array - * @param mixed $tables Table name or array of table names - * @param mixed $alias Table alias, or null for no alias. Cannot be used with multiple tables + * @param $tables mixed Table name or array of table names + * @param $alias mixed Table alias, or null for no alias. Cannot be + * used with multiple tables */ protected function addTables($tables, $alias = null) { if (is_array($tables)) { @@ -75,8 +77,8 @@ abstract class ApiQueryBase extends ApiBase { /** * Get the SQL for a table name with alias - * @param string $table Table name - * @param string $alias Alias + * @param $table string Table name + * @param $alias string Alias * @return string SQL */ protected function getAliasedName($table, $alias) { @@ -86,9 +88,11 @@ abstract class ApiQueryBase extends ApiBase { /** * Add a set of JOIN conditions to the internal array * - * JOIN conditions are formatted as array( tablename => array(jointype, conditions) - * e.g. array('page' => array('LEFT JOIN', 'page_id=rev_page')) - * @param array $join_conds JOIN conditions + * JOIN conditions are formatted as array( tablename => array(jointype, + * conditions) e.g. array('page' => array('LEFT JOIN', + * 'page_id=rev_page')) . conditions may be a string or an + * addWhere()-style array + * @param $join_conds array JOIN conditions */ protected function addJoinConds($join_conds) { if(!is_array($join_conds)) @@ -98,7 +102,7 @@ abstract class ApiQueryBase extends ApiBase { /** * Add a set of fields to select to the internal array - * @param mixed $value Field name or array of field names + * @param $value mixed Field name or array of field names */ protected function addFields($value) { if (is_array($value)) @@ -109,8 +113,8 @@ abstract class ApiQueryBase extends ApiBase { /** * Same as addFields(), but add the fields only if a condition is met - * @param mixed $value See addFields() - * @param bool $condition If false, do nothing + * @param $value mixed See addFields() + * @param $condition bool If false, do nothing * @return bool $condition */ protected function addFieldsIf($value, $condition) { @@ -130,7 +134,7 @@ abstract class ApiQueryBase extends ApiBase { * * For example, array('foo=bar', 'baz' => 3, 'bla' => 'foo') translates * to "foo=bar AND baz='3' AND bla='foo'" - * @param mixed $value String or array + * @param $value mixed String or array */ protected function addWhere($value) { if (is_array($value)) { @@ -145,8 +149,8 @@ abstract class ApiQueryBase extends ApiBase { /** * Same as addWhere(), but add the WHERE clauses only if a condition is met - * @param mixed $value See addWhere() - * @param bool $condition If false, do nothing + * @param $value mixed See addWhere() + * @param $condition boolIf false, do nothing * @return bool $condition */ protected function addWhereIf($value, $condition) { @@ -159,8 +163,8 @@ abstract class ApiQueryBase extends ApiBase { /** * Equivalent to addWhere(array($field => $value)) - * @param string $field Field name - * @param string $value Value; ignored if null or empty array; + * @param $field string Field name + * @param $value string Value; ignored if null or empty array; */ protected function addWhereFld($field, $value) { // Use count() to its full documented capabilities to simultaneously @@ -172,12 +176,16 @@ abstract class ApiQueryBase extends ApiBase { /** * Add a WHERE clause corresponding to a range, and an ORDER BY * clause to sort in the right direction - * @param string $field Field name - * @param string $dir If 'newer', sort in ascending order, otherwise sort in descending order - * @param string $start Value to start the list at. If $dir == 'newer' this is the lower boundary, otherwise it's the upper boundary - * @param string $end Value to end the list at. If $dir == 'newer' this is the upper boundary, otherwise it's the lower boundary + * @param $field string Field name + * @param $dir string If 'newer', sort in ascending order, otherwise + * sort in descending order + * @param $start string Value to start the list at. If $dir == 'newer' + * this is the lower boundary, otherwise it's the upper boundary + * @param $end string Value to end the list at. If $dir == 'newer' this + * is the upper boundary, otherwise it's the lower boundary + * @param $sort bool If false, don't add an ORDER BY clause */ - protected function addWhereRange($field, $dir, $start, $end) { + protected function addWhereRange($field, $dir, $start, $end, $sort = true) { $isDirNewer = ($dir === 'newer'); $after = ($isDirNewer ? '>=' : '<='); $before = ($isDirNewer ? '<=' : '>='); @@ -189,17 +197,20 @@ abstract class ApiQueryBase extends ApiBase { if (!is_null($end)) $this->addWhere($field . $before . $db->addQuotes($end)); - $order = $field . ($isDirNewer ? '' : ' DESC'); - if (!isset($this->options['ORDER BY'])) - $this->addOption('ORDER BY', $order); - else - $this->addOption('ORDER BY', $this->options['ORDER BY'] . ', ' . $order); + if ($sort) { + $order = $field . ($isDirNewer ? '' : ' DESC'); + if (!isset($this->options['ORDER BY'])) + $this->addOption('ORDER BY', $order); + else + $this->addOption('ORDER BY', $this->options['ORDER BY'] . ', ' . $order); + } } /** - * Add an option such as LIMIT or USE INDEX - * @param string $name Option name - * @param string $value Option value + * Add an option such as LIMIT or USE INDEX. If an option was set + * before, the old value will be overwritten + * @param $name string Option name + * @param $value string Option value */ protected function addOption($name, $value = null) { if (is_null($value)) @@ -210,7 +221,8 @@ abstract class ApiQueryBase extends ApiBase { /** * Execute a SELECT query based on the values in the internal arrays - * @param string $method Function the query should be attributed to. You should usually use __METHOD__ here + * @param $method string Function the query should be attributed to. + * You should usually use __METHOD__ here * @return ResultWrapper */ protected function select($method) { @@ -243,10 +255,11 @@ abstract class ApiQueryBase extends ApiBase { } /** - * Add information (title and namespace) about a Title object to a result array - * @param array $arr Result array à la ApiResult - * @param Title $title Title object - * @param string $prefix Module prefix + * Add information (title and namespace) about a Title object to a + * result array + * @param $arr array Result array à la ApiResult + * @param $title Title + * @param $prefix string Module prefix */ public static function addTitleInfo(&$arr, $title, $prefix='') { $arr[$prefix . 'ns'] = intval($title->getNamespace()); @@ -256,7 +269,7 @@ abstract class ApiQueryBase extends ApiBase { /** * Override this method to request extra fields from the pageSet * using $pageSet->requestField('fieldName') - * @param ApiPageSet $pageSet + * @param $pageSet ApiPageSet */ public function requestExtraData($pageSet) { } @@ -271,31 +284,54 @@ abstract class ApiQueryBase extends ApiBase { /** * Add a sub-element under the page element with the given page ID - * @param int $pageId Page ID - * @param array $data Data array à la ApiResult + * @param $pageId int Page ID + * @param $data array Data array à la ApiResult + * @return bool Whether the element fit in the result */ protected function addPageSubItems($pageId, $data) { $result = $this->getResult(); $result->setIndexedTagName($data, $this->getModulePrefix()); - $result->addValue(array ('query', 'pages', intval($pageId)), + return $result->addValue(array('query', 'pages', intval($pageId)), $this->getModuleName(), $data); } + + /** + * Same as addPageSubItems(), but one element of $data at a time + * @param $pageId int Page ID + * @param $data array Data array à la ApiResult + * @param $elemname string XML element name. If null, getModuleName() + * is used + * @return bool Whether the element fit in the result + */ + protected function addPageSubItem($pageId, $item, $elemname = null) { + if(is_null($elemname)) + $elemname = $this->getModulePrefix(); + $result = $this->getResult(); + $fit = $result->addValue(array('query', 'pages', $pageId, + $this->getModuleName()), null, $item); + if(!$fit) + return false; + $result->setIndexedTagName_internal(array('query', 'pages', $pageId, + $this->getModuleName()), $elemname); + return true; + } /** * Set a query-continue value - * @param $paramName Parameter name - * @param $paramValue Parameter value + * @param $paramName string Parameter name + * @param $paramValue string Parameter value */ protected function setContinueEnumParameter($paramName, $paramValue) { - $paramName = $this->encodeParamName($paramName); $msg = array( $paramName => $paramValue ); + $this->getResult()->disableSizeCheck(); $this->getResult()->addValue('query-continue', $this->getModuleName(), $msg); + $this->getResult()->enableSizeCheck(); } /** - * Get the Query database connection (readonly) + * Get the Query database connection (read-only) * @return Database */ protected function getDB() { @@ -306,12 +342,10 @@ abstract class ApiQueryBase extends ApiBase { /** * Selects the query database connection with the given name. - * If no such connection has been requested before, it will be created. - * Subsequent calls with the same $name will return the same connection - * as the first, regardless of $db or $groups new values. - * @param string $name Name to assign to the database connection - * @param int $db One of the DB_* constants - * @param array $groups Query groups + * See ApiQuery::getNamedDB() for more information + * @param $name string Name to assign to the database connection + * @param $db int One of the DB_* constants + * @param $groups array Query groups * @return Database */ public function selectNamedDB($name, $db, $groups) { @@ -328,7 +362,7 @@ abstract class ApiQueryBase extends ApiBase { /** * Convert a title to a DB key - * @param string $title Page title with spaces + * @param $title string Page title with spaces * @return string Page title with underscores */ public function titleToKey($title) { @@ -343,7 +377,7 @@ abstract class ApiQueryBase extends ApiBase { /** * The inverse of titleToKey() - * @param string $key Page title with underscores + * @param $key string Page title with underscores * @return string Page title with spaces */ public function keyToTitle($key) { @@ -359,7 +393,7 @@ abstract class ApiQueryBase extends ApiBase { /** * An alternative to titleToKey() that doesn't trim trailing spaces - * @param string $titlePart Title part with spaces + * @param $titlePart string Title part with spaces * @return string Title part with underscores */ public function titlePartToKey($titlePart) { @@ -368,7 +402,7 @@ abstract class ApiQueryBase extends ApiBase { /** * An alternative to keyToTitle() that doesn't trim trailing spaces - * @param string $keyPart Key part with spaces + * @param $keyPart string Key part with spaces * @return string Key part with underscores */ public function keyPartToTitle($keyPart) { @@ -380,7 +414,7 @@ abstract class ApiQueryBase extends ApiBase { * @return string */ public static function getBaseVersion() { - return __CLASS__ . ': $Id: ApiQueryBase.php 44461 2008-12-11 19:11:11Z ialex $'; + return __CLASS__ . ': $Id: ApiQueryBase.php 47450 2009-02-18 15:26:09Z catrope $'; } } @@ -406,6 +440,8 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { /** * Overrides base class to prepend 'g' to every generator parameter + * @param $paramNames string Parameter name + * @return string Prefixed parameter name */ public function encodeParamName($paramName) { if ($this->mIsGenerator) @@ -416,7 +452,8 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { /** * Execute this module as a generator - * @param $resultPageSet PageSet: All output should be appended to this object + * @param $resultPageSet ApiPageSet: All output should be appended to + * this object */ public abstract function executeGenerator($resultPageSet); } diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index 6f356cea..c5ffc37b 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -115,7 +115,7 @@ class ApiQueryBlocks extends ApiQueryBase { "ipb_range_end >= '$upper'" )); } - if(!$wgUser->isAllowed('suppress')) + if(!$wgUser->isAllowed('hideuser')) $this->addWhereFld('ipb_deleted', 0); // Purge expired entries on one in every 10 queries @@ -169,10 +169,14 @@ class ApiQueryBlocks extends ApiQueryBase { if($row->ipb_allow_usertalk) $block['allowusertalk'] = ''; } - $data[] = $block; + $fit = $result->addValue(array('query', $this->getModuleName()), null, $block); + if(!$fit) + { + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ipb_timestamp)); + break; + } } - $result->setIndexedTagName($data, 'block'); - $result->addValue('query', $this->getModuleName(), $data); + $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'block'); } protected function prepareUsername($user) @@ -259,6 +263,6 @@ class ApiQueryBlocks extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryBlocks.php 43676 2008-11-18 15:11:11Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryBlocks.php 48213 2009-03-09 10:01:00Z aaron $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index 9c4e9b41..f91a04e5 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -81,6 +81,19 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $this->addTables('categorylinks'); $this->addWhereFld('cl_from', array_keys($this->getPageSet()->getGoodTitles())); + if(!is_null($params['categories'])) + { + $cats = array(); + foreach($params['categories'] as $cat) + { + $title = Title::newFromText($cat); + if(!$title || $title->getNamespace() != NS_CATEGORY) + $this->setWarning("``$cat'' is not a category"); + else + $cats[] = $title->getDBkey(); + } + $this->addWhereFld('cl_to', $cats); + } if(!is_null($params['continue'])) { $cont = explode('|', $params['continue']); if(count($cont) != 2) @@ -112,6 +125,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $this->addWhere(array('pp_propname IS NULL')); } + $this->addOption('USE INDEX', array('categorylinks' => 'cl_from')); # Don't order by cl_from if it's constant in the WHERE clause if(count($this->getPageSet()->getGoodTitles()) == 1) $this->addOption('ORDER BY', 'cl_to'); @@ -123,8 +137,6 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { if (is_null($resultPageSet)) { - $data = array(); - $lastId = 0; // database has no ID 0 $count = 0; while ($row = $db->fetchObject($res)) { if (++$count > $params['limit']) { @@ -134,16 +146,8 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { '|' . $this->keyToTitle($row->cl_to)); break; } - if ($lastId != $row->cl_from) { - if($lastId != 0) { - $this->addPageSubItems($lastId, $data); - $data = array(); - } - $lastId = $row->cl_from; - } $title = Title :: makeTitle(NS_CATEGORY, $row->cl_to); - $vals = array(); ApiQueryBase :: addTitleInfo($vals, $title); if ($fld_sortkey) @@ -151,13 +155,14 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { if ($fld_timestamp) $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->cl_timestamp); - $data[] = $vals; - } - - if($lastId != 0) { - $this->addPageSubItems($lastId, $data); + $fit = $this->addPageSubItem($row->cl_from, $vals); + if(!$fit) + { + $this->setContinueEnumParameter('continue', $row->cl_from . + '|' . $this->keyToTitle($row->cl_to)); + break; + } } - } else { $titles = array(); @@ -202,6 +207,9 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 ), 'continue' => null, + 'categories' => array( + ApiBase :: PARAM_ISMULTI => true, + ), ); } @@ -211,6 +219,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { 'limit' => 'How many categories to return', 'show' => 'Which kind of categories to show', 'continue' => 'When more results are available, use this to continue', + 'categories' => 'Only list these categories. Useful for checking whether a certain page is in a certain category', ); } @@ -228,6 +237,6 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryCategories.php 44585 2008-12-14 17:39:50Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryCategories.php 50097 2009-05-01 06:35:57Z tstarling $'; } } diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php index f83c4a5b..49e4554e 100644 --- a/includes/api/ApiQueryCategoryInfo.php +++ b/includes/api/ApiQueryCategoryInfo.php @@ -29,7 +29,7 @@ if (!defined('MEDIAWIKI')) { } /** - * This query adds <categories> subelement to all pages with the list of images embedded into those pages. + * This query adds the <categories> subelement to all pages with the list of categories the page is in * * @ingroup API */ @@ -39,7 +39,8 @@ class ApiQueryCategoryInfo extends ApiQueryBase { parent :: __construct($query, $moduleName, 'ci'); } - public function execute() { + public function execute() { + $params = $this->extractRequestParams(); $alltitles = $this->getPageSet()->getAllTitlesByNamespace(); if ( empty( $alltitles[NS_CATEGORY] ) ) { return; @@ -65,27 +66,49 @@ class ApiQueryCategoryInfo extends ApiQueryBase { 'pp_propname' => 'hiddencat')), )); $this->addFields(array('cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'pp_propname AS cat_hidden')); - $this->addWhere(array('cat_title' => $cattitles)); + $this->addWhere(array('cat_title' => $cattitles)); + if(!is_null($params['continue'])) + { + $title = $this->getDB()->addQuotes($params['continue']); + $this->addWhere("cat_title >= $title"); + } + $this->addOption('ORDER BY', 'cat_title'); $db = $this->getDB(); $res = $this->select(__METHOD__); - $data = array(); $catids = array_flip($cattitles); while($row = $db->fetchObject($res)) { $vals = array(); - $vals['size'] = $row->cat_pages; + $vals['size'] = intval($row->cat_pages); $vals['pages'] = $row->cat_pages - $row->cat_subcats - $row->cat_files; - $vals['files'] = $row->cat_files; - $vals['subcats'] = $row->cat_subcats; + $vals['files'] = intval($row->cat_files); + $vals['subcats'] = intval($row->cat_subcats); if($row->cat_hidden) $vals['hidden'] = ''; - $this->addPageSubItems($catids[$row->cat_title], $vals); + $fit = $this->addPageSubItems($catids[$row->cat_title], $vals); + if(!$fit) + { + $this->setContinueEnumParameter('continue', $row->cat_title); + break; + } } $db->freeResult($res); } + public function getAllowedParams() { + return array ( + 'continue' => null, + ); + } + + public function getParamDescription() { + return array ( + 'continue' => 'When more results are available, use this to continue', + ); + } + public function getDescription() { return 'Returns information about the given categories'; } @@ -95,6 +118,6 @@ class ApiQueryCategoryInfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryCategoryInfo.php 44590 2008-12-14 20:24:23Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryCategoryInfo.php 47865 2009-02-27 16:03:01Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index e2f577a2..dc5a8265 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -112,31 +112,38 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { break; } - $lastSortKey = $row->cl_sortkey; // detect duplicate sortkeys - if (is_null($resultPageSet)) { $vals = array(); if ($fld_ids) $vals['pageid'] = intval($row->page_id); if ($fld_title) { $title = Title :: makeTitle($row->page_namespace, $row->page_title); - $vals['ns'] = intval($title->getNamespace()); - $vals['title'] = $title->getPrefixedText(); + ApiQueryBase::addTitleInfo($vals, $title); } if ($fld_sortkey) $vals['sortkey'] = $row->cl_sortkey; if ($fld_timestamp) $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->cl_timestamp); - $data[] = $vals; + $fit = $this->getResult()->addValue(array('query', $this->getModuleName()), + null, $vals); + if(!$fit) + { + if ($params['sort'] == 'timestamp') + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->cl_timestamp)); + else + $this->setContinueEnumParameter('continue', $this->getContinueStr($row, $lastSortKey)); + break; + } } else { $resultPageSet->processDbRow($row); } + $lastSortKey = $row->cl_sortkey; // detect duplicate sortkeys } $db->freeResult($res); if (is_null($resultPageSet)) { - $this->getResult()->setIndexedTagName($data, 'cm'); - $this->getResult()->addValue('query', $this->getModuleName(), $data); + $this->getResult()->setIndexedTagName_internal( + array('query', $this->getModuleName()), 'cm'); } } @@ -255,6 +262,6 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 42197 2008-10-18 10:09:19Z ialex $'; + return __CLASS__ . ': $Id: ApiQueryCategoryMembers.php 47865 2009-02-27 16:03:01Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index 408421c4..bdbe05e8 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -56,13 +56,27 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $fld_len = isset($prop['len']); $fld_content = isset($prop['content']); $fld_token = isset($prop['token']); - + $result = $this->getResult(); $pageSet = $this->getPageSet(); $titles = $pageSet->getTitles(); $data = array(); + + // This module operates in three modes: + // 'revs': List deleted revs for certain titles + // 'user': List deleted revs by a certain user + // 'all': List all deleted revs + $mode = 'all'; + if(count($titles) > 0) + $mode = 'revs'; + else if(!is_null($params['user'])) + $mode = 'user'; + + if(!is_null($params['user']) && !is_null($params['excludeuser'])) + $this->dieUsage('user and excludeuser cannot be used together', 'badparams'); $this->addTables('archive'); + $this->addWhere('ar_deleted = 0'); $this->addFields(array('ar_title', 'ar_namespace', 'ar_timestamp')); if($fld_revid) $this->addFields('ar_rev_id'); @@ -102,34 +116,88 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $token = $wgUser->editToken(); // We need a custom WHERE clause that matches all titles. - if(count($titles) > 0) + if($mode == 'revs') { $lb = new LinkBatch($titles); $where = $lb->constructSet('ar', $db); $this->addWhere($where); - } else { - $this->dieUsage('You have to specify a page title or titles'); + } + elseif($mode == 'all') + { + $this->addWhereFld('ar_namespace', $params['namespace']); + if(!is_null($params['from'])) + { + $from = $this->getDB()->strencode($this->titleToKey($params['from'])); + $this->addWhere("ar_title >= '$from'"); + } + } + + if(!is_null($params['user'])) { + $this->addWhereFld('ar_user_text', $params['user']); + } elseif(!is_null($params['excludeuser'])) { + $this->addWhere('ar_user_text != ' . + $this->getDB()->addQuotes($params['excludeuser'])); + } + + if(!is_null($params['continue']) && ($mode == 'all' || $mode == 'revs')) + { + $cont = explode('|', $params['continue']); + if(count($cont) != 3) + $this->dieUsage("Invalid continue param. You should pass the original value returned by the previous query", "badcontinue"); + $ns = intval($cont[0]); + $title = $this->getDB()->strencode($this->titleToKey($cont[1])); + $ts = $this->getDB()->strencode($cont[2]); + $op = ($params['dir'] == 'newer' ? '>' : '<'); + $this->addWhere("ar_namespace $op $ns OR " . + "(ar_namespace = $ns AND " . + "(ar_title $op '$title' OR " . + "(ar_title = '$title' AND " . + "ar_timestamp = '$ts')))"); } $this->addOption('LIMIT', $limit + 1); - $this->addWhereRange('ar_timestamp', $params['dir'], $params['start'], $params['end']); + $this->addOption('USE INDEX', array('archive' => ($mode == 'user' ? 'usertext_timestamp' : 'name_title_timestamp'))); + if($mode == 'all') + { + if($params['unique']) + { + $this->addOption('GROUP BY', 'ar_title'); + $this->addOption('ORDER BY', 'ar_title'); + } + else + $this->addOption('ORDER BY', 'ar_title, ar_timestamp'); + } + else + { + if($mode == 'revs') + { + // Sort by ns and title in the same order as timestamp for efficiency + $this->addWhereRange('ar_namespace', $params['dir'], null, null); + $this->addWhereRange('ar_title', $params['dir'], null, null); + } + $this->addWhereRange('ar_timestamp', $params['dir'], $params['start'], $params['end']); + } $res = $this->select(__METHOD__); - $pages = array(); + $pageMap = array(); // Maps ns&title to (fake) pageid $count = 0; - // First populate the $pages array + $newPageID = 0; while($row = $db->fetchObject($res)) { if(++$count > $limit) { // We've had enough - $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ar_timestamp)); + if($mode == 'all' || $mode == 'revs') + $this->setContinueEnumParameter('continue', intval($row->ar_namespace) . '|' . + $this->keyToTitle($row->ar_title) . '|' . $row->ar_timestamp); + else + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ar_timestamp)); break; } $rev = array(); $rev['timestamp'] = wfTimestamp(TS_ISO_8601, $row->ar_timestamp); if($fld_revid) - $rev['revid'] = $row->ar_rev_id; + $rev['revid'] = intval($row->ar_rev_id); if($fld_user) $rev['user'] = $row->ar_user_text; if($fld_comment) @@ -142,31 +210,38 @@ class ApiQueryDeletedrevs extends ApiQueryBase { if($fld_content) ApiResult::setContent($rev, Revision::getRevisionText($row)); - $t = Title::makeTitle($row->ar_namespace, $row->ar_title); - if(!isset($pages[$t->getPrefixedText()])) + if(!isset($pageMap[$row->ar_namespace][$row->ar_title])) { - $pages[$t->getPrefixedText()] = array( - 'title' => $t->getPrefixedText(), - 'ns' => intval($row->ar_namespace), - 'revisions' => array($rev) - ); + $pageID = $newPageID++; + $pageMap[$row->ar_namespace][$row->ar_title] = $pageID; + $t = Title::makeTitle($row->ar_namespace, $row->ar_title); + $a['revisions'] = array($rev); + $result->setIndexedTagName($a['revisions'], 'rev'); + ApiQueryBase::addTitleInfo($a, $t); if($fld_token) - $pages[$t->getPrefixedText()]['token'] = $token; + $a['token'] = $token; + $fit = $result->addValue(array('query', $this->getModuleName()), $pageID, $a); } else - $pages[$t->getPrefixedText()]['revisions'][] = $rev; + { + $pageID = $pageMap[$row->ar_namespace][$row->ar_title]; + $fit = $result->addValue( + array('query', $this->getModuleName(), $pageID, 'revisions'), + null, $rev); + } + if(!$fit) + { + if($mode == 'all' || $mode == 'revs') + $this->setContinueEnumParameter('continue', intval($row->ar_namespace) . '|' . + $this->keyToTitle($row->ar_title) . '|' . $row->ar_timestamp); + else + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->ar_timestamp)); + break; + } } $db->freeResult($res); - - // We don't want entire pagenames as keys, so let's make this array indexed - foreach($pages as $page) - { - $result->setIndexedTagName($page['revisions'], 'rev'); - $data[] = $page; - } - $result->setIndexedTagName($data, 'page'); - $result->addValue('query', $this->getModuleName(), $data); - } + $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'page'); + } public function getAllowedParams() { return array ( @@ -183,6 +258,19 @@ class ApiQueryDeletedrevs extends ApiQueryBase { ), ApiBase :: PARAM_DFLT => 'older' ), + 'from' => null, + 'continue' => null, + 'unique' => false, + 'user' => array( + ApiBase :: PARAM_TYPE => 'user' + ), + 'excludeuser' => array( + ApiBase :: PARAM_TYPE => 'user' + ), + 'namespace' => array( + ApiBase :: PARAM_TYPE => 'namespace', + ApiBase :: PARAM_DFLT => 0, + ), 'limit' => array( ApiBase :: PARAM_DFLT => 10, ApiBase :: PARAM_TYPE => 'limit', @@ -202,34 +290,51 @@ class ApiQueryDeletedrevs extends ApiQueryBase { 'token' ), ApiBase :: PARAM_ISMULTI => true - ) + ), ); } public function getParamDescription() { return array ( - 'start' => 'The timestamp to start enumerating from', - 'end' => 'The timestamp to stop enumerating at', - 'dir' => 'The direction in which to enumerate', + 'start' => 'The timestamp to start enumerating from. (1,2)', + 'end' => 'The timestamp to stop enumerating at. (1,2)', + 'dir' => 'The direction in which to enumerate. (1,2)', 'limit' => 'The maximum amount of revisions to list', - 'prop' => 'Which properties to get' + 'prop' => 'Which properties to get', + 'namespace' => 'Only list pages in this namespace (3)', + 'user' => 'Only list revisions by this user', + 'excludeuser' => 'Don\'t list revisions by this user', + 'from' => 'Start listing at this title (3)', + 'continue' => 'When more results are available, use this to continue (3)', + 'unique' => 'List only one revision for each page (3)', ); } public function getDescription() { - return 'List deleted revisions.'; + return array( 'List deleted revisions.', + 'This module operates in three modes:', + '1) List deleted revisions for the given title(s), sorted by timestamp', + '2) List deleted contributions for the given user, sorted by timestamp (no titles specified)', + '3) List all deleted revisions in the given namespace, sorted by title and timestamp (no titles specified, druser not set)', + 'Certain parameters only apply to some modes and are ignored in others.', + 'For instance, a parameter marked (1) only applies to mode 1 and is ignored in modes 2 and 3.', + ); } protected function getExamples() { return array ( - 'List the first 50 deleted revisions', + 'List the last deleted revisions of Main Page and Talk:Main Page, with content (mode 1):', + ' api.php?action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&drprop=user|comment|content', + 'List the last 50 deleted contributions by Bob (mode 2):', + ' api.php?action=query&list=deletedrevs&druser=Bob&drlimit=50', + 'List the first 50 deleted revisions in the main namespace (mode 3):', ' api.php?action=query&list=deletedrevs&drdir=newer&drlimit=50', - 'List the last deleted revisions of Main Page and Talk:Main Page, with content:', - ' api.php?action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&drprop=user|comment|content' + 'List the first 50 deleted pages in the Talk namespace (mode 3):', + ' api.php?action=query&list=deletedrevs&drdir=newer&drlimit=50&drnamespace=1&drunique', ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 40798 2008-09-13 20:41:58Z aaron $'; + return __CLASS__ . ': $Id: ApiQueryDeletedrevs.php 50220 2009-05-05 14:07:59Z tstarling $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php index 5f7d7ee0..84a8a96d 100644 --- a/includes/api/ApiQueryDuplicateFiles.php +++ b/includes/api/ApiQueryDuplicateFiles.php @@ -86,9 +86,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { $res = $this->select(__METHOD__); $db = $this->getDB(); $count = 0; - $data = array(); $titles = array(); - $lastName = ''; while($row = $db->fetchObject($res)) { if(++$count > $params['limit']) @@ -104,27 +102,23 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { $titles[] = Title::makeTitle(NS_FILE, $row->dup_name); else { - if($row->orig_name != $lastName) - { - if($lastName != '') - { - $this->addPageSubItems($images[$lastName], $data); - $data = array(); - } - $lastName = $row->orig_name; - } - - $data[] = array( + $r = array( 'name' => $row->dup_name, 'user' => $row->dup_user_text, 'timestamp' => wfTimestamp(TS_ISO_8601, $row->dup_timestamp) ); + $fit = $this->addPageSubItem($images[$row->orig_name], $r); + if(!$fit) + { + $this->setContinueEnumParameter('continue', + $this->keyToTitle($row->orig_name) . '|' . + $this->keyToTitle($row->dup_name)); + break; + } } } if(!is_null($resultPageSet)) $resultPageSet->populateFromTitles($titles); - else if($lastName != '') - $this->addPageSubItems($images[$lastName], $data); $db->freeResult($res); } @@ -153,12 +147,12 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { } protected function getExamples() { - return array ( 'api.php?action=query&titles=Image:Albert_Einstein_Head.jpg&prop=duplicatefiles', + return array ( 'api.php?action=query&titles=File:Albert_Einstein_Head.jpg&prop=duplicatefiles', 'api.php?action=query&generator=allimages&prop=duplicatefiles', ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryDuplicateFiles.php 44121 2008-12-01 17:14:30Z vyznev $'; + return __CLASS__ . ': $Id: ApiQueryDuplicateFiles.php 48215 2009-03-09 10:44:34Z catrope $'; } } diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index 85e21f42..0ba2767a 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -110,7 +110,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { $res = $this->select(__METHOD__); - $data = array (); + $result = $this->getResult(); $count = 0; while ($row = $db->fetchObject($res)) { if (++ $count > $limit) { @@ -125,12 +125,16 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { $vals['pageid'] = intval($row->page_id); if ($fld_title) { $title = Title :: makeTitle($row->page_namespace, $row->page_title); - $vals['ns'] = intval($title->getNamespace()); - $vals['title'] = $title->getPrefixedText(); + ApiQueryBase::addTitleInfo($vals, $title); } if ($fld_url) $vals['url'] = $row->el_to; - $data[] = $vals; + $fit = $result->addValue(array('query', $this->getModuleName()), null, $vals); + if(!$fit) + { + $this->setContinueEnumParameter('offset', $offset + $count - 1); + break; + } } else { $resultPageSet->processDbRow($row); } @@ -138,9 +142,8 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { $db->freeResult($res); if (is_null($resultPageSet)) { - $result = $this->getResult(); - $result->setIndexedTagName($data, $this->getModulePrefix()); - $result->addValue('query', $this->getModuleName(), $data); + $result->setIndexedTagName_internal(array('query', $this->getModuleName()), + $this->getModulePrefix()); } } @@ -206,6 +209,6 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 43271 2008-11-06 22:38:42Z siebrand $'; + return __CLASS__ . ': $Id: ApiQueryExtLinksUsage.php 47865 2009-02-27 16:03:01Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php index a24f15d8..7a91f432 100644 --- a/includes/api/ApiQueryExternalLinks.php +++ b/includes/api/ApiQueryExternalLinks.php @@ -61,8 +61,6 @@ class ApiQueryExternalLinks extends ApiQueryBase { $db = $this->getDB(); $res = $this->select(__METHOD__); - $data = array(); - $lastId = 0; // database has no ID 0 $count = 0; while ($row = $db->fetchObject($res)) { if (++$count > $params['limit']) { @@ -71,23 +69,15 @@ class ApiQueryExternalLinks extends ApiQueryBase { $this->setContinueEnumParameter('offset', @$params['offset'] + $params['limit']); break; } - if ($lastId != $row->el_from) { - if($lastId != 0) { - $this->addPageSubItems($lastId, $data); - $data = array(); - } - $lastId = $row->el_from; - } - $entry = array(); ApiResult :: setContent($entry, $row->el_to); - $data[] = $entry; - } - - if($lastId != 0) { - $this->addPageSubItems($lastId, $data); + $fit = $this->addPageSubItem($row->el_from, $entry); + if(!$fit) + { + $this->setContinueEnumParameter('offset', @$params['offset'] + $count - 1); + break; + } } - $db->freeResult($res); } @@ -123,6 +113,6 @@ class ApiQueryExternalLinks extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 37270 2008-07-07 17:32:22Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryExternalLinks.php 46845 2009-02-05 14:30:59Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index 612d5cc9..7d880456 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -56,49 +56,116 @@ class ApiQueryImageInfo extends ApiQueryBase { } $pageIds = $this->getPageSet()->getAllTitlesByNamespace(); - if (!empty($pageIds[NS_FILE])) { - + if ( !empty( $pageIds[NS_FILE] ) ) { + $titles = array_keys($pageIds[NS_FILE]); + asort($titles); // Ensure the order is always the same + + $skip = false; + if(!is_null($params['continue'])) + { + $skip = true; + $cont = explode('|', $params['continue']); + if(count($cont) != 2) + $this->dieUsage("Invalid continue param. You should pass the original " . + "value returned by the previous query", "_badcontinue"); + $fromTitle = strval($cont[0]); + $fromTimestamp = $cont[1]; + // Filter out any titles before $fromTitle + foreach($titles as $key => $title) + if($title < $fromTitle) + unset($titles[$key]); + else + break; + } + $result = $this->getResult(); - $images = RepoGroup::singleton()->findFiles( array_keys( $pageIds[NS_FILE] ) ); + $images = RepoGroup::singleton()->findFiles( $titles ); foreach ( $images as $img ) { - $data = array(); - + $start = $skip ? $fromTimestamp : $params['start']; + $pageId = $pageIds[NS_IMAGE][ $img->getOriginalTitle()->getDBkey() ]; + + $fit = $result->addValue( + array('query', 'pages', intval($pageId)), + 'imagerepository', $img->getRepoName() + ); + if(!$fit) + { + if(count($pageIds[NS_IMAGE]) == 1) + # The user is screwed. imageinfo can't be solely + # responsible for exceeding the limit in this case, + # so set a query-continue that just returns the same + # thing again. When the violating queries have been + # out-continued, the result will get through + $this->setContinueEnumParameter('start', + wfTimestamp(TS_ISO_8601, $img->getTimestamp())); + else + $this->setContinueEnumParameter('continue', + $this->getContinueStr($img)); + break; + } + // Get information about the current version first // Check that the current version is within the start-end boundaries - if((is_null($params['start']) || $img->getTimestamp() <= $params['start']) && + $gotOne = false; + if((is_null($start) || $img->getTimestamp() <= $start) && (is_null($params['end']) || $img->getTimestamp() >= $params['end'])) { - $data[] = self::getInfo( $img, $prop, $result, $scale ); + $gotOne = true; + $fit = $this->addPageSubItem($pageId, + self::getInfo( $img, $prop, $result, $scale)); + if(!$fit) + { + if(count($pageIds[NS_IMAGE]) == 1) + # See the 'the user is screwed' comment above + $this->setContinueEnumParameter('start', + wfTimestamp(TS_ISO_8601, $img->getTimestamp())); + else + $this->setContinueEnumParameter('continue', + $this->getContinueStr($img)); + break; + } } // Now get the old revisions // Get one more to facilitate query-continue functionality - $count = count($data); - $oldies = $img->getHistory($params['limit'] - $count + 1, $params['start'], $params['end']); + $count = ($gotOne ? 1 : 0); + $oldies = $img->getHistory($params['limit'] - $count + 1, $start, $params['end']); foreach($oldies as $oldie) { if(++$count > $params['limit']) { // We've reached the extra one which shows that there are additional pages to be had. Stop here... // Only set a query-continue if there was only one title if(count($pageIds[NS_FILE]) == 1) - $this->setContinueEnumParameter('start', $oldie->getTimestamp()); + { + $this->setContinueEnumParameter('start', + wfTimestamp(TS_ISO_8601, $oldie->getTimestamp())); + } + break; + } + $fit = $this->addPageSubItem($pageId, + self::getInfo($oldie, $prop, $result)); + if(!$fit) + { + if(count($pageIds[NS_IMAGE]) == 1) + $this->setContinueEnumParameter('start', + wfTimestamp(TS_ISO_8601, $oldie->getTimestamp())); + else + $this->setContinueEnumParameter('continue', + $this->getContinueStr($oldie)); break; } - $data[] = self::getInfo( $oldie, $prop, $result ); } - - $pageId = $pageIds[NS_FILE][ $img->getOriginalTitle()->getDBkey() ]; - $result->addValue( - array( 'query', 'pages', intval( $pageId ) ), - 'imagerepository', $img->getRepoName() - ); - $this->addPageSubItems($pageId, $data); + if(!$fit) + break; + $skip = false; } $missing = array_diff( array_keys( $pageIds[NS_FILE] ), array_keys( $images ) ); - foreach ( $missing as $title ) + foreach ($missing as $title) { $result->addValue( - array( 'query', 'pages', intval( $pageIds[NS_FILE][$title] ) ), + array('query', 'pages', intval($pageIds[NS_FILE][$title])), 'imagerepository', '' ); + // The above can't fail because it doesn't increase the result size + } } } @@ -127,8 +194,8 @@ class ApiQueryImageInfo extends ApiQueryBase { if( $mto && !$mto->isError() ) { $vals['thumburl'] = $mto->getUrl(); - $vals['thumbwidth'] = $mto->getWidth(); - $vals['thumbheight'] = $mto->getHeight(); + $vals['thumbwidth'] = intval( $mto->getWidth() ); + $vals['thumbheight'] = intval( $mto->getHeight() ); } } $vals['url'] = $file->getFullURL(); @@ -140,8 +207,7 @@ class ApiQueryImageInfo extends ApiQueryBase { $vals['sha1'] = wfBaseConvert( $file->getSha1(), 36, 16, 40 ); if( isset( $prop['metadata'] ) ) { $metadata = $file->getMetadata(); - $vals['metadata'] = $metadata ? unserialize( $metadata ) : null; - $result->setIndexedTagName_recursive( $vals['metadata'], 'meta' ); + $vals['metadata'] = $metadata ? self::processMetaData( unserialize( $metadata ), $result ) : null; } if( isset( $prop['mime'] ) ) $vals['mime'] = $file->getMimeType(); @@ -154,6 +220,30 @@ class ApiQueryImageInfo extends ApiQueryBase { return $vals; } + + public static function processMetaData($metadata, $result) + { + $retval = array(); + if ( is_array( $metadata ) ) { + foreach($metadata as $key => $value) + { + $r = array('name' => $key); + if(is_array($value)) + $r['value'] = self::processMetaData($value, $result); + else + $r['value'] = $value; + $retval[] = $r; + } + } + $result->setIndexedTagName($retval, 'metadata'); + return $retval; + } + + private function getContinueStr($img) + { + return $img->getOriginalTitle()->getText() . + '|' . $img->getTimestamp(); + } public function getAllowedParams() { return array ( @@ -193,7 +283,8 @@ class ApiQueryImageInfo extends ApiQueryBase { 'urlheight' => array( ApiBase :: PARAM_TYPE => 'integer', ApiBase :: PARAM_DFLT => -1 - ) + ), + 'continue' => null, ); } @@ -206,6 +297,7 @@ class ApiQueryImageInfo extends ApiQueryBase { 'urlwidth' => array('If iiprop=url is set, a URL to an image scaled to this width will be returned.', 'Only the current version of the image can be scaled.'), 'urlheight' => 'Similar to iiurlwidth. Cannot be used without iiurlwidth', + 'continue' => 'When more results are available, use this to continue', ); } @@ -217,12 +309,12 @@ class ApiQueryImageInfo extends ApiQueryBase { protected function getExamples() { return array ( - 'api.php?action=query&titles=Image:Albert%20Einstein%20Head.jpg&prop=imageinfo', - 'api.php?action=query&titles=Image:Test.jpg&prop=imageinfo&iilimit=50&iiend=20071231235959&iiprop=timestamp|user|url', + 'api.php?action=query&titles=File:Albert%20Einstein%20Head.jpg&prop=imageinfo', + 'api.php?action=query&titles=File:Test.jpg&prop=imageinfo&iilimit=50&iiend=20071231235959&iiprop=timestamp|user|url', ); } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryImageInfo.php 44121 2008-12-01 17:14:30Z vyznev $'; + return __CLASS__ . ': $Id: ApiQueryImageInfo.php 50097 2009-05-01 06:35:57Z tstarling $'; } } diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php index 02fe24f1..69569e9b 100644 --- a/includes/api/ApiQueryImages.php +++ b/includes/api/ApiQueryImages.php @@ -82,9 +82,6 @@ class ApiQueryImages extends ApiQueryGeneratorBase { $res = $this->select(__METHOD__); if (is_null($resultPageSet)) { - - $data = array(); - $lastId = 0; // database has no ID 0 $count = 0; while ($row = $db->fetchObject($res)) { if (++$count > $params['limit']) { @@ -94,23 +91,16 @@ class ApiQueryImages extends ApiQueryGeneratorBase { '|' . $this->keyToTitle($row->il_to)); break; } - if ($lastId != $row->il_from) { - if($lastId != 0) { - $this->addPageSubItems($lastId, $data); - $data = array(); - } - $lastId = $row->il_from; - } - $vals = array(); ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle(NS_FILE, $row->il_to)); - $data[] = $vals; - } - - if($lastId != 0) { - $this->addPageSubItems($lastId, $data); + $fit = $this->addPageSubItem($row->il_from, $vals); + if(!$fit) + { + $this->setContinueEnumParameter('continue', $row->il_from . + '|' . $this->keyToTitle($row->il_to)); + break; + } } - } else { $titles = array(); @@ -165,6 +155,6 @@ class ApiQueryImages extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryImages.php 44121 2008-12-01 17:14:30Z vyznev $'; + return __CLASS__ . ': $Id: ApiQueryImages.php 46845 2009-02-05 14:30:59Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index 0c5c72fc..b7affabc 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -34,6 +34,10 @@ if (!defined('MEDIAWIKI')) { * @ingroup API */ class ApiQueryInfo extends ApiQueryBase { + + private $fld_protection = false, $fld_talkid = false, + $fld_subjectid = false, $fld_url = false, + $fld_readable = false; public function __construct($query, $moduleName) { parent :: __construct($query, $moduleName, 'in'); @@ -49,11 +53,13 @@ class ApiQueryInfo extends ApiQueryBase { $pageSet->requestField('page_len'); } + /** + * Get an array mapping token names to their handler functions. + * The prototype for a token function is func($pageid, $title) + * it should return a token or false (permission denied) + * @return array(tokenname => function) + */ protected function getTokenFunctions() { - // tokenname => function - // function prototype is func($pageid, $title) - // should return token or false - // Don't call the hooks twice if(isset($this->tokenFunctions)) return $this->tokenFunctions; @@ -70,6 +76,7 @@ class ApiQueryInfo extends ApiQueryBase { 'block' => array( 'ApiQueryInfo', 'getBlockToken' ), 'unblock' => array( 'ApiQueryInfo', 'getUnblockToken' ), 'email' => array( 'ApiQueryInfo', 'getEmailToken' ), + 'import' => array( 'ApiQueryInfo', 'getImportToken' ), ); wfRunHooks('APIQueryInfoTokens', array(&$this->tokenFunctions)); return $this->tokenFunctions; @@ -79,6 +86,7 @@ class ApiQueryInfo extends ApiQueryBase { { // We could check for $title->userCan('edit') here, // but that's too expensive for this purpose + // and would break caching global $wgUser; if(!$wgUser->isAllowed('edit')) return false; @@ -167,76 +175,201 @@ class ApiQueryInfo extends ApiQueryBase { $cachedEmailToken = $wgUser->editToken(); return $cachedEmailToken; } + + public static function getImportToken($pageid, $title) + { + global $wgUser; + if(!$wgUser->isAllowed('import')) + return false; - public function execute() { + static $cachedImportToken = null; + if(!is_null($cachedImportToken)) + return $cachedImportToken; - global $wgUser; + $cachedImportToken = $wgUser->editToken(); + return $cachedImportToken; + } - $params = $this->extractRequestParams(); - $fld_protection = $fld_talkid = $fld_subjectid = $fld_url = $fld_readable = false; - if(!is_null($params['prop'])) { - $prop = array_flip($params['prop']); - $fld_protection = isset($prop['protection']); - $fld_talkid = isset($prop['talkid']); - $fld_subjectid = isset($prop['subjectid']); - $fld_url = isset($prop['url']); - $fld_readable = isset($prop['readable']); + public function execute() { + $this->params = $this->extractRequestParams(); + if(!is_null($this->params['prop'])) { + $prop = array_flip($this->params['prop']); + $this->fld_protection = isset($prop['protection']); + $this->fld_talkid = isset($prop['talkid']); + $this->fld_subjectid = isset($prop['subjectid']); + $this->fld_url = isset($prop['url']); + $this->fld_readable = isset($prop['readable']); } $pageSet = $this->getPageSet(); - $titles = $pageSet->getGoodTitles(); - $missing = $pageSet->getMissingTitles(); + $this->titles = $pageSet->getGoodTitles(); + $this->missing = $pageSet->getMissingTitles(); + $this->everything = $this->titles + $this->missing; $result = $this->getResult(); - $pageRestrictions = $pageSet->getCustomField('page_restrictions'); - $pageIsRedir = $pageSet->getCustomField('page_is_redirect'); - $pageIsNew = $pageSet->getCustomField('page_is_new'); - $pageCounter = $pageSet->getCustomField('page_counter'); - $pageTouched = $pageSet->getCustomField('page_touched'); - $pageLatest = $pageSet->getCustomField('page_latest'); - $pageLength = $pageSet->getCustomField('page_len'); + uasort($this->everything, array('Title', 'compare')); + if(!is_null($this->params['continue'])) + { + // Throw away any titles we're gonna skip so they don't + // clutter queries + $cont = explode('|', $this->params['continue']); + if(count($cont) != 2) + $this->dieUsage("Invalid continue param. You should pass the original " . + "value returned by the previous query", "_badcontinue"); + $conttitle = Title::makeTitleSafe($cont[0], $cont[1]); + foreach($this->everything as $pageid => $title) + { + if(Title::compare($title, $conttitle) >= 0) + break; + unset($this->titles[$pageid]); + unset($this->missing[$pageid]); + unset($this->everything[$pageid]); + } + } + + $this->pageRestrictions = $pageSet->getCustomField('page_restrictions'); + $this->pageIsRedir = $pageSet->getCustomField('page_is_redirect'); + $this->pageIsNew = $pageSet->getCustomField('page_is_new'); + $this->pageCounter = $pageSet->getCustomField('page_counter'); + $this->pageTouched = $pageSet->getCustomField('page_touched'); + $this->pageLatest = $pageSet->getCustomField('page_latest'); + $this->pageLength = $pageSet->getCustomField('page_len'); $db = $this->getDB(); - if ($fld_protection && count($titles)) { - $this->addTables('page_restrictions'); - $this->addFields(array('pr_page', 'pr_type', 'pr_level', 'pr_expiry', 'pr_cascade')); - $this->addWhereFld('pr_page', array_keys($titles)); + // Get protection info if requested + if ($this->fld_protection) + $this->getProtectionInfo(); + + // Run the talkid/subjectid query if requested + if($this->fld_talkid || $this->fld_subjectid) + $this->getTSIDs(); + + foreach($this->everything as $pageid => $title) { + $pageInfo = $this->extractPageInfo($pageid, $title); + $fit = $result->addValue(array ( + 'query', + 'pages' + ), $pageid, $pageInfo); + if(!$fit) + { + $this->setContinueEnumParameter('continue', + $title->getNamespace() . '|' . + $title->getText()); + break; + } + } + } + + /** + * Get a result array with information about a title + * @param $pageid int Page ID (negative for missing titles) + * @param $title Title object + * @return array + */ + private function extractPageInfo($pageid, $title) + { + $pageInfo = array(); + if($title->exists()) + { + $pageInfo['touched'] = wfTimestamp(TS_ISO_8601, $this->pageTouched[$pageid]); + $pageInfo['lastrevid'] = intval($this->pageLatest[$pageid]); + $pageInfo['counter'] = intval($this->pageCounter[$pageid]); + $pageInfo['length'] = intval($this->pageLength[$pageid]); + if ($this->pageIsRedir[$pageid]) + $pageInfo['redirect'] = ''; + if ($this->pageIsNew[$pageid]) + $pageInfo['new'] = ''; + } + + if (!is_null($this->params['token'])) { + $tokenFunctions = $this->getTokenFunctions(); + $pageInfo['starttimestamp'] = wfTimestamp(TS_ISO_8601, time()); + foreach($this->params['token'] as $t) + { + $val = call_user_func($tokenFunctions[$t], $pageid, $title); + if($val === false) + $this->setWarning("Action '$t' is not allowed for the current user"); + else + $pageInfo[$t . 'token'] = $val; + } + } + + if($this->fld_protection) { + $pageInfo['protection'] = array(); + if (isset($this->protections[$title->getNamespace()][$title->getDBkey()])) + $pageInfo['protection'] = + $this->protections[$title->getNamespace()][$title->getDBkey()]; + $this->getResult()->setIndexedTagName($pageInfo['protection'], 'pr'); + } + if($this->fld_talkid && isset($this->talkids[$title->getNamespace()][$title->getDBKey()])) + $pageInfo['talkid'] = $this->talkids[$title->getNamespace()][$title->getDBKey()]; + if($this->fld_subjectid && isset($this->subjectids[$title->getNamespace()][$title->getDBKey()])) + $pageInfo['subjectid'] = $this->subjectids[$title->getNamespace()][$title->getDBKey()]; + if($this->fld_url) { + $pageInfo['fullurl'] = $title->getFullURL(); + $pageInfo['editurl'] = $title->getFullURL('action=edit'); + } + if($this->fld_readable) + if($title->userCanRead()) + $pageInfo['readable'] = ''; + return $pageInfo; + } + + /** + * Get information about protections and put it in $protections + */ + private function getProtectionInfo() + { + $this->protections = array(); + $db = $this->getDB(); + + // Get normal protections for existing titles + if(count($this->titles)) + { + $this->addTables(array('page_restrictions', 'page')); + $this->addWhere('page_id=pr_page'); + $this->addFields(array('pr_page', 'pr_type', 'pr_level', + 'pr_expiry', 'pr_cascade', 'page_namespace', + 'page_title')); + $this->addWhereFld('pr_page', array_keys($this->titles)); $res = $this->select(__METHOD__); while($row = $db->fetchObject($res)) { $a = array( 'type' => $row->pr_type, 'level' => $row->pr_level, - 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ) + 'expiry' => Block::decodeExpiry($row->pr_expiry, TS_ISO_8601) ); if($row->pr_cascade) $a['cascade'] = ''; - $protections[$row->pr_page][] = $a; - + $this->protections[$row->page_namespace][$row->page_title][] = $a; + # Also check old restrictions - if($pageRestrictions[$row->pr_page]) { - foreach(explode(':', trim($pageRestrictions[$pageid])) as $restrict) { + if($this->pageRestrictions[$row->pr_page]) { + $restrictions = explode(':', trim($this->pageRestrictions[$row->pr_page])); + foreach($restrictions as $restrict) { $temp = explode('=', trim($restrict)); if(count($temp) == 1) { // old old format should be treated as edit/move restriction - $restriction = trim( $temp[0] ); + $restriction = trim($temp[0]); + if($restriction == '') continue; - $protections[$row->pr_page][] = array( + $this->protections[$row->page_namespace][$row->page_title][] = array( 'type' => 'edit', 'level' => $restriction, 'expiry' => 'infinity', ); - $protections[$row->pr_page][] = array( + $this->protections[$row->page_namespace][$row->page_title][] = array( 'type' => 'move', 'level' => $restriction, 'expiry' => 'infinity', ); } else { - $restriction = trim( $temp[1] ); + $restriction = trim($temp[1]); if($restriction == '') continue; - $protections[$row->pr_page][] = array( + $this->protections[$row->page_namespace][$row->page_title][] = array( 'type' => $temp[0], 'level' => $restriction, 'expiry' => 'infinity', @@ -246,272 +379,123 @@ class ApiQueryInfo extends ApiQueryBase { } } $db->freeResult($res); - - $imageIds = array(); - foreach ($titles as $id => $title) - if ($title->getNamespace() == NS_FILE) - $imageIds[] = $id; - // To avoid code duplication - $cascadeTypes = array( - array( - 'prefix' => 'tl', - 'table' => 'templatelinks', - 'ns' => 'tl_namespace', - 'title' => 'tl_title', - 'ids' => array_diff(array_keys($titles), $imageIds) - ), - array( - 'prefix' => 'il', - 'table' => 'imagelinks', - 'ns' => NS_FILE, - 'title' => 'il_to', - 'ids' => $imageIds - ) - ); - - foreach ($cascadeTypes as $type) - { - if (count($type['ids']) != 0) { - $this->resetQueryParams(); - $this->addTables(array('page_restrictions', $type['table'])); - $this->addTables('page', 'page_source'); - $this->addTables('page', 'page_target'); - $this->addFields(array('pr_type', 'pr_level', 'pr_expiry', - 'page_target.page_id AS page_target_id', - 'page_source.page_namespace AS page_source_namespace', - 'page_source.page_title AS page_source_title')); - $this->addWhere(array("{$type['prefix']}_from = pr_page", - 'page_target.page_namespace = '.$type['ns'], - 'page_target.page_title = '.$type['title'], - 'page_source.page_id = pr_page' - )); - $this->addWhereFld('pr_cascade', 1); - $this->addWhereFld('page_target.page_id', $type['ids']); - - $res = $this->select(__METHOD__); - while($row = $db->fetchObject($res)) { - $source = Title::makeTitle($row->page_source_namespace, $row->page_source_title); - $a = array( - 'type' => $row->pr_type, - 'level' => $row->pr_level, - 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ), - 'source' => $source->getPrefixedText() - ); - $protections[$row->page_target_id][] = $a; - } - $db->freeResult($res); - } - } } - // We don't need to check for pt stuff if there are no nonexistent titles - if($fld_protection && count($missing)) + // Get protections for missing titles + if(count($this->missing)) { $this->resetQueryParams(); - // Construct a custom WHERE clause that matches all titles in $missing - $lb = new LinkBatch($missing); + $lb = new LinkBatch($this->missing); $this->addTables('protected_titles'); $this->addFields(array('pt_title', 'pt_namespace', 'pt_create_perm', 'pt_expiry')); $this->addWhere($lb->constructSet('pt', $db)); $res = $this->select(__METHOD__); - $prottitles = array(); while($row = $db->fetchObject($res)) { - $prottitles[$row->pt_namespace][$row->pt_title][] = array( + $this->protections[$row->pt_namespace][$row->pt_title][] = array( 'type' => 'create', 'level' => $row->pt_create_perm, 'expiry' => Block::decodeExpiry($row->pt_expiry, TS_ISO_8601) ); } $db->freeResult($res); - - $images = array(); - $others = array(); - foreach ($missing as $title) - if ($title->getNamespace() == NS_FILE) - $images[] = $title->getDBKey(); - else - $others[] = $title; - - if (count($others) != 0) { - $lb = new LinkBatch($others); - $this->resetQueryParams(); - $this->addTables(array('page_restrictions', 'page', 'templatelinks')); - $this->addFields(array('pr_type', 'pr_level', 'pr_expiry', - 'page_title', 'page_namespace', - 'tl_title', 'tl_namespace')); - $this->addWhere($lb->constructSet('tl', $db)); - $this->addWhere('pr_page = page_id'); - $this->addWhere('pr_page = tl_from'); - $this->addWhereFld('pr_cascade', 1); - - $res = $this->select(__METHOD__); - while($row = $db->fetchObject($res)) { - $source = Title::makeTitle($row->page_namespace, $row->page_title); - $a = array( - 'type' => $row->pr_type, - 'level' => $row->pr_level, - 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ), - 'source' => $source->getPrefixedText() - ); - $prottitles[$row->tl_namespace][$row->tl_title][] = $a; - } - $db->freeResult($res); - } - - if (count($images) != 0) { - $this->resetQueryParams(); - $this->addTables(array('page_restrictions', 'page', 'imagelinks')); - $this->addFields(array('pr_type', 'pr_level', 'pr_expiry', - 'page_title', 'page_namespace', 'il_to')); - $this->addWhere('pr_page = page_id'); - $this->addWhere('pr_page = il_from'); - $this->addWhereFld('pr_cascade', 1); - $this->addWhereFld('il_to', $images); - - $res = $this->select(__METHOD__); - while($row = $db->fetchObject($res)) { - $source = Title::makeTitle($row->page_namespace, $row->page_title); - $a = array( - 'type' => $row->pr_type, - 'level' => $row->pr_level, - 'expiry' => Block::decodeExpiry( $row->pr_expiry, TS_ISO_8601 ), - 'source' => $source->getPrefixedText() - ); - $prottitles[NS_FILE][$row->il_to][] = $a; - } - $db->freeResult($res); - } - } - - // Run the talkid/subjectid query - if($fld_talkid || $fld_subjectid) - { - $talktitles = $subjecttitles = - $talkids = $subjectids = array(); - $everything = array_merge($titles, $missing); - foreach($everything as $t) - { - if(MWNamespace::isTalk($t->getNamespace())) - { - if($fld_subjectid) - $subjecttitles[] = $t->getSubjectPage(); - } - else if($fld_talkid) - $talktitles[] = $t->getTalkPage(); - } - if(count($talktitles) || count($subjecttitles)) - { - // Construct a custom WHERE clause that matches - // all titles in $talktitles and $subjecttitles - $lb = new LinkBatch(array_merge($talktitles, $subjecttitles)); - $this->resetQueryParams(); - $this->addTables('page'); - $this->addFields(array('page_title', 'page_namespace', 'page_id')); - $this->addWhere($lb->constructSet('page', $db)); - $res = $this->select(__METHOD__); - while($row = $db->fetchObject($res)) - { - if(MWNamespace::isTalk($row->page_namespace)) - $talkids[MWNamespace::getSubject($row->page_namespace)][$row->page_title] = $row->page_id; - else - $subjectids[MWNamespace::getTalk($row->page_namespace)][$row->page_title] = $row->page_id; - } - } } - foreach ( $titles as $pageid => $title ) { - $pageInfo = array ( - 'touched' => wfTimestamp(TS_ISO_8601, $pageTouched[$pageid]), - 'lastrevid' => intval($pageLatest[$pageid]), - 'counter' => intval($pageCounter[$pageid]), - 'length' => intval($pageLength[$pageid]), - ); - - if ($pageIsRedir[$pageid]) - $pageInfo['redirect'] = ''; - - if ($pageIsNew[$pageid]) - $pageInfo['new'] = ''; + // Cascading protections + $images = $others = array(); + foreach ($this->everything as $title) + if ($title->getNamespace() == NS_FILE) + $images[] = $title->getDBKey(); + else + $others[] = $title; + + if (count($others)) { + // Non-images: check templatelinks + $lb = new LinkBatch($others); + $this->resetQueryParams(); + $this->addTables(array('page_restrictions', 'page', 'templatelinks')); + $this->addFields(array('pr_type', 'pr_level', 'pr_expiry', + 'page_title', 'page_namespace', + 'tl_title', 'tl_namespace')); + $this->addWhere($lb->constructSet('tl', $db)); + $this->addWhere('pr_page = page_id'); + $this->addWhere('pr_page = tl_from'); + $this->addWhereFld('pr_cascade', 1); - if (!is_null($params['token'])) { - $tokenFunctions = $this->getTokenFunctions(); - $pageInfo['starttimestamp'] = wfTimestamp(TS_ISO_8601, time()); - foreach($params['token'] as $t) - { - $val = call_user_func($tokenFunctions[$t], $pageid, $title); - if($val === false) - $this->setWarning("Action '$t' is not allowed for the current user"); - else - $pageInfo[$t . 'token'] = $val; - } + $res = $this->select(__METHOD__); + while($row = $db->fetchObject($res)) { + $source = Title::makeTitle($row->page_namespace, $row->page_title); + $this->protections[$row->tl_namespace][$row->tl_title][] = array( + 'type' => $row->pr_type, + 'level' => $row->pr_level, + 'expiry' => Block::decodeExpiry($row->pr_expiry, TS_ISO_8601), + 'source' => $source->getPrefixedText() + ); } + $db->freeResult($res); + } - if($fld_protection) { - $pageInfo['protection'] = array(); - if (isset($protections[$pageid])) { - $pageInfo['protection'] = $protections[$pageid]; - $result->setIndexedTagName($pageInfo['protection'], 'pr'); - } - } - if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDBKey()])) - $pageInfo['talkid'] = $talkids[$title->getNamespace()][$title->getDBKey()]; - if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDBKey()])) - $pageInfo['subjectid'] = $subjectids[$title->getNamespace()][$title->getDBKey()]; - if($fld_url) { - $pageInfo['fullurl'] = $title->getFullURL(); - $pageInfo['editurl'] = $title->getFullURL('action=edit'); + if (count($images)) { + // Images: check imagelinks + $this->resetQueryParams(); + $this->addTables(array('page_restrictions', 'page', 'imagelinks')); + $this->addFields(array('pr_type', 'pr_level', 'pr_expiry', + 'page_title', 'page_namespace', 'il_to')); + $this->addWhere('pr_page = page_id'); + $this->addWhere('pr_page = il_from'); + $this->addWhereFld('pr_cascade', 1); + $this->addWhereFld('il_to', $images); + + $res = $this->select(__METHOD__); + while($row = $db->fetchObject($res)) { + $source = Title::makeTitle($row->page_namespace, $row->page_title); + $this->protections[NS_FILE][$row->il_to][] = array( + 'type' => $row->pr_type, + 'level' => $row->pr_level, + 'expiry' => Block::decodeExpiry($row->pr_expiry, TS_ISO_8601), + 'source' => $source->getPrefixedText() + ); } - if($fld_readable) - if($title->userCanRead()) - $pageInfo['readable'] = ''; - - $result->addValue(array ( - 'query', - 'pages' - ), $pageid, $pageInfo); + $db->freeResult($res); } + } - // Get properties for missing titles if requested - if(!is_null($params['token']) || $fld_protection || $fld_talkid || $fld_subjectid || - $fld_url || $fld_readable) + /** + * Get talk page IDs (if requested) and subject page IDs (if requested) + * and put them in $talkids and $subjectids + */ + private function getTSIDs() + { + $getTitles = $this->talkids = $this->subjectids = array(); + $db = $this->getDB(); + foreach($this->everything as $t) { - $res = &$result->getData(); - foreach($missing as $pageid => $title) { - if(!is_null($params['token'])) - { - $tokenFunctions = $this->getTokenFunctions(); - $res['query']['pages'][$pageid]['starttimestamp'] = wfTimestamp(TS_ISO_8601, time()); - foreach($params['token'] as $t) - { - $val = call_user_func($tokenFunctions[$t], $pageid, $title); - if($val === false) - $this->setWarning("Action '$t' is not allowed for the current user"); - else - $res['query']['pages'][$pageid][$t . 'token'] = $val; - } - } - if($fld_protection) - { - // Apparently the XML formatting code doesn't like array(null) - // This is painful to fix, so we'll just work around it - if(isset($prottitles[$title->getNamespace()][$title->getDBkey()])) - $res['query']['pages'][$pageid]['protection'] = $prottitles[$title->getNamespace()][$title->getDBkey()]; - else - $res['query']['pages'][$pageid]['protection'] = array(); - $result->setIndexedTagName($res['query']['pages'][$pageid]['protection'], 'pr'); - } - if($fld_talkid && isset($talkids[$title->getNamespace()][$title->getDBKey()])) - $res['query']['pages'][$pageid]['talkid'] = $talkids[$title->getNamespace()][$title->getDBKey()]; - if($fld_subjectid && isset($subjectids[$title->getNamespace()][$title->getDBKey()])) - $res['query']['pages'][$pageid]['subjectid'] = $subjectids[$title->getNamespace()][$title->getDBKey()]; - if($fld_url) { - $res['query']['pages'][$pageid]['fullurl'] = $title->getFullURL(); - $res['query']['pages'][$pageid]['editurl'] = $title->getFullURL('action=edit'); - } - if($fld_readable) - if($title->userCanRead()) - $res['query']['pages'][$pageid]['readable'] = ''; + if(MWNamespace::isTalk($t->getNamespace())) + { + if($this->fld_subjectid) + $getTitles[] = $t->getSubjectPage(); } + else if($this->fld_talkid) + $getTitles[] = $t->getTalkPage(); + } + if(!count($getTitles)) + return; + + // Construct a custom WHERE clause that matches + // all titles in $getTitles + $lb = new LinkBatch($getTitles); + $this->resetQueryParams(); + $this->addTables('page'); + $this->addFields(array('page_title', 'page_namespace', 'page_id')); + $this->addWhere($lb->constructSet('page', $db)); + $res = $this->select(__METHOD__); + while($row = $db->fetchObject($res)) + { + if(MWNamespace::isTalk($row->page_namespace)) + $this->talkids[MWNamespace::getSubject($row->page_namespace)][$row->page_title] = + intval($row->page_id); + else + $this->subjectids[MWNamespace::getTalk($row->page_namespace)][$row->page_title] = + intval($row->page_id); } } @@ -531,7 +515,8 @@ class ApiQueryInfo extends ApiQueryBase { ApiBase :: PARAM_DFLT => NULL, ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()) - ) + ), + 'continue' => null, ); } @@ -539,15 +524,15 @@ class ApiQueryInfo extends ApiQueryBase { return array ( 'prop' => array ( 'Which additional properties to get:', - ' "protection" - List the protection level of each page', - ' "talkid" - The page ID of the talk page for each non-talk page', - ' "subjectid" - The page ID of the parent page for each talk page' + ' protection - List the protection level of each page', + ' talkid - The page ID of the talk page for each non-talk page', + ' subjectid - The page ID of the parent page for each talk page' ), 'token' => 'Request a token to perform a data-modifying action on a page', + 'continue' => 'When more results are available, use this to continue', ); } - public function getDescription() { return 'Get basic page information such as namespace, title, last touched date, ...'; } @@ -560,6 +545,6 @@ class ApiQueryInfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryInfo.php 45683 2009-01-12 19:10:42Z raymond $'; + return __CLASS__ . ': $Id: ApiQueryInfo.php 48488 2009-03-17 15:18:26Z catrope $'; } } diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index 8eaf8d02..3abe5e3d 100644 --- a/includes/api/ApiQueryLangLinks.php +++ b/includes/api/ApiQueryLangLinks.php @@ -71,8 +71,6 @@ class ApiQueryLangLinks extends ApiQueryBase { $this->addOption('LIMIT', $params['limit'] + 1); $res = $this->select(__METHOD__); - $data = array(); - $lastId = 0; // database has no ID 0 $count = 0; $db = $this->getDB(); while ($row = $db->fetchObject($res)) { @@ -82,23 +80,15 @@ class ApiQueryLangLinks extends ApiQueryBase { $this->setContinueEnumParameter('continue', "{$row->ll_from}|{$row->ll_lang}"); break; } - if ($lastId != $row->ll_from) { - if($lastId != 0) { - $this->addPageSubItems($lastId, $data); - $data = array(); - } - $lastId = $row->ll_from; - } - $entry = array('lang' => $row->ll_lang); ApiResult :: setContent($entry, $row->ll_title); - $data[] = $entry; - } - - if($lastId != 0) { - $this->addPageSubItems($lastId, $data); + $fit = $this->addPageSubItem($row->ll_from, $entry); + if(!$fit) + { + $this->setContinueEnumParameter('continue', "{$row->ll_from}|{$row->ll_lang}"); + break; + } } - $db->freeResult($res); } @@ -134,6 +124,6 @@ class ApiQueryLangLinks extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLangLinks.php 43271 2008-11-06 22:38:42Z siebrand $'; + return __CLASS__ . ': $Id: ApiQueryLangLinks.php 46845 2009-02-05 14:30:59Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index 91b5b529..40a7c114 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -119,9 +119,6 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $res = $this->select(__METHOD__); if (is_null($resultPageSet)) { - - $data = array(); - $lastId = 0; // database has no ID 0 $count = 0; while ($row = $db->fetchObject($res)) { if(++$count > $params['limit']) { @@ -132,23 +129,17 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $this->keyToTitle($row->pl_title)); break; } - if ($lastId != $row->pl_from) { - if($lastId != 0) { - $this->addPageSubItems($lastId, $data); - $data = array(); - } - $lastId = $row->pl_from; - } - $vals = array(); ApiQueryBase :: addTitleInfo($vals, Title :: makeTitle($row->pl_namespace, $row->pl_title)); - $data[] = $vals; - } - - if($lastId != 0) { - $this->addPageSubItems($lastId, $data); + $fit = $this->addPageSubItem($row->pl_from, $vals); + if(!$fit) + { + $this->setContinueEnumParameter('continue', + "{$row->pl_from}|{$row->pl_namespace}|" . + $this->keyToTitle($row->pl_title)); + break; + } } - } else { $titles = array(); @@ -213,6 +204,6 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLinks.php 43271 2008-11-06 22:38:42Z siebrand $'; + return __CLASS__ . ': $Id: ApiQueryLinks.php 46845 2009-02-05 14:30:59Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 83c73b83..864aaa03 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -59,18 +59,21 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->addWhere($hideLogs); // Order is significant here - $this->addTables(array('user', 'page', 'logging')); + $this->addTables(array('logging', 'user', 'page')); + $this->addOption('STRAIGHT_JOIN'); $this->addJoinConds(array( + 'user' => array('JOIN', + 'user_id=log_user'), 'page' => array('LEFT JOIN', array( 'log_namespace=page_namespace', 'log_title=page_title')))); - $this->addWhere('user_id=log_user'); - $this->addOption('USE INDEX', array('logging' => 'times')); // default, may change + $index = 'times'; // default, may change $this->addFields(array ( 'log_type', 'log_action', 'log_timestamp', + 'log_deleted', )); $this->addFieldsIf('log_id', $this->fld_ids); @@ -81,20 +84,17 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->addFieldsIf('log_title', $this->fld_title); $this->addFieldsIf('log_comment', $this->fld_comment); $this->addFieldsIf('log_params', $this->fld_details); - - $this->addWhereFld('log_deleted', 0); if( !is_null($params['type']) ) { $this->addWhereFld('log_type', $params['type']); - $this->addOption('USE INDEX', array('logging' => array('type_time'))); + $index = 'type_time'; } $this->addWhereRange('log_timestamp', $params['dir'], $params['start'], $params['end']); $limit = $params['limit']; $this->addOption('LIMIT', $limit +1); - - $index = false; + $user = $params['user']; if (!is_null($user)) { $userid = User::idFromName($user); @@ -113,14 +113,19 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->addWhereFld('log_title', $titleObj->getDBkey()); // Use the title index in preference to the user index if there is a conflict - $index = 'page_time'; - } - if ( $index ) { - $this->addOption( 'USE INDEX', array( 'logging' => $index ) ); + $index = is_null($user) ? 'page_time' : array('page_time','user_time'); } + $this->addOption( 'USE INDEX', array( 'logging' => $index ) ); + + // Paranoia: avoid brute force searches (bug 17342) + if (!is_null($title)) { + $this->addWhere('log_deleted & ' . LogPage::DELETED_ACTION . ' = 0'); + } + if (!is_null($user)) { + $this->addWhere('log_deleted & ' . LogPage::DELETED_USER . ' = 0'); + } - $data = array (); $count = 0; $res = $this->select(__METHOD__); while ($row = $db->fetchObject($res)) { @@ -131,13 +136,18 @@ class ApiQueryLogEvents extends ApiQueryBase { } $vals = $this->extractRowInfo($row); - if($vals) - $data[] = $vals; + if(!$vals) + continue; + $fit = $this->getResult()->addValue(array('query', $this->getModuleName()), null, $vals); + if(!$fit) + { + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->log_timestamp)); + break; + } } $db->freeResult($res); - $this->getResult()->setIndexedTagName($data, 'item'); - $this->getResult()->addValue('query', $this->getModuleName(), $data); + $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'item'); } public static function addLogParams($result, &$vals, $params, $type, $ts) { @@ -150,9 +160,12 @@ class ApiQueryLogEvents extends ApiQueryBase { $vals2 = array(); ApiQueryBase :: addTitleInfo($vals2, $title, "new_"); $vals[$type] = $vals2; - $params = null; } } + if (isset ($params[1]) && $params[1]) { + $vals[$type]['suppressedredirect'] = ''; + } + $params = null; break; case 'patrol': $vals2 = array(); @@ -191,8 +204,12 @@ class ApiQueryLogEvents extends ApiQueryBase { } if ($this->fld_title) { - $title = Title :: makeTitle($row->log_namespace, $row->log_title); - ApiQueryBase :: addTitleInfo($vals, $title); + if (LogEventsList::isDeleted($row, LogPage::DELETED_ACTION)) { + $vals['actionhidden'] = ''; + } else { + $title = Title :: makeTitle($row->log_namespace, $row->log_title); + ApiQueryBase :: addTitleInfo($vals, $title); + } } if ($this->fld_type) { @@ -201,21 +218,33 @@ class ApiQueryLogEvents extends ApiQueryBase { } if ($this->fld_details && $row->log_params !== '') { - self::addLogParams($this->getResult(), $vals, - $row->log_params, $row->log_type, - $row->log_timestamp); + if (LogEventsList::isDeleted($row, LogPage::DELETED_ACTION)) { + $vals['actionhidden'] = ''; + } else { + self::addLogParams($this->getResult(), $vals, + $row->log_params, $row->log_type, + $row->log_timestamp); + } } if ($this->fld_user) { - $vals['user'] = $row->user_name; - if(!$row->log_user) - $vals['anon'] = ''; + if (LogEventsList::isDeleted($row, LogPage::DELETED_USER)) { + $vals['userhidden'] = ''; + } else { + $vals['user'] = $row->user_name; + if(!$row->log_user) + $vals['anon'] = ''; + } } if ($this->fld_timestamp) { $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->log_timestamp); } if ($this->fld_comment && isset($row->log_comment)) { - $vals['comment'] = $row->log_comment; + if (LogEventsList::isDeleted($row, LogPage::DELETED_COMMENT)) { + $vals['commenthidden'] = ''; + } else { + $vals['comment'] = $row->log_comment; + } } return $vals; @@ -290,6 +319,6 @@ class ApiQueryLogEvents extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryLogEvents.php 44234 2008-12-04 15:59:26Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryLogEvents.php 47904 2009-03-01 11:02:49Z catrope $'; } } diff --git a/includes/api/ApiQueryProtectedTitles.php b/includes/api/ApiQueryProtectedTitles.php new file mode 100644 index 00000000..779deee5 --- /dev/null +++ b/includes/api/ApiQueryProtectedTitles.php @@ -0,0 +1,191 @@ +<?php + +/* + * Created on Feb 13, 2009 + * + * API for MediaWiki 1.8+ + * + * Copyright (C) 2009 Roan Kattouw <Firstname>.<Lastname>@home.nl + * + * 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'); +} + +/** + * Query module to enumerate all create-protected pages. + * + * @ingroup API + */ +class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { + + public function __construct($query, $moduleName) { + parent :: __construct($query, $moduleName, 'pt'); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator($resultPageSet) { + $this->run($resultPageSet); + } + + private function run($resultPageSet = null) { + $db = $this->getDB(); + $params = $this->extractRequestParams(); + + $this->addTables('protected_titles'); + $this->addFields(array('pt_namespace', 'pt_title', 'pt_timestamp')); + + $prop = array_flip($params['prop']); + $this->addFieldsIf('pt_user', isset($prop['user'])); + $this->addFieldsIf('pt_reason', isset($prop['comment'])); + $this->addFieldsIf('pt_expiry', isset($prop['expiry'])); + $this->addFieldsIf('pt_create_perm', isset($prop['level'])); + + $this->addWhereRange('pt_timestamp', $params['dir'], $params['start'], $params['end']); + $this->addWhereFld('pt_namespace', $params['namespace']); + $this->addWhereFld('pt_create_perm', $params['level']); + + if(isset($prop['user'])) + { + $this->addTables('user'); + $this->addFields('user_name'); + $this->addJoinConds(array('user' => array('LEFT JOIN', + 'user_id=pt_user' + ))); + } + + $this->addOption('LIMIT', $params['limit'] + 1); + $res = $this->select(__METHOD__); + + $count = 0; + $result = $this->getResult(); + while ($row = $db->fetchObject($res)) { + if (++ $count > $params['limit']) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->pt_timestamp)); + break; + } + + $title = Title::makeTitle($row->pt_namespace, $row->pt_title); + if (is_null($resultPageSet)) { + $vals = array(); + ApiQueryBase::addTitleInfo($vals, $title); + if(isset($prop['timestamp'])) + $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->pt_timestamp); + if(isset($prop['user']) && !is_null($row->user_name)) + $vals['user'] = $row->user_name; + if(isset($prop['comment'])) + $vals['comment'] = $row->pt_reason; + if(isset($prop['expiry'])) + $vals['expiry'] = Block::decodeExpiry($row->pt_expiry, TS_ISO_8601); + if(isset($prop['level'])) + $vals['level'] = $row->pt_create_perm; + + $fit = $result->addValue(array('query', $this->getModuleName()), null, $vals); + if(!$fit) + { + $this->setContinueEnumParameter('start', + wfTimestamp(TS_ISO_8601, $row->pt_timestamp)); + break; + } + } else { + $titles[] = $title; + } + } + $db->freeResult($res); + if(is_null($resultPageSet)) + $result->setIndexedTagName_internal(array('query', $this->getModuleName()), $this->getModulePrefix()); + else + $resultPageSet->populateFromTitles($titles); + } + + public function getAllowedParams() { + global $wgRestrictionLevels; + return array ( + 'namespace' => array ( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => 'namespace', + ), + 'level' => array( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_TYPE => array_diff($wgRestrictionLevels, array('')) + ), + 'limit' => array ( + ApiBase :: PARAM_DFLT => 10, + ApiBase :: PARAM_TYPE => 'limit', + ApiBase :: PARAM_MIN => 1, + ApiBase :: PARAM_MAX => ApiBase :: LIMIT_BIG1, + ApiBase :: PARAM_MAX2 => ApiBase :: LIMIT_BIG2 + ), + 'dir' => array ( + ApiBase :: PARAM_DFLT => 'older', + ApiBase :: PARAM_TYPE => array ( + 'older', + 'newer' + ) + ), + 'start' => array( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'end' => array( + ApiBase :: PARAM_TYPE => 'timestamp' + ), + 'prop' => array( + ApiBase :: PARAM_ISMULTI => true, + ApiBase :: PARAM_DFLT => 'timestamp|level', + ApiBase :: PARAM_TYPE => array( + 'timestamp', + 'user', + 'comment', + 'expiry', + 'level' + ) + ), + ); + } + + public function getParamDescription() { + return array ( + 'namespace' => 'Only list titles in these namespaces', + 'start' => 'Start listing at this protection timestamp', + 'end' => 'Stop listing at this protection timestamp', + 'dir' => 'The direction in which to list', + 'limit' => 'How many total pages to return.', + 'prop' => 'Which properties to get', + 'level' => 'Only list titles with these protection levels', + ); + } + + public function getDescription() { + return 'List all titles protected from creation'; + } + + protected function getExamples() { + return array ( + 'api.php?action=query&list=protectedtitles', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id: ApiQueryProtectedTitles.php 47235 2009-02-13 21:53:08Z catrope $'; + } +}
\ No newline at end of file diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php index e7b8bf46..73c4a81c 100644 --- a/includes/api/ApiQueryRandom.php +++ b/includes/api/ApiQueryRandom.php @@ -62,7 +62,7 @@ if (!defined('MEDIAWIKI')) { $this->addFields($resultPageSet->getPageTableFields()); } - protected function runQuery(&$data, &$resultPageSet) { + protected function runQuery(&$resultPageSet) { $db = $this->getDB(); $res = $this->select(__METHOD__); $count = 0; @@ -73,7 +73,14 @@ if (!defined('MEDIAWIKI')) { // Prevent duplicates if(!in_array($row->page_id, $this->pageIDs)) { - $data[] = $this->extractRowInfo($row); + $fit = $this->getResult()->addValue( + array('query', $this->getModuleName()), + null, $this->extractRowInfo($row)); + if(!$fit) + # We can't really query-continue a random list. + # Return an insanely high value so + # $count < $limit is false + return 1E9; $this->pageIDs[] = $row->page_id; } } @@ -87,11 +94,10 @@ if (!defined('MEDIAWIKI')) { public function run($resultPageSet = null) { $params = $this->extractRequestParams(); $result = $this->getResult(); - $data = array(); $this->pageIDs = array(); $this->prepareQuery(wfRandom(), $params['limit'], $params['namespace'], $resultPageSet, $params['redirect']); - $count = $this->runQuery($data, $resultPageSet); + $count = $this->runQuery($resultPageSet); if($count < $params['limit']) { /* We got too few pages, we probably picked a high value @@ -99,21 +105,19 @@ if (!defined('MEDIAWIKI')) { * also the comment in Title::getRandomTitle() */ $this->prepareQuery(0, $params['limit'] - $count, $params['namespace'], $resultPageSet, $params['redirect']); - $this->runQuery($data, $resultPageSet); + $this->runQuery($resultPageSet); } if(is_null($resultPageSet)) { - $result->setIndexedTagName($data, 'page'); - $result->addValue('query', $this->getModuleName(), $data); + $result->setIndexedTagName_internal(array('query', $this->getModuleName()), 'page'); } } private function extractRowInfo($row) { $title = Title::makeTitle($row->page_namespace, $row->page_title); $vals = array(); - $vals['title'] = $title->getPrefixedText(); - $vals['ns'] = $row->page_namespace; - $vals['id'] = $row->page_id; + $vals['id'] = intval($row->page_id); + ApiQueryBase::addTitleInfo($vals, $title); return $vals; } @@ -157,4 +161,4 @@ if (!defined('MEDIAWIKI')) { public function getVersion() { return __CLASS__ . ': $Id: ApiQueryRandom.php overlordq$'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 04eb910f..191eec28 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -97,25 +97,6 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->addWhereRange('rc_timestamp', $params['dir'], $params['start'], $params['end']); $this->addWhereFld('rc_namespace', $params['namespace']); $this->addWhereFld('rc_deleted', 0); - if($params['titles']) - { - $lb = new LinkBatch; - foreach($params['titles'] as $t) - { - $obj = Title::newFromText($t); - $lb->addObj($obj); - if($obj->getNamespace() < 0) - { - // LinkBatch refuses these, but we need them anyway - if(!array_key_exists($obj->getNamespace(), $lb->data)) - $lb->data[$obj->getNamespace()] = array(); - $lb->data[$obj->getNamespace()][$obj->getDBKey()] = 1; - } - } - $where = $lb->constructSet('rc', $this->getDB()); - if($where != '') - $this->addWhere($where); - } if(!is_null($params['type'])) $this->addWhereFld('rc_type', $this->parseRCType($params['type'])); @@ -210,9 +191,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { $this->token = $params['token']; $this->addOption('LIMIT', $params['limit'] +1); - $data = array (); $count = 0; - /* Perform the actual query. */ $db = $this->getDB(); $res = $this->select(__METHOD__); @@ -229,16 +208,20 @@ class ApiQueryRecentChanges extends ApiQueryBase { $vals = $this->extractRowInfo($row); /* Add that row's data to our final output. */ - if($vals) - $data[] = $vals; + if(!$vals) + continue; + $fit = $this->getResult()->addValue(array('query', $this->getModuleName()), null, $vals); + if(!$fit) + { + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rc_timestamp)); + break; + } } $db->freeResult($res); /* Format the result */ - $result = $this->getResult(); - $result->setIndexedTagName($data, 'rc'); - $result->addValue('query', $this->getModuleName(), $data); + $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'rc'); } /** @@ -328,7 +311,7 @@ class ApiQueryRecentChanges extends ApiQueryBase { $vals['patrolled'] = ''; if ($this->fld_loginfo && $row->rc_type == RC_LOG) { - $vals['logid'] = $row->rc_logid; + $vals['logid'] = intval($row->rc_logid); $vals['logtype'] = $row->rc_log_type; $vals['logaction'] = $row->rc_log_action; ApiQueryLogEvents::addLogParams($this->getResult(), @@ -389,9 +372,6 @@ class ApiQueryRecentChanges extends ApiQueryBase { ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_TYPE => 'namespace' ), - 'titles' => array( - ApiBase :: PARAM_ISMULTI => true - ), 'prop' => array ( ApiBase :: PARAM_ISMULTI => true, ApiBase :: PARAM_DFLT => 'title|timestamp|ids', @@ -451,7 +431,6 @@ class ApiQueryRecentChanges extends ApiQueryBase { 'end' => 'The timestamp to end enumerating.', 'dir' => 'In which direction to enumerate.', 'namespace' => 'Filter log entries to only this namespace(s)', - 'titles' => 'Filter log entries to only these page titles', 'prop' => 'Include additional pieces of information', 'token' => 'Which tokens to obtain for each change', 'show' => array ( @@ -474,6 +453,6 @@ class ApiQueryRecentChanges extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 44719 2008-12-17 16:34:01Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryRecentChanges.php 50094 2009-05-01 06:24:09Z tstarling $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 977e792b..ca9152ad 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -100,9 +100,29 @@ class ApiQueryRevisions extends ApiQueryBase { if ($pageCount > 1 && $enumRevMode) $this->dieUsage('titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start and end parameters may only be used on a single page.', 'multpages'); + if (!is_null($params['diffto'])) { + if ($params['diffto'] == 'cur') + $params['diffto'] = 0; + if ((!ctype_digit($params['diffto']) || $params['diffto'] < 0) + && $params['diffto'] != 'prev' && $params['diffto'] != 'next') + $this->dieUsage('rvdiffto must be set to a non-negative number, "prev", "next" or "cur"', 'diffto'); + // Check whether the revision exists and is readable, + // DifferenceEngine returns a rather ambiguous empty + // string if that's not the case + if ($params['diffto'] != 0) { + $difftoRev = Revision::newFromID($params['diffto']); + if (!$difftoRev) + $this->dieUsageMsg(array('nosuchrevid', $params['diffto'])); + if (!$difftoRev->userCan(Revision::DELETED_TEXT)) { + $this->setWarning("Couldn't diff to r{$difftoRev->getID()}: content is hidden"); + $params['diffto'] = null; + } + } + } + $this->addTables('revision'); - $this->addFields( Revision::selectFields() ); - $this->addTables( 'page' ); + $this->addFields(Revision::selectFields()); + $this->addTables('page'); $this->addWhere('page_id = rev_page'); $prop = array_flip($params['prop']); @@ -116,6 +136,7 @@ class ApiQueryRevisions extends ApiQueryBase { $this->fld_size = isset ($prop['size']); $this->fld_user = isset ($prop['user']); $this->token = $params['token']; + $this->diffto = $params['diffto']; if ( !is_null($this->token) || $pageCount > 0) { $this->addFields( Revision::selectPageFields() ); @@ -134,7 +155,7 @@ class ApiQueryRevisions extends ApiQueryBase { $this->addTables('text'); $this->addWhere('rev_text_id=old_id'); $this->addFields('old_id'); - $this->addFields( Revision::selectTextFields() ); + $this->addFields(Revision::selectTextFields()); $this->fld_content = true; @@ -176,9 +197,14 @@ class ApiQueryRevisions extends ApiQueryBase { if (is_null($params['startid']) && is_null($params['endid'])) $this->addWhereRange('rev_timestamp', $params['dir'], $params['start'], $params['end']); - else + else { $this->addWhereRange('rev_id', $params['dir'], $params['startid'], $params['endid']); + // One of start and end can be set + // If neither is set, this does nothing + $this->addWhereRange('rev_timestamp', $params['dir'], + $params['start'], $params['end'], false); + } // must manually initialize unset limit if (is_null($limit)) @@ -186,14 +212,18 @@ class ApiQueryRevisions extends ApiQueryBase { $this->validateLimit('limit', $limit, 1, $userMax, $botMax); // There is only one ID, use it - $this->addWhereFld('rev_page', current(array_keys($pageSet->getGoodTitles()))); + $this->addWhereFld('rev_page', reset(array_keys($pageSet->getGoodTitles()))); if(!is_null($params['user'])) { $this->addWhereFld('rev_user_text', $params['user']); - } elseif (!is_null( $params['excludeuser'])) { + } elseif (!is_null($params['excludeuser'])) { $this->addWhere('rev_user_text != ' . $this->getDB()->addQuotes($params['excludeuser'])); } + if(!is_null($params['user']) || !is_null($params['excludeuser'])) { + // Paranoia: avoid brute force searches (bug 17342) + $this->addWhere('rev_deleted & ' . Revision::DELETED_USER . ' = 0'); + } } elseif ($revCount > 0) { $max = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; @@ -204,6 +234,10 @@ class ApiQueryRevisions extends ApiQueryBase { // Get all revision IDs $this->addWhereFld('rev_id', array_keys($revs)); + if(!is_null($params['continue'])) + $this->addWhere("rev_id >= '" . intval($params['continue']) . "'"); + $this->addOption('ORDER BY', 'rev_id'); + // assumption testing -- we should never get more then $revCount rows. $limit = $revCount; } @@ -220,6 +254,22 @@ class ApiQueryRevisions extends ApiQueryBase { // Get all page IDs $this->addWhereFld('page_id', array_keys($titles)); + // Every time someone relies on equality propagation, god kills a kitten :) + $this->addWhereFld('rev_page', array_keys($titles)); + + if(!is_null($params['continue'])) + { + $cont = explode('|', $params['continue']); + if(count($cont) != 2) + $this->dieUsage("Invalid continue param. You should pass the original " . + "value returned by the previous query", "_badcontinue"); + $pageid = intval($cont[0]); + $revid = intval($cont[1]); + $this->addWhere("rev_page > '$pageid' OR " . + "(rev_page = '$pageid' AND " . + "rev_id >= '$revid')"); + } + $this->addOption('ORDER BY', 'rev_page, rev_id'); // assumption testing -- we should never get more then $pageCount rows. $limit = $pageCount; @@ -242,37 +292,30 @@ class ApiQueryRevisions extends ApiQueryBase { $this->setContinueEnumParameter('startid', intval($row->rev_id)); break; } - $revision = new Revision( $row ); - $this->getResult()->addValue( - array ( - 'query', - 'pages', - $revision->getPage(), - 'revisions'), - null, - $this->extractRowInfo( $revision )); - } - $db->freeResult($res); - - // Ensure that all revisions are shown as '<rev>' elements - $result = $this->getResult(); - if ($result->getIsRawMode()) { - $data =& $result->getData(); - foreach ($data['query']['pages'] as & $page) { - if (is_array($page) && array_key_exists('revisions', $page)) { - $result->setIndexedTagName($page['revisions'], 'rev'); - } + // + $fit = $this->addPageSubItem($revision->getPage(), $this->extractRowInfo($revision), 'rev'); + if(!$fit) + { + if($enumRevMode) + $this->setContinueEnumParameter('startid', intval($row->rev_id)); + else if($revCount > 0) + $this->setContinueEnumParameter('continue', intval($row->rev_id)); + else + $this->setContinueEnumParameter('continue', intval($row->rev_page) . + '|' . intval($row->rev_id)); + break; } } + $db->freeResult($res); } private function extractRowInfo( $revision ) { - + $title = $revision->getTitle(); $vals = array (); if ($this->fld_ids) { - $vals['revid'] = $revision->getId(); + $vals['revid'] = intval($revision->getId()); // $vals['oldid'] = intval($row->rev_text_id); // todo: should this be exposed? } @@ -280,9 +323,13 @@ class ApiQueryRevisions extends ApiQueryBase { $vals['minor'] = ''; if ($this->fld_user) { - $vals['user'] = $revision->getUserText(); - if (!$revision->getUser()) - $vals['anon'] = ''; + if ($revision->isDeleted(Revision::DELETED_USER)) { + $vals['userhidden'] = ''; + } else { + $vals['user'] = $revision->getUserText(); + if (!$revision->getUser()) + $vals['anon'] = ''; + } } if ($this->fld_timestamp) { @@ -290,17 +337,18 @@ class ApiQueryRevisions extends ApiQueryBase { } if ($this->fld_size && !is_null($revision->getSize())) { - $vals['size'] = $revision->getSize(); + $vals['size'] = intval($revision->getSize()); } if ($this->fld_comment) { - $comment = $revision->getComment(); - if (strval($comment) !== '') - $vals['comment'] = $comment; - } - - if(!is_null($this->token) || ($this->fld_content && $this->expandTemplates)) - $title = $revision->getTitle(); + if ($revision->isDeleted(Revision::DELETED_COMMENT)) { + $vals['commenthidden'] = ''; + } else { + $comment = $revision->getComment(); + if (strval($comment) !== '') + $vals['comment'] = $comment; + } + } if(!is_null($this->token)) { @@ -314,8 +362,8 @@ class ApiQueryRevisions extends ApiQueryBase { $vals[$t . 'token'] = $val; } } - - if ($this->fld_content) { + + if ($this->fld_content && !$revision->isDeleted(Revision::DELETED_TEXT)) { global $wgParser; $text = $revision->getText(); # Expand templates after getting section content because @@ -341,6 +389,24 @@ class ApiQueryRevisions extends ApiQueryBase { $text = $wgParser->preprocess( $text, $title, new ParserOptions() ); } ApiResult :: setContent($vals, $text); + } else if ($this->fld_content) { + $vals['texthidden'] = ''; + } + + if (!is_null($this->diffto)) { + global $wgAPIMaxUncachedDiffs; + static $n = 0; // Numer of uncached diffs we've had + if($n< $wgAPIMaxUncachedDiffs) { + $engine = new DifferenceEngine($title, $revision->getID(), $this->diffto); + $difftext = $engine->getDiffBody(); + $vals['diff']['from'] = $engine->getOldid(); + $vals['diff']['to'] = $engine->getNewid(); + ApiResult::setContent($vals['diff'], $difftext); + if(!$engine->wasCacheHit()) + $n++; + } else { + $vals['diff']['notcached'] = ''; + } } return $vals; } @@ -398,6 +464,8 @@ class ApiQueryRevisions extends ApiQueryBase { ApiBase :: PARAM_TYPE => array_keys($this->getTokenFunctions()), ApiBase :: PARAM_ISMULTI => true ), + 'continue' => null, + 'diffto' => null, ); } @@ -416,6 +484,9 @@ class ApiQueryRevisions extends ApiQueryBase { 'generatexml' => 'generate XML parse tree for revision content', 'section' => 'only retrieve the content of this section', 'token' => 'Which tokens to obtain for each revision', + 'continue' => 'When more results are available, use this to continue', + 'diffto' => array('Revision ID to diff each revision to.', + 'Use "prev", "next" and "cur" for the previous, next and current revision respectively.'), ); } @@ -448,6 +519,6 @@ class ApiQueryRevisions extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRevisions.php 44719 2008-12-17 16:34:01Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryRevisions.php 48642 2009-03-20 20:21:38Z midom $'; } } diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index cb020fff..668f00e5 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -87,7 +87,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $this->dieUsage("{$what} search is disabled", "search-{$what}-disabled"); - $data = array (); + $titles = array (); $count = 0; while( $result = $matches->next() ) { if (++ $count > $limit) { @@ -102,20 +102,23 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $title = $result->getTitle(); if (is_null($resultPageSet)) { - $data[] = array( - 'ns' => intval($title->getNamespace()), - 'title' => $title->getPrefixedText()); + $vals = array(); + ApiQueryBase::addTitleInfo($vals, $title); + $fit = $this->getResult()->addValue(array('query', $this->getModuleName()), null, $vals); + if(!$fit) + { + $this->setContinueEnumParameter('offset', $params['offset'] + $count - 1); + break; + } } else { - $data[] = $title; + $titles[] = $title; } } if (is_null($resultPageSet)) { - $result = $this->getResult(); - $result->setIndexedTagName($data, 'p'); - $result->addValue('query', $this->getModuleName(), $data); + $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'p'); } else { - $resultPageSet->populateFromTitles($data); + $resultPageSet->populateFromTitles($titles); } } @@ -170,6 +173,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQuerySearch.php 44186 2008-12-03 19:33:57Z catrope $'; + return __CLASS__ . ': $Id: ApiQuerySearch.php 47865 2009-02-27 16:03:01Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index 84757f7f..6b867abb 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -41,44 +41,60 @@ class ApiQuerySiteinfo extends ApiQueryBase { public function execute() { $params = $this->extractRequestParams(); + $done = array(); foreach( $params['prop'] as $p ) { switch ( $p ) { case 'general': - $this->appendGeneralInfo( $p ); + $fit = $this->appendGeneralInfo( $p ); break; case 'namespaces': - $this->appendNamespaces( $p ); + $fit = $this->appendNamespaces( $p ); break; case 'namespacealiases': - $this->appendNamespaceAliases( $p ); + $fit = $this->appendNamespaceAliases( $p ); break; case 'specialpagealiases': - $this->appendSpecialPageAliases( $p ); + $fit = $this->appendSpecialPageAliases( $p ); break; case 'magicwords': - $this->appendMagicWords( $p ); + $fit = $this->appendMagicWords( $p ); break; case 'interwikimap': $filteriw = isset( $params['filteriw'] ) ? $params['filteriw'] : false; - $this->appendInterwikiMap( $p, $filteriw ); + $fit = $this->appendInterwikiMap( $p, $filteriw ); break; case 'dbrepllag': - $this->appendDbReplLagInfo( $p, $params['showalldb'] ); + $fit = $this->appendDbReplLagInfo( $p, $params['showalldb'] ); break; case 'statistics': - $this->appendStatistics( $p ); + $fit = $this->appendStatistics( $p ); break; case 'usergroups': - $this->appendUserGroups( $p ); + $fit = $this->appendUserGroups( $p ); break; case 'extensions': - $this->appendExtensions( $p ); + $fit = $this->appendExtensions( $p ); + break; + case 'fileextensions': + $fit = $this->appendFileExtensions( $p ); + break; + case 'rightsinfo': + $fit = $this->appendRightsInfo( $p ); break; default : ApiBase :: dieDebug( __METHOD__, "Unknown prop=$p" ); } + if(!$fit) + { + # Abuse siprop as a query-continue parameter + # and set it to all unprocessed props + $this->setContinueEnumParameter('prop', implode('|', + array_diff($params['prop'], $done))); + break; + } + $done[] = $p; } } @@ -121,9 +137,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { $offset = 0; } $data['timezone'] = $tz; - $data['timeoffset'] = $offset; + $data['timeoffset'] = intval($offset); - $this->getResult()->addValue( 'query', $property, $data ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendNamespaces( $property ) { @@ -132,7 +148,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { foreach( $wgContLang->getFormattedNamespaces() as $ns => $title ) { $data[$ns] = array( - 'id' => $ns + 'id' => intval($ns) ); ApiResult :: setContent( $data[$ns], $title ); $canonical = MWNamespace::getCanonicalName( $ns ); @@ -145,24 +161,29 @@ class ApiQuerySiteinfo extends ApiQueryBase { } $this->getResult()->setIndexedTagName( $data, 'ns' ); - $this->getResult()->addValue( 'query', $property, $data ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendNamespaceAliases( $property ) { global $wgNamespaceAliases, $wgContLang; $wgContLang->load(); - $aliases = array_merge($wgNamespaceAliases, $wgContLang->namespaceAliases); + $aliases = array_merge( $wgNamespaceAliases, $wgContLang->namespaceAliases ); + $namespaces = $wgContLang->getNamespaces(); $data = array(); foreach( $aliases as $title => $ns ) { + if( $namespaces[$ns] == $title ) { + // Don't list duplicates + continue; + } $item = array( - 'id' => $ns + 'id' => intval($ns) ); ApiResult :: setContent( $item, strtr( $title, '_', ' ' ) ); $data[] = $item; } $this->getResult()->setIndexedTagName( $data, 'ns' ); - $this->getResult()->addValue( 'query', $property, $data ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendSpecialPageAliases( $property ) { @@ -175,7 +196,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data[] = $arr; } $this->getResult()->setIndexedTagName( $data, 'specialpage' ); - $this->getResult()->addValue( 'query', $property, $data ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendMagicWords( $property ) { @@ -191,7 +212,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data[] = $arr; } $this->getResult()->setIndexedTagName($data, 'magicword'); - $this->getResult()->addValue('query', $property, $data); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendInterwikiMap( $property, $filter ) { @@ -229,7 +250,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $db->freeResult( $res ); $this->getResult()->setIndexedTagName( $data, 'iw' ); - $this->getResult()->addValue( 'query', $property, $data ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendDbReplLagInfo( $property, $includeAll ) { @@ -251,27 +272,30 @@ class ApiQuerySiteinfo extends ApiQueryBase { list( $host, $lag ) = wfGetLB()->getMaxLag(); $data[] = array( 'host' => $wgShowHostnames ? $host : '', - 'lag' => $lag + 'lag' => intval( $lag ) ); } $result = $this->getResult(); $result->setIndexedTagName( $data, 'db' ); - $result->addValue( 'query', $property, $data ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendStatistics( $property ) { + global $wgDisableCounters; $data = array(); $data['pages'] = intval( SiteStats::pages() ); $data['articles'] = intval( SiteStats::articles() ); - $data['views'] = intval( SiteStats::views() ); + if ( !$wgDisableCounters ) { + $data['views'] = intval( SiteStats::views() ); + } $data['edits'] = intval( SiteStats::edits() ); $data['images'] = intval( SiteStats::images() ); $data['users'] = intval( SiteStats::users() ); $data['activeusers'] = intval( SiteStats::activeUsers() ); $data['admins'] = intval( SiteStats::numberingroup('sysop') ); $data['jobs'] = intval( SiteStats::jobs() ); - $this->getResult()->addValue( 'query', $property, $data ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendUserGroups( $property ) { @@ -284,7 +308,18 @@ class ApiQuerySiteinfo extends ApiQueryBase { } $this->getResult()->setIndexedTagName( $data, 'group' ); - $this->getResult()->addValue( 'query', $property, $data ); + return $this->getResult()->addValue( 'query', $property, $data ); + } + + protected function appendFileExtensions( $property ) { + global $wgFileExtensions; + + $data = array(); + foreach( $wgFileExtensions as $ext ) { + $data[] = array( 'ext' => $ext ); + } + $this->getResult()->setIndexedTagName( $data, 'fe' ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendExtensions( $property ) { @@ -317,7 +352,25 @@ class ApiQuerySiteinfo extends ApiQueryBase { } $this->getResult()->setIndexedTagName( $data, 'ext' ); - $this->getResult()->addValue( 'query', $property, $data ); + return $this->getResult()->addValue( 'query', $property, $data ); + } + + + protected function appendRightsInfo( $property ) { + global $wgRightsPage, $wgRightsUrl, $wgRightsText; + $title = Title::newFromText( $wgRightsPage ); + $url = $title ? $title->getFullURL() : $wgRightsUrl; + $text = $wgRightsText; + if( !$text && $title ) { + $text = $title->getPrefixedText(); + } + + $data = array( + 'url' => $url ? $url : '', + 'text' => $text ? $text : '' + ); + + return $this->getResult()->addValue( 'query', $property, $data ); } @@ -337,6 +390,8 @@ class ApiQuerySiteinfo extends ApiQueryBase { 'statistics', 'usergroups', 'extensions', + 'fileextensions', + 'rightsinfo', ) ), 'filteriw' => array( @@ -353,16 +408,18 @@ class ApiQuerySiteinfo extends ApiQueryBase { return array( 'prop' => array( 'Which sysinfo properties to get:', - ' "general" - Overall system information', - ' "namespaces" - List of registered namespaces and their canonical names', - ' "namespacealiases" - List of registered namespace aliases', - ' "specialpagealiases" - List of special page aliases', - ' "magicwords" - List of magic words and their aliases', - ' "statistics" - Returns site statistics', - ' "interwikimap" - Returns interwiki map (optionally filtered)', - ' "dbrepllag" - Returns database server with the highest replication lag', - ' "usergroups" - Returns user groups and the associated permissions', - ' "extensions" - Returns extensions installed on the wiki', + ' general - Overall system information', + ' namespaces - List of registered namespaces and their canonical names', + ' namespacealiases - List of registered namespace aliases', + ' specialpagealiases - List of special page aliases', + ' magicwords - List of magic words and their aliases', + ' statistics - Returns site statistics', + ' interwikimap - Returns interwiki map (optionally filtered)', + ' dbrepllag - Returns database server with the highest replication lag', + ' usergroups - Returns user groups and the associated permissions', + ' extensions - Returns extensions installed on the wiki', + ' fileextensions - Returns list of file extensions allowed to be uploaded', + ' rightsinfo - Returns wiki rights (license) information if available', ), 'filteriw' => 'Return only local or only nonlocal entries of the interwiki map', 'showalldb' => 'List all database servers, not just the one lagging the most', @@ -382,6 +439,6 @@ class ApiQuerySiteinfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 44862 2008-12-20 23:49:16Z catrope $'; + return __CLASS__ . ': $Id: ApiQuerySiteinfo.php 48060 2009-03-05 13:52:14Z demon $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index be6c8bc4..24c73996 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -41,7 +41,8 @@ class ApiQueryContributions extends ApiQueryBase { private $params, $username; private $fld_ids = false, $fld_title = false, $fld_timestamp = false, - $fld_comment = false, $fld_flags = false; + $fld_comment = false, $fld_flags = false, + $fld_patrolled = false; public function execute() { @@ -54,6 +55,7 @@ class ApiQueryContributions extends ApiQueryBase { $this->fld_comment = isset($prop['comment']); $this->fld_flags = isset($prop['flags']); $this->fld_timestamp = isset($prop['timestamp']); + $this->fld_patrolled = isset($prop['patrolled']); // TODO: if the query is going only against the revision table, should this be done? $this->selectNamedDB('contributions', DB_SLAVE, 'contributions'); @@ -81,7 +83,6 @@ class ApiQueryContributions extends ApiQueryBase { $res = $this->select( __METHOD__ ); //Initialise some variables - $data = array (); $count = 0; $limit = $this->params['limit']; @@ -97,16 +98,21 @@ class ApiQueryContributions extends ApiQueryBase { } $vals = $this->extractRowInfo($row); - if ($vals) - $data[] = $vals; + $fit = $this->getResult()->addValue(array('query', $this->getModuleName()), null, $vals); + if(!$fit) + { + if($this->multiUserMode) + $this->setContinueEnumParameter('continue', $this->continueStr($row)); + else + $this->setContinueEnumParameter('start', wfTimestamp(TS_ISO_8601, $row->rev_timestamp)); + break; + } } //Free the database record so the connection can get on with other stuff $db->freeResult($res); - //And send the whole shebang out as output. - $this->getResult()->setIndexedTagName($data, 'item'); - $this->getResult()->addValue('query', $this->getModuleName(), $data); + $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'item'); } /** @@ -132,12 +138,12 @@ class ApiQueryContributions extends ApiQueryBase { * Prepares the query and returns the limit of rows requested */ private function prepareQuery() { - - //We're after the revision table, and the corresponding page row for - //anything we retrieve. - $this->addTables(array('revision', 'page')); + // We're after the revision table, and the corresponding page + // row for anything we retrieve. We may also need the + // recentchanges row. + $tables = array('page', 'revision'); // Order may change $this->addWhere('page_id=rev_page'); - + // Handle continue parameter if($this->multiUserMode && !is_null($this->params['continue'])) { @@ -162,7 +168,8 @@ class ApiQueryContributions extends ApiQueryBase { // ... and in the specified timeframe. // Ensure the same sort order for rev_user_text and rev_timestamp // so our query is indexed - $this->addWhereRange('rev_user_text', $this->params['dir'], null, null); + if($this->multiUserMode) + $this->addWhereRange('rev_user_text', $this->params['dir'], null, null); $this->addWhereRange('rev_timestamp', $this->params['dir'], $this->params['start'], $this->params['end'] ); $this->addWhereFld('page_namespace', $this->params['namespace']); @@ -170,14 +177,17 @@ class ApiQueryContributions extends ApiQueryBase { $show = $this->params['show']; if (!is_null($show)) { $show = array_flip($show); - if (isset ($show['minor']) && isset ($show['!minor'])) + if ((isset($show['minor']) && isset($show['!minor'])) + || (isset($show['patrolled']) && isset($show['!patrolled']))) $this->dieUsage("Incorrect parameter - mutually exclusive values may not be supplied", 'show'); - $this->addWhereIf('rev_minor_edit = 0', isset ($show['!minor'])); - $this->addWhereIf('rev_minor_edit != 0', isset ($show['minor'])); + $this->addWhereIf('rev_minor_edit = 0', isset($show['!minor'])); + $this->addWhereIf('rev_minor_edit != 0', isset($show['minor'])); + $this->addWhereIf('rc_patrolled = 0', isset($show['!patrolled'])); + $this->addWhereIf('rc_patrolled != 0', isset($show['patrolled'])); } $this->addOption('LIMIT', $this->params['limit'] + 1); - $this->addOption( 'USE INDEX', array( 'revision' => 'usertext_timestamp' ) ); + $index['revision'] = 'usertext_timestamp'; // Mandatory fields: timestamp allows request continuation // ns+title checks if the user has access rights for this page @@ -187,15 +197,49 @@ class ApiQueryContributions extends ApiQueryBase { 'page_namespace', 'page_title', 'rev_user_text', - )); + )); + + if(isset($show['patrolled']) || isset($show['!patrolled']) || + $this->fld_patrolled) + { + global $wgUser; + if(!$wgUser->useRCPatrol() && !$wgUser->useNPPatrol()) + $this->dieUsage("You need the patrol right to request the patrolled flag", 'permissiondenied'); + // Use a redundant join condition on both + // timestamp and ID so we can use the timestamp + // index + $index['recentchanges'] = 'rc_user_text'; + if(isset($show['patrolled']) || isset($show['!patrolled'])) + { + // Put the tables in the right order for + // STRAIGHT_JOIN + $tables = array('revision', 'recentchanges', 'page'); + $this->addOption('STRAIGHT_JOIN'); + $this->addWhere('rc_user_text=rev_user_text'); + $this->addWhere('rc_timestamp=rev_timestamp'); + $this->addWhere('rc_this_oldid=rev_id'); + } + else + { + $tables[] = 'recentchanges'; + $this->addJoinConds(array('recentchanges' => array( + 'LEFT JOIN', array( + 'rc_user_text=rev_user_text', + 'rc_timestamp=rev_timestamp', + 'rc_this_oldid=rev_id')))); + } + } + $this->addTables($tables); + $this->addOption('USE INDEX', $index); $this->addFieldsIf('rev_page', $this->fld_ids); $this->addFieldsIf('rev_id', $this->fld_ids || $this->fld_flags); $this->addFieldsIf('page_latest', $this->fld_flags); // $this->addFieldsIf('rev_text_id', $this->fld_ids); // Should this field be exposed? $this->addFieldsIf('rev_comment', $this->fld_comment); $this->addFieldsIf('rev_minor_edit', $this->fld_flags); - $this->addFieldsIf('page_is_new', $this->fld_flags); + $this->addFieldsIf('rev_parent_id', $this->fld_flags); + $this->addFieldsIf('rc_patrolled', $this->fld_patrolled); } /** @@ -220,7 +264,7 @@ class ApiQueryContributions extends ApiQueryBase { $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rev_timestamp); if ($this->fld_flags) { - if ($row->page_is_new) + if ($row->rev_parent_id == 0) $vals['new'] = ''; if ($row->rev_minor_edit) $vals['minor'] = ''; @@ -228,9 +272,12 @@ class ApiQueryContributions extends ApiQueryBase { $vals['top'] = ''; } - if ($this->fld_comment && isset( $row->rev_comment ) ) + if ($this->fld_comment && isset($row->rev_comment)) $vals['comment'] = $row->rev_comment; + if ($this->fld_patrolled && $row->rc_patrolled) + $vals['patrolled'] = ''; + return $vals; } @@ -279,7 +326,8 @@ class ApiQueryContributions extends ApiQueryBase { 'title', 'timestamp', 'comment', - 'flags' + 'flags', + 'patrolled', ) ), 'show' => array ( @@ -287,6 +335,8 @@ class ApiQueryContributions extends ApiQueryBase { ApiBase :: PARAM_TYPE => array ( 'minor', '!minor', + 'patrolled', + '!patrolled', ) ), ); @@ -303,7 +353,8 @@ class ApiQueryContributions extends ApiQueryBase { 'dir' => 'The direction to search (older or newer).', 'namespace' => 'Only list contributions in these namespaces', 'prop' => 'Include additional pieces of information', - 'show' => 'Show only items that meet this criteria, e.g. non minor edits only: show=!minor', + 'show' => array('Show only items that meet this criteria, e.g. non minor edits only: show=!minor', + 'NOTE: if show=patrolled or show=!patrolled is set, revisions older than $wgRCMaxAge won\'t be shown',), ); } @@ -319,6 +370,6 @@ class ApiQueryContributions extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryUserContributions.php 43271 2008-11-06 22:38:42Z siebrand $'; + return __CLASS__ . ': $Id: ApiQueryUserContributions.php 47037 2009-02-09 14:07:18Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index 203b7e25..ac99ad6d 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -57,7 +57,7 @@ class ApiQueryUserInfo extends ApiQueryBase { global $wgUser; $result = $this->getResult(); $vals = array(); - $vals['id'] = $wgUser->getId(); + $vals['id'] = intval($wgUser->getId()); $vals['name'] = $wgUser->getName(); if($wgUser->isAnon()) @@ -87,11 +87,17 @@ class ApiQueryUserInfo extends ApiQueryBase { $vals['preferencestoken'] = $wgUser->editToken(); } if (isset($this->prop['editcount'])) { - $vals['editcount'] = $wgUser->getEditCount(); + $vals['editcount'] = intval($wgUser->getEditCount()); } if (isset($this->prop['ratelimits'])) { $vals['ratelimits'] = $this->getRateLimits(); } + if (isset($this->prop['email'])) { + $vals['email'] = $wgUser->getEmail(); + $auth = $wgUser->getEmailAuthenticationTimestamp(); + if(!is_null($auth)) + $vals['emailauthenticated'] = wfTimestamp(TS_ISO_8601, $auth); + } return $vals; } @@ -122,8 +128,8 @@ class ApiQueryUserInfo extends ApiQueryBase { foreach($categories as $cat) if(isset($limits[$cat]) && !is_null($limits[$cat])) { - $retval[$action][$cat]['hits'] = $limits[$cat][0]; - $retval[$action][$cat]['seconds'] = $limits[$cat][1]; + $retval[$action][$cat]['hits'] = intval($limits[$cat][0]); + $retval[$action][$cat]['seconds'] = intval($limits[$cat][1]); } return $retval; } @@ -141,7 +147,8 @@ class ApiQueryUserInfo extends ApiQueryBase { 'options', 'preferencestoken', 'editcount', - 'ratelimits' + 'ratelimits', + 'email', ) ) ); @@ -174,6 +181,6 @@ class ApiQueryUserInfo extends ApiQueryBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryUserInfo.php 43764 2008-11-20 15:15:00Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryUserInfo.php 47865 2009-02-27 16:03:01Z catrope $'; } } diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index e50d8d82..b8aa60e7 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -51,82 +51,91 @@ if (!defined('MEDIAWIKI')) { $this->prop = array(); } - if(is_array($params['users'])) { - $r = $this->getOtherUsersInfo($params['users']); - $result->setIndexedTagName($r, 'user'); - } - $result->addValue("query", $this->getModuleName(), $r); - } - - protected function getOtherUsersInfo($users) { - $goodNames = $retval = array(); + $users = (array)$params['users']; + $goodNames = $done = array(); + $result = $this->getResult(); // Canonicalize user names foreach($users as $u) { $n = User::getCanonicalName($u); if($n === false || $n === '') - $retval[] = array('name' => $u, 'invalid' => ''); + { + $vals = array('name' => $u, 'invalid' => ''); + $fit = $result->addValue(array('query', $this->getModuleName()), + null, $vals); + if(!$fit) + { + $this->setContinueEnumParameter('users', + implode('|', array_diff($users, $done))); + $goodNames = array(); + break; + } + $done[] = $u; + } else $goodNames[] = $n; } - if(!count($goodNames)) - return $retval; - - $db = $this->getDB(); - $this->addTables('user', 'u1'); - $this->addFields('u1.*'); - $this->addWhereFld('u1.user_name', $goodNames); - - if(isset($this->prop['groups'])) { - $this->addTables('user_groups'); - $this->addJoinConds(array('user_groups' => array('LEFT JOIN', 'ug_user=u1.user_id'))); - $this->addFields('ug_group'); - } - if(isset($this->prop['blockinfo'])) { - $this->addTables('ipblocks'); - $this->addTables('user', 'u2'); - $u2 = $this->getAliasedName('user', 'u2'); - $this->addJoinConds(array( - 'ipblocks' => array('LEFT JOIN', 'ipb_user=u1.user_id'), - $u2 => array('LEFT JOIN', 'ipb_by=u2.user_id'))); - $this->addFields(array('ipb_reason', 'u2.user_name blocker_name')); - } + if(count($goodNames)) + { + $db = $this->getDb(); + $this->addTables('user', 'u1'); + $this->addFields('u1.*'); + $this->addWhereFld('u1.user_name', $goodNames); + + if(isset($this->prop['groups'])) { + $this->addTables('user_groups'); + $this->addJoinConds(array('user_groups' => array('LEFT JOIN', 'ug_user=u1.user_id'))); + $this->addFields('ug_group'); + } + if(isset($this->prop['blockinfo'])) { + $this->addTables('ipblocks'); + $this->addTables('user', 'u2'); + $u2 = $this->getAliasedName('user', 'u2'); + $this->addJoinConds(array( + 'ipblocks' => array('LEFT JOIN', 'ipb_user=u1.user_id'), + $u2 => array('LEFT JOIN', 'ipb_by=u2.user_id'))); + $this->addFields(array('ipb_reason', 'u2.user_name AS blocker_name')); + } - $data = array(); - $res = $this->select(__METHOD__); - while(($r = $db->fetchObject($res))) { - $user = User::newFromRow($r); - $name = $user->getName(); - $data[$name]['name'] = $name; - if(isset($this->prop['editcount'])) - // No proper member function in User class for this - $data[$name]['editcount'] = $r->user_editcount; - if(isset($this->prop['registration'])) - // Nor for this one - $data[$name]['registration'] = wfTimestampOrNull(TS_ISO_8601, $r->user_registration); - if(isset($this->prop['groups'])) - // This row contains only one group, others will be added from other rows - if(!is_null($r->ug_group)) + $data = array(); + $res = $this->select(__METHOD__); + while(($r = $db->fetchObject($res))) { + $user = User::newFromRow($r); + $name = $user->getName(); + $data[$name]['name'] = $name; + if(isset($this->prop['editcount'])) + $data[$name]['editcount'] = intval($user->getEditCount()); + if(isset($this->prop['registration'])) + $data[$name]['registration'] = wfTimestampOrNull(TS_ISO_8601, $user->getRegistration()); + if(isset($this->prop['groups']) && !is_null($r->ug_group)) + // This row contains only one group, others will be added from other rows $data[$name]['groups'][] = $r->ug_group; - if(isset($this->prop['blockinfo'])) - if(!is_null($r->blocker_name)) { + if(isset($this->prop['blockinfo']) && !is_null($r->blocker_name)) { $data[$name]['blockedby'] = $r->blocker_name; $data[$name]['blockreason'] = $r->ipb_reason; } - if(isset($this->prop['emailable']) && $user->canReceiveEmail()) - $data[$name]['emailable'] = ''; + if(isset($this->prop['emailable']) && $user->canReceiveEmail()) + $data[$name]['emailable'] = ''; + } } - // Second pass: add result data to $retval foreach($goodNames as $u) { if(!isset($data[$u])) - $retval[] = array('name' => $u, 'missing' => ''); + $data[$u] = array('name' => $u, 'missing' => ''); else { if(isset($this->prop['groups']) && isset($data[$u]['groups'])) $this->getResult()->setIndexedTagName($data[$u]['groups'], 'g'); - $retval[] = $data[$u]; } + $fit = $result->addValue(array('query', $this->getModuleName()), + null, $data[$u]); + if(!$fit) + { + $this->setContinueEnumParameter('users', + implode('|', array_diff($users, $done))); + break; + } + $done[] = $u; } - return $retval; + return $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'user'); } public function getAllowedParams() { @@ -171,6 +180,6 @@ if (!defined('MEDIAWIKI')) { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryUsers.php 44231 2008-12-04 14:42:30Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryUsers.php 50094 2009-05-01 06:24:09Z tstarling $'; } } diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index ed3482fb..b3949102 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -92,6 +92,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->addFieldsIf('rc_new', $this->fld_flags); $this->addFieldsIf('rc_minor', $this->fld_flags); + $this->addFieldsIf('rc_bot', $this->fld_flags); $this->addFieldsIf('rc_user', $this->fld_user); $this->addFieldsIf('rc_user_text', $this->fld_user); $this->addFieldsIf('rc_comment', $this->fld_comment); @@ -168,7 +169,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->addOption('LIMIT', $params['limit'] +1); - $data = array (); + $ids = array (); $count = 0; $res = $this->select(__METHOD__); @@ -182,13 +183,18 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { if (is_null($resultPageSet)) { $vals = $this->extractRowInfo($row); - if ($vals) - $data[] = $vals; + $fit = $this->getResult()->addValue(array('query', $this->getModuleName()), null, $vals); + if(!$fit) + { + $this->setContinueEnumParameter('start', + wfTimestamp(TS_ISO_8601, $row->rc_timestamp)); + break; + } } else { if ($params['allrev']) { - $data[] = intval($row->rc_this_oldid); + $ids[] = intval($row->rc_this_oldid); } else { - $data[] = intval($row->rc_cur_id); + $ids[] = intval($row->rc_cur_id); } } } @@ -196,13 +202,12 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $db->freeResult($res); if (is_null($resultPageSet)) { - $this->getResult()->setIndexedTagName($data, 'item'); - $this->getResult()->addValue('query', $this->getModuleName(), $data); + $this->getResult()->setIndexedTagName_internal(array('query', $this->getModuleName()), 'item'); } elseif ($params['allrev']) { - $resultPageSet->populateFromRevisionIDs($data); + $resultPageSet->populateFromRevisionIDs($ids); } else { - $resultPageSet->populateFromPageIDs($data); + $resultPageSet->populateFromPageIDs($ids); } } @@ -229,6 +234,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $vals['new'] = ''; if ($row->rc_minor) $vals['minor'] = ''; + if ($row->rc_bot) + $vals['bot'] = ''; } if ($this->fld_patrol && isset($row->rc_patrolled)) @@ -237,8 +244,6 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { if ($this->fld_timestamp) $vals['timestamp'] = wfTimestamp(TS_ISO_8601, $row->rc_timestamp); - $this->addFieldsIf('rc_new_len', $this->fld_sizes); - if ($this->fld_sizes) { $vals['oldlen'] = intval($row->rc_old_len); $vals['newlen'] = intval($row->rc_new_len); @@ -338,6 +343,6 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryWatchlist.php 44719 2008-12-17 16:34:01Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryWatchlist.php 47865 2009-02-27 16:03:01Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php index e9951b42..54bb5a35 100644 --- a/includes/api/ApiQueryWatchlistRaw.php +++ b/includes/api/ApiQueryWatchlistRaw.php @@ -89,7 +89,6 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { $res = $this->select(__METHOD__); $db = $this->getDB(); - $data = array(); $titles = array(); $count = 0; while($row = $db->fetchObject($res)) @@ -108,16 +107,19 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { ApiQueryBase::addTitleInfo($vals, $t); if(isset($prop['changed']) && !is_null($row->wl_notificationtimestamp)) $vals['changed'] = wfTimestamp(TS_ISO_8601, $row->wl_notificationtimestamp); - $data[] = $vals; + $fit = $this->getResult()->addValue($this->getModuleName(), null, $vals); + if(!$fit) + { + $this->setContinueEnumParameter('continue', $row->wl_namespace . '|' . + $this->keyToTitle($row->wl_title)); + break; + } } else $titles[] = $t; } if(is_null($resultPageSet)) - { - $this->getResult()->setIndexedTagName($data, 'wr'); - $this->getResult()->addValue(null, $this->getModuleName(), $data); - } + $this->getResult()->setIndexedTagName_internal($this->getModuleName(), 'wr'); else $resultPageSet->populateFromTitles($titles); } @@ -174,6 +176,6 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryWatchlistRaw.php 41651 2008-10-04 14:30:33Z catrope $'; + return __CLASS__ . ': $Id: ApiQueryWatchlistRaw.php 46845 2009-02-05 14:30:59Z catrope $'; } -} +}
\ No newline at end of file diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index 900953e0..3dbee08a 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -47,14 +47,16 @@ if (!defined('MEDIAWIKI')) { */ class ApiResult extends ApiBase { - private $mData, $mIsRawMode; + private $mData, $mIsRawMode, $mSize, $mCheckingSize; /** - * Constructor - */ + * Constructor + * @param $main ApiMain object + */ public function __construct($main) { parent :: __construct($main, 'result'); $this->mIsRawMode = false; + $this->mCheckingSize = true; $this->reset(); } @@ -63,6 +65,7 @@ class ApiResult extends ApiBase { */ public function reset() { $this->mData = array (); + $this->mSize = 0; } /** @@ -74,22 +77,68 @@ class ApiResult extends ApiBase { } /** - * Returns true if the result is being created for the formatter that requested raw data. + * Returns true whether the formatter requested raw data. + * @return bool */ public function getIsRawMode() { return $this->mIsRawMode; } /** - * Get result's internal data array + * Get the result's internal data array (read-only) + * @return array */ - public function & getData() { + public function getData() { return $this->mData; } + + /** + * Get the 'real' size of a result item. This means the strlen() of the item, + * or the sum of the strlen()s of the elements if the item is an array. + * @param $value mixed + * @return int + */ + public static function size($value) { + $s = 0; + if(is_array($value)) + foreach($value as $v) + $s += self::size($v); + else if(!is_object($value)) + // Objects can't always be cast to string + $s = strlen($value); + return $s; + } + + /** + * Get the size of the result, i.e. the amount of bytes in it + * @return int + */ + public function getSize() { + return $this->mSize; + } + + /** + * Disable size checking in addValue(). Don't use this unless you + * REALLY know what you're doing. Values added while size checking + * was disabled will not be counted (ever) + */ + public function disableSizeCheck() { + $this->mCheckingSize = false; + } + + /** + * Re-enable size checking in addValue() + */ + public function enableSizeCheck() { + $this->mCheckingSize = true; + } /** * Add an output value to the array by name. * Verifies that value with the same name has not been added before. + * @param $arr array to add $value to + * @param $name string Index of $arr to add $value at + * @param $value mixed */ public static function setElement(& $arr, $name, $value) { if ($arr === null || $name === null || $value === null || !is_array($arr) || is_array($name)) @@ -109,10 +158,12 @@ class ApiResult extends ApiBase { } /** - * Adds the content element to the array. + * Adds a content element to an array. * Use this function instead of hardcoding the '*' element. - * @param string $subElemName when present, content element is created as a sub item of the arr. - * Use this parameter to create elements in format <elem>text</elem> without attributes + * @param $arr array to add the content element to + * @param $subElemName string when present, content element is created + * as a sub item of $arr. Use this parameter to create elements in + * format <elem>text</elem> without attributes */ public static function setContent(& $arr, $value, $subElemName = null) { if (is_array($value)) @@ -128,7 +179,10 @@ class ApiResult extends ApiBase { /** * In case the array contains indexed values (in addition to named), - * all indexed values will have the given tag name. + * give all indexed values the given tag name. This function MUST be + * called on every arrray that has numerical indexes. + * @param $arr array + * @param $tag string Tag name */ public function setIndexedTagName(& $arr, $tag) { // In raw mode, add the '_element', otherwise just ignore @@ -141,7 +195,9 @@ class ApiResult extends ApiBase { } /** - * Calls setIndexedTagName() on $arr and each sub-array + * Calls setIndexedTagName() on each sub-array of $arr + * @param $arr array + * @param $tag string Tag name */ public function setIndexedTagName_recursive(&$arr, $tag) { @@ -157,14 +213,41 @@ class ApiResult extends ApiBase { } /** + * Calls setIndexedTagName() on an array already in the result. + * Don't specify a path to a value that's not in the result, or + * you'll get nasty errors. + * @param $path array Path to the array, like addValue()'s $path + * @param $tag string + */ + public function setIndexedTagName_internal( $path, $tag ) { + $data = & $this->mData; + foreach((array)$path as $p) { + if ( !isset( $data[$p] ) ) { + $data[$p] = array(); + } + $data = & $data[$p]; + } + if(is_null($data)) + return; + $this->setIndexedTagName($data, $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 * If $name is empty, the $value is added as a next list element data[] = $value + * @return bool True if $value fits in the result, false if not */ public function addValue($path, $name, $value) { - - $data = & $this->getData(); + global $wgAPIMaxResultSize; + $data = & $this->mData; + if( $this->mCheckingSize ) { + $newsize = $this->mSize + self::size($value); + if($newsize > $wgAPIMaxResultSize) + return false; + $this->mSize = $newsize; + } if (!is_null($path)) { if (is_array($path)) { @@ -184,6 +267,26 @@ class ApiResult extends ApiBase { $data[] = $value; // Add list element else ApiResult :: setElement($data, $name, $value); // Add named element + return true; + } + + /** + * Unset a value previously added to the result set. + * Fails silently if the value isn't found. + * For parameters, see addValue() + * @param $path array + * @param $name string + */ + public function unsetValue($path, $name) { + $data = & $this->mData; + if(!is_null($path)) + foreach((array)$path as $p) { + if(!isset($data[$p])) + return; + $data = & $data[$p]; + } + $this->mSize -= self::size($data[$name]); + unset($data[$name]); } /** @@ -191,8 +294,17 @@ class ApiResult extends ApiBase { */ public function cleanUpUTF8() { - $data = & $this->getData(); - array_walk_recursive($data, array('UtfNormal', 'cleanUp')); + array_walk_recursive($this->mData, array('ApiResult', 'cleanUp_helper')); + } + + /** + * Callback function for cleanUpUTF8() + */ + private static function cleanUp_helper(&$s) + { + if(!is_string($s)) + return; + $s = UtfNormal::cleanUp($s); } public function execute() { @@ -200,7 +312,7 @@ class ApiResult extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiResult.php 45752 2009-01-14 21:36:57Z catrope $'; + return __CLASS__ . ': $Id: ApiResult.php 47447 2009-02-18 12:41:28Z tstarling $'; } } diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index 653dca9e..0f0eae10 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -37,7 +37,6 @@ class ApiRollback extends ApiBase { } public function execute() { - $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); $titleObj = NULL; @@ -68,15 +67,15 @@ class ApiRollback extends ApiBase { if($retval) // We don't care about multiple errors, just report one of them - $this->dieUsageMsg(current($retval)); + $this->dieUsageMsg(reset($retval)); $info = array( 'title' => $titleObj->getPrefixedText(), - 'pageid' => $details['current']->getPage(), + 'pageid' => intval($details['current']->getPage()), 'summary' => $details['summary'], - 'revid' => $titleObj->getLatestRevID(), - 'old_revid' => $details['current']->getID(), - 'last_revid' => $details['target']->getID() + 'revid' => intval($titleObj->getLatestRevID()), + 'old_revid' => intval($details['current']->getID()), + 'last_revid' => intval($details['target']->getID()) ); $this->getResult()->addValue(null, $this->getModuleName(), $info); @@ -84,6 +83,10 @@ class ApiRollback extends ApiBase { public function mustBePosted() { return true; } + public function isWriteMode() { + return true; + } + public function getAllowedParams() { return array ( 'title' => null, @@ -119,6 +122,6 @@ class ApiRollback extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiRollback.php 45043 2008-12-26 04:13:47Z mrzman $'; + return __CLASS__ . ': $Id: ApiRollback.php 48122 2009-03-07 12:58:41Z catrope $'; } } diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index cd52c518..9216317a 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -44,7 +44,6 @@ class ApiUnblock extends ApiBase { */ public function execute() { global $wgUser; - $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); if($params['gettoken']) @@ -72,7 +71,7 @@ class ApiUnblock extends ApiBase { if($retval) $this->dieUsageMsg($retval); - $res['id'] = $id; + $res['id'] = intval($id); $res['user'] = $user; $res['reason'] = $reason; $this->getResult()->addValue(null, $this->getModuleName(), $res); @@ -80,6 +79,10 @@ class ApiUnblock extends ApiBase { public function mustBePosted() { return true; } + public function isWriteMode() { + return true; + } + public function getAllowedParams() { return array ( 'id' => null, @@ -114,6 +117,6 @@ class ApiUnblock extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiUnblock.php 42651 2008-10-27 12:06:49Z catrope $'; + return __CLASS__ . ': $Id: ApiUnblock.php 48091 2009-03-06 13:49:44Z catrope $'; } } diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php index 7ae9a3c0..ddc9f7f8 100644 --- a/includes/api/ApiUndelete.php +++ b/includes/api/ApiUndelete.php @@ -38,7 +38,6 @@ class ApiUndelete extends ApiBase { public function execute() { global $wgUser; - $this->getMain()->requestWriteMode(); $params = $this->extractRequestParams(); $titleObj = NULL; @@ -78,14 +77,18 @@ class ApiUndelete extends ApiBase { array($titleObj, array(), $wgUser, $params['reason']) ); $info['title'] = $titleObj->getPrefixedText(); - $info['revisions'] = $retval[0]; - $info['fileversions'] = $retval[1]; - $info['reason'] = $retval[2]; + $info['revisions'] = intval($retval[0]); + $info['fileversions'] = intval($retval[1]); + $info['reason'] = intval($retval[2]); $this->getResult()->addValue(null, $this->getModuleName(), $info); } public function mustBePosted() { return true; } + public function isWriteMode() { + return true; + } + public function getAllowedParams() { return array ( 'title' => null, @@ -121,6 +124,6 @@ class ApiUndelete extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiUndelete.php 43270 2008-11-06 22:30:55Z siebrand $'; + return __CLASS__ . ': $Id: ApiUndelete.php 48091 2009-03-06 13:49:44Z catrope $'; } } diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php index ab122fea..1b98fb86 100644 --- a/includes/api/ApiWatch.php +++ b/includes/api/ApiWatch.php @@ -29,8 +29,7 @@ if (!defined('MEDIAWIKI')) { } /** - * API module to allow users to log out of the wiki. API equivalent of - * Special:Userlogout. + * API module to allow users to watch a page * * @ingroup API */ @@ -42,7 +41,6 @@ class ApiWatch extends ApiBase { public function execute() { global $wgUser; - $this->getMain()->requestWriteMode(); if(!$wgUser->isLoggedIn()) $this->dieUsage('You must be logged-in to have a watchlist', 'notloggedin'); $params = $this->extractRequestParams(); @@ -66,6 +64,10 @@ class ApiWatch extends ApiBase { $this->getResult()->addValue(null, $this->getModuleName(), $res); } + public function isWriteMode() { + return true; + } + public function getAllowedParams() { return array ( 'title' => null, @@ -94,6 +96,6 @@ class ApiWatch extends ApiBase { } public function getVersion() { - return __CLASS__ . ': $Id: ApiWatch.php 40460 2008-09-04 22:20:32Z ialex $'; + return __CLASS__ . ': $Id: ApiWatch.php 48091 2009-03-06 13:49:44Z catrope $'; } } |