diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2014-12-27 15:41:37 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2014-12-31 11:43:28 +0100 |
commit | c1f9b1f7b1b77776192048005dcc66dcf3df2bfb (patch) | |
tree | 2b38796e738dd74cb42ecd9bfd151803108386bc /includes/api | |
parent | b88ab0086858470dd1f644e64cb4e4f62bb2be9b (diff) |
Update to MediaWiki 1.24.1
Diffstat (limited to 'includes/api')
102 files changed, 8106 insertions, 6020 deletions
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index c1454e76..944e4895 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -30,36 +30,40 @@ * 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. + * which parameters to expect, how to parse and validate them. * * Profiling: various methods to allow keeping tabs on various tasks and their - * time costs + * time costs * * Self-documentation: code to allow the API to document its own state * * @ingroup API */ abstract class ApiBase extends ContextSource { - // These constants allow modules to specify exactly how to treat incoming parameters. - const PARAM_DFLT = 0; // Default value of the parameter - const PARAM_ISMULTI = 1; // Boolean, do we accept more than one item for this parameter (e.g.: titles)? - const PARAM_TYPE = 2; // Can be either a string type (e.g.: 'integer') or an array of allowed values - const PARAM_MAX = 3; // Max value allowed for a parameter. Only applies if TYPE='integer' - const PARAM_MAX2 = 4; // Max value allowed for a parameter for bots and sysops. Only applies if TYPE='integer' - const PARAM_MIN = 5; // Lowest value allowed for a parameter. Only applies if TYPE='integer' - const PARAM_ALLOW_DUPLICATES = 6; // Boolean, do we allow the same value to be set more than once when ISMULTI=true - const PARAM_DEPRECATED = 7; // Boolean, is the parameter deprecated (will show a warning) + // Default value of the parameter + const PARAM_DFLT = 0; + // Boolean, do we accept more than one item for this parameter (e.g.: titles)? + const PARAM_ISMULTI = 1; + // Can be either a string type (e.g.: 'integer') or an array of allowed values + const PARAM_TYPE = 2; + // Max value allowed for a parameter. Only applies if TYPE='integer' + const PARAM_MAX = 3; + // Max value allowed for a parameter for bots and sysops. Only applies if TYPE='integer' + const PARAM_MAX2 = 4; + // Lowest value allowed for a parameter. Only applies if TYPE='integer' + const PARAM_MIN = 5; + // Boolean, do we allow the same value to be set more than once when ISMULTI=true + const PARAM_ALLOW_DUPLICATES = 6; + // Boolean, is the parameter deprecated (will show a warning) + const PARAM_DEPRECATED = 7; /// @since 1.17 const PARAM_REQUIRED = 8; // Boolean, is the parameter required? /// @since 1.17 - const PARAM_RANGE_ENFORCE = 9; // Boolean, if MIN/MAX are set, enforce (die) these? Only applies if TYPE='integer' Use with extreme caution - - const PROP_ROOT = 'ROOT'; // Name of property group that is on the root element of the result, i.e. not part of a list - const PROP_LIST = 'LIST'; // Boolean, is the result multiple items? Defaults to true for query modules, to false for other modules - const PROP_TYPE = 0; // Type of the property, uses same format as PARAM_TYPE - const PROP_NULLABLE = 1; // Boolean, can the property be not included in the result? Defaults to false + // Boolean, if MIN/MAX are set, enforce (die) these? + // Only applies if TYPE='integer' Use with extreme caution + const PARAM_RANGE_ENFORCE = 9; const LIMIT_BIG1 = 500; // Fast query, std user limit const LIMIT_BIG2 = 5000; // Fast query, bot/sysop limit @@ -73,17 +77,19 @@ abstract class ApiBase extends ContextSource { */ const GET_VALUES_FOR_HELP = 1; - private $mMainModule, $mModuleName, $mModulePrefix; + /** @var ApiMain */ + private $mMainModule; + /** @var string */ + private $mModuleName, $mModulePrefix; private $mSlaveDB = null; private $mParamCache = array(); /** - * Constructor - * @param $mainModule ApiMain object + * @param ApiMain $mainModule * @param string $moduleName Name of this module * @param string $modulePrefix Prefix to use for parameter names */ - public function __construct( $mainModule, $moduleName, $modulePrefix = '' ) { + public function __construct( ApiMain $mainModule, $moduleName, $modulePrefix = '' ) { $this->mMainModule = $mainModule; $this->mModuleName = $moduleName; $this->mModulePrefix = $modulePrefix; @@ -93,9 +99,11 @@ abstract class ApiBase extends ContextSource { } } - /***************************************************************************** - * ABSTRACT METHODS * - *****************************************************************************/ + + /************************************************************************//** + * @name Methods to implement + * @{ + */ /** * Evaluates the parameters, performs the requested query, and sets up @@ -116,432 +124,232 @@ abstract class ApiBase extends ContextSource { abstract public 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. Usually done with SVN's Id keyword - * @return string - * @deprecated since 1.21, version string is no longer supported + * Get the module manager, or null if this module has no sub-modules + * @since 1.21 + * @return ApiModuleManager */ - public function getVersion() { - wfDeprecated( __METHOD__, '1.21' ); - return ''; + public function getModuleManager() { + return null; } /** - * Get the name of the module being executed by this instance - * @return string + * 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 getModuleName() { - return $this->mModuleName; + public function getCustomPrinter() { + return null; } /** - * Get the module manager, or null if this module has no sub-modules - * @since 1.21 - * @return ApiModuleManager + * Returns the description string for this module + * @return string|array */ - public function getModuleManager() { - return null; + protected function getDescription() { + return false; } /** - * Get parameter prefix (usually two letters or an empty string). - * @return string + * Returns usage examples for this module. Return false if no examples are available. + * @return bool|string|array */ - public function getModulePrefix() { - return $this->mModulePrefix; + protected function getExamples() { + return false; } /** - * Get the name of the module as shown in the profiler log + * @return bool|string|array Returns a false if the module has no help URL, + * else returns a (array of) string + */ + public function getHelpUrls() { + return false; + } + + /** + * 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. * - * @param $db DatabaseBase|bool + * Some derived classes may choose to handle an integer $flags parameter + * in the overriding methods. Callers of this method can pass zero or + * more OR-ed flags like GET_VALUES_FOR_HELP. * - * @return string + * @return array|bool */ - public function getModuleProfileName( $db = false ) { - if ( $db ) { - return 'API:' . $this->mModuleName . '-DB'; - } else { - return 'API:' . $this->mModuleName; - } + protected function getAllowedParams( /* $flags = 0 */ ) { + // int $flags is not declared because it causes "Strict standards" + // warning. Most derived classes do not implement it. + return false; } /** - * Get the main module - * @return ApiMain object + * Returns an array of parameter descriptions. + * Don't call this function directly: use getFinalParamDescription() to + * allow hooks to modify descriptions as needed. + * @return array|bool False on no parameter descriptions */ - public function getMain() { - return $this->mMainModule; + protected function getParamDescription() { + return false; } /** - * Returns true if this module is the main module ($this === $this->mMainModule), - * false otherwise. + * Indicates if this module needs maxlag to be checked * @return bool */ - public function isMain() { - return $this === $this->mMainModule; + public function shouldCheckMaxlag() { + return true; } /** - * Get the result object - * @return ApiResult + * Indicates whether this module requires read rights + * @return bool */ - public function getResult() { - // Main module has getResult() method overridden - // Safety - avoid infinite loop: - if ( $this->isMain() ) { - ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' ); - } - return $this->getMain()->getResult(); + public function isReadMode() { + return true; } /** - * Get the result data array (read-only) - * @return array + * Indicates whether this module requires write mode + * @return bool */ - public function getResultData() { - return $this->getResult()->getData(); + public function isWriteMode() { + return false; } /** - * Create a new RequestContext object to use e.g. for calls to other parts - * the software. - * The object will have the WebRequest and the User object set to the ones - * used in this instance. - * - * @deprecated since 1.19 use getContext to get the current context - * @return DerivativeContext + * Indicates whether this module must be called with a POST request + * @return bool */ - public function createContext() { - wfDeprecated( __METHOD__, '1.19' ); - return new DerivativeContext( $this->getContext() ); + public function mustBePosted() { + return $this->needsToken() !== false; } /** - * 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 string $warning Warning message + * Returns the token type this module requires in order to execute. + * + * Modules are strongly encouraged to use the core 'csrf' type unless they + * have specialized security needs. If the token type is not one of the + * core types, you must use the ApiQueryTokensRegisterTypes hook to + * register it. + * + * Returning a non-falsey value here will cause self::getFinalParams() to + * return a required string 'token' parameter and + * self::getFinalParamDescription() to ensure there is standardized + * documentation for it. Also, self::mustBePosted() must return true when + * tokens are used. + * + * In previous versions of MediaWiki, true was a valid return value. + * Returning true will generate errors indicating that the API module needs + * updating. + * + * @return string|false */ - public function setWarning( $warning ) { - $result = $this->getResult(); - $data = $result->getData(); - $moduleName = $this->getModuleName(); - if ( isset( $data['warnings'][$moduleName] ) ) { - // Don't add duplicate warnings - $oldWarning = $data['warnings'][$moduleName]['*']; - $warnPos = strpos( $oldWarning, $warning ); - // If $warning was found in $oldWarning, check if it starts at 0 or after "\n" - if ( $warnPos !== false && ( $warnPos === 0 || $oldWarning[$warnPos - 1] === "\n" ) ) { - // Check if $warning is followed by "\n" or the end of the $oldWarning - $warnPos += strlen( $warning ); - if ( strlen( $oldWarning ) <= $warnPos || $oldWarning[$warnPos] === "\n" ) { - return; - } - } - // If there is a warning already, append it to the existing one - $warning = "$oldWarning\n$warning"; - } - $msg = array(); - ApiResult::setContent( $msg, $warning ); - $result->disableSizeCheck(); - $result->addValue( 'warnings', $moduleName, - $msg, ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP ); - $result->enableSizeCheck(); + public function needsToken() { + return false; } /** - * 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 + * Fetch the salt used in the Web UI corresponding to this module. + * + * Only override this if the Web UI uses a token with a non-constant salt. + * + * @since 1.24 + * @param array $params All supplied parameters for the module + * @return string|array|null */ - public function getCustomPrinter() { + protected function getWebUITokenSalt( array $params ) { return null; } - /** - * Generates help message for this module, or false if there is no description - * @return mixed string or false - */ - public function makeHelpMsg() { - static $lnPrfx = "\n "; - - $msg = $this->getFinalDescription(); - - if ( $msg !== false ) { - - if ( !is_array( $msg ) ) { - $msg = array( - $msg - ); - } - $msg = $lnPrfx . implode( $lnPrfx, $msg ) . "\n"; + /**@}*/ - $msg .= $this->makeHelpArrayToString( $lnPrfx, false, $this->getHelpUrls() ); - - 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"; - } - if ( $this->isReadMode() || $this->isWriteMode() || - $this->mustBePosted() ) { - $msg .= "\n"; - } - - // Parameters - $paramsMsg = $this->makeHelpMsgParameters(); - if ( $paramsMsg !== false ) { - $msg .= "Parameters:\n$paramsMsg"; - } - - $examples = $this->getExamples(); - if ( $examples ) { - if ( !is_array( $examples ) ) { - $examples = array( - $examples - ); - } - $msg .= "Example" . ( count( $examples ) > 1 ? 's' : '' ) . ":\n"; - foreach ( $examples as $k => $v ) { - if ( is_numeric( $k ) ) { - $msg .= " $v\n"; - } else { - if ( is_array( $v ) ) { - $msgExample = implode( "\n", array_map( array( $this, 'indentExampleText' ), $v ) ); - } else { - $msgExample = " $v"; - } - $msgExample .= ":"; - $msg .= wordwrap( $msgExample, 100, "\n" ) . "\n $k\n"; - } - } - } - } - - return $msg; - } + /************************************************************************//** + * @name Data access methods + * @{ + */ /** - * @param $item string + * Get the name of the module being executed by this instance * @return string */ - private function indentExampleText( $item ) { - return " " . $item; + public function getModuleName() { + return $this->mModuleName; } /** - * @param string $prefix Text to split output items - * @param string $title What is being output - * @param $input string|array + * Get parameter prefix (usually two letters or an empty string). * @return string */ - protected function makeHelpArrayToString( $prefix, $title, $input ) { - if ( $input === false ) { - return ''; - } - if ( !is_array( $input ) ) { - $input = array( $input ); - } - - if ( count( $input ) > 0 ) { - if ( $title ) { - $msg = $title . ( count( $input ) > 1 ? 's' : '' ) . ":\n "; - } else { - $msg = ' '; - } - $msg .= implode( $prefix, $input ) . "\n"; - return $msg; - } - return ''; + public function getModulePrefix() { + return $this->mModulePrefix; } /** - * Generates the parameter descriptions for this module, to be displayed in the - * module's help. - * @return string or false + * Get the main module + * @return ApiMain */ - public function makeHelpMsgParameters() { - $params = $this->getFinalParams( ApiBase::GET_VALUES_FOR_HELP ); - if ( $params ) { - - $paramsDescription = $this->getFinalParamDescription(); - $msg = ''; - $paramPrefix = "\n" . str_repeat( ' ', 24 ); - $descWordwrap = "\n" . str_repeat( ' ', 28 ); - foreach ( $params as $paramName => $paramSettings ) { - $desc = isset( $paramsDescription[$paramName] ) ? $paramsDescription[$paramName] : ''; - if ( is_array( $desc ) ) { - $desc = implode( $paramPrefix, $desc ); - } - - //handle shorthand - if ( !is_array( $paramSettings ) ) { - $paramSettings = array( - self::PARAM_DFLT => $paramSettings, - ); - } - - //handle missing type - if ( !isset( $paramSettings[ApiBase::PARAM_TYPE] ) ) { - $dflt = isset( $paramSettings[ApiBase::PARAM_DFLT] ) ? $paramSettings[ApiBase::PARAM_DFLT] : null; - if ( is_bool( $dflt ) ) { - $paramSettings[ApiBase::PARAM_TYPE] = 'boolean'; - } elseif ( is_string( $dflt ) || is_null( $dflt ) ) { - $paramSettings[ApiBase::PARAM_TYPE] = 'string'; - } elseif ( is_int( $dflt ) ) { - $paramSettings[ApiBase::PARAM_TYPE] = 'integer'; - } - } - - if ( isset( $paramSettings[self::PARAM_DEPRECATED] ) && $paramSettings[self::PARAM_DEPRECATED] ) { - $desc = "DEPRECATED! $desc"; - } - - if ( isset( $paramSettings[self::PARAM_REQUIRED] ) && $paramSettings[self::PARAM_REQUIRED] ) { - $desc .= $paramPrefix . "This parameter is required"; - } - - $type = isset( $paramSettings[self::PARAM_TYPE] ) ? $paramSettings[self::PARAM_TYPE] : null; - if ( isset( $type ) ) { - $hintPipeSeparated = true; - $multi = isset( $paramSettings[self::PARAM_ISMULTI] ) ? $paramSettings[self::PARAM_ISMULTI] : false; - if ( $multi ) { - $prompt = 'Values (separate with \'|\'): '; - } else { - $prompt = 'One value: '; - } - - if ( is_array( $type ) ) { - $choices = array(); - $nothingPrompt = ''; - foreach ( $type as $t ) { - if ( $t === '' ) { - $nothingPrompt = 'Can be empty, or '; - } else { - $choices[] = $t; - } - } - $desc .= $paramPrefix . $nothingPrompt . $prompt; - $choicesstring = implode( ', ', $choices ); - $desc .= wordwrap( $choicesstring, 100, $descWordwrap ); - $hintPipeSeparated = false; - } else { - switch ( $type ) { - case 'namespace': - // Special handling because namespaces are type-limited, yet they are not given - $desc .= $paramPrefix . $prompt; - $desc .= wordwrap( implode( ', ', MWNamespace::getValidNamespaces() ), - 100, $descWordwrap ); - $hintPipeSeparated = false; - break; - case 'limit': - $desc .= $paramPrefix . "No more than {$paramSettings[self::PARAM_MAX]}"; - if ( isset( $paramSettings[self::PARAM_MAX2] ) ) { - $desc .= " ({$paramSettings[self::PARAM_MAX2]} for bots)"; - } - $desc .= ' allowed'; - break; - case 'integer': - $s = $multi ? 's' : ''; - $hasMin = isset( $paramSettings[self::PARAM_MIN] ); - $hasMax = isset( $paramSettings[self::PARAM_MAX] ); - if ( $hasMin || $hasMax ) { - if ( !$hasMax ) { - $intRangeStr = "The value$s must be no less than {$paramSettings[self::PARAM_MIN]}"; - } elseif ( !$hasMin ) { - $intRangeStr = "The value$s must be no more than {$paramSettings[self::PARAM_MAX]}"; - } else { - $intRangeStr = "The value$s must be between {$paramSettings[self::PARAM_MIN]} and {$paramSettings[self::PARAM_MAX]}"; - } - - $desc .= $paramPrefix . $intRangeStr; - } - break; - case 'upload': - $desc .= $paramPrefix . "Must be posted as a file upload using multipart/form-data"; - break; - } - } - - if ( $multi ) { - if ( $hintPipeSeparated ) { - $desc .= $paramPrefix . "Separate values with '|'"; - } - - $isArray = is_array( $type ); - if ( !$isArray - || $isArray && count( $type ) > self::LIMIT_SML1 ) { - $desc .= $paramPrefix . "Maximum number of values " . - self::LIMIT_SML1 . " (" . self::LIMIT_SML2 . " for bots)"; - } - } - } - - $default = isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null; - if ( !is_null( $default ) && $default !== false ) { - $desc .= $paramPrefix . "Default: $default"; - } - - $msg .= sprintf( " %-19s - %s\n", $this->encodeParamName( $paramName ), $desc ); - } - return $msg; + public function getMain() { + return $this->mMainModule; + } - } else { - return false; - } + /** + * Returns true if this module is the main module ($this === $this->mMainModule), + * false otherwise. + * @return bool + */ + public function isMain() { + return $this === $this->mMainModule; } /** - * Returns the description string for this module - * @return mixed string or array of strings + * Get the result object + * @return ApiResult */ - protected function getDescription() { - return false; + public function getResult() { + // Main module has getResult() method overridden + // Safety - avoid infinite loop: + if ( $this->isMain() ) { + ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' ); + } + + return $this->getMain()->getResult(); } /** - * Returns usage examples for this module. Return false if no examples are available. - * @return bool|string|array + * Get the result data array (read-only) + * @return array */ - protected function getExamples() { - return false; + public function getResultData() { + return $this->getResult()->getData(); } /** - * 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. - * - * Some derived classes may choose to handle an integer $flags parameter - * in the overriding methods. Callers of this method can pass zero or - * more OR-ed flags like GET_VALUES_FOR_HELP. - * - * @return array|bool + * Gets a default slave database connection object + * @return DatabaseBase */ - protected function getAllowedParams( /* $flags = 0 */ ) { - // int $flags is not declared because it causes "Strict standards" - // warning. Most derived classes do not implement it. - return false; + protected function getDB() { + if ( !isset( $this->mSlaveDB ) ) { + $this->profileDBIn(); + $this->mSlaveDB = wfGetDB( DB_SLAVE, 'api' ); + $this->profileDBOut(); + } + + return $this->mSlaveDB; } /** - * Returns an array of parameter descriptions. - * Don't call this function directly: use getFinalParamDescription() to - * allow hooks to modify descriptions as needed. - * @return array|bool False on no parameter descriptions + * Get final module description, after hooks have had a chance to tweak it as + * needed. + * + * @return array|bool False on no parameters */ - protected function getParamDescription() { - return false; + public function getFinalDescription() { + $desc = $this->getDescription(); + wfRunHooks( 'APIGetDescription', array( &$this, &$desc ) ); + + return $desc; } /** @@ -549,12 +357,21 @@ abstract class ApiBase extends ContextSource { * tweak it as needed. * * @param int $flags Zero or more flags like GET_VALUES_FOR_HELP - * @return array|Bool False on no parameters + * @return array|bool False on no parameters * @since 1.21 $flags param added */ public function getFinalParams( $flags = 0 ) { $params = $this->getAllowedParams( $flags ); + + if ( $this->needsToken() ) { + $params['token'] = array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true, + ); + } + wfRunHooks( 'APIGetAllowedParams', array( &$this, &$params, $flags ) ); + return $params; } @@ -566,67 +383,33 @@ abstract class ApiBase extends ContextSource { */ public function getFinalParamDescription() { $desc = $this->getParamDescription(); - wfRunHooks( 'APIGetParamDescription', array( &$this, &$desc ) ); - return $desc; - } - /** - * Returns possible properties in the result, grouped by the value of the prop parameter - * that shows them. - * - * Properties that are shown always are in a group with empty string as a key. - * Properties that can be shown by several values of prop are included multiple times. - * If some properties are part of a list and some are on the root object (see ApiQueryQueryPage), - * those on the root object are under the key PROP_ROOT. - * The array can also contain a boolean under the key PROP_LIST, - * indicating whether the result is a list. - * - * Don't call this function directly: use getFinalResultProperties() to - * allow hooks to modify descriptions as needed. - * - * @return array|bool False on no properties - */ - protected function getResultProperties() { - return false; - } - - /** - * Get final possible result properties, after hooks have had a chance to tweak it as - * needed. - * - * @return array - */ - public function getFinalResultProperties() { - $properties = $this->getResultProperties(); - wfRunHooks( 'APIGetResultProperties', array( $this, &$properties ) ); - return $properties; - } - - /** - * Add token properties to the array used by getResultProperties, - * based on a token functions mapping. - */ - protected static function addTokenProperties( &$props, $tokenFunctions ) { - foreach ( array_keys( $tokenFunctions ) as $token ) { - $props[''][$token . 'token'] = array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true + $tokenType = $this->needsToken(); + if ( $tokenType ) { + if ( !isset( $desc['token'] ) ) { + $desc['token'] = array(); + } elseif ( !is_array( $desc['token'] ) ) { + // We ignore a plain-string token, because it's probably an + // extension that is supplying the string for BC. + $desc['token'] = array(); + } + array_unshift( $desc['token'], + "A '$tokenType' token retrieved from action=query&meta=tokens" ); } - } - /** - * Get final module description, after hooks have had a chance to tweak it as - * needed. - * - * @return array|bool False on no parameters - */ - public function getFinalDescription() { - $desc = $this->getDescription(); - wfRunHooks( 'APIGetDescription', array( &$this, &$desc ) ); + wfRunHooks( 'APIGetParamDescription', array( &$this, &$desc ) ); + return $desc; } + /**@}*/ + + /************************************************************************//** + * @name Parameter handling + * @{ + */ + /** * This method mangles parameter name based on the prefix supplied to the constructor. * Override this method to change parameter name during runtime @@ -643,7 +426,7 @@ abstract class ApiBase extends ContextSource { * value - validated value from user or default. limits will not be * parsed if $parseLimit is set to false; use this when the max * limit is not definitive yet, e.g. when getting revisions. - * @param $parseLimit Boolean: true by default + * @param bool $parseLimit True by default * @return array */ public function extractRequestParams( $parseLimit = true ) { @@ -660,26 +443,30 @@ abstract class ApiBase extends ContextSource { } $this->mParamCache[$parseLimit] = $results; } + return $this->mParamCache[$parseLimit]; } /** * Get a value for the given parameter * @param string $paramName Parameter name - * @param bool $parseLimit see extractRequestParams() + * @param bool $parseLimit See extractRequestParams() * @return mixed Parameter value */ protected function getParameter( $paramName, $parseLimit = true ) { $params = $this->getFinalParams(); $paramSettings = $params[$paramName]; + return $this->getParameterFromSettings( $paramName, $paramSettings, $parseLimit ); } /** * Die if none or more than one of a certain set of parameters is set and not false. - * @param array $params of parameter names + * + * @param array $params User provided set of parameters, as from $this->extractRequestParams() + * @param string $required,... Names of parameters of which exactly one must be set */ - public function requireOnlyOneParameter( $params ) { + public function requireOnlyOneParameter( $params, $required /*...*/ ) { $required = func_get_args(); array_shift( $required ); $p = $this->getModulePrefix(); @@ -688,34 +475,24 @@ abstract class ApiBase extends ContextSource { array( $this, "parameterNotEmpty" ) ) ), $required ); if ( count( $intersection ) > 1 ) { - $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', 'invalidparammix' ); + $this->dieUsage( + "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', + 'invalidparammix' ); } elseif ( count( $intersection ) == 0 ) { - $this->dieUsage( "One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required', 'missingparam' ); + $this->dieUsage( + "One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required', + 'missingparam' + ); } } /** - * Generates the possible errors requireOnlyOneParameter() can die with - * - * @param $params array - * @return array - */ - public function getRequireOnlyOneParameterErrorMessages( $params ) { - $p = $this->getModulePrefix(); - $params = implode( ", {$p}", $params ); - - return array( - array( 'code' => "{$p}missingparam", 'info' => "One of the parameters {$p}{$params} is required" ), - array( 'code' => "{$p}invalidparammix", 'info' => "The parameters {$p}{$params} can not be used together" ) - ); - } - - /** * Die if more than one of a certain set of parameters is set and not false. * - * @param $params array + * @param array $params User provided set of parameters, as from $this->extractRequestParams() + * @param string $required,... Names of parameters of which at most one must be set */ - public function requireMaxOneParameter( $params ) { + public function requireMaxOneParameter( $params, $required /*...*/ ) { $required = func_get_args(); array_shift( $required ); $p = $this->getModulePrefix(); @@ -724,27 +501,51 @@ abstract class ApiBase extends ContextSource { array( $this, "parameterNotEmpty" ) ) ), $required ); if ( count( $intersection ) > 1 ) { - $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', 'invalidparammix' ); + $this->dieUsage( + "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', + 'invalidparammix' + ); } } /** - * Generates the possible error requireMaxOneParameter() can die with + * Die if none of a certain set of parameters is set and not false. * - * @param $params array - * @return array + * @since 1.23 + * @param array $params User provided set of parameters, as from $this->extractRequestParams() + * @param string $required,... Names of parameters of which at least one must be set */ - public function getRequireMaxOneParameterErrorMessages( $params ) { + public function requireAtLeastOneParameter( $params, $required /*...*/ ) { + $required = func_get_args(); + array_shift( $required ); $p = $this->getModulePrefix(); - $params = implode( ", {$p}", $params ); - return array( - array( 'code' => "{$p}invalidparammix", 'info' => "The parameters {$p}{$params} can not be used together" ) + $intersection = array_intersect( + array_keys( array_filter( $params, array( $this, "parameterNotEmpty" ) ) ), + $required ); + + if ( count( $intersection ) == 0 ) { + $this->dieUsage( "At least one of the parameters {$p}" . + implode( ", {$p}", $required ) . ' is required', "{$p}missingparam" ); + } + } + + /** + * Callback function used in requireOnlyOneParameter to check whether required parameters are set + * + * @param object $x Parameter to check is not null/false + * @return bool + */ + private function parameterNotEmpty( $x ) { + return !is_null( $x ) && $x !== false; } /** - * @param $params array + * Get a WikiPage object from a title or pageid param, if possible. + * Can die, if no param is set or if the title or page id is not valid. + * + * @param array $params * @param bool|string $load Whether load the object's state from the database: * - false: don't load (if the pageid is given, it will still be loaded) * - 'fromdb': load from a slave database @@ -781,44 +582,11 @@ abstract class ApiBase extends ContextSource { } /** - * @return array - */ - public function getTitleOrPageIdErrorMessage() { - return array_merge( - $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ), - array( - array( 'invalidtitle', 'title' ), - array( 'nosuchpageid', 'pageid' ), - ) - ); - } - - /** - * Callback function used in requireOnlyOneParameter to check whether required parameters are set - * - * @param $x object Parameter to check is not null/false - * @return bool - */ - private function parameterNotEmpty( $x ) { - return !is_null( $x ) && $x !== false; - } - - /** - * @deprecated since 1.17 use MWNamespace::getValidNamespaces() - * - * @return array - */ - public static function getValidNamespaces() { - wfDeprecated( __METHOD__, '1.17' ); - return MWNamespace::getValidNamespaces(); - } - - /** * Return true if we're to watch the page, false if not, null if no change. * @param string $watchlist Valid values: 'watch', 'unwatch', 'preferences', 'nochange' - * @param $titleObj Title the page under consideration + * @param Title $titleObj The page under consideration * @param string $userOption The user option to consider when $watchlist=preferences. - * If not set will magically default to either watchdefault or watchcreations + * If not set will use watchdefault always and watchcreations if $titleObj doesn't exist. * @return bool */ protected function getWatchlistValue( $watchlist, $titleObj, $userOption = null ) { @@ -837,11 +605,12 @@ abstract class ApiBase extends ContextSource { if ( $userWatching ) { return true; } - # If no user option was passed, use watchdefault or watchcreations + # If no user option was passed, use watchdefault and watchcreations if ( is_null( $userOption ) ) { - $userOption = $titleObj->exists() - ? 'watchdefault' : 'watchcreations'; + return $this->getUser()->getBoolOption( 'watchdefault' ) || + $this->getUser()->getBoolOption( 'watchcreations' ) && !$titleObj->exists(); } + # Watch the article based on the user preference return $this->getUser()->getBoolOption( $userOption ); @@ -854,27 +623,12 @@ abstract class ApiBase extends ContextSource { } /** - * Set a watch (or unwatch) based the based on a watchlist parameter. - * @param string $watch Valid values: 'watch', 'unwatch', 'preferences', 'nochange' - * @param $titleObj Title the article's title to change - * @param string $userOption The user option to consider when $watch=preferences - */ - protected function setWatch( $watch, $titleObj, $userOption = null ) { - $value = $this->getWatchlistValue( $watch, $titleObj, $userOption ); - if ( $value === null ) { - return; - } - - WatchAction::doWatchOrUnwatch( $value, $titleObj, $this->getUser() ); - } - - /** * Using the settings determine the value for the given parameter * - * @param string $paramName parameter name - * @param array|mixed $paramSettings default value or an array of settings + * @param string $paramName Parameter name + * @param array|mixed $paramSettings Default value or an array of settings * using PARAM_* constants. - * @param $parseLimit Boolean: parse limit? + * @param bool $parseLimit Parse limit? * @return mixed Parameter value */ protected function getParameterFromSettings( $paramName, $paramSettings, $parseLimit ) { @@ -889,12 +643,24 @@ abstract class ApiBase extends ContextSource { $deprecated = false; $required = false; } else { - $default = isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null; - $multi = isset( $paramSettings[self::PARAM_ISMULTI] ) ? $paramSettings[self::PARAM_ISMULTI] : false; - $type = isset( $paramSettings[self::PARAM_TYPE] ) ? $paramSettings[self::PARAM_TYPE] : null; - $dupes = isset( $paramSettings[self::PARAM_ALLOW_DUPLICATES] ) ? $paramSettings[self::PARAM_ALLOW_DUPLICATES] : false; - $deprecated = isset( $paramSettings[self::PARAM_DEPRECATED] ) ? $paramSettings[self::PARAM_DEPRECATED] : false; - $required = isset( $paramSettings[self::PARAM_REQUIRED] ) ? $paramSettings[self::PARAM_REQUIRED] : false; + $default = isset( $paramSettings[self::PARAM_DFLT] ) + ? $paramSettings[self::PARAM_DFLT] + : null; + $multi = isset( $paramSettings[self::PARAM_ISMULTI] ) + ? $paramSettings[self::PARAM_ISMULTI] + : false; + $type = isset( $paramSettings[self::PARAM_TYPE] ) + ? $paramSettings[self::PARAM_TYPE] + : null; + $dupes = isset( $paramSettings[self::PARAM_ALLOW_DUPLICATES] ) + ? $paramSettings[self::PARAM_ALLOW_DUPLICATES] + : false; + $deprecated = isset( $paramSettings[self::PARAM_DEPRECATED] ) + ? $paramSettings[self::PARAM_DEPRECATED] + : false; + $required = isset( $paramSettings[self::PARAM_REQUIRED] ) + ? $paramSettings[self::PARAM_REQUIRED] + : false; // When type is not given, and no choices, the type is the same as $default if ( !isset( $type ) ) { @@ -909,14 +675,21 @@ abstract class ApiBase extends ContextSource { if ( $type == 'boolean' ) { if ( isset( $default ) && $default !== false ) { // Having a default value of anything other than 'false' is not allowed - ApiBase::dieDebug( __METHOD__, "Boolean param $encParamName's default is set to '$default'. Boolean parameters must default to false." ); + ApiBase::dieDebug( + __METHOD__, + "Boolean param $encParamName's default is set to '$default'. " . + "Boolean parameters must default to false." + ); } $value = $this->getMain()->getCheck( $encParamName ); } elseif ( $type == 'upload' ) { if ( isset( $default ) ) { // Having a default value is not allowed - ApiBase::dieDebug( __METHOD__, "File upload param $encParamName's default is set to '$default'. File upload parameters may not have a default." ); + ApiBase::dieDebug( + __METHOD__, + "File upload param $encParamName's default is set to " . + "'$default'. File upload parameters may not have a default." ); } if ( $multi ) { ApiBase::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" ); @@ -930,8 +703,8 @@ abstract class ApiBase extends ContextSource { if ( $value !== null ) { $this->dieUsage( "File upload param $encParamName is not a file upload; " . - "be sure to use multipart/form-data for your POST and include " . - "a filename in the Content-Disposition header.", + "be sure to use multipart/form-data for your POST and include " . + "a filename in the Content-Disposition header.", "badupload_{$encParamName}" ); } @@ -942,10 +715,18 @@ abstract class ApiBase extends ContextSource { if ( isset( $value ) && $type == 'namespace' ) { $type = MWNamespace::getValidNamespaces(); } + if ( isset( $value ) && $type == 'submodule' ) { + $type = $this->getModuleManager()->getNames( $paramName ); + } } if ( isset( $value ) && ( $multi || is_array( $type ) ) ) { - $value = $this->parseMultiValue( $encParamName, $value, $multi, is_array( $type ) ? $type : null ); + $value = $this->parseMultiValue( + $encParamName, + $value, + $multi, + is_array( $type ) ? $type : null + ); } // More validation only when choices were not given @@ -964,7 +745,7 @@ abstract class ApiBase extends ContextSource { $min = isset( $paramSettings[self::PARAM_MIN] ) ? $paramSettings[self::PARAM_MIN] : null; $max = isset( $paramSettings[self::PARAM_MAX] ) ? $paramSettings[self::PARAM_MAX] : null; $enforceLimits = isset( $paramSettings[self::PARAM_RANGE_ENFORCE] ) - ? $paramSettings[self::PARAM_RANGE_ENFORCE] : false; + ? $paramSettings[self::PARAM_RANGE_ENFORCE] : false; if ( is_array( $value ) ) { $value = array_map( 'intval', $value ); @@ -985,19 +766,32 @@ abstract class ApiBase extends ContextSource { // Don't do any validation whatsoever break; } - if ( !isset( $paramSettings[self::PARAM_MAX] ) || !isset( $paramSettings[self::PARAM_MAX2] ) ) { - ApiBase::dieDebug( __METHOD__, "MAX1 or MAX2 are not defined for the limit $encParamName" ); + if ( !isset( $paramSettings[self::PARAM_MAX] ) + || !isset( $paramSettings[self::PARAM_MAX2] ) + ) { + ApiBase::dieDebug( + __METHOD__, + "MAX1 or MAX2 are not defined for the limit $encParamName" + ); } if ( $multi ) { ApiBase::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" ); } $min = isset( $paramSettings[self::PARAM_MIN] ) ? $paramSettings[self::PARAM_MIN] : 0; if ( $value == 'max' ) { - $value = $this->getMain()->canApiHighLimits() ? $paramSettings[self::PARAM_MAX2] : $paramSettings[self::PARAM_MAX]; + $value = $this->getMain()->canApiHighLimits() + ? $paramSettings[self::PARAM_MAX2] + : $paramSettings[self::PARAM_MAX]; $this->getResult()->setParsedLimit( $this->getModuleName(), $value ); } else { $value = intval( $value ); - $this->validateLimit( $paramName, $value, $min, $paramSettings[self::PARAM_MAX], $paramSettings[self::PARAM_MAX2] ); + $this->validateLimit( + $paramName, + $value, + $min, + $paramSettings[self::PARAM_MAX], + $paramSettings[self::PARAM_MAX2] + ); } break; case 'boolean': @@ -1052,25 +846,28 @@ abstract class ApiBase extends ContextSource { * * @param string $valueName The name of the parameter (for error * reporting) - * @param $value mixed The value being parsed + * @param mixed $value The value being parsed * @param bool $allowMultiple Can $value contain more than one value * separated by '|'? - * @param $allowedValues mixed An array of values to check against. If + * @param string[]|null $allowedValues An array of values to check against. If * null, all values are accepted. - * @return mixed (allowMultiple ? an_array_of_values : a_single_value) + * @return string|string[] (allowMultiple ? an_array_of_values : a_single_value) */ protected function parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues ) { if ( trim( $value ) === '' && $allowMultiple ) { return array(); } - // This is a bit awkward, but we want to avoid calling canApiHighLimits() because it unstubs $wgUser + // This is a bit awkward, but we want to avoid calling canApiHighLimits() + // because it unstubs $wgUser $valuesList = explode( '|', $value, self::LIMIT_SML2 + 1 ); - $sizeLimit = count( $valuesList ) > self::LIMIT_SML1 && $this->mMainModule->canApiHighLimits() ? - self::LIMIT_SML2 : self::LIMIT_SML1; + $sizeLimit = count( $valuesList ) > self::LIMIT_SML1 && $this->mMainModule->canApiHighLimits() + ? self::LIMIT_SML2 + : self::LIMIT_SML1; if ( self::truncateArray( $valuesList, $sizeLimit ) ) { - $this->setWarning( "Too many values supplied for parameter '$valueName': the limit is $sizeLimit" ); + $this->setWarning( "Too many values supplied for parameter '$valueName': " . + "the limit is $sizeLimit" ); } if ( !$allowMultiple && count( $valuesList ) != 1 ) { @@ -1079,8 +876,13 @@ abstract class ApiBase extends ContextSource { return $value; } - $possibleValues = is_array( $allowedValues ) ? "of '" . implode( "', '", $allowedValues ) . "'" : ''; - $this->dieUsage( "Only one $possibleValues is allowed for parameter '$valueName'", "multival_$valueName" ); + $possibleValues = is_array( $allowedValues ) + ? "of '" . implode( "', '", $allowedValues ) . "'" + : ''; + $this->dieUsage( + "Only one $possibleValues is allowed for parameter '$valueName'", + "multival_$valueName" + ); } if ( is_array( $allowedValues ) ) { @@ -1092,7 +894,10 @@ abstract class ApiBase extends ContextSource { $vals = implode( ", ", $unknown ); $this->setWarning( "Unrecognized value$s for parameter '$valueName': $vals" ); } else { - $this->dieUsage( "Unrecognized value for parameter '$valueName': {$valuesList[0]}", "unknown_$valueName" ); + $this->dieUsage( + "Unrecognized value for parameter '$valueName': {$valuesList[0]}", + "unknown_$valueName" + ); } } // Now throw them out @@ -1110,9 +915,9 @@ abstract class ApiBase extends ContextSource { * @param int|null $min Minimum value * @param int|null $max Maximum value for users * @param int $botMax Maximum value for sysops/bots - * @param $enforceLimits Boolean Whether to enforce (die) if value is outside limits + * @param bool $enforceLimits Whether to enforce (die) if value is outside limits */ - function validateLimit( $paramName, &$value, $min, $max, $botMax = null, $enforceLimits = false ) { + protected function validateLimit( $paramName, &$value, $min, $max, $botMax = null, $enforceLimits = false ) { if ( !is_null( $min ) && $value < $min ) { $msg = $this->encodeParamName( $paramName ) . " may not be less than $min (set to $value)"; @@ -1120,7 +925,8 @@ abstract class ApiBase extends ContextSource { $value = $min; } - // Minimum is always validated, whereas maximum is checked only if not running in internal call mode + // Minimum is always validated, whereas maximum is checked only if not + // running in internal call mode if ( $this->getMain()->isInternalMode() ) { return; } @@ -1130,7 +936,8 @@ abstract class ApiBase extends ContextSource { if ( !is_null( $max ) && $value > $max ) { if ( !is_null( $botMax ) && $this->getMain()->canApiHighLimits() ) { if ( $value > $botMax ) { - $msg = $this->encodeParamName( $paramName ) . " may not be over $botMax (set to $value) for bots or sysops"; + $msg = $this->encodeParamName( $paramName ) . + " may not be over $botMax (set to $value) for bots or sysops"; $this->warnOrDie( $msg, $enforceLimits ); $value = $botMax; } @@ -1148,15 +955,57 @@ abstract class ApiBase extends ContextSource { * @param string $encParamName Parameter name * @return string Validated and normalized parameter */ - function validateTimestamp( $value, $encParamName ) { + protected function validateTimestamp( $value, $encParamName ) { $unixTimestamp = wfTimestamp( TS_UNIX, $value ); if ( $unixTimestamp === false ) { - $this->dieUsage( "Invalid value '$value' for timestamp parameter $encParamName", "badtimestamp_{$encParamName}" ); + $this->dieUsage( + "Invalid value '$value' for timestamp parameter $encParamName", + "badtimestamp_{$encParamName}" + ); } + return wfTimestamp( TS_MW, $unixTimestamp ); } /** + * Validate the supplied token. + * + * @since 1.24 + * @param string $token Supplied token + * @param array $params All supplied parameters for the module + * @return bool + */ + public final function validateToken( $token, array $params ) { + $tokenType = $this->needsToken(); + $salts = ApiQueryTokens::getTokenTypeSalts(); + if ( !isset( $salts[$tokenType] ) ) { + throw new MWException( + "Module '{$this->getModuleName()}' tried to use token type '$tokenType' " . + 'without registering it' + ); + } + + if ( $this->getUser()->matchEditToken( + $token, + $salts[$tokenType], + $this->getRequest() + ) ) { + return true; + } + + $webUiSalt = $this->getWebUITokenSalt( $params ); + if ( $webUiSalt !== null && $this->getUser()->matchEditToken( + $token, + $webUiSalt, + $this->getRequest() + ) ) { + return true; + } + + return false; + } + + /** * Validate and normalize of parameters of type 'user' * @param string $value Parameter value * @param string $encParamName Parameter name @@ -1165,23 +1014,35 @@ abstract class ApiBase extends ContextSource { private function validateUser( $value, $encParamName ) { $title = Title::makeTitleSafe( NS_USER, $value ); if ( $title === null ) { - $this->dieUsage( "Invalid value '$value' for user parameter $encParamName", "baduser_{$encParamName}" ); + $this->dieUsage( + "Invalid value '$value' for user parameter $encParamName", + "baduser_{$encParamName}" + ); } + return $title->getText(); } + /**@}*/ + + /************************************************************************//** + * @name Utility methods + * @{ + */ + /** - * Adds a warning to the output, else dies - * - * @param $msg String Message to show as a warning, or error message if dying - * @param $enforceLimits Boolean Whether this is an enforce (die) + * Set a watch (or unwatch) based the based on a watchlist parameter. + * @param string $watch Valid values: 'watch', 'unwatch', 'preferences', 'nochange' + * @param Title $titleObj The article's title to change + * @param string $userOption The user option to consider when $watch=preferences */ - private function warnOrDie( $msg, $enforceLimits = false ) { - if ( $enforceLimits ) { - $this->dieUsage( $msg, 'integeroutofrange' ); - } else { - $this->setWarning( $msg ); + protected function setWatch( $watch, $titleObj, $userOption = null ) { + $value = $this->getWatchlistValue( $watch, $titleObj, $userOption ); + if ( $value === null ) { + return; } + + WatchAction::doWatchOrUnwatch( $value, $titleObj, $this->getUser() ); } /** @@ -1196,10 +1057,96 @@ abstract class ApiBase extends ContextSource { array_pop( $arr ); $modified = true; } + return $modified; } /** + * Gets the user for whom to get the watchlist + * + * @param array $params + * @return User + */ + public function getWatchlistUser( $params ) { + if ( !is_null( $params['owner'] ) && !is_null( $params['token'] ) ) { + $user = User::newFromName( $params['owner'], false ); + if ( !( $user && $user->getId() ) ) { + $this->dieUsage( 'Specified user does not exist', 'bad_wlowner' ); + } + $token = $user->getOption( 'watchlisttoken' ); + if ( $token == '' || $token != $params['token'] ) { + $this->dieUsage( + 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences', + 'bad_wltoken' + ); + } + } else { + if ( !$this->getUser()->isLoggedIn() ) { + $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' ); + } + if ( !$this->getUser()->isAllowed( 'viewmywatchlist' ) ) { + $this->dieUsage( 'You don\'t have permission to view your watchlist', 'permissiondenied' ); + } + $user = $this->getUser(); + } + + return $user; + } + + /**@}*/ + + /************************************************************************//** + * @name Warning and error reporting + * @{ + */ + + /** + * 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 string $warning Warning message + */ + public function setWarning( $warning ) { + $result = $this->getResult(); + $data = $result->getData(); + $moduleName = $this->getModuleName(); + if ( isset( $data['warnings'][$moduleName] ) ) { + // Don't add duplicate warnings + $oldWarning = $data['warnings'][$moduleName]['*']; + $warnPos = strpos( $oldWarning, $warning ); + // If $warning was found in $oldWarning, check if it starts at 0 or after "\n" + if ( $warnPos !== false && ( $warnPos === 0 || $oldWarning[$warnPos - 1] === "\n" ) ) { + // Check if $warning is followed by "\n" or the end of the $oldWarning + $warnPos += strlen( $warning ); + if ( strlen( $oldWarning ) <= $warnPos || $oldWarning[$warnPos] === "\n" ) { + return; + } + } + // If there is a warning already, append it to the existing one + $warning = "$oldWarning\n$warning"; + } + $msg = array(); + ApiResult::setContent( $msg, $warning ); + $result->addValue( 'warnings', $moduleName, + $msg, ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK ); + } + + /** + * Adds a warning to the output, else dies + * + * @param string $msg Message to show as a warning, or error message if dying + * @param bool $enforceLimits Whether this is an enforce (die) + */ + private function warnOrDie( $msg, $enforceLimits = false ) { + if ( $enforceLimits ) { + $this->dieUsage( $msg, 'integeroutofrange' ); + } + + $this->setWarning( $msg ); + } + + /** * Throw a UsageException, which will (if uncaught) call the main module's * error handler and die with an error message. * @@ -1213,17 +1160,22 @@ abstract class ApiBase extends ContextSource { */ public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) { Profiler::instance()->close(); - throw new UsageException( $description, $this->encodeParamName( $errorCode ), $httpRespCode, $extradata ); + throw new UsageException( + $description, + $this->encodeParamName( $errorCode ), + $httpRespCode, + $extradata + ); } /** - * Throw a UsageException based on the errors in the Status object. + * Get error (as code, string) from a Status object. * - * @since 1.22 - * @param Status $status Status object - * @throws UsageException + * @since 1.23 + * @param Status $status + * @return array Array of code and error string */ - public function dieStatus( $status ) { + public function getErrorFromStatus( $status ) { if ( $status->isGood() ) { throw new MWException( 'Successful status passed to ApiBase::dieStatus' ); } @@ -1248,13 +1200,28 @@ abstract class ApiBase extends ContextSource { $msg = wfMessage( $code, $errors[0] ); } if ( isset( ApiBase::$messageMap[$code] ) ) { - // Translate message to code, for backwards compatability + // Translate message to code, for backwards compatibility $code = ApiBase::$messageMap[$code]['code']; } - $this->dieUsage( $msg->inLanguage( 'en' )->useDatabase( false )->plain(), $code ); + + return array( $code, $msg->inLanguage( 'en' )->useDatabase( false )->plain() ); } /** + * Throw a UsageException based on the errors in the Status object. + * + * @since 1.22 + * @param Status $status + * @throws MWException + */ + public function dieStatus( $status ) { + + list( $code, $msg ) = $this->getErrorFromStatus( $status ); + $this->dieUsage( $msg, $code ); + } + + // @codingStandardsIgnoreStart Allow long lines. Cannot split these. + /** * Array that maps message keys to error messages. $1 and friends are replaced. */ public static $messageMap = array( @@ -1263,74 +1230,243 @@ abstract class ApiBase extends ContextSource { 'unknownerror-nocode' => array( 'code' => 'unknownerror', 'info' => 'Unknown error' ), // Messages from Title::getUserPermissionsErrors() - 'ns-specialprotected' => array( 'code' => 'unsupportednamespace', 'info' => "Pages in the Special namespace can't be edited" ), - 'protectedinterface' => array( 'code' => 'protectednamespace-interface', 'info' => "You're not allowed to edit interface messages" ), - 'namespaceprotected' => array( 'code' => 'protectednamespace', 'info' => "You're not allowed to edit pages in the \"\$1\" namespace" ), - 'customcssprotected' => array( 'code' => 'customcssprotected', 'info' => "You're not allowed to edit custom CSS pages" ), - 'customjsprotected' => array( 'code' => 'customjsprotected', 'info' => "You're not allowed to edit custom JavaScript pages" ), - 'cascadeprotected' => array( 'code' => 'cascadeprotected', 'info' => "The page you're trying to edit is protected because it's included in a cascade-protected page" ), - 'protectedpagetext' => array( 'code' => 'protectedpage', 'info' => "The \"\$1\" right is required to edit this page" ), - 'protect-cantedit' => array( 'code' => 'cantedit', 'info' => "You can't protect this page because you can't edit it" ), - 'badaccess-group0' => array( 'code' => 'permissiondenied', 'info' => "Permission denied" ), // Generic permission denied message - 'badaccess-groups' => array( 'code' => 'permissiondenied', 'info' => "Permission denied" ), - 'titleprotected' => array( 'code' => 'protectedtitle', 'info' => "This title has been protected from creation" ), - 'nocreate-loggedin' => array( 'code' => 'cantcreate', 'info' => "You don't have permission to create new pages" ), - 'nocreatetext' => array( 'code' => 'cantcreate-anon', 'info' => "Anonymous users can't create new pages" ), - 'movenologintext' => array( 'code' => 'cantmove-anon', 'info' => "Anonymous users can't move pages" ), - 'movenotallowed' => array( 'code' => 'cantmove', 'info' => "You don't have permission to move pages" ), - 'confirmedittext' => array( 'code' => 'confirmemail', 'info' => "You must confirm your email address before you can edit" ), - 'blockedtext' => array( 'code' => 'blocked', 'info' => "You have been blocked from editing" ), - 'autoblockedtext' => array( 'code' => 'autoblocked', 'info' => "Your IP address has been blocked automatically, because it was used by a blocked user" ), + 'ns-specialprotected' => array( + 'code' => 'unsupportednamespace', + 'info' => "Pages in the Special namespace can't be edited" + ), + 'protectedinterface' => array( + 'code' => 'protectednamespace-interface', + 'info' => "You're not allowed to edit interface messages" + ), + 'namespaceprotected' => array( + 'code' => 'protectednamespace', + 'info' => "You're not allowed to edit pages in the \"\$1\" namespace" + ), + 'customcssprotected' => array( + 'code' => 'customcssprotected', + 'info' => "You're not allowed to edit custom CSS pages" + ), + 'customjsprotected' => array( + 'code' => 'customjsprotected', + 'info' => "You're not allowed to edit custom JavaScript pages" + ), + 'cascadeprotected' => array( + 'code' => 'cascadeprotected', + 'info' => "The page you're trying to edit is protected because it's included in a cascade-protected page" + ), + 'protectedpagetext' => array( + 'code' => 'protectedpage', + 'info' => "The \"\$1\" right is required to edit this page" + ), + 'protect-cantedit' => array( + 'code' => 'cantedit', + 'info' => "You can't protect this page because you can't edit it" + ), + 'deleteprotected' => array( + 'code' => 'cantedit', + 'info' => "You can't delete this page because it has been protected" + ), + 'badaccess-group0' => array( + 'code' => 'permissiondenied', + 'info' => "Permission denied" + ), // Generic permission denied message + 'badaccess-groups' => array( + 'code' => 'permissiondenied', + 'info' => "Permission denied" + ), + 'titleprotected' => array( + 'code' => 'protectedtitle', + 'info' => "This title has been protected from creation" + ), + 'nocreate-loggedin' => array( + 'code' => 'cantcreate', + 'info' => "You don't have permission to create new pages" + ), + 'nocreatetext' => array( + 'code' => 'cantcreate-anon', + 'info' => "Anonymous users can't create new pages" + ), + 'movenologintext' => array( + 'code' => 'cantmove-anon', + 'info' => "Anonymous users can't move pages" + ), + 'movenotallowed' => array( + 'code' => 'cantmove', + 'info' => "You don't have permission to move pages" + ), + 'confirmedittext' => array( + 'code' => 'confirmemail', + 'info' => "You must confirm your email address before you can edit" + ), + 'blockedtext' => array( + 'code' => 'blocked', + 'info' => "You have been blocked from editing" + ), + 'autoblockedtext' => array( + 'code' => 'autoblocked', + 'info' => "Your IP address has been blocked automatically, because it was used by a blocked user" + ), // Miscellaneous interface messages - 'actionthrottledtext' => array( 'code' => 'ratelimited', 'info' => "You've exceeded your rate limit. Please wait some time and try again" ), - 'alreadyrolled' => array( 'code' => 'alreadyrolled', 'info' => "The page you tried to rollback was already rolled back" ), - 'cantrollback' => array( 'code' => 'onlyauthor', 'info' => "The page you tried to rollback only has one author" ), - 'readonlytext' => array( 'code' => 'readonly', 'info' => "The wiki is currently in read-only mode" ), - 'sessionfailure' => array( 'code' => 'badtoken', 'info' => "Invalid token" ), - 'cannotdelete' => array( 'code' => 'cantdelete', 'info' => "Couldn't delete \"\$1\". Maybe it was deleted already by someone else" ), - 'notanarticle' => array( 'code' => 'missingtitle', 'info' => "The page you requested doesn't exist" ), - 'selfmove' => array( 'code' => 'selfmove', 'info' => "Can't move a page to itself" ), - 'immobile_namespace' => array( 'code' => 'immobilenamespace', 'info' => "You tried to move pages from or to a namespace that is protected from moving" ), - 'articleexists' => array( 'code' => 'articleexists', 'info' => "The destination article already exists and is not a redirect to the source article" ), - 'protectedpage' => array( 'code' => 'protectedpage', 'info' => "You don't have permission to perform this move" ), - 'hookaborted' => array( 'code' => 'hookaborted', 'info' => "The modification you tried to make was aborted by an extension hook" ), - 'cantmove-titleprotected' => array( 'code' => 'protectedtitle', 'info' => "The destination article has been protected from creation" ), - 'imagenocrossnamespace' => array( 'code' => 'nonfilenamespace', 'info' => "Can't move a file to a non-file namespace" ), - 'imagetypemismatch' => array( 'code' => 'filetypemismatch', 'info' => "The new file extension doesn't match its type" ), + 'actionthrottledtext' => array( + 'code' => 'ratelimited', + 'info' => "You've exceeded your rate limit. Please wait some time and try again" + ), + 'alreadyrolled' => array( + 'code' => 'alreadyrolled', + 'info' => "The page you tried to rollback was already rolled back" + ), + 'cantrollback' => array( + 'code' => 'onlyauthor', + 'info' => "The page you tried to rollback only has one author" + ), + 'readonlytext' => array( + 'code' => 'readonly', + 'info' => "The wiki is currently in read-only mode" + ), + 'sessionfailure' => array( + 'code' => 'badtoken', + 'info' => "Invalid token" ), + 'cannotdelete' => array( + 'code' => 'cantdelete', + 'info' => "Couldn't delete \"\$1\". Maybe it was deleted already by someone else" + ), + 'notanarticle' => array( + 'code' => 'missingtitle', + 'info' => "The page you requested doesn't exist" + ), + 'selfmove' => array( 'code' => 'selfmove', 'info' => "Can't move a page to itself" + ), + 'immobile_namespace' => array( + 'code' => 'immobilenamespace', + 'info' => "You tried to move pages from or to a namespace that is protected from moving" + ), + 'articleexists' => array( + 'code' => 'articleexists', + 'info' => "The destination article already exists and is not a redirect to the source article" + ), + 'protectedpage' => array( + 'code' => 'protectedpage', + 'info' => "You don't have permission to perform this move" + ), + 'hookaborted' => array( + 'code' => 'hookaborted', + 'info' => "The modification you tried to make was aborted by an extension hook" + ), + 'cantmove-titleprotected' => array( + 'code' => 'protectedtitle', + 'info' => "The destination article has been protected from creation" + ), + 'imagenocrossnamespace' => array( + 'code' => 'nonfilenamespace', + 'info' => "Can't move a file to a non-file namespace" + ), + 'imagetypemismatch' => array( + 'code' => 'filetypemismatch', + 'info' => "The new file extension doesn't match its type" + ), // 'badarticleerror' => shouldn't happen // 'badtitletext' => shouldn't happen 'ip_range_invalid' => array( 'code' => 'invalidrange', 'info' => "Invalid IP range" ), - 'range_block_disabled' => array( 'code' => 'rangedisabled', 'info' => "Blocking IP ranges has been disabled" ), - 'nosuchusershort' => array( 'code' => 'nosuchuser', 'info' => "The user you specified doesn't exist" ), + 'range_block_disabled' => array( + 'code' => 'rangedisabled', + 'info' => "Blocking IP ranges has been disabled" + ), + 'nosuchusershort' => array( + 'code' => 'nosuchuser', + 'info' => "The user you specified doesn't exist" + ), 'badipaddress' => array( 'code' => 'invalidip', 'info' => "Invalid IP address specified" ), 'ipb_expiry_invalid' => array( 'code' => 'invalidexpiry', 'info' => "Invalid expiry time" ), - 'ipb_already_blocked' => array( 'code' => 'alreadyblocked', 'info' => "The user you tried to block was already blocked" ), - 'ipb_blocked_as_range' => array( 'code' => 'blockedasrange', 'info' => "IP address \"\$1\" was blocked as part of range \"\$2\". You can't unblock the IP individually, but you can unblock the range as a whole." ), - 'ipb_cant_unblock' => array( 'code' => 'cantunblock', 'info' => "The block you specified was not found. It may have been unblocked already" ), - 'mailnologin' => array( 'code' => 'cantsend', 'info' => "You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email" ), - 'ipbblocked' => array( 'code' => 'ipbblocked', 'info' => 'You cannot block or unblock users while you are yourself blocked' ), - 'ipbnounblockself' => array( 'code' => 'ipbnounblockself', 'info' => 'You are not allowed to unblock yourself' ), - 'usermaildisabled' => array( 'code' => 'usermaildisabled', 'info' => "User email has been disabled" ), - 'blockedemailuser' => array( 'code' => 'blockedfrommail', 'info' => "You have been blocked from sending email" ), - 'notarget' => array( 'code' => 'notarget', 'info' => "You have not specified a valid target for this action" ), - 'noemail' => array( 'code' => 'noemail', 'info' => "The user has not specified a valid email address, or has chosen not to receive email 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" ), - 'userrights-no-interwiki' => array( 'code' => 'nointerwikiuserrights', 'info' => "You don't have permission to change user rights on other wikis" ), - 'userrights-nodatabase' => array( 'code' => 'nosuchdatabase', 'info' => "Database \"\$1\" does not exist or is not local" ), + 'ipb_already_blocked' => array( + 'code' => 'alreadyblocked', + 'info' => "The user you tried to block was already blocked" + ), + 'ipb_blocked_as_range' => array( + 'code' => 'blockedasrange', + 'info' => "IP address \"\$1\" was blocked as part of range \"\$2\". You can't unblock the IP individually, but you can unblock the range as a whole." + ), + 'ipb_cant_unblock' => array( + 'code' => 'cantunblock', + 'info' => "The block you specified was not found. It may have been unblocked already" + ), + 'mailnologin' => array( + 'code' => 'cantsend', + 'info' => "You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email" + ), + 'ipbblocked' => array( + 'code' => 'ipbblocked', + 'info' => 'You cannot block or unblock users while you are yourself blocked' + ), + 'ipbnounblockself' => array( + 'code' => 'ipbnounblockself', + 'info' => 'You are not allowed to unblock yourself' + ), + 'usermaildisabled' => array( + 'code' => 'usermaildisabled', + 'info' => "User email has been disabled" + ), + 'blockedemailuser' => array( + 'code' => 'blockedfrommail', + 'info' => "You have been blocked from sending email" + ), + 'notarget' => array( + 'code' => 'notarget', + 'info' => "You have not specified a valid target for this action" + ), + 'noemail' => array( + 'code' => 'noemail', + 'info' => "The user has not specified a valid email address, or has chosen not to receive email 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" + ), + 'userrights-no-interwiki' => array( + 'code' => 'nointerwikiuserrights', + 'info' => "You don't have permission to change user rights on other wikis" + ), + 'userrights-nodatabase' => array( + 'code' => 'nosuchdatabase', + 'info' => "Database \"\$1\" does not exist or is not local" + ), 'nouserspecified' => array( 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ), 'noname' => array( 'code' => 'invaliduser', 'info' => "Invalid username \"\$1\"" ), 'summaryrequired' => array( 'code' => 'summaryrequired', 'info' => 'Summary required' ), - 'import-rootpage-invalid' => array( 'code' => 'import-rootpage-invalid', 'info' => 'Root page is an invalid title' ), - 'import-rootpage-nosubpage' => array( 'code' => 'import-rootpage-nosubpage', 'info' => 'Namespace "$1" of the root page does not allow subpages' ), + 'import-rootpage-invalid' => array( + 'code' => 'import-rootpage-invalid', + 'info' => 'Root page is an invalid title' + ), + 'import-rootpage-nosubpage' => array( + 'code' => 'import-rootpage-nosubpage', + 'info' => 'Namespace "$1" of the root page does not allow subpages' + ), // 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" ), + '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" ), @@ -1339,81 +1475,253 @@ abstract class ApiBase extends ContextSource { '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" ), - 'create-titleexists' => array( 'code' => 'create-titleexists', 'info' => "Existing titles can't be protected with 'create'" ), - 'missingtitle-createonly' => array( 'code' => 'missingtitle-createonly', 'info' => "Missing titles can only be protected with 'create'" ), - 'cantblock' => array( 'code' => 'cantblock', 'info' => "You don't have permission to block users" ), - 'canthide' => array( 'code' => 'canthide', 'info' => "You don't have permission to hide user names from the block log" ), - 'cantblock-email' => array( 'code' => 'cantblock-email', 'info' => "You don't have permission to block users from sending email through the wiki" ), - 'unblock-notarget' => array( 'code' => 'notarget', 'info' => "Either the id or the user parameter must be set" ), - 'unblock-idanduser' => array( 'code' => 'idanduser', 'info' => "The id and user parameters can't be used together" ), - 'cantunblock' => array( 'code' => 'permissiondenied', 'info' => "You don't have permission to unblock users" ), - 'cannotundelete' => array( 'code' => 'cantundelete', 'info' => "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already" ), - 'permdenied-undelete' => array( 'code' => 'permissiondenied', 'info' => "You don't have permission to restore deleted revisions" ), - 'createonly-exists' => array( 'code' => 'articleexists', 'info' => "The article you tried to create has been created already" ), - 'nocreate-missing' => array( 'code' => 'missingtitle', 'info' => "The article you tried to edit doesn't exist" ), - 'cantchangecontentmodel' => array( 'code' => 'cantchangecontentmodel', 'info' => "You don't have permission to change the content model of a page" ), - 'nosuchrcid' => array( 'code' => 'nosuchrcid', 'info' => "There is no change with rcid \"\$1\"" ), - '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" ), + 'create-titleexists' => array( + 'code' => 'create-titleexists', + 'info' => "Existing titles can't be protected with 'create'" + ), + 'missingtitle-createonly' => array( + 'code' => 'missingtitle-createonly', + 'info' => "Missing titles can only be protected with 'create'" + ), + 'cantblock' => array( 'code' => 'cantblock', + 'info' => "You don't have permission to block users" + ), + 'canthide' => array( + 'code' => 'canthide', + 'info' => "You don't have permission to hide user names from the block log" + ), + 'cantblock-email' => array( + 'code' => 'cantblock-email', + 'info' => "You don't have permission to block users from sending email through the wiki" + ), + 'unblock-notarget' => array( + 'code' => 'notarget', + 'info' => "Either the id or the user parameter must be set" + ), + 'unblock-idanduser' => array( + 'code' => 'idanduser', + 'info' => "The id and user parameters can't be used together" + ), + 'cantunblock' => array( + 'code' => 'permissiondenied', + 'info' => "You don't have permission to unblock users" + ), + 'cannotundelete' => array( + 'code' => 'cantundelete', + 'info' => "Couldn't undelete: the requested revisions may not exist, or may have been undeleted already" + ), + 'permdenied-undelete' => array( + 'code' => 'permissiondenied', + 'info' => "You don't have permission to restore deleted revisions" + ), + 'createonly-exists' => array( + 'code' => 'articleexists', + 'info' => "The article you tried to create has been created already" + ), + 'nocreate-missing' => array( + 'code' => 'missingtitle', + 'info' => "The article you tried to edit doesn't exist" + ), + 'cantchangecontentmodel' => array( + 'code' => 'cantchangecontentmodel', + 'info' => "You don't have permission to change the content model of a page" + ), + 'nosuchrcid' => array( + 'code' => 'nosuchrcid', + 'info' => "There is no change with rcid \"\$1\"" + ), + '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\"" ), - 'cantoverwrite-sharedfile' => array( 'code' => 'cantoverwrite-sharedfile', 'info' => 'The target file exists on a shared repository and you do not have permission to override it' ), - 'sharedfile-exists' => array( 'code' => 'fileexists-sharedrepo-perm', 'info' => 'The target file exists on a shared repository. Use the ignorewarnings parameter to override it.' ), - 'mustbeposted' => array( 'code' => 'mustbeposted', 'info' => "The \$1 module requires a POST request" ), - 'show' => array( 'code' => 'show', 'info' => 'Incorrect parameter - mutually exclusive values may not be supplied' ), - 'specialpage-cantexecute' => array( 'code' => 'specialpage-cantexecute', 'info' => "You don't have permission to view the results of this special page" ), - 'invalidoldimage' => array( 'code' => 'invalidoldimage', 'info' => 'The oldimage parameter has invalid format' ), - 'nodeleteablefile' => array( 'code' => 'nodeleteablefile', 'info' => 'No such old version of the file' ), - 'fileexists-forbidden' => array( 'code' => 'fileexists-forbidden', 'info' => 'A file with name "$1" already exists, and cannot be overwritten.' ), - 'fileexists-shared-forbidden' => array( 'code' => 'fileexists-shared-forbidden', 'info' => 'A file with name "$1" already exists in the shared file repository, and cannot be overwritten.' ), - 'filerevert-badversion' => array( 'code' => 'filerevert-badversion', 'info' => 'There is no previous local version of this file with the provided timestamp.' ), + '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\"" + ), + 'cantoverwrite-sharedfile' => array( + 'code' => 'cantoverwrite-sharedfile', + 'info' => 'The target file exists on a shared repository and you do not have permission to override it' + ), + 'sharedfile-exists' => array( + 'code' => 'fileexists-sharedrepo-perm', + 'info' => 'The target file exists on a shared repository. Use the ignorewarnings parameter to override it.' + ), + 'mustbeposted' => array( + 'code' => 'mustbeposted', + 'info' => "The \$1 module requires a POST request" + ), + 'show' => array( + 'code' => 'show', + 'info' => 'Incorrect parameter - mutually exclusive values may not be supplied' + ), + 'specialpage-cantexecute' => array( + 'code' => 'specialpage-cantexecute', + 'info' => "You don't have permission to view the results of this special page" + ), + 'invalidoldimage' => array( + 'code' => 'invalidoldimage', + 'info' => 'The oldimage parameter has invalid format' + ), + 'nodeleteablefile' => array( + 'code' => 'nodeleteablefile', + 'info' => 'No such old version of the file' + ), + 'fileexists-forbidden' => array( + 'code' => 'fileexists-forbidden', + 'info' => 'A file with name "$1" already exists, and cannot be overwritten.' + ), + 'fileexists-shared-forbidden' => array( + 'code' => 'fileexists-shared-forbidden', + 'info' => 'A file with name "$1" already exists in the shared file repository, and cannot be overwritten.' + ), + 'filerevert-badversion' => array( + 'code' => 'filerevert-badversion', + 'info' => 'There is no previous local version of this file with the provided timestamp.' + ), // 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\"" ), - 'contenttoobig' => array( 'code' => 'contenttoobig', 'info' => "The content you supplied exceeds the article size limit of \$1 kilobytes" ), + '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\"" + ), + '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" ), + '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, 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' ), + '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' + ), + 'content-not-allowed-here' => array( + 'code' => 'contentnotallowedhere', + 'info' => 'Content model "$1" is not allowed at title "$2"' + ), // Messages from WikiPage::doEit() - 'edit-hook-aborted' => array( 'code' => 'edit-hook-aborted', 'info' => "Your edit was aborted by an ArticleSave hook" ), - 'edit-gone-missing' => array( 'code' => 'edit-gone-missing', 'info' => "The page you tried to edit doesn't seem to exist anymore" ), + 'edit-hook-aborted' => array( + 'code' => 'edit-hook-aborted', + 'info' => "Your edit was aborted by an ArticleSave hook" + ), + 'edit-gone-missing' => array( + 'code' => 'edit-gone-missing', + 'info' => "The page you tried to edit doesn't seem to exist anymore" + ), 'edit-conflict' => array( 'code' => 'editconflict', 'info' => "Edit conflict detected" ), - 'edit-already-exists' => array( 'code' => 'edit-already-exists', 'info' => "It seems the page you tried to create already exist" ), + 'edit-already-exists' => array( + 'code' => 'edit-already-exists', + 'info' => 'It seems the page you tried to create already exist' + ), // uploadMsgs 'invalid-file-key' => array( 'code' => 'invalid-file-key', 'info' => 'Not a valid file key' ), 'nouploadmodule' => array( 'code' => 'nouploadmodule', 'info' => 'No upload module set' ), - 'uploaddisabled' => array( 'code' => 'uploaddisabled', 'info' => 'Uploads are not enabled. Make sure $wgEnableUploads is set to true in LocalSettings.php and the PHP ini setting file_uploads is true' ), - 'copyuploaddisabled' => array( 'code' => 'copyuploaddisabled', 'info' => 'Uploads by URL is not enabled. Make sure $wgAllowCopyUploads is set to true in LocalSettings.php.' ), - 'copyuploadbaddomain' => array( 'code' => 'copyuploadbaddomain', 'info' => 'Uploads by URL are not allowed from this domain.' ), - 'copyuploadbadurl' => array( 'code' => 'copyuploadbadurl', 'info' => 'Upload not allowed from this URL.' ), - - 'filename-tooshort' => array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ), + 'uploaddisabled' => array( + 'code' => 'uploaddisabled', + 'info' => 'Uploads are not enabled. Make sure $wgEnableUploads is set to true in LocalSettings.php and the PHP ini setting file_uploads is true' + ), + 'copyuploaddisabled' => array( + 'code' => 'copyuploaddisabled', + 'info' => 'Uploads by URL is not enabled. Make sure $wgAllowCopyUploads is set to true in LocalSettings.php.' + ), + 'copyuploadbaddomain' => array( + 'code' => 'copyuploadbaddomain', + 'info' => 'Uploads by URL are not allowed from this domain.' + ), + 'copyuploadbadurl' => array( + 'code' => 'copyuploadbadurl', + 'info' => 'Upload not allowed from this URL.' + ), + + 'filename-tooshort' => array( + 'code' => 'filename-tooshort', + 'info' => 'The filename is too short' + ), 'filename-toolong' => array( 'code' => 'filename-toolong', 'info' => 'The filename is too long' ), - 'illegal-filename' => array( 'code' => 'illegal-filename', 'info' => 'The filename is not allowed' ), - 'filetype-missing' => array( 'code' => 'filetype-missing', 'info' => 'The file is missing an extension' ), + 'illegal-filename' => array( + 'code' => 'illegal-filename', + 'info' => 'The filename is not allowed' + ), + 'filetype-missing' => array( + 'code' => 'filetype-missing', + 'info' => 'The file is missing an extension' + ), 'mustbeloggedin' => array( 'code' => 'mustbeloggedin', 'info' => 'You must be logged in to $1.' ) ); + // @codingStandardsIgnoreEnd /** * Helper function for readonly errors @@ -1426,7 +1734,7 @@ abstract class ApiBase extends ContextSource { /** * Output the error message related to a certain array - * @param $error (array|string) Element of a getUserPermissionsErrors()-style array + * @param array|string $error Element of a getUserPermissionsErrors()-style array */ public function dieUsageMsg( $error ) { # most of the time we send a 1 element, so we might as well send it as @@ -1441,26 +1749,25 @@ abstract class ApiBase extends ContextSource { /** * Will only set a warning instead of failing if the global $wgDebugAPI * is set to true. Otherwise behaves exactly as dieUsageMsg(). - * @param $error (array|string) Element of a getUserPermissionsErrors()-style array + * @param array|string $error Element of a getUserPermissionsErrors()-style array * @since 1.21 */ public function dieUsageMsgOrDebug( $error ) { - global $wgDebugAPI; - if ( $wgDebugAPI !== true ) { + if ( $this->getConfig()->get( 'DebugAPI' ) !== true ) { $this->dieUsageMsg( $error ); - } else { - if ( is_string( $error ) ) { - $error = array( $error ); - } - $parsed = $this->parseMsg( $error ); - $this->setWarning( '$wgDebugAPI: ' . $parsed['code'] - . ' - ' . $parsed['info'] ); } + + if ( is_string( $error ) ) { + $error = array( $error ); + } + $parsed = $this->parseMsg( $error ); + $this->setWarning( '$wgDebugAPI: ' . $parsed['code'] . ' - ' . $parsed['info'] ); } /** - * Die with the $prefix.'badcontinue' error. This call is common enough to make it into the base method. - * @param $condition boolean will only die if this value is true + * Die with the $prefix.'badcontinue' error. This call is common enough to + * make it into the base method. + * @param bool $condition Will only die if this value is true * @since 1.21 */ protected function dieContinueUsageIf( $condition ) { @@ -1502,193 +1809,311 @@ abstract class ApiBase extends ContextSource { * Internal code errors should be reported with this method * @param string $method Method or function name * @param string $message Error message + * @throws MWException */ protected static function dieDebug( $method, $message ) { throw new MWException( "Internal error in $method: $message" ); } - /** - * Indicates if this module needs maxlag to be checked - * @return bool - */ - public function shouldCheckMaxlag() { - return true; - } + /**@}*/ - /** - * Indicates whether this module requires read rights - * @return bool + /************************************************************************//** + * @name Help message generation + * @{ */ - public function isReadMode() { - return true; - } - /** - * Indicates whether this module requires write mode - * @return bool - */ - public function isWriteMode() { - return false; - } /** - * Indicates whether this module must be called with a POST request - * @return bool + * Generates help message for this module, or false if there is no description + * @return string|bool */ - public function mustBePosted() { - return false; - } + public function makeHelpMsg() { + static $lnPrfx = "\n "; - /** - * Returns whether this module requires a token to execute - * It is used to show possible errors in action=paraminfo - * see bug 25248 - * @return bool - */ - public function needsToken() { - return false; - } + $msg = $this->getFinalDescription(); - /** - * Returns the token salt if there is one, - * '' if the module doesn't require a salt, - * else false if the module doesn't need a token - * You have also to override needsToken() - * Value is passed to User::getEditToken - * @return bool|string|array - */ - public function getTokenSalt() { - return false; - } + if ( $msg !== false ) { - /** - * Gets the user for whom to get the watchlist - * - * @param $params array - * @return User - */ - public function getWatchlistUser( $params ) { - if ( !is_null( $params['owner'] ) && !is_null( $params['token'] ) ) { - $user = User::newFromName( $params['owner'], false ); - if ( !( $user && $user->getId() ) ) { - $this->dieUsage( 'Specified user does not exist', 'bad_wlowner' ); + if ( !is_array( $msg ) ) { + $msg = array( + $msg + ); } - $token = $user->getOption( 'watchlisttoken' ); - if ( $token == '' || $token != $params['token'] ) { - $this->dieUsage( 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences', 'bad_wltoken' ); + $msg = $lnPrfx . implode( $lnPrfx, $msg ) . "\n"; + + $msg .= $this->makeHelpArrayToString( $lnPrfx, false, $this->getHelpUrls() ); + + if ( $this->isReadMode() ) { + $msg .= "\nThis module requires read rights"; } - } else { - if ( !$this->getUser()->isLoggedIn() ) { - $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' ); + if ( $this->isWriteMode() ) { + $msg .= "\nThis module requires write rights"; } - if ( !$this->getUser()->isAllowed( 'viewmywatchlist' ) ) { - $this->dieUsage( 'You don\'t have permission to view your watchlist', 'permissiondenied' ); + if ( $this->mustBePosted() ) { + $msg .= "\nThis module only accepts POST requests"; + } + if ( $this->isReadMode() || $this->isWriteMode() || + $this->mustBePosted() + ) { + $msg .= "\n"; + } + + // Parameters + $paramsMsg = $this->makeHelpMsgParameters(); + if ( $paramsMsg !== false ) { + $msg .= "Parameters:\n$paramsMsg"; + } + + $examples = $this->getExamples(); + if ( $examples ) { + if ( !is_array( $examples ) ) { + $examples = array( + $examples + ); + } + $msg .= "Example" . ( count( $examples ) > 1 ? 's' : '' ) . ":\n"; + foreach ( $examples as $k => $v ) { + if ( is_numeric( $k ) ) { + $msg .= " $v\n"; + } else { + if ( is_array( $v ) ) { + $msgExample = implode( "\n", array_map( array( $this, 'indentExampleText' ), $v ) ); + } else { + $msgExample = " $v"; + } + $msgExample .= ":"; + $msg .= wordwrap( $msgExample, 100, "\n" ) . "\n $k\n"; + } + } } - $user = $this->getUser(); } - return $user; + + return $msg; } /** - * @return bool|string|array Returns a false if the module has no help url, else returns a (array of) string + * @param string $item + * @return string */ - public function getHelpUrls() { - return false; + private function indentExampleText( $item ) { + return " " . $item; } /** - * Returns a list of all possible errors returned by the module - * - * Don't call this function directly: use getFinalPossibleErrors() to allow - * hooks to modify parameters as needed. - * - * @return array in the format of array( key, param1, param2, ... ) or array( 'code' => ..., 'info' => ... ) + * @param string $prefix Text to split output items + * @param string $title What is being output + * @param string|array $input + * @return string */ - public function getPossibleErrors() { - $ret = array(); - - $params = $this->getFinalParams(); - if ( $params ) { - foreach ( $params as $paramName => $paramSettings ) { - if ( isset( $paramSettings[ApiBase::PARAM_REQUIRED] ) && $paramSettings[ApiBase::PARAM_REQUIRED] ) { - $ret[] = array( 'missingparam', $paramName ); - } - } - if ( array_key_exists( 'continue', $params ) ) { - $ret[] = array( - 'code' => 'badcontinue', - 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' - ); - } - } - - if ( $this->mustBePosted() ) { - $ret[] = array( 'mustbeposted', $this->getModuleName() ); - } - - if ( $this->isReadMode() ) { - $ret[] = array( 'readrequired' ); + protected function makeHelpArrayToString( $prefix, $title, $input ) { + if ( $input === false ) { + return ''; } - - if ( $this->isWriteMode() ) { - $ret[] = array( 'writerequired' ); - $ret[] = array( 'writedisabled' ); + if ( !is_array( $input ) ) { + $input = array( $input ); } - if ( $this->needsToken() ) { - if ( !isset( $params['token'][ApiBase::PARAM_REQUIRED] ) - || !$params['token'][ApiBase::PARAM_REQUIRED] - ) { - // Add token as possible missing parameter, if not already done - $ret[] = array( 'missingparam', 'token' ); + if ( count( $input ) > 0 ) { + if ( $title ) { + $msg = $title . ( count( $input ) > 1 ? 's' : '' ) . ":\n "; + } else { + $msg = ' '; } - $ret[] = array( 'sessionfailure' ); + $msg .= implode( $prefix, $input ) . "\n"; + + return $msg; } - return $ret; + return ''; } /** - * Get final list of possible errors, after hooks have had a chance to - * tweak it as needed. - * - * @return array - * @since 1.22 + * Generates the parameter descriptions for this module, to be displayed in the + * module's help. + * @return string|bool */ - public function getFinalPossibleErrors() { - $possibleErrors = $this->getPossibleErrors(); - wfRunHooks( 'APIGetPossibleErrors', array( $this, &$possibleErrors ) ); - return $possibleErrors; - } + public function makeHelpMsgParameters() { + $params = $this->getFinalParams( ApiBase::GET_VALUES_FOR_HELP ); + if ( $params ) { - /** - * Parses a list of errors into a standardised format - * @param array $errors List of errors. Items can be in the for array( key, param1, param2, ... ) or array( 'code' => ..., 'info' => ... ) - * @return array Parsed list of errors with items in the form array( 'code' => ..., 'info' => ... ) - */ - public function parseErrors( $errors ) { - $ret = array(); + $paramsDescription = $this->getFinalParamDescription(); + $msg = ''; + $paramPrefix = "\n" . str_repeat( ' ', 24 ); + $descWordwrap = "\n" . str_repeat( ' ', 28 ); + foreach ( $params as $paramName => $paramSettings ) { + $desc = isset( $paramsDescription[$paramName] ) ? $paramsDescription[$paramName] : ''; + if ( is_array( $desc ) ) { + $desc = implode( $paramPrefix, $desc ); + } - foreach ( $errors as $row ) { - if ( isset( $row['code'] ) && isset( $row['info'] ) ) { - $ret[] = $row; - } else { - $ret[] = $this->parseMsg( $row ); + //handle shorthand + if ( !is_array( $paramSettings ) ) { + $paramSettings = array( + self::PARAM_DFLT => $paramSettings, + ); + } + + //handle missing type + if ( !isset( $paramSettings[ApiBase::PARAM_TYPE] ) ) { + $dflt = isset( $paramSettings[ApiBase::PARAM_DFLT] ) + ? $paramSettings[ApiBase::PARAM_DFLT] + : null; + if ( is_bool( $dflt ) ) { + $paramSettings[ApiBase::PARAM_TYPE] = 'boolean'; + } elseif ( is_string( $dflt ) || is_null( $dflt ) ) { + $paramSettings[ApiBase::PARAM_TYPE] = 'string'; + } elseif ( is_int( $dflt ) ) { + $paramSettings[ApiBase::PARAM_TYPE] = 'integer'; + } + } + + if ( isset( $paramSettings[self::PARAM_DEPRECATED] ) + && $paramSettings[self::PARAM_DEPRECATED] + ) { + $desc = "DEPRECATED! $desc"; + } + + if ( isset( $paramSettings[self::PARAM_REQUIRED] ) + && $paramSettings[self::PARAM_REQUIRED] + ) { + $desc .= $paramPrefix . "This parameter is required"; + } + + $type = isset( $paramSettings[self::PARAM_TYPE] ) + ? $paramSettings[self::PARAM_TYPE] + : null; + if ( isset( $type ) ) { + $hintPipeSeparated = true; + $multi = isset( $paramSettings[self::PARAM_ISMULTI] ) + ? $paramSettings[self::PARAM_ISMULTI] + : false; + if ( $multi ) { + $prompt = 'Values (separate with \'|\'): '; + } else { + $prompt = 'One value: '; + } + + if ( $type === 'submodule' ) { + $type = $this->getModuleManager()->getNames( $paramName ); + sort( $type ); + } + if ( is_array( $type ) ) { + $choices = array(); + $nothingPrompt = ''; + foreach ( $type as $t ) { + if ( $t === '' ) { + $nothingPrompt = 'Can be empty, or '; + } else { + $choices[] = $t; + } + } + $desc .= $paramPrefix . $nothingPrompt . $prompt; + $choicesstring = implode( ', ', $choices ); + $desc .= wordwrap( $choicesstring, 100, $descWordwrap ); + $hintPipeSeparated = false; + } else { + switch ( $type ) { + case 'namespace': + // Special handling because namespaces are + // type-limited, yet they are not given + $desc .= $paramPrefix . $prompt; + $desc .= wordwrap( implode( ', ', MWNamespace::getValidNamespaces() ), + 100, $descWordwrap ); + $hintPipeSeparated = false; + break; + case 'limit': + $desc .= $paramPrefix . "No more than {$paramSettings[self::PARAM_MAX]}"; + if ( isset( $paramSettings[self::PARAM_MAX2] ) ) { + $desc .= " ({$paramSettings[self::PARAM_MAX2]} for bots)"; + } + $desc .= ' allowed'; + break; + case 'integer': + $s = $multi ? 's' : ''; + $hasMin = isset( $paramSettings[self::PARAM_MIN] ); + $hasMax = isset( $paramSettings[self::PARAM_MAX] ); + if ( $hasMin || $hasMax ) { + if ( !$hasMax ) { + $intRangeStr = "The value$s must be no less than " . + "{$paramSettings[self::PARAM_MIN]}"; + } elseif ( !$hasMin ) { + $intRangeStr = "The value$s must be no more than " . + "{$paramSettings[self::PARAM_MAX]}"; + } else { + $intRangeStr = "The value$s must be between " . + "{$paramSettings[self::PARAM_MIN]} and {$paramSettings[self::PARAM_MAX]}"; + } + + $desc .= $paramPrefix . $intRangeStr; + } + break; + case 'upload': + $desc .= $paramPrefix . "Must be posted as a file upload using multipart/form-data"; + break; + } + } + + if ( $multi ) { + if ( $hintPipeSeparated ) { + $desc .= $paramPrefix . "Separate values with '|'"; + } + + $isArray = is_array( $type ); + if ( !$isArray + || $isArray && count( $type ) > self::LIMIT_SML1 + ) { + $desc .= $paramPrefix . "Maximum number of values " . + self::LIMIT_SML1 . " (" . self::LIMIT_SML2 . " for bots)"; + } + } + } + + $default = isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null; + if ( !is_null( $default ) && $default !== false ) { + $desc .= $paramPrefix . "Default: $default"; + } + + $msg .= sprintf( " %-19s - %s\n", $this->encodeParamName( $paramName ), $desc ); } + + return $msg; } - return $ret; + + return false; } + /**@}*/ + + /************************************************************************//** + * @name Profiling + * @{ + */ + /** * Profiling: total module execution time */ private $mTimeIn = 0, $mModuleTime = 0; /** + * Get the name of the module as shown in the profiler log + * + * @param DatabaseBase|bool $db + * + * @return string + */ + public function getModuleProfileName( $db = false ) { + if ( $db ) { + return 'API:' . $this->mModuleName . '-DB'; + } + + return 'API:' . $this->mModuleName; + } + + /** * Start module profiling */ public function profileIn() { if ( $this->mTimeIn !== 0 ) { - ApiBase::dieDebug( __METHOD__, 'called twice without calling profileOut()' ); + ApiBase::dieDebug( __METHOD__, 'Called twice without calling profileOut()' ); } $this->mTimeIn = microtime( true ); wfProfileIn( $this->getModuleProfileName() ); @@ -1699,10 +2124,13 @@ abstract class ApiBase extends ContextSource { */ public function profileOut() { if ( $this->mTimeIn === 0 ) { - ApiBase::dieDebug( __METHOD__, 'called without calling profileIn() first' ); + ApiBase::dieDebug( __METHOD__, 'Called without calling profileIn() first' ); } if ( $this->mDBTimeIn !== 0 ) { - ApiBase::dieDebug( __METHOD__, 'must be called after database profiling is done with profileDBOut()' ); + ApiBase::dieDebug( + __METHOD__, + 'Must be called after database profiling is done with profileDBOut()' + ); } $this->mModuleTime += microtime( true ) - $this->mTimeIn; @@ -1729,8 +2157,9 @@ abstract class ApiBase extends ContextSource { */ public function getProfileTime() { if ( $this->mTimeIn !== 0 ) { - ApiBase::dieDebug( __METHOD__, 'called without calling profileOut() first' ); + ApiBase::dieDebug( __METHOD__, 'Called without calling profileOut() first' ); } + return $this->mModuleTime; } @@ -1744,10 +2173,13 @@ abstract class ApiBase extends ContextSource { */ public function profileDBIn() { if ( $this->mTimeIn === 0 ) { - ApiBase::dieDebug( __METHOD__, 'must be called while profiling the entire module with profileIn()' ); + ApiBase::dieDebug( + __METHOD__, + 'Must be called while profiling the entire module with profileIn()' + ); } if ( $this->mDBTimeIn !== 0 ) { - ApiBase::dieDebug( __METHOD__, 'called twice without calling profileDBOut()' ); + ApiBase::dieDebug( __METHOD__, 'Called twice without calling profileDBOut()' ); } $this->mDBTimeIn = microtime( true ); wfProfileIn( $this->getModuleProfileName( true ) ); @@ -1758,10 +2190,11 @@ abstract class ApiBase extends ContextSource { */ public function profileDBOut() { if ( $this->mTimeIn === 0 ) { - ApiBase::dieDebug( __METHOD__, 'must be called while profiling the entire module with profileIn()' ); + ApiBase::dieDebug( __METHOD__, 'Must be called while profiling ' . + 'the entire module with profileIn()' ); } if ( $this->mDBTimeIn === 0 ) { - ApiBase::dieDebug( __METHOD__, 'called without calling profileDBIn() first' ); + ApiBase::dieDebug( __METHOD__, 'Called without calling profileDBIn() first' ); } $time = microtime( true ) - $this->mDBTimeIn; @@ -1778,36 +2211,168 @@ abstract class ApiBase extends ContextSource { */ public function getProfileDBTime() { if ( $this->mDBTimeIn !== 0 ) { - ApiBase::dieDebug( __METHOD__, 'called without calling profileDBOut() first' ); + ApiBase::dieDebug( __METHOD__, 'Called without calling profileDBOut() first' ); } + return $this->mDBTime; } /** - * Gets a default slave database connection object - * @return DatabaseBase + * Write logging information for API features to a debug log, for usage + * analysis. + * @param string $feature Feature being used. */ - protected function getDB() { - if ( !isset( $this->mSlaveDB ) ) { - $this->profileDBIn(); - $this->mSlaveDB = wfGetDB( DB_SLAVE, 'api' ); - $this->profileDBOut(); - } - return $this->mSlaveDB; + protected function logFeatureUsage( $feature ) { + $request = $this->getRequest(); + $s = '"' . addslashes( $feature ) . '"' . + ' "' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) . '"' . + ' "' . $request->getIP() . '"' . + ' "' . addslashes( $request->getHeader( 'Referer' ) ) . '"' . + ' "' . addslashes( $request->getHeader( 'User-agent' ) ) . '"'; + wfDebugLog( 'api-feature-usage', $s, 'private' ); } + /**@}*/ + + /************************************************************************//** + * @name Deprecated + * @{ + */ + + /// @deprecated since 1.24 + const PROP_ROOT = 'ROOT'; + /// @deprecated since 1.24 + const PROP_LIST = 'LIST'; + /// @deprecated since 1.24 + const PROP_TYPE = 0; + /// @deprecated since 1.24 + const PROP_NULLABLE = 1; + /** - * Debugging function that prints a value and an optional backtrace - * @param $value mixed Value to print - * @param string $name Description of the printed value - * @param bool $backtrace If true, print a backtrace + * Formerly returned a string that identifies the version of the extending + * class. Typically included the class name, the svn revision, timestamp, + * and last author. Usually done with SVN's Id keyword + * + * @deprecated since 1.21, version string is no longer supported + * @return string */ - public static function debugPrint( $value, $name = 'unknown', $backtrace = false ) { - print "\n\n<pre><b>Debugging value '$name':</b>\n\n"; - var_export( $value ); - if ( $backtrace ) { - print "\n" . wfBacktrace(); - } - print "\n</pre>\n"; + public function getVersion() { + wfDeprecated( __METHOD__, '1.21' ); + return ''; + } + + /** + * Formerly used to fetch a list of possible properites in the result, + * somehow organized with respect to the prop parameter that causes them to + * be returned. The specific semantics of the return value was never + * specified. Since this was never possible to be accurately updated, it + * has been removed. + * + * @deprecated since 1.24 + * @return array|bool + */ + protected function getResultProperties() { + wfDeprecated( __METHOD__, '1.24' ); + return false; + } + + /** + * @see self::getResultProperties() + * @deprecated since 1.24 + * @return array|bool + */ + public function getFinalResultProperties() { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /** + * @see self::getResultProperties() + * @deprecated since 1.24 + */ + protected static function addTokenProperties( &$props, $tokenFunctions ) { + wfDeprecated( __METHOD__, '1.24' ); + } + + /** + * @see self::getPossibleErrors() + * @deprecated since 1.24 + * @return array + */ + public function getRequireOnlyOneParameterErrorMessages( $params ) { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /** + * @see self::getPossibleErrors() + * @deprecated since 1.24 + * @return array + */ + public function getRequireMaxOneParameterErrorMessages( $params ) { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /** + * @see self::getPossibleErrors() + * @deprecated since 1.24 + * @return array + */ + public function getRequireAtLeastOneParameterErrorMessages( $params ) { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /** + * @see self::getPossibleErrors() + * @deprecated since 1.24 + * @return array + */ + public function getTitleOrPageIdErrorMessage() { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /** + * This formerly attempted to return a list of all possible errors returned + * by the module. However, this was impossible to maintain in many cases + * since errors could come from other areas of MediaWiki and in some cases + * from arbitrary extension hooks. Since a partial list claiming to be + * comprehensive is unlikely to be useful, it was removed. + * + * @deprecated since 1.24 + * @return array + */ + public function getPossibleErrors() { + wfDeprecated( __METHOD__, '1.24' ); + return array(); } + + /** + * @see self::getPossibleErrors() + * @deprecated since 1.24 + * @return array + */ + public function getFinalPossibleErrors() { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /** + * @see self::getPossibleErrors() + * @deprecated since 1.24 + * @return array + */ + public function parseErrors( $errors ) { + wfDeprecated( __METHOD__, '1.24' ); + return array(); + } + + /**@}*/ } + +/** + * For really cool vim folding this needs to be at the end: + * vim: foldmarker=@{,@} foldmethod=marker + */ diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index 975153ac..07f62c66 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -55,8 +55,11 @@ class ApiBlock extends ApiBase { } $target = User::newFromName( $params['user'] ); - // Bug 38633 - if the target is a user (not an IP address), but it doesn't exist or is unusable, error. - if ( $target instanceof User && ( $target->isAnon() /* doesn't exist */ || !User::isUsableName( $target->getName() ) ) ) { + // Bug 38633 - if the target is a user (not an IP address), but it + // doesn't exist or is unusable, error. + if ( $target instanceof User && + ( $target->isAnon() /* doesn't exist */ || !User::isUsableName( $target->getName() ) ) + ) { $this->dieUsageMsg( array( 'nosuchuser', $params['user'] ) ); } @@ -149,7 +152,6 @@ class ApiBlock extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => null, 'expiry' => 'never', 'reason' => '', 'anononly' => false, @@ -166,80 +168,35 @@ class ApiBlock extends ApiBase { public function getParamDescription() { return array( 'user' => 'Username, IP address or IP range you want to block', - 'token' => 'A block token previously obtained through prop=info', - 'expiry' => 'Relative expiry time, e.g. \'5 months\' or \'2 weeks\'. If set to \'infinite\', \'indefinite\' or \'never\', the block will never expire.', + 'expiry' => 'Relative expiry time, e.g. \'5 months\' or \'2 weeks\'. ' . + 'If set to \'infinite\', \'indefinite\' or \'never\', the block will never expire.', 'reason' => 'Reason for block', 'anononly' => 'Block anonymous users only (i.e. disable anonymous edits for this IP)', 'nocreate' => 'Prevent account creation', - 'autoblock' => 'Automatically block the last used IP address, and any subsequent IP addresses they try to login from', - 'noemail' => 'Prevent user from sending email through the wiki. (Requires the "blockemail" right.)', + 'autoblock' => 'Automatically block the last used IP address, and ' . + 'any subsequent IP addresses they try to login from', + 'noemail' + => 'Prevent user from sending email through the wiki. (Requires the "blockemail" right.)', 'hidename' => 'Hide the username from the block log. (Requires the "hideuser" right.)', - 'allowusertalk' => 'Allow the user to edit their own talk page (depends on $wgBlockAllowsUTEdit)', + 'allowusertalk' + => 'Allow the user to edit their own talk page (depends on $wgBlockAllowsUTEdit)', 'reblock' => 'If the user is already blocked, overwrite the existing block', 'watchuser' => 'Watch the user/IP\'s user and talk pages', ); } - public function getResultProperties() { - return array( - '' => array( - 'user' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'userID' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'expiry' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'id' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'reason' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'anononly' => 'boolean', - 'nocreate' => 'boolean', - 'autoblock' => 'boolean', - 'noemail' => 'boolean', - 'hidename' => 'boolean', - 'allowusertalk' => 'boolean', - 'watchuser' => 'boolean' - ) - ); - } - public function getDescription() { - return 'Block a user'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'cantblock' ), - array( 'canthide' ), - array( 'cantblock-email' ), - array( 'ipbblocked' ), - array( 'ipbnounblockself' ), - ) ); + return 'Block a user.'; } public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { return array( - 'api.php?action=block&user=123.5.5.12&expiry=3%20days&reason=First%20strike', - 'api.php?action=block&user=Vandal&expiry=never&reason=Vandalism&nocreate=&autoblock=&noemail=' + 'api.php?action=block&user=123.5.5.12&expiry=3%20days&reason=First%20strike&token=123ABC', + 'api.php?action=block&user=Vandal&expiry=never&reason=Vandalism&nocreate=&autoblock=&noemail=&token=123ABC' ); } diff --git a/includes/api/ApiClearHasMsg.php b/includes/api/ApiClearHasMsg.php new file mode 100644 index 00000000..32e20e80 --- /dev/null +++ b/includes/api/ApiClearHasMsg.php @@ -0,0 +1,58 @@ +<?php + +/** + * Created on August 26, 2014 + * + * Copyright © 2014 Petr Bena (benapetr@gmail.com) + * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * API module that clears the hasmsg flag for current user + * @ingroup API + */ +class ApiClearHasMsg extends ApiBase { + public function execute() { + $user = $this->getUser(); + $user->setNewtalk( false ); + $this->getResult()->addValue( null, $this->getModuleName(), 'success' ); + } + + public function isWriteMode() { + return true; + } + + public function mustBePosted() { + return false; + } + + public function getDescription() { + return array( 'Clears the hasmsg flag for current user.' ); + } + + public function getExamples() { + return array( + 'api.php?action=clearhasmsg' => 'Clears the hasmsg flag for current user', + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:ClearHasMsg'; + } +} diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index 1e35c349..48559268 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -65,19 +65,22 @@ class ApiComparePages extends ApiBase { $difftext = $de->getDiffBody(); if ( $difftext === false ) { - $this->dieUsage( 'The diff cannot be retrieved. ' . - 'Maybe one or both revisions do not exist or you do not have permission to view them.', 'baddiff' ); - } else { - ApiResult::setContent( $vals, $difftext ); + $this->dieUsage( + 'The diff cannot be retrieved. Maybe one or both revisions do ' . + 'not exist or you do not have permission to view them.', + 'baddiff' + ); } + ApiResult::setContent( $vals, $difftext ); + $this->getResult()->addValue( null, $this->getModuleName(), $vals ); } /** - * @param $revision int - * @param $titleText string - * @param $titleId int + * @param int $revision + * @param string $titleText + * @param int $titleId * @return int */ private function revisionOrTitleOrId( $revision, $titleText, $titleId ) { @@ -88,15 +91,20 @@ class ApiComparePages extends ApiBase { if ( !$title || $title->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $titleText ) ); } + return $title->getLatestRevID(); } elseif ( $titleId ) { $title = Title::newFromID( $titleId ); if ( !$title ) { $this->dieUsageMsg( array( 'nosuchpageid', $titleId ) ); } + return $title->getLatestRevID(); } - $this->dieUsage( 'inputneeded', 'A title, a page ID, or a revision number is needed for both the from and the to parameters' ); + $this->dieUsage( + 'A title, a page ID, or a revision number is needed for both the from and the to parameters', + 'inputneeded' + ); } public function getAllowedParams() { @@ -129,40 +137,13 @@ class ApiComparePages extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'fromtitle' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'fromrevid' => 'integer', - 'totitle' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'torevid' => 'integer', - '*' => 'string' - ) - ); - } - public function getDescription() { return array( - 'Get the difference between 2 pages', - 'You must pass a revision number or a page title or a page ID id for each part (1 and 2)' + 'Get the difference between 2 pages.', + 'You must pass a revision number or a page title or a page ID id for each part (1 and 2).' ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'inputneeded', 'info' => 'A title or a revision is needed' ), - array( 'invalidtitle', 'title' ), - array( 'nosuchpageid', 'pageid' ), - array( 'code' => 'baddiff', 'info' => 'The diff cannot be retrieved. Maybe one or both revisions do not exist or you do not have permission to view them.' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=compare&fromrev=1&torev=2' => 'Create a diff between revision 1 and 2', diff --git a/includes/api/ApiCreateAccount.php b/includes/api/ApiCreateAccount.php index 0e752c56..2ce532b9 100644 --- a/includes/api/ApiCreateAccount.php +++ b/includes/api/ApiCreateAccount.php @@ -39,7 +39,10 @@ class ApiCreateAccount extends ApiBase { // Use userCan in order to hit GlobalBlock checks (according to Special:userlogin) $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); if ( !$loginTitle->userCan( 'createaccount', $this->getUser() ) ) { - $this->dieUsage( 'You do not have the right to create a new account', 'permdenied-createaccount' ); + $this->dieUsage( + 'You do not have the right to create a new account', + 'permdenied-createaccount' + ); } if ( $this->getUser()->isBlockedFromCreateAccount() ) { $this->dieUsage( 'You cannot create a new account because you are blocked', 'blocked' ); @@ -80,13 +83,13 @@ class ApiCreateAccount extends ApiBase { $loginForm = new LoginForm(); $loginForm->setContext( $context ); + wfRunHooks( 'AddNewAccountApiForm', array( $this, $loginForm ) ); $loginForm->load(); $status = $loginForm->addNewaccountInternal(); $result = array(); if ( $status->isGood() ) { // Success! - global $wgEmailAuthentication; $user = $status->getValue(); if ( $params['language'] ) { @@ -96,8 +99,13 @@ class ApiCreateAccount extends ApiBase { if ( $params['mailpassword'] ) { // If mailpassword was set, disable the password and send an email. $user->setPassword( null ); - $status->merge( $loginForm->mailPasswordInternal( $user, false, 'createaccount-title', 'createaccount-text' ) ); - } elseif ( $wgEmailAuthentication && Sanitizer::validateEmail( $user->getEmail() ) ) { + $status->merge( $loginForm->mailPasswordInternal( + $user, + false, + 'createaccount-title', + 'createaccount-text' + ) ); + } elseif ( $this->getConfig()->get( 'EmailAuthentication' ) && Sanitizer::validateEmail( $user->getEmail() ) ) { // Send out an email authentication message if needed $status->merge( $user->sendConfirmationMail() ); } @@ -129,13 +137,13 @@ class ApiCreateAccount extends ApiBase { // since not having the correct token is part of the normal // flow of events. $result['token'] = LoginForm::getCreateaccountToken(); - $result['result'] = 'needtoken'; + $result['result'] = 'NeedToken'; } elseif ( !$status->isOK() ) { // There was an error. Die now. $this->dieStatus( $status ); } elseif ( !$status->isGood() ) { // Status is not good, but OK. This means warnings. - $result['result'] = 'warning'; + $result['result'] = 'Warning'; // Add any warnings to the result $warnings = $status->getErrorsByType( 'warning' ); @@ -148,9 +156,12 @@ class ApiCreateAccount extends ApiBase { } } else { // Everything was fine. - $result['result'] = 'success'; + $result['result'] = 'Success'; } + // Give extensions a chance to modify the API result data + wfRunHooks( 'AddNewAccountApiResult', array( $this, $loginForm, &$result ) ); + $apiResult->addValue( null, 'createaccount', $result ); } @@ -171,7 +182,6 @@ class ApiCreateAccount extends ApiBase { } public function getAllowedParams() { - global $wgEmailConfirmToEdit; return array( 'name' => array( ApiBase::PARAM_TYPE => 'user', @@ -182,7 +192,7 @@ class ApiCreateAccount extends ApiBase { 'token' => null, 'email' => array( ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => $wgEmailConfirmToEdit + ApiBase::PARAM_REQUIRED => $this->getConfig()->get( 'EmailConfirmToEdit' ), ), 'realname' => null, 'mailpassword' => array( @@ -196,6 +206,7 @@ class ApiCreateAccount extends ApiBase { public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'name' => 'Username', 'password' => "Password (ignored if {$p}mailpassword is set)", @@ -205,82 +216,9 @@ class ApiCreateAccount extends ApiBase { 'realname' => 'Real name of user (optional)', 'mailpassword' => 'If set to any value, a random password will be emailed to the user', 'reason' => 'Optional reason for creating the account to be put in the logs', - 'language' => 'Language code to set as default for the user (optional, defaults to content language)' - ); - } - - public function getResultProperties() { - return array( - 'createaccount' => array( - 'result' => array( - ApiBase::PROP_TYPE => array( - 'success', - 'warning', - 'needtoken' - ) - ), - 'username' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'userid' => array( - ApiBase::PROP_TYPE => 'int', - ApiBase::PROP_NULLABLE => true - ), - 'token' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - ) - ); - } - - public function getPossibleErrors() { - // Note the following errors aren't possible and don't need to be listed: - // sessionfailure, nocookiesfornew, badretype - $localErrors = array( - 'wrongpassword', // Actually caused by wrong domain field. Riddle me that... - 'sorbs_create_account_reason', - 'noname', - 'userexists', - 'password-name-match', // from User::getPasswordValidity - 'password-login-forbidden', // from User::getPasswordValidity - 'noemailtitle', - 'invalidemailaddress', - 'externaldberror', - 'acct_creation_throttle_hit', - ); - - $errors = parent::getPossibleErrors(); - // All local errors are from LoginForm, which means they're actually message keys. - foreach ( $localErrors as $error ) { - $errors[] = array( 'code' => $error, 'info' => wfMessage( $error )->inLanguage( 'en' )->useDatabase( false )->parse() ); - } - - $errors[] = array( - 'code' => 'permdenied-createaccount', - 'info' => 'You do not have the right to create a new account' - ); - $errors[] = array( - 'code' => 'blocked', - 'info' => 'You cannot create a new account because you are blocked' - ); - $errors[] = array( - 'code' => 'aborted', - 'info' => 'Account creation aborted by hook (info may vary)' - ); - $errors[] = array( - 'code' => 'langinvalid', - 'info' => 'Invalid language parameter' - ); - - // 'passwordtooshort' has parameters. :( - global $wgMinimalPasswordLength; - $errors[] = array( - 'code' => 'passwordtooshort', - 'info' => wfMessage( 'passwordtooshort', $wgMinimalPasswordLength )->inLanguage( 'en' )->useDatabase( false )->parse() + 'language' + => 'Language code to set as default for the user (optional, defaults to content language)' ); - return $errors; } public function getExamples() { diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index aea10482..abca8245 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -31,7 +31,6 @@ * @ingroup API */ class ApiDelete extends ApiBase { - /** * Extracts the title, token, and reason from the request parameters and invokes * the local delete() function with these as arguments. It does not make use of @@ -52,7 +51,14 @@ class ApiDelete extends ApiBase { $user = $this->getUser(); if ( $titleObj->getNamespace() == NS_FILE ) { - $status = self::deleteFile( $pageObj, $user, $params['token'], $params['oldimage'], $reason, false ); + $status = self::deleteFile( + $pageObj, + $user, + $params['token'], + $params['oldimage'], + $reason, + false + ); } else { $status = self::delete( $pageObj, $user, $params['token'], $reason ); } @@ -66,8 +72,10 @@ class ApiDelete extends ApiBase { // Deprecated parameters if ( $params['watch'] ) { + $this->logFeatureUsage( 'action=delete&watch' ); $watch = 'watch'; } elseif ( $params['unwatch'] ) { + $this->logFeatureUsage( 'action=delete&unwatch' ); $watch = 'unwatch'; } else { $watch = $params['watchlist']; @@ -83,9 +91,9 @@ class ApiDelete extends ApiBase { } /** - * @param $title Title - * @param $user User doing the action - * @param $token String + * @param Title $title + * @param User $user User doing the action + * @param string $token * @return array */ private static function getPermissionsError( $title, $user, $token ) { @@ -96,10 +104,10 @@ class ApiDelete extends ApiBase { /** * We have our own delete() function, since Article.php's implementation is split in two phases * - * @param $page Page|WikiPage object to work on - * @param $user User doing the action - * @param string $token delete token (same as edit token) - * @param string|null $reason reason for the deletion. Autogenerated if NULL + * @param Page|WikiPage $page Page or WikiPage object to work on + * @param User $user User doing the action + * @param string $token Delete token (same as edit token) + * @param string|null $reason Reason for the deletion. Autogenerated if null * @return Status|array */ public static function delete( Page $page, User $user, $token, &$reason = null ) { @@ -121,20 +129,23 @@ class ApiDelete extends ApiBase { } $error = ''; + // Luckily, Article.php provides a reusable delete function that does the hard work for us return $page->doDeleteArticleReal( $reason, false, 0, true, $error ); } /** - * @param $page WikiPage|Page object to work on - * @param $user User doing the action - * @param $token - * @param $oldimage - * @param $reason - * @param $suppress bool + * @param Page $page Object to work on + * @param User $user User doing the action + * @param string $token Delete token (same as edit token) + * @param string $oldimage Archive name + * @param string $reason Reason for the deletion. Autogenerated if null. + * @param bool $suppress Whether to mark all deleted versions as restricted * @return Status|array */ - public static function deleteFile( Page $page, User $user, $token, $oldimage, &$reason = null, $suppress = false ) { + public static function deleteFile( Page $page, User $user, $token, $oldimage, + &$reason = null, $suppress = false + ) { $title = $page->getTitle(); $errors = self::getPermissionsError( $title, $user, $token ); if ( count( $errors ) ) { @@ -159,6 +170,7 @@ class ApiDelete extends ApiBase { if ( is_null( $reason ) ) { // Log and RC don't like null reasons $reason = ''; } + return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress, $user ); } @@ -176,10 +188,6 @@ class ApiDelete extends ApiBase { 'pageid' => array( ApiBase::PARAM_TYPE => 'integer' ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'reason' => null, 'watch' => array( ApiBase::PARAM_DFLT => false, @@ -204,58 +212,33 @@ class ApiDelete extends ApiBase { public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'title' => "Title of the page you want to delete. Cannot be used together with {$p}pageid", 'pageid' => "Page ID of the page you want to delete. Cannot be used together with {$p}title", - 'token' => 'A delete token previously retrieved through prop=info', - 'reason' => 'Reason for the deletion. If not set, an automatically generated reason will be used', + 'reason' + => 'Reason for the deletion. If not set, an automatically generated reason will be used', 'watch' => 'Add the page to your watchlist', - 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', + 'watchlist' => 'Unconditionally add or remove the page from your ' . + 'watchlist, use preferences or do not change watch', 'unwatch' => 'Remove the page from your watchlist', 'oldimage' => 'The name of the old image to delete as provided by iiprop=archivename' ); } - public function getResultProperties() { - return array( - '' => array( - 'title' => 'string', - 'reason' => 'string', - 'logid' => 'integer' - ) - ); - } - public function getDescription() { - return 'Delete a page'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getTitleOrPageIdErrorMessage(), - array( - array( 'notanarticle' ), - array( 'hookaborted', 'error' ), - array( 'delete-toobig', 'limit' ), - array( 'cannotdelete', 'title' ), - array( 'invalidoldimage' ), - array( 'nodeleteablefile' ), - ) - ); + return 'Delete a page.'; } public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { return array( 'api.php?action=delete&title=Main%20Page&token=123ABC' => 'Delete the Main Page', - 'api.php?action=delete&title=Main%20Page&token=123ABC&reason=Preparing%20for%20move' => 'Delete the Main Page with the reason "Preparing for move"', + 'api.php?action=delete&title=Main%20Page&token=123ABC&reason=Preparing%20for%20move' + => 'Delete the Main Page with the reason "Preparing for move"', ); } diff --git a/includes/api/ApiDisabled.php b/includes/api/ApiDisabled.php index e5ef3b7e..6ea5d202 100644 --- a/includes/api/ApiDisabled.php +++ b/includes/api/ApiDisabled.php @@ -53,7 +53,7 @@ class ApiDisabled extends ApiBase { } public function getDescription() { - return 'This module has been disabled'; + return 'This module has been disabled.'; } public function getExamples() { diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index 51c9efc6..a423b560 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -32,15 +32,14 @@ * @ingroup API */ class ApiEditPage extends ApiBase { - public function execute() { $user = $this->getUser(); $params = $this->extractRequestParams(); if ( is_null( $params['text'] ) && is_null( $params['appendtext'] ) && - is_null( $params['prependtext'] ) && - $params['undo'] == 0 ) - { + is_null( $params['prependtext'] ) && + $params['undo'] == 0 + ) { $this->dieUsageMsg( 'missingtext' ); } @@ -49,12 +48,15 @@ class ApiEditPage extends ApiBase { $apiResult = $this->getResult(); if ( $params['redirect'] ) { + if ( $params['prependtext'] === null && $params['appendtext'] === null && $params['section'] !== 'new' ) { + $this->dieUsage( 'You have attempted to edit using the "redirect"-following mode, which must be used in conjuction with section=new, prependtext, or appendtext.', 'redirect-appendonly' ); + } if ( $titleObj->isRedirect() ) { $oldTitle = $titleObj; $titles = Revision::newFromTitle( $oldTitle, false, Revision::READ_LATEST ) - ->getContent( Revision::FOR_THIS_USER, $user ) - ->getRedirectChain(); + ->getContent( Revision::FOR_THIS_USER, $user ) + ->getRedirectChain(); // array_shift( $titles ); $redirValues = array(); @@ -88,7 +90,8 @@ class ApiEditPage extends ApiBase { $contentHandler = ContentHandler::getForModelID( $params['contentmodel'] ); } - // @todo ask handler whether direct editing is supported at all! make allowFlatEdit() method or some such + // @todo Ask handler whether direct editing is supported at all! make + // allowFlatEdit() method or some such if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) { $params['contentformat'] = $contentHandler->getDefaultFormat(); @@ -101,7 +104,7 @@ class ApiEditPage extends ApiBase { $model = $contentHandler->getModelID(); $this->dieUsage( "The requested format $contentFormat is not supported for content model " . - " $model used by $name", 'badformat' ); + " $model used by $name", 'badformat' ); } if ( $params['createonly'] && $titleObj->exists() ) { @@ -121,8 +124,7 @@ class ApiEditPage extends ApiBase { } $toMD5 = $params['text']; - if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) ) - { + if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) ) { $content = $pageObj->getContent(); if ( !$content ) { @@ -138,6 +140,7 @@ class ApiEditPage extends ApiBase { $content = ContentHandler::makeContent( $text, $this->getTitle() ); } catch ( MWContentSerializationException $ex ) { $this->dieUsage( $ex->getMessage(), 'parseerror' ); + return; } } else { @@ -156,7 +159,10 @@ class ApiEditPage extends ApiBase { if ( !is_null( $params['section'] ) ) { if ( !$contentHandler->supportsSections() ) { $modelName = $contentHandler->getModelID(); - $this->dieUsage( "Sections are not supported for this content model: $modelName.", 'sectionsnotsupported' ); + $this->dieUsage( + "Sections are not supported for this content model: $modelName.", + 'sectionsnotsupported' + ); } if ( $params['section'] == 'new' ) { @@ -164,7 +170,7 @@ class ApiEditPage extends ApiBase { $content = null; } else { // Process the content for section edits - $section = intval( $params['section'] ); + $section = $params['section']; $content = $content->getSection( $section ); if ( !$content ) { @@ -187,7 +193,7 @@ class ApiEditPage extends ApiBase { if ( $params['undoafter'] > 0 ) { if ( $params['undo'] < $params['undoafter'] ) { list( $params['undo'], $params['undoafter'] ) = - array( $params['undoafter'], $params['undo'] ); + array( $params['undoafter'], $params['undo'] ); } $undoafterRev = Revision::newFromID( $params['undoafter'] ); } @@ -204,13 +210,19 @@ class ApiEditPage extends ApiBase { } if ( $undoRev->getPage() != $pageObj->getID() ) { - $this->dieUsageMsg( array( 'revwrongpage', $undoRev->getID(), $titleObj->getPrefixedText() ) ); + $this->dieUsageMsg( array( 'revwrongpage', $undoRev->getID(), + $titleObj->getPrefixedText() ) ); } if ( $undoafterRev->getPage() != $pageObj->getID() ) { - $this->dieUsageMsg( array( 'revwrongpage', $undoafterRev->getID(), $titleObj->getPrefixedText() ) ); + $this->dieUsageMsg( array( 'revwrongpage', $undoafterRev->getID(), + $titleObj->getPrefixedText() ) ); } - $newContent = $contentHandler->getUndoContent( $pageObj->getRevision(), $undoRev, $undoafterRev ); + $newContent = $contentHandler->getUndoContent( + $pageObj->getRevision(), + $undoRev, + $undoafterRev + ); if ( !$newContent ) { $this->dieUsageMsg( 'undo-failure' ); @@ -220,8 +232,11 @@ class ApiEditPage extends ApiBase { // 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'] = wfMessage( 'undo-summary', $params['undo'], $undoRev->getUserText() )->inContentLanguage()->text(); + if ( is_null( $params['summary'] ) && + $titleObj->getNextRevisionID( $undoafterRev->getID() ) == $params['undo'] + ) { + $params['summary'] = wfMessage( 'undo-summary' ) + ->params ( $params['undo'], $undoRev->getUserText() )->inContentLanguage()->text(); } } @@ -237,7 +252,8 @@ class ApiEditPage extends ApiBase { 'format' => $contentFormat, 'model' => $contentHandler->getModelID(), 'wpEditToken' => $params['token'], - 'wpIgnoreBlankSummary' => '' + 'wpIgnoreBlankSummary' => '', + 'wpIgnoreBlankArticle' => true ); if ( !is_null( $params['summary'] ) ) { @@ -276,12 +292,12 @@ class ApiEditPage extends ApiBase { } if ( !is_null( $params['section'] ) ) { - $section = intval( $params['section'] ); - if ( $section == 0 && $params['section'] != '0' && $params['section'] != 'new' ) { - $this->dieUsage( "The section parameter must be set to an integer or 'new'", "invalidsection" ); + $section = $params['section']; + if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) { + $this->dieUsage( "The section parameter must be a valid section id or 'new'", "invalidsection" ); } $content = $pageObj->getContent(); - if ( $section !== 0 && ( !$content || !$content->getSection( $section ) ) ) { + if ( $section !== '0' && $section != 'new' && ( !$content || !$content->getSection( $section ) ) ) { $this->dieUsage( "There is no section {$section}.", 'nosuchsection' ); } $requestArray['wpSection'] = $params['section']; @@ -293,8 +309,10 @@ class ApiEditPage extends ApiBase { // Deprecated parameters if ( $params['watch'] ) { + $this->logFeatureUsage( 'action=edit&watch' ); $watch = true; } elseif ( $params['unwatch'] ) { + $this->logFeatureUsage( 'action=edit&unwatch' ); $watch = false; } @@ -333,9 +351,9 @@ class ApiEditPage extends ApiBase { // The following is needed to give the hook the full content of the // new revision rather than just the current section. (Bug 52077) - if ( !is_null( $params['section'] ) && $contentHandler->supportsSections() && $titleObj->exists() ) { - - $sectionTitle = ''; + if ( !is_null( $params['section'] ) && + $contentHandler->supportsSections() && $titleObj->exists() + ) { // If sectiontitle is set, use it, otherwise use the summary as the section title (for // backwards compatibility with old forms/bots). if ( $ep->sectiontitle !== '' ) { @@ -346,7 +364,11 @@ class ApiEditPage extends ApiBase { $contentObj = $contentHandler->unserializeContent( $content, $contentFormat ); - $fullContentObj = $articleObject->replaceSectionContent( $params['section'], $contentObj, $sectionTitle ); + $fullContentObj = $articleObject->replaceSectionContent( + $params['section'], + $contentObj, + $sectionTitle + ); if ( $fullContentObj ) { $content = $fullContentObj->serialize( $contentFormat ); } else { @@ -363,10 +385,11 @@ class ApiEditPage extends ApiBase { if ( count( $r ) ) { $r['result'] = 'Failure'; $apiResult->addValue( null, $this->getModuleName(), $r ); + return; - } else { - $this->dieUsageMsg( 'hookaborted' ); } + + $this->dieUsageMsg( 'hookaborted' ); } // Do the actual save @@ -379,7 +402,6 @@ class ApiEditPage extends ApiBase { $status = $ep->internalAttemptSave( $result, $user->isAllowed( 'bot' ) && $params['bot'] ); $wgRequest = $oldRequest; - global $wgMaxArticleSize; switch ( $status->value ) { case EditPage::AS_HOOK_ERROR: @@ -403,7 +425,7 @@ class ApiEditPage extends ApiBase { case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED: case EditPage::AS_CONTENT_TOO_BIG: - $this->dieUsageMsg( array( 'contenttoobig', $wgMaxArticleSize ) ); + $this->dieUsageMsg( array( 'contenttoobig', $this->getConfig()->get( 'MaxArticleSize' ) ) ); case EditPage::AS_READ_ONLY_PAGE_ANON: $this->dieUsageMsg( 'noedit-anon' ); @@ -481,52 +503,6 @@ class ApiEditPage extends ApiBase { return 'Create and edit pages.'; } - public function getPossibleErrors() { - global $wgMaxArticleSize; - - return array_merge( parent::getPossibleErrors(), - $this->getTitleOrPageIdErrorMessage(), - array( - array( 'missingtext' ), - array( 'createonly-exists' ), - array( 'nocreate-missing' ), - array( 'nosuchrevid', 'undo' ), - array( 'nosuchrevid', 'undoafter' ), - array( 'revwrongpage', 'id', 'text' ), - array( 'undo-failure' ), - array( 'hashcheckfailed' ), - array( 'hookaborted' ), - array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ), - array( 'noimageredirect-anon' ), - array( 'noimageredirect-logged' ), - array( 'spamdetected', 'spam' ), - array( 'summaryrequired' ), - array( 'blockedtext' ), - array( 'contenttoobig', $wgMaxArticleSize ), - array( 'noedit-anon' ), - array( 'noedit' ), - array( 'actionthrottledtext' ), - array( 'wasdeleted' ), - array( 'nocreate-loggedin' ), - array( 'blankpage' ), - array( 'editconflict' ), - array( 'emptynewsection' ), - array( 'unknownerror', 'retval' ), - array( 'code' => 'nosuchsection', 'info' => 'There is no section section.' ), - array( 'code' => 'invalidsection', 'info' => 'The section parameter must be set to an integer or \'new\'' ), - array( 'code' => 'sectionsnotsupported', 'info' => 'Sections are not supported for this type of page.' ), - array( 'code' => 'editnotsupported', 'info' => 'Editing of this type of page is not supported using ' - . 'the text based edit API.' ), - array( 'code' => 'appendnotsupported', 'info' => 'This type of page can not be edited by appending ' - . 'or prepending text.' ), - array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied to ' - . 'the page\'s content model' ), - array( 'customcssprotected' ), - array( 'customjsprotected' ), - ) - ); - } - public function getAllowedParams() { return array( 'title' => array( @@ -540,10 +516,6 @@ class ApiEditPage extends ApiBase { ApiBase::PARAM_TYPE => 'string', ), 'text' => null, - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'summary' => null, 'minor' => false, 'notminor' => false, @@ -594,36 +566,47 @@ class ApiEditPage extends ApiBase { public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'title' => "Title of the page you want to edit. Cannot be used together with {$p}pageid", 'pageid' => "Page ID of the page you want to edit. Cannot be used together with {$p}title", 'section' => 'Section number. 0 for the top section, \'new\' for a new section', 'sectiontitle' => 'The title for a new section', 'text' => 'Page content', - 'token' => array( 'Edit token. You can get one of these through prop=info.', - "The token should always be sent as the last parameter, or at least, after the {$p}text parameter" + 'token' => array( + /* Standard description is automatically prepended */ + 'The token should always be sent as the last parameter, or at ' . + "least, after the {$p}text parameter" ), - 'summary' => "Edit summary. Also section title when {$p}section=new and {$p}sectiontitle is not set", + 'summary' + => "Edit summary. Also section title when {$p}section=new and {$p}sectiontitle is not set", 'minor' => 'Minor edit', 'notminor' => 'Non-minor edit', 'bot' => 'Mark this edit as bot', - 'basetimestamp' => array( 'Timestamp of the base revision (obtained through prop=revisions&rvprop=timestamp).', - 'Used to detect edit conflicts; leave unset to ignore conflicts' + 'basetimestamp' => array( + 'Timestamp of the base revision (obtained through prop=revisions&rvprop=timestamp).', + 'Used to detect edit conflicts; leave unset to ignore conflicts' ), - 'starttimestamp' => array( 'Timestamp when you obtained the edit token.', - 'Used to detect edit conflicts; leave unset to ignore conflicts' + 'starttimestamp' => array( + 'Timestamp when you began the editing process, e.g. when the current page content ' . + 'was loaded for editing.', + 'Used to detect edit conflicts; leave unset to ignore conflicts' ), 'recreate' => 'Override any errors about the article having been deleted in the meantime', 'createonly' => 'Don\'t edit the page if it exists already', 'nocreate' => 'Throw an error if the page doesn\'t exist', 'watch' => 'Add the page to your watchlist', 'unwatch' => 'Remove the page from your watchlist', - 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', - 'md5' => array( "The MD5 hash of the {$p}text parameter, or the {$p}prependtext and {$p}appendtext parameters concatenated.", - 'If set, the edit won\'t be done unless the hash is correct' ), + 'watchlist' => 'Unconditionally add or remove the page from your ' . + 'watchlist, use preferences or do not change watch', + 'md5' => array( + "The MD5 hash of the {$p}text parameter, or the {$p}prependtext " . + "and {$p}appendtext parameters concatenated.", + 'If set, the edit won\'t be done unless the hash is correct' + ), 'prependtext' => "Add this text to the beginning of the page. Overrides {$p}text", 'appendtext' => array( "Add this text to the end of the page. Overrides {$p}text.", - "Use {$p}section=new to append a new section" ), + "Use {$p}section=new to append a new section" ), 'undo' => "Undo this revision. Overrides {$p}text, {$p}prependtext and {$p}appendtext", 'undoafter' => 'Undo all revisions from undo to this one. If not set, just undo one revision', 'redirect' => 'Automatically resolve redirects', @@ -632,56 +615,20 @@ class ApiEditPage extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'new' => 'boolean', - 'result' => array( - ApiBase::PROP_TYPE => array( - 'Success', - 'Failure' - ), - ), - 'pageid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'title' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'nochange' => 'boolean', - 'oldrevid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'newrevid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'newtimestamp' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { return array( - '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\\' => 'Edit a page (anonymous user)', - 'api.php?action=edit&title=Test&summary=NOTOC&minor=&prependtext=__NOTOC__%0A&basetimestamp=20070824123454&token=%2B\\' + 'api.php?action=edit&title=Test&summary=NOTOC&minor=&' . + 'prependtext=__NOTOC__%0A&basetimestamp=20070824123454&token=%2B\\' => 'Prepend __NOTOC__ to a page (anonymous user)', - 'api.php?action=edit&title=Test&undo=13585&undoafter=13579&basetimestamp=20070824123454&token=%2B\\' + 'api.php?action=edit&title=Test&undo=13585&undoafter=13579&' . + 'basetimestamp=20070824123454&token=%2B\\' => 'Undo r13579 through r13585 with autosummary (anonymous user)', ); } diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php index cd0d0cba..9870b2de 100644 --- a/includes/api/ApiEmailUser.php +++ b/includes/api/ApiEmailUser.php @@ -40,7 +40,11 @@ class ApiEmailUser extends ApiBase { } // Check permissions and errors - $error = SpecialEmailUser::getPermissionsError( $this->getUser(), $params['token'] ); + $error = SpecialEmailUser::getPermissionsError( + $this->getUser(), + $params['token'], + $this->getConfig() + ); if ( $error ) { $this->dieUsageMsg( array( $error ) ); } @@ -94,10 +98,6 @@ class ApiEmailUser extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'ccme' => false, ); } @@ -107,49 +107,22 @@ class ApiEmailUser extends ApiBase { 'target' => 'User to send email to', 'subject' => 'Subject header', 'text' => 'Mail body', - 'token' => 'A token previously acquired via prop=info', 'ccme' => 'Send a copy of this mail to me', ); } - public function getResultProperties() { - return array( - '' => array( - 'result' => array( - ApiBase::PROP_TYPE => array( - 'Success', - 'Failure' - ), - ), - 'message' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return 'Email a user.'; } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'usermaildisabled' ), - ) ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { return array( - 'api.php?action=emailuser&target=WikiSysop&text=Content' => 'Send an email to the User "WikiSysop" with the text "Content"', + 'api.php?action=emailuser&target=WikiSysop&text=Content&token=123ABC' + => 'Send an email to the User "WikiSysop" with the text "Content"', ); } diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php index d5c789c3..8a3b534d 100644 --- a/includes/api/ApiExpandTemplates.php +++ b/includes/api/ApiExpandTemplates.php @@ -39,6 +39,18 @@ class ApiExpandTemplates extends ApiBase { // Get parameters $params = $this->extractRequestParams(); + $this->requireMaxOneParameter( $params, 'prop', 'generatexml' ); + + if ( $params['prop'] === null ) { + $this->logFeatureUsage( 'action=expandtemplates&!prop' ); + $this->setWarning( 'Because no values have been specified for the prop parameter, a ' . + 'legacy format has been used for the output. This format is deprecated, and in ' . + 'the future, a default value will be set for the prop parameter, causing the new' . + 'format to always be used.' ); + $prop = array(); + } else { + $prop = array_flip( $params['prop'] ); + } // Create title for parser $title_obj = Title::newFromText( $params['title'] ); @@ -56,7 +68,13 @@ class ApiExpandTemplates extends ApiBase { $options->setRemoveComments( false ); } - if ( $params['generatexml'] ) { + $retval = array(); + + if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) { + if ( !isset( $prop['parsetree'] ) ) { + $this->logFeatureUsage( 'action=expandtemplates&generatexml' ); + } + $wgParser->startExternalParse( $title_obj, $options, OT_PREPROCESS ); $dom = $wgParser->preprocessToDom( $params['text'] ); if ( is_callable( array( $dom, 'saveXML' ) ) ) { @@ -64,16 +82,54 @@ class ApiExpandTemplates extends ApiBase { } else { $xml = $dom->__toString(); } - $xml_result = array(); - ApiResult::setContent( $xml_result, $xml ); - $result->addValue( null, 'parsetree', $xml_result ); + if ( isset( $prop['parsetree'] ) ) { + unset( $prop['parsetree'] ); + $retval['parsetree'] = $xml; + } else { + // the old way + $xml_result = array(); + ApiResult::setContent( $xml_result, $xml ); + $result->addValue( null, 'parsetree', $xml_result ); + } } - $retval = $wgParser->preprocess( $params['text'], $title_obj, $options ); - // Return result - $retval_array = array(); - ApiResult::setContent( $retval_array, $retval ); - $result->addValue( null, $this->getModuleName(), $retval_array ); + // if they didn't want any output except (probably) the parse tree, + // then don't bother actually fully expanding it + if ( $prop || $params['prop'] === null ) { + $wgParser->startExternalParse( $title_obj, $options, OT_PREPROCESS ); + $frame = $wgParser->getPreprocessor()->newFrame(); + $wikitext = $wgParser->preprocess( $params['text'], $title_obj, $options, null, $frame ); + if ( $params['prop'] === null ) { + // the old way + ApiResult::setContent( $retval, $wikitext ); + } else { + if ( isset( $prop['categories'] ) ) { + $categories = $wgParser->getOutput()->getCategories(); + if ( !empty( $categories ) ) { + $categories_result = array(); + foreach ( $categories as $category => $sortkey ) { + $entry = array(); + $entry['sortkey'] = $sortkey; + ApiResult::setContent( $entry, $category ); + $categories_result[] = $entry; + } + $result->setIndexedTagName( $categories_result, 'category' ); + $retval['categories'] = $categories_result; + } + } + if ( isset( $prop['volatile'] ) && $frame->isVolatile() ) { + $retval['volatile'] = ''; + } + if ( isset( $prop['ttl'] ) && $frame->getTTL() !== null ) { + $retval['ttl'] = $frame->getTTL(); + } + if ( isset( $prop['wikitext'] ) ) { + $retval['wikitext'] = $wikitext; + } + } + } + $result->setSubelements( $retval, array( 'wikitext', 'parsetree' ) ); + $result->addValue( null, $this->getModuleName(), $retval ); } public function getAllowedParams() { @@ -85,8 +141,21 @@ class ApiExpandTemplates extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true, ), - 'generatexml' => false, + 'prop' => array( + ApiBase::PARAM_TYPE => array( + 'wikitext', + 'categories', + 'volatile', + 'ttl', + 'parsetree', + ), + ApiBase::PARAM_ISMULTI => true, + ), 'includecomments' => false, + 'generatexml' => array( + ApiBase::PARAM_TYPE => 'boolean', + ApiBase::PARAM_DEPRECATED => true, + ), ); } @@ -94,27 +163,26 @@ class ApiExpandTemplates extends ApiBase { return array( 'text' => 'Wikitext to convert', 'title' => 'Title of page', - 'generatexml' => 'Generate XML parse tree', + 'prop' => array( + 'Which pieces of information to get', + ' wikitext - The expanded wikitext', + ' categories - Any categories present in the input that are not represented in ' . + 'the wikitext output', + ' volatile - Whether the output is volatile and should not be reused ' . + 'elsewhere within the page', + ' ttl - The maximum time after which caches of the result should be ' . + 'invalidated', + ' parsetree - The XML parse tree of the input', + 'Note that if no values are selected, the result will contain the wikitext,', + 'but the output will be in a deprecated format.', + ), 'includecomments' => 'Whether to include HTML comments in the output', - ); - } - - public function getResultProperties() { - return array( - '' => array( - '*' => 'string' - ) + 'generatexml' => 'Generate XML parse tree (replaced by prop=parsetree)', ); } public function getDescription() { - return 'Expands all templates in wikitext'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'invalidtitle', 'title' ), - ) ); + return 'Expands all templates in wikitext.'; } public function getExamples() { diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index 05691093..374203eb 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -41,30 +41,29 @@ class ApiFeedContributions extends ApiBase { public function execute() { $params = $this->extractRequestParams(); - global $wgFeed, $wgFeedClasses, $wgSitename, $wgLanguageCode; - - if ( !$wgFeed ) { + $config = $this->getConfig(); + if ( !$config->get( 'Feed' ) ) { $this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' ); } - if ( !isset( $wgFeedClasses[$params['feedformat']] ) ) { + $feedClasses = $config->get( 'FeedClasses' ); + if ( !isset( $feedClasses[$params['feedformat']] ) ) { $this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' ); } - global $wgMiserMode; - if ( $params['showsizediff'] && $wgMiserMode ) { + if ( $params['showsizediff'] && $this->getConfig()->get( 'MiserMode' ) ) { $this->dieUsage( 'Size difference is disabled in Miser Mode', 'sizediffdisabled' ); } $msg = wfMessage( 'Contributions' )->inContentLanguage()->text(); - $feedTitle = $wgSitename . ' - ' . $msg . ' [' . $wgLanguageCode . ']'; + $feedTitle = $config->get( 'Sitename' ) . ' - ' . $msg . ' [' . $config->get( 'LanguageCode' ) . ']'; $feedUrl = SpecialPage::getTitleFor( 'Contributions', $params['user'] )->getFullURL(); $target = $params['user'] == 'newbies' - ? 'newbies' - : Title::makeTitleSafe( NS_USER, $params['user'] )->getText(); + ? 'newbies' + : Title::makeTitleSafe( NS_USER, $params['user'] )->getText(); - $feed = new $wgFeedClasses[$params['feedformat']] ( + $feed = new $feedClasses[$params['feedformat']] ( $feedTitle, htmlspecialchars( $msg ), $feedUrl @@ -78,12 +77,24 @@ class ApiFeedContributions extends ApiBase { 'tagFilter' => $params['tagfilter'], 'deletedOnly' => $params['deletedonly'], 'topOnly' => $params['toponly'], + 'newOnly' => $params['newonly'], 'showSizeDiff' => $params['showsizediff'], ) ); + $feedLimit = $this->getConfig()->get( 'FeedLimit' ); + if ( $pager->getLimit() > $feedLimit ) { + $pager->setLimit( $feedLimit ); + } + $feedItems = array(); if ( $pager->getNumRows() > 0 ) { + $count = 0; + $limit = $pager->getLimit(); foreach ( $pager->mResult as $row ) { + // ContribsPager selects one more row for navigation, skip that row + if ( ++$count > $limit ) { + break; + } $feedItems[] = $this->feedItem( $row ); } } @@ -101,17 +112,18 @@ class ApiFeedContributions extends ApiBase { return new FeedItem( $title->getPrefixedText(), $this->feedItemDesc( $revision ), - $title->getFullURL(), + $title->getFullURL( array( 'diff' => $revision->getId() ) ), $date, $this->feedItemAuthor( $revision ), $comments ); } + return null; } /** - * @param $revision Revision + * @param Revision $revision * @return string */ protected function feedItemAuthor( $revision ) { @@ -119,7 +131,7 @@ class ApiFeedContributions extends ApiBase { } /** - * @param $revision Revision + * @param Revision $revision * @return string */ protected function feedItemDesc( $revision ) { @@ -142,12 +154,13 @@ class ApiFeedContributions extends ApiBase { htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) . "</p>\n<hr />\n<div>" . $html . "</div>"; } + return ''; } public function getAllowedParams() { - global $wgFeedClasses; - $feedFormatNames = array_keys( $wgFeedClasses ); + $feedFormatNames = array_keys( $this->getConfig()->get( 'FeedClasses' ) ); + return array( 'feedformat' => array( ApiBase::PARAM_DFLT => 'rss', @@ -173,6 +186,7 @@ class ApiFeedContributions extends ApiBase { ), 'deletedonly' => false, 'toponly' => false, + 'newonly' => false, 'showsizediff' => false, ); } @@ -187,20 +201,13 @@ class ApiFeedContributions extends ApiBase { 'tagfilter' => 'Filter contributions that have these tags', 'deletedonly' => 'Show only deleted contributions', 'toponly' => 'Only show edits that are latest revisions', + 'newonly' => 'Only show edits that are page creations', 'showsizediff' => 'Show the size difference between revisions. Disabled in Miser Mode', ); } public function getDescription() { - return 'Returns a user contributions feed'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'feed-unavailable', 'info' => 'Syndication feeds are not available' ), - array( 'code' => 'feed-invalid', 'info' => 'Invalid subscription feed type' ), - array( 'code' => 'sizediffdisabled', 'info' => 'Size difference is disabled in Miser Mode' ), - ) ); + return 'Returns a user contributions feed.'; } public function getExamples() { diff --git a/includes/api/ApiFeedRecentChanges.php b/includes/api/ApiFeedRecentChanges.php new file mode 100644 index 00000000..7239a296 --- /dev/null +++ b/includes/api/ApiFeedRecentChanges.php @@ -0,0 +1,207 @@ +<?php +/** + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.23 + */ + +/** + * Recent changes feed. + * + * @ingroup API + */ +class ApiFeedRecentChanges extends ApiBase { + + /** + * This module uses a custom feed wrapper printer. + * + * @return ApiFormatFeedWrapper + */ + public function getCustomPrinter() { + return new ApiFormatFeedWrapper( $this->getMain() ); + } + + /** + * Format the rows (generated by SpecialRecentchanges or SpecialRecentchangeslinked) + * as an RSS/Atom feed. + */ + public function execute() { + $config = $this->getConfig(); + + $this->params = $this->extractRequestParams(); + + if ( !$config->get( 'Feed' ) ) { + $this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' ); + } + + $feedClasses = $config->get( 'FeedClasses' ); + if ( !isset( $feedClasses[$this->params['feedformat']] ) ) { + $this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' ); + } + + $this->getMain()->setCacheMode( 'public' ); + if ( !$this->getMain()->getParameter( 'smaxage' ) ) { + // bug 63249: This page gets hit a lot, cache at least 15 seconds. + $this->getMain()->setCacheMaxAge( 15 ); + } + + $feedFormat = $this->params['feedformat']; + $specialClass = $this->params['target'] !== null + ? 'SpecialRecentchangeslinked' + : 'SpecialRecentchanges'; + + $formatter = $this->getFeedObject( $feedFormat, $specialClass ); + + // Everything is passed implicitly via $wgRequest… :( + // The row-getting functionality should maybe be factored out of ChangesListSpecialPage too… + $rc = new $specialClass(); + $rows = $rc->getRows(); + + $feedItems = $rows ? ChangesFeed::buildItems( $rows ) : array(); + + ApiFormatFeedWrapper::setResult( $this->getResult(), $formatter, $feedItems ); + } + + /** + * Return a ChannelFeed object. + * + * @param string $feedFormat Feed's format (either 'rss' or 'atom') + * @param string $specialClass Relevant special page name (either 'SpecialRecentchanges' or + * 'SpecialRecentchangeslinked') + * @return ChannelFeed + */ + public function getFeedObject( $feedFormat, $specialClass ) { + if ( $specialClass === 'SpecialRecentchangeslinked' ) { + $title = Title::newFromText( $this->params['target'] ); + if ( !$title ) { + $this->dieUsageMsg( array( 'invalidtitle', $this->params['target'] ) ); + } + + $feed = new ChangesFeed( $feedFormat, false ); + $feedObj = $feed->getFeedObject( + $this->msg( 'recentchangeslinked-title', $title->getPrefixedText() ) + ->inContentLanguage()->text(), + $this->msg( 'recentchangeslinked-feed' )->inContentLanguage()->text(), + SpecialPage::getTitleFor( 'Recentchangeslinked' )->getFullURL() + ); + } else { + $feed = new ChangesFeed( $feedFormat, 'rcfeed' ); + $feedObj = $feed->getFeedObject( + $this->msg( 'recentchanges' )->inContentLanguage()->text(), + $this->msg( 'recentchanges-feed-description' )->inContentLanguage()->text(), + SpecialPage::getTitleFor( 'Recentchanges' )->getFullURL() + ); + } + + return $feedObj; + } + + public function getAllowedParams() { + $config = $this->getConfig(); + $feedFormatNames = array_keys( $config->get( 'FeedClasses' ) ); + + $ret = array( + 'feedformat' => array( + ApiBase::PARAM_DFLT => 'rss', + ApiBase::PARAM_TYPE => $feedFormatNames, + ), + + 'namespace' => array( + ApiBase::PARAM_TYPE => 'namespace', + ), + 'invert' => false, + 'associated' => false, + + 'days' => array( + ApiBase::PARAM_DFLT => 7, + ApiBase::PARAM_MIN => 1, + ApiBase::PARAM_TYPE => 'integer', + ), + 'limit' => array( + ApiBase::PARAM_DFLT => 50, + ApiBase::PARAM_MIN => 1, + ApiBase::PARAM_MAX => $config->get( 'FeedLimit' ), + ApiBase::PARAM_TYPE => 'integer', + ), + 'from' => array( + ApiBase::PARAM_TYPE => 'timestamp', + ), + + 'hideminor' => false, + 'hidebots' => false, + 'hideanons' => false, + 'hideliu' => false, + 'hidepatrolled' => false, + 'hidemyself' => false, + + 'tagfilter' => array( + ApiBase::PARAM_TYPE => 'string', + ), + + 'target' => array( + ApiBase::PARAM_TYPE => 'string', + ), + 'showlinkedto' => false, + ); + + if ( $config->get( 'AllowCategorizedRecentChanges' ) ) { + $ret += array( + 'categories' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_ISMULTI => true, + ), + 'categories_any' => false, + ); + } + + return $ret; + } + + public function getParamDescription() { + return array( + 'feedformat' => 'The format of the feed', + 'namespace' => 'Namespace to limit the results to', + 'invert' => 'All namespaces but the selected one', + 'associated' => 'Include associated (talk or main) namespace', + 'days' => 'Days to limit the results to', + 'limit' => 'Maximum number of results to return', + 'from' => 'Show changes since then', + 'hideminor' => 'Hide minor changes', + 'hidebots' => 'Hide changes made by bots', + 'hideanons' => 'Hide changes made by anonymous users', + 'hideliu' => 'Hide changes made by registered users', + 'hidepatrolled' => 'Hide patrolled changes', + 'hidemyself' => 'Hide changes made by yourself', + 'tagfilter' => 'Filter by tag', + 'target' => 'Show only changes on pages linked from this page', + 'showlinkedto' => 'Show changes on pages linked to the selected page instead', + 'categories' => 'Show only changes on pages in all of these categories', + 'categories_any' => 'Show only changes on pages in any of the categories instead', + ); + } + + public function getDescription() { + return 'Returns a recent changes feed'; + } + + public function getExamples() { + return array( + 'api.php?action=feedrecentchanges', + 'api.php?action=feedrecentchanges&days=30' + ); + } +} diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index fbb70fbc..6aef8fc2 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -34,7 +34,6 @@ class ApiFeedWatchlist extends ApiBase { private $watchlistModule = null; - private $linkToDiffs = false; private $linkToSections = false; /** @@ -51,16 +50,16 @@ class ApiFeedWatchlist extends ApiBase { * Wrap the result as an RSS/Atom feed. */ public function execute() { - global $wgFeed, $wgFeedClasses, $wgFeedLimit, $wgSitename, $wgLanguageCode; - + $config = $this->getConfig(); + $feedClasses = $config->get( 'FeedClasses' ); try { $params = $this->extractRequestParams(); - if ( !$wgFeed ) { + if ( !$config->get( 'Feed' ) ) { $this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' ); } - if ( !isset( $wgFeedClasses[$params['feedformat']] ) ) { + if ( !isset( $feedClasses[$params['feedformat']] ) ) { $this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' ); } @@ -73,10 +72,10 @@ class ApiFeedWatchlist extends ApiBase { 'meta' => 'siteinfo', 'siprop' => 'general', 'list' => 'watchlist', - 'wlprop' => 'title|user|comment|timestamp', + 'wlprop' => 'title|user|comment|timestamp|ids', 'wldir' => 'older', // reverse order - from newest to oldest 'wlend' => $endTime, // stop at this time - 'wllimit' => min( 50, $wgFeedLimit ) + 'wllimit' => min( 50, $this->getConfig()->get( 'FeedLimit' ) ) ); if ( $params['wlowner'] !== null ) { @@ -95,12 +94,6 @@ class ApiFeedWatchlist extends ApiBase { $fauxReqArr['wltype'] = $params['wltype']; } - // Support linking to diffs instead of article - if ( $params['linktodiffs'] ) { - $this->linkToDiffs = true; - $fauxReqArr['wlprop'] .= '|ids'; - } - // Support linking directly to sections when possible // (possible only if section name is present in comment) if ( $params['linktosections'] ) { @@ -129,24 +122,29 @@ class ApiFeedWatchlist extends ApiBase { $msg = wfMessage( 'watchlist' )->inContentLanguage()->text(); - $feedTitle = $wgSitename . ' - ' . $msg . ' [' . $wgLanguageCode . ']'; + $feedTitle = $this->getConfig()->get( 'Sitename' ) . ' - ' . $msg . ' [' . $this->getConfig()->get( 'LanguageCode' ) . ']'; $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL(); - $feed = new $wgFeedClasses[$params['feedformat']] ( $feedTitle, htmlspecialchars( $msg ), $feedUrl ); + $feed = new $feedClasses[$params['feedformat']] ( + $feedTitle, + htmlspecialchars( $msg ), + $feedUrl + ); ApiFormatFeedWrapper::setResult( $this->getResult(), $feed, $feedItems ); - } catch ( Exception $e ) { - // Error results should not be cached $this->getMain()->setCacheMaxAge( 0 ); - $feedTitle = $wgSitename . ' - Error - ' . wfMessage( 'watchlist' )->inContentLanguage()->text() . ' [' . $wgLanguageCode . ']'; + // @todo FIXME: Localise brackets + $feedTitle = $this->getConfig()->get( 'Sitename' ) . ' - Error - ' . + wfMessage( 'watchlist' )->inContentLanguage()->text() . + ' [' . $this->getConfig()->get( 'LanguageCode' ) . ']'; $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL(); $feedFormat = isset( $params['feedformat'] ) ? $params['feedformat'] : 'rss'; $msg = wfMessage( 'watchlist' )->inContentLanguage()->escaped(); - $feed = new $wgFeedClasses[$feedFormat] ( $feedTitle, $msg, $feedUrl ); + $feed = new $feedClasses[$feedFormat] ( $feedTitle, $msg, $feedUrl ); if ( $e instanceof UsageException ) { $errorCode = $e->getCodeString(); @@ -162,13 +160,13 @@ class ApiFeedWatchlist extends ApiBase { } /** - * @param $info array + * @param array $info * @return FeedItem */ private function createFeedItem( $info ) { $titleStr = $info['title']; $title = Title::newFromText( $titleStr ); - if ( $this->linkToDiffs && isset( $info['revid'] ) ) { + if ( isset( $info['revid'] ) ) { $titleUrl = $title->getFullURL( array( 'diff' => $info['revid'] ) ); } else { $titleUrl = $title->getFullURL(); @@ -179,8 +177,11 @@ class ApiFeedWatchlist extends ApiBase { // The anchor won't work for sections that have dupes on page // as there's no way to strip that info from ApiWatchlist (apparently?). // RegExp in the line below is equal to Linker::formatAutocomments(). - if ( $this->linkToSections && $comment !== null && preg_match( '!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment, $matches ) ) { + if ( $this->linkToSections && $comment !== null && + preg_match( '!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment, $matches ) + ) { global $wgParser; + $sectionTitle = $wgParser->stripSectionName( $matches[2] ); $sectionTitle = Sanitizer::normalizeSectionNameWhitespace( $sectionTitle ); $titleUrl .= Title::newFromText( '#' . $sectionTitle )->getFragmentForURL(); @@ -199,12 +200,12 @@ class ApiFeedWatchlist extends ApiBase { $this->watchlistModule = $this->getMain()->getModuleManager()->getModule( 'query' ) ->getModuleManager()->getModule( 'watchlist' ); } + return $this->watchlistModule; } public function getAllowedParams( $flags = 0 ) { - global $wgFeedClasses; - $feedFormatNames = array_keys( $wgFeedClasses ); + $feedFormatNames = array_keys( $this->getConfig()->get( 'FeedClasses' ) ); $ret = array( 'feedformat' => array( ApiBase::PARAM_DFLT => 'rss', @@ -216,7 +217,6 @@ class ApiFeedWatchlist extends ApiBase { ApiBase::PARAM_MIN => 1, ApiBase::PARAM_MAX => 72, ), - 'linktodiffs' => false, 'linktosections' => false, ); if ( $flags ) { @@ -235,15 +235,16 @@ class ApiFeedWatchlist extends ApiBase { $ret['wltype'] = null; $ret['wlexcludeuser'] = null; } + return $ret; } public function getParamDescription() { $wldescr = $this->getWatchlistModule()->getParamDescription(); + return array( 'feedformat' => 'The format of the feed', 'hours' => 'List pages modified within this many hours from now', - 'linktodiffs' => 'Link to change differences instead of article pages', 'linktosections' => 'Link directly to changed sections if possible', 'allrev' => $wldescr['allrev'], 'wlowner' => $wldescr['owner'], @@ -255,20 +256,13 @@ class ApiFeedWatchlist extends ApiBase { } public function getDescription() { - return 'Returns a watchlist feed'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'feed-unavailable', 'info' => 'Syndication feeds are not available' ), - array( 'code' => 'feed-invalid', 'info' => 'Invalid subscription feed type' ), - ) ); + return 'Returns a watchlist feed.'; } public function getExamples() { return array( 'api.php?action=feedwatchlist', - 'api.php?action=feedwatchlist&allrev=&linktodiffs=&hours=6' + 'api.php?action=feedwatchlist&allrev=&hours=6' ); } diff --git a/includes/api/ApiFileRevert.php b/includes/api/ApiFileRevert.php index cbb2ba6a..f518e172 100644 --- a/includes/api/ApiFileRevert.php +++ b/includes/api/ApiFileRevert.php @@ -28,13 +28,13 @@ * @ingroup API */ class ApiFileRevert extends ApiBase { - - /** - * @var File - */ + /** @var LocalFile */ protected $file; + + /** @var string */ protected $archiveName; + /** @var array */ protected $params; public function execute() { @@ -46,7 +46,15 @@ class ApiFileRevert extends ApiBase { $this->checkPermissions( $this->getUser() ); $sourceUrl = $this->file->getArchiveVirtualUrl( $this->archiveName ); - $status = $this->file->upload( $sourceUrl, $this->params['comment'], $this->params['comment'], 0, false, false, $this->getUser() ); + $status = $this->file->upload( + $sourceUrl, + $this->params['comment'], + $this->params['comment'], + 0, + false, + false, + $this->getUser() + ); if ( $status->isGood() ) { $result = array( 'result' => 'Success' ); @@ -58,13 +66,12 @@ class ApiFileRevert extends ApiBase { } $this->getResult()->addValue( null, $this->getModuleName(), $result ); - } /** * Checks that the user has permissions to perform this revert. * Dies with usage message on inadequate permissions. - * @param $user User The user to check. + * @param User $user The user to check. */ protected function checkPermissions( $user ) { $title = $this->file->getTitle(); @@ -125,69 +132,31 @@ class ApiFileRevert extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true, ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), ); - } public function getParamDescription() { return array( 'filename' => 'Target filename without the File: prefix', - 'token' => 'Edit token. You can get one of these through prop=info', 'comment' => 'Upload comment', 'archivename' => 'Archive name of the revision to revert to', ); } - public function getResultProperties() { - return array( - '' => array( - 'result' => array( - ApiBase::PROP_TYPE => array( - 'Success', - 'Failure' - ) - ), - 'errors' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return array( - 'Revert a file to an old version' - ); - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - array( - array( 'mustbeloggedin', 'upload' ), - array( 'badaccess-groups' ), - array( 'invalidtitle', 'title' ), - array( 'notanarticle' ), - array( 'filerevert-badversion' ), - ) + 'Revert a file to an old version.' ); } public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { return array( - 'api.php?action=filerevert&filename=Wiki.png&comment=Revert&archivename=20110305152740!Wiki.png&token=123ABC' + 'api.php?action=filerevert&filename=Wiki.png&comment=Revert&' . + 'archivename=20110305152740!Wiki.png&token=123ABC' => 'Revert Wiki.png to the version of 20110305152740', ); } diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index 70495439..9165ce88 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -30,22 +30,20 @@ * @ingroup API */ abstract class ApiFormatBase extends ApiBase { - private $mIsHtml, $mFormat, $mUnescapeAmps, $mHelp, $mCleared; private $mBufferResult = false, $mBuffer, $mDisabled = false; /** - * Constructor * If $format ends with 'fm', pretty-print the output in HTML. - * @param $main ApiMain + * @param ApiMain $main * @param string $format Format name */ - public function __construct( $main, $format ) { + public function __construct( ApiMain $main, $format ) { parent::__construct( $main, $format ); - $this->mIsHtml = ( substr( $format, - 2, 2 ) === 'fm' ); // ends with 'fm' + $this->mIsHtml = ( substr( $format, -2, 2 ) === 'fm' ); // ends with 'fm' if ( $this->mIsHtml ) { - $this->mFormat = substr( $format, 0, - 2 ); // remove ending 'fm' + $this->mFormat = substr( $format, 0, -2 ); // remove ending 'fm' } else { $this->mFormat = $format; } @@ -54,7 +52,7 @@ abstract class ApiFormatBase extends ApiBase { } /** - * Overriding class returns the mime type that should be sent to the client. + * Overriding class returns the MIME type that should be sent to the client. * This method is not called if getIsHtml() returns true. * @return string */ @@ -122,6 +120,16 @@ abstract class ApiFormatBase extends ApiBase { } /** + * Whether this formatter can handle printing API errors. If this returns + * false, then on API errors the default printer will be instantiated. + * @since 1.23 + * @return bool + */ + public function canPrintErrors() { + return true; + } + + /** * Initialize the printer function and prepare the output headers, etc. * This method must be the first outputting method during execution. * A human-targeted notice about available formats is printed for the HTML-based output, @@ -146,9 +154,9 @@ abstract class ApiFormatBase extends ApiBase { $this->getMain()->getRequest()->response()->header( "Content-Type: $mime; charset=utf-8" ); //Set X-Frame-Options API results (bug 39180) - global $wgApiFrameOptions; - if ( $wgApiFrameOptions ) { - $this->getMain()->getRequest()->response()->header( "X-Frame-Options: $wgApiFrameOptions" ); + $apiFrameOptions = $this->getConfig()->get( 'ApiFrameOptions' ); + if ( $apiFrameOptions ) { + $this->getMain()->getRequest()->response()->header( "X-Frame-Options: $apiFrameOptions" ); } if ( $isHtml ) { @@ -156,17 +164,20 @@ abstract class ApiFormatBase extends ApiBase { <!DOCTYPE HTML> <html> <head> -<?php if ( $this->mUnescapeAmps ) { +<?php + if ( $this->mUnescapeAmps ) { ?> <title>MediaWiki API</title> -<?php } else { +<?php + } else { ?> <title>MediaWiki API Result</title> -<?php } ?> +<?php + } +?> </head> <body> <?php - - if ( !$isHelpScreen ) { +// @codingStandardsIgnoreStart Exclude long line from CodeSniffer checks ?> <br /> <small> @@ -179,15 +190,14 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, </small> <pre style='white-space: pre-wrap;'> <?php - - - } else { // don't wrap the contents of the <pre> for help screens - // because these are actually formatted to rely on - // the monospaced font for layout purposes +// @codingStandardsIgnoreEnd + // don't wrap the contents of the <pre> for help screens + // because these are actually formatted to rely on + // the monospaced font for layout purposes + } else { ?> <pre> <?php - } } } @@ -206,8 +216,6 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, </body> </html> <?php - - } } @@ -215,7 +223,7 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, * 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 + * @param string $text */ public function printText( $text ) { if ( $this->mDisabled ) { @@ -239,6 +247,7 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, /** * Get the contents of the buffer. + * @return string */ public function getBuffer() { return $this->mBuffer; @@ -246,7 +255,7 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, /** * Set the flag to buffer the result instead of printing it. - * @param $value bool + * @param bool $value */ public function setBufferResult( $value ) { $this->mBufferResult = $value; @@ -254,7 +263,7 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, /** * Sets whether the pretty-printer should format *bold* - * @param $help bool + * @param bool $help */ public function setHelp( $help = true ) { $this->mHelp = $help; @@ -263,7 +272,7 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, /** * Pretty-print various elements in HTML format, such as xml tags and * URLs. This method also escapes characters like < - * @param $text string + * @param string $text * @return string */ protected function formatHTML( $text ) { @@ -276,8 +285,8 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, // identify requests to api.php $text = preg_replace( '#^(\s*)(api\.php\?[^ <\n\t]+)$#m', '\1<a href="\2">\2</a>', $text ); if ( $this->mHelp ) { - // make strings inside * bold - $text = preg_replace( "#\\*[^<>\n]+\\*#", '<b>\\0</b>', $text ); + // make lines inside * bold + $text = preg_replace( '#^(\s*)(\*[^<>\n]+\*)(\s*)$#m', '$1<b>$2</b>$3', $text ); } // Armor links (bug 61362) @@ -291,7 +300,11 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, // identify URLs $protos = wfUrlProtocolsWithoutProtRel(); // This regex hacks around bug 13218 (" included in the URL) - $text = preg_replace( "#(((?i)$protos).*?)(")?([ \\'\"<>\n]|<|>|")#", '<a href="\\1">\\1</a>\\3\\4', $text ); + $text = preg_replace( + "#(((?i)$protos).*?)(")?([ \\'\"<>\n]|<|>|")#", + '<a href="\\1">\\1</a>\\3\\4', + $text + ); // Unarmor links $text = preg_replace_callback( '#<([0-9a-f]{40})>#', function ( $matches ) use ( &$masked ) { @@ -326,73 +339,16 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, public function getDescription() { return $this->getIsHtml() ? ' (pretty-print in HTML)' : ''; } -} - -/** - * This printer is used to wrap an instance of the Feed class - * @ingroup API - */ -class ApiFormatFeedWrapper extends ApiFormatBase { - - public function __construct( $main ) { - parent::__construct( $main, 'feed' ); - } - - /** - * Call this method to initialize output data. See execute() - * @param $result ApiResult - * @param $feed object an instance of one of the $wgFeedClasses classes - * @param array $feedItems 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 occurred - // 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(); - } - - /** - * Feed does its own headers - * - * @return null - */ - public function getMimeType() { - return null; - } - - /** - * Optimization - no need to sanitize data that will not be needed - * - * @return bool - */ - public function getNeedsRawData() { - return true; - } /** - * 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 + * To avoid code duplication with the deprecation of dbg, dump, txt, wddx, + * and yaml, this method is added to do the necessary work. It should be + * removed when those deprecated formats are removed. */ - public function execute() { - $data = $this->getResultData(); - if ( isset( $data['_feed'] ) && isset( $data['_feeditems'] ) ) { - $feed = $data['_feed']; - $items = $data['_feeditems']; - - $feed->outHeader(); - foreach ( $items as & $item ) { - $feed->outItem( $item ); - } - $feed->outFooter(); - } else { - // Error has occurred, print something useful - ApiBase::dieDebug( __METHOD__, 'Invalid feed class/item' ); - } + protected function markDeprecated() { + $fm = $this->getIsHtml() ? 'fm' : ''; + $name = $this->getModuleName(); + $this->logFeatureUsage( "format=$name" ); + $this->setWarning( "format=$name has been deprecated. Please use format=json$fm instead." ); } } diff --git a/includes/api/ApiFormatDbg.php b/includes/api/ApiFormatDbg.php index 1b2e02c9..5ec518b3 100644 --- a/includes/api/ApiFormatDbg.php +++ b/includes/api/ApiFormatDbg.php @@ -26,6 +26,7 @@ /** * API PHP's var_export() output formatter + * @deprecated since 1.24 * @ingroup API */ class ApiFormatDbg extends ApiFormatBase { @@ -38,10 +39,11 @@ class ApiFormatDbg extends ApiFormatBase { } public function execute() { + $this->markDeprecated(); $this->printText( var_export( $this->getResultData(), true ) ); } public function getDescription() { - return 'Output data in PHP\'s var_export() format' . parent::getDescription(); + return 'DEPRECATED! Output data in PHP\'s var_export() format' . parent::getDescription(); } } diff --git a/includes/api/ApiFormatDump.php b/includes/api/ApiFormatDump.php index 62253e14..d4c7cab4 100644 --- a/includes/api/ApiFormatDump.php +++ b/includes/api/ApiFormatDump.php @@ -26,6 +26,7 @@ /** * API PHP's var_dump() output formatter + * @deprecated since 1.24 * @ingroup API */ class ApiFormatDump extends ApiFormatBase { @@ -38,6 +39,7 @@ class ApiFormatDump extends ApiFormatBase { } public function execute() { + $this->markDeprecated(); ob_start(); var_dump( $this->getResultData() ); $result = ob_get_contents(); @@ -46,6 +48,6 @@ class ApiFormatDump extends ApiFormatBase { } public function getDescription() { - return 'Output data in PHP\'s var_dump() format' . parent::getDescription(); + return 'DEPRECATED! Output data in PHP\'s var_dump() format' . parent::getDescription(); } } diff --git a/includes/api/ApiFormatFeedWrapper.php b/includes/api/ApiFormatFeedWrapper.php new file mode 100644 index 00000000..92600067 --- /dev/null +++ b/includes/api/ApiFormatFeedWrapper.php @@ -0,0 +1,101 @@ +<?php +/** + * + * + * Created on Sep 19, 2006 + * + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" + * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * This printer is used to wrap an instance of the Feed class + * @ingroup API + */ +class ApiFormatFeedWrapper extends ApiFormatBase { + + public function __construct( ApiMain $main ) { + parent::__construct( $main, 'feed' ); + } + + /** + * Call this method to initialize output data. See execute() + * @param ApiResult $result + * @param object $feed An instance of one of the $wgFeedClasses classes + * @param array $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 occurred + // Disable size checking for this because we can't continue + // cleanly; size checking would cause more problems than it'd + // solve + $result->addValue( null, '_feed', $feed, ApiResult::NO_SIZE_CHECK ); + $result->addValue( null, '_feeditems', $feedItems, ApiResult::NO_SIZE_CHECK ); + } + + /** + * Feed does its own headers + * + * @return null + */ + public function getMimeType() { + return null; + } + + /** + * Optimization - no need to sanitize data that will not be needed + * + * @return bool + */ + public function getNeedsRawData() { + return true; + } + + /** + * ChannelFeed doesn't give us a method to print errors in a friendly + * manner, so just punt errors to the default printer. + * @return bool + */ + public function canPrintErrors() { + return false; + } + + /** + * 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 + */ + public function execute() { + $data = $this->getResultData(); + if ( isset( $data['_feed'] ) && isset( $data['_feeditems'] ) ) { + $feed = $data['_feed']; + $items = $data['_feeditems']; + + $feed->outHeader(); + foreach ( $items as & $item ) { + $feed->outItem( $item ); + } + $feed->outFooter(); + } else { + // Error has occurred, print something useful + ApiBase::dieDebug( __METHOD__, 'Invalid feed class/item' ); + } + } +} diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index 47d82124..d9f9d46a 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -32,7 +32,7 @@ class ApiFormatJson extends ApiFormatBase { private $mIsRaw; - public function __construct( $main, $format ) { + public function __construct( ApiMain $main, $format ) { parent::__construct( $main, $format ); $this->mIsRaw = ( $format === 'rawfm' ); } @@ -43,6 +43,7 @@ class ApiFormatJson extends ApiFormatBase { if ( $params['callback'] ) { return 'text/javascript'; } + return 'application/json'; } @@ -92,16 +93,18 @@ class ApiFormatJson extends ApiFormatBase { public function getParamDescription() { return array( - 'callback' => 'If specified, wraps the output into a given function call. For safety, all user-specific data will be restricted.', - 'utf8' => 'If specified, encodes most (but not all) non-ASCII characters as UTF-8 instead of replacing them with hexadecimal escape sequences.', + 'callback' => 'If specified, wraps the output into a given function ' . + 'call. For safety, all user-specific data will be restricted.', + 'utf8' => 'If specified, encodes most (but not all) non-ASCII ' . + 'characters as UTF-8 instead of replacing them with hexadecimal escape sequences.', ); } public function getDescription() { if ( $this->mIsRaw ) { return 'Output data with the debugging elements in JSON format' . parent::getDescription(); - } else { - return 'Output data in JSON format' . parent::getDescription(); } + + return 'Output data in JSON format' . parent::getDescription(); } } diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php index bda1c180..73ce80ef 100644 --- a/includes/api/ApiFormatPhp.php +++ b/includes/api/ApiFormatPhp.php @@ -35,14 +35,13 @@ class ApiFormatPhp extends ApiFormatBase { } public function execute() { - global $wgMangleFlashPolicy; $text = serialize( $this->getResultData() ); // Bug 66776: wfMangleFlashPolicy() is needed to avoid a nasty bug in // Flash, but what it does isn't friendly for the API. There's nothing // we can do here that isn't actively broken in some manner, so let's // just be broken in a useful manner. - if ( $wgMangleFlashPolicy && + if ( $this->getConfig()->get( 'MangleFlashPolicy' ) && in_array( 'wfOutputHandler', ob_list_handlers(), true ) && preg_match( '/\<\s*cross-domain-policy\s*\>/i', $text ) ) { diff --git a/includes/api/ApiFormatRaw.php b/includes/api/ApiFormatRaw.php index d278efa0..3f5c8b73 100644 --- a/includes/api/ApiFormatRaw.php +++ b/includes/api/ApiFormatRaw.php @@ -31,11 +31,10 @@ class ApiFormatRaw extends ApiFormatBase { /** - * Constructor - * @param $main ApiMain object - * @param $errorFallback ApiFormatBase object to fall back on for errors + * @param ApiMain $main + * @param ApiFormatBase $errorFallback Object to fall back on for errors */ - public function __construct( $main, $errorFallback ) { + public function __construct( ApiMain $main, ApiFormatBase $errorFallback ) { parent::__construct( $main, 'raw' ); $this->mErrorFallback = $errorFallback; } @@ -58,6 +57,7 @@ class ApiFormatRaw extends ApiFormatBase { $data = $this->getResultData(); if ( isset( $data['error'] ) ) { $this->mErrorFallback->execute(); + return; } diff --git a/includes/api/ApiFormatTxt.php b/includes/api/ApiFormatTxt.php index 4130e70c..c451ed77 100644 --- a/includes/api/ApiFormatTxt.php +++ b/includes/api/ApiFormatTxt.php @@ -26,6 +26,7 @@ /** * API Text output formatter + * @deprecated since 1.24 * @ingroup API */ class ApiFormatTxt extends ApiFormatBase { @@ -38,10 +39,11 @@ class ApiFormatTxt extends ApiFormatBase { } public function execute() { + $this->markDeprecated(); $this->printText( print_r( $this->getResultData(), true ) ); } public function getDescription() { - return 'Output data in PHP\'s print_r() format' . parent::getDescription(); + return 'DEPRECATED! Output data in PHP\'s print_r() format' . parent::getDescription(); } } diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php index 5685d937..ba90c260 100644 --- a/includes/api/ApiFormatWddx.php +++ b/includes/api/ApiFormatWddx.php @@ -26,6 +26,7 @@ /** * API WDDX output formatter + * @deprecated since 1.24 * @ingroup API */ class ApiFormatWddx extends ApiFormatBase { @@ -35,13 +36,17 @@ class ApiFormatWddx extends ApiFormatBase { } public function execute() { + $this->markDeprecated(); + // 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>"; + $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->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 +65,8 @@ class ApiFormatWddx extends ApiFormatBase { /** * Recursively go through the object and output its data in WDDX format. - * @param $elemValue - * @param $indent int + * @param mixed $elemValue + * @param int $indent */ function slowWddxPrinter( $elemValue, $indent = 0 ) { $indstr = ( $this->getIsHtml() ? str_repeat( ' ', $indent ) : '' ); @@ -105,6 +110,6 @@ class ApiFormatWddx extends ApiFormatBase { } public function getDescription() { - return 'Output data in WDDX format' . parent::getDescription(); + return 'DEPRECATED! Output data in WDDX format' . parent::getDescription(); } } diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php index 4ec149c0..b3d59379 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -69,7 +69,7 @@ class ApiFormatXml extends ApiFormatBase { $this->printText( self::recXmlPrint( $this->mRootElemName, $data, - $this->getIsHtml() ? - 2 : null + $this->getIsHtml() ? -2 : null ) ); } @@ -111,9 +111,9 @@ class ApiFormatXml extends ApiFormatBase { * @note The method is recursive, so the same rules apply to any * sub-arrays. * - * @param $elemName - * @param $elemValue - * @param $indent + * @param string $elemName + * @param mixed $elemValue + * @param int $indent * * @return string */ @@ -147,6 +147,15 @@ class ApiFormatXml extends ApiFormatBase { $subElemIndName = null; } + if ( isset( $elemValue['_subelements'] ) ) { + foreach ( $elemValue['_subelements'] as $subElemId ) { + if ( isset( $elemValue[$subElemId] ) && !is_array( $elemValue[$subElemId] ) ) { + $elemValue[$subElemId] = array( '*' => $elemValue[$subElemId] ); + } + } + unset( $elemValue['_subelements'] ); + } + $indElements = array(); $subElements = array(); foreach ( $elemValue as $subElemId => & $subElemValue ) { @@ -156,11 +165,19 @@ class ApiFormatXml extends ApiFormatBase { } elseif ( is_array( $subElemValue ) ) { $subElements[$subElemId] = $subElemValue; unset( $elemValue[$subElemId] ); + } elseif ( is_bool( $subElemValue ) ) { + // treat true as empty string, skip false in xml format + if ( $subElemValue === true ) { + $subElemValue = ''; + } else { + unset( $elemValue[$subElemId] ); + } } } if ( is_null( $subElemIndName ) && count( $indElements ) ) { - ApiBase::dieDebug( __METHOD__, "($elemName, ...) has integer keys without _element value. Use ApiResult::setIndexedTagName()." ); + ApiBase::dieDebug( __METHOD__, "($elemName, ...) has integer keys " . + "without _element value. Use ApiResult::setIndexedTagName()." ); } if ( count( $subElements ) && count( $indElements ) && !is_null( $subElemContent ) ) { @@ -193,6 +210,7 @@ class ApiFormatXml extends ApiFormatBase { $retval .= $indstr . Xml::element( $elemName, null, $elemValue ); } } + return $retval; } @@ -200,17 +218,21 @@ class ApiFormatXml extends ApiFormatBase { $nt = Title::newFromText( $this->mXslt ); if ( is_null( $nt ) || !$nt->exists() ) { $this->setWarning( 'Invalid or non-existent stylesheet specified' ); + return; } if ( $nt->getNamespace() != NS_MEDIAWIKI ) { $this->setWarning( 'Stylesheet should be in the MediaWiki namespace.' ); + return; } - if ( substr( $nt->getText(), - 4 ) !== '.xsl' ) { + if ( substr( $nt->getText(), -4 ) !== '.xsl' ) { $this->setWarning( 'Stylesheet should have .xsl extension.' ); + return; } - $this->printText( '<?xml-stylesheet href="' . htmlspecialchars( $nt->getLocalURL( 'action=raw' ) ) . '" type="text/xsl" ?>' ); + $this->printText( '<?xml-stylesheet href="' . + htmlspecialchars( $nt->getLocalURL( 'action=raw' ) ) . '" type="text/xsl" ?>' ); } public function getAllowedParams() { diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php index 700d4a5e..3798f894 100644 --- a/includes/api/ApiFormatYaml.php +++ b/includes/api/ApiFormatYaml.php @@ -26,6 +26,7 @@ /** * API YAML output formatter + * @deprecated since 1.24 * @ingroup API */ class ApiFormatYaml extends ApiFormatJson { @@ -34,7 +35,12 @@ class ApiFormatYaml extends ApiFormatJson { return 'application/yaml'; } + public function execute() { + $this->markDeprecated(); + parent::execute(); + } + public function getDescription() { - return 'Output data in YAML format' . ApiFormatBase::getDescription(); + return 'DEPRECATED! Output data in YAML format' . ApiFormatBase::getDescription(); } } diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index 9cafc5bb..bcd6c12e 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -30,7 +30,6 @@ * @ingroup API */ class ApiHelp extends ApiBase { - /** * Module for displaying help */ @@ -52,6 +51,7 @@ class ApiHelp extends ApiBase { } if ( is_array( $params['querymodules'] ) ) { + $this->logFeatureUsage( 'action=help&querymodules' ); $queryModules = $params['querymodules']; foreach ( $queryModules as $m ) { $modules[] = 'query+' . $m; @@ -68,19 +68,22 @@ class ApiHelp extends ApiBase { // In case the '+' was typed into URL, it resolves as a space $subNames = explode( ' ', $m ); } + $module = $this->getMain(); - for ( $i = 0; $i < count( $subNames ); $i++ ) { + $subNamesCount = count( $subNames ); + for ( $i = 0; $i < $subNamesCount; $i++ ) { $subs = $module->getModuleManager(); if ( $subs === null ) { $module = null; } else { $module = $subs->getModule( $subNames[$i] ); } + if ( $module === null ) { if ( count( $subNames ) === 2 - && $i === 1 - && $subNames[0] === 'query' - && in_array( $subNames[1], $queryModules ) + && $i === 1 + && $subNames[0] === 'query' + && in_array( $subNames[1], $queryModules ) ) { // Legacy: This is one of the renamed 'querymodule=...' parameters, // do not use '+' notation in the output, use submodule's name instead. @@ -94,6 +97,7 @@ class ApiHelp extends ApiBase { $type = $subs->getModuleGroup( $subNames[$i] ); } } + if ( $module !== null ) { $r[] = $this->buildModuleHelp( $module, $type ); } @@ -104,8 +108,8 @@ class ApiHelp extends ApiBase { } /** - * @param $module ApiBase - * @param $type String What type of request is this? e.g. action, query, list, prop, meta, format + * @param ApiBase $module + * @param string $type What type of request is this? e.g. action, query, list, prop, meta, format * @return string */ private function buildModuleHelp( $module, $type ) { @@ -141,21 +145,25 @@ class ApiHelp extends ApiBase { public function getParamDescription() { return array( - 'modules' => 'List of module names (value of the action= parameter). Can specify submodules with a \'+\'', - 'querymodules' => 'Use modules=query+value instead. List of query module names (value of prop=, meta= or list= parameter)', + 'modules' => 'List of module names (value of the action= parameter). ' . + 'Can specify submodules with a \'+\'', + 'querymodules' => 'Use modules=query+value instead. List of query ' . + 'module names (value of prop=, meta= or list= parameter)', ); } public function getDescription() { - return 'Display this help screen. Or the help screen for the specified module'; + return 'Display this help screen. Or the help screen for the specified module.'; } public function getExamples() { return array( 'api.php?action=help' => 'Whole help page', 'api.php?action=help&modules=protect' => 'Module (action) help page', - 'api.php?action=help&modules=query+categorymembers' => 'Help for the query/categorymembers module', - 'api.php?action=help&modules=login|query+info' => 'Help for the login and query/info modules', + 'api.php?action=help&modules=query+categorymembers' + => 'Help for the query/categorymembers module', + 'api.php?action=help&modules=login|query+info' + => 'Help for the login and query/info modules', ); } diff --git a/includes/api/ApiImageRotate.php b/includes/api/ApiImageRotate.php index 7a60e831..20396dd7 100644 --- a/includes/api/ApiImageRotate.php +++ b/includes/api/ApiImageRotate.php @@ -24,16 +24,12 @@ class ApiImageRotate extends ApiBase { private $mPageSet = null; - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - /** * Add all items from $values into the result - * @param array $result output - * @param array $values values to add - * @param string $flag the name of the boolean flag to mark this element - * @param string $name if given, name of the value + * @param array $result Output + * @param array $values Values to add + * @param string $flag The name of the boolean flag to mark this element + * @param string $name If given, name of the value */ private static function addValues( array &$result, $values, $flag = null, $name = null ) { foreach ( $values as $val ) { @@ -56,6 +52,8 @@ class ApiImageRotate extends ApiBase { $params = $this->extractRequestParams(); $rotation = $params['rotation']; + $this->getResult()->beginContinuation( $params['continue'], array(), array() ); + $pageSet = $this->getPageSet(); $pageSet->execute(); @@ -135,6 +133,7 @@ class ApiImageRotate extends ApiBase { $apiResult = $this->getResult(); $apiResult->setIndexedTagName( $result, 'page' ); $apiResult->addValue( null, $this->getModuleName(), $result ); + $apiResult->endContinuation(); } /** @@ -145,6 +144,7 @@ class ApiImageRotate extends ApiBase { if ( $this->mPageSet === null ) { $this->mPageSet = new ApiPageSet( $this, 0, NS_FILE ); } + return $this->mPageSet; } @@ -163,6 +163,7 @@ class ApiImageRotate extends ApiBase { if ( $permissionErrors ) { // Just return the first error $msg = $this->parseMsg( $permissionErrors[0] ); + return $msg['info']; } @@ -183,43 +184,30 @@ class ApiImageRotate extends ApiBase { ApiBase::PARAM_TYPE => array( '90', '180', '270' ), ApiBase::PARAM_REQUIRED => true ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), + 'continue' => '', ); if ( $flags ) { $result += $this->getPageSet()->getFinalParams( $flags ); } + return $result; } public function getParamDescription() { $pageSet = $this->getPageSet(); + return $pageSet->getFinalParamDescription() + array( 'rotation' => 'Degrees to rotate image clockwise', - 'token' => 'Edit token. You can get one of these through action=tokens', + 'continue' => 'When more results are available, use this to continue', ); } public function getDescription() { - return 'Rotate one or more images'; + return 'Rotate one or more images.'; } public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; - } - - public function getPossibleErrors() { - $pageSet = $this->getPageSet(); - return array_merge( - parent::getPossibleErrors(), - $pageSet->getFinalPossibleErrors() - ); + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php index f48a822e..b11348e5 100644 --- a/includes/api/ApiImport.php +++ b/includes/api/ApiImport.php @@ -98,18 +98,13 @@ class ApiImport extends ApiBase { } public function getAllowedParams() { - global $wgImportSources; return array( - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'summary' => null, 'xml' => array( ApiBase::PARAM_TYPE => 'upload', ), 'interwikisource' => array( - ApiBase::PARAM_TYPE => $wgImportSources + ApiBase::PARAM_TYPE => $this->getConfig()->get( 'ImportSources' ), ), 'interwikipage' => null, 'fullhistory' => false, @@ -123,7 +118,6 @@ class ApiImport extends ApiBase { 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', @@ -135,17 +129,6 @@ class ApiImport extends ApiBase { ); } - public function getResultProperties() { - return array( - ApiBase::PROP_LIST => true, - '' => array( - 'ns' => 'namespace', - 'title' => 'string', - 'revisions' => 'integer' - ) - ); - } - public function getDescription() { return array( 'Import a page from another wiki, or an XML file.', @@ -154,29 +137,14 @@ class ApiImport extends ApiBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'cantimport' ), - array( 'missingparam', 'interwikipage' ), - array( 'cantimport-upload' ), - array( 'import-unknownerror', 'source' ), - array( 'import-unknownerror', 'result' ), - array( 'import-rootpage-nosubpage', 'namespace' ), - array( 'import-rootpage-invalid' ), - ) ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { return array( - 'api.php?action=import&interwikisource=meta&interwikipage=Help:ParserFunctions&namespace=100&fullhistory=&token=123ABC' + 'api.php?action=import&interwikisource=meta&interwikipage=Help:ParserFunctions&' . + 'namespace=100&fullhistory=&token=123ABC' => 'Import [[meta:Help:Parserfunctions]] to namespace 100 with full history', ); } @@ -194,11 +162,11 @@ class ApiImportReporter extends ImportReporter { private $mResultArr = array(); /** - * @param $title Title - * @param $origTitle Title - * @param $revisionCount int - * @param $successCount int - * @param $pageInfo + * @param Title $title + * @param Title $origTitle + * @param int $revisionCount + * @param int $successCount + * @param array $pageInfo * @return void */ function reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ) { diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index b51d441d..976f4c12 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -32,7 +32,7 @@ */ class ApiLogin extends ApiBase { - public function __construct( $main, $action ) { + public function __construct( ApiMain $main, $action ) { parent::__construct( $main, $action, 'lg' ); } @@ -52,6 +52,7 @@ class ApiLogin extends ApiBase { 'result' => 'Aborted', 'reason' => 'Cannot log in when using a callback', ) ); + return; } @@ -78,15 +79,12 @@ class ApiLogin extends ApiBase { $loginForm = new LoginForm(); $loginForm->setContext( $context ); - global $wgCookiePrefix, $wgPasswordAttemptThrottle; - $authRes = $loginForm->authenticateUserData(); switch ( $authRes ) { case LoginForm::SUCCESS: $user = $context->getUser(); $this->getContext()->setUser( $user ); - $user->setOption( 'rememberpassword', 1 ); - $user->setCookies( $this->getRequest() ); + $user->setCookies( $this->getRequest(), null, true ); ApiQueryInfo::resetTokenCache(); @@ -100,14 +98,14 @@ class ApiLogin extends ApiBase { $result['lguserid'] = intval( $user->getId() ); $result['lgusername'] = $user->getName(); $result['lgtoken'] = $user->getToken(); - $result['cookieprefix'] = $wgCookiePrefix; + $result['cookieprefix'] = $this->getConfig()->get( 'CookiePrefix' ); $result['sessionid'] = session_id(); break; case LoginForm::NEED_TOKEN: $result['result'] = 'NeedToken'; $result['token'] = $loginForm->getLoginToken(); - $result['cookieprefix'] = $wgCookiePrefix; + $result['cookieprefix'] = $this->getConfig()->get( 'CookiePrefix' ); $result['sessionid'] = session_id(); break; @@ -131,7 +129,9 @@ class ApiLogin extends ApiBase { $result['result'] = 'NotExists'; break; - case LoginForm::RESET_PASS: // bug 20223 - Treat a temporary password as wrong. Per SpecialUserLogin - "The e-mailed temporary password should not be used for actual logins;" + // bug 20223 - Treat a temporary password as wrong. Per SpecialUserLogin: + // The e-mailed temporary password should not be used for actual logins. + case LoginForm::RESET_PASS: case LoginForm::WRONG_PASS: $result['result'] = 'WrongPass'; break; @@ -147,7 +147,8 @@ class ApiLogin extends ApiBase { case LoginForm::THROTTLED: $result['result'] = 'Throttled'; - $result['wait'] = intval( $wgPasswordAttemptThrottle['seconds'] ); + $throttle = $this->getConfig()->get( 'PasswordAttemptThrottle' ); + $result['wait'] = intval( $throttle['seconds'] ); break; case LoginForm::USER_BLOCKED: @@ -192,92 +193,16 @@ class ApiLogin extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'result' => array( - ApiBase::PROP_TYPE => array( - 'Success', - 'NeedToken', - 'WrongToken', - 'NoName', - 'Illegal', - 'WrongPluginPass', - 'NotExists', - 'WrongPass', - 'EmptyPass', - 'CreateBlocked', - 'Throttled', - 'Blocked', - 'Aborted' - ) - ), - 'lguserid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'lgusername' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'lgtoken' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'cookieprefix' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'sessionid' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'token' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'details' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'wait' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'reason' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { return array( - 'Log in and get the authentication tokens. ', - 'In the event of a successful log-in, a cookie will be attached', - 'to your session. In the event of a failed log-in, you will not ', - 'be able to attempt another log-in through this method for 5 seconds.', - 'This is to prevent password guessing by automated password crackers' + 'Log in and get the authentication tokens.', + 'In the event of a successful log-in, a cookie will be attached to your session.', + 'In the event of a failed log-in, you will not be able to attempt another log-in', + 'through this method for 5 seconds. This is to prevent password guessing by', + 'automated password crackers.' ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'NeedToken', 'info' => 'You need to resubmit your login with the specified token. See https://bugzilla.wikimedia.org/show_bug.cgi?id=23076' ), - array( 'code' => 'WrongToken', 'info' => 'You specified an invalid token' ), - array( 'code' => 'NoName', 'info' => 'You didn\'t set the lgname parameter' ), - array( 'code' => 'Illegal', 'info' => ' You provided an illegal username' ), - array( 'code' => 'NotExists', 'info' => ' The username you provided doesn\'t exist' ), - array( 'code' => 'EmptyPass', 'info' => ' You didn\'t set the lgpassword parameter or you left it empty' ), - array( 'code' => 'WrongPass', 'info' => ' The password you provided is incorrect' ), - array( 'code' => 'WrongPluginPass', 'info' => 'Same as "WrongPass", returned when an authentication plugin rather than MediaWiki itself rejected the password' ), - array( 'code' => 'CreateBlocked', 'info' => 'The wiki tried to automatically create a new account for you, but your IP address has been blocked from account creation' ), - array( 'code' => 'Throttled', 'info' => 'You\'ve logged in too many times in a short time' ), - array( 'code' => 'Blocked', 'info' => 'User is blocked' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=login&lgname=user&lgpassword=password' diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php index 2ba92a63..324f4b2f 100644 --- a/includes/api/ApiLogout.php +++ b/includes/api/ApiLogout.php @@ -50,16 +50,12 @@ class ApiLogout extends ApiBase { return array(); } - public function getResultProperties() { - return array(); - } - public function getParamDescription() { return array(); } public function getDescription() { - return 'Log out and clear session data'; + return 'Log out and clear session data.'; } public function getExamples() { diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index ea2fcc78..119d7b48 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -39,7 +39,6 @@ * @ingroup API */ class ApiMain extends ApiBase { - /** * When no format parameter is given, this format will be used */ @@ -57,6 +56,7 @@ class ApiMain extends ApiBase { 'parse' => 'ApiParse', 'opensearch' => 'ApiOpenSearch', 'feedcontributions' => 'ApiFeedContributions', + 'feedrecentchanges' => 'ApiFeedRecentChanges', 'feedwatchlist' => 'ApiFeedWatchlist', 'help' => 'ApiHelp', 'paraminfo' => 'ApiParamInfo', @@ -81,9 +81,11 @@ class ApiMain extends ApiBase { 'watch' => 'ApiWatch', 'patrol' => 'ApiPatrol', 'import' => 'ApiImport', + 'clearhasmsg' => 'ApiClearHasMsg', 'userrights' => 'ApiUserrights', 'options' => 'ApiOptions', 'imagerotate' => 'ApiImageRotate', + 'revisiondelete' => 'ApiRevisionDelete', ); /** @@ -110,6 +112,7 @@ class ApiMain extends ApiBase { 'none' => 'ApiFormatNone', ); + // @codingStandardsIgnoreStart String contenation on "msg" not allowed to break long line /** * List of user roles that are specifically relevant to the API. * array( 'right' => array ( 'msg' => 'Some message with a $1', @@ -126,6 +129,7 @@ class ApiMain extends ApiBase { 'params' => array( ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ) ) ); + // @codingStandardsIgnoreEnd /** * @var ApiFormatBase @@ -144,8 +148,9 @@ class ApiMain extends ApiBase { /** * Constructs an instance of ApiMain that utilizes the module and format specified by $request. * - * @param $context IContextSource|WebRequest - if this is an instance of FauxRequest, errors are thrown and no printing occurs - * @param bool $enableWrite should be set to true if the api may modify data + * @param IContextSource|WebRequest $context If this is an instance of + * FauxRequest, errors are thrown and no printing occurs + * @param bool $enableWrite Should be set to true if the api may modify data */ public function __construct( $context = null, $enableWrite = false ) { if ( $context === null ) { @@ -182,16 +187,17 @@ class ApiMain extends ApiBase { } } - global $wgAPIModules; + $config = $this->getConfig(); $this->mModuleMgr = new ApiModuleManager( $this ); $this->mModuleMgr->addModules( self::$Modules, 'action' ); - $this->mModuleMgr->addModules( $wgAPIModules, 'action' ); + $this->mModuleMgr->addModules( $config->get( 'APIModules' ), 'action' ); $this->mModuleMgr->addModules( self::$Formats, 'format' ); + $this->mModuleMgr->addModules( $config->get( 'APIFormatModules' ), 'format' ); $this->mResult = new ApiResult( $this ); $this->mEnableWrite = $enableWrite; - $this->mSquidMaxage = - 1; // flag for executeActionWithErrorHandling() + $this->mSquidMaxage = -1; // flag for executeActionWithErrorHandling() $this->mCommit = false; } @@ -233,7 +239,7 @@ class ApiMain extends ApiBase { /** * Set how long the response should be cached. * - * @param $maxage + * @param int $maxage */ public function setCacheMaxAge( $maxage ) { $this->setCacheControl( array( @@ -270,6 +276,7 @@ class ApiMain extends ApiBase { public function setCacheMode( $mode ) { if ( !in_array( $mode, array( 'private', 'public', 'anon-public-user-private' ) ) ) { wfDebug( __METHOD__ . ": unrecognised cache mode \"$mode\"\n" ); + // Ignore for forwards-compatibility return; } @@ -278,6 +285,7 @@ class ApiMain extends ApiBase { // Private wiki, only private headers if ( $mode !== 'private' ) { wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" ); + return; } } @@ -287,16 +295,6 @@ class ApiMain extends ApiBase { } /** - * @deprecated since 1.17 Private caching is now the default, so there is usually no - * need to call this function. If there is a need, you can use - * $this->setCacheMode('private') - */ - public function setCachePrivate() { - wfDeprecated( __METHOD__, '1.17' ); - $this->setCacheMode( 'private' ); - } - - /** * Set directives (key/value pairs) for the Cache-Control header. * Boolean values will be formatted as such, by including or omitting * without an equals sign. @@ -304,31 +302,16 @@ class ApiMain extends ApiBase { * Cache control values set here will only be used if the cache mode is not * private, see setCacheMode(). * - * @param $directives array + * @param array $directives */ public function setCacheControl( $directives ) { $this->mCacheControl = $directives + $this->mCacheControl; } /** - * Make sure Vary: Cookie and friends are set. Use this when the output of a request - * may be cached for anons but may not be cached for logged-in users. - * - * WARNING: This function must be called CONSISTENTLY for a given URL. This means that a - * given URL must either always or never call this function; if it sometimes does and - * sometimes doesn't, stuff will break. - * - * @deprecated since 1.17 Use setCacheMode( 'anon-public-user-private' ) - */ - public function setVaryCookie() { - wfDeprecated( __METHOD__, '1.17' ); - $this->setCacheMode( 'anon-public-user-private' ); - } - - /** * Create an instance of an output formatter by its name * - * @param $format string + * @param string $format * * @return ApiFormatBase */ @@ -337,6 +320,7 @@ class ApiMain extends ApiBase { if ( $printer === null ) { $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' ); } + return $printer; } @@ -379,37 +363,7 @@ class ApiMain extends ApiBase { try { $this->executeAction(); } catch ( Exception $e ) { - // Allow extra cleanup and logging - wfRunHooks( 'ApiMain::onException', array( $this, $e ) ); - - // Log it - if ( !( $e instanceof UsageException ) ) { - MWExceptionHandler::logException( $e ); - } - - // Handle any kind of exception by outputting properly formatted error message. - // If this fails, an unhandled exception should be thrown so that global error - // handler will process and log it. - - $errCode = $this->substituteResultWithError( $e ); - - // Error results should not be cached - $this->setCacheMode( 'private' ); - - $response = $this->getRequest()->response(); - $headerStr = 'MediaWiki-API-Error: ' . $errCode; - if ( $e->getCode() === 0 ) { - $response->header( $headerStr ); - } else { - $response->header( $headerStr, true, $e->getCode() ); - } - - // Reset and print just the error message - ob_clean(); - - // If the error occurred during printing, do a printer->profileOut() - $this->mPrinter->safeProfileOut(); - $this->printResult( true ); + $this->handleException( $e ); } // Log the request whether or not there was an error @@ -427,6 +381,79 @@ class ApiMain extends ApiBase { } /** + * Handle an exception as an API response + * + * @since 1.23 + * @param Exception $e + */ + protected function handleException( Exception $e ) { + // Bug 63145: Rollback any open database transactions + if ( !( $e instanceof UsageException ) ) { + // UsageExceptions are intentional, so don't rollback if that's the case + MWExceptionHandler::rollbackMasterChangesAndLog( $e ); + } + + // Allow extra cleanup and logging + wfRunHooks( 'ApiMain::onException', array( $this, $e ) ); + + // Log it + if ( !( $e instanceof UsageException ) ) { + MWExceptionHandler::logException( $e ); + } + + // Handle any kind of exception by outputting properly formatted error message. + // If this fails, an unhandled exception should be thrown so that global error + // handler will process and log it. + + $errCode = $this->substituteResultWithError( $e ); + + // Error results should not be cached + $this->setCacheMode( 'private' ); + + $response = $this->getRequest()->response(); + $headerStr = 'MediaWiki-API-Error: ' . $errCode; + if ( $e->getCode() === 0 ) { + $response->header( $headerStr ); + } else { + $response->header( $headerStr, true, $e->getCode() ); + } + + // Reset and print just the error message + ob_clean(); + + // If the error occurred during printing, do a printer->profileOut() + $this->mPrinter->safeProfileOut(); + $this->printResult( true ); + } + + /** + * Handle an exception from the ApiBeforeMain hook. + * + * This tries to print the exception as an API response, to be more + * friendly to clients. If it fails, it will rethrow the exception. + * + * @since 1.23 + * @param Exception $e + */ + public static function handleApiBeforeMainException( Exception $e ) { + ob_start(); + + try { + $main = new self( RequestContext::getMain(), false ); + $main->handleException( $e ); + } catch ( Exception $e2 ) { + // Nope, even that didn't work. Punt. + throw $e; + } + + // Log the request and reset cache headers + $main->logRequest( 0 ); + $main->sendCacheHeaders(); + + ob_end_flush(); + } + + /** * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately. * * If no origin parameter is present, nothing happens. @@ -439,8 +466,6 @@ class ApiMain extends ApiBase { * @return bool False if the caller should abort (403 case), true otherwise (all other cases) */ protected function handleCORS() { - global $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions; - $originParam = $this->getParameter( 'origin' ); // defaults to null if ( $originParam === null ) { // No origin parameter, nothing to do @@ -456,6 +481,7 @@ class ApiMain extends ApiBase { } else { $origins = explode( ' ', $originHeader ); } + if ( !in_array( $originParam, $origins ) ) { // origin parameter set but incorrect // Send a 403 response @@ -463,13 +489,23 @@ class ApiMain extends ApiBase { $response->header( "HTTP/1.1 403 $message", true, 403 ); $response->header( 'Cache-Control: no-cache' ); echo "'origin' parameter does not match Origin header\n"; + return false; } - if ( self::matchOrigin( $originParam, $wgCrossSiteAJAXdomains, $wgCrossSiteAJAXdomainExceptions ) ) { + + $config = $this->getConfig(); + $matchOrigin = self::matchOrigin( + $originParam, + $config->get( 'CrossSiteAJAXdomains' ), + $config->get( 'CrossSiteAJAXdomainExceptions' ) + ); + + if ( $matchOrigin ) { $response->header( "Access-Control-Allow-Origin: $originParam" ); $response->header( 'Access-Control-Allow-Credentials: true' ); $this->getOutput()->addVaryHeader( 'Origin' ); } + return true; } @@ -478,7 +514,8 @@ class ApiMain extends ApiBase { * @param string $value Origin header * @param array $rules Set of wildcard rules * @param array $exceptions Set of wildcard rules - * @return bool True if $value matches a rule in $rules and doesn't match any rules in $exceptions, false otherwise + * @return bool True if $value matches a rule in $rules and doesn't match + * any rules in $exceptions, false otherwise */ protected static function matchOrigin( $value, $rules, $exceptions ) { foreach ( $rules as $rule ) { @@ -489,9 +526,11 @@ class ApiMain extends ApiBase { return false; } } + return true; } } + return false; } @@ -510,15 +549,17 @@ class ApiMain extends ApiBase { array( '.*?', '.' ), $wildcard ); + return "/^https?:\/\/$wildcard$/"; } protected function sendCacheHeaders() { - global $wgUseXVO, $wgVaryOnXFP; $response = $this->getRequest()->response(); $out = $this->getOutput(); - if ( $wgVaryOnXFP ) { + $config = $this->getConfig(); + + if ( $config->get( 'VaryOnXFP' ) ) { $out->addVaryHeader( 'X-Forwarded-Proto' ); } @@ -527,10 +568,11 @@ class ApiMain extends ApiBase { return; } + $useXVO = $config->get( 'UseXVO' ); if ( $this->mCacheMode == 'anon-public-user-private' ) { $out->addVaryHeader( 'Cookie' ); $response->header( $out->getVaryHeader() ); - if ( $wgUseXVO ) { + if ( $useXVO ) { $response->header( $out->getXVO() ); if ( $out->haveCacheVaryCookies() ) { // Logged in, mark this request private @@ -542,13 +584,14 @@ class ApiMain extends ApiBase { // Logged in or otherwise has session (e.g. anonymous users who have edited) // Mark request private $response->header( 'Cache-Control: private' ); + return; } // else no XVO and anonymous, send public headers below } // Send public headers $response->header( $out->getVaryHeader() ); - if ( $wgUseXVO ) { + if ( $useXVO ) { $response->header( $out->getXVO() ); } @@ -565,6 +608,7 @@ class ApiMain extends ApiBase { // Sending a Vary header in this case is harmless, and protects us // against conditional calls of setCacheMaxAge(). $response->header( 'Cache-Control: private' ); + return; } @@ -596,13 +640,12 @@ class ApiMain extends ApiBase { /** * Replace the result data with the information about an exception. * Returns the error code - * @param $e Exception + * @param Exception $e * @return string */ protected function substituteResultWithError( $e ) { - global $wgShowHostnames; - $result = $this->getResult(); + // Printer may not be initialized if the extractRequestParams() fails for the main module if ( !isset( $this->mPrinter ) ) { // The printer has not been created yet. Try to manually get formatter value. @@ -612,11 +655,20 @@ class ApiMain extends ApiBase { } $this->mPrinter = $this->createPrinterByName( $value ); - if ( $this->mPrinter->getNeedsRawData() ) { - $result->setRawMode(); - } } + // Printer may not be able to handle errors. This is particularly + // likely if the module returns something for getCustomPrinter(). + if ( !$this->mPrinter->canPrintErrors() ) { + $this->mPrinter->safeProfileOut(); + $this->mPrinter = $this->createPrinterByName( self::API_DEFAULT_FORMAT ); + } + + // Update raw mode flag for the selected printer. + $result->setRawMode( $this->mPrinter->getNeedsRawData() ); + + $config = $this->getConfig(); + if ( $e instanceof UsageException ) { // User entered incorrect parameters - print usage screen $errMessage = $e->getMessageArray(); @@ -626,9 +678,8 @@ class ApiMain extends ApiBase { ApiResult::setContent( $errMessage, $this->makeHelpMsg() ); } } else { - global $wgShowSQLErrors, $wgShowExceptionDetails; // Something is seriously wrong - if ( ( $e instanceof DBQueryError ) && !$wgShowSQLErrors ) { + if ( ( $e instanceof DBQueryError ) && !$config->get( 'ShowSQLErrors' ) ) { $info = 'Database query error'; } else { $info = "Exception Caught: {$e->getMessage()}"; @@ -638,7 +689,10 @@ class ApiMain extends ApiBase { 'code' => 'internal_api_error_' . get_class( $e ), 'info' => $info, ); - ApiResult::setContent( $errMessage, $wgShowExceptionDetails ? "\n\n{$e->getTraceAsString()}\n\n" : '' ); + ApiResult::setContent( + $errMessage, + $config->get( 'ShowExceptionDetails' ) ? "\n\n{$e->getTraceAsString()}\n\n" : '' + ); } // Remember all the warnings to re-add them later @@ -646,21 +700,20 @@ class ApiMain extends ApiBase { $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null; $result->reset(); - $result->disableSizeCheck(); // Re-add the id $requestid = $this->getParameter( 'requestid' ); if ( !is_null( $requestid ) ) { - $result->addValue( null, 'requestid', $requestid ); + $result->addValue( null, 'requestid', $requestid, ApiResult::NO_SIZE_CHECK ); } - if ( $wgShowHostnames ) { + if ( $config->get( 'ShowHostnames' ) ) { // servedby is especially useful when debugging errors - $result->addValue( null, 'servedby', wfHostName() ); + $result->addValue( null, 'servedby', wfHostName(), ApiResult::NO_SIZE_CHECK ); } if ( $warnings !== null ) { - $result->addValue( null, 'warnings', $warnings ); + $result->addValue( null, 'warnings', $warnings, ApiResult::NO_SIZE_CHECK ); } - $result->addValue( null, 'error', $errMessage ); + $result->addValue( null, 'error', $errMessage, ApiResult::NO_SIZE_CHECK ); return $errMessage['code']; } @@ -670,8 +723,6 @@ class ApiMain extends ApiBase { * @return array */ protected function setupExecuteAction() { - global $wgShowHostnames; - // First add the id to the top element $result = $this->getResult(); $requestid = $this->getParameter( 'requestid' ); @@ -679,13 +730,18 @@ class ApiMain extends ApiBase { $result->addValue( null, 'requestid', $requestid ); } - if ( $wgShowHostnames ) { + if ( $this->getConfig()->get( 'ShowHostnames' ) ) { $servedby = $this->getParameter( 'servedby' ); if ( $servedby ) { $result->addValue( null, 'servedby', wfHostName() ); } } + if ( $this->getParameter( 'curtimestamp' ) ) { + $result->addValue( null, 'curtimestamp', wfTimestamp( TS_ISO_8601, time() ), + ApiResult::NO_SIZE_CHECK ); + } + $params = $this->extractRequestParams(); $this->mAction = $params['action']; @@ -709,30 +765,53 @@ class ApiMain extends ApiBase { } $moduleParams = $module->extractRequestParams(); - // Die if token required, but not provided - $salt = $module->getTokenSalt(); - if ( $salt !== false ) { + // Check token, if necessary + if ( $module->needsToken() === true ) { + throw new MWException( + "Module '{$module->getModuleName()}' must be updated for the new token handling. " . + "See documentation for ApiBase::needsToken for details." + ); + } + if ( $module->needsToken() ) { + if ( !$module->mustBePosted() ) { + throw new MWException( + "Module '{$module->getModuleName()}' must require POST to use tokens." + ); + } + if ( !isset( $moduleParams['token'] ) ) { $this->dieUsageMsg( array( 'missingparam', 'token' ) ); - } else { - if ( !$this->getUser()->matchEditToken( $moduleParams['token'], $salt, $this->getContext()->getRequest() ) ) { - $this->dieUsageMsg( 'sessionfailure' ); - } + } + + if ( !$this->getConfig()->get( 'DebugAPI' ) && + array_key_exists( + $module->encodeParamName( 'token' ), + $this->getRequest()->getQueryValues() + ) + ) { + $this->dieUsage( + "The '{$module->encodeParamName( 'token' )}' parameter was found in the query string, but must be in the POST body", + 'mustposttoken' + ); + } + + if ( !$module->validateToken( $moduleParams['token'], $moduleParams ) ) { + $this->dieUsageMsg( 'sessionfailure' ); } } + return $module; } /** * Check the max lag if necessary - * @param $module ApiBase object: Api module being used - * @param array $params an array containing the request parameters. - * @return boolean True on success, false should exit immediately + * @param ApiBase $module Api module being used + * @param array $params Array an array containing the request parameters. + * @return bool True on success, false should exit immediately */ protected function checkMaxLag( $module, $params ) { if ( $module->shouldCheckMaxlag() && isset( $params['maxlag'] ) ) { // Check for maxlag - global $wgShowHostnames; $maxLag = $params['maxlag']; list( $host, $lag ) = wfGetLB()->getMaxLag(); if ( $lag > $maxLag ) { @@ -741,26 +820,26 @@ class ApiMain extends ApiBase { $response->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) ); $response->header( 'X-Database-Lag: ' . intval( $lag ) ); - if ( $wgShowHostnames ) { + if ( $this->getConfig()->get( 'ShowHostnames' ) ) { $this->dieUsage( "Waiting for $host: $lag seconds lagged", 'maxlag' ); - } else { - $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' ); } - return false; + + $this->dieUsage( "Waiting for a database server: $lag seconds lagged", 'maxlag' ); } } + return true; } /** * Check for sufficient permissions to execute - * @param $module ApiBase An Api module + * @param ApiBase $module An Api module */ protected function checkExecutePermissions( $module ) { $user = $this->getUser(); if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) && - !$user->isAllowed( 'read' ) ) - { + !$user->isAllowed( 'read' ) + ) { $this->dieUsageMsg( 'readrequired' ); } if ( $module->isWriteMode() ) { @@ -783,9 +862,31 @@ class ApiMain extends ApiBase { } /** + * Check asserts of the user's rights + * @param array $params + */ + protected function checkAsserts( $params ) { + if ( isset( $params['assert'] ) ) { + $user = $this->getUser(); + switch ( $params['assert'] ) { + case 'user': + if ( $user->isAnon() ) { + $this->dieUsage( 'Assertion that the user is logged in failed', 'assertuserfailed' ); + } + break; + case 'bot': + if ( !$user->isAllowed( 'bot' ) ) { + $this->dieUsage( 'Assertion that the user has the bot right failed', 'assertbotfailed' ); + } + break; + } + } + } + + /** * Check POST for external response and setup result printer - * @param $module ApiBase An Api module - * @param array $params an array with the request parameters + * @param ApiBase $module An Api module + * @param array $params An array with the request parameters */ protected function setupExternalResponse( $module, $params ) { if ( !$this->getRequest()->wasPosted() && $module->mustBePosted() ) { @@ -824,6 +925,8 @@ class ApiMain extends ApiBase { $this->setupExternalResponse( $module, $params ); } + $this->checkAsserts( $params ); + // Execute $module->profileIn(); $module->execute(); @@ -843,7 +946,7 @@ class ApiMain extends ApiBase { /** * Log the preceding request - * @param $time Time in seconds + * @param int $time Time in seconds */ protected function logRequest( $time ) { $request = $this->getRequest(); @@ -867,25 +970,30 @@ class ApiMain extends ApiBase { } } $s .= "\n"; - wfDebugLog( 'api', $s, false ); + wfDebugLog( 'api', $s, 'private' ); } /** * Encode a value in a format suitable for a space-separated log line. + * @param string $s + * @return string */ protected function encodeRequestLogValue( $s ) { static $table; if ( !$table ) { $chars = ';@$!*(),/:'; - for ( $i = 0; $i < strlen( $chars ); $i++ ) { + $numChars = strlen( $chars ); + for ( $i = 0; $i < $numChars; $i++ ) { $table[rawurlencode( $chars[$i] )] = $chars[$i]; } } + return strtr( rawurlencode( $s ), $table ); } /** * Get the request parameters used in the course of the preceding execute() request + * @return array */ protected function getParamsUsed() { return array_keys( $this->mParamsUsed ); @@ -893,19 +1001,35 @@ class ApiMain extends ApiBase { /** * Get a request value, and register the fact that it was used, for logging. + * @param string $name + * @param mixed $default + * @return mixed */ public function getVal( $name, $default = null ) { $this->mParamsUsed[$name] = true; - return $this->getRequest()->getVal( $name, $default ); + + $ret = $this->getRequest()->getVal( $name ); + if ( $ret === null ) { + if ( $this->getRequest()->getArray( $name ) !== null ) { + // See bug 10262 for why we don't just join( '|', ... ) the + // array. + $this->setWarning( + "Parameter '$name' uses unsupported PHP array syntax" + ); + } + $ret = $default; + } + return $ret; } /** * Get a boolean request value, and register the fact that the parameter * was used, for logging. + * @param string $name + * @return bool */ public function getCheck( $name ) { - $this->mParamsUsed[$name] = true; - return $this->getRequest()->getCheck( $name ); + return $this->getVal( $name, null ) !== null; } /** @@ -917,6 +1041,7 @@ class ApiMain extends ApiBase { */ public function getUpload( $name ) { $this->mParamsUsed[$name] = true; + return $this->getRequest()->getUpload( $name ); } @@ -948,11 +1073,10 @@ class ApiMain extends ApiBase { /** * Print results using the current printer * - * @param $isError bool + * @param bool $isError */ protected function printResult( $isError ) { - global $wgDebugAPI; - if ( $wgDebugAPI !== false ) { + if ( $this->getConfig()->get( 'DebugAPI' ) !== false ) { $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' ); } @@ -991,11 +1115,11 @@ class ApiMain extends ApiBase { return array( 'format' => array( ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT, - ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'format' ) + ApiBase::PARAM_TYPE => 'submodule', ), 'action' => array( ApiBase::PARAM_DFLT => 'help', - ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'action' ) + ApiBase::PARAM_TYPE => 'submodule', ), 'maxlag' => array( ApiBase::PARAM_TYPE => 'integer' @@ -1008,8 +1132,12 @@ class ApiMain extends ApiBase { ApiBase::PARAM_TYPE => 'integer', ApiBase::PARAM_DFLT => 0 ), + 'assert' => array( + ApiBase::PARAM_TYPE => array( 'user', 'bot' ) + ), 'requestid' => null, 'servedby' => false, + 'curtimestamp' => false, 'origin' => null, ); } @@ -1033,14 +1161,20 @@ class ApiMain extends ApiBase { ), 'smaxage' => 'Set the s-maxage header to this many seconds. Errors are never cached', 'maxage' => 'Set the max-age header to this many seconds. Errors are never cached', + 'assert' => 'Verify the user is logged in if set to "user", or has the bot userright if "bot"', 'requestid' => 'Request ID to distinguish requests. This will just be output back to you', - 'servedby' => 'Include the hostname that served the request in the results. Unconditionally shown on error', + 'servedby' => 'Include the hostname that served the request in the ' . + 'results. Unconditionally shown on error', + 'curtimestamp' => 'Include the current timestamp in the result.', 'origin' => array( - 'When accessing the API using a cross-domain AJAX request (CORS), set this to the originating domain.', - 'This must be included in any pre-flight request, and therefore must be part of the request URI (not the POST body).', - 'This must match one of the origins in the Origin: header exactly, so it has to be set to something like http://en.wikipedia.org or https://meta.wikimedia.org .', - 'If this parameter does not match the Origin: header, a 403 response will be returned.', - 'If this parameter matches the Origin: header and the origin is whitelisted, an Access-Control-Allow-Origin header will be set.', + 'When accessing the API using a cross-domain AJAX request (CORS), set this to the', + 'originating domain. This must be included in any pre-flight request, and', + 'therefore must be part of the request URI (not the POST body). This must match', + 'one of the origins in the Origin: header exactly, so it has to be set to ', + 'something like http://en.wikipedia.org or https://meta.wikimedia.org . If this', + 'parameter does not match the Origin: header, a 403 response will be returned. If', + 'this parameter matches the Origin: header and the origin is whitelisted, an', + 'Access-Control-Allow-Origin header will be set.', ), ); } @@ -1054,33 +1188,35 @@ class ApiMain extends ApiBase { return array( '', '', - '**********************************************************************************************************', - '** **', - '** This is an auto-generated MediaWiki API documentation page **', - '** **', - '** Documentation and Examples: **', - '** https://www.mediawiki.org/wiki/API **', - '** **', - '**********************************************************************************************************', + '**********************************************************************************************', + '** **', + '** This is an auto-generated MediaWiki API documentation page **', + '** **', + '** Documentation and Examples: **', + '** https://www.mediawiki.org/wiki/API **', + '** **', + '**********************************************************************************************', '', 'Status: All features shown on this page should be working, but the API', ' is still in active development, and may change at any time.', - ' Make sure to monitor our mailing list for any updates', + ' Make sure to monitor our mailing list for any updates.', '', 'Erroneous requests: When erroneous requests are sent to the API, a HTTP header will be sent', ' with the key "MediaWiki-API-Error" and then both the value of the', - ' header and the error code sent back will be set to the same value', + ' header and the error code sent back will be set to the same value.', '', ' In the case of an invalid action being passed, these will have a value', - ' of "unknown_action"', + ' of "unknown_action".', '', - ' For more information see https://www.mediawiki.org/wiki/API:Errors_and_warnings', + ' For more information see https://www.mediawiki.org' . + '/wiki/API:Errors_and_warnings', '', 'Documentation: https://www.mediawiki.org/wiki/API:Main_page', 'FAQ https://www.mediawiki.org/wiki/API:FAQ', 'Mailing list: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api', 'Api Announcements: https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce', - 'Bugs & Requests: https://bugzilla.wikimedia.org/buglist.cgi?component=API&bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts', + 'Bugs & Requests: https://bugzilla.wikimedia.org/buglist.cgi?component=API&' . + 'bug_status=NEW&bug_status=ASSIGNED&bug_status=REOPENED&order=bugs.delta_ts', '', '', '', @@ -1090,30 +1226,18 @@ class ApiMain extends ApiBase { } /** - * @return array - */ - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'readonlytext' ), - array( 'code' => 'unknown_format', 'info' => 'Unrecognized format: format' ), - array( 'code' => 'unknown_action', 'info' => 'The API requires a valid action parameter' ), - array( 'code' => 'maxlag', 'info' => 'Waiting for host: x seconds lagged' ), - array( 'code' => 'maxlag', 'info' => 'Waiting for a database server: x seconds lagged' ), - ) ); - } - - /** * Returns an array of strings with credits for the API * @return array */ protected function getCredits() { return array( 'API developers:', - ' Roan Kattouw "<Firstname>.<Lastname>@gmail.com" (lead developer Sep 2007-2009)', - ' Victor Vasiliev - vasilvv @ gmail . com', - ' Bryan Tong Minh - bryan . tongminh @ gmail . com', - ' Sam Reed - sam @ reedyboy . net', - ' Yuri Astrakhan "<Firstname><Lastname>@gmail.com" (creator, lead developer Sep 2006-Sep 2007, 2012-present)', + ' Roan Kattouw (lead developer Sep 2007-2009)', + ' Victor Vasiliev', + ' Bryan Tong Minh', + ' Sam Reed', + ' Yuri Astrakhan (creator, lead developer Sep 2006-Sep 2007, 2012-2013)', + ' Brad Jorsch (lead developer 2013-now)', '', 'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org', 'or file a bug report at https://bugzilla.wikimedia.org/' @@ -1123,7 +1247,7 @@ class ApiMain extends ApiBase { /** * Sets whether the pretty-printer should format *bold* and $italics$ * - * @param $help bool + * @param bool $help */ public function setHelp( $help = true ) { $this->mPrinter->setHelp( $help ); @@ -1135,21 +1259,24 @@ class ApiMain extends ApiBase { * @return string */ public function makeHelpMsg() { - global $wgMemc, $wgAPICacheHelpTimeout; + global $wgMemc; $this->setHelp(); // Get help text from cache if present $key = wfMemcKey( 'apihelp', $this->getModuleName(), str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) ) ); - if ( $wgAPICacheHelpTimeout > 0 ) { + + $cacheHelpTimeout = $this->getConfig()->get( 'APICacheHelpTimeout' ); + if ( $cacheHelpTimeout > 0 ) { $cached = $wgMemc->get( $key ); if ( $cached ) { return $cached; } } $retval = $this->reallyMakeHelpMsg(); - if ( $wgAPICacheHelpTimeout > 0 ) { - $wgMemc->set( $key, $retval, $wgAPICacheHelpTimeout ); + if ( $cacheHelpTimeout > 0 ) { + $wgMemc->set( $key, $retval, $cacheHelpTimeout ); } + return $retval; } @@ -1180,7 +1307,7 @@ class ApiMain extends ApiBase { foreach ( self::$mRights as $right => $rightMsg ) { $groups = User::getGroupsWithPermission( $right ); $msg .= "* " . $right . " *\n " . wfMsgReplaceArgs( $rightMsg['msg'], $rightMsg['params'] ) . - "\nGranted to:\n " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n"; + "\nGranted to:\n " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n"; } $msg .= "\n$astriks Formats $astriks\n\n"; @@ -1200,8 +1327,9 @@ class ApiMain extends ApiBase { } /** - * @param $module ApiBase - * @param string $paramName What type of request is this? e.g. action, query, list, prop, meta, format + * @param ApiBase $module + * @param string $paramName What type of request is this? e.g. action, + * query, list, prop, meta, format * @return string */ public static function makeHelpMsgHeader( $module, $paramName ) { @@ -1234,6 +1362,7 @@ class ApiMain extends ApiBase { */ public function getShowVersions() { wfDeprecated( __METHOD__, '1.21' ); + return false; } @@ -1252,7 +1381,7 @@ class ApiMain extends ApiBase { * * @deprecated since 1.21, Use getModuleManager()->addModule() instead. * @param string $name The identifier for this module. - * @param $class ApiBase The class where this module is implemented. + * @param ApiBase $class The class where this module is implemented. */ protected function addModule( $name, $class ) { $this->getModuleManager()->addModule( $name, 'action', $class ); @@ -1264,7 +1393,7 @@ class ApiMain extends ApiBase { * * @deprecated since 1.21, Use getModuleManager()->addModule() instead. * @param string $name The identifier for this format. - * @param $class ApiFormatBase The class implementing this format. + * @param ApiFormatBase $class The class implementing this format. */ protected function addFormat( $name, $class ) { $this->getModuleManager()->addModule( $name, 'format', $class ); @@ -1307,10 +1436,10 @@ class UsageException extends MWException { private $mExtraData; /** - * @param $message string - * @param $codestr string - * @param $code int - * @param $extradata array|null + * @param string $message + * @param string $codestr + * @param int $code + * @param array|null $extradata */ public function __construct( $message, $codestr, $code = 0, $extradata = null ) { parent::__construct( $message, $code ); @@ -1336,6 +1465,7 @@ class UsageException extends MWException { if ( is_array( $this->mExtraData ) ) { $result = array_merge( $result, $this->mExtraData ); } + return $result; } diff --git a/includes/api/ApiModuleManager.php b/includes/api/ApiModuleManager.php index 100392bf..a0300ab5 100644 --- a/includes/api/ApiModuleManager.php +++ b/includes/api/ApiModuleManager.php @@ -33,9 +33,21 @@ */ class ApiModuleManager extends ContextSource { + /** + * @var ApiBase + */ private $mParent; + /** + * @var ApiBase[] + */ private $mInstances = array(); + /** + * @var null[] + */ private $mGroups = array(); + /** + * @var array[] + */ private $mModules = array(); /** @@ -47,13 +59,55 @@ class ApiModuleManager extends ContextSource { } /** - * Add a list of modules to the manager - * @param array $modules A map of ModuleName => ModuleClass + * Add a list of modules to the manager. Each module is described + * by a module spec. + * + * Each module spec is an associative array containing at least + * the 'class' key for the module's class, and optionally a + * 'factory' key for the factory function to use for the module. + * + * That factory function will be called with two parameters, + * the parent module (an instance of ApiBase, usually ApiMain) + * and the name the module was registered under. The return + * value must be an instance of the class given in the 'class' + * field. + * + * For backward compatibility, the module spec may also be a + * simple string containing the module's class name. In that + * case, the class' constructor will be called with the parent + * module and module name as parameters, as described above. + * + * Examples for defining module specs: + * + * @code + * $modules['foo'] = 'ApiFoo'; + * $modules['bar'] = array( + * 'class' => 'ApiBar', + * 'factory' => function( $main, $name ) { ... } + * ); + * $modules['xyzzy'] = array( + * 'class' => 'ApiXyzzy', + * 'factory' => array( 'XyzzyFactory', 'newApiModule' ) + * ); + * @endcode + * + * @param array $modules A map of ModuleName => ModuleSpec; The ModuleSpec + * is either a string containing the module's class name, or an associative + * array (see above for details). * @param string $group Which group modules belong to (action,format,...) */ public function addModules( array $modules, $group ) { - foreach ( $modules as $name => $class ) { - $this->addModule( $name, $group, $class ); + + foreach ( $modules as $name => $moduleSpec ) { + if ( is_array( $moduleSpec ) ) { + $class = $moduleSpec['class']; + $factory = ( isset( $moduleSpec['factory'] ) ? $moduleSpec['factory'] : null ); + } else { + $class = $moduleSpec; + $factory = null; + } + + $this->addModule( $name, $group, $class, $factory ); } } @@ -62,49 +116,100 @@ class ApiModuleManager extends ContextSource { * classes who wish to add their own modules to their lexicon or override the * behavior of inherent ones. * - * @param string $group Name of the module group * @param string $name The identifier for this module. + * @param string $group Name of the module group * @param string $class The class where this module is implemented. + * @param callable|null $factory Callback for instantiating the module. + * + * @throws InvalidArgumentException */ - public function addModule( $name, $group, $class ) { + public function addModule( $name, $group, $class, $factory = null ) { + if ( !is_string( $name ) ) { + throw new InvalidArgumentException( '$name must be a string' ); + } + + if ( !is_string( $group ) ) { + throw new InvalidArgumentException( '$group must be a string' ); + } + + if ( !is_string( $class ) ) { + throw new InvalidArgumentException( '$class must be a string' ); + } + + if ( $factory !== null && !is_callable( $factory ) ) { + throw new InvalidArgumentException( '$factory must be a callable (or null)' ); + } + $this->mGroups[$group] = null; - $this->mModules[$name] = array( $group, $class ); + $this->mModules[$name] = array( $group, $class, $factory ); } /** * Get module instance by name, or instantiate it if it does not exist - * @param string $moduleName module name - * @param string $group optionally validate that the module is in a specific group - * @param bool $ignoreCache if true, force-creates a new instance and does not cache it - * @return mixed the new module instance, or null if failed + * + * @param string $moduleName Module name + * @param string $group Optionally validate that the module is in a specific group + * @param bool $ignoreCache If true, force-creates a new instance and does not cache it + * + * @return ApiBase|null The new module instance, or null if failed */ public function getModule( $moduleName, $group = null, $ignoreCache = false ) { if ( !isset( $this->mModules[$moduleName] ) ) { return null; } - $grpCls = $this->mModules[$moduleName]; - if ( $group !== null && $grpCls[0] !== $group ) { + + list( $moduleGroup, $moduleClass, $moduleFactory ) = $this->mModules[$moduleName]; + + if ( $group !== null && $moduleGroup !== $group ) { return null; } + if ( !$ignoreCache && isset( $this->mInstances[$moduleName] ) ) { // already exists return $this->mInstances[$moduleName]; } else { // new instance - $class = $grpCls[1]; - $instance = new $class ( $this->mParent, $moduleName ); + $instance = $this->instantiateModule( $moduleName, $moduleClass, $moduleFactory ); + if ( !$ignoreCache ) { // cache this instance in case it is needed later $this->mInstances[$moduleName] = $instance; } + return $instance; } } /** + * Instantiate the module using the given class or factory function. + * + * @param string $name The identifier for this module. + * @param string $class The class where this module is implemented. + * @param callable|null $factory Callback for instantiating the module. + * + * @throws MWException + * @return ApiBase + */ + private function instantiateModule( $name, $class, $factory = null ) { + if ( $factory !== null ) { + // create instance from factory + $instance = call_user_func( $factory, $this->mParent, $name ); + + if ( !$instance instanceof $class ) { + throw new MWException( "The factory function for module $name did not return an instance of $class!" ); + } + } else { + // create instance from class name + $instance = new $class( $this->mParent, $name ); + } + + return $instance; + } + + /** * Get an array of modules in a specific group or all if no group is set. - * @param string $group optional group filter - * @return array list of module names + * @param string $group Optional group filter + * @return array List of module names */ public function getNames( $group = null ) { if ( $group === null ) { @@ -116,13 +221,14 @@ class ApiModuleManager extends ContextSource { $result[] = $name; } } + return $result; } /** * Create an array of (moduleName => moduleClass) for a specific group or for all. - * @param string $group name of the group to get or null for all - * @return array name=>class map + * @param string $group Name of the group to get or null for all + * @return array Name=>class map */ public function getNamesWithClasses( $group = null ) { $result = array(); @@ -131,34 +237,50 @@ class ApiModuleManager extends ContextSource { $result[$name] = $grpCls[1]; } } + return $result; } /** + * Returns the class name of the given module + * + * @param string $module Module name + * @return string|bool class name or false if the module does not exist + * @since 1.24 + */ + public function getClassName( $module ) { + if ( isset( $this->mModules[$module] ) ) { + return $this->mModules[$module][1]; + } + + return false; + } + + /** * Returns true if the specific module is defined at all or in a specific group. - * @param string $moduleName module name - * @param string $group group name to check against, or null to check all groups, - * @return boolean true if defined + * @param string $moduleName Module name + * @param string $group Group name to check against, or null to check all groups, + * @return bool True if defined */ public function isDefined( $moduleName, $group = null ) { if ( isset( $this->mModules[$moduleName] ) ) { return $group === null || $this->mModules[$moduleName][0] === $group; - } else { - return false; } + + return false; } /** * Returns the group name for the given module * @param string $moduleName - * @return string group name or null if missing + * @return string Group name or null if missing */ public function getModuleGroup( $moduleName ) { if ( isset( $this->mModules[$moduleName] ) ) { return $this->mModules[$moduleName][0]; - } else { - return null; } + + return null; } /** diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index c18036cf..04e931d2 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -61,8 +61,8 @@ class ApiMove extends ApiBase { if ( $toTitle->getNamespace() == NS_FILE && !RepoGroup::singleton()->getLocalRepo()->findFile( $toTitle ) - && wfFindFile( $toTitle ) ) - { + && wfFindFile( $toTitle ) + ) { if ( !$params['ignorewarnings'] && $user->isAllowed( 'reupload-shared' ) ) { $this->dieUsageMsg( 'sharedfile-exists' ); } elseif ( !$user->isAllowed( 'reupload-shared' ) ) { @@ -77,7 +77,11 @@ class ApiMove extends ApiBase { $this->dieUsageMsg( reset( $retval ) ); } - $r = array( 'from' => $fromTitle->getPrefixedText(), 'to' => $toTitle->getPrefixedText(), 'reason' => $params['reason'] ); + $r = array( + 'from' => $fromTitle->getPrefixedText(), + 'to' => $toTitle->getPrefixedText(), + 'reason' => $params['reason'] + ); if ( $fromTitle->exists() ) { //NOTE: we assume that if the old title exists, it's because it was re-created as @@ -115,7 +119,7 @@ class ApiMove extends ApiBase { // Move subpages if ( $params['movesubpages'] ) { $r['subpages'] = $this->moveSubpages( $fromTitle, $toTitle, - $params['reason'], $params['noredirect'] ); + $params['reason'], $params['noredirect'] ); $result->setIndexedTagName( $r['subpages'], 'subpage' ); if ( $params['movetalk'] ) { @@ -130,8 +134,10 @@ class ApiMove extends ApiBase { $watch = $params['watchlist']; } elseif ( $params['watch'] ) { $watch = 'watch'; + $this->logFeatureUsage( 'action=move&watch' ); } elseif ( $params['unwatch'] ) { $watch = 'unwatch'; + $this->logFeatureUsage( 'action=move&unwatch' ); } // Watch pages @@ -144,8 +150,8 @@ class ApiMove extends ApiBase { /** * @param Title $fromTitle * @param Title $toTitle - * @param $reason - * @param $noredirect + * @param string $reason + * @param bool $noredirect * @return array */ public function moveSubpages( $fromTitle, $toTitle, $reason, $noredirect ) { @@ -153,20 +159,21 @@ class ApiMove extends ApiBase { $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; + } + + // 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; } @@ -188,10 +195,6 @@ class ApiMove extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'reason' => '', 'movetalk' => false, 'movesubpages' => false, @@ -219,79 +222,35 @@ class ApiMove extends ApiBase { public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'from' => "Title of the page you want to move. Cannot be used together with {$p}fromid", 'fromid' => "Page ID of the page you want to move. Cannot be used together with {$p}from", 'to' => 'Title you want to rename the page to', - 'token' => 'A move token previously retrieved through prop=info', 'reason' => 'Reason for the move', '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', - 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', + 'watchlist' => 'Unconditionally add or remove the page from your ' . + 'watchlist, use preferences or do not change watch', 'ignorewarnings' => 'Ignore any warnings' ); } - public function getResultProperties() { - return array( - '' => array( - 'from' => 'string', - 'to' => 'string', - 'reason' => 'string', - 'redirectcreated' => 'boolean', - 'moveoverredirect' => 'boolean', - 'talkfrom' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'talkto' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'talkmoveoverredirect' => 'boolean', - 'talkmove-error-code' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'talkmove-error-info' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { - return 'Move a page'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getRequireOnlyOneParameterErrorMessages( array( 'from', 'fromid' ) ), - array( - array( 'invalidtitle', 'from' ), - array( 'nosuchpageid', 'fromid' ), - array( 'notanarticle' ), - array( 'invalidtitle', 'to' ), - array( 'sharedfile-exists' ), - ) - ); + return 'Move a page.'; } public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { return array( - 'api.php?action=move&from=Badtitle&to=Goodtitle&token=123ABC&reason=Misspelled%20title&movetalk=&noredirect=' + 'api.php?action=move&from=Badtitle&to=Goodtitle&token=123ABC&' . + 'reason=Misspelled%20title&movetalk=&noredirect=' ); } diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index 315ace37..7fb045e3 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -40,11 +40,11 @@ class ApiOpenSearch extends ApiBase { if ( in_array( $format, $allowed ) ) { return $this->getMain()->createPrinterByName( $format ); } + return $this->getMain()->createPrinterByName( $allowed[0] ); } public function execute() { - global $wgEnableOpenSearchSuggest, $wgSearchSuggestCacheExpiry; $params = $this->extractRequestParams(); $search = $params['search']; $limit = $params['limit']; @@ -52,35 +52,15 @@ class ApiOpenSearch extends ApiBase { $suggest = $params['suggest']; // Some script that was loaded regardless of wgEnableOpenSearchSuggest, likely cached. - if ( $suggest && !$wgEnableOpenSearchSuggest ) { + if ( $suggest && !$this->getConfig()->get( 'EnableOpenSearchSuggest' ) ) { $searches = array(); } else { // Open search results may be stored for a very long time - $this->getMain()->setCacheMaxAge( $wgSearchSuggestCacheExpiry ); + $this->getMain()->setCacheMaxAge( $this->getConfig()->get( 'SearchSuggestCacheExpiry' ) ); $this->getMain()->setCacheMode( 'public' ); - $searches = PrefixSearch::titleSearch( $search, $limit, - $namespaces ); - - // if the content language has variants, try to retrieve fallback results - $fallbackLimit = $limit - count( $searches ); - if ( $fallbackLimit > 0 ) { - global $wgContLang; - - $fallbackSearches = $wgContLang->autoConvertToAllVariants( $search ); - $fallbackSearches = array_diff( array_unique( $fallbackSearches ), array( $search ) ); - - foreach ( $fallbackSearches as $fbs ) { - $fallbackSearchResult = PrefixSearch::titleSearch( $fbs, $fallbackLimit, - $namespaces ); - $searches = array_merge( $searches, $fallbackSearchResult ); - $fallbackLimit -= count( $fallbackSearchResult ); - - if ( $fallbackLimit == 0 ) { - break; - } - } - } + $searcher = new StringPrefixSearch; + $searches = $searcher->searchWithVariants( $search, $limit, $namespaces ); } // Set top level elements $result = $this->getResult(); @@ -92,7 +72,7 @@ class ApiOpenSearch extends ApiBase { return array( 'search' => null, 'limit' => array( - ApiBase::PARAM_DFLT => 10, + ApiBase::PARAM_DFLT => $this->getConfig()->get( 'OpenSearchDefaultLimit' ), ApiBase::PARAM_TYPE => 'limit', ApiBase::PARAM_MIN => 1, ApiBase::PARAM_MAX => 100, @@ -122,7 +102,7 @@ class ApiOpenSearch extends ApiBase { } public function getDescription() { - return 'Search the wiki using the OpenSearch protocol'; + return 'Search the wiki using the OpenSearch protocol.'; } public function getExamples() { diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php index 7256066d..b01dc3e2 100644 --- a/includes/api/ApiOptions.php +++ b/includes/api/ApiOptions.php @@ -31,7 +31,6 @@ * @ingroup API */ class ApiOptions extends ApiBase { - /** * Changes preferences of the current user. */ @@ -99,6 +98,9 @@ class ApiOptions extends ApiBase { $validation = true; } break; + case 'special': + $validation = "cannot be set by this module"; + break; case 'unused': default: $validation = "not a valid preference"; @@ -133,10 +135,6 @@ class ApiOptions extends ApiBase { $optionKinds[] = 'all'; return array( - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'reset' => false, 'resetkinds' => array( ApiBase::PARAM_TYPE => $optionKinds, @@ -155,50 +153,32 @@ class ApiOptions extends ApiBase { ); } - public function getResultProperties() { - return array( - '' => array( - '*' => array( - ApiBase::PROP_TYPE => array( - 'success' - ) - ) - ) - ); - } - public function getParamDescription() { return array( - 'token' => 'An options token previously obtained through the action=tokens', 'reset' => 'Resets preferences to the site defaults', 'resetkinds' => 'List of types of options to reset when the "reset" option is set', - 'change' => 'List of changes, formatted name=value (e.g. skin=vector), value cannot contain pipe characters. If no value is given (not even an equals sign), e.g., optionname|otheroption|..., the option will be reset to its default value', + 'change' => array( 'List of changes, formatted name=value (e.g. skin=vector), ' . + 'value cannot contain pipe characters. If no value is given (not ', + 'even an equals sign), e.g., optionname|otheroption|..., the ' . + 'option will be reset to its default value' + ), 'optionname' => 'A name of a option which should have an optionvalue set', - 'optionvalue' => 'A value of the option specified by the optionname, can contain pipe characters', + 'optionvalue' => 'A value of the option specified by the optionname, ' . + 'can contain pipe characters', ); } public function getDescription() { return array( - 'Change preferences of the current user', + 'Change preferences of the current user.', 'Only options which are registered in core or in one of installed extensions,', - 'or as options with keys prefixed with \'userjs-\' (intended to be used by user scripts), can be set.' + 'or as options with keys prefixed with \'userjs-\' (intended to be used by user', + 'scripts), can be set.' ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'notloggedin', 'info' => 'Anonymous users cannot change preferences' ), - array( 'code' => 'nochanges', 'info' => 'No changes were requested' ), - ) ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getHelpUrls() { @@ -209,7 +189,8 @@ class ApiOptions extends ApiBase { return array( 'api.php?action=options&reset=&token=123ABC', 'api.php?action=options&change=skin=vector|hideminor=1&token=123ABC', - 'api.php?action=options&reset=&change=skin=monobook&optionname=nickname&optionvalue=[[User:Beau|Beau]]%20([[User_talk:Beau|talk]])&token=123ABC', + 'api.php?action=options&reset=&change=skin=monobook&optionname=nickname&' . + 'optionvalue=[[User:Beau|Beau]]%20([[User_talk:Beau|talk]])&token=123ABC', ); } } diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index b05cb2b6..0f264675 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -39,7 +39,6 @@ * @since 1.21 derives from ApiBase instead of ApiQueryBase */ class ApiPageSet extends ApiBase { - /** * Constructor flag: The new instance of ApiPageSet will ignore the 'generator=' parameter * @since 1.21 @@ -62,6 +61,7 @@ class ApiPageSet extends ApiBase { private $mSpecialTitles = array(); private $mNormalizedTitles = array(); private $mInterwikiTitles = array(); + /** @var Title[] */ private $mPendingRedirectIDs = array(); private $mConvertedTitles = array(); private $mGoodRevIDs = array(); @@ -69,17 +69,38 @@ class ApiPageSet extends ApiBase { private $mFakePageId = -1; private $mCacheMode = 'public'; private $mRequestedPageFields = array(); + /** @var int */ + private $mDefaultNamespace = NS_MAIN; + /** - * @var int + * Add all items from $values into the result + * @param array $result Output + * @param array $values Values to add + * @param string $flag The name of the boolean flag to mark this element + * @param string $name If given, name of the value */ - private $mDefaultNamespace = NS_MAIN; + private static function addValues( array &$result, $values, $flag = null, $name = null ) { + foreach ( $values as $val ) { + if ( $val instanceof Title ) { + $v = array(); + ApiQueryBase::addTitleInfo( $v, $val ); + } elseif ( $name !== null ) { + $v = array( $name => $val ); + } else { + $v = $val; + } + if ( $flag !== null ) { + $v[$flag] = ''; + } + $result[] = $v; + } + } /** - * Constructor - * @param $dbSource ApiBase Module implementing getDB(). + * @param ApiBase $dbSource Module implementing getDB(). * Allows PageSet to reuse existing db connection from the shared state like ApiQuery. * @param int $flags Zero or more flags like DISABLE_GENERATORS - * @param int $defaultNamespace the namespace to use if none is specified by a prefix. + * @param int $defaultNamespace The namespace to use if none is specified by a prefix. * @since 1.21 accepts $flags instead of two boolean values */ public function __construct( ApiBase $dbSource, $flags = 0, $defaultNamespace = NS_MAIN ) { @@ -112,7 +133,8 @@ class ApiPageSet extends ApiBase { /** * Populate the PageSet from the request parameters. - * @param bool $isDryRun If true, instantiates generator, but only to mark relevant parameters as used + * @param bool $isDryRun If true, instantiates generator, but only to mark + * relevant parameters as used */ private function executeInternal( $isDryRun ) { $this->profileIn(); @@ -200,8 +222,9 @@ class ApiPageSet extends ApiBase { break; case 'revids': if ( $this->mResolveRedirects ) { - $this->setWarning( 'Redirect resolution cannot be used together with the revids= parameter. ' . - 'Any redirects the revids= point to have not been resolved.' ); + $this->setWarning( 'Redirect resolution cannot be used ' . + 'together with the revids= parameter. Any redirects ' . + 'the revids= point to have not been resolved.' ); } $this->mResolveRedirects = false; $this->initFromRevIDs( $this->mParams['revids'] ); @@ -244,6 +267,7 @@ class ApiPageSet extends ApiBase { if ( isset( $this->mParams['revids'] ) ) { return 'revids'; } + return null; } @@ -270,7 +294,7 @@ class ApiPageSet extends ApiBase { * 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 + * @return array Array of field names */ public function getPageTableFields() { // Ensure we get minimum required fields @@ -289,6 +313,7 @@ class ApiPageSet extends ApiBase { $this->mRequestedPageFields = array_diff_key( $this->mRequestedPageFields, $pageFlds ); $pageFlds = array_merge( $pageFlds, $this->mRequestedPageFields ); + return array_keys( $pageFlds ); } @@ -304,7 +329,7 @@ class ApiPageSet extends ApiBase { /** * All Title objects provided. - * @return array of Title objects + * @return Title[] */ public function getTitles() { return $this->mTitles; @@ -320,7 +345,7 @@ class ApiPageSet extends ApiBase { /** * Title objects that were found in the database. - * @return array page_id (int) => Title (obj) + * @return Title[] Array page_id (int) => Title (obj) */ public function getGoodTitles() { return $this->mGoodTitles; @@ -337,7 +362,7 @@ class ApiPageSet extends ApiBase { /** * Title objects that were NOT found in the database. * The array's index will be negative for each item - * @return array of Title objects + * @return Title[] */ public function getMissingTitles() { return $this->mMissingTitles; @@ -346,7 +371,7 @@ class ApiPageSet extends ApiBase { /** * Titles that were deemed invalid by Title::newFromText() * The array's index will be unique and negative for each item - * @return array of strings (not Title objects) + * @return string[] Array of strings (not Title objects) */ public function getInvalidTitles() { return $this->mInvalidTitles; @@ -354,7 +379,7 @@ class ApiPageSet extends ApiBase { /** * Page IDs that were not found in the database - * @return array of page IDs + * @return array Array of page IDs */ public function getMissingPageIDs() { return $this->mMissingPageIDs; @@ -363,7 +388,7 @@ class ApiPageSet extends ApiBase { /** * Get a list of redirect resolutions - maps a title to its redirect * target, as an array of output-ready arrays - * @return array + * @return Title[] */ public function getRedirectTitles() { return $this->mRedirectTitles; @@ -372,8 +397,8 @@ class ApiPageSet extends ApiBase { /** * Get a list of redirect resolutions - maps a title to its redirect * target. - * @param $result ApiResult - * @return array of prefixed_title (string) => Title object + * @param ApiResult $result + * @return array Array of prefixed_title (string) => Title object * @since 1.21 */ public function getRedirectTitlesAsResult( $result = null ) { @@ -383,7 +408,7 @@ class ApiPageSet extends ApiBase { 'from' => strval( $titleStrFrom ), 'to' => $titleTo->getPrefixedText(), ); - if ( $titleTo->getFragment() !== '' ) { + if ( $titleTo->hasFragment() ) { $r['tofragment'] = $titleTo->getFragment(); } $values[] = $r; @@ -391,13 +416,14 @@ class ApiPageSet extends ApiBase { if ( !empty( $values ) && $result ) { $result->setIndexedTagName( $values, 'r' ); } + return $values; } /** * Get a list of title normalizations - maps a title to its normalized * version. - * @return array raw_prefixed_title (string) => prefixed_title (string) + * @return array Array of raw_prefixed_title (string) => prefixed_title (string) */ public function getNormalizedTitles() { return $this->mNormalizedTitles; @@ -406,8 +432,8 @@ class ApiPageSet extends ApiBase { /** * Get a list of title normalizations - maps a title to its normalized * version in the form of result array. - * @param $result ApiResult - * @return array of raw_prefixed_title (string) => prefixed_title (string) + * @param ApiResult $result + * @return array Array of raw_prefixed_title (string) => prefixed_title (string) * @since 1.21 */ public function getNormalizedTitlesAsResult( $result = null ) { @@ -421,13 +447,14 @@ class ApiPageSet extends ApiBase { if ( !empty( $values ) && $result ) { $result->setIndexedTagName( $values, 'n' ); } + return $values; } /** * Get a list of title conversions - maps a title to its converted * version. - * @return array raw_prefixed_title (string) => prefixed_title (string) + * @return array Array of raw_prefixed_title (string) => prefixed_title (string) */ public function getConvertedTitles() { return $this->mConvertedTitles; @@ -436,8 +463,8 @@ class ApiPageSet extends ApiBase { /** * Get a list of title conversions - maps a title to its converted * version as a result array. - * @param $result ApiResult - * @return array of (from, to) strings + * @param ApiResult $result + * @return array Array of (from, to) strings * @since 1.21 */ public function getConvertedTitlesAsResult( $result = null ) { @@ -451,13 +478,14 @@ class ApiPageSet extends ApiBase { if ( !empty( $values ) && $result ) { $result->setIndexedTagName( $values, 'c' ); } + return $values; } /** * Get a list of interwiki titles - maps a title to its interwiki * prefix. - * @return array raw_prefixed_title (string) => interwiki_prefix (string) + * @return array Array of raw_prefixed_title (string) => interwiki_prefix (string) */ public function getInterwikiTitles() { return $this->mInterwikiTitles; @@ -466,9 +494,9 @@ class ApiPageSet extends ApiBase { /** * Get a list of interwiki titles - maps a title to its interwiki * prefix as result. - * @param $result ApiResult - * @param $iwUrl boolean - * @return array raw_prefixed_title (string) => interwiki_prefix (string) + * @param ApiResult $result + * @param bool $iwUrl + * @return array Array of raw_prefixed_title (string) => interwiki_prefix (string) * @since 1.21 */ public function getInterwikiTitlesAsResult( $result = null, $iwUrl = false ) { @@ -487,12 +515,53 @@ class ApiPageSet extends ApiBase { if ( !empty( $values ) && $result ) { $result->setIndexedTagName( $values, 'i' ); } + return $values; } /** + * Get an array of invalid/special/missing titles. + * + * @param array $invalidChecks List of types of invalid titles to include. + * Recognized values are: + * - invalidTitles: Titles from $this->getInvalidTitles() + * - special: Titles from $this->getSpecialTitles() + * - missingIds: ids from $this->getMissingPageIDs() + * - missingRevIds: ids from $this->getMissingRevisionIDs() + * - missingTitles: Titles from $this->getMissingTitles() + * - interwikiTitles: Titles from $this->getInterwikiTitlesAsResult() + * @return array Array suitable for inclusion in the response + * @since 1.23 + */ + public function getInvalidTitlesAndRevisions( $invalidChecks = array( 'invalidTitles', + 'special', 'missingIds', 'missingRevIds', 'missingTitles', 'interwikiTitles' ) + ) { + $result = array(); + if ( in_array( "invalidTitles", $invalidChecks ) ) { + self::addValues( $result, $this->getInvalidTitles(), 'invalid', 'title' ); + } + if ( in_array( "special", $invalidChecks ) ) { + self::addValues( $result, $this->getSpecialTitles(), 'special', 'title' ); + } + if ( in_array( "missingIds", $invalidChecks ) ) { + self::addValues( $result, $this->getMissingPageIDs(), 'missing', 'pageid' ); + } + if ( in_array( "missingRevIds", $invalidChecks ) ) { + self::addValues( $result, $this->getMissingRevisionIDs(), 'missing', 'revid' ); + } + if ( in_array( "missingTitles", $invalidChecks ) ) { + self::addValues( $result, $this->getMissingTitles(), 'missing' ); + } + if ( in_array( "interwikiTitles", $invalidChecks ) ) { + self::addValues( $result, $this->getInterwikiTitlesAsResult() ); + } + + return $result; + } + + /** * Get the list of revision IDs (requested with the revids= parameter) - * @return array revID (int) => pageID (int) + * @return array Array of revID (int) => pageID (int) */ public function getRevisionIDs() { return $this->mGoodRevIDs; @@ -500,7 +569,7 @@ class ApiPageSet extends ApiBase { /** * Revision IDs that were not found in the database - * @return array of revision IDs + * @return array Array of revision IDs */ public function getMissingRevisionIDs() { return $this->mMissingRevIDs; @@ -508,8 +577,8 @@ class ApiPageSet extends ApiBase { /** * Revision IDs that were not found in the database as result array. - * @param $result ApiResult - * @return array of revision IDs + * @param ApiResult $result + * @return array Array of revision IDs * @since 1.21 */ public function getMissingRevisionIDsAsResult( $result = null ) { @@ -522,12 +591,13 @@ class ApiPageSet extends ApiBase { if ( !empty( $values ) && $result ) { $result->setIndexedTagName( $values, 'rev' ); } + return $values; } /** * Get the list of titles with negative namespace - * @return array Title + * @return Title[] */ public function getSpecialTitles() { return $this->mSpecialTitles; @@ -543,7 +613,7 @@ class ApiPageSet extends ApiBase { /** * Populate this PageSet from a list of Titles - * @param array $titles of Title objects + * @param array $titles Array of Title objects */ public function populateFromTitles( $titles ) { $this->profileIn(); @@ -553,7 +623,7 @@ class ApiPageSet extends ApiBase { /** * Populate this PageSet from a list of page IDs - * @param array $pageIDs of page IDs + * @param array $pageIDs Array of page IDs */ public function populateFromPageIDs( $pageIDs ) { $this->profileIn(); @@ -563,8 +633,8 @@ class ApiPageSet extends ApiBase { /** * Populate this PageSet from a rowset returned from the database - * @param $db DatabaseBase object - * @param $queryResult ResultWrapper Query result object + * @param DatabaseBase $db + * @param ResultWrapper $queryResult Query result object */ public function populateFromQueryResult( $db, $queryResult ) { $this->profileIn(); @@ -574,7 +644,7 @@ class ApiPageSet extends ApiBase { /** * Populate this PageSet from a list of revision IDs - * @param array $revIDs of revision IDs + * @param array $revIDs Array of revision IDs */ public function populateFromRevisionIDs( $revIDs ) { $this->profileIn(); @@ -584,7 +654,7 @@ class ApiPageSet extends ApiBase { /** * Extract all requested fields from the row received from the database - * @param $row Result row + * @param stdClass $row Result row */ public function processDbRow( $row ) { // Store Title object in various data structures @@ -627,7 +697,7 @@ class ApiPageSet extends ApiBase { * #5 Substitute the original LinkBatch object with the new list * #6 Repeat from step #1 * - * @param array $titles of Title objects or strings + * @param array $titles Array of Title objects or strings */ private function initFromTitles( $titles ) { // Get validated and normalized title objects @@ -642,7 +712,7 @@ class ApiPageSet extends ApiBase { // Get pageIDs data from the `page` table $this->profileDBIn(); $res = $db->select( 'page', $this->getPageTableFields(), $set, - __METHOD__ ); + __METHOD__ ); $this->profileDBOut(); // Hack: get the ns:titles stored in array(ns => array(titles)) format @@ -654,7 +724,7 @@ class ApiPageSet extends ApiBase { /** * Does the same as initFromTitles(), but is based on page IDs instead - * @param array $pageids of page IDs + * @param array $pageids Array of page IDs */ private function initFromPageIds( $pageids ) { if ( !$pageids ) { @@ -676,7 +746,7 @@ class ApiPageSet extends ApiBase { // Get pageIDs data from the `page` table $this->profileDBIn(); $res = $db->select( 'page', $this->getPageTableFields(), $set, - __METHOD__ ); + __METHOD__ ); $this->profileDBOut(); } @@ -689,8 +759,8 @@ class ApiPageSet extends ApiBase { /** * Iterate through the result of the query on 'page' table, * and for each row create and store title object and save any extra fields requested. - * @param $res ResultWrapper DB Query result - * @param array $remaining of either pageID or ns/title elements (optional). + * @param ResultWrapper $res DB Query result + * @param array $remaining Array of either pageID or ns/title elements (optional). * If given, any missing items will go to $mMissingPageIDs and $mMissingTitles * @param bool $processTitles Must be provided together with $remaining. * If true, treat $remaining as an array of [ns][title] @@ -761,7 +831,7 @@ class ApiPageSet extends ApiBase { /** * Does the same as initFromTitles(), but is based on revision IDs * instead - * @param array $revids of revision IDs + * @param array $revids Array of revision IDs */ private function initFromRevIDs( $revids ) { if ( !$revids ) { @@ -863,7 +933,12 @@ class ApiPageSet extends ApiBase { foreach ( $res as $row ) { $rdfrom = intval( $row->rd_from ); $from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText(); - $to = Title::makeTitle( $row->rd_namespace, $row->rd_title, $row->rd_fragment, $row->rd_interwiki ); + $to = Title::makeTitle( + $row->rd_namespace, + $row->rd_title, + $row->rd_fragment, + $row->rd_interwiki + ); unset( $this->mPendingRedirectIDs[$rdfrom] ); if ( !$to->isExternal() && !isset( $this->mAllPages[$row->rd_namespace][$row->rd_title] ) ) { $lb->add( $row->rd_namespace, $row->rd_title ); @@ -886,6 +961,7 @@ class ApiPageSet extends ApiBase { unset( $this->mPendingRedirectIDs[$id] ); } } + return $lb; } @@ -898,7 +974,7 @@ class ApiPageSet extends ApiBase { * Public caching will only be allowed if *all* the modules that supply * data for a given request return a cache mode of public. * - * @param $params + * @param array|null $params * @return string * @since 1.21 */ @@ -912,7 +988,7 @@ class ApiPageSet extends ApiBase { * This method validates access rights for the title, * and appends normalization values to the output. * - * @param array $titles of Title objects or strings + * @param array $titles Array of Title objects or strings * @return LinkBatch */ private function processTitlesArray( $titles ) { @@ -941,8 +1017,9 @@ class ApiPageSet extends ApiBase { // Variants checking global $wgContLang; if ( $this->mConvertTitles && - count( $wgContLang->getVariants() ) > 1 && - !$titleObj->exists() ) { + count( $wgContLang->getVariants() ) > 1 && + !$titleObj->exists() + ) { // Language::findVariantLink will modify titleText and titleObj into // the canonical variant if possible $titleText = is_string( $title ) ? $title : $titleObj->getPrefixedText(); @@ -999,7 +1076,7 @@ class ApiPageSet extends ApiBase { /** * Returns the input array of integers with all values < 0 removed * - * @param $array array + * @param array $array * @return array */ private static function getPositiveIntegers( $array ) { @@ -1040,6 +1117,7 @@ class ApiPageSet extends ApiBase { $result['generator'] = null; } } + return $result; } @@ -1067,6 +1145,7 @@ class ApiPageSet extends ApiBase { sort( $gens ); self::$generators = $gens; } + return self::$generators; } @@ -1075,19 +1154,17 @@ class ApiPageSet extends ApiBase { 'titles' => 'A list of titles to work on', 'pageids' => 'A list of page IDs to work on', 'revids' => 'A list of revision IDs to work on', - 'generator' => array( 'Get the list of pages to work on by executing the specified query module.', - 'NOTE: generator parameter names must be prefixed with a \'g\', see examples' ), + 'generator' => array( + 'Get the list of pages to work on by executing the specified query module.', + 'NOTE: generator parameter names must be prefixed with a \'g\', see examples' + ), 'redirects' => 'Automatically resolve redirects', - 'converttitles' => array( 'Convert titles to other variants if necessary. Only works if the wiki\'s content language supports variant conversion.', - 'Languages that support variant conversion include ' . implode( ', ', LanguageConverter::$languagesWithVariants ) ), + 'converttitles' => array( + 'Convert titles to other variants if necessary. Only works if ' . + 'the wiki\'s content language supports variant conversion.', + 'Languages that support variant conversion include ' . + implode( ', ', LanguageConverter::$languagesWithVariants ) + ), ); } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'multisource', 'info' => "Cannot use 'pageids' at the same time as 'dataSource'" ), - array( 'code' => 'multisource', 'info' => "Cannot use 'revids' at the same time as 'dataSource'" ), - array( 'code' => 'badgenerator', 'info' => 'Module $generatorName cannot be used as a generator' ), - ) ); - } } diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index 3e1a7531..067b2f59 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -34,7 +34,7 @@ class ApiParamInfo extends ApiBase { */ protected $queryObj; - public function __construct( $main, $action ) { + public function __construct( ApiMain $main, $action ) { parent::__construct( $main, $action ); $this->queryObj = new ApiQuery( $this->getMain(), 'query' ); } @@ -66,10 +66,10 @@ class ApiParamInfo extends ApiBase { /** * If the type is requested in parameters, adds a section to res with module info. - * @param array $params user parameters array - * @param string $type parameter name - * @param array $res store results in this array - * @param ApiResult $resultObj results object to set indexed tag. + * @param array $params User parameters array + * @param string $type Parameter name + * @param array $res Store results in this array + * @param ApiResult $resultObj Results object to set indexed tag. */ private function addModulesInfo( $params, $type, &$res, $resultObj ) { if ( !is_array( $params[$type] ) ) { @@ -99,7 +99,7 @@ class ApiParamInfo extends ApiBase { } /** - * @param $obj ApiBase + * @param ApiBase $obj * @return ApiResult */ private function getClassInfo( $obj ) { @@ -198,6 +198,10 @@ class ApiParamInfo extends ApiBase { $a['required'] = ''; } + if ( $n === 'token' && $obj->needsToken() ) { + $a['tokentype'] = $obj->needsToken(); + } + if ( isset( $p[ApiBase::PARAM_DFLT] ) ) { $type = $p[ApiBase::PARAM_TYPE]; if ( $type === 'boolean' ) { @@ -224,9 +228,16 @@ class ApiParamInfo extends ApiBase { } if ( isset( $p[ApiBase::PARAM_TYPE] ) ) { - $a['type'] = $p[ApiBase::PARAM_TYPE]; + if ( $p[ApiBase::PARAM_TYPE] === 'submodule' ) { + $a['type'] = $obj->getModuleManager()->getNames( $n ); + sort( $a['type'] ); + $a['submodules'] = ''; + } else { + $a['type'] = $p[ApiBase::PARAM_TYPE]; + } if ( is_array( $a['type'] ) ) { - $a['type'] = array_values( $a['type'] ); // to prevent sparse arrays from being serialized to JSON as objects + // To prevent sparse arrays from being serialized to JSON as objects + $a['type'] = array_values( $a['type'] ); $result->setIndexedTagName( $a['type'], 't' ); } } @@ -243,66 +254,6 @@ class ApiParamInfo extends ApiBase { } $result->setIndexedTagName( $retval['parameters'], 'param' ); - $props = $obj->getFinalResultProperties(); - $listResult = null; - if ( $props !== false ) { - $retval['props'] = array(); - - foreach ( $props as $prop => $properties ) { - $propResult = array(); - if ( $prop == ApiBase::PROP_LIST ) { - $listResult = $properties; - continue; - } - if ( $prop != ApiBase::PROP_ROOT ) { - $propResult['name'] = $prop; - } - $propResult['properties'] = array(); - - foreach ( $properties as $name => $p ) { - $propertyResult = array(); - - $propertyResult['name'] = $name; - - if ( !is_array( $p ) ) { - $p = array( ApiBase::PROP_TYPE => $p ); - } - - $propertyResult['type'] = $p[ApiBase::PROP_TYPE]; - - if ( is_array( $propertyResult['type'] ) ) { - $propertyResult['type'] = array_values( $propertyResult['type'] ); - $result->setIndexedTagName( $propertyResult['type'], 't' ); - } - - $nullable = null; - if ( isset( $p[ApiBase::PROP_NULLABLE] ) ) { - $nullable = $p[ApiBase::PROP_NULLABLE]; - } - - if ( $nullable === true ) { - $propertyResult['nullable'] = ''; - } - - $propResult['properties'][] = $propertyResult; - } - - $result->setIndexedTagName( $propResult['properties'], 'property' ); - $retval['props'][] = $propResult; - } - - // default is true for query modules, false for other modules, overridden by ApiBase::PROP_LIST - if ( $listResult === true || ( $listResult !== false && $obj instanceof ApiQueryBase ) ) { - $retval['listresult'] = ''; - } - - $result->setIndexedTagName( $retval['props'], 'prop' ); - } - - // Errors - $retval['errors'] = $this->parseErrors( $obj->getFinalPossibleErrors() ); - $result->setIndexedTagName( $retval['errors'], 'error' ); - return $retval; } @@ -317,6 +268,7 @@ class ApiParamInfo extends ApiBase { sort( $querymodules ); $formatmodules = $this->getMain()->getModuleManager()->getNames( 'format' ); sort( $formatmodules ); + return array( 'modules' => array( ApiBase::PARAM_ISMULTI => true, @@ -340,13 +292,14 @@ class ApiParamInfo extends ApiBase { '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', + 'pagesetmodule' => 'Get information about the pageset module ' . + '(providing titles= and friends) as well', 'formatmodules' => 'List of format module names (value of format= parameter)', ); } public function getDescription() { - return 'Obtain information about certain API parameters and errors'; + return 'Obtain information about certain API parameters and errors.'; } public function getExamples() { diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index a369994b..06fdf85b 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -27,7 +27,7 @@ */ class ApiParse extends ApiBase { - /** @var String $section */ + /** @var string $section */ private $section = null; /** @var Content $content */ @@ -60,7 +60,10 @@ class ApiParse extends ApiBase { $format = $params['contentformat']; if ( !is_null( $page ) && ( !is_null( $text ) || $titleProvided ) ) { - $this->dieUsage( 'The page parameter cannot be used together with the text and title parameters', 'params' ); + $this->dieUsage( + 'The page parameter cannot be used together with the text and title parameters', + 'params' + ); } $prop = array_flip( $params['prop'] ); @@ -76,9 +79,12 @@ class ApiParse extends ApiBase { // TODO: Does this still need $wgTitle? global $wgParser, $wgTitle; - // Currently unnecessary, code to act as a safeguard against any change in current behavior of uselang + // Currently unnecessary, code to act as a safeguard against any change + // in current behavior of uselang $oldLang = null; - if ( isset( $params['uselang'] ) && $params['uselang'] != $this->getContext()->getLanguage()->getCode() ) { + if ( isset( $params['uselang'] ) + && $params['uselang'] != $this->getContext()->getLanguage()->getCode() + ) { $oldLang = $this->getContext()->getLanguage(); // Backup language $this->getContext()->setLanguage( Language::factory( $params['uselang'] ) ); } @@ -125,7 +131,7 @@ class ApiParse extends ApiBase { 'action' => 'query', 'redirects' => '', ); - if ( !is_null ( $pageid ) ) { + if ( !is_null( $pageid ) ) { $reqParams['pageids'] = $pageid; } else { // $page $reqParams['titles'] = $page; @@ -170,15 +176,19 @@ class ApiParse extends ApiBase { if ( !$titleObj || $titleObj->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $title ) ); } - if ( !$titleObj->canExist() ) { - $this->dieUsage( "Namespace doesn't allow actual pages", 'pagecannotexist' ); - } $wgTitle = $titleObj; - $pageObj = WikiPage::factory( $titleObj ); + if ( $titleObj->canExist() ) { + $pageObj = WikiPage::factory( $titleObj ); + } else { + // Do like MediaWiki::initializeArticle() + $article = Article::newFromTitle( $titleObj, $this->getContext() ); + $pageObj = $article->getPage(); + } $popts = $this->makeParserOptions( $pageObj, $params ); + $textProvided = !is_null( $text ); - if ( is_null( $text ) ) { + if ( !$textProvided ) { if ( $titleProvided && ( $prop || $params['generatexml'] ) ) { $this->setWarning( "'title' used without 'text', and parsed page properties were requested " . @@ -191,7 +201,7 @@ class ApiParse extends ApiBase { // If we are parsing text, do not use the content model of the default // API title, but default to wikitext to keep BC. - if ( !$titleProvided && is_null( $model ) ) { + if ( $textProvided && !$titleProvided && is_null( $model ) ) { $model = CONTENT_MODEL_WIKITEXT; $this->setWarning( "No 'title' or 'contentmodel' was given, assuming $model." ); } @@ -203,7 +213,7 @@ class ApiParse extends ApiBase { } if ( $this->section !== false ) { - $this->content = $this->getSectionContent( $this->content, $titleObj->getText() ); + $this->content = $this->getSectionContent( $this->content, $titleObj->getPrefixedText() ); } if ( $params['pst'] || $params['onlypst'] ) { @@ -219,6 +229,7 @@ class ApiParse extends ApiBase { ApiResult::setContent( $result_array['wikitext'], $this->content->serialize( $format ) ); } $result->addValue( null, $this->getModuleName(), $result_array ); + return; } @@ -242,6 +253,10 @@ class ApiParse extends ApiBase { $result_array['redirects'] = $redirValues; } + if ( $params['disabletoc'] ) { + $p_result->setTOCEnabled( false ); + } + if ( isset( $prop['text'] ) ) { $result_array['text'] = array(); ApiResult::setContent( $result_array['text'], $p_result->getText() ); @@ -249,10 +264,13 @@ class ApiParse extends ApiBase { if ( !is_null( $params['summary'] ) ) { $result_array['parsedsummary'] = array(); - ApiResult::setContent( $result_array['parsedsummary'], Linker::formatComment( $params['summary'], $titleObj ) ); + ApiResult::setContent( + $result_array['parsedsummary'], + Linker::formatComment( $params['summary'], $titleObj ) + ); } - if ( isset( $prop['langlinks'] ) || isset( $prop['languageshtml'] ) ) { + if ( isset( $prop['langlinks'] ) ) { $langlinks = $p_result->getLanguageLinks(); if ( $params['effectivelanglinks'] ) { @@ -268,12 +286,6 @@ class ApiParse extends ApiBase { if ( isset( $prop['langlinks'] ) ) { $result_array['langlinks'] = $this->formatLangLinks( $langlinks ); } - if ( isset( $prop['languageshtml'] ) ) { - $languagesHtml = $this->languagesHtml( $langlinks ); - - $result_array['languageshtml'] = array(); - ApiResult::setContent( $result_array['languageshtml'], $languagesHtml ); - } if ( isset( $prop['categories'] ) ) { $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() ); } @@ -300,14 +312,14 @@ class ApiParse extends ApiBase { if ( isset( $prop['displaytitle'] ) ) { $result_array['displaytitle'] = $p_result->getDisplayTitle() ? - $p_result->getDisplayTitle() : - $titleObj->getPrefixedText(); + $p_result->getDisplayTitle() : + $titleObj->getPrefixedText(); } if ( isset( $prop['headitems'] ) || isset( $prop['headhtml'] ) ) { $context = $this->getContext(); $context->setTitle( $titleObj ); - $context->getOutput()->addParserOutputNoText( $p_result ); + $context->getOutput()->addParserOutputMetadata( $p_result ); if ( isset( $prop['headitems'] ) ) { $headItems = $this->formatHeadItems( $p_result->getHeadItems() ); @@ -321,10 +333,20 @@ class ApiParse extends ApiBase { if ( isset( $prop['headhtml'] ) ) { $result_array['headhtml'] = array(); - ApiResult::setContent( $result_array['headhtml'], $context->getOutput()->headElement( $context->getSkin() ) ); + ApiResult::setContent( + $result_array['headhtml'], + $context->getOutput()->headElement( $context->getSkin() ) + ); } } + if ( isset( $prop['modules'] ) ) { + $result_array['modules'] = array_values( array_unique( $p_result->getModules() ) ); + $result_array['modulescripts'] = array_values( array_unique( $p_result->getModuleScripts() ) ); + $result_array['modulestyles'] = array_values( array_unique( $p_result->getModuleStyles() ) ); + $result_array['modulemessages'] = array_values( array_unique( $p_result->getModuleMessages() ) ); + } + if ( isset( $prop['iwlinks'] ) ) { $result_array['iwlinks'] = $this->formatIWLinks( $p_result->getInterwikiLinks() ); } @@ -341,6 +363,16 @@ class ApiParse extends ApiBase { $result_array['properties'] = $this->formatProperties( $p_result->getProperties() ); } + if ( isset( $prop['limitreportdata'] ) ) { + $result_array['limitreportdata'] = + $this->formatLimitReportData( $p_result->getLimitReportData() ); + } + if ( isset( $prop['limitreporthtml'] ) ) { + $limitreportHtml = EditPage::getPreviewLimitReport( $p_result ); + $result_array['limitreporthtml'] = array(); + ApiResult::setContent( $result_array['limitreporthtml'], $limitreportHtml ); + } + if ( $params['generatexml'] ) { if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) { $this->dieUsage( "generatexml is only supported for wikitext content", "notwikitext" ); @@ -368,7 +400,12 @@ class ApiParse extends ApiBase { 'iwlinks' => 'iw', 'sections' => 's', 'headitems' => 'hi', + 'modules' => 'm', + 'modulescripts' => 'm', + 'modulestyles' => 'm', + 'modulemessages' => 'm', 'properties' => 'pp', + 'limitreportdata' => 'lr', ); $this->setIndexedTagNames( $result_array, $result_mapping ); $result->addValue( null, $this->getModuleName(), $result_array ); @@ -393,16 +430,18 @@ class ApiParse extends ApiBase { $popts->enableLimitReport( !$params['disablepp'] ); $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] ); $popts->setIsSectionPreview( $params['sectionpreview'] ); + $popts->setEditSection( !$params['disableeditsection'] ); wfProfileOut( __METHOD__ ); + return $popts; } /** - * @param $page WikiPage - * @param $popts ParserOptions - * @param $pageId Int - * @param $getWikitext Bool + * @param WikiPage $page + * @param ParserOptions $popts + * @param int $pageId + * @param bool $getWikitext * @return ParserOutput */ private function getParsedContent( WikiPage $page, $popts, $pageId = null, $getWikitext = false ) { @@ -411,24 +450,31 @@ class ApiParse extends ApiBase { if ( $this->section !== false && $this->content !== null ) { $this->content = $this->getSectionContent( $this->content, - !is_null( $pageId ) ? 'page id ' . $pageId : $page->getTitle()->getText() ); + !is_null( $pageId ) ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText() + ); // Not cached (save or load) return $this->content->getParserOutput( $page->getTitle(), null, $popts ); - } else { - // Try the parser cache first - // getParserOutput will save to Parser cache if able - $pout = $page->getParserOutput( $popts ); - if ( !$pout ) { - $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' ); - } - if ( $getWikitext ) { - $this->content = $page->getContent( Revision::RAW ); - } - return $pout; } + + // Try the parser cache first + // getParserOutput will save to Parser cache if able + $pout = $page->getParserOutput( $popts ); + if ( !$pout ) { + $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' ); + } + if ( $getWikitext ) { + $this->content = $page->getContent( Revision::RAW ); + } + + return $pout; } + /** + * @param Content $content + * @param string $what Identifies the content in error messages, e.g. page title. + * @return Content|bool + */ private function getSectionContent( Content $content, $what ) { // Not cached (save or load) $section = $content->getSection( $this->section ); @@ -439,6 +485,7 @@ class ApiParse extends ApiBase { $this->dieUsage( "Sections are not supported by " . $what, 'nosuchsection' ); $section = false; } + return $section; } @@ -452,64 +499,67 @@ class ApiParse extends ApiBase { $entry['lang'] = $bits[0]; if ( $title ) { $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); + // localised language name in user language (maybe set by uselang=) + $entry['langname'] = Language::fetchLanguageName( + $title->getInterwiki(), + $this->getLanguage()->getCode() + ); + + // native language name + $entry['autonym'] = Language::fetchLanguageName( $title->getInterwiki() ); } ApiResult::setContent( $entry, $bits[1] ); $result[] = $entry; } + return $result; } private function formatCategoryLinks( $links ) { $result = array(); + + if ( !$links ) { + return $result; + } + + // Fetch hiddencat property + $lb = new LinkBatch; + $lb->setArray( array( NS_CATEGORY => $links ) ); + $db = $this->getDB(); + $res = $db->select( array( 'page', 'page_props' ), + array( 'page_title', 'pp_propname' ), + $lb->constructSet( 'page', $db ), + __METHOD__, + array(), + array( 'page_props' => array( + 'LEFT JOIN', array( 'pp_propname' => 'hiddencat', 'pp_page = page_id' ) + ) ) + ); + $hiddencats = array(); + foreach ( $res as $row ) { + $hiddencats[$row->page_title] = isset( $row->pp_propname ); + } + foreach ( $links as $link => $sortkey ) { $entry = array(); $entry['sortkey'] = $sortkey; ApiResult::setContent( $entry, $link ); + if ( !isset( $hiddencats[$link] ) ) { + $entry['missing'] = ''; + } elseif ( $hiddencats[$link] ) { + $entry['hidden'] = ''; + } $result[] = $entry; } + return $result; } private function categoriesHtml( $categories ) { $context = $this->getContext(); $context->getOutput()->addCategoryLinks( $categories ); - return $context->getSkin()->getCategories(); - } - /** - * @deprecated since 1.18 No modern skin generates language links this way, please use language links - * data to generate your own HTML. - * @param $languages array - * @return string - */ - private function languagesHtml( $languages ) { - wfDeprecated( __METHOD__, '1.18' ); - - global $wgContLang, $wgHideInterlanguageLinks; - - if ( $wgHideInterlanguageLinks || count( $languages ) == 0 ) { - return ''; - } - - $s = htmlspecialchars( wfMessage( 'otherlanguages' )->text() . wfMessage( 'colon-separator' )->text() ); - - $langs = array(); - foreach ( $languages as $l ) { - $nt = Title::newFromText( $l ); - $text = Language::fetchLanguageName( $nt->getInterwiki() ); - - $langs[] = Html::element( 'a', - array( 'href' => $nt->getFullURL(), 'title' => $nt->getText(), 'class' => 'external' ), - $text == '' ? $l : $text ); - } - - $s .= implode( wfMessage( 'pipe-separator' )->escaped(), $langs ); - - if ( $wgContLang->isRTL() ) { - $s = Html::rawElement( 'span', array( 'dir' => 'LTR' ), $s ); - } - - return $s; + return $context->getSkin()->getCategories(); } private function formatLinks( $links ) { @@ -525,6 +575,7 @@ class ApiParse extends ApiBase { $result[] = $entry; } } + return $result; } @@ -544,6 +595,7 @@ class ApiParse extends ApiBase { $result[] = $entry; } } + return $result; } @@ -555,6 +607,7 @@ class ApiParse extends ApiBase { ApiResult::setContent( $entry, $content ); $result[] = $entry; } + return $result; } @@ -566,6 +619,7 @@ class ApiParse extends ApiBase { ApiResult::setContent( $entry, $value ); $result[] = $entry; } + return $result; } @@ -577,6 +631,26 @@ class ApiParse extends ApiBase { ApiResult::setContent( $entry, $link ); $result[] = $entry; } + + return $result; + } + + private function formatLimitReportData( $limitReportData ) { + $result = array(); + $apiResult = $this->getResult(); + + foreach ( $limitReportData as $name => $value ) { + $entry = array(); + $entry['name'] = $name; + if ( !is_array( $value ) ) { + $value = array( $value ); + } + $apiResult->setIndexedTagName( $value, 'param' ); + $apiResult->setIndexedTagName_recursive( $value, 'param' ); + $entry = array_merge( $entry, $value ); + $result[] = $entry; + } + return $result; } @@ -602,12 +676,12 @@ class ApiParse extends ApiBase { ApiBase::PARAM_TYPE => 'integer', ), 'prop' => array( - ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid|displaytitle|iwlinks|properties', + ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|' . + 'images|externallinks|sections|revid|displaytitle|iwlinks|properties', ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => array( 'text', 'langlinks', - 'languageshtml', 'categories', 'categorieshtml', 'links', @@ -619,9 +693,12 @@ class ApiParse extends ApiBase { 'displaytitle', 'headitems', 'headhtml', + 'modules', 'iwlinks', 'wikitext', 'properties', + 'limitreportdata', + 'limitreporthtml', ) ), 'pst' => false, @@ -630,9 +707,11 @@ class ApiParse extends ApiBase { 'uselang' => null, 'section' => null, 'disablepp' => false, + 'disableeditsection' => false, 'generatexml' => false, 'preview' => false, 'sectionpreview' => false, + 'disabletoc' => false, 'contentformat' => array( ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(), ), @@ -645,12 +724,13 @@ class ApiParse extends ApiBase { public function getParamDescription() { $p = $this->getModulePrefix(); $wikitext = CONTENT_MODEL_WIKITEXT; + return array( 'text' => "Text to parse. Use {$p}title or {$p}contentmodel to control the content model", 'summary' => 'Summary to parse', 'redirects' => "If the {$p}page or the {$p}pageid parameter is set to a redirect, resolve it", 'title' => "Title of page the text belongs to. " . - "If omitted, \"API\" is used as the title with content model $wikitext", + "If omitted, {$p}contentmodel must be specified, and \"API\" will be used as the title", 'page' => "Parse the content of this page. Cannot be used together with {$p}text and {$p}title", 'pageid' => "Parse the content of this page. Overrides {$p}page", 'oldid' => "Parse the content of this revision. Overrides {$p}page and {$p}pageid", @@ -660,7 +740,6 @@ class ApiParse extends ApiBase { ' langlinks - Gives the language links in the parsed wikitext', ' categories - Gives the categories in the parsed wikitext', ' categorieshtml - Gives the HTML version of the categories', - ' languageshtml - Gives the HTML version of the language links', ' links - Gives the internal links in the parsed wikitext', ' templates - Gives the templates in the parsed wikitext', ' images - Gives the images in the parsed wikitext', @@ -670,13 +749,18 @@ class ApiParse extends ApiBase { ' displaytitle - Adds the title of the parsed wikitext', ' headitems - Gives items to put in the <head> of the page', ' headhtml - Gives parsed <head> of the page', + ' modules - Gives the ResourceLoader modules used on the page', ' iwlinks - Gives interwiki links in the parsed wikitext', ' wikitext - Gives the original wikitext that was parsed', ' properties - Gives various properties defined in the parsed wikitext', + ' limitreportdata - Gives the limit report in a structured way.', + " Gives no data, when {$p}disablepp is set.", + ' limitreporthtml - Gives the HTML version of the limit report.', + " Gives no data, when {$p}disablepp is set.", ), 'effectivelanglinks' => array( 'Includes language links supplied by extensions', - '(for use with prop=langlinks|languageshtml)', + '(for use with prop=langlinks)', ), 'pst' => array( 'Do a pre-save transform on the input before parsing it', @@ -690,16 +774,18 @@ class ApiParse extends ApiBase { 'uselang' => 'Which language to parse the request in', 'section' => 'Only retrieve the content of this section number', 'disablepp' => 'Disable the PP Report from the parser output', + 'disableeditsection' => 'Disable edit section links from the parser output', 'generatexml' => "Generate XML parse tree (requires contentmodel=$wikitext)", 'preview' => 'Parse in preview mode', 'sectionpreview' => 'Parse in section preview mode (enables preview mode too)', + 'disabletoc' => 'Disable table of contents in output', 'contentformat' => array( 'Content serialization format used for the input text', "Only valid when used with {$p}text", ), 'contentmodel' => array( - "Content model of the input text. Default is the model of the " . - "specified ${p}title, or $wikitext if ${p}title is not specified", + "Content model of the input text. If omitted, ${p}title must be specified, " . + "and default will be the model of the specified ${p}title", "Only valid when used with {$p}text", ), ); @@ -707,9 +793,11 @@ class ApiParse extends ApiBase { public function getDescription() { $p = $this->getModulePrefix(); + return array( - 'Parses content and returns parser output', - 'See the various prop-Modules of action=query to get information from the current version of a page', + 'Parses content and returns parser output.', + 'See the various prop-Modules of action=query to get information from the current' . + 'version of a page.', 'There are several ways to specify the text to parse:', "1) Specify a page or revision, using {$p}page, {$p}pageid, or {$p}oldid.", "2) Specify content explicitly, using {$p}text, {$p}title, and {$p}contentmodel.", @@ -717,26 +805,12 @@ class ApiParse extends ApiBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'params', 'info' => 'The page parameter cannot be used together with the text and title parameters' ), - array( 'code' => 'missingrev', 'info' => 'There is no revision ID oldid' ), - array( 'code' => 'permissiondenied', 'info' => 'You don\'t have permission to view deleted revisions' ), - array( 'code' => 'missingtitle', 'info' => 'The page you specified doesn\'t exist' ), - array( 'code' => 'nosuchsection', 'info' => 'There is no section sectionnumber in page' ), - array( 'nosuchpageid' ), - array( 'invalidtitle', 'title' ), - array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ), - array( 'code' => 'notwikitext', 'info' => 'The requested operation is only supported on wikitext content.' ), - array( 'code' => 'pagecannotexist', 'info' => "Namespace doesn't allow actual pages" ), - ) ); - } - public function getExamples() { return array( 'api.php?action=parse&page=Project:Sandbox' => 'Parse a page', - 'api.php?action=parse&text={{Project:Sandbox}}' => 'Parse wikitext', - 'api.php?action=parse&text={{PAGENAME}}&title=Test' => 'Parse wikitext, specifying the page title', + 'api.php?action=parse&text={{Project:Sandbox}}&contentmodel=wikitext' => 'Parse wikitext', + 'api.php?action=parse&text={{PAGENAME}}&title=Test' + => 'Parse wikitext, specifying the page title', 'api.php?action=parse&summary=Some+[[link]]&prop=' => 'Parse a summary', ); } diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php index bd2fde2b..8b66781a 100644 --- a/includes/api/ApiPatrol.php +++ b/includes/api/ApiPatrol.php @@ -77,10 +77,6 @@ class ApiPatrol extends ApiBase { public function getAllowedParams() { return array( - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'rcid' => array( ApiBase::PARAM_TYPE => 'integer' ), @@ -92,52 +88,23 @@ class ApiPatrol extends ApiBase { public function getParamDescription() { return array( - 'token' => 'Patrol token obtained from list=recentchanges', 'rcid' => 'Recentchanges ID to patrol', 'revid' => 'Revision ID to patrol', ); } - public function getResultProperties() { - return array( - '' => array( - 'rcid' => 'integer', - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { - return 'Patrol a page or revision'; - } - - public function getPossibleErrors() { - return array_merge( - parent::getPossibleErrors(), - parent::getRequireOnlyOneParameterErrorMessages( array( 'rcid', 'revid' ) ), - array( - array( 'nosuchrcid', 'rcid' ), - array( 'nosuchrevid', 'revid' ), - array( - 'code' => 'notpatrollable', - 'info' => "The revision can't be patrolled as it's too old" - ) - ) ); + return 'Patrol a page or revision.'; } public function needsToken() { - return true; - } - - public function getTokenSalt() { return 'patrol'; } public function getExamples() { return array( - 'api.php?action=patrol&token=123abc&rcid=230672766', - 'api.php?action=patrol&token=123abc&revid=230672766' + 'api.php?action=patrol&token=123ABC&rcid=230672766', + 'api.php?action=patrol&token=123ABC&revid=230672766' ); } diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php index 7830c8b4..a3d12b7f 100644 --- a/includes/api/ApiProtect.php +++ b/includes/api/ApiProtect.php @@ -28,9 +28,7 @@ * @ingroup API */ class ApiProtect extends ApiBase { - public function execute() { - global $wgRestrictionLevels; $params = $this->extractRequestParams(); $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' ); @@ -47,7 +45,11 @@ class ApiProtect extends ApiBase { if ( count( $expiry ) == 1 ) { $expiry = array_fill( 0, count( $params['protections'] ), $expiry[0] ); } else { - $this->dieUsageMsg( array( 'toofewexpiries', count( $expiry ), count( $params['protections'] ) ) ); + $this->dieUsageMsg( array( + 'toofewexpiries', + count( $expiry ), + count( $params['protections'] ) + ) ); } } @@ -71,11 +73,11 @@ class ApiProtect extends ApiBase { if ( !in_array( $p[0], $restrictionTypes ) && $p[0] != 'create' ) { $this->dieUsageMsg( array( 'protect-invalidaction', $p[0] ) ); } - if ( !in_array( $p[1], $wgRestrictionLevels ) && $p[1] != 'all' ) { + if ( !in_array( $p[1], $this->getConfig()->get( 'RestrictionLevels' ) ) && $p[1] != 'all' ) { $this->dieUsageMsg( array( 'protect-invalidlevel', $p[1] ) ); } - if ( in_array( $expiry[$i], array( 'infinite', 'indefinite', 'never' ) ) ) { + if ( in_array( $expiry[$i], array( 'infinite', 'indefinite', 'infinity', 'never' ) ) ) { $expiryarray[$p[0]] = $db->getInfinity(); } else { $exp = strtotime( $expiry[$i] ); @@ -89,18 +91,30 @@ class ApiProtect extends ApiBase { } $expiryarray[$p[0]] = $exp; } - $resultProtections[] = array( $p[0] => $protections[$p[0]], - 'expiry' => ( $expiryarray[$p[0]] == $db->getInfinity() ? - 'infinite' : - wfTimestamp( TS_ISO_8601, $expiryarray[$p[0]] ) ) ); + $resultProtections[] = array( + $p[0] => $protections[$p[0]], + 'expiry' => ( $expiryarray[$p[0]] == $db->getInfinity() + ? 'infinite' + : wfTimestamp( TS_ISO_8601, $expiryarray[$p[0]] ) + ) + ); } $cascade = $params['cascade']; + if ( $params['watch'] ) { + $this->logFeatureUsage( 'action=protect&watch' ); + } $watch = $params['watch'] ? 'watch' : $params['watchlist']; - $this->setWatch( $watch, $titleObj ); - - $status = $pageObj->doUpdateRestrictions( $protections, $expiryarray, $cascade, $params['reason'], $this->getUser() ); + $this->setWatch( $watch, $titleObj, 'watchdefault' ); + + $status = $pageObj->doUpdateRestrictions( + $protections, + $expiryarray, + $cascade, + $params['reason'], + $this->getUser() + ); if ( !$status->isOK() ) { $this->dieStatus( $status ); @@ -134,10 +148,6 @@ class ApiProtect extends ApiBase { 'pageid' => array( ApiBase::PARAM_TYPE => 'integer', ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'protections' => array( ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_REQUIRED => true, @@ -167,62 +177,41 @@ class ApiProtect extends ApiBase { public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'title' => "Title of the page you want to (un)protect. Cannot be used together with {$p}pageid", 'pageid' => "ID of the page you want to (un)protect. Cannot be used together with {$p}title", - 'token' => 'A protect token previously retrieved through prop=info', 'protections' => 'List of protection levels, formatted action=group (e.g. edit=sysop)', - 'expiry' => array( 'Expiry timestamps. If only one timestamp is set, it\'ll be used for all protections.', - 'Use \'infinite\', \'indefinite\' or \'never\', for a never-expiring protection.' ), + 'expiry' => array( + 'Expiry timestamps. If only one timestamp is ' . + 'set, it\'ll be used for all protections.', + 'Use \'infinite\', \'indefinite\', \'infinity\' or \'never\', for a never-expiring protection.' + ), 'reason' => 'Reason for (un)protecting', - 'cascade' => array( 'Enable cascading protection (i.e. protect pages included in this page)', - 'Ignored if not all protection levels are \'sysop\' or \'protect\'' ), + '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', - 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', - ); - } - - public function getResultProperties() { - return array( - '' => array( - 'title' => 'string', - 'reason' => 'string', - 'cascade' => 'boolean' - ) + 'watchlist' => 'Unconditionally add or remove the page from your ' . + 'watchlist, use preferences or do not change watch', ); } public function getDescription() { - return 'Change the protection level of a page'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getTitleOrPageIdErrorMessage(), - array( - array( 'toofewexpiries', 'noofexpiries', 'noofprotections' ), - array( 'create-titleexists' ), - array( 'missingtitle-createonly' ), - array( 'protect-invalidaction', 'action' ), - array( 'protect-invalidlevel', 'level' ), - array( 'invalidexpiry', 'expiry' ), - array( 'pastexpiry', 'expiry' ), - ) - ); + return 'Change the protection level of a page.'; } public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { return array( - 'api.php?action=protect&title=Main%20Page&token=123ABC&protections=edit=sysop|move=sysop&cascade=&expiry=20070901163000|never', - 'api.php?action=protect&title=Main%20Page&token=123ABC&protections=edit=all|move=all&reason=Lifting%20restrictions' + 'api.php?action=protect&title=Main%20Page&token=123ABC&' . + 'protections=edit=sysop|move=sysop&cascade=&expiry=20070901163000|never', + 'api.php?action=protect&title=Main%20Page&token=123ABC&' . + 'protections=edit=all|move=all&reason=Lifting%20restrictions' ); } diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php index 0812ba51..7667b235 100644 --- a/includes/api/ApiPurge.php +++ b/includes/api/ApiPurge.php @@ -30,51 +30,22 @@ * @ingroup API */ class ApiPurge extends ApiBase { - private $mPageSet; /** - * Add all items from $values into the result - * @param array $result output - * @param array $values values to add - * @param string $flag the name of the boolean flag to mark this element - * @param string $name if given, name of the value - */ - private static function addValues( array &$result, $values, $flag = null, $name = null ) { - foreach ( $values as $val ) { - if ( $val instanceof Title ) { - $v = array(); - ApiQueryBase::addTitleInfo( $v, $val ); - } elseif ( $name !== null ) { - $v = array( $name => $val ); - } else { - $v = $val; - } - if ( $flag !== null ) { - $v[$flag] = ''; - } - $result[] = $v; - } - } - - /** * Purges the cache of a page */ public function execute() { $params = $this->extractRequestParams(); + $this->getResult()->beginContinuation( $params['continue'], array(), array() ); + $forceLinkUpdate = $params['forcelinkupdate']; $forceRecursiveLinkUpdate = $params['forcerecursivelinkupdate']; $pageSet = $this->getPageSet(); $pageSet->execute(); - $result = array(); - self::addValues( $result, $pageSet->getInvalidTitles(), 'invalid', 'title' ); - self::addValues( $result, $pageSet->getSpecialTitles(), 'special', 'title' ); - self::addValues( $result, $pageSet->getMissingPageIDs(), 'missing', 'pageid' ); - self::addValues( $result, $pageSet->getMissingRevisionIDs(), 'missing', 'revid' ); - self::addValues( $result, $pageSet->getMissingTitles(), 'missing' ); - self::addValues( $result, $pageSet->getInterwikiTitlesAsResult() ); + $result = $pageSet->getInvalidTitlesAndRevisions(); foreach ( $pageSet->getGoodTitles() as $title ) { $r = array(); @@ -85,13 +56,17 @@ class ApiPurge extends ApiBase { if ( $forceLinkUpdate || $forceRecursiveLinkUpdate ) { if ( !$this->getUser()->pingLimiter( 'linkpurge' ) ) { - global $wgEnableParserCache; - $popts = $page->makeParserOptions( 'canonical' ); # Parse content; note that HTML generation is only needed if we want to cache the result. $content = $page->getContent( Revision::RAW ); - $p_result = $content->getParserOutput( $title, $page->getLatest(), $popts, $wgEnableParserCache ); + $enableParserCache = $this->getConfig()->get( 'EnableParserCache' ); + $p_result = $content->getParserOutput( + $title, + $page->getLatest(), + $popts, + $enableParserCache + ); # Update the links tables $updates = $content->getSecondaryDataUpdates( @@ -100,7 +75,7 @@ class ApiPurge extends ApiBase { $r['linkupdate'] = ''; - if ( $wgEnableParserCache ) { + if ( $enableParserCache ) { $pcache = ParserCache::singleton(); $pcache->save( $p_result, $page, $popts ); } @@ -129,6 +104,8 @@ class ApiPurge extends ApiBase { if ( $values ) { $apiResult->addValue( null, 'redirects', $values ); } + + $apiResult->endContinuation(); } /** @@ -139,6 +116,7 @@ class ApiPurge extends ApiBase { if ( !isset( $this->mPageSet ) ) { $this->mPageSet = new ApiPageSet( $this ); } + return $this->mPageSet; } @@ -154,11 +132,13 @@ class ApiPurge extends ApiBase { public function getAllowedParams( $flags = 0 ) { $result = array( 'forcelinkupdate' => false, - 'forcerecursivelinkupdate' => false + 'forcerecursivelinkupdate' => false, + 'continue' => '', ); if ( $flags ) { $result += $this->getPageSet()->getFinalParams( $flags ); } + return $result; } @@ -168,55 +148,16 @@ class ApiPurge extends ApiBase { 'forcelinkupdate' => 'Update the links tables', 'forcerecursivelinkupdate' => 'Update the links table, and update ' . 'the links tables for any page that uses this page as a template', + 'continue' => 'When more results are available, use this to continue', ); } - public function getResultProperties() { - return array( - ApiBase::PROP_LIST => true, - '' => array( - 'ns' => array( - ApiBase::PROP_TYPE => 'namespace', - ApiBase::PROP_NULLABLE => true - ), - 'title' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'pageid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'revid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'invalid' => 'boolean', - 'special' => 'boolean', - 'missing' => 'boolean', - 'purged' => 'boolean', - 'linkupdate' => 'boolean', - 'iw' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - ) - ); - } - public function getDescription() { return array( 'Purge the cache for the given titles.', 'Requires a POST request if the user is not logged in.' ); } - public function getPossibleErrors() { - return array_merge( - parent::getPossibleErrors(), - $this->getPageSet()->getFinalPossibleErrors() - ); - } - public function getExamples() { return array( 'api.php?action=purge&titles=Main_Page|API' => 'Purge the "Main Page" and the "API" page', diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index e03837fc..7c750e41 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -44,18 +44,23 @@ class ApiQuery extends ApiBase { private static $QueryPropModules = array( 'categories' => 'ApiQueryCategories', 'categoryinfo' => 'ApiQueryCategoryInfo', + 'contributors' => 'ApiQueryContributors', 'duplicatefiles' => 'ApiQueryDuplicateFiles', 'extlinks' => 'ApiQueryExternalLinks', + 'fileusage' => 'ApiQueryBacklinksprop', 'images' => 'ApiQueryImages', 'imageinfo' => 'ApiQueryImageInfo', 'info' => 'ApiQueryInfo', 'links' => 'ApiQueryLinks', + 'linkshere' => 'ApiQueryBacklinksprop', 'iwlinks' => 'ApiQueryIWLinks', 'langlinks' => 'ApiQueryLangLinks', 'pageprops' => 'ApiQueryPageProps', + 'redirects' => 'ApiQueryBacklinksprop', 'revisions' => 'ApiQueryRevisions', 'stashimageinfo' => 'ApiQueryStashImageInfo', 'templates' => 'ApiQueryLinks', + 'transcludedin' => 'ApiQueryBacklinksprop', ); /** @@ -68,6 +73,7 @@ class ApiQuery extends ApiBase { 'allimages' => 'ApiQueryAllImages', 'alllinks' => 'ApiQueryAllLinks', 'allpages' => 'ApiQueryAllPages', + 'allredirects' => 'ApiQueryAllLinks', 'alltransclusions' => 'ApiQueryAllLinks', 'allusers' => 'ApiQueryAllUsers', 'backlinks' => 'ApiQueryBacklinks', @@ -83,6 +89,7 @@ class ApiQuery extends ApiBase { 'logevents' => 'ApiQueryLogEvents', 'pageswithprop' => 'ApiQueryPagesWithProp', 'pagepropnames' => 'ApiQueryPagePropNames', + 'prefixsearch' => 'ApiQueryPrefixSearch', 'protectedtitles' => 'ApiQueryProtectedTitles', 'querypage' => 'ApiQueryQueryPage', 'random' => 'ApiQueryRandom', @@ -104,6 +111,7 @@ class ApiQuery extends ApiBase { 'siteinfo' => 'ApiQuerySiteinfo', 'userinfo' => 'ApiQueryUserInfo', 'filerepoinfo' => 'ApiQueryFileRepoInfo', + 'tokens' => 'ApiQueryTokens', ); /** @@ -114,26 +122,24 @@ class ApiQuery extends ApiBase { private $mParams; private $mNamedDB = array(); private $mModuleMgr; - private $mGeneratorContinue; - private $mUseLegacyContinue; /** - * @param $main ApiMain - * @param $action string + * @param ApiMain $main + * @param string $action */ - public function __construct( $main, $action ) { + public function __construct( ApiMain $main, $action ) { parent::__construct( $main, $action ); $this->mModuleMgr = new ApiModuleManager( $this ); // Allow custom modules to be added in LocalSettings.php - global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules; + $config = $this->getConfig(); $this->mModuleMgr->addModules( self::$QueryPropModules, 'prop' ); - $this->mModuleMgr->addModules( $wgAPIPropModules, 'prop' ); + $this->mModuleMgr->addModules( $config->get( 'APIPropModules' ), 'prop' ); $this->mModuleMgr->addModules( self::$QueryListModules, 'list' ); - $this->mModuleMgr->addModules( $wgAPIListModules, 'list' ); + $this->mModuleMgr->addModules( $config->get( 'APIListModules' ), 'list' ); $this->mModuleMgr->addModules( self::$QueryMetaModules, 'meta' ); - $this->mModuleMgr->addModules( $wgAPIMetaModules, 'meta' ); + $this->mModuleMgr->addModules( $config->get( 'APIMetaModules' ), 'meta' ); // Create PageSet that will process titles/pageids/revids/generator $this->mPageSet = new ApiPageSet( $this ); @@ -163,6 +169,7 @@ class ApiQuery extends ApiBase { $this->mNamedDB[$name] = wfGetDB( $db, $groups ); $this->profileDBOut(); } + return $this->mNamedDB[$name]; } @@ -177,17 +184,18 @@ class ApiQuery extends ApiBase { /** * Get the array mapping module names to class names * @deprecated since 1.21, use getModuleManager()'s methods instead - * @return array array(modulename => classname) + * @return array Array(modulename => classname) */ public function getModules() { wfDeprecated( __METHOD__, '1.21' ); + return $this->getModuleManager()->getNamesWithClasses(); } /** * Get the generators array mapping module names to class names * @deprecated since 1.21, list of generators is maintained by ApiPageSet - * @return array array(modulename => classname) + * @return array Array(modulename => classname) */ public function getGenerators() { wfDeprecated( __METHOD__, '1.21' ); @@ -197,6 +205,7 @@ class ApiQuery extends ApiBase { $gens[$name] = $class; } } + return $gens; } @@ -204,7 +213,7 @@ class ApiQuery extends ApiBase { * Get whether the specified module is a prop, list or a meta query module * @deprecated since 1.21, use getModuleManager()->getModuleGroup() * @param string $moduleName Name of the module to find type for - * @return mixed string or null + * @return string|null */ function getModuleType( $moduleName ) { return $this->getModuleManager()->getModuleGroup( $moduleName ); @@ -216,8 +225,8 @@ class ApiQuery extends ApiBase { public function getCustomPrinter() { // If &exportnowrap is set, use the raw formatter if ( $this->getParameter( 'export' ) && - $this->getParameter( 'exportnowrap' ) ) - { + $this->getParameter( 'exportnowrap' ) + ) { return new ApiFormatRaw( $this->getMain(), $this->getMain()->createPrinterByName( 'xml' ) ); } else { @@ -238,23 +247,24 @@ class ApiQuery extends ApiBase { public function execute() { $this->mParams = $this->extractRequestParams(); - // $pagesetParams is a array of parameter names used by the pageset generator - // or null if pageset has already finished and is no longer needed - // $completeModules is a set of complete modules with the name as key - $this->initContinue( $pagesetParams, $completeModules ); - // Instantiate requested modules $allModules = array(); $this->instantiateModules( $allModules, 'prop' ); - $propModules = $allModules; // Keep a copy + $propModules = array_keys( $allModules ); $this->instantiateModules( $allModules, 'list' ); $this->instantiateModules( $allModules, 'meta' ); // Filter modules based on continue parameter - $modules = $this->initModules( $allModules, $completeModules, $pagesetParams !== null ); + list( $generatorDone, $modules ) = $this->getResult()->beginContinuation( + $this->mParams['continue'], $allModules, $propModules + ); - // Execute pageset if in legacy mode or if pageset is not done - if ( $completeModules === null || $pagesetParams !== null ) { + if ( !$generatorDone ) { + // Query modules may optimize data requests through the $this->getPageSet() + // object by adding extra fields from the page table. + foreach ( $modules as $module ) { + $module->requestExtraData( $this->mPageSet ); + } // Populate page/revision information $this->mPageSet->execute(); // Record page information (title, namespace, if exists, etc) @@ -280,134 +290,10 @@ class ApiQuery extends ApiBase { // Set the cache mode $this->getMain()->setCacheMode( $cacheMode ); - if ( $completeModules === null ) { - return; // Legacy continue, we are done - } - - // Reformat query-continue result section - $result = $this->getResult(); - $qc = $result->getData(); - if ( isset( $qc['query-continue'] ) ) { - $qc = $qc['query-continue']; - $result->unsetValue( null, 'query-continue' ); - } elseif ( $this->mGeneratorContinue !== null ) { - $qc = array(); - } else { - // no more "continue"s, we are done! - return; - } - - // we are done with all the modules that do not have result in query-continue - $completeModules = array_merge( $completeModules, array_diff_key( $modules, $qc ) ); - if ( $pagesetParams !== null ) { - // The pageset is still in use, check if all props have finished - $incompleteProps = array_intersect_key( $propModules, $qc ); - if ( count( $incompleteProps ) > 0 ) { - // Properties are not done, continue with the same pageset state - copy current parameters - $main = $this->getMain(); - $contValues = array(); - foreach ( $pagesetParams as $param ) { - // The param name is already prefix-encoded - $contValues[$param] = $main->getVal( $param ); - } - } elseif ( $this->mGeneratorContinue !== null ) { - // Move to the next set of pages produced by pageset, properties need to be restarted - $contValues = $this->mGeneratorContinue; - $pagesetParams = array_keys( $contValues ); - $completeModules = array_diff_key( $completeModules, $propModules ); - } else { - // Done with the pageset, finish up with the the lists and meta modules - $pagesetParams = null; - } - } - - $continue = '||' . implode( '|', array_keys( $completeModules ) ); - if ( $pagesetParams !== null ) { - // list of all pageset parameters to use in the next request - $continue = implode( '|', $pagesetParams ) . $continue; - } else { - // we are done with the pageset - $contValues = array(); - $continue = '-' . $continue; - } - $contValues['continue'] = $continue; - foreach ( $qc as $qcModule ) { - foreach ( $qcModule as $qcKey => $qcValue ) { - $contValues[$qcKey] = $qcValue; - } - } - $this->getResult()->addValue( null, 'continue', $contValues ); - } - - /** - * Parse 'continue' parameter into the list of complete modules and a list of generator parameters - * @param array|null $pagesetParams returns list of generator params or null if pageset is done - * @param array|null $completeModules returns list of finished modules (as keys), or null if legacy - */ - private function initContinue( &$pagesetParams, &$completeModules ) { - $pagesetParams = array(); - $continue = $this->mParams['continue']; - if ( $continue !== null ) { - $this->mUseLegacyContinue = false; - if ( $continue !== '' ) { - // Format: ' pagesetParam1 | pagesetParam2 || module1 | module2 | module3 | ... - // If pageset is done, use '-' - $continue = explode( '||', $continue ); - $this->dieContinueUsageIf( count( $continue ) !== 2 ); - if ( $continue[0] === '-' ) { - $pagesetParams = null; // No need to execute pageset - } elseif ( $continue[0] !== '' ) { - // list of pageset params that might need to be repeated - $pagesetParams = explode( '|', $continue[0] ); - } - $continue = $continue[1]; - } - if ( $continue !== '' ) { - $completeModules = array_flip( explode( '|', $continue ) ); - } else { - $completeModules = array(); - } - } else { - $this->mUseLegacyContinue = true; - $completeModules = null; - } - } - - /** - * Validate sub-modules, filter out completed ones, and do requestExtraData() - * @param array $allModules An dict of name=>instance of all modules requested by the client - * @param array|null $completeModules list of finished modules, or null if legacy continue - * @param bool $usePageset True if pageset will be executed - * @return array of modules to be processed during this execution - */ - private function initModules( $allModules, $completeModules, $usePageset ) { - $modules = $allModules; - $tmp = $completeModules; - $wasPosted = $this->getRequest()->wasPosted(); - - /** @var $module ApiQueryBase */ - foreach ( $allModules as $moduleName => $module ) { - if ( !$wasPosted && $module->mustBePosted() ) { - $this->dieUsageMsgOrDebug( array( 'mustbeposted', $moduleName ) ); - } - if ( $completeModules !== null && array_key_exists( $moduleName, $completeModules ) ) { - // If this module is done, mark all its params as used - $module->extractRequestParams(); - // Make sure this module is not used during execution - unset( $modules[$moduleName] ); - unset( $tmp[$moduleName] ); - } elseif ( $completeModules === null || $usePageset ) { - // 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. - $module->requestExtraData( $this->mPageSet ); - } else { - // Error - this prop module must have finished before generator is done - $this->dieContinueUsageIf( $this->mModuleMgr->getModuleGroup( $moduleName ) === 'prop' ); - } - } - $this->dieContinueUsageIf( $completeModules !== null && count( $tmp ) !== 0 ); - return $modules; + // Write the continuation data into the result + $this->getResult()->endContinuation( + $this->mParams['continue'] === null ? 'raw' : 'standard' + ); } /** @@ -415,8 +301,8 @@ class ApiQuery extends ApiBase { * The cache mode may increase in the level of privacy, but public modules * added to private data do not decrease the level of privacy. * - * @param $cacheMode string - * @param $modCacheMode string + * @param string $cacheMode + * @param string $modCacheMode * @return string */ protected function mergeCacheMode( $cacheMode, $modCacheMode ) { @@ -429,21 +315,26 @@ class ApiQuery extends ApiBase { } else { // private $cacheMode = 'private'; } + return $cacheMode; } /** * Create instances of all modules requested by the client - * @param array $modules to append instantiated modules to + * @param array $modules To append instantiated modules to * @param string $param Parameter name to read modules from */ private function instantiateModules( &$modules, $param ) { + $wasPosted = $this->getRequest()->wasPosted(); if ( isset( $this->mParams[$param] ) ) { foreach ( $this->mParams[$param] as $moduleName ) { $instance = $this->mModuleMgr->getModule( $moduleName, $param ); if ( $instance === null ) { ApiBase::dieDebug( __METHOD__, 'Error instantiating module' ); } + if ( !$wasPosted && $instance->mustBePosted() ) { + $this->dieUsageMsgOrDebug( array( 'mustbeposted', $moduleName ) ); + } // Ignore duplicates. TODO 2.0: die()? if ( !array_key_exists( $moduleName, $modules ) ) { $modules[$moduleName] = $instance; @@ -461,29 +352,29 @@ class ApiQuery extends ApiBase { $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. + // We can't really handle max-result-size failure here, but we need to + // check anyway in case someone set the limit stupidly low. + $fit = true; $values = $pageSet->getNormalizedTitlesAsResult( $result ); if ( $values ) { - $result->addValue( 'query', 'normalized', $values ); + $fit = $fit && $result->addValue( 'query', 'normalized', $values ); } $values = $pageSet->getConvertedTitlesAsResult( $result ); if ( $values ) { - $result->addValue( 'query', 'converted', $values ); + $fit = $fit && $result->addValue( 'query', 'converted', $values ); } $values = $pageSet->getInterwikiTitlesAsResult( $result, $this->mParams['iwurl'] ); if ( $values ) { - $result->addValue( 'query', 'interwiki', $values ); + $fit = $fit && $result->addValue( 'query', 'interwiki', $values ); } $values = $pageSet->getRedirectTitlesAsResult( $result ); if ( $values ) { - $result->addValue( 'query', 'redirects', $values ); + $fit = $fit && $result->addValue( 'query', 'redirects', $values ); } $values = $pageSet->getMissingRevisionIDsAsResult( $result ); if ( $values ) { - $result->addValue( 'query', 'badrevids', $values ); + $fit = $fit && $result->addValue( 'query', 'badrevids', $values ); } // Page elements @@ -514,10 +405,12 @@ class ApiQuery extends ApiBase { ApiQueryBase::addTitleInfo( $vals, $title ); $vals['special'] = ''; if ( $title->isSpecialPage() && - !SpecialPageFactory::exists( $title->getDBkey() ) ) { + !SpecialPageFactory::exists( $title->getDBkey() ) + ) { $vals['missing'] = ''; } elseif ( $title->getNamespace() == NS_MEDIA && - !wfFindFile( $title ) ) { + !wfFindFile( $title ) + ) { $vals['missing'] = ''; } $pages[$fakeId] = $vals; @@ -537,12 +430,21 @@ class ApiQuery extends ApiBase { // json treats all map keys as strings - converting to match $pageIDs = array_map( 'strval', $pageIDs ); $result->setIndexedTagName( $pageIDs, 'id' ); - $result->addValue( 'query', 'pageids', $pageIDs ); + $fit = $fit && $result->addValue( 'query', 'pageids', $pageIDs ); } $result->setIndexedTagName( $pages, 'page' ); - $result->addValue( 'query', 'pages', $pages ); + $fit = $fit && $result->addValue( 'query', 'pages', $pages ); } + + if ( !$fit ) { + $this->dieUsage( + 'The value of $wgAPIMaxResultSize on this wiki is ' . + 'too small to hold basic result information', + 'badconfig' + ); + } + if ( $this->mParams['export'] ) { $this->doExport( $pageSet, $result ); } @@ -552,26 +454,21 @@ class ApiQuery extends ApiBase { * This method is called by the generator base when generator in the smart-continue * mode tries to set 'query-continue' value. ApiQuery stores those values separately * until the post-processing when it is known if the generation should continue or repeat. - * @param ApiQueryGeneratorBase $module generator module + * @deprecated since 1.24 + * @param ApiQueryGeneratorBase $module Generator module * @param string $paramName * @param mixed $paramValue - * @return bool true if processed, false if this is a legacy continue + * @return bool True if processed, false if this is a legacy continue */ public function setGeneratorContinue( $module, $paramName, $paramValue ) { - if ( $this->mUseLegacyContinue ) { - return false; - } - $paramName = $module->encodeParamName( $paramName ); - if ( $this->mGeneratorContinue === null ) { - $this->mGeneratorContinue = array(); - } - $this->mGeneratorContinue[$paramName] = $paramValue; - return true; + wfDeprecated( __METHOD__, '1.24' ); + $this->getResult()->setGeneratorContinueParam( $module, $paramName, $paramValue ); + return $this->getParameter( 'continue' ) !== null; } /** - * @param $pageSet ApiPageSet Pages to be exported - * @param $result ApiResult Result to output to + * @param ApiPageSet $pageSet Pages to be exported + * @param ApiResult $result Result to output to */ private function doExport( $pageSet, $result ) { $exportTitles = array(); @@ -601,43 +498,43 @@ class ApiQuery extends ApiBase { // 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->mParams['exportnowrap'] ) { $result->reset(); // Raw formatter will handle this - $result->addValue( null, 'text', $exportxml ); - $result->addValue( null, 'mime', 'text/xml' ); + $result->addValue( null, 'text', $exportxml, ApiResult::NO_SIZE_CHECK ); + $result->addValue( null, 'mime', 'text/xml', ApiResult::NO_SIZE_CHECK ); } else { $r = array(); ApiResult::setContent( $r, $exportxml ); - $result->addValue( 'query', 'export', $r ); + $result->addValue( 'query', 'export', $r, ApiResult::NO_SIZE_CHECK ); } - $result->enableSizeCheck(); } public function getAllowedParams( $flags = 0 ) { $result = array( 'prop' => array( ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'prop' ) + ApiBase::PARAM_TYPE => 'submodule', ), 'list' => array( ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'list' ) + ApiBase::PARAM_TYPE => 'submodule', ), 'meta' => array( ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'meta' ) + ApiBase::PARAM_TYPE => 'submodule', ), 'indexpageids' => false, 'export' => false, 'exportnowrap' => false, 'iwurl' => false, 'continue' => null, + 'rawcontinue' => false, ); if ( $flags ) { $result += $this->getPageSet()->getFinalParams( $flags ); } + return $result; } @@ -699,38 +596,41 @@ class ApiQuery extends ApiBase { public function getParamDescription() { return $this->getPageSet()->getFinalParamDescription() + array( - 'prop' => 'Which properties to get for the titles/revisions/pageids. Module help is available below', + 'prop' => 'Which properties to get for the titles/revisions/pageids. ' . + 'Module help is available below', 'list' => 'Which lists to get. Module help is available below', 'meta' => 'Which metadata to get about the site. Module help is available below', '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', + 'exportnowrap' => 'Return the export XML without wrapping it in an ' . + 'XML result (same format as Special:Export). Can only be used with export', 'iwurl' => 'Whether to get the full URL if the title is an interwiki link', 'continue' => array( - 'When present, formats query-continue as key-value pairs that should simply be merged into the original request.', + 'When present, formats query-continue as key-value pairs that ' . + 'should simply be merged into the original request.', 'This parameter must be set to an empty string in the initial query.', - 'This parameter is recommended for all new development, and will be made default in the next API version.' ), + 'This parameter is recommended for all new development, and ' . + 'will be made default in the next API version.' + ), + 'rawcontinue' => 'Currently ignored. In the future, \'continue=\' will become the ' . + 'default and this will be needed to receive the raw query-continue data.', ); } public function getDescription() { return array( - 'Query API module allows applications to get needed pieces of data from the MediaWiki databases,', + 'Query API module allows applications to get needed pieces of data ' . + 'from the MediaWiki databases,', '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' - ); - } - - public function getPossibleErrors() { - return array_merge( - parent::getPossibleErrors(), - $this->getPageSet()->getFinalPossibleErrors() + 'All data modifications will first have to use query to acquire a ' . + 'token to prevent abuse from malicious sites.' ); } public function getExamples() { return array( - 'api.php?action=query&prop=revisions&meta=siteinfo&titles=Main%20Page&rvprop=user|comment&continue=', + 'api.php?action=query&prop=revisions&meta=siteinfo&' . + 'titles=Main%20Page&rvprop=user|comment&continue=', 'api.php?action=query&generator=allpages&gapprefix=API/&prop=revisions&continue=', ); } diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php index 3f5c6ee7..79fab727 100644 --- a/includes/api/ApiQueryAllCategories.php +++ b/includes/api/ApiQueryAllCategories.php @@ -32,7 +32,7 @@ */ class ApiQueryAllCategories extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'ac' ); } @@ -49,7 +49,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet */ private function run( $resultPageSet = null ) { $db = $this->getDB(); @@ -67,8 +67,12 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { } $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' ); - $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); - $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) ); + $from = ( $params['from'] === null + ? null + : $this->titlePartToKey( $params['from'], NS_CATEGORY ) ); + $to = ( $params['to'] === null + ? null + : $this->titlePartToKey( $params['to'], NS_CATEGORY ) ); $this->addWhereRange( 'cat_title', $dir, $from, $to ); $min = $params['min']; @@ -80,7 +84,9 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { } if ( isset( $params['prefix'] ) ) { - $this->addWhere( 'cat_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); + $this->addWhere( 'cat_title' . $db->buildLike( + $this->titlePartToKey( $params['prefix'], NS_CATEGORY ), + $db->anyString() ) ); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -109,8 +115,9 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { $result = $this->getResult(); $count = 0; foreach ( $res as $row ) { - if ( ++ $count > $params['limit'] ) { - // We've reached the one extra which shows that there are additional cats to be had. Stop here... + if ( ++$count > $params['limit'] ) { + // We've reached the one extra which shows that there are + // additional cats to be had. Stop here... $this->setContinueEnumParameter( 'continue', $row->cat_title ); break; } @@ -200,25 +207,8 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - '*' => 'string' - ), - 'size' => array( - 'size' => 'integer', - 'pages' => 'integer', - 'files' => 'integer', - 'subcats' => 'integer' - ), - 'hidden' => array( - 'hidden' => 'boolean' - ) - ); - } - public function getDescription() { - return 'Enumerate all categories'; + return 'Enumerate all categories.'; } public function getExamples() { diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php index ccc7a3a2..9dc5f69a 100644 --- a/includes/api/ApiQueryAllImages.php +++ b/includes/api/ApiQueryAllImages.php @@ -32,10 +32,9 @@ * @ingroup API */ class ApiQueryAllImages extends ApiQueryGeneratorBase { - protected $mRepo; - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'ai' ); $this->mRepo = RepoGroup::singleton()->getLocalRepo(); } @@ -60,25 +59,32 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ public function executeGenerator( $resultPageSet ) { if ( $resultPageSet->isResolvingRedirects() ) { - $this->dieUsage( 'Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator', 'params' ); + $this->dieUsage( + 'Use "gaifilterredir=nonredirects" option instead of "redirects" ' . + 'when using allimages as a generator', + 'params' + ); } $this->run( $resultPageSet ); } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function run( $resultPageSet = null ) { $repo = $this->mRepo; if ( !$repo instanceof LocalRepo ) { - $this->dieUsage( 'Local file repository does not support querying all images', 'unsupportedrepo' ); + $this->dieUsage( + 'Local file repository does not support querying all images', + 'unsupportedrepo' + ); } $prefix = $this->getModulePrefix(); @@ -103,11 +109,17 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { $disallowed = array( 'start', 'end', 'user' ); foreach ( $disallowed as $pname ) { if ( isset( $params[$pname] ) ) { - $this->dieUsage( "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=timestamp", 'badparams' ); + $this->dieUsage( + "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=timestamp", + 'badparams' + ); } } if ( $params['filterbots'] != 'all' ) { - $this->dieUsage( "Parameter '{$prefix}filterbots' can only be used with {$prefix}sort=timestamp", 'badparams' ); + $this->dieUsage( + "Parameter '{$prefix}filterbots' can only be used with {$prefix}sort=timestamp", + 'badparams' + ); } // Pagination @@ -120,28 +132,56 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { } // Image filters - $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); - $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) ); + $from = ( $params['from'] === null ? null : $this->titlePartToKey( $params['from'], NS_FILE ) ); + $to = ( $params['to'] === null ? null : $this->titlePartToKey( $params['to'], NS_FILE ) ); $this->addWhereRange( 'img_name', ( $ascendingOrder ? 'newer' : 'older' ), $from, $to ); if ( isset( $params['prefix'] ) ) { - $this->addWhere( 'img_name' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); + $this->addWhere( 'img_name' . $db->buildLike( + $this->titlePartToKey( $params['prefix'], NS_FILE ), + $db->anyString() ) ); } } else { // Check mutually exclusive params $disallowed = array( 'from', 'to', 'prefix' ); foreach ( $disallowed as $pname ) { if ( isset( $params[$pname] ) ) { - $this->dieUsage( "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=name", 'badparams' ); + $this->dieUsage( + "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=name", + 'badparams' + ); } } if ( !is_null( $params['user'] ) && $params['filterbots'] != 'all' ) { - // Since filterbots checks if each user has the bot right, it doesn't make sense to use it with user - $this->dieUsage( "Parameters '{$prefix}user' and '{$prefix}filterbots' cannot be used together", 'badparams' ); + // Since filterbots checks if each user has the bot right, it + // doesn't make sense to use it with user + $this->dieUsage( + "Parameters '{$prefix}user' and '{$prefix}filterbots' cannot be used together", + 'badparams' + ); } // Pagination - $this->addTimestampWhereRange( 'img_timestamp', ( $ascendingOrder ? 'newer' : 'older' ), $params['start'], $params['end'] ); + $this->addTimestampWhereRange( + 'img_timestamp', + $ascendingOrder ? 'newer' : 'older', + $params['start'], + $params['end'] + ); + // Include in ORDER BY for uniqueness + $this->addWhereRange( 'img_name', $ascendingOrder ? 'newer' : 'older', null, null ); + + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 2 ); + $op = ( $ascendingOrder ? '>' : '<' ); + $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) ); + $continueName = $db->addQuotes( $cont[1] ); + $this->addWhere( "img_timestamp $op $continueTimestamp OR " . + "(img_timestamp = $continueTimestamp AND " . + "img_name $op= $continueName)" + ); + } // Image filters if ( !is_null( $params['user'] ) ) { @@ -156,7 +196,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { 'ug_user = img_user' ) ) ) ); - $groupCond = ( $params['filterbots'] == 'nobots' ? 'NULL': 'NOT NULL' ); + $groupCond = ( $params['filterbots'] == 'nobots' ? 'NULL' : 'NOT NULL' ); $this->addWhere( "ug_group IS $groupCond" ); } } @@ -188,8 +228,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { } if ( !is_null( $params['mime'] ) ) { - global $wgMiserMode; - if ( $wgMiserMode ) { + if ( $this->getConfig()->get( 'MiserMode' ) ) { $this->dieUsage( 'MIME search disabled in Miser Mode', 'mimesearchdisabled' ); } @@ -222,12 +261,13 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { $count = 0; $result = $this->getResult(); foreach ( $res as $row ) { - if ( ++ $count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... if ( $params['sort'] == 'name' ) { $this->setContinueEnumParameter( 'continue', $row->img_name ); } else { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->img_timestamp ) ); + $this->setContinueEnumParameter( 'continue', "$row->img_timestamp|$row->img_name" ); } break; } @@ -243,7 +283,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { if ( $params['sort'] == 'name' ) { $this->setContinueEnumParameter( 'continue', $row->img_name ); } else { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->img_timestamp ) ); + $this->setContinueEnumParameter( 'continue', "$row->img_timestamp|$row->img_name" ); } break; } @@ -326,6 +366,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'sort' => 'Property to sort by', 'dir' => 'The direction in which to list', @@ -335,54 +376,25 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { 'start' => "The timestamp to start enumerating from. Can only be used with {$p}sort=timestamp", 'end' => "The timestamp to end enumerating. Can only be used with {$p}sort=timestamp", 'prop' => ApiQueryImageInfo::getPropertyDescriptions( $this->propertyFilter ), - 'prefix' => "Search for all image titles that begin with this value. Can only be used with {$p}sort=name", + 'prefix' => "Search for all image titles that begin with this " . + "value. Can only be used with {$p}sort=name", 'minsize' => 'Limit to images with at least this many bytes', 'maxsize' => 'Limit to images with at most this many bytes', 'sha1' => "SHA1 hash of image. Overrides {$p}sha1base36", 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki)', - 'user' => "Only return files uploaded by this user. Can only be used with {$p}sort=timestamp. Cannot be used together with {$p}filterbots", - 'filterbots' => "How to filter files uploaded by bots. Can only be used with {$p}sort=timestamp. Cannot be used together with {$p}user", + 'user' => "Only return files uploaded by this user. Can only be used " . + "with {$p}sort=timestamp. Cannot be used together with {$p}filterbots", + 'filterbots' => "How to filter files uploaded by bots. Can only be " . + "used with {$p}sort=timestamp. Cannot be used together with {$p}user", 'mime' => 'What MIME type to search for. e.g. image/jpeg. Disabled in Miser Mode', 'limit' => 'How many images in total to return', ); } - private $propertyFilter = array( 'archivename', 'thumbmime' ); - - public function getResultProperties() { - return array_merge( - array( - '' => array( - 'name' => 'string', - 'ns' => 'namespace', - 'title' => 'string' - ) - ), - ApiQueryImageInfo::getResultPropertiesFiltered( $this->propertyFilter ) - ); - } + private $propertyFilter = array( 'archivename', 'thumbmime', 'uploadwarning' ); public function getDescription() { - return 'Enumerate all images sequentially'; - } - - public function getPossibleErrors() { - $p = $this->getModulePrefix(); - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'params', 'info' => 'Use "gaifilterredir=nonredirects" option instead of "redirects" when using allimages as a generator' ), - array( 'code' => 'badparams', 'info' => "Parameter'{$p}start' can only be used with {$p}sort=timestamp" ), - array( 'code' => 'badparams', 'info' => "Parameter'{$p}end' can only be used with {$p}sort=timestamp" ), - array( 'code' => 'badparams', 'info' => "Parameter'{$p}user' can only be used with {$p}sort=timestamp" ), - array( 'code' => 'badparams', 'info' => "Parameter'{$p}filterbots' can only be used with {$p}sort=timestamp" ), - array( 'code' => 'badparams', 'info' => "Parameter'{$p}from' can only be used with {$p}sort=name" ), - array( 'code' => 'badparams', 'info' => "Parameter'{$p}to' can only be used with {$p}sort=name" ), - array( 'code' => 'badparams', 'info' => "Parameter'{$p}prefix' can only be used with {$p}sort=name" ), - array( 'code' => 'badparams', 'info' => "Parameters '{$p}user' and '{$p}filterbots' cannot be used together" ), - array( 'code' => 'unsupportedrepo', 'info' => 'Local file repository does not support querying all images' ), - array( 'code' => 'mimesearchdisabled', 'info' => 'MIME search disabled in Miser Mode' ), - array( 'code' => 'invalidsha1hash', 'info' => 'The SHA1 hash provided is not valid' ), - array( 'code' => 'invalidsha1base36hash', 'info' => 'The SHA1Base36 hash provided is not valid' ), - ) ); + return 'Enumerate all images sequentially.'; } public function getExamples() { @@ -391,11 +403,13 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { 'Simple Use', 'Show a list of files starting at the letter "B"', ), - 'api.php?action=query&list=allimages&aiprop=user|timestamp|url&aisort=timestamp&aidir=older' => array( + 'api.php?action=query&list=allimages&aiprop=user|timestamp|url&' . + 'aisort=timestamp&aidir=older' => array( 'Simple Use', 'Show a list of recently uploaded files similar to Special:NewFiles', ), - 'api.php?action=query&generator=allimages&gailimit=4&gaifrom=T&prop=imageinfo' => array( + 'api.php?action=query&generator=allimages&gailimit=4&' . + 'gaifrom=T&prop=imageinfo' => array( 'Using as Generator', 'Show info about 4 files starting at the letter "T"', ), diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index 47d1bcef..903dee42 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -31,15 +31,21 @@ */ class ApiQueryAllLinks extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + private $table, $tablePrefix, $indexTag, + $description, $descriptionWhat, $descriptionTargets, $descriptionLinking; + private $fieldTitle = 'title'; + private $dfltNamespace = NS_MAIN; + private $hasNamespace = true; + private $useIndex = null; + private $props = array(), $propHelp = array(); + + public function __construct( ApiQuery $query, $moduleName ) { switch ( $moduleName ) { case 'alllinks': $prefix = 'al'; $this->table = 'pagelinks'; $this->tablePrefix = 'pl_'; - $this->fieldTitle = 'title'; - $this->dfltNamespace = NS_MAIN; - $this->hasNamespace = true; + $this->useIndex = 'pl_namespace'; $this->indexTag = 'l'; $this->description = 'Enumerate all links that point to a given namespace'; $this->descriptionWhat = 'link'; @@ -50,11 +56,11 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $prefix = 'at'; $this->table = 'templatelinks'; $this->tablePrefix = 'tl_'; - $this->fieldTitle = 'title'; $this->dfltNamespace = NS_TEMPLATE; - $this->hasNamespace = true; + $this->useIndex = 'tl_namespace'; $this->indexTag = 't'; - $this->description = 'List all transclusions (pages embedded using {{x}}), including non-existing'; + $this->description = + 'List all transclusions (pages embedded using {{x}}), including non-existing'; $this->descriptionWhat = 'transclusion'; $this->descriptionTargets = 'transcluded titles'; $this->descriptionLinking = 'transcluding'; @@ -72,6 +78,24 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $this->descriptionTargets = 'file titles'; $this->descriptionLinking = 'using'; break; + case 'allredirects': + $prefix = 'ar'; + $this->table = 'redirect'; + $this->tablePrefix = 'rd_'; + $this->indexTag = 'r'; + $this->description = 'List all redirects to a namespace'; + $this->descriptionWhat = 'redirect'; + $this->descriptionTargets = 'target pages'; + $this->descriptionLinking = 'redirecting'; + $this->props = array( + 'fragment' => 'rd_fragment', + 'interwiki' => 'rd_interwiki', + ); + $this->propHelp = array( + ' fragment - Adds the fragment from the redirect, if any', + ' interwiki - Adds the interwiki prefix from the redirect, if any', + ); + break; default: ApiBase::dieDebug( __METHOD__, 'Unknown module name' ); } @@ -92,7 +116,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function run( $resultPageSet = null ) { @@ -111,10 +135,13 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { } if ( $params['unique'] ) { - if ( $fld_ids ) { + $matches = array_intersect_key( $prop, $this->props + array( 'ids' => 1 ) ); + if ( $matches ) { + $p = $this->getModulePrefix(); $this->dieUsage( - "{$this->getModuleName()} cannot return corresponding page ids in unique {$this->descriptionWhat}s mode", - 'params' ); + "Cannot use {$p}prop=" . join( '|', array_keys( $matches ) ) . " with {$p}unique", + 'params' + ); } $this->addOption( 'DISTINCT' ); } @@ -145,19 +172,25 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { } // 'continue' always overrides 'from' - $from = ( $continue || is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); - $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) ); + $from = ( $continue || $params['from'] === null ? null : + $this->titlePartToKey( $params['from'], $namespace ) ); + $to = ( $params['to'] === null ? null : + $this->titlePartToKey( $params['to'], $namespace ) ); $this->addWhereRange( $pfx . $fieldTitle, 'newer', $from, $to ); if ( isset( $params['prefix'] ) ) { - $this->addWhere( $pfx . $fieldTitle . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); + $this->addWhere( $pfx . $fieldTitle . $db->buildLike( $this->titlePartToKey( + $params['prefix'], $namespace ), $db->anyString() ) ); } $this->addFields( array( 'pl_title' => $pfx . $fieldTitle ) ); $this->addFieldsIf( array( 'pl_from' => $pfx . 'from' ), !$params['unique'] ); + foreach ( $this->props as $name => $field ) { + $this->addFieldsIf( $field, isset( $prop[$name] ) ); + } - if ( $this->hasNamespace ) { - $this->addOption( 'USE INDEX', $pfx . 'namespace' ); + if ( $this->useIndex ) { + $this->addOption( 'USE INDEX', $this->useIndex ); } $limit = $params['limit']; $this->addOption( 'LIMIT', $limit + 1 ); @@ -177,8 +210,9 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $count = 0; $result = $this->getResult(); foreach ( $res as $row ) { - if ( ++ $count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... if ( $params['unique'] ) { $this->setContinueEnumParameter( 'continue', $row->pl_title ); } else { @@ -196,6 +230,11 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $title = Title::makeTitle( $namespace, $row->pl_title ); ApiQueryBase::addTitleInfo( $vals, $title ); } + foreach ( $this->props as $name => $field ) { + if ( isset( $prop[$name] ) && $row->$field !== null && $row->$field !== '' ) { + $vals[$name] = $row->$field; + } + } $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { if ( $params['unique'] ) { @@ -231,10 +270,9 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { 'prop' => array( ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_DFLT => 'title', - ApiBase::PARAM_TYPE => array( - 'ids', - 'title' - ) + ApiBase::PARAM_TYPE => array_merge( + array( 'ids', 'title' ), array_keys( $this->props ) + ), ), 'namespace' => array( ApiBase::PARAM_DFLT => $this->dfltNamespace, @@ -258,6 +296,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { if ( !$this->hasNamespace ) { unset( $allowedParams['namespace'] ); } + return $allowedParams; } @@ -271,68 +310,55 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { 'to' => "The title of the $what to stop enumerating at", 'prefix' => "Search for all $targets that begin with this value", 'unique' => array( - "Only show distinct $targets. Cannot be used with {$p}prop=ids.", + "Only show distinct $targets. Cannot be used with {$p}prop=" . + join( '|', array_keys( array( 'ids' => 1 ) + $this->props ) ) . '.', 'When used as a generator, yields target pages instead of source pages.', ), 'prop' => array( 'What pieces of information to include', - " ids - Adds the pageid of the $linking page (Cannot be used with {$p}unique)", - " title - Adds the title of the $what", + " ids - Adds the pageid of the $linking page (Cannot be used with {$p}unique)", + " title - Adds the title of the $what", ), 'namespace' => 'The namespace to enumerate', 'limit' => 'How many total items to return', 'continue' => 'When more results are available, use this to continue', 'dir' => 'The direction in which to list', ); + foreach ( $this->propHelp as $help ) { + $paramDescription['prop'][] = "$help (Cannot be used with {$p}unique)"; + } if ( !$this->hasNamespace ) { unset( $paramDescription['namespace'] ); } - return $paramDescription; - } - public function getResultProperties() { - return array( - 'ids' => array( - 'fromid' => 'integer' - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ) - ); + return $paramDescription; } public function getDescription() { return $this->description; } - public function getPossibleErrors() { - $m = $this->getModuleName(); - $what = $this->descriptionWhat; - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'params', 'info' => "{$m} cannot return corresponding page ids in unique {$what}s mode" ), - ) ); - } - public function getExamples() { $p = $this->getModulePrefix(); $name = $this->getModuleName(); $what = $this->descriptionWhat; $targets = $this->descriptionTargets; + return array( "api.php?action=query&list={$name}&{$p}from=B&{$p}prop=ids|title" - => "List $targets with page ids they are from, including missing ones. Start at B", + => "List $targets with page ids they are from, including missing ones. Start at B", "api.php?action=query&list={$name}&{$p}unique=&{$p}from=B" - => "List unique $targets", + => "List unique $targets", "api.php?action=query&generator={$name}&g{$p}unique=&g{$p}from=B" - => "Gets all $targets, marking the missing ones", + => "Gets all $targets, marking the missing ones", "api.php?action=query&generator={$name}&g{$p}from=B" - => "Gets pages containing the {$what}s", + => "Gets pages containing the {$what}s", ); } public function getHelpUrls() { $name = ucfirst( $this->getModuleName() ); + return "https://www.mediawiki.org/wiki/API:{$name}"; } } diff --git a/includes/api/ApiQueryAllMessages.php b/includes/api/ApiQueryAllMessages.php index d47c7b76..a75a16fc 100644 --- a/includes/api/ApiQueryAllMessages.php +++ b/includes/api/ApiQueryAllMessages.php @@ -31,7 +31,7 @@ */ class ApiQueryAllMessages extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'am' ); } @@ -63,14 +63,13 @@ class ApiQueryAllMessages extends ApiQueryBase { if ( in_array( '*', $params['messages'] ) ) { $message_names = Language::getMessageKeysFor( $langObj->getCode() ); if ( $params['includelocal'] ) { - global $wgLanguageCode; $message_names = array_unique( array_merge( $message_names, // Pass in the content language code so we get local messages that have a // MediaWiki:msgkey page. We might theoretically miss messages that have no // MediaWiki:msgkey page but do have a MediaWiki:msgkey/lang page, but that's // just a stupid case. - MessageCache::singleton()->getAllMessageKeys( $wgLanguageCode ) + MessageCache::singleton()->getAllMessageKeys( $this->getConfig()->get( 'LanguageCode' ) ) ) ); } sort( $message_names ); @@ -116,7 +115,7 @@ class ApiQueryAllMessages extends ApiQueryBase { global $wgContLang; $lang = $langObj->getCode(); - $customisedMessages = AllmessagesTablePager::getCustomisedStatuses( + $customisedMessages = AllMessagesTablePager::getCustomisedStatuses( array_map( array( $langObj, 'ucfirst' ), $messages_target ), $lang, $lang != $wgContLang->getCode() ); $customised = $params['customised'] === 'modified'; @@ -241,10 +240,10 @@ class ApiQueryAllMessages extends ApiQueryBase { 'messages' => 'Which messages to output. "*" (default) means all messages', 'prop' => 'Which properties to get', 'enableparser' => array( 'Set to enable parser, will preprocess the wikitext of message', - 'Will substitute magic words, handle templates etc.' ), + 'Will substitute magic words, handle templates etc.' ), 'nocontent' => 'If set, do not include the content of the messages in the output.', 'includelocal' => array( "Also include local messages, i.e. messages that don't exist in the software but do exist as a MediaWiki: page.", - "This lists all MediaWiki: pages, so it will also list those that aren't 'really' messages such as Common.js", + "This lists all MediaWiki: pages, so it will also list those that aren't 'really' messages such as Common.js", ), 'title' => 'Page name to use as context when parsing message (for enableparser option)', 'args' => 'Arguments to be substituted into message', @@ -257,35 +256,8 @@ class ApiQueryAllMessages extends ApiQueryBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'invalidlang', 'info' => 'Invalid language code for parameter lang' ), - ) ); - } - - public function getResultProperties() { - return array( - '' => array( - 'name' => 'string', - 'customised' => 'boolean', - 'missing' => 'boolean', - '*' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'default' => array( - 'defaultmissing' => 'boolean', - 'default' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { - return 'Return messages from this site'; + return 'Return messages from this site.'; } public function getExamples() { diff --git a/includes/api/ApiQueryAllPages.php b/includes/api/ApiQueryAllPages.php index d95980c2..b7bd65a5 100644 --- a/includes/api/ApiQueryAllPages.php +++ b/includes/api/ApiQueryAllPages.php @@ -31,7 +31,7 @@ */ class ApiQueryAllPages extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'ap' ); } @@ -44,19 +44,23 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ public function executeGenerator( $resultPageSet ) { if ( $resultPageSet->isResolvingRedirects() ) { - $this->dieUsage( 'Use "gapfilterredir=nonredirects" option instead of "redirects" when using allpages as a generator', 'params' ); + $this->dieUsage( + 'Use "gapfilterredir=nonredirects" option instead of "redirects" ' . + 'when using allpages as a generator', + 'params' + ); } $this->run( $resultPageSet ); } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function run( $resultPageSet = null ) { @@ -83,12 +87,18 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { $this->addWhereFld( 'page_namespace', $params['namespace'] ); $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' ); - $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); - $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) ); + $from = ( $params['from'] === null + ? null + : $this->titlePartToKey( $params['from'], $params['namespace'] ) ); + $to = ( $params['to'] === null + ? null + : $this->titlePartToKey( $params['to'], $params['namespace'] ) ); $this->addWhereRange( 'page_title', $dir, $from, $to ); if ( isset( $params['prefix'] ) ) { - $this->addWhere( 'page_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); + $this->addWhere( 'page_title' . $db->buildLike( + $this->titlePartToKey( $params['prefix'], $params['namespace'] ), + $db->anyString() ) ); } if ( is_null( $resultPageSet ) ) { @@ -145,7 +155,6 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { } $this->addOption( 'DISTINCT' ); - } elseif ( isset( $params['prlevel'] ) ) { $this->dieUsage( 'prlevel may not be used without prtype', 'params' ); } @@ -186,8 +195,9 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { $count = 0; $result = $this->getResult(); foreach ( $res as $row ) { - if ( ++ $count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... $this->setContinueEnumParameter( 'continue', $row->page_title ); break; } @@ -215,8 +225,6 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { } public function getAllowedParams() { - global $wgRestrictionLevels; - return array( 'from' => null, 'continue' => null, @@ -245,7 +253,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { ApiBase::PARAM_ISMULTI => true ), 'prlevel' => array( - ApiBase::PARAM_TYPE => $wgRestrictionLevels, + ApiBase::PARAM_TYPE => $this->getConfig()->get( 'RestrictionLevels' ), ApiBase::PARAM_ISMULTI => true ), 'prfiltercascade' => array( @@ -291,6 +299,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'from' => 'The page title to start enumerating from', 'continue' => 'When more results are available, use this to continue', @@ -303,7 +312,8 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { 'maxsize' => 'Limit to pages with at most this many bytes', 'prtype' => 'Limit to protected pages only', 'prlevel' => "The protection level (must be used with {$p}prtype= parameter)", - 'prfiltercascade' => "Filter protections based on cascadingness (ignored when {$p}prtype isn't set)", + 'prfiltercascade' + => "Filter protections based on cascadingness (ignored when {$p}prtype isn't set)", 'filterlanglinks' => array( 'Filter based on whether a page has langlinks', 'Note that this may not consider langlinks added by extensions.', @@ -318,25 +328,8 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'pageid' => 'integer', - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { - return 'Enumerate all pages sequentially in a given namespace'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'params', 'info' => 'Use "gapfilterredir=nonredirects" option instead of "redirects" when using allpages as a generator' ), - array( 'code' => 'params', 'info' => 'prlevel may not be used without prtype' ), - ) ); + return 'Enumerate all pages sequentially in a given namespace.'; } public function getExamples() { @@ -349,9 +342,9 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { 'Using as Generator', 'Show info about 4 pages starting at the letter "T"', ), - 'api.php?action=query&generator=allpages&gaplimit=2&gapfilterredir=nonredirects&gapfrom=Re&prop=revisions&rvprop=content' => array( - 'Show content of first 2 non-redirect pages beginning at "Re"', - ) + 'api.php?action=query&generator=allpages&gaplimit=2&' . + 'gapfilterredir=nonredirects&gapfrom=Re&prop=revisions&rvprop=content' + => array( 'Show content of first 2 non-redirect pages beginning at "Re"' ) ); } diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index 1948a51a..affddda7 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -30,7 +30,7 @@ * @ingroup API */ class ApiQueryAllUsers extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'au' ); } @@ -38,15 +38,22 @@ class ApiQueryAllUsers extends ApiQueryBase { * This function converts the user name to a canonical form * which is stored in the database. * @param string $name - * @return String + * @return string */ private function getCanonicalUserName( $name ) { return str_replace( '_', ' ', $name ); } public function execute() { - $db = $this->getDB(); $params = $this->extractRequestParams(); + $activeUserDays = $this->getConfig()->get( 'ActiveUserDays' ); + + if ( $params['activeusers'] ) { + // Update active user cache + SpecialActiveUsers::mergeActiveUsers( 600, $activeUserDays ); + } + + $db = $this->getDB(); $prop = $params['prop']; if ( !is_null( $prop ) ) { @@ -58,7 +65,8 @@ class ApiQueryAllUsers extends ApiQueryBase { $fld_registration = isset( $prop['registration'] ); $fld_implicitgroups = isset( $prop['implicitgroups'] ); } else { - $fld_blockinfo = $fld_editcount = $fld_groups = $fld_registration = $fld_rights = $fld_implicitgroups = false; + $fld_blockinfo = $fld_editcount = $fld_groups = $fld_registration = + $fld_rights = $fld_implicitgroups = false; } $limit = $params['limit']; @@ -70,9 +78,9 @@ class ApiQueryAllUsers extends ApiQueryBase { $from = is_null( $params['from'] ) ? null : $this->getCanonicalUserName( $params['from'] ); $to = is_null( $params['to'] ) ? null : $this->getCanonicalUserName( $params['to'] ); - # MySQL doesn't seem to use 'equality propagation' here, so like the - # ActiveUsers special page, we have to use rc_user_text for some cases. - $userFieldToSort = $params['activeusers'] ? 'rc_user_text' : 'user_name'; + # MySQL can't figure out that 'user_name' and 'qcc_title' are the same + # despite the JOIN condition, so manually sort on the correct one. + $userFieldToSort = $params['activeusers'] ? 'qcc_title' : 'user_name'; $this->addWhereRange( $userFieldToSort, $dir, $from, $to ); @@ -90,6 +98,7 @@ class ApiQueryAllUsers extends ApiQueryBase { // no group with the given right(s) exists, no need for a query if ( !count( $groups ) ) { $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), '' ); + return; } @@ -111,7 +120,7 @@ class ApiQueryAllUsers extends ApiQueryBase { // Filter only users that belong to a given group $this->addTables( 'user_groups', 'ug1' ); $this->addJoinConds( array( 'ug1' => array( 'INNER JOIN', array( 'ug1.ug_user=user_id', - 'ug1.ug_group' => $params['group'] ) ) ) ); + 'ug1.ug_group' => $params['group'] ) ) ) ); } if ( !is_null( $params['excludegroup'] ) && count( $params['excludegroup'] ) ) { @@ -122,12 +131,14 @@ class ApiQueryAllUsers extends ApiQueryBase { if ( count( $params['excludegroup'] ) == 1 ) { $exclude = array( 'ug1.ug_group' => $params['excludegroup'][0] ); } else { - $exclude = array( $db->makeList( array( 'ug1.ug_group' => $params['excludegroup'] ), LIST_OR ) ); + $exclude = array( $db->makeList( + array( 'ug1.ug_group' => $params['excludegroup'] ), + LIST_OR + ) ); } $this->addJoinConds( array( 'ug1' => array( 'LEFT OUTER JOIN', array_merge( array( 'ug1.ug_user=user_id' ), $exclude ) - ) - ) ); + ) ) ); $this->addWhere( 'ug1.ug_user IS NULL' ); } @@ -145,26 +156,38 @@ class ApiQueryAllUsers extends ApiQueryBase { $this->addTables( 'user_groups', 'ug2' ); $this->addJoinConds( array( 'ug2' => array( 'LEFT JOIN', 'ug2.ug_user=user_id' ) ) ); - $this->addFields( 'ug2.ug_group ug_group2' ); + $this->addFields( array( 'ug_group2' => 'ug2.ug_group' ) ); } else { $sqlLimit = $limit + 1; } if ( $params['activeusers'] ) { - global $wgActiveUserDays; - $this->addTables( 'recentchanges' ); - - $this->addJoinConds( array( 'recentchanges' => array( - 'INNER JOIN', 'rc_user_text=user_name' + $activeUserSeconds = $activeUserDays * 86400; + + // Filter query to only include users in the active users cache + $this->addTables( 'querycachetwo' ); + $this->addJoinConds( array( 'querycachetwo' => array( + 'INNER JOIN', array( + 'qcc_type' => 'activeusers', + 'qcc_namespace' => NS_USER, + 'qcc_title=user_name', + ), ) ) ); - $this->addFields( array( 'recentedits' => 'COUNT(*)' ) ); - - $this->addWhere( 'rc_log_type IS NULL OR rc_log_type != ' . $db->addQuotes( 'newusers' ) ); - $timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays * 24 * 3600 ); - $this->addWhere( 'rc_timestamp >= ' . $db->addQuotes( $timestamp ) ); - - $this->addOption( 'GROUP BY', $userFieldToSort ); + // Actually count the actions using a subquery (bug 64505 and bug 64507) + $timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds ); + $this->addFields( array( + 'recentactions' => '(' . $db->selectSQLText( + 'recentchanges', + 'COUNT(*)', + array( + 'rc_user_text = user_name', + 'rc_type != ' . $db->addQuotes( RC_EXTERNAL ), // no wikidata + 'rc_log_type IS NULL OR rc_log_type != ' . $db->addQuotes( 'newusers' ), + 'rc_timestamp >= ' . $db->addQuotes( $timestamp ), + ) + ) . ')' + ) ); } $this->addOption( 'LIMIT', $sqlLimit ); @@ -187,12 +210,12 @@ class ApiQueryAllUsers extends ApiQueryBase { $lastUser = false; $result = $this->getResult(); - // - // This loop keeps track of the last entry. - // For each new row, if the new row is for different user then the last, the last entry is added to results. - // Otherwise, the group of the new row is appended to the last entry. - // The setContinue... is more complex because of this, and takes into account the higher sql limit - // to make sure all rows that belong to the same user are received. + // This loop keeps track of the last entry. For each new row, if the + // new row is for different user then the last, the last entry is added + // to results. Otherwise, the group of the new row is appended to the + // last entry. The setContinue... is more complex because of this, and + // takes into account the higher sql limit to make sure all rows that + // belong to the same user are received. foreach ( $res as $row ) { $count++; @@ -200,8 +223,13 @@ class ApiQueryAllUsers extends ApiQueryBase { if ( $lastUser !== $row->user_name ) { // Save the last pass's user data if ( is_array( $lastUserData ) ) { - $fit = $result->addValue( array( 'query', $this->getModuleName() ), + if ( $params['activeusers'] && $lastUserData['recentactions'] === 0 ) { + // activeusers cache was out of date + $fit = true; + } else { + $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $lastUserData ); + } $lastUserData = null; @@ -212,7 +240,8 @@ class ApiQueryAllUsers extends ApiQueryBase { } if ( $count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... $this->setContinueEnumParameter( 'from', $row->user_name ); break; } @@ -227,6 +256,7 @@ class ApiQueryAllUsers extends ApiQueryBase { $lastUserData['blockid'] = $row->ipb_id; $lastUserData['blockedby'] = $row->ipb_by_text; $lastUserData['blockedbyid'] = $row->ipb_by; + $lastUserData['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp ); $lastUserData['blockreason'] = $row->ipb_reason; $lastUserData['blockexpiry'] = $row->ipb_expiry; } @@ -237,7 +267,9 @@ class ApiQueryAllUsers extends ApiQueryBase { $lastUserData['editcount'] = intval( $row->user_editcount ); } if ( $params['activeusers'] ) { - $lastUserData['recenteditcount'] = intval( $row->recentedits ); + $lastUserData['recentactions'] = intval( $row->recentactions ); + // @todo 'recenteditcount' is set for BC, remove in 1.25 + $lastUserData['recenteditcount'] = $lastUserData['recentactions']; } if ( $fld_registration ) { $lastUserData['registration'] = $row->user_registration ? @@ -246,10 +278,13 @@ class ApiQueryAllUsers extends ApiQueryBase { } if ( $sqlLimit == $count ) { - // BUG! database contains group name that User::getAllGroups() does not return - // TODO: should handle this more gracefully - ApiBase::dieDebug( __METHOD__, - 'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function' ); + // @todo BUG! database contains group name that User::getAllGroups() does not return + // Should handle this more gracefully + ApiBase::dieDebug( + __METHOD__, + 'MediaWiki configuration error: The database contains more ' . + 'user groups than known to User::getAllGroups() function' + ); } $lastUserObj = User::newFromId( $row->user_id ); @@ -295,7 +330,9 @@ class ApiQueryAllUsers extends ApiQueryBase { } } - if ( is_array( $lastUserData ) ) { + if ( is_array( $lastUserData ) && + !( $params['activeusers'] && $lastUserData['recentactions'] === 0 ) + ) { $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $lastUserData ); if ( !$fit ) { @@ -312,6 +349,7 @@ class ApiQueryAllUsers extends ApiQueryBase { public function getAllowedParams() { $userGroups = User::getAllGroups(); + return array( 'from' => null, 'to' => null, @@ -359,7 +397,6 @@ class ApiQueryAllUsers extends ApiQueryBase { } public function getParamDescription() { - global $wgActiveUserDays; return array( 'from' => 'The user name to start enumerating from', 'to' => 'The user name to stop enumerating at', @@ -367,72 +404,26 @@ class ApiQueryAllUsers extends ApiQueryBase { 'dir' => 'Direction to sort in', 'group' => 'Limit users to given group name(s)', 'excludegroup' => 'Exclude users in given group name(s)', - 'rights' => 'Limit users to given right(s) (does not include rights granted by implicit or auto-promoted groups like *, user, or autoconfirmed)', + 'rights' => 'Limit users to given right(s) (does not include rights ' . + 'granted by implicit or auto-promoted groups like *, user, or autoconfirmed)', 'prop' => array( 'What pieces of information to include.', ' blockinfo - Adds the information about a current block on the user', - ' groups - Lists groups that the user is in. This uses more server resources and may return fewer results than the limit', + ' groups - Lists groups that the user is in. This uses ' . + 'more server resources and may return fewer results than the limit', ' implicitgroups - Lists all the groups the user is automatically in', ' rights - Lists rights that the user has', ' editcount - Adds the edit count of the user', ' registration - Adds the timestamp of when the user registered if available (may be blank)', - ), + ), 'limit' => 'How many total user names to return', 'witheditsonly' => 'Only list users who have made edits', - 'activeusers' => "Only list users active in the last {$wgActiveUserDays} days(s)" - ); - } - - public function getResultProperties() { - return array( - '' => array( - 'userid' => 'integer', - 'name' => 'string', - 'recenteditcount' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'blockinfo' => array( - 'blockid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'blockedby' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'blockedbyid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'blockedreason' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'blockedexpiry' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'hidden' => 'boolean' - ), - 'editcount' => array( - 'editcount' => 'integer' - ), - 'registration' => array( - 'registration' => 'string' - ) + 'activeusers' => "Only list users active in the last {$this->getConfig()->get( 'ActiveUserDays' )} days(s)" ); } public function getDescription() { - return 'Enumerate all registered users'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'group-excludegroup', 'info' => 'group and excludegroup cannot be used together' ), - ) ); + return 'Enumerate all registered users.'; } public function getExamples() { diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index 2d1089a7..c141246d 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -75,7 +75,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { ) ); - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { $settings = $this->backlinksSettings[$moduleName]; $prefix = $settings['prefix']; $code = $settings['code']; @@ -116,7 +116,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function prepareFirstQuery( $resultPageSet = null ) { @@ -149,7 +149,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if ( $this->params['filterredir'] == 'redirects' ) { $this->addWhereFld( 'page_is_redirect', 1 ); } elseif ( $this->params['filterredir'] == 'nonredirects' && !$this->redirect ) { - // bug 22245 - Check for !redirect, as filtering nonredirects, when getting what links to them is contradictory + // bug 22245 - Check for !redirect, as filtering nonredirects, when + // getting what links to them is contradictory $this->addWhereFld( 'page_is_redirect', 0 ); } @@ -160,7 +161,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function prepareSecondQuery( $resultPageSet = null ) { @@ -193,7 +194,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $redirNs = $t->getNamespace(); $redirDBkey = $t->getDBkey(); $titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $redirDBkey ) . - ( $this->hasNS ? " AND {$this->bl_ns} = {$redirNs}" : '' ); + ( $this->hasNS ? " AND {$this->bl_ns} = {$redirNs}" : '' ); $allRedirNs[] = $redirNs; $allRedirDBkey[] = $redirDBkey; } @@ -209,14 +210,14 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $from = $this->redirID; if ( $this->hasNS ) { $this->addWhere( "{$this->bl_ns} $op $ns OR " . - "({$this->bl_ns} = $ns AND " . - "({$this->bl_title} $op $title OR " . - "({$this->bl_title} = $title AND " . - "{$this->bl_from} $op= $from)))" ); + "({$this->bl_ns} = $ns AND " . + "({$this->bl_title} $op $title OR " . + "({$this->bl_title} = $title AND " . + "{$this->bl_from} $op= $from)))" ); } else { $this->addWhere( "{$this->bl_title} $op $title OR " . - "({$this->bl_title} = $title AND " . - "{$this->bl_from} $op= $from)" ); + "({$this->bl_title} = $title AND " . + "{$this->bl_from} $op= $from)" ); } } if ( $this->params['filterredir'] == 'redirects' ) { @@ -241,7 +242,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function run( $resultPageSet = null ) { @@ -268,8 +269,9 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $count = 0; foreach ( $res as $row ) { - if ( ++ $count > $this->params['limit'] ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... + if ( ++$count > $this->params['limit'] ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... // Continue string preserved in case the redirect query doesn't pass the limit $this->continueStr = $this->getContinueStr( $row->page_id ); break; @@ -294,7 +296,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $count = 0; foreach ( $res as $row ) { if ( ++$count > $this->params['limit'] ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... + // 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 ) { $parentID = $this->pageMap[$row->{$this->bl_ns}][$row->{$this->bl_title}]; @@ -384,7 +387,10 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $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 ); + $this->getResult()->setIndexedTagName( + $this->resultArr[$parentID]['redirlinks'], + $this->bl_code + ); } protected function processContinue() { @@ -396,7 +402,10 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { // only image titles are allowed for the root in imageinfo mode if ( !$this->hasNS && $this->rootTitle->getNamespace() !== NS_FILE ) { - $this->dieUsage( "The title for {$this->getModuleName()} query must be an image", 'bad_image_title' ); + $this->dieUsage( + "The title for {$this->getModuleName()} query must be an image", + 'bad_image_title' + ); } } @@ -428,7 +437,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { return; } $this->redirID = $redirID; - } protected function getContinueStr( $lastPageID ) { @@ -481,6 +489,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { return $retval; } $retval['redirect'] = false; + return $retval; } @@ -494,50 +503,36 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { ); if ( $this->getModuleName() != 'embeddedin' ) { return array_merge( $retval, array( - 'redirect' => 'If linking page is a redirect, find all pages that link to that redirect as well. Maximum limit is halved.', - 'filterredir' => "How to filter for redirects. If set to nonredirects when {$this->bl_code}redirect is enabled, this is only applied to the second level", - 'limit' => "How many total pages to return. If {$this->bl_code}redirect is enabled, limit applies to each level separately (which means you may get up to 2 * limit results)." + 'redirect' => 'If linking page is a redirect, find all pages ' . + 'that link to that redirect as well. Maximum limit is halved.', + 'filterredir' => 'How to filter for redirects. If set to ' . + "nonredirects when {$this->bl_code}redirect is enabled, " . + 'this is only applied to the second level', + 'limit' => 'How many total pages to return. If ' . + "{$this->bl_code}redirect is enabled, limit applies to each " . + 'level separately (which means you may get up to 2 * limit results).' ) ); } + return array_merge( $retval, array( 'filterredir' => 'How to filter for redirects', 'limit' => 'How many total pages to return' ) ); } - public function getResultProperties() { - return array( - '' => array( - 'pageid' => 'integer', - 'ns' => 'namespace', - 'title' => 'string', - 'redirect' => 'boolean' - ) - ); - } - public function getDescription() { switch ( $this->getModuleName() ) { case 'backlinks': - return 'Find all pages that link to the given page'; + return 'Find all pages that link to the given page.'; case 'embeddedin': - return 'Find all pages that embed (transclude) the given title'; + return 'Find all pages that embed (transclude) the given title.'; case 'imageusage': return 'Find all pages that use the given image title.'; default: - ApiBase::dieDebug( __METHOD__, 'Unknown module name' ); + ApiBase::dieDebug( __METHOD__, 'Unknown module name.' ); } } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getTitleOrPageIdErrorMessage(), - array( - array( 'code' => 'bad_image_title', 'info' => "The title for {$this->getModuleName()} query must be an image" ), - ) - ); - } - public function getExamples() { static $examples = array( 'backlinks' => array( diff --git a/includes/api/ApiQueryBacklinksprop.php b/includes/api/ApiQueryBacklinksprop.php new file mode 100644 index 00000000..cd682612 --- /dev/null +++ b/includes/api/ApiQueryBacklinksprop.php @@ -0,0 +1,472 @@ +<?php +/** + * API module to handle links table back-queries + * + * Created on Aug 19, 2014 + * + * Copyright © 2014 Brad Jorsch <bjorsch@wikimedia.org> + * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.24 + */ + +/** + * This implements prop=redirects, prop=linkshere, prop=catmembers, + * prop=transcludedin, and prop=fileusage + * + * @ingroup API + * @since 1.24 + */ +class ApiQueryBacklinksprop extends ApiQueryGeneratorBase { + + // Data for the various modules implemented by this class + private static $settings = array( + 'redirects' => array( + 'code' => 'rd', + 'prefix' => 'rd', + 'linktable' => 'redirect', + 'what' => 'redirects to', + 'description' => 'Returns all redirects to the given pages.', + 'props' => array( + 'fragment' => 'Fragment of each redirect, if any', + ), + 'showredirects' => false, + 'show' => array( + 'fragment' => 'Only show redirects with a fragment', + '!fragment' => 'Only show redirects without a fragment', + ), + ), + 'linkshere' => array( + 'code' => 'lh', + 'prefix' => 'pl', + 'linktable' => 'pagelinks', + 'from_namespace' => true, + 'what' => 'pages linking to', + 'description' => 'Find all pages that link to the given pages.', + 'showredirects' => true, + ), + 'transcludedin' => array( + 'code' => 'ti', + 'prefix' => 'tl', + 'linktable' => 'templatelinks', + 'from_namespace' => true, + 'what' => 'pages transcluding', + 'description' => 'Find all pages that transclude the given pages.', + 'showredirects' => true, + ), + 'fileusage' => array( + 'code' => 'fu', + 'prefix' => 'il', + 'linktable' => 'imagelinks', + 'from_namespace' => true, + 'to_namespace' => NS_FILE, + 'what' => 'pages using', + 'exampletitle' => 'File:Example.jpg', + 'description' => 'Find all pages that use the given files.', + 'showredirects' => true, + ), + ); + + public function __construct( ApiQuery $query, $moduleName ) { + parent::__construct( $query, $moduleName, self::$settings[$moduleName]['code'] ); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator( $resultPageSet ) { + $this->run( $resultPageSet ); + } + + /** + * @param ApiPageSet $resultPageSet + */ + private function run( ApiPageSet $resultPageSet = null ) { + $settings = self::$settings[$this->getModuleName()]; + + $db = $this->getDB(); + $params = $this->extractRequestParams(); + $prop = array_flip( $params['prop'] ); + $emptyString = $db->addQuotes( '' ); + + $pageSet = $this->getPageSet(); + $titles = $pageSet->getGoodTitles() + $pageSet->getMissingTitles(); + $map = $pageSet->getAllTitlesByNamespace(); + + // Determine our fields to query on + $p = $settings['prefix']; + $hasNS = !isset( $settings['to_namespace'] ); + if ( $hasNS ) { + $bl_namespace = "{$p}_namespace"; + $bl_title = "{$p}_title"; + } else { + $bl_namespace = $settings['to_namespace']; + $bl_title = "{$p}_to"; + + $titles = array_filter( $titles, function ( $t ) use ( $bl_namespace ) { + return $t->getNamespace() === $bl_namespace; + } ); + $map = array_intersect_key( $map, array( $bl_namespace => true ) ); + } + $bl_from = "{$p}_from"; + + if ( !$titles ) { + return; // nothing to do + } + + // Figure out what we're sorting by, and add associated WHERE clauses. + // MySQL's query planner screws up if we include a field in ORDER BY + // when it's constant in WHERE, so we have to test that for each field. + $sortby = array(); + if ( $hasNS && count( $map ) > 1 ) { + $sortby[$bl_namespace] = 'ns'; + } + $theTitle = null; + foreach ( $map as $nsTitles ) { + reset( $nsTitles ); + $key = key( $nsTitles ); + if ( $theTitle === null ) { + $theTitle = $key; + } + if ( count( $nsTitles ) > 1 || $key !== $theTitle ) { + $sortby[$bl_title] = 'title'; + break; + } + } + $miser_ns = null; + if ( $params['namespace'] !== null ) { + if ( empty( $settings['from_namespace'] ) && $this->getConfig()->get( 'MiserMode' ) ) { + $miser_ns = $params['namespace']; + } else { + $this->addWhereFld( "{$p}_from_namespace", $params['namespace'] ); + if ( !empty( $settings['from_namespace'] ) && count( $params['namespace'] ) > 1 ) { + $sortby["{$p}_from_namespace"] = 'int'; + } + } + } + $sortby[$bl_from] = 'int'; + + // Now use the $sortby to figure out the continuation + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != count( $sortby ) ); + $where = ''; + $i = count( $sortby ) - 1; + $cont_ns = 0; + $cont_title = ''; + foreach ( array_reverse( $sortby, true ) as $field => $type ) { + $v = $cont[$i]; + switch ( $type ) { + case 'ns': + $cont_ns = (int)$v; + /* fall through */ + case 'int': + $v = (int)$v; + $this->dieContinueUsageIf( $v != $cont[$i] ); + break; + + case 'title': + $cont_title = $v; + /* fall through */ + default: + $v = $db->addQuotes( $v ); + break; + } + + if ( $where === '' ) { + $where = "$field >= $v"; + } else { + $where = "$field > $v OR ($field = $v AND ($where))"; + } + + $i--; + } + $this->addWhere( $where ); + } + + // Populate the rest of the query + $this->addTables( array( $settings['linktable'], 'page' ) ); + $this->addWhere( "$bl_from = page_id" ); + + if ( $this->getModuleName() === 'redirects' ) { + $this->addWhere( "rd_interwiki = $emptyString OR rd_interwiki IS NULL" ); + } + + $this->addFields( array_keys( $sortby ) ); + $this->addFields( array( 'bl_namespace' => $bl_namespace, 'bl_title' => $bl_title ) ); + if ( is_null( $resultPageSet ) ) { + $fld_pageid = isset( $prop['pageid'] ); + $fld_title = isset( $prop['title'] ); + $fld_redirect = isset( $prop['redirect'] ); + + $this->addFieldsIf( 'page_id', $fld_pageid ); + $this->addFieldsIf( array( 'page_title', 'page_namespace' ), $fld_title ); + $this->addFieldsIf( 'page_is_redirect', $fld_redirect ); + + // prop=redirects + $fld_fragment = isset( $prop['fragment'] ); + $this->addFieldsIf( 'rd_fragment', $fld_fragment ); + } else { + $this->addFields( $resultPageSet->getPageTableFields() ); + } + + $this->addFieldsIf( 'page_namespace', $miser_ns !== null ); + + if ( $hasNS ) { + $lb = new LinkBatch( $titles ); + $this->addWhere( $lb->constructSet( $p, $db ) ); + } else { + $where = array(); + foreach ( $titles as $t ) { + if ( $t->getNamespace() == $bl_namespace ) { + $where[] = "$bl_title = " . $db->addQuotes( $t->getDBkey() ); + } + } + $this->addWhere( $db->makeList( $where, LIST_OR ) ); + } + + if ( $params['show'] !== null ) { + // prop=redirects only + $show = array_flip( $params['show'] ); + if ( isset( $show['fragment'] ) && isset( $show['!fragment'] ) || + isset( $show['redirect'] ) && isset( $show['!redirect'] ) + ) { + $this->dieUsageMsg( 'show' ); + } + $this->addWhereIf( "rd_fragment != $emptyString", isset( $show['fragment'] ) ); + $this->addWhereIf( + "rd_fragment = $emptyString OR rd_fragment IS NULL", + isset( $show['!fragment'] ) + ); + $this->addWhereIf( array( 'page_is_redirect' => 1 ), isset( $show['redirect'] ) ); + $this->addWhereIf( array( 'page_is_redirect' => 0 ), isset( $show['!redirect'] ) ); + } + + // Override any ORDER BY from above with what we calculated earlier. + $this->addOption( 'ORDER BY', array_keys( $sortby ) ); + + $this->addOption( 'LIMIT', $params['limit'] + 1 ); + + $res = $this->select( __METHOD__ ); + + if ( is_null( $resultPageSet ) ) { + $count = 0; + foreach ( $res as $row ) { + if ( ++$count > $params['limit'] ) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinue( $row, $sortby ); + break; + } + + if ( $miser_ns !== null && !in_array( $row->page_namespace, $miser_ns ) ) { + // Miser mode namespace check + continue; + } + + // Get the ID of the current page + $id = $map[$row->bl_namespace][$row->bl_title]; + + $vals = array(); + if ( $fld_pageid ) { + $vals['pageid'] = $row->page_id; + } + if ( $fld_title ) { + ApiQueryBase::addTitleInfo( $vals, + Title::makeTitle( $row->page_namespace, $row->page_title ) + ); + } + if ( $fld_fragment && $row->rd_fragment !== null && $row->rd_fragment !== '' ) { + $vals['fragment'] = $row->rd_fragment; + } + if ( $fld_redirect && $row->page_is_redirect ) { + $vals['redirect'] = ''; + } + $fit = $this->addPageSubItem( $id, $vals ); + if ( !$fit ) { + $this->setContinue( $row, $sortby ); + break; + } + } + } else { + $titles = array(); + $count = 0; + foreach ( $res as $row ) { + if ( ++$count > $params['limit'] ) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinue( $row, $sortby ); + break; + } + $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title ); + } + $resultPageSet->populateFromTitles( $titles ); + } + } + + private function setContinue( $row, $sortby ) { + $cont = array(); + foreach ( $sortby as $field => $v ) { + $cont[] = $row->$field; + } + $this->setContinueEnumParameter( 'continue', join( '|', $cont ) ); + } + + public function getCacheMode( $params ) { + return 'public'; + } + + public function getAllowedParams() { + $settings = self::$settings[$this->getModuleName()]; + + $ret = array( + 'prop' => array( + ApiBase::PARAM_TYPE => array( + 'pageid', + 'title', + ), + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_DFLT => 'pageid|title', + ), + 'namespace' => array( + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => 'namespace', + ), + '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 + ), + 'continue' => null, + ); + + if ( !empty( $settings['showredirects'] ) ) { + $ret['prop'][ApiBase::PARAM_TYPE][] = 'redirect'; + $ret['prop'][ApiBase::PARAM_DFLT] .= '|redirect'; + } + if ( isset( $settings['props'] ) ) { + $ret['prop'][ApiBase::PARAM_TYPE] = array_merge( + $ret['prop'][ApiBase::PARAM_TYPE], array_keys( $settings['props'] ) + ); + } + + $show = array(); + if ( !empty( $settings['showredirects'] ) ) { + $show[] = 'redirect'; + $show[] = '!redirect'; + } + if ( isset( $settings['show'] ) ) { + $show = array_merge( $show, array_keys( $settings['show'] ) ); + } + if ( $show ) { + $ret['show'] = array( + ApiBase::PARAM_TYPE => $show, + ApiBase::PARAM_ISMULTI => true, + ); + } + + return $ret; + } + + public function getParamDescription() { + $settings = self::$settings[$this->getModuleName()]; + $p = $this->getModulePrefix(); + + $ret = array( + 'prop' => array( + 'Which properties to get:', + ), + 'show' => array( + 'Show only items that meet this criteria.', + ), + 'namespace' => 'Only include pages in these namespaces', + 'limit' => 'How many to return', + 'continue' => 'When more results are available, use this to continue', + ); + + if ( empty( $settings['from_namespace'] ) && $this->getConfig()->get( 'MiserMode' ) ) { + $ret['namespace'] = array( + $ret['namespace'], + "NOTE: Due to \$wgMiserMode, using this may result in fewer than \"{$p}limit\" results", + 'returned before continuing; in extreme cases, zero results may be returned.', + ); + if ( isset( $ret['type'] ) ) { + $ret['namespace'][] = "Note that you can use {$p}type=subcat or {$p}type=file " . + "instead of {$p}namespace=14 or 6."; + } + } + + $props = array( + 'pageid' => 'Adds the ID of page', + 'title' => 'Adds the title and namespace ID of the page', + ); + if ( !empty( $settings['showredirects'] ) ) { + $props['redirect'] = 'Indicate if the page is a redirect'; + } + if ( isset( $settings['props'] ) ) { + $props += $settings['props']; + } + foreach ( $props as $k => $v ) { + $ret['props'][] = sprintf( "%-9s - %s", $k, $v ); + } + + $show = array(); + if ( !empty( $settings['showredirects'] ) ) { + $show += array( + 'redirect' => 'Only show redirects', + '!redirect' => 'Only show non-redirects', + ); + } + if ( isset( $settings['show'] ) ) { + $show += $settings['show']; + } + foreach ( $show as $k => $v ) { + $ret['show'][] = sprintf( "%-9s - %s", $k, $v ); + } + + return $ret; + } + + public function getDescription() { + return self::$settings[$this->getModuleName()]['description']; + } + + public function getExamples() { + $settings = self::$settings[$this->getModuleName()]; + $name = $this->getModuleName(); + $what = $settings['what']; + $title = isset( $settings['exampletitle'] ) ? $settings['exampletitle'] : 'Main Page'; + $etitle = rawurlencode( $title ); + + return array( + "api.php?action=query&prop={$name}&titles={$etitle}" + => "Get a list of $what [[$title]]", + "api.php?action=query&generator={$name}&titles={$etitle}&prop=info" + => "Get information about $what [[$title]]", + ); + } + + public function getHelpUrls() { + $name = $this->getModuleName(); + $prefix = $this->getModulePrefix(); + return "https://www.mediawiki.org/wiki/API:Properties#{$name}_.2F_{$prefix}"; + } +} diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 8668e04b..65e10ab7 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -36,17 +36,22 @@ abstract class ApiQueryBase extends ApiBase { private $mQueryModule, $mDb, $tables, $where, $fields, $options, $join_conds; /** - * @param $query ApiBase - * @param $moduleName string - * @param $paramPrefix string + * @param ApiQuery $queryModule + * @param string $moduleName + * @param string $paramPrefix */ - public function __construct( ApiBase $query, $moduleName, $paramPrefix = '' ) { - parent::__construct( $query->getMain(), $moduleName, $paramPrefix ); - $this->mQueryModule = $query; + public function __construct( ApiQuery $queryModule, $moduleName, $paramPrefix = '' ) { + parent::__construct( $queryModule->getMain(), $moduleName, $paramPrefix ); + $this->mQueryModule = $queryModule; $this->mDb = null; $this->resetQueryParams(); } + /************************************************************************//** + * @name Methods to implement + * @{ + */ + /** * Get the cache mode for the data generated by this module. Override * this in the module subclass. For possible return values and other @@ -55,7 +60,7 @@ abstract class ApiQueryBase extends ApiBase { * Public caching will only be allowed if *all* the modules that supply * data for a given request return a cache mode of public. * - * @param $params + * @param array $params * @return string */ public function getCacheMode( $params ) { @@ -63,6 +68,68 @@ abstract class ApiQueryBase extends ApiBase { } /** + * Override this method to request extra fields from the pageSet + * using $pageSet->requestField('fieldName') + * @param ApiPageSet $pageSet + */ + public function requestExtraData( $pageSet ) { + } + + /**@}*/ + + /************************************************************************//** + * @name Data access + * @{ + */ + + /** + * Get the main Query module + * @return ApiQuery + */ + public function getQuery() { + return $this->mQueryModule; + } + + /** + * Get the Query database connection (read-only) + * @return DatabaseBase + */ + protected function getDB() { + if ( is_null( $this->mDb ) ) { + $this->mDb = $this->getQuery()->getDB(); + } + + return $this->mDb; + } + + /** + * Selects the query database connection with the given name. + * See ApiQuery::getNamedDB() for more information + * @param string $name Name to assign to the database connection + * @param int $db One of the DB_* constants + * @param array $groups Query groups + * @return DatabaseBase + */ + public function selectNamedDB( $name, $db, $groups ) { + $this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups ); + } + + /** + * Get the PageSet object to work on + * @return ApiPageSet + */ + protected function getPageSet() { + return $this->getQuery()->getPageSet(); + } + + /**@}*/ + + /************************************************************************//** + * @name Querying + * @{ + */ + + /** * Blank the internal arrays with query parameters */ protected function resetQueryParams() { @@ -75,8 +142,8 @@ abstract class ApiQueryBase extends ApiBase { /** * Add a set of tables to the internal array - * @param $tables mixed Table name or array of table names - * @param $alias mixed Table alias, or null for no alias. Cannot be + * @param string|string[] $tables Table name or array of table names + * @param string|null $alias Table alias, or null for no alias. Cannot be * used with multiple tables */ protected function addTables( $tables, $alias = null ) { @@ -101,7 +168,7 @@ abstract class ApiQueryBase extends ApiBase { * 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 + * @param array $join_conds JOIN conditions */ protected function addJoinConds( $join_conds ) { if ( !is_array( $join_conds ) ) { @@ -131,8 +198,10 @@ abstract class ApiQueryBase extends ApiBase { protected function addFieldsIf( $value, $condition ) { if ( $condition ) { $this->addFields( $value ); + return true; } + return false; } @@ -145,7 +214,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 $value mixed String or array + * @param string|array $value */ protected function addWhere( $value ) { if ( is_array( $value ) ) { @@ -161,15 +230,17 @@ abstract class ApiQueryBase extends ApiBase { /** * Same as addWhere(), but add the WHERE clauses only if a condition is met - * @param $value mixed See addWhere() + * @param string|array $value * @param bool $condition If false, do nothing * @return bool $condition */ protected function addWhereIf( $value, $condition ) { if ( $condition ) { $this->addWhere( $value ); + return true; } + return false; } @@ -215,7 +286,9 @@ abstract class ApiQueryBase extends ApiBase { if ( $sort ) { $order = $field . ( $isDirNewer ? '' : ' DESC' ); // Append ORDER BY - $optionOrderBy = isset( $this->options['ORDER BY'] ) ? (array)$this->options['ORDER BY'] : array(); + $optionOrderBy = isset( $this->options['ORDER BY'] ) + ? (array)$this->options['ORDER BY'] + : array(); $optionOrderBy[] = $order; $this->addOption( 'ORDER BY', $optionOrderBy ); } @@ -225,11 +298,11 @@ abstract class ApiQueryBase extends ApiBase { * Add a WHERE clause corresponding to a range, similar to addWhereRange, * but converts $start and $end to database timestamps. * @see addWhereRange - * @param $field - * @param $dir - * @param $start - * @param $end - * @param $sort bool + * @param string $field + * @param string $dir + * @param string $start + * @param string $end + * @param bool $sort */ protected function addTimestampWhereRange( $field, $dir, $start, $end, $sort = true ) { $db = $this->getDb(); @@ -256,16 +329,37 @@ abstract class ApiQueryBase extends ApiBase { * @param string $method Function the query should be attributed to. * You should usually use __METHOD__ here * @param array $extraQuery Query data to add but not store in the object - * Format is array( 'tables' => ..., 'fields' => ..., 'where' => ..., 'options' => ..., 'join_conds' => ... ) + * Format is array( + * 'tables' => ..., + * 'fields' => ..., + * 'where' => ..., + * 'options' => ..., + * 'join_conds' => ... + * ) * @return ResultWrapper */ protected function select( $method, $extraQuery = array() ) { - $tables = array_merge( $this->tables, isset( $extraQuery['tables'] ) ? (array)$extraQuery['tables'] : array() ); - $fields = array_merge( $this->fields, isset( $extraQuery['fields'] ) ? (array)$extraQuery['fields'] : array() ); - $where = array_merge( $this->where, isset( $extraQuery['where'] ) ? (array)$extraQuery['where'] : array() ); - $options = array_merge( $this->options, isset( $extraQuery['options'] ) ? (array)$extraQuery['options'] : array() ); - $join_conds = array_merge( $this->join_conds, isset( $extraQuery['join_conds'] ) ? (array)$extraQuery['join_conds'] : array() ); + $tables = array_merge( + $this->tables, + isset( $extraQuery['tables'] ) ? (array)$extraQuery['tables'] : array() + ); + $fields = array_merge( + $this->fields, + isset( $extraQuery['fields'] ) ? (array)$extraQuery['fields'] : array() + ); + $where = array_merge( + $this->where, + isset( $extraQuery['where'] ) ? (array)$extraQuery['where'] : array() + ); + $options = array_merge( + $this->options, + isset( $extraQuery['options'] ) ? (array)$extraQuery['options'] : array() + ); + $join_conds = array_merge( + $this->join_conds, + isset( $extraQuery['join_conds'] ) ? (array)$extraQuery['join_conds'] : array() + ); // getDB has its own profileDBIn/Out calls $db = $this->getDB(); @@ -278,28 +372,69 @@ abstract class ApiQueryBase extends ApiBase { } /** - * Estimate the row count for the SELECT query that would be run if we - * called select() right now, and check if it's acceptable. - * @return bool true if acceptable, false otherwise + * @param string $query + * @param string $protocol + * @return null|string */ - protected function checkRowCount() { - $db = $this->getDB(); - $this->profileDBIn(); - $rowcount = $db->estimateRowCount( $this->tables, $this->fields, $this->where, __METHOD__, $this->options ); - $this->profileDBOut(); + public function prepareUrlQuerySearchString( $query = null, $protocol = null ) { + $db = $this->getDb(); + if ( !is_null( $query ) || $query != '' ) { + if ( is_null( $protocol ) ) { + $protocol = 'http://'; + } - global $wgAPIMaxDBRows; - if ( $rowcount > $wgAPIMaxDBRows ) { - return false; + $likeQuery = LinkFilter::makeLikeArray( $query, $protocol ); + if ( !$likeQuery ) { + $this->dieUsage( 'Invalid query', 'bad_query' ); + } + + $likeQuery = LinkFilter::keepOneWildcard( $likeQuery ); + + return 'el_index ' . $db->buildLike( $likeQuery ); + } elseif ( !is_null( $protocol ) ) { + return 'el_index ' . $db->buildLike( "$protocol", $db->anyString() ); + } + + return null; + } + + /** + * Filters hidden users (where the user doesn't have the right to view them) + * Also adds relevant block information + * + * @param bool $showBlockInfo + * @return void + */ + public function showHiddenUsersAddBlockInfo( $showBlockInfo ) { + $this->addTables( 'ipblocks' ); + $this->addJoinConds( array( + 'ipblocks' => array( 'LEFT JOIN', 'ipb_user=user_id' ), + ) ); + + $this->addFields( 'ipb_deleted' ); + + if ( $showBlockInfo ) { + $this->addFields( array( 'ipb_id', 'ipb_by', 'ipb_by_text', 'ipb_reason', 'ipb_expiry', 'ipb_timestamp' ) ); + } + + // Don't show hidden names + if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { + $this->addWhere( 'ipb_deleted = 0 OR ipb_deleted IS NULL' ); } - return true; } + /**@}*/ + + /************************************************************************//** + * @name Utility methods + * @{ + */ + /** * Add information (title and namespace) about a Title object to a * result array * @param array $arr Result array à la ApiResult - * @param $title Title + * @param Title $title * @param string $prefix Module prefix */ public static function addTitleInfo( &$arr, $title, $prefix = '' ) { @@ -308,22 +443,6 @@ abstract class ApiQueryBase extends ApiBase { } /** - * Override this method to request extra fields from the pageSet - * using $pageSet->requestField('fieldName') - * @param $pageSet ApiPageSet - */ - public function requestExtraData( $pageSet ) { - } - - /** - * Get the main Query module - * @return ApiQuery - */ - public function getQuery() { - return $this->mQueryModule; - } - - /** * 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 @@ -332,6 +451,7 @@ abstract class ApiQueryBase extends ApiBase { protected function addPageSubItems( $pageId, $data ) { $result = $this->getResult(); $result->setIndexedTagName( $data, $this->getModulePrefix() ); + return $result->addValue( array( 'query', 'pages', intval( $pageId ) ), $this->getModuleName(), $data ); @@ -356,61 +476,134 @@ abstract class ApiQueryBase extends ApiBase { return false; } $result->setIndexedTagName_internal( array( 'query', 'pages', $pageId, - $this->getModuleName() ), $elemname ); + $this->getModuleName() ), $elemname ); + return true; } /** * Set a query-continue value * @param string $paramName Parameter name - * @param string $paramValue Parameter value + * @param string|array $paramValue Parameter value */ protected function setContinueEnumParameter( $paramName, $paramValue ) { - $paramName = $this->encodeParamName( $paramName ); - $msg = array( $paramName => $paramValue ); - $result = $this->getResult(); - $result->disableSizeCheck(); - $result->addValue( 'query-continue', $this->getModuleName(), $msg, ApiResult::ADD_ON_TOP ); - $result->enableSizeCheck(); + $this->getResult()->setContinueParam( $this, $paramName, $paramValue ); } /** - * Get the Query database connection (read-only) - * @return DatabaseBase + * Convert an input title or title prefix into a dbkey. + * + * $namespace should always be specified in order to handle per-namespace + * capitalization settings. + * + * @param string $titlePart Title part + * @param int $defaultNamespace Namespace of the title + * @return string DBkey (no namespace prefix) */ - protected function getDB() { - if ( is_null( $this->mDb ) ) { - $this->mDb = $this->getQuery()->getDB(); + public function titlePartToKey( $titlePart, $namespace = NS_MAIN ) { + $t = Title::makeTitleSafe( $namespace, $titlePart . 'x' ); + if ( !$t ) { + $this->dieUsageMsg( array( 'invalidtitle', $titlePart ) ); } - return $this->mDb; + if ( $namespace != $t->getNamespace() || $t->isExternal() ) { + // This can happen in two cases. First, if you call titlePartToKey with a title part + // that looks like a namespace, but with $defaultNamespace = NS_MAIN. It would be very + // difficult to handle such a case. Such cases cannot exist and are therefore treated + // as invalid user input. The second case is when somebody specifies a title interwiki + // prefix. + $this->dieUsageMsg( array( 'invalidtitle', $titlePart ) ); + } + + return substr( $t->getDbKey(), 0, -1 ); } /** - * Selects the query database connection with the given name. - * See ApiQuery::getNamedDB() for more information - * @param string $name Name to assign to the database connection - * @param int $db One of the DB_* constants - * @param array $groups Query groups - * @return DatabaseBase + * Gets the personalised direction parameter description + * + * @param string $p ModulePrefix + * @param string $extraDirText Any extra text to be appended on the description + * @return array */ - public function selectNamedDB( $name, $db, $groups ) { - $this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups ); + public function getDirectionDescription( $p = '', $extraDirText = '' ) { + return array( + "In which direction to enumerate{$extraDirText}", + " newer - List oldest first. Note: {$p}start has to be before {$p}end.", + " older - List newest first (default). Note: {$p}start has to be later than {$p}end.", + ); } /** - * Get the PageSet object to work on - * @return ApiPageSet + * @param string $hash + * @return bool */ - protected function getPageSet() { - return $this->getQuery()->getPageSet(); + public function validateSha1Hash( $hash ) { + return preg_match( '/^[a-f0-9]{40}$/', $hash ); + } + + /** + * @param string $hash + * @return bool + */ + public function validateSha1Base36Hash( $hash ) { + return preg_match( '/^[a-z0-9]{31}$/', $hash ); + } + + /** + * Check whether the current user has permission to view revision-deleted + * fields. + * @return bool + */ + public function userCanSeeRevDel() { + return $this->getUser()->isAllowedAny( + 'deletedhistory', + 'deletedtext', + 'suppressrevision', + 'viewsuppressed' + ); + } + + /**@}*/ + + /************************************************************************//** + * @name Deprecated + * @{ + */ + + /** + * Estimate the row count for the SELECT query that would be run if we + * called select() right now, and check if it's acceptable. + * @deprecated since 1.24 + * @return bool True if acceptable, false otherwise + */ + protected function checkRowCount() { + wfDeprecated( __METHOD__, '1.24' ); + $db = $this->getDB(); + $this->profileDBIn(); + $rowcount = $db->estimateRowCount( + $this->tables, + $this->fields, + $this->where, + __METHOD__, + $this->options + ); + $this->profileDBOut(); + + if ( $rowcount > $this->getConfig()->get( 'APIMaxDBRows' ) ) { + return false; + } + + return true; } /** * Convert a title to a DB key + * @deprecated since 1.24, past uses of this were always incorrect and should + * have used self::titlePartToKey() instead * @param string $title Page title with spaces * @return string Page title with underscores */ public function titleToKey( $title ) { + wfDeprecated( __METHOD__, '1.24' ); // Don't throw an error if we got an empty string if ( trim( $title ) == '' ) { return ''; @@ -419,15 +612,18 @@ abstract class ApiQueryBase extends ApiBase { if ( !$t ) { $this->dieUsageMsg( array( 'invalidtitle', $title ) ); } + return $t->getPrefixedDBkey(); } /** * The inverse of titleToKey() + * @deprecated since 1.24, unused and probably never needed * @param string $key Page title with underscores * @return string Page title with spaces */ public function keyToTitle( $key ) { + wfDeprecated( __METHOD__, '1.24' ); // Don't throw an error if we got an empty string if ( trim( $key ) == '' ) { return ''; @@ -437,124 +633,22 @@ abstract class ApiQueryBase extends ApiBase { if ( !$t ) { $this->dieUsageMsg( array( 'invalidtitle', $key ) ); } - return $t->getPrefixedText(); - } - /** - * An alternative to titleToKey() that doesn't trim trailing spaces - * @param string $titlePart Title part with spaces - * @return string Title part with underscores - */ - public function titlePartToKey( $titlePart ) { - return substr( $this->titleToKey( $titlePart . 'x' ), 0, - 1 ); + return $t->getPrefixedText(); } /** - * An alternative to keyToTitle() that doesn't trim trailing spaces - * @param string $keyPart Key part with spaces + * Inverse of titlePartToKey() + * @deprecated since 1.24, unused and probably never needed + * @param string $keyPart DBkey, with prefix * @return string Key part with underscores */ public function keyPartToTitle( $keyPart ) { - return substr( $this->keyToTitle( $keyPart . 'x' ), 0, - 1 ); + wfDeprecated( __METHOD__, '1.24' ); + return substr( $this->keyToTitle( $keyPart . 'x' ), 0, -1 ); } - /** - * Gets the personalised direction parameter description - * - * @param string $p ModulePrefix - * @param string $extraDirText Any extra text to be appended on the description - * @return array - */ - public function getDirectionDescription( $p = '', $extraDirText = '' ) { - return array( - "In which direction to enumerate{$extraDirText}", - " newer - List oldest first. Note: {$p}start has to be before {$p}end.", - " older - List newest first (default). Note: {$p}start has to be later than {$p}end.", - ); - } - - /** - * @param $query String - * @param $protocol String - * @return null|string - */ - public function prepareUrlQuerySearchString( $query = null, $protocol = null ) { - $db = $this->getDb(); - if ( !is_null( $query ) || $query != '' ) { - if ( is_null( $protocol ) ) { - $protocol = 'http://'; - } - - $likeQuery = LinkFilter::makeLikeArray( $query, $protocol ); - if ( !$likeQuery ) { - $this->dieUsage( 'Invalid query', 'bad_query' ); - } - - $likeQuery = LinkFilter::keepOneWildcard( $likeQuery ); - return 'el_index ' . $db->buildLike( $likeQuery ); - } elseif ( !is_null( $protocol ) ) { - return 'el_index ' . $db->buildLike( "$protocol", $db->anyString() ); - } - - return null; - } - - /** - * Filters hidden users (where the user doesn't have the right to view them) - * Also adds relevant block information - * - * @param bool $showBlockInfo - * @return void - */ - public function showHiddenUsersAddBlockInfo( $showBlockInfo ) { - $userCanViewHiddenUsers = $this->getUser()->isAllowed( 'hideuser' ); - - if ( $showBlockInfo || !$userCanViewHiddenUsers ) { - $this->addTables( 'ipblocks' ); - $this->addJoinConds( array( - 'ipblocks' => array( 'LEFT JOIN', 'ipb_user=user_id' ), - ) ); - - $this->addFields( 'ipb_deleted' ); - - if ( $showBlockInfo ) { - $this->addFields( array( 'ipb_id', 'ipb_by', 'ipb_by_text', 'ipb_reason', 'ipb_expiry' ) ); - } - - // Don't show hidden names - if ( !$userCanViewHiddenUsers ) { - $this->addWhere( 'ipb_deleted = 0 OR ipb_deleted IS NULL' ); - } - } - } - - /** - * @param $hash string - * @return bool - */ - public function validateSha1Hash( $hash ) { - return preg_match( '/^[a-f0-9]{40}$/', $hash ); - } - - /** - * @param $hash string - * @return bool - */ - public function validateSha1Base36Hash( $hash ) { - return preg_match( '/^[a-z0-9]{31}$/', $hash ); - } - - /** - * @return array - */ - public function getPossibleErrors() { - $errors = parent::getPossibleErrors(); - $errors = array_merge( $errors, array( - array( 'invalidtitle', 'title' ), - array( 'invalidtitle', 'key' ), - ) ); - return $errors; - } + /**@}*/ } /** @@ -568,7 +662,7 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { * Switch this module to generator mode. By default, generator mode is * switched off and the module acts like a normal query module. * @since 1.21 requires pageset parameter - * @param $generatorPageSet ApiPageSet object that the module will get + * @param ApiPageSet $generatorPageSet ApiPageSet object that the module will get * by calling getPageSet() when in generator mode. */ public function setGeneratorMode( ApiPageSet $generatorPageSet ) { @@ -587,11 +681,12 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { if ( $this->mGeneratorPageSet !== null ) { return $this->mGeneratorPageSet; } + return parent::getPageSet(); } /** - * Overrides base class to prepend 'g' to every generator parameter + * Overrides ApiBase to prepend 'g' to every generator parameter * @param string $paramName Parameter name * @return string Prefixed parameter name */ @@ -604,24 +699,21 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { } /** - * Overrides base in case of generator & smart continue to - * notify ApiQueryMain instead of adding them to the result right away. + * Overridden to set the generator param if in generator mode * @param string $paramName Parameter name - * @param string $paramValue Parameter value + * @param string|array $paramValue Parameter value */ protected function setContinueEnumParameter( $paramName, $paramValue ) { - // If this is a generator and query->setGeneratorContinue() returns false, treat as before - if ( $this->mGeneratorPageSet === null - || !$this->getQuery()->setGeneratorContinue( $this, $paramName, $paramValue ) - ) { + if ( $this->mGeneratorPageSet !== null ) { + $this->getResult()->setGeneratorContinueParam( $this, $paramName, $paramValue ); + } else { parent::setContinueEnumParameter( $paramName, $paramValue ); } } /** * Execute this module as a generator - * @param $resultPageSet ApiPageSet: All output should be appended to - * this object + * @param ApiPageSet $resultPageSet All output should be appended to this object */ abstract public function executeGenerator( $resultPageSet ); } diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index e3c27f5e..33b25fd9 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -32,17 +32,18 @@ class ApiQueryBlocks extends ApiQueryBase { /** - * @var Array + * @var array */ protected $usernames; - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'bk' ); } public function execute() { global $wgContLang; + $db = $this->getDB(); $params = $this->extractRequestParams(); $this->requireMaxOneParameter( $params, 'users', 'ip' ); @@ -61,9 +62,8 @@ class ApiQueryBlocks extends ApiQueryBase { $result = $this->getResult(); $this->addTables( 'ipblocks' ); - $this->addFields( 'ipb_auto' ); + $this->addFields( array( 'ipb_auto', 'ipb_id' ) ); - $this->addFieldsIf( 'ipb_id', $fld_id ); $this->addFieldsIf( array( 'ipb_address', 'ipb_user' ), $fld_user || $fld_userid ); $this->addFieldsIf( 'ipb_by_text', $fld_by ); $this->addFieldsIf( 'ipb_by', $fld_byid ); @@ -72,13 +72,31 @@ class ApiQueryBlocks extends ApiQueryBase { $this->addFieldsIf( 'ipb_reason', $fld_reason ); $this->addFieldsIf( array( 'ipb_range_start', 'ipb_range_end' ), $fld_range ); $this->addFieldsIf( array( 'ipb_anon_only', 'ipb_create_account', 'ipb_enable_autoblock', - 'ipb_block_email', 'ipb_deleted', 'ipb_allow_usertalk' ), - $fld_flags ); + 'ipb_block_email', 'ipb_deleted', 'ipb_allow_usertalk' ), + $fld_flags ); $this->addOption( 'LIMIT', $params['limit'] + 1 ); - $this->addTimestampWhereRange( 'ipb_timestamp', $params['dir'], $params['start'], $params['end'] ); - - $db = $this->getDB(); + $this->addTimestampWhereRange( + 'ipb_timestamp', + $params['dir'], + $params['start'], + $params['end'] + ); + // Include in ORDER BY for uniqueness + $this->addWhereRange( 'ipb_id', $params['dir'], null, null ); + + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 2 ); + $op = ( $params['dir'] == 'newer' ? '>' : '<' ); + $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) ); + $continueId = (int)$cont[1]; + $this->dieContinueUsageIf( $continueId != $cont[1] ); + $this->addWhere( "ipb_timestamp $op $continueTimestamp OR " . + "(ipb_timestamp = $continueTimestamp AND " . + "ipb_id $op= $continueId)" + ); + } if ( isset( $params['ids'] ) ) { $this->addWhereFld( 'ipb_id', $params['ids'] ); @@ -91,14 +109,14 @@ class ApiQueryBlocks extends ApiQueryBase { $this->addWhereFld( 'ipb_auto', 0 ); } if ( isset( $params['ip'] ) ) { - global $wgBlockCIDRLimit; + $blockCIDRLimit = $this->getConfig()->get( 'BlockCIDRLimit' ); if ( IP::isIPv4( $params['ip'] ) ) { $type = 'IPv4'; - $cidrLimit = $wgBlockCIDRLimit['IPv4']; + $cidrLimit = $blockCIDRLimit['IPv4']; $prefixLen = 0; } elseif ( IP::isIPv6( $params['ip'] ) ) { $type = 'IPv6'; - $cidrLimit = $wgBlockCIDRLimit['IPv6']; + $cidrLimit = $blockCIDRLimit['IPv6']; $prefixLen = 3; // IP::toHex output is prefixed with "v6-" } else { $this->dieUsage( 'IP parameter is not valid', 'param_ip' ); @@ -107,7 +125,10 @@ class ApiQueryBlocks extends ApiQueryBase { # Check range validity, if it's a CIDR list( $ip, $range ) = IP::parseCIDR( $params['ip'] ); if ( $ip !== false && $range !== false && $range < $cidrLimit ) { - $this->dieUsage( "$type CIDR ranges broader than /$cidrLimit are not accepted", 'cidrtoobroad' ); + $this->dieUsage( + "$type CIDR ranges broader than /$cidrLimit are not accepted", + 'cidrtoobroad' + ); } # Let IP::parseRange handle calculating $upper, instead of duplicating the logic here. @@ -134,9 +155,9 @@ class ApiQueryBlocks extends ApiQueryBase { /* Check for conflicting parameters. */ if ( ( isset( $show['account'] ) && isset( $show['!account'] ) ) - || ( isset( $show['ip'] ) && isset( $show['!ip'] ) ) - || ( isset( $show['range'] ) && isset( $show['!range'] ) ) - || ( isset( $show['temp'] ) && isset( $show['!temp'] ) ) + || ( isset( $show['ip'] ) && isset( $show['!ip'] ) ) + || ( isset( $show['range'] ) && isset( $show['!range'] ) ) + || ( isset( $show['temp'] ) && isset( $show['!temp'] ) ) ) { $this->dieUsageMsg( 'show' ); } @@ -145,8 +166,10 @@ class ApiQueryBlocks extends ApiQueryBase { $this->addWhereIf( 'ipb_user != 0', isset( $show['account'] ) ); $this->addWhereIf( 'ipb_user != 0 OR ipb_range_end > ipb_range_start', isset( $show['!ip'] ) ); $this->addWhereIf( 'ipb_user = 0 AND ipb_range_end = ipb_range_start', isset( $show['ip'] ) ); - $this->addWhereIf( 'ipb_expiry = ' . $db->addQuotes( $db->getInfinity() ), isset( $show['!temp'] ) ); - $this->addWhereIf( 'ipb_expiry != ' . $db->addQuotes( $db->getInfinity() ), isset( $show['temp'] ) ); + $this->addWhereIf( 'ipb_expiry = ' . + $db->addQuotes( $db->getInfinity() ), isset( $show['!temp'] ) ); + $this->addWhereIf( 'ipb_expiry != ' . + $db->addQuotes( $db->getInfinity() ), isset( $show['temp'] ) ); $this->addWhereIf( 'ipb_range_end = ipb_range_start', isset( $show['!range'] ) ); $this->addWhereIf( 'ipb_range_end > ipb_range_start', isset( $show['range'] ) ); } @@ -166,7 +189,7 @@ class ApiQueryBlocks extends ApiQueryBase { foreach ( $res as $row ) { if ( ++$count > $params['limit'] ) { // We've had enough - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ipb_timestamp ) ); + $this->setContinueEnumParameter( 'continue', "$row->ipb_timestamp|$row->ipb_id" ); break; } $block = array(); @@ -224,7 +247,7 @@ class ApiQueryBlocks extends ApiQueryBase { } $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $block ); if ( !$fit ) { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ipb_timestamp ) ); + $this->setContinueEnumParameter( 'continue', "$row->ipb_timestamp|$row->ipb_id" ); break; } } @@ -303,12 +326,14 @@ class ApiQueryBlocks extends ApiQueryBase { ), ApiBase::PARAM_ISMULTI => true ), + 'continue' => null, ); } public function getParamDescription() { - global $wgBlockCIDRLimit; + $blockCIDRLimit = $this->getConfig()->get( 'BlockCIDRLimit' ); $p = $this->getModulePrefix(); + return array( 'start' => 'The timestamp to start enumerating from', 'end' => 'The timestamp to stop enumerating at', @@ -318,7 +343,7 @@ class ApiQueryBlocks extends ApiQueryBase { 'ip' => array( 'Get all blocks applying to this IP or CIDR range, including range blocks.', "Cannot be used together with bkusers. CIDR ranges broader than " . - "IPv4/{$wgBlockCIDRLimit['IPv4']} or IPv6/{$wgBlockCIDRLimit['IPv6']} " . + "IPv4/{$blockCIDRLimit['IPv4']} or IPv6/{$blockCIDRLimit['IPv6']} " . "are not accepted" ), 'limit' => 'The maximum amount of blocks to list', @@ -339,86 +364,12 @@ class ApiQueryBlocks extends ApiQueryBase { 'Show only items that meet this criteria.', "For example, to see only indefinite blocks on IPs, set {$p}show=ip|!temp" ), - ); - } - - public function getResultProperties() { - return array( - 'id' => array( - 'id' => 'integer' - ), - 'user' => array( - 'user' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'userid' => array( - 'userid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'by' => array( - 'by' => 'string' - ), - 'byid' => array( - 'byid' => 'integer' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'expiry' => array( - 'expiry' => 'timestamp' - ), - 'reason' => array( - 'reason' => 'string' - ), - 'range' => array( - 'rangestart' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'rangeend' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'flags' => array( - 'automatic' => 'boolean', - 'anononly' => 'boolean', - 'nocreate' => 'boolean', - 'autoblock' => 'boolean', - 'noemail' => 'boolean', - 'hidden' => 'boolean', - 'allowusertalk' => 'boolean' - ) + 'continue' => 'When more results are available, use this to continue', ); } public function getDescription() { - return 'List all blocked users and IP addresses'; - } - - public function getPossibleErrors() { - global $wgBlockCIDRLimit; - return array_merge( parent::getPossibleErrors(), - $this->getRequireOnlyOneParameterErrorMessages( array( 'users', 'ip' ) ), - array( - array( - 'code' => 'cidrtoobroad', - 'info' => "IPv4 CIDR ranges broader than /{$wgBlockCIDRLimit['IPv4']} are not accepted" - ), - array( - 'code' => 'cidrtoobroad', - 'info' => "IPv6 CIDR ranges broader than /{$wgBlockCIDRLimit['IPv6']} are not accepted" - ), - array( 'code' => 'param_ip', 'info' => 'IP parameter is not valid' ), - array( 'code' => 'param_user', 'info' => 'User parameter may not be empty' ), - array( 'code' => 'param_user', 'info' => 'User name user is not valid' ), - array( 'show' ), - ) - ); + return 'List all blocked users and IP addresses.'; } public function getExamples() { diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index 5d714f57..1926dd09 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -31,7 +31,7 @@ */ class ApiQueryCategories extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'cl' ); } @@ -48,7 +48,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet */ private function run( $resultPageSet = null ) { if ( $this->getPageSet()->getGoodTitleCount() == 0 ) { @@ -98,8 +98,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { if ( isset( $show['hidden'] ) && isset( $show['!hidden'] ) ) { $this->dieUsageMsg( 'show' ); } - if ( isset( $show['hidden'] ) || isset( $show['!hidden'] ) || isset( $prop['hidden'] ) ) - { + if ( isset( $show['hidden'] ) || isset( $show['!hidden'] ) || isset( $prop['hidden'] ) ) { $this->addOption( 'STRAIGHT_JOIN' ); $this->addTables( array( 'page', 'page_props' ) ); $this->addFieldsIf( 'pp_propname', isset( $prop['hidden'] ) ); @@ -126,9 +125,9 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $this->addOption( 'ORDER BY', 'cl_to' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'cl_from' . $sort, - 'cl_to' . $sort - )); + 'cl_from' . $sort, + 'cl_to' . $sort + ) ); } $res = $this->select( __METHOD__ ); @@ -221,51 +220,30 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { return array( 'prop' => array( 'Which additional properties to get for each category', - ' sortkey - Adds the sortkey (hexadecimal string) and sortkey prefix (human-readable part) for the category', + ' sortkey - Adds the sortkey (hexadecimal string) and sortkey prefix', + ' (human-readable part) for the category', ' timestamp - Adds timestamp of when the category was added', ' hidden - Tags categories that are hidden with __HIDDENCAT__', ), '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', + 'categories' => 'Only list these categories. Useful for checking ' . + 'whether a certain page is in a certain category', 'dir' => 'The direction in which to list', ); } - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'sortkey' => array( - 'sortkey' => 'string', - 'sortkeyprefix' => 'string' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'hidden' => array( - 'hidden' => 'boolean' - ) - ); - } - public function getDescription() { - return 'List all categories the page(s) belong to'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'show' ), - ) ); + return 'List all categories the page(s) belong to.'; } public function getExamples() { return array( - 'api.php?action=query&prop=categories&titles=Albert%20Einstein' => 'Get a list of categories [[Albert Einstein]] belongs to', - 'api.php?action=query&generator=categories&titles=Albert%20Einstein&prop=info' => 'Get information about all categories used in the [[Albert Einstein]]', + 'api.php?action=query&prop=categories&titles=Albert%20Einstein' + => 'Get a list of categories [[Albert Einstein]] belongs to', + 'api.php?action=query&generator=categories&titles=Albert%20Einstein&prop=info' + => 'Get information about all categories used in the [[Albert Einstein]]', ); } diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php index a889272e..6e9f33c1 100644 --- a/includes/api/ApiQueryCategoryInfo.php +++ b/includes/api/ApiQueryCategoryInfo.php @@ -32,7 +32,7 @@ */ class ApiQueryCategoryInfo extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'ci' ); } @@ -45,7 +45,7 @@ class ApiQueryCategoryInfo extends ApiQueryBase { $categories = $alltitles[NS_CATEGORY]; $titles = $this->getPageSet()->getGoodTitles() + - $this->getPageSet()->getMissingTitles(); + $this->getPageSet()->getMissingTitles(); $cattitles = array(); foreach ( $categories as $c ) { /** @var $t Title */ @@ -63,7 +63,13 @@ class ApiQueryCategoryInfo extends ApiQueryBase { 'pp_propname' => 'hiddencat' ) ), ) ); - $this->addFields( array( 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'cat_hidden' => 'pp_propname' ) ); + $this->addFields( array( + 'cat_title', + 'cat_pages', + 'cat_subcats', + 'cat_files', + 'cat_hidden' => 'pp_propname' + ) ); $this->addWhere( array( 'cat_title' => $cattitles ) ); if ( !is_null( $params['continue'] ) ) { @@ -108,36 +114,8 @@ class ApiQueryCategoryInfo extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - ApiBase::PROP_LIST => false, - '' => array( - 'size' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => false - ), - 'pages' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => false - ), - 'files' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => false - ), - 'subcats' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => false - ), - 'hidden' => array( - ApiBase::PROP_TYPE => 'boolean', - ApiBase::PROP_NULLABLE => false - ) - ) - ); - } - public function getDescription() { - return 'Returns information about the given categories'; + return 'Returns information about the given categories.'; } public function getExamples() { diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index 704d108a..a88a9cb1 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -31,7 +31,7 @@ */ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'cm' ); } @@ -48,7 +48,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function run( $resultPageSet = null ) { @@ -86,9 +86,8 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { // Scanning large datasets for rare categories sucks, and I already told // how to have efficient subcategory access :-) ~~~~ (oh well, domas) - global $wgMiserMode; $miser_ns = array(); - if ( $wgMiserMode ) { + if ( $this->getConfig()->get( 'MiserMode' ) ) { $miser_ns = $params['namespace']; } else { $this->addWhereFld( 'page_namespace', $params['namespace'] ); @@ -101,6 +100,22 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $dir, $params['start'], $params['end'] ); + // Include in ORDER BY for uniqueness + $this->addWhereRange( 'cl_from', $dir, null, null ); + + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 2 ); + $op = ( $dir === 'newer' ? '>' : '<' ); + $db = $this->getDB(); + $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) ); + $continueFrom = (int)$cont[1]; + $this->dieContinueUsageIf( $continueFrom != $cont[1] ); + $this->addWhere( "cl_timestamp $op $continueTimestamp OR " . + "(cl_timestamp = $continueTimestamp AND " . + "cl_from $op= $continueFrom)" + ); + } $this->addOption( 'USE INDEX', 'cl_timestamp' ); } else { @@ -125,12 +140,22 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $this->addWhereRange( 'cl_sortkey', $dir, null, null ); $this->addWhereRange( 'cl_from', $dir, null, null ); } else { - $startsortkey = $params['startsortkeyprefix'] !== null ? - Collation::singleton()->getSortkey( $params['startsortkeyprefix'] ) : - $params['startsortkey']; - $endsortkey = $params['endsortkeyprefix'] !== null ? - Collation::singleton()->getSortkey( $params['endsortkeyprefix'] ) : - $params['endsortkey']; + if ( $params['startsortkeyprefix'] !== null ) { + $startsortkey = Collation::singleton()->getSortkey( $params['startsortkeyprefix'] ); + } elseif ( $params['starthexsortkey'] !== null ) { + $startsortkey = pack( 'H*', $params['starthexsortkey'] ); + } else { + $this->logFeatureUsage( 'list=categorymembers&cmstartsortkey' ); + $startsortkey = $params['startsortkey']; + } + if ( $params['endsortkeyprefix'] !== null ) { + $endsortkey = Collation::singleton()->getSortkey( $params['endsortkeyprefix'] ); + } elseif ( $params['endhexsortkey'] !== null ) { + $endsortkey = pack( 'H*', $params['endhexsortkey'] ); + } else { + $this->logFeatureUsage( 'list=categorymembers&cmendsortkey' ); + $endsortkey = $params['endsortkey']; + } // The below produces ORDER BY cl_sortkey, cl_from, possibly with DESC added to each of them $this->addWhereRange( 'cl_sortkey', @@ -180,11 +205,13 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $result = $this->getResult(); $count = 0; foreach ( $rows as $row ) { - if ( ++ $count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... - // TODO: Security issue - if the user has no right to view next title, it will still be shown + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... + // @todo Security issue - if the user has no right to view next + // title, it will still be shown if ( $params['sort'] == 'timestamp' ) { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->cl_timestamp ) ); + $this->setContinueEnumParameter( 'continue', "$row->cl_timestamp|$row->cl_from" ); } else { $sortkey = bin2hex( $row->cl_sortkey ); $this->setContinueEnumParameter( 'continue', @@ -224,10 +251,10 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->cl_timestamp ); } $fit = $result->addValue( array( 'query', $this->getModuleName() ), - null, $vals ); + null, $vals ); if ( !$fit ) { if ( $params['sort'] == 'timestamp' ) { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->cl_timestamp ) ); + $this->setContinueEnumParameter( 'continue', "$row->cl_timestamp|$row->cl_from" ); } else { $sortkey = bin2hex( $row->cl_sortkey ); $this->setContinueEnumParameter( 'continue', @@ -313,25 +340,32 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { 'end' => array( ApiBase::PARAM_TYPE => 'timestamp' ), - 'startsortkey' => null, - 'endsortkey' => null, + 'starthexsortkey' => null, + 'endhexsortkey' => null, 'startsortkeyprefix' => null, 'endsortkeyprefix' => null, + 'startsortkey' => array( + ApiBase::PARAM_DEPRECATED => true, + ), + 'endsortkey' => array( + ApiBase::PARAM_DEPRECATED => true, + ), ); } public function getParamDescription() { - global $wgMiserMode; $p = $this->getModulePrefix(); $desc = array( - 'title' => "Which category to enumerate (required). Must include Category: prefix. Cannot be used together with {$p}pageid", + 'title' => "Which category to enumerate (required). Must include " . + "'Category:' prefix. Cannot be used together with {$p}pageid", 'pageid' => "Page ID of the category to enumerate. Cannot be used together with {$p}title", 'prop' => array( 'What pieces of information to include', ' ids - Adds the page ID', ' title - Adds the title and namespace ID of the page', ' sortkey - Adds the sortkey used for sorting in the category (hexadecimal string)', - ' sortkeyprefix - Adds the sortkey prefix used for sorting in the category (human-readable part of the sortkey)', + ' sortkeyprefix - Adds the sortkey prefix used for sorting in the ' . + 'category (human-readable part of the sortkey)', ' type - Adds the type that the page has been categorised as (page, subcat or file)', ' timestamp - Adds the timestamp of when the page was included', ), @@ -341,15 +375,22 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { 'dir' => 'In which direction to sort', 'start' => "Timestamp to start listing from. Can only be used with {$p}sort=timestamp", 'end' => "Timestamp to end listing at. Can only be used with {$p}sort=timestamp", - 'startsortkey' => "Sortkey to start listing from. Must be given in binary format. Can only be used with {$p}sort=sortkey", - 'endsortkey' => "Sortkey to end listing at. Must be given in binary format. Can only be used with {$p}sort=sortkey", - 'startsortkeyprefix' => "Sortkey prefix to start listing from. Can only be used with {$p}sort=sortkey. Overrides {$p}startsortkey", - 'endsortkeyprefix' => "Sortkey prefix to end listing BEFORE (not at, if this value occurs it will not be included!). Can only be used with {$p}sort=sortkey. Overrides {$p}endsortkey", + 'starthexsortkey' => "Sortkey to start listing from, as returned by prop=sortkey. " . + "Can only be used with {$p}sort=sortkey", + 'endhexsortkey' => "Sortkey to end listing from, as returned by prop=sortkey. " . + "Can only be used with {$p}sort=sortkey", + 'startsortkeyprefix' => "Sortkey prefix to start listing from. Can " . + "only be used with {$p}sort=sortkey. Overrides {$p}starthexsortkey", + 'endsortkeyprefix' => "Sortkey prefix to end listing BEFORE (not at, " . + "if this value occurs it will not be included!). Can only be used with " . + "{$p}sort=sortkey. Overrides {$p}endhexsortkey", + 'startsortkey' => "Use starthexsortkey instead", + 'endsortkey' => "Use endhexsortkey instead", 'continue' => 'For large categories, give the value returned from previous query', 'limit' => 'The maximum number of pages to return.', ); - if ( $wgMiserMode ) { + if ( $this->getConfig()->get( 'MiserMode' ) ) { $desc['namespace'] = array( $desc['namespace'], "NOTE: Due to \$wgMiserMode, using this may result in fewer than \"{$p}limit\" results", @@ -357,56 +398,20 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { "Note that you can use {$p}type=subcat or {$p}type=file instead of {$p}namespace=14 or 6.", ); } - return $desc; - } - public function getResultProperties() { - return array( - 'ids' => array( - 'pageid' => 'integer' - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'sortkey' => array( - 'sortkey' => 'string' - ), - 'sortkeyprefix' => array( - 'sortkeyprefix' => 'string' - ), - 'type' => array( - 'type' => array( - ApiBase::PROP_TYPE => array( - 'page', - 'subcat', - 'file' - ) - ) - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ) - ); + return $desc; } public function getDescription() { - return 'List all pages in a given category'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getTitleOrPageIdErrorMessage(), - array( - array( 'code' => 'invalidcategory', 'info' => 'The category name you entered is not valid' ), - ) - ); + return 'List all pages in a given category.'; } public function getExamples() { return array( - 'api.php?action=query&list=categorymembers&cmtitle=Category:Physics' => 'Get first 10 pages in [[Category:Physics]]', - 'api.php?action=query&generator=categorymembers&gcmtitle=Category:Physics&prop=info' => 'Get page info about first 10 pages in [[Category:Physics]]', + 'api.php?action=query&list=categorymembers&cmtitle=Category:Physics' + => 'Get first 10 pages in [[Category:Physics]]', + 'api.php?action=query&generator=categorymembers&gcmtitle=Category:Physics&prop=info' + => 'Get page info about first 10 pages in [[Category:Physics]]', ); } diff --git a/includes/api/ApiQueryContributors.php b/includes/api/ApiQueryContributors.php new file mode 100644 index 00000000..55ea4702 --- /dev/null +++ b/includes/api/ApiQueryContributors.php @@ -0,0 +1,282 @@ +<?php +/** + * Query the list of contributors to a page + * + * Created on Nov 14, 2013 + * + * Copyright © 2013 Brad Jorsch + * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.23 + */ + +/** + * A query module to show contributors to a page + * + * @ingroup API + * @since 1.23 + */ +class ApiQueryContributors extends ApiQueryBase { + /** We don't want to process too many pages at once (it hits cold + * database pages too heavily), so only do the first MAX_PAGES input pages + * in each API call (leaving the rest for continuation). + */ + const MAX_PAGES = 100; + + public function __construct( ApiQuery $query, $moduleName ) { + // "pc" is short for "page contributors", "co" was already taken by the + // GeoData extension's prop=coordinates. + parent::__construct( $query, $moduleName, 'pc' ); + } + + public function execute() { + $db = $this->getDB(); + $params = $this->extractRequestParams(); + $this->requireMaxOneParameter( $params, 'group', 'excludegroup', 'rights', 'excluderights' ); + + // Only operate on existing pages + $pages = array_keys( $this->getPageSet()->getGoodTitles() ); + + // Filter out already-processed pages + if ( $params['continue'] !== null ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 2 ); + $cont_page = (int)$cont[0]; + $pages = array_filter( $pages, function ( $v ) use ( $cont_page ) { + return $v >= $cont_page; + } ); + } + if ( !count( $pages ) ) { + // Nothing to do + return; + } + + // Apply MAX_PAGES, leaving any over the limit for a continue. + sort( $pages ); + $continuePages = null; + if ( count( $pages ) > self::MAX_PAGES ) { + $continuePages = $pages[self::MAX_PAGES] . '|0'; + $pages = array_slice( $pages, 0, self::MAX_PAGES ); + } + + $result = $this->getResult(); + + // First, count anons + $this->addTables( 'revision' ); + $this->addFields( array( + 'page' => 'rev_page', + 'anons' => 'COUNT(DISTINCT rev_user_text)', + ) ); + $this->addWhereFld( 'rev_page', $pages ); + $this->addWhere( 'rev_user = 0' ); + $this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ); + $this->addOption( 'GROUP BY', 'rev_page' ); + $res = $this->select( __METHOD__ ); + foreach ( $res as $row ) { + $fit = $result->addValue( array( 'query', 'pages', $row->page ), + 'anoncontributors', $row->anons + ); + if ( !$fit ) { + // This not fitting isn't reasonable, so it probably means that + // some other module used up all the space. Just set a dummy + // continue and hope it works next time. + $this->setContinueEnumParameter( 'continue', + $params['continue'] !== null ? $params['continue'] : '0|0' + ); + + return; + } + } + + // Next, add logged-in users + $this->resetQueryParams(); + $this->addTables( 'revision' ); + $this->addFields( array( + 'page' => 'rev_page', + 'user' => 'rev_user', + 'username' => 'MAX(rev_user_text)', // Non-MySQL databases don't like partial group-by + ) ); + $this->addWhereFld( 'rev_page', $pages ); + $this->addWhere( 'rev_user != 0' ); + $this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ); + $this->addOption( 'GROUP BY', 'rev_page, rev_user' ); + $this->addOption( 'LIMIT', $params['limit'] + 1 ); + + // Force a sort order to ensure that properties are grouped by page + // But only if pp_page is not constant in the WHERE clause. + if ( count( $pages ) > 1 ) { + $this->addOption( 'ORDER BY', 'rev_page, rev_user' ); + } else { + $this->addOption( 'ORDER BY', 'rev_user' ); + } + + $limitGroups = array(); + if ( $params['group'] ) { + $excludeGroups = false; + $limitGroups = $params['group']; + } elseif ( $params['excludegroup'] ) { + $excludeGroups = true; + $limitGroups = $params['excludegroup']; + } elseif ( $params['rights'] ) { + $excludeGroups = false; + foreach ( $params['rights'] as $r ) { + $limitGroups = array_merge( $limitGroups, User::getGroupsWithPermission( $r ) ); + } + + // If no group has the rights requested, no need to query + if ( !$limitGroups ) { + if ( $continuePages !== null ) { + // But we still need to continue for the next page's worth + // of anoncontributors + $this->setContinueEnumParameter( 'continue', $continuePages ); + } + + return; + } + } elseif ( $params['excluderights'] ) { + $excludeGroups = true; + foreach ( $params['excluderights'] as $r ) { + $limitGroups = array_merge( $limitGroups, User::getGroupsWithPermission( $r ) ); + } + } + + if ( $limitGroups ) { + $limitGroups = array_unique( $limitGroups ); + $this->addTables( 'user_groups' ); + $this->addJoinConds( array( 'user_groups' => array( + $excludeGroups ? 'LEFT OUTER JOIN' : 'INNER JOIN', + array( 'ug_user=rev_user', 'ug_group' => $limitGroups ) + ) ) ); + $this->addWhereIf( 'ug_user IS NULL', $excludeGroups ); + } + + if ( $params['continue'] !== null ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 2 ); + $cont_page = (int)$cont[0]; + $cont_user = (int)$cont[1]; + $this->addWhere( + "rev_page > $cont_page OR " . + "(rev_page = $cont_page AND " . + "rev_user >= $cont_user)" + ); + } + + $res = $this->select( __METHOD__ ); + $count = 0; + foreach ( $res as $row ) { + if ( ++$count > $params['limit'] ) { + // We've reached the one extra which shows that + // there are additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->user ); + + return; + } + + $fit = $this->addPageSubItem( $row->page, + array( 'userid' => $row->user, 'name' => $row->username ), + 'user' + ); + if ( !$fit ) { + $this->setContinueEnumParameter( 'continue', $row->page . '|' . $row->user ); + + return; + } + } + + if ( $continuePages !== null ) { + $this->setContinueEnumParameter( 'continue', $continuePages ); + } + } + + public function getCacheMode( $params ) { + return 'public'; + } + + public function getAllowedParams() { + $userGroups = User::getAllGroups(); + $userRights = User::getAllRights(); + + return array( + 'group' => array( + ApiBase::PARAM_TYPE => $userGroups, + ApiBase::PARAM_ISMULTI => true, + ), + 'excludegroup' => array( + ApiBase::PARAM_TYPE => $userGroups, + ApiBase::PARAM_ISMULTI => true, + ), + 'rights' => array( + ApiBase::PARAM_TYPE => $userRights, + ApiBase::PARAM_ISMULTI => true, + ), + 'excluderights' => array( + ApiBase::PARAM_TYPE => $userRights, + ApiBase::PARAM_ISMULTI => true, + ), + '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 + ), + 'continue' => null, + ); + } + + public function getParamDescription() { + return array( + 'group' => array( + 'Limit users to given group name(s)', + 'Does not include implicit or auto-promoted groups like *, user, or autoconfirmed' + ), + 'excludegroup' => array( + 'Exclude users in given group name(s)', + 'Does not include implicit or auto-promoted groups like *, user, or autoconfirmed' + ), + 'rights' => array( + 'Limit users to those having given right(s)', + 'Does not include rights granted by implicit or auto-promoted groups ' . + 'like *, user, or autoconfirmed' + ), + 'excluderights' => array( + 'Limit users to those not having given right(s)', + 'Does not include rights granted by implicit or auto-promoted groups ' . + 'like *, user, or autoconfirmed' + ), + 'limit' => 'How many contributors to return', + 'continue' => 'When more results are available, use this to continue', + ); + } + + public function getDescription() { + return 'Get the list of logged-in contributors and ' . + 'the count of anonymous contributors to a page.'; + } + + public function getExamples() { + return array( + 'api.php?action=query&prop=contributors&titles=Main_Page', + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Properties#contributors_.2F_pc'; + } +} diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index 82733133..9042696b 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -31,7 +31,7 @@ */ class ApiQueryDeletedrevs extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'dr' ); } @@ -39,7 +39,10 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $user = $this->getUser(); // Before doing anything at all, let's check permissions if ( !$user->isAllowed( 'deletedhistory' ) ) { - $this->dieUsage( 'You don\'t have permission to view deleted revision information', 'permissiondenied' ); + $this->dieUsage( + 'You don\'t have permission to view deleted revision information', + 'permissiondenied' + ); } $db = $this->getDB(); @@ -56,12 +59,25 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $fld_sha1 = isset( $prop['sha1'] ); $fld_content = isset( $prop['content'] ); $fld_token = isset( $prop['token'] ); + $fld_tags = isset( $prop['tags'] ); + + if ( isset( $prop['token'] ) ) { + $p = $this->getModulePrefix(); + $this->setWarning( + "{$p}prop=token has been deprecated. Please use action=query&meta=tokens instead." + ); + } // If we're in JSON callback mode, no tokens can be obtained if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) ) { $fld_token = false; } + // If user can't undelete, no tokens + if ( !$user->isAllowed( 'undelete' ) ) { + $fld_token = false; + } + $result = $this->getResult(); $pageSet = $this->getPageSet(); $titles = $pageSet->getTitles(); @@ -97,8 +113,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { } $this->addTables( 'archive' ); - $this->addWhere( 'ar_deleted = 0' ); - $this->addFields( array( 'ar_title', 'ar_namespace', 'ar_timestamp' ) ); + $this->addFields( array( 'ar_title', 'ar_namespace', 'ar_timestamp', 'ar_deleted', 'ar_id' ) ); $this->addFieldsIf( 'ar_parent_id', $fld_parentid ); $this->addFieldsIf( 'ar_rev_id', $fld_revid ); @@ -109,14 +124,41 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $this->addFieldsIf( 'ar_len', $fld_len ); $this->addFieldsIf( 'ar_sha1', $fld_sha1 ); + if ( $fld_tags ) { + $this->addTables( 'tag_summary' ); + $this->addJoinConds( + array( 'tag_summary' => array( 'LEFT JOIN', array( 'ar_rev_id=ts_rev_id' ) ) ) + ); + $this->addFields( 'ts_tags' ); + } + + if ( !is_null( $params['tag'] ) ) { + $this->addTables( 'change_tag' ); + $this->addJoinConds( + array( 'change_tag' => array( 'INNER JOIN', array( 'ar_rev_id=ct_rev_id' ) ) ) + ); + $this->addWhereFld( 'ct_tag', $params['tag'] ); + } + if ( $fld_content ) { + // Modern MediaWiki has the content for deleted revs in the 'text' + // table using fields old_text and old_flags. But revisions deleted + // pre-1.5 store the content in the 'archive' table directly using + // fields ar_text and ar_flags, and no corresponding 'text' row. So + // we have to LEFT JOIN and fetch all four fields, plus ar_text_id + // to be able to tell the difference. $this->addTables( 'text' ); - $this->addFields( array( 'ar_text', 'ar_text_id', 'old_text', 'old_flags' ) ); - $this->addWhere( 'ar_text_id = old_id' ); + $this->addJoinConds( + array( 'text' => array( 'LEFT JOIN', array( 'ar_text_id=old_id' ) ) ) + ); + $this->addFields( array( 'ar_text', 'ar_flags', 'ar_text_id', 'old_text', 'old_flags' ) ); // This also means stricter restrictions - if ( !$user->isAllowed( 'undelete' ) ) { - $this->dieUsage( 'You don\'t have permission to view deleted revision content', 'permissiondenied' ); + if ( !$user->isAllowedAny( 'undelete', 'deletedtext' ) ) { + $this->dieUsage( + 'You don\'t have permission to view deleted revision content', + 'permissiondenied' + ); } } // Check limits @@ -147,12 +189,18 @@ class ApiQueryDeletedrevs extends ApiQueryBase { } elseif ( $mode == 'all' ) { $this->addWhereFld( 'ar_namespace', $params['namespace'] ); - $from = is_null( $params['from'] ) ? null : $this->titleToKey( $params['from'] ); - $to = is_null( $params['to'] ) ? null : $this->titleToKey( $params['to'] ); + $from = $params['from'] === null + ? null + : $this->titlePartToKey( $params['from'], $params['namespace'] ); + $to = $params['to'] === null + ? null + : $this->titlePartToKey( $params['to'], $params['namespace'] ); $this->addWhereRange( 'ar_title', $dir, $from, $to ); if ( isset( $params['prefix'] ) ) { - $this->addWhere( 'ar_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); + $this->addWhere( 'ar_title' . $db->buildLike( + $this->titlePartToKey( $params['prefix'], $params['namespace'] ), + $db->anyString() ) ); } } @@ -163,32 +211,67 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $db->addQuotes( $params['excludeuser'] ) ); } - if ( !is_null( $params['continue'] ) && ( $mode == 'all' || $mode == 'revs' ) ) { + if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) { + // Paranoia: avoid brute force searches (bug 17342) + // (shouldn't be able to get here without 'deletedhistory', but + // check it again just in case) + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $bitmask = Revision::DELETED_USER; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + } else { + $bitmask = 0; + } + if ( $bitmask ) { + $this->addWhere( $db->bitAnd( 'ar_deleted', $bitmask ) . " != $bitmask" ); + } + } + + if ( !is_null( $params['continue'] ) ) { $cont = explode( '|', $params['continue'] ); - $this->dieContinueUsageIf( count( $cont ) != 3 ); - $ns = intval( $cont[0] ); - $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] ); - $title = $db->addQuotes( $cont[1] ); - $ts = $db->addQuotes( $db->timestamp( $cont[2] ) ); $op = ( $dir == 'newer' ? '>' : '<' ); - $this->addWhere( "ar_namespace $op $ns OR " . + if ( $mode == 'all' || $mode == 'revs' ) { + $this->dieContinueUsageIf( count( $cont ) != 4 ); + $ns = intval( $cont[0] ); + $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] ); + $title = $db->addQuotes( $cont[1] ); + $ts = $db->addQuotes( $db->timestamp( $cont[2] ) ); + $ar_id = (int)$cont[3]; + $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[3] ); + $this->addWhere( "ar_namespace $op $ns OR " . "(ar_namespace = $ns AND " . "(ar_title $op $title OR " . "(ar_title = $title AND " . - "ar_timestamp $op= $ts)))" ); + "(ar_timestamp $op $ts OR " . + "(ar_timestamp = $ts AND " . + "ar_id $op= $ar_id)))))" ); + } else { + $this->dieContinueUsageIf( count( $cont ) != 2 ); + $ts = $db->addQuotes( $db->timestamp( $cont[0] ) ); + $ar_id = (int)$cont[1]; + $this->dieContinueUsageIf( strval( $ar_id ) !== $cont[1] ); + $this->addWhere( "ar_timestamp $op $ts OR " . + "(ar_timestamp = $ts AND " . + "ar_id $op= $ar_id)" ); + } } $this->addOption( 'LIMIT', $limit + 1 ); - $this->addOption( 'USE INDEX', array( 'archive' => ( $mode == 'user' ? 'usertext_timestamp' : 'name_title_timestamp' ) ) ); + $this->addOption( + 'USE INDEX', + array( 'archive' => ( $mode == 'user' ? 'usertext_timestamp' : 'name_title_timestamp' ) ) + ); if ( $mode == 'all' ) { if ( $params['unique'] ) { + // @todo Does this work on non-MySQL? $this->addOption( 'GROUP BY', 'ar_title' ); } else { $sort = ( $dir == 'newer' ? '' : ' DESC' ); $this->addOption( 'ORDER BY', array( 'ar_title' . $sort, - 'ar_timestamp' . $sort - )); + 'ar_timestamp' . $sort, + 'ar_id' . $sort, + ) ); } } else { if ( $mode == 'revs' ) { @@ -197,6 +280,8 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $this->addWhereRange( 'ar_title', $dir, null, null ); } $this->addTimestampWhereRange( 'ar_timestamp', $dir, $params['start'], $params['end'] ); + // Include in ORDER BY for uniqueness + $this->addWhereRange( 'ar_id', $dir, null, null ); } $res = $this->select( __METHOD__ ); $pageMap = array(); // Maps ns&title to (fake) pageid @@ -206,15 +291,18 @@ class ApiQueryDeletedrevs extends ApiQueryBase { if ( ++$count > $limit ) { // We've had enough if ( $mode == 'all' || $mode == 'revs' ) { - $this->setContinueEnumParameter( 'continue', intval( $row->ar_namespace ) . '|' . - $row->ar_title . '|' . $row->ar_timestamp ); + $this->setContinueEnumParameter( 'continue', + "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id" + ); } else { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ar_timestamp ) ); + $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" ); } break; } $rev = array(); + $anyHidden = false; + $rev['timestamp'] = wfTimestamp( TS_ISO_8601, $row->ar_timestamp ); if ( $fld_revid ) { $rev['revid'] = intval( $row->ar_rev_id ); @@ -222,21 +310,37 @@ class ApiQueryDeletedrevs extends ApiQueryBase { if ( $fld_parentid && !is_null( $row->ar_parent_id ) ) { $rev['parentid'] = intval( $row->ar_parent_id ); } - if ( $fld_user ) { - $rev['user'] = $row->ar_user_text; - } - if ( $fld_userid ) { - $rev['userid'] = $row->ar_user; - } - if ( $fld_comment ) { - $rev['comment'] = $row->ar_comment; + if ( $fld_user || $fld_userid ) { + if ( $row->ar_deleted & Revision::DELETED_USER ) { + $rev['userhidden'] = ''; + $anyHidden = true; + } + if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_USER, $user ) ) { + if ( $fld_user ) { + $rev['user'] = $row->ar_user_text; + } + if ( $fld_userid ) { + $rev['userid'] = $row->ar_user; + } + } } - $title = Title::makeTitle( $row->ar_namespace, $row->ar_title ); - - if ( $fld_parsedcomment ) { - $rev['parsedcomment'] = Linker::formatComment( $row->ar_comment, $title ); + if ( $fld_comment || $fld_parsedcomment ) { + if ( $row->ar_deleted & Revision::DELETED_COMMENT ) { + $rev['commenthidden'] = ''; + $anyHidden = true; + } + if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_COMMENT, $user ) ) { + if ( $fld_comment ) { + $rev['comment'] = $row->ar_comment; + } + if ( $fld_parsedcomment ) { + $title = Title::makeTitle( $row->ar_namespace, $row->ar_title ); + $rev['parsedcomment'] = Linker::formatComment( $row->ar_comment, $title ); + } + } } + if ( $fld_minor && $row->ar_minor_edit == 1 ) { $rev['minor'] = ''; } @@ -244,14 +348,45 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $rev['len'] = $row->ar_len; } if ( $fld_sha1 ) { - if ( $row->ar_sha1 != '' ) { - $rev['sha1'] = wfBaseConvert( $row->ar_sha1, 36, 16, 40 ); - } else { - $rev['sha1'] = ''; + if ( $row->ar_deleted & Revision::DELETED_TEXT ) { + $rev['sha1hidden'] = ''; + $anyHidden = true; + } + if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_TEXT, $user ) ) { + if ( $row->ar_sha1 != '' ) { + $rev['sha1'] = wfBaseConvert( $row->ar_sha1, 36, 16, 40 ); + } else { + $rev['sha1'] = ''; + } } } if ( $fld_content ) { - ApiResult::setContent( $rev, Revision::getRevisionText( $row ) ); + if ( $row->ar_deleted & Revision::DELETED_TEXT ) { + $rev['texthidden'] = ''; + $anyHidden = true; + } + if ( Revision::userCanBitfield( $row->ar_deleted, Revision::DELETED_TEXT, $user ) ) { + if ( isset( $row->ar_text ) && !$row->ar_text_id ) { + // Pre-1.5 ar_text row (if condition from Revision::newFromArchiveRow) + ApiResult::setContent( $rev, Revision::getRevisionText( $row, 'ar_' ) ); + } else { + ApiResult::setContent( $rev, Revision::getRevisionText( $row ) ); + } + } + } + + if ( $fld_tags ) { + if ( $row->ts_tags ) { + $tags = explode( ',', $row->ts_tags ); + $this->getResult()->setIndexedTagName( $tags, 'tag' ); + $rev['tags'] = $tags; + } else { + $rev['tags'] = array(); + } + } + + if ( $anyHidden && ( $row->ar_deleted & Revision::DELETED_RESTRICTED ) ) { + $rev['suppressed'] = ''; } if ( !isset( $pageMap[$row->ar_namespace][$row->ar_title] ) ) { @@ -259,6 +394,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $pageMap[$row->ar_namespace][$row->ar_title] = $pageID; $a['revisions'] = array( $rev ); $result->setIndexedTagName( $a['revisions'], 'rev' ); + $title = Title::makeTitle( $row->ar_namespace, $row->ar_title ); ApiQueryBase::addTitleInfo( $a, $title ); if ( $fld_token ) { $a['token'] = $token; @@ -272,10 +408,11 @@ class ApiQueryDeletedrevs extends ApiQueryBase { } if ( !$fit ) { if ( $mode == 'all' || $mode == 'revs' ) { - $this->setContinueEnumParameter( 'continue', intval( $row->ar_namespace ) . '|' . - $row->ar_title . '|' . $row->ar_timestamp ); + $this->setContinueEnumParameter( 'continue', + "$row->ar_namespace|$row->ar_title|$row->ar_timestamp|$row->ar_id" + ); } else { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ar_timestamp ) ); + $this->setContinueEnumParameter( 'continue', "$row->ar_timestamp|$row->ar_id" ); } break; } @@ -303,6 +440,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { 'prefix' => null, 'continue' => null, 'unique' => false, + 'tag' => null, 'user' => array( ApiBase::PARAM_TYPE => 'user' ), @@ -333,7 +471,8 @@ class ApiQueryDeletedrevs extends ApiQueryBase { 'len', 'sha1', 'content', - 'token' + 'token', + 'tags' ), ApiBase::PARAM_ISMULTI => true ), @@ -361,57 +500,37 @@ class ApiQueryDeletedrevs extends ApiQueryBase { ' len - Adds the length (bytes) of the revision', ' sha1 - Adds the SHA-1 (base 16) of the revision', ' content - Adds the content of the revision', - ' token - Gives the edit token', + ' token - DEPRECATED! Gives the edit token', + ' tags - Tags for the revision', ), 'namespace' => 'Only list pages in this namespace (3)', 'user' => 'Only list revisions by this user', 'excludeuser' => 'Don\'t list revisions by this user', - 'continue' => 'When more results are available, use this to continue (1, 3)', + 'continue' => 'When more results are available, use this to continue', 'unique' => 'List only one revision for each page (3)', - ); - } - - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'token' => array( - 'token' => 'string' - ) + 'tag' => 'Only list revisions tagged with this tag', ); } public function getDescription() { $p = $this->getModulePrefix(); + return array( 'List deleted revisions.', '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, {$p}user not set)", + ' 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, {$p}user 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', + 'For instance, a parameter marked (1) only applies to mode 1 and is ignored in modes 2 and 3.', ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'permissiondenied', 'info' => 'You don\'t have permission to view deleted revision information' ), - array( 'code' => 'badparams', 'info' => 'user and excludeuser cannot be used together' ), - array( 'code' => 'permissiondenied', 'info' => 'You don\'t have permission to view deleted revision content' ), - array( 'code' => 'badparams', 'info' => "The 'from' parameter cannot be used in modes 1 or 2" ), - array( 'code' => 'badparams', 'info' => "The 'to' parameter cannot be used in modes 1 or 2" ), - array( 'code' => 'badparams', 'info' => "The 'prefix' parameter cannot be used in modes 1 or 2" ), - array( 'code' => 'badparams', 'info' => "The 'start' parameter cannot be used in mode 3" ), - array( 'code' => 'badparams', 'info' => "The 'end' parameter cannot be used in mode 3" ), - ) ); - } - public function getExamples() { return array( - 'api.php?action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&drprop=user|comment|content' + 'api.php?action=query&list=deletedrevs&titles=Main%20Page|Talk:Main%20Page&' . + 'drprop=user|comment|content' => 'List the last deleted revisions of Main Page and Talk:Main Page, with content (mode 1)', 'api.php?action=query&list=deletedrevs&druser=Bob&drlimit=50' => 'List the last 50 deleted contributions by Bob (mode 2)', diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php index 0311fa7f..6d836cd5 100644 --- a/includes/api/ApiQueryDuplicateFiles.php +++ b/includes/api/ApiQueryDuplicateFiles.php @@ -31,7 +31,7 @@ */ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'df' ); } @@ -95,7 +95,8 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { $sha1s[$file->getName()] = $file->getSha1(); } - // find all files with the hashes, result format is: array( hash => array( dup1, dup2 ), hash1 => ... ) + // find all files with the hashes, result format is: + // array( hash => array( dup1, dup2 ), hash1 => ... ) $filesToFindBySha1s = array_unique( array_values( $sha1s ) ); if ( $params['localonly'] ) { $filesBySha1s = RepoGroup::singleton()->getLocalRepo()->findBySha1s( $filesToFindBySha1s ); @@ -187,19 +188,8 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'name' => 'string', - 'user' => 'string', - 'timestamp' => 'timestamp', - 'shared' => 'boolean', - ) - ); - } - public function getDescription() { - return 'List all files that are duplicates of the given file(s) based on hash values'; + return 'List all files that are duplicates of the given file(s) based on hash values.'; } public function getExamples() { diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index 456e87ba..faabb920 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -29,7 +29,7 @@ */ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'eu' ); } @@ -46,7 +46,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function run( $resultPageSet = null ) { @@ -59,9 +59,8 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { $this->addOption( 'USE INDEX', 'el_index' ); $this->addWhere( 'page_id=el_from' ); - global $wgMiserMode; $miser_ns = array(); - if ( $wgMiserMode ) { + if ( $this->getConfig()->get( 'MiserMode' ) ) { $miser_ns = $params['namespace']; } else { $this->addWhereFld( 'page_namespace', $params['namespace'] ); @@ -101,8 +100,9 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { $result = $this->getResult(); $count = 0; foreach ( $res as $row ) { - if ( ++ $count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... $this->setContinueEnumParameter( 'offset', $offset + $limit ); break; } @@ -140,7 +140,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { if ( is_null( $resultPageSet ) ) { $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), - $this->getModulePrefix() ); + $this->getModulePrefix() ); } } @@ -186,6 +186,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { $protocols[] = substr( $p, 0, strpos( $p, ':' ) ); } } + return $protocols; } @@ -207,7 +208,6 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { } public function getParamDescription() { - global $wgMiserMode; $p = $this->getModulePrefix(); $desc = array( 'prop' => array( @@ -221,13 +221,14 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { "Protocol of the URL. If empty and {$p}query set, the protocol is http.", "Leave both this and {$p}query empty to list all external links" ), - 'query' => 'Search string without protocol. See [[Special:LinkSearch]]. Leave empty to list all external links', + 'query' => 'Search string without protocol. See [[Special:LinkSearch]]. ' . + 'Leave empty to list all external links', 'namespace' => 'The page namespace(s) to enumerate.', 'limit' => 'How many pages to return.', 'expandurl' => 'Expand protocol-relative URLs with the canonical protocol', ); - if ( $wgMiserMode ) { + if ( $this->getConfig()->get( 'MiserMode' ) ) { $desc['namespace'] = array( $desc['namespace'], "NOTE: Due to \$wgMiserMode, using this may result in fewer than \"{$p}limit\" results", @@ -238,29 +239,8 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { return $desc; } - public function getResultProperties() { - return array( - 'ids' => array( - 'pageid' => 'integer' - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'url' => array( - 'url' => 'string' - ) - ); - } - public function getDescription() { - return 'Enumerate pages that contain a given URL'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'bad_query', 'info' => 'Invalid query' ), - ) ); + return 'Enumerate pages that contain a given URL.'; } public function getExamples() { diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php index 583ef697..95666354 100644 --- a/includes/api/ApiQueryExternalLinks.php +++ b/includes/api/ApiQueryExternalLinks.php @@ -31,7 +31,7 @@ */ class ApiQueryExternalLinks extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'el' ); } @@ -127,6 +127,7 @@ class ApiQueryExternalLinks extends ApiQueryBase { public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'limit' => 'How many links to return', 'offset' => 'When more results are available, use this to continue', @@ -134,32 +135,20 @@ class ApiQueryExternalLinks extends ApiQueryBase { "Protocol of the URL. If empty and {$p}query set, the protocol is http.", "Leave both this and {$p}query empty to list all external links" ), - 'query' => 'Search string without protocol. Useful for checking whether a certain page contains a certain external url', + 'query' => 'Search string without protocol. Useful for checking ' . + 'whether a certain page contains a certain external url', 'expandurl' => 'Expand protocol-relative URLs with the canonical protocol', ); } - public function getResultProperties() { - return array( - '' => array( - '*' => 'string' - ) - ); - } - public function getDescription() { - return 'Returns all external URLs (not interwikis) from the given page(s)'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'bad_query', 'info' => 'Invalid query' ), - ) ); + return 'Returns all external URLs (not interwikis) from the given page(s).'; } public function getExamples() { return array( - 'api.php?action=query&prop=extlinks&titles=Main%20Page' => 'Get a list of external links on the [[Main Page]]', + 'api.php?action=query&prop=extlinks&titles=Main%20Page' + => 'Get a list of external links on the [[Main Page]]', ); } diff --git a/includes/api/ApiQueryFileRepoInfo.php b/includes/api/ApiQueryFileRepoInfo.php index 3a353533..d1600efe 100644 --- a/includes/api/ApiQueryFileRepoInfo.php +++ b/includes/api/ApiQueryFileRepoInfo.php @@ -29,16 +29,13 @@ */ class ApiQueryFileRepoInfo extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'fri' ); } protected function getInitialisedRepoGroup() { $repoGroup = RepoGroup::singleton(); - - if ( !$repoGroup->reposInitialised ) { - $repoGroup->initialiseRepos(); - } + $repoGroup->initialiseRepos(); return $repoGroup; } @@ -55,7 +52,7 @@ class ApiQueryFileRepoInfo extends ApiQueryBase { $repos[] = array_intersect_key( $repo->getInfo(), $props ); } ); - $repos[] = array_intersect_key( $repoGroup->localRepo->getInfo(), $props ); + $repos[] = array_intersect_key( $repoGroup->getLocalRepo()->getInfo(), $props ); $result = $this->getResult(); $result->setIndexedTagName( $repos, 'repo' ); @@ -86,16 +83,19 @@ class ApiQueryFileRepoInfo extends ApiQueryBase { $props = array_merge( $props, array_keys( $repo->getInfo() ) ); } ); - return array_values( array_unique( array_merge( $props, array_keys( $repoGroup->localRepo->getInfo() ) ) ) ); + return array_values( array_unique( array_merge( + $props, + array_keys( $repoGroup->getLocalRepo()->getInfo() ) + ) ) ); } public function getParamDescription() { - $p = $this->getModulePrefix(); return array( 'prop' => array( 'Which repository properties to get (there may be more available on some wikis):', ' apiurl - URL to the repository API - helpful for getting image info from the host.', - ' name - The key of the repository - used in e.g. $wgForeignFileRepos and imageinfo return values.', + ' name - The key of the repository - used in e.g. ' . + '$wgForeignFileRepos and imageinfo return values.', ' displayname - The human-readable name of the repository wiki.', ' rooturl - Root URL for image paths.', ' local - Whether that repository is the local one or not.', diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php index f53cd386..f047d8d4 100644 --- a/includes/api/ApiQueryFilearchive.php +++ b/includes/api/ApiQueryFilearchive.php @@ -33,7 +33,7 @@ */ class ApiQueryFilearchive extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'fa' ); } @@ -41,7 +41,10 @@ class ApiQueryFilearchive extends ApiQueryBase { $user = $this->getUser(); // Before doing anything at all, let's check permissions if ( !$user->isAllowed( 'deletedhistory' ) ) { - $this->dieUsage( 'You don\'t have permission to view deleted file information', 'permissiondenied' ); + $this->dieUsage( + 'You don\'t have permission to view deleted file information', + 'permissiondenied' + ); } $db = $this->getDB(); @@ -63,9 +66,9 @@ class ApiQueryFilearchive extends ApiQueryBase { $this->addTables( 'filearchive' ); - $this->addFields( array( 'fa_name', 'fa_deleted' ) ); + $this->addFields( ArchivedFile::selectFields() ); + $this->addFields( array( 'fa_id', 'fa_name', 'fa_timestamp', 'fa_deleted' ) ); $this->addFieldsIf( 'fa_sha1', $fld_sha1 ); - $this->addFieldsIf( 'fa_timestamp', $fld_timestamp ); $this->addFieldsIf( array( 'fa_user', 'fa_user_text' ), $fld_user ); $this->addFieldsIf( array( 'fa_height', 'fa_width', 'fa_size' ), $fld_dimensions || $fld_size ); $this->addFieldsIf( 'fa_description', $fld_description ); @@ -77,22 +80,29 @@ class ApiQueryFilearchive extends ApiQueryBase { if ( !is_null( $params['continue'] ) ) { $cont = explode( '|', $params['continue'] ); - $this->dieContinueUsageIf( count( $cont ) != 1 ); + $this->dieContinueUsageIf( count( $cont ) != 3 ); $op = $params['dir'] == 'descending' ? '<' : '>'; $cont_from = $db->addQuotes( $cont[0] ); - $this->addWhere( "fa_name $op= $cont_from" ); + $cont_timestamp = $db->addQuotes( $db->timestamp( $cont[1] ) ); + $cont_id = (int)$cont[2]; + $this->dieContinueUsageIf( $cont[2] !== (string)$cont_id ); + $this->addWhere( "fa_name $op $cont_from OR " . + "(fa_name = $cont_from AND " . + "(fa_timestamp $op $cont_timestamp OR " . + "(fa_timestamp = $cont_timestamp AND " . + "fa_id $op= $cont_id )))" + ); } // Image filters $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' ); - $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); - if ( !is_null( $params['continue'] ) ) { - $from = $params['continue']; - } - $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) ); + $from = ( $params['from'] === null ? null : $this->titlePartToKey( $params['from'], NS_FILE ) ); + $to = ( $params['to'] === null ? null : $this->titlePartToKey( $params['to'], NS_FILE ) ); $this->addWhereRange( 'fa_name', $dir, $from, $to ); if ( isset( $params['prefix'] ) ) { - $this->addWhere( 'fa_name' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); + $this->addWhere( 'fa_name' . $db->buildLike( + $this->titlePartToKey( $params['prefix'], NS_FILE ), + $db->anyString() ) ); } $sha1Set = isset( $params['sha1'] ); @@ -116,20 +126,26 @@ class ApiQueryFilearchive extends ApiQueryBase { } } - if ( !$user->isAllowed( 'suppressrevision' ) ) { - // Filter out revisions that the user is not allowed to see. There - // is no way to indicate that we have skipped stuff because the - // continuation parameter is fa_name - - // Note that this field is unindexed. This should however not be - // a big problem as files with fa_deleted are rare - $this->addWhereFld( 'fa_deleted', 0 ); + // Exclude files this user can't view. + if ( !$user->isAllowed( 'deletedtext' ) ) { + $bitmask = File::DELETED_FILE; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $bitmask = File::DELETED_FILE | File::DELETED_RESTRICTED; + } else { + $bitmask = 0; + } + if ( $bitmask ) { + $this->addWhere( $this->getDB()->bitAnd( 'fa_deleted', $bitmask ) . " != $bitmask" ); } $limit = $params['limit']; $this->addOption( 'LIMIT', $limit + 1 ); $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); - $this->addOption( 'ORDER BY', 'fa_name' . $sort ); + $this->addOption( 'ORDER BY', array( + 'fa_name' . $sort, + 'fa_timestamp' . $sort, + 'fa_id' . $sort, + ) ); $res = $this->select( __METHOD__ ); @@ -137,26 +153,41 @@ class ApiQueryFilearchive extends ApiQueryBase { $result = $this->getResult(); foreach ( $res as $row ) { if ( ++$count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'continue', $row->fa_name ); + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... + $this->setContinueEnumParameter( + 'continue', "$row->fa_name|$row->fa_timestamp|$row->fa_id" + ); break; } $file = array(); + $file['id'] = $row->fa_id; $file['name'] = $row->fa_name; $title = Title::makeTitle( NS_FILE, $row->fa_name ); self::addTitleInfo( $file, $title ); + if ( $fld_description && + Revision::userCanBitfield( $row->fa_deleted, File::DELETED_COMMENT, $user ) + ) { + $file['description'] = $row->fa_description; + if ( isset( $prop['parseddescription'] ) ) { + $file['parseddescription'] = Linker::formatComment( + $row->fa_description, $title ); + } + } + if ( $fld_user && + Revision::userCanBitfield( $row->fa_deleted, File::DELETED_USER, $user ) + ) { + $file['userid'] = $row->fa_user; + $file['user'] = $row->fa_user_text; + } if ( $fld_sha1 ) { $file['sha1'] = wfBaseConvert( $row->fa_sha1, 36, 16, 40 ); } if ( $fld_timestamp ) { $file['timestamp'] = wfTimestamp( TS_ISO_8601, $row->fa_timestamp ); } - if ( $fld_user ) { - $file['userid'] = $row->fa_user; - $file['user'] = $row->fa_user_text; - } if ( $fld_size || $fld_dimensions ) { $file['size'] = $row->fa_size; @@ -168,20 +199,13 @@ class ApiQueryFilearchive extends ApiQueryBase { $file['height'] = $row->fa_height; $file['width'] = $row->fa_width; } - if ( $fld_description ) { - $file['description'] = $row->fa_description; - if ( isset( $prop['parseddescription'] ) ) { - $file['parseddescription'] = Linker::formatComment( - $row->fa_description, $title ); - } - } if ( $fld_mediatype ) { $file['mediatype'] = $row->fa_media_type; } if ( $fld_metadata ) { $file['metadata'] = $row->fa_metadata - ? ApiQueryImageInfo::processMetaData( unserialize( $row->fa_metadata ), $result ) - : null; + ? ApiQueryImageInfo::processMetaData( unserialize( $row->fa_metadata ), $result ) + : null; } if ( $fld_bitdepth ) { $file['bitdepth'] = $row->fa_bits; @@ -209,7 +233,9 @@ class ApiQueryFilearchive extends ApiQueryBase { $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $file ); if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', $row->fa_name ); + $this->setContinueEnumParameter( + 'continue', "$row->fa_name|$row->fa_timestamp|$row->fa_id" + ); break; } } @@ -275,7 +301,8 @@ class ApiQueryFilearchive extends ApiQueryBase { ' sha1 - Adds SHA-1 hash for the image', ' timestamp - Adds timestamp for the uploaded version', ' user - Adds user who uploaded the image version', - ' size - Adds the size of the image in bytes and the height, width and page count (if applicable)', + ' size - Adds the size of the image in bytes and the height, ' . + 'width and page count (if applicable)', ' dimensions - Alias for size', ' description - Adds description the image version', ' parseddescription - Parse the description on the version', @@ -288,81 +315,8 @@ class ApiQueryFilearchive extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'name' => 'string', - 'ns' => 'namespace', - 'title' => 'string', - 'filehidden' => 'boolean', - 'commenthidden' => 'boolean', - 'userhidden' => 'boolean', - 'suppressed' => 'boolean' - ), - 'sha1' => array( - 'sha1' => 'string' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'user' => array( - 'userid' => 'integer', - 'user' => 'string' - ), - 'size' => array( - 'size' => 'integer', - 'pagecount' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'height' => 'integer', - 'width' => 'integer' - ), - 'dimensions' => array( - 'size' => 'integer', - 'pagecount' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'height' => 'integer', - 'width' => 'integer' - ), - 'description' => array( - 'description' => 'string' - ), - 'parseddescription' => array( - 'description' => 'string', - 'parseddescription' => 'string' - ), - 'metadata' => array( - 'metadata' => 'string' - ), - 'bitdepth' => array( - 'bitdepth' => 'integer' - ), - 'mime' => array( - 'mime' => 'string' - ), - 'mediatype' => array( - 'mediatype' => 'string' - ), - 'archivename' => array( - 'archivename' => 'string' - ), - ); - } - public function getDescription() { - return 'Enumerate all deleted files sequentially'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'permissiondenied', 'info' => 'You don\'t have permission to view deleted file information' ), - array( 'code' => 'hashsearchdisabled', 'info' => 'Search by hash disabled in Miser Mode' ), - array( 'code' => 'invalidsha1hash', 'info' => 'The SHA1 hash provided is not valid' ), - array( 'code' => 'invalidsha1base36hash', 'info' => 'The SHA1Base36 hash provided is not valid' ), - ) ); + return 'Enumerate all deleted files sequentially.'; } public function getExamples() { diff --git a/includes/api/ApiQueryIWBacklinks.php b/includes/api/ApiQueryIWBacklinks.php index ebae3e76..b5aa45bf 100644 --- a/includes/api/ApiQueryIWBacklinks.php +++ b/includes/api/ApiQueryIWBacklinks.php @@ -31,7 +31,7 @@ */ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'iwbl' ); } @@ -44,7 +44,7 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ public function run( $resultPageSet = null ) { @@ -92,14 +92,14 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { $this->addOption( 'ORDER BY', array( 'iwl_title' . $sort, 'iwl_from' . $sort - )); + ) ); } } else { $this->addOption( 'ORDER BY', array( 'iwl_prefix' . $sort, 'iwl_title' . $sort, 'iwl_from' . $sort - )); + ) ); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -111,10 +111,15 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { $count = 0; $result = $this->getResult(); foreach ( $res as $row ) { - if ( ++ $count > $params['limit'] ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... - // Continue string preserved in case the redirect query doesn't pass the limit - $this->setContinueEnumParameter( 'continue', "{$row->iwl_prefix}|{$row->iwl_title}|{$row->iwl_from}" ); + if ( ++$count > $params['limit'] ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... + // Continue string preserved in case the redirect query doesn't + // pass the limit + $this->setContinueEnumParameter( + 'continue', + "{$row->iwl_prefix}|{$row->iwl_title}|{$row->iwl_from}" + ); break; } @@ -140,7 +145,10 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $entry ); if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', "{$row->iwl_prefix}|{$row->iwl_title}|{$row->iwl_from}" ); + $this->setContinueEnumParameter( + 'continue', + "{$row->iwl_prefix}|{$row->iwl_title}|{$row->iwl_from}" + ); break; } } @@ -202,37 +210,14 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'pageid' => 'integer', - 'ns' => 'namespace', - 'title' => 'string', - 'redirect' => 'boolean' - ), - 'iwprefix' => array( - 'iwprefix' => 'string' - ), - 'iwtitle' => array( - 'iwtitle' => 'string' - ) - ); - } - public function getDescription() { return array( 'Find all pages that link to the given interwiki link.', 'Can be used to find all links with a prefix, or', 'all links to a title (with a given prefix).', - 'Using neither parameter is effectively "All IW Links"', + 'Using neither parameter is effectively "All IW Links".', ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'missingparam', 'prefix' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=iwbacklinks&iwbltitle=Test&iwblprefix=wikibooks', diff --git a/includes/api/ApiQueryIWLinks.php b/includes/api/ApiQueryIWLinks.php index be539311..a185ee24 100644 --- a/includes/api/ApiQueryIWLinks.php +++ b/includes/api/ApiQueryIWLinks.php @@ -32,7 +32,7 @@ */ class ApiQueryIWLinks extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'iw' ); } @@ -42,11 +42,19 @@ class ApiQueryIWLinks extends ApiQueryBase { } $params = $this->extractRequestParams(); + $prop = array_flip( (array)$params['prop'] ); if ( isset( $params['title'] ) && !isset( $params['prefix'] ) ) { $this->dieUsageMsg( array( 'missingparam', 'prefix' ) ); } + // Handle deprecated param + $this->requireMaxOneParameter( $params, 'url', 'prop' ); + if ( $params['url'] ) { + $this->logFeatureUsage( 'prop=iwlinks&iwurl' ); + $prop = array( 'url' => 1 ); + } + $this->addFields( array( 'iwl_from', 'iwl_prefix', @@ -81,9 +89,9 @@ class ApiQueryIWLinks extends ApiQueryBase { $this->addOption( 'ORDER BY', 'iwl_from' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'iwl_from' . $sort, - 'iwl_title' . $sort - )); + 'iwl_from' . $sort, + 'iwl_title' . $sort + ) ); } } else { // Don't order by iwl_from if it's constant in the WHERE clause @@ -91,10 +99,10 @@ class ApiQueryIWLinks extends ApiQueryBase { $this->addOption( 'ORDER BY', 'iwl_prefix' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'iwl_from' . $sort, - 'iwl_prefix' . $sort, - 'iwl_title' . $sort - )); + 'iwl_from' . $sort, + 'iwl_prefix' . $sort, + 'iwl_title' . $sort + ) ); } } @@ -106,12 +114,15 @@ class ApiQueryIWLinks extends ApiQueryBase { if ( ++$count > $params['limit'] ) { // We've reached the one extra which shows that // there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'continue', "{$row->iwl_from}|{$row->iwl_prefix}|{$row->iwl_title}" ); + $this->setContinueEnumParameter( + 'continue', + "{$row->iwl_from}|{$row->iwl_prefix}|{$row->iwl_title}" + ); break; } $entry = array( 'prefix' => $row->iwl_prefix ); - if ( $params['url'] ) { + if ( isset( $prop['url'] ) ) { $title = Title::newFromText( "{$row->iwl_prefix}:{$row->iwl_title}" ); if ( $title ) { $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); @@ -121,7 +132,10 @@ class ApiQueryIWLinks extends ApiQueryBase { ApiResult::setContent( $entry, $row->iwl_title ); $fit = $this->addPageSubItem( $row->iwl_from, $entry ); if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', "{$row->iwl_from}|{$row->iwl_prefix}|{$row->iwl_title}" ); + $this->setContinueEnumParameter( + 'continue', + "{$row->iwl_from}|{$row->iwl_prefix}|{$row->iwl_title}" + ); break; } } @@ -133,7 +147,16 @@ class ApiQueryIWLinks extends ApiQueryBase { public function getAllowedParams() { return array( - 'url' => false, + 'url' => array( + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_DEPRECATED => true, + ), + 'prop' => array( + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => array( + 'url', + ) + ), 'limit' => array( ApiBase::PARAM_DFLT => 10, ApiBase::PARAM_TYPE => 'limit', @@ -156,7 +179,11 @@ class ApiQueryIWLinks extends ApiQueryBase { public function getParamDescription() { return array( - 'url' => 'Whether to get the full URL', + 'prop' => array( + 'Which additional properties to get for each interlanguage link', + ' url - Adds the full URL', + ), + 'url' => "Whether to get the full URL (Cannot be used with {$this->getModulePrefix()}prop)", 'limit' => 'How many interwiki links to return', 'continue' => 'When more results are available, use this to continue', 'prefix' => 'Prefix for the interwiki', @@ -165,32 +192,14 @@ class ApiQueryIWLinks extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'prefix' => 'string', - 'url' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - '*' => 'string' - ) - ); - } - public function getDescription() { - return 'Returns all interwiki links from the given page(s)'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'missingparam', 'prefix' ), - ) ); + return 'Returns all interwiki links from the given page(s).'; } public function getExamples() { return array( - 'api.php?action=query&prop=iwlinks&titles=Main%20Page' => 'Get interwiki links from the [[Main Page]]', + 'api.php?action=query&prop=iwlinks&titles=Main%20Page' + => 'Get interwiki links from the [[Main Page]]', ); } diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index 0ea28684..945374b1 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -33,9 +33,10 @@ class ApiQueryImageInfo extends ApiQueryBase { const TRANSFORM_LIMIT = 50; private static $transformCount = 0; - public function __construct( $query, $moduleName, $prefix = 'ii' ) { - // We allow a subclass to override the prefix, to create a related API module. - // Some other parts of MediaWiki construct this with a null $prefix, which used to be ignored when this only took two arguments + public function __construct( ApiQuery $query, $moduleName, $prefix = 'ii' ) { + // We allow a subclass to override the prefix, to create a related API + // module. Some other parts of MediaWiki construct this with a null + // $prefix, which used to be ignored when this only took two arguments if ( is_null( $prefix ) ) { $prefix = 'ii'; } @@ -49,6 +50,14 @@ class ApiQueryImageInfo extends ApiQueryBase { $scale = $this->getScale( $params ); + $opts = array( + 'version' => $params['metadataversion'], + 'language' => $params['extmetadatalanguage'], + 'multilang' => $params['extmetadatamultilang'], + 'extmetadatafilter' => $params['extmetadatafilter'], + 'revdelUser' => $this->getUser(), + ); + $pageIds = $this->getPageSet()->getAllTitlesByNamespace(); if ( !empty( $pageIds[NS_FILE] ) ) { $titles = array_keys( $pageIds[NS_FILE] ); @@ -70,13 +79,21 @@ class ApiQueryImageInfo extends ApiQueryBase { } } - $result = $this->getResult(); - //search only inside the local repo + $user = $this->getUser(); + $findTitles = array_map( function ( $title ) use ( $user ) { + return array( + 'title' => $title, + 'private' => $user, + ); + }, $titles ); + if ( $params['localonly'] ) { - $images = RepoGroup::singleton()->getLocalRepo()->findFiles( $titles ); + $images = RepoGroup::singleton()->getLocalRepo()->findFiles( $findTitles ); } else { - $images = RepoGroup::singleton()->findFiles( $titles ); + $images = RepoGroup::singleton()->findFiles( $findTitles ); } + + $result = $this->getResult(); foreach ( $titles as $title ) { $pageId = $pageIds[NS_FILE][$title]; $start = $title === $fromTitle ? $fromTimestamp : $params['start']; @@ -146,7 +163,9 @@ class ApiQueryImageInfo extends ApiQueryBase { $fit = $this->addPageSubItem( $pageId, self::getInfo( $img, $prop, $result, - $finalThumbParams, $params['metadataversion'] ) ); + $finalThumbParams, $opts + ) + ); if ( !$fit ) { if ( count( $pageIds[NS_FILE] ) == 1 ) { // See the 'the user is screwed' comment above @@ -167,7 +186,8 @@ class ApiQueryImageInfo extends ApiQueryBase { /** @var $oldie File */ 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... + // 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', @@ -178,7 +198,7 @@ class ApiQueryImageInfo extends ApiQueryBase { $fit = self::getTransformCount() < self::TRANSFORM_LIMIT && $this->addPageSubItem( $pageId, self::getInfo( $oldie, $prop, $result, - $finalThumbParams, $params['metadataversion'] + $finalThumbParams, $opts ) ); if ( !$fit ) { @@ -202,7 +222,7 @@ class ApiQueryImageInfo extends ApiQueryBase { /** * From parameters, construct a 'scale' array * @param array $params Parameters passed to api. - * @return Array or Null: key-val array of 'width' and 'height', or null + * @return array|null Key-val array of 'width' and 'height', or null */ public function getScale( $params ) { $p = $this->getModulePrefix(); @@ -217,9 +237,11 @@ class ApiQueryImageInfo extends ApiQueryBase { $scale = array(); $scale['height'] = $params['urlheight']; } else { - $scale = null; if ( $params['urlparam'] ) { - $this->dieUsage( "{$p}urlparam requires {$p}urlwidth", "urlparam_no_width" ); + // Audio files might not have a width/height. + $scale = array(); + } else { + $scale = null; } } @@ -231,26 +253,29 @@ class ApiQueryImageInfo extends ApiQueryBase { * We do this later than getScale, since we need the image * to know which handler, since handlers can make their own parameters. * @param File $image Image that params are for. - * @param array $thumbParams thumbnail parameters from getScale - * @param string $otherParams of otherParams (iiurlparam). - * @return Array of parameters for transform. + * @param array $thumbParams Thumbnail parameters from getScale + * @param string $otherParams String of otherParams (iiurlparam). + * @return array Array of parameters for transform. */ protected function mergeThumbParams( $image, $thumbParams, $otherParams ) { - global $wgThumbLimits; - + if ( $thumbParams === null ) { + // No scaling requested + return null; + } if ( !isset( $thumbParams['width'] ) && isset( $thumbParams['height'] ) ) { // We want to limit only by height in this situation, so pass the // image's full width as the limiting width. But some file types // don't have a width of their own, so pick something arbitrary so // thumbnailing the default icon works. if ( $image->getWidth() <= 0 ) { - $thumbParams['width'] = max( $wgThumbLimits ); + $thumbParams['width'] = max( $this->getConfig()->get( 'ThumbLimits' ) ); } else { $thumbParams['width'] = $image->getWidth(); } } if ( !$otherParams ) { + $this->checkParameterNormalise( $image, $thumbParams ); return $thumbParams; } $p = $this->getModulePrefix(); @@ -259,6 +284,7 @@ class ApiQueryImageInfo extends ApiQueryBase { if ( !$h ) { $this->setWarning( 'Could not create thumbnail because ' . $image->getName() . ' does not have an associated image handler' ); + return $thumbParams; } @@ -270,13 +296,15 @@ class ApiQueryImageInfo extends ApiQueryBase { // handlers. $this->setWarning( "Could not parse {$p}urlparam for " . $image->getName() . '. Using only width and height' ); + $this->checkParameterNormalise( $image, $thumbParams ); return $thumbParams; } - if ( isset( $paramList['width'] ) ) { + if ( isset( $paramList['width'] ) && isset( $thumbParams['width'] ) ) { if ( intval( $paramList['width'] ) != intval( $thumbParams['width'] ) ) { $this->setWarning( "Ignoring width value set in {$p}urlparam ({$paramList['width']}) " - . "in favor of width value derived from {$p}urlwidth/{$p}urlheight ({$thumbParams['width']})" ); + . "in favor of width value derived from {$p}urlwidth/{$p}urlheight " + . "({$thumbParams['width']})" ); } } @@ -286,20 +314,65 @@ class ApiQueryImageInfo extends ApiQueryBase { } } - return $thumbParams + $paramList; + $finalParams = $thumbParams + $paramList; + $this->checkParameterNormalise( $image, $finalParams ); + return $finalParams; + } + + /** + * Verify that the final image parameters can be normalised. + * + * This doesn't use the normalised parameters, since $file->transform + * expects the pre-normalised parameters, but doing the normalisation + * allows us to catch certain error conditions early (such as missing + * required parameter). + * + * @param $image File + * @param $finalParams array List of parameters to transform image with + */ + protected function checkParameterNormalise( $image, $finalParams ) { + $h = $image->getHandler(); + if ( !$h ) { + return; + } + // Note: normaliseParams modifies the array in place, but we aren't interested + // in the actual normalised version, only if we can actually normalise them, + // so we use the functions scope to throw away the normalisations. + if ( !$h->normaliseParams( $image, $finalParams ) ) { + $this->dieUsage( "Could not normalise image parameters for " . $image->getName(), "urlparamnormal" ); + } } /** * Get result information for an image revision * - * @param $file File object - * @param array $prop of properties to get (in the keys) - * @param $result ApiResult object - * @param array $thumbParams containing 'width' and 'height' items, or null - * @param string $version Version of image metadata (for things like jpeg which have different versions). - * @return Array: result array + * @param File $file + * @param array $prop Array of properties to get (in the keys) + * @param ApiResult $result + * @param array $thumbParams Containing 'width' and 'height' items, or null + * @param array|bool|string $opts Options for data fetching. + * This is an array consisting of the keys: + * 'version': The metadata version for the metadata option + * 'language': The language for extmetadata property + * 'multilang': Return all translations in extmetadata property + * 'revdelUser': User to use when checking whether to show revision-deleted fields. + * @return array Result array */ - static function getInfo( $file, $prop, $result, $thumbParams = null, $version = 'latest' ) { + static function getInfo( $file, $prop, $result, $thumbParams = null, $opts = false ) { + global $wgContLang; + + $anyHidden = false; + + if ( !$opts || is_string( $opts ) ) { + $opts = array( + 'version' => $opts ?: 'latest', + 'language' => $wgContLang, + 'multilang' => false, + 'extmetadatafilter' => array(), + 'revdelUser' => null, + ); + } + $version = $opts['version']; $vals = array(); // Timestamp is shown even if the file is revdelete'd in interface // so do same here. @@ -307,13 +380,27 @@ class ApiQueryImageInfo extends ApiQueryBase { $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $file->getTimestamp() ); } + // Handle external callers who don't pass revdelUser + if ( isset( $opts['revdelUser'] ) && $opts['revdelUser'] ) { + $revdelUser = $opts['revdelUser']; + $canShowField = function ( $field ) use ( $file, $revdelUser ) { + return $file->userCan( $field, $revdelUser ); + }; + } else { + $canShowField = function ( $field ) use ( $file ) { + return !$file->isDeleted( $field ); + }; + } + $user = isset( $prop['user'] ); $userid = isset( $prop['userid'] ); if ( $user || $userid ) { if ( $file->isDeleted( File::DELETED_USER ) ) { $vals['userhidden'] = ''; - } else { + $anyHidden = true; + } + if ( $canShowField( File::DELETED_USER ) ) { if ( $user ) { $vals['user'] = $file->getUser(); } @@ -337,6 +424,13 @@ class ApiQueryImageInfo extends ApiQueryBase { if ( $pageCount !== false ) { $vals['pagecount'] = $pageCount; } + + // length as in how many seconds long a video is. + $length = $file->getLength(); + if ( $length ) { + // Call it duration, because "length" can be ambiguous. + $vals['duration'] = (float)$length; + } } $pcomment = isset( $prop['parsedcomment'] ); @@ -345,34 +439,53 @@ class ApiQueryImageInfo extends ApiQueryBase { if ( $pcomment || $comment ) { if ( $file->isDeleted( File::DELETED_COMMENT ) ) { $vals['commenthidden'] = ''; - } else { + $anyHidden = true; + } + if ( $canShowField( File::DELETED_COMMENT ) ) { if ( $pcomment ) { $vals['parsedcomment'] = Linker::formatComment( - $file->getDescription(), $file->getTitle() ); + $file->getDescription( File::RAW ), $file->getTitle() ); } if ( $comment ) { - $vals['comment'] = $file->getDescription(); + $vals['comment'] = $file->getDescription( File::RAW ); } } } + $canonicaltitle = isset( $prop['canonicaltitle'] ); $url = isset( $prop['url'] ); $sha1 = isset( $prop['sha1'] ); $meta = isset( $prop['metadata'] ); + $extmetadata = isset( $prop['extmetadata'] ); + $commonmeta = isset( $prop['commonmetadata'] ); $mime = isset( $prop['mime'] ); $mediatype = isset( $prop['mediatype'] ); $archive = isset( $prop['archivename'] ); $bitdepth = isset( $prop['bitdepth'] ); $uploadwarning = isset( $prop['uploadwarning'] ); - if ( ( $url || $sha1 || $meta || $mime || $mediatype || $archive || $bitdepth ) - && $file->isDeleted( File::DELETED_FILE ) ) { + if ( $uploadwarning ) { + $vals['html'] = SpecialUpload::getExistsWarning( UploadBase::getExistsWarning( $file ) ); + } + + if ( $file->isDeleted( File::DELETED_FILE ) ) { $vals['filehidden'] = ''; + $anyHidden = true; + } + + if ( $anyHidden && $file->isDeleted( File::DELETED_RESTRICTED ) ) { + $vals['suppressed'] = true; + } + if ( !$canShowField( File::DELETED_FILE ) ) { //Early return, tidier than indenting all following things one level return $vals; } + if ( $canonicaltitle ) { + $vals['canonicaltitle'] = $file->getTitle()->getPrefixedText(); + } + if ( $url ) { if ( !is_null( $thumbParams ) ) { $mto = $file->transform( $thumbParams ); @@ -416,6 +529,26 @@ class ApiQueryImageInfo extends ApiQueryBase { } $vals['metadata'] = $metadata ? self::processMetaData( $metadata, $result ) : null; } + if ( $commonmeta ) { + $metaArray = $file->getCommonMetaArray(); + $vals['commonmetadata'] = $metaArray ? self::processMetaData( $metaArray, $result ) : array(); + } + + if ( $extmetadata ) { + // Note, this should return an array where all the keys + // start with a letter, and all the values are strings. + // Thus there should be no issue with format=xml. + $format = new FormatMetadata; + $format->setSingleLanguage( !$opts['multilang'] ); + $format->getContext()->setLanguage( $opts['language'] ); + $extmetaArray = $format->fetchExtendedMetadata( $file ); + if ( $opts['extmetadatafilter'] ) { + $extmetaArray = array_intersect_key( + $extmetaArray, array_flip( $opts['extmetadatafilter'] ) + ); + } + $vals['extmetadata'] = $extmetaArray; + } if ( $mime ) { $vals['mime'] = $file->getMimeType(); @@ -433,10 +566,6 @@ class ApiQueryImageInfo extends ApiQueryBase { $vals['bitdepth'] = $file->getBitDepth(); } - if ( $uploadwarning ) { - $vals['html'] = SpecialUpload::getExistsWarning( UploadBase::getExistsWarning( $file ) ); - } - return $vals; } @@ -445,7 +574,7 @@ class ApiQueryImageInfo extends ApiQueryBase { * * If this is >= TRANSFORM_LIMIT, you should probably stop processing images. * - * @return integer count + * @return int Count */ static function getTransformCount() { return self::$transformCount; @@ -453,9 +582,9 @@ class ApiQueryImageInfo extends ApiQueryBase { /** * - * @param $metadata Array - * @param $result ApiResult - * @return Array + * @param array $metadata + * @param ApiResult $result + * @return array */ public static function processMetaData( $metadata, $result ) { $retval = array(); @@ -471,15 +600,20 @@ class ApiQueryImageInfo extends ApiQueryBase { } } $result->setIndexedTagName( $retval, 'metadata' ); + return $retval; } public function getCacheMode( $params ) { + if ( $this->userCanSeeRevDel() ) { + return 'private'; + } + return 'public'; } /** - * @param $img File + * @param File $img * @param null|string $start * @return string */ @@ -487,10 +621,13 @@ class ApiQueryImageInfo extends ApiQueryBase { if ( $start === null ) { $start = $img->getTimestamp(); } + return $img->getOriginalTitle()->getDBkey() . '|' . $start; } public function getAllowedParams() { + global $wgContLang; + return array( 'prop' => array( ApiBase::PARAM_ISMULTI => true, @@ -522,6 +659,18 @@ class ApiQueryImageInfo extends ApiQueryBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_DFLT => '1', ), + 'extmetadatalanguage' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_DFLT => $wgContLang->getCode(), + ), + 'extmetadatamultilang' => array( + ApiBase::PARAM_TYPE => 'boolean', + ApiBase::PARAM_DFLT => false, + ), + 'extmetadatafilter' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_ISMULTI => true, + ), 'urlparam' => array( ApiBase::PARAM_DFLT => '', ApiBase::PARAM_TYPE => 'string', @@ -536,7 +685,7 @@ class ApiQueryImageInfo extends ApiQueryBase { * * @param array $filter List of properties to filter out * - * @return Array + * @return array */ public static function getPropertyNames( $filter = array() ) { return array_diff( array_keys( self::getProperties() ), $filter ); @@ -555,18 +704,26 @@ class ApiQueryImageInfo extends ApiQueryBase { 'userid' => ' userid - Add the user ID that uploaded the image version', 'comment' => ' comment - Comment on the version', 'parsedcomment' => ' parsedcomment - Parse the comment on the version', + 'canonicaltitle' => ' canonicaltitle - Adds the canonical title of the image file', 'url' => ' url - Gives URL to the image and the description page', - 'size' => ' size - Adds the size of the image in bytes and the height, width and page count (if applicable)', - 'dimensions' => ' dimensions - Alias for size', // For backwards compatibility with Allimages + 'size' => ' size - Adds the size of the image in bytes, ' . + 'its height and its width. Page count and duration are added if applicable', + 'dimensions' => ' dimensions - Alias for size', // B/C with Allimages 'sha1' => ' sha1 - Adds SHA-1 hash for the image', 'mime' => ' mime - Adds MIME type of the image', 'thumbmime' => ' thumbmime - Adds MIME type of the image thumbnail' . ' (requires url and param ' . $modulePrefix . 'urlwidth)', 'mediatype' => ' mediatype - Adds the media type of the image', 'metadata' => ' metadata - Lists Exif metadata for the version of the image', - 'archivename' => ' archivename - Adds the file name of the archive version for non-latest versions', + 'commonmetadata' => ' commonmetadata - Lists file format generic metadata ' . + 'for the version of the image', + 'extmetadata' => ' extmetadata - Lists formatted metadata combined ' . + 'from multiple sources. Results are HTML formatted.', + 'archivename' => ' archivename - Adds the file name of the archive ' . + 'version for non-latest versions', 'bitdepth' => ' bitdepth - Adds the bit depth of the version', - 'uploadwarning' => ' uploadwarning - Used by the Special:Upload page to get information about an existing file. Not intended for use outside MediaWiki core', + 'uploadwarning' => ' uploadwarning - Used by the Special:Upload page to ' . + 'get information about an existing file. Not intended for use outside MediaWiki core', ); } @@ -586,169 +743,53 @@ class ApiQueryImageInfo extends ApiQueryBase { /** * Return the API documentation for the parameters. - * @return Array parameter documentation. + * @return array Parameter documentation. */ public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'prop' => self::getPropertyDescriptions( array(), $p ), - 'urlwidth' => array( "If {$p}prop=url is set, a URL to an image scaled to this width will be returned.", + 'urlwidth' => array( + "If {$p}prop=url is set, a URL to an image scaled to this width will be returned.", 'For performance reasons if this option is used, ' . - 'no more than ' . self::TRANSFORM_LIMIT . ' scaled images will be returned.' ), + 'no more than ' . self::TRANSFORM_LIMIT . ' scaled images will be returned.' + ), 'urlheight' => "Similar to {$p}urlwidth.", - 'urlparam' => array( "A handler specific parameter string. For example, pdf's ", - "might use 'page15-100px'. {$p}urlwidth must be used and be consistent with {$p}urlparam" ), + 'urlparam' => array( + "A handler specific parameter string. For example, pdf's ", + "might use 'page15-100px'." + ), 'limit' => 'How many image revisions to return per image', 'start' => 'Timestamp to start listing from', 'end' => 'Timestamp to stop listing at', - 'metadataversion' => array( "Version of metadata to use. if 'latest' is specified, use latest version.", - "Defaults to '1' for backwards compatibility" ), - 'continue' => 'If the query response includes a continue value, use it here to get another page of results', - 'localonly' => 'Look only for files in the local repository', - ); - } - - public static function getResultPropertiesFiltered( $filter = array() ) { - $props = array( - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'user' => array( - 'userhidden' => 'boolean', - 'user' => 'string', - 'anon' => 'boolean' - ), - 'userid' => array( - 'userhidden' => 'boolean', - 'userid' => 'integer', - 'anon' => 'boolean' - ), - 'size' => array( - 'size' => 'integer', - 'width' => 'integer', - 'height' => 'integer', - 'pagecount' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'dimensions' => array( - 'size' => 'integer', - 'width' => 'integer', - 'height' => 'integer', - 'pagecount' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'comment' => array( - 'commenthidden' => 'boolean', - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'commenthidden' => 'boolean', - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'url' => array( - 'filehidden' => 'boolean', - 'thumburl' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'thumbwidth' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'thumbheight' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'thumberror' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'url' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'descriptionurl' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'sha1' => array( - 'filehidden' => 'boolean', - 'sha1' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'mime' => array( - 'filehidden' => 'boolean', - 'mime' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'thumbmime' => array( - 'filehidden' => 'boolean', - 'thumbmime' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'mediatype' => array( - 'filehidden' => 'boolean', - 'mediatype' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'archivename' => array( - 'filehidden' => 'boolean', - 'archivename' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'bitdepth' => array( - 'filehidden' => 'boolean', - 'bitdepth' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) + 'metadataversion' + => array( "Version of metadata to use. if 'latest' is specified, use latest version.", + "Defaults to '1' for backwards compatibility" ), + 'extmetadatalanguage' => array( + 'What language to fetch extmetadata in. This affects both which', + 'translation to fetch, if multiple are available, as well as how things', + 'like numbers and various values are formatted.' ), + 'extmetadatamultilang' + =>'If translations for extmetadata property are available, fetch all of them.', + 'extmetadatafilter' + => "If specified and non-empty, only these keys will be returned for {$p}prop=extmetadata", + 'continue' => 'If the query response includes a continue value, ' . + 'use it here to get another page of results', + 'localonly' => 'Look only for files in the local repository', ); - return array_diff_key( $props, array_flip( $filter ) ); - } - - public function getResultProperties() { - return self::getResultPropertiesFiltered(); } public function getDescription() { - return 'Returns image information and upload history'; - } - - public function getPossibleErrors() { - $p = $this->getModulePrefix(); - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => "{$p}urlwidth", 'info' => "{$p}urlheight cannot be used without {$p}urlwidth" ), - array( 'code' => 'urlparam', 'info' => "Invalid value for {$p}urlparam" ), - array( 'code' => 'urlparam_no_width', 'info' => "{$p}urlparam requires {$p}urlwidth" ), - ) ); + return 'Returns image information and upload history.'; } public function getExamples() { return array( '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', + 'api.php?action=query&titles=File:Test.jpg&prop=imageinfo&iilimit=50&' . + 'iiend=20071231235959&iiprop=timestamp|user|url', ); } diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php index f2bf0a7b..9bc3abed 100644 --- a/includes/api/ApiQueryImages.php +++ b/includes/api/ApiQueryImages.php @@ -32,7 +32,7 @@ */ class ApiQueryImages extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'im' ); } @@ -45,7 +45,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet */ private function run( $resultPageSet = null ) { if ( $this->getPageSet()->getGoodTitleCount() == 0 ) { @@ -79,9 +79,9 @@ class ApiQueryImages extends ApiQueryGeneratorBase { $this->addOption( 'ORDER BY', 'il_to' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'il_from' . $sort, - 'il_to' . $sort - )); + 'il_from' . $sort, + 'il_to' . $sort + ) ); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -164,28 +164,22 @@ class ApiQueryImages extends ApiQueryGeneratorBase { return array( 'limit' => 'How many images to return', 'continue' => 'When more results are available, use this to continue', - 'images' => 'Only list these images. Useful for checking whether a certain page has a certain Image.', + 'images' => 'Only list these images. Useful for checking whether a ' . + 'certain page has a certain Image.', 'dir' => 'The direction in which to list', ); } - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { - return 'Returns all images contained on the given page(s)'; + return 'Returns all images contained on the given page(s).'; } public function getExamples() { return array( - 'api.php?action=query&prop=images&titles=Main%20Page' => 'Get a list of images used in the [[Main Page]]', - 'api.php?action=query&generator=images&titles=Main%20Page&prop=info' => 'Get information about all images used in the [[Main Page]]', + 'api.php?action=query&prop=images&titles=Main%20Page' + => 'Get a list of images used in the [[Main Page]]', + 'api.php?action=query&generator=images&titles=Main%20Page&prop=info' + => 'Get information about all images used in the [[Main Page]]', ); } diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index 017684ed..d7037e3a 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -42,35 +42,35 @@ class ApiQueryInfo extends ApiQueryBase { private $pageRestrictions, $pageIsRedir, $pageIsNew, $pageTouched, $pageLatest, $pageLength; - private $protections, $watched, $watchers, $notificationtimestamps, $talkids, $subjectids, $displaytitles; + private $protections, $watched, $watchers, $notificationtimestamps, + $talkids, $subjectids, $displaytitles; private $showZeroWatchers = false; private $tokenFunctions; - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'in' ); } /** - * @param $pageSet ApiPageSet + * @param ApiPageSet $pageSet * @return void */ public function requestExtraData( $pageSet ) { - global $wgDisableCounters, $wgContentHandlerUseDB; - $pageSet->requestField( 'page_restrictions' ); // when resolving redirects, no page will have this field if ( !$pageSet->isResolvingRedirects() ) { $pageSet->requestField( 'page_is_redirect' ); } $pageSet->requestField( 'page_is_new' ); - if ( !$wgDisableCounters ) { + $config = $this->getConfig(); + if ( !$config->get( 'DisableCounters' ) ) { $pageSet->requestField( 'page_counter' ); } $pageSet->requestField( 'page_touched' ); $pageSet->requestField( 'page_latest' ); $pageSet->requestField( 'page_len' ); - if ( $wgContentHandlerUseDB ) { + if ( $config->get( 'ContentHandlerUseDB' ) ) { $pageSet->requestField( 'page_content_model' ); } } @@ -79,7 +79,8 @@ class ApiQueryInfo extends ApiQueryBase { * 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 array(tokenname => function) + * @deprecated since 1.24 + * @return array Array(tokenname => function) */ protected function getTokenFunctions() { // Don't call the hooks twice @@ -104,15 +105,22 @@ class ApiQueryInfo extends ApiQueryBase { 'watch' => array( 'ApiQueryInfo', 'getWatchToken' ), ); wfRunHooks( 'APIQueryInfoTokens', array( &$this->tokenFunctions ) ); + return $this->tokenFunctions; } - static $cachedTokens = array(); + static protected $cachedTokens = array(); + /** + * @deprecated since 1.24 + */ public static function resetTokenCache() { ApiQueryInfo::$cachedTokens = array(); } + /** + * @deprecated since 1.24 + */ public static function getEditToken( $pageid, $title ) { // We could check for $title->userCan('edit') here, // but that's too expensive for this purpose @@ -130,6 +138,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['edit']; } + /** + * @deprecated since 1.24 + */ public static function getDeleteToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isAllowed( 'delete' ) ) { @@ -144,6 +155,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['delete']; } + /** + * @deprecated since 1.24 + */ public static function getProtectToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isAllowed( 'protect' ) ) { @@ -158,6 +172,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['protect']; } + /** + * @deprecated since 1.24 + */ public static function getMoveToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isAllowed( 'move' ) ) { @@ -172,6 +189,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['move']; } + /** + * @deprecated since 1.24 + */ public static function getBlockToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isAllowed( 'block' ) ) { @@ -186,11 +206,17 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['block']; } + /** + * @deprecated since 1.24 + */ public static function getUnblockToken( $pageid, $title ) { // Currently, this is exactly the same as the block token return self::getBlockToken( $pageid, $title ); } + /** + * @deprecated since 1.24 + */ public static function getEmailToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->canSendEmail() || $wgUser->isBlockedFromEmailUser() ) { @@ -205,6 +231,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['email']; } + /** + * @deprecated since 1.24 + */ public static function getImportToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isAllowedAny( 'import', 'importupload' ) ) { @@ -219,6 +248,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['import']; } + /** + * @deprecated since 1.24 + */ public static function getWatchToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isLoggedIn() ) { @@ -233,6 +265,9 @@ class ApiQueryInfo extends ApiQueryBase { return ApiQueryInfo::$cachedTokens['watch']; } + /** + * @deprecated since 1.24 + */ public static function getOptionsToken( $pageid, $title ) { global $wgUser; if ( !$wgUser->isLoggedIn() ) { @@ -293,9 +328,7 @@ class ApiQueryInfo extends ApiQueryBase { : array(); $this->pageIsNew = $pageSet->getCustomField( 'page_is_new' ); - global $wgDisableCounters; - - if ( !$wgDisableCounters ) { + if ( !$this->getConfig()->get( 'DisableCounters' ) ) { $this->pageCounter = $pageSet->getCustomField( 'page_counter' ); } $this->pageTouched = $pageSet->getCustomField( 'page_touched' ); @@ -333,8 +366,8 @@ class ApiQueryInfo extends ApiQueryBase { ), $pageid, $pageInfo ); if ( !$fit ) { $this->setContinueEnumParameter( 'continue', - $title->getNamespace() . '|' . - $title->getText() ); + $title->getNamespace() . '|' . + $title->getText() ); break; } } @@ -343,12 +376,13 @@ class ApiQueryInfo extends ApiQueryBase { /** * Get a result array with information about a title * @param int $pageid Page ID (negative for missing titles) - * @param $title Title object + * @param Title $title * @return array */ private function extractPageInfo( $pageid, $title ) { $pageInfo = array(); - $titleExists = $pageid > 0; //$title->exists() needs pageid, which is not set for all title objects + // $title->exists() needs pageid, which is not set for all title objects + $titleExists = $pageid > 0; $ns = $title->getNamespace(); $dbkey = $title->getDBkey(); @@ -356,11 +390,9 @@ class ApiQueryInfo extends ApiQueryBase { $pageInfo['pagelanguage'] = $title->getPageLanguage()->getCode(); if ( $titleExists ) { - global $wgDisableCounters; - $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] ); $pageInfo['lastrevid'] = intval( $this->pageLatest[$pageid] ); - $pageInfo['counter'] = $wgDisableCounters + $pageInfo['counter'] = $this->getConfig()->get( 'DisableCounters' ) ? '' : intval( $this->pageCounter[$pageid] ); $pageInfo['length'] = intval( $this->pageLength[$pageid] ); @@ -410,7 +442,8 @@ class ApiQueryInfo extends ApiQueryBase { if ( $this->fld_notificationtimestamp ) { $pageInfo['notificationtimestamp'] = ''; if ( isset( $this->notificationtimestamps[$ns][$dbkey] ) ) { - $pageInfo['notificationtimestamp'] = wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] ); + $pageInfo['notificationtimestamp'] = + wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] ); } } @@ -425,6 +458,7 @@ class ApiQueryInfo extends ApiQueryBase { if ( $this->fld_url ) { $pageInfo['fullurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); $pageInfo['editurl'] = wfExpandUrl( $title->getFullURL( 'action=edit' ), PROTO_CURRENT ); + $pageInfo['canonicalurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CANONICAL ); } if ( $this->fld_readable && $title->userCan( 'read', $this->getUser() ) ) { $pageInfo['readable'] = ''; @@ -465,7 +499,7 @@ class ApiQueryInfo extends ApiQueryBase { $this->resetQueryParams(); $this->addTables( 'page_restrictions' ); $this->addFields( array( 'pr_page', 'pr_type', 'pr_level', - 'pr_expiry', 'pr_cascade' ) ); + 'pr_expiry', 'pr_cascade' ) ); $this->addWhereFld( 'pr_page', array_keys( $this->titles ) ); $res = $this->select( __METHOD__ ); @@ -556,8 +590,8 @@ class ApiQueryInfo extends ApiQueryBase { $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' ) ); + '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' ); @@ -580,7 +614,7 @@ class ApiQueryInfo extends ApiQueryBase { $this->resetQueryParams(); $this->addTables( array( 'page_restrictions', 'page', 'imagelinks' ) ); $this->addFields( array( 'pr_type', 'pr_level', 'pr_expiry', - 'page_title', 'page_namespace', 'il_to' ) ); + 'page_title', 'page_namespace', 'il_to' ) ); $this->addWhere( 'pr_page = page_id' ); $this->addWhere( 'pr_page = il_from' ); $this->addWhereFld( 'pr_cascade', 1 ); @@ -633,10 +667,10 @@ class ApiQueryInfo extends ApiQueryBase { foreach ( $res as $row ) { if ( MWNamespace::isTalk( $row->page_namespace ) ) { $this->talkids[MWNamespace::getSubject( $row->page_namespace )][$row->page_title] = - intval( $row->page_id ); + intval( $row->page_id ); } else { $this->subjectids[MWNamespace::getTalk( $row->page_namespace )][$row->page_title] = - intval( $row->page_id ); + intval( $row->page_id ); } } } @@ -697,7 +731,8 @@ class ApiQueryInfo extends ApiQueryBase { $this->watched[$row->wl_namespace][$row->wl_title] = true; } if ( $this->fld_notificationtimestamp ) { - $this->notificationtimestamps[$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp; + $this->notificationtimestamps[$row->wl_namespace][$row->wl_title] = + $row->wl_notificationtimestamp; } } } @@ -706,15 +741,14 @@ class ApiQueryInfo extends ApiQueryBase { * Get the count of watchers and put it in $this->watchers */ private function getWatcherInfo() { - global $wgUnwatchedPageThreshold; - if ( count( $this->everything ) == 0 ) { return; } $user = $this->getUser(); $canUnwatchedpages = $user->isAllowed( 'unwatchedpages' ); - if ( !$canUnwatchedpages && !is_int( $wgUnwatchedPageThreshold ) ) { + $unwatchedPageThreshold = $this->getConfig()->get( 'UnwatchedPageThreshold' ); + if ( !$canUnwatchedpages && !is_int( $unwatchedPageThreshold ) ) { return; } @@ -732,7 +766,7 @@ class ApiQueryInfo extends ApiQueryBase { ) ); $this->addOption( 'GROUP BY', array( 'wl_namespace', 'wl_title' ) ); if ( !$canUnwatchedpages ) { - $this->addOption( 'HAVING', "COUNT(*) >= $wgUnwatchedPageThreshold" ); + $this->addOption( 'HAVING', "COUNT(*) >= $unwatchedPageThreshold" ); } $res = $this->select( __METHOD__ ); @@ -761,6 +795,7 @@ class ApiQueryInfo extends ApiQueryBase { if ( !is_null( $params['token'] ) ) { return 'private'; } + return 'public'; } @@ -784,6 +819,7 @@ class ApiQueryInfo extends ApiQueryBase { // need to be added to getCacheMode() ) ), 'token' => array( + ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_DFLT => null, ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ) @@ -802,7 +838,7 @@ class ApiQueryInfo extends ApiQueryBase { ' watchers - The number of watchers, if allowed', ' notificationtimestamp - The watchlist notification timestamp of each page', ' subjectid - The page ID of the parent page for each talk page', - ' url - Gives a full URL to the page, and also an edit URL', + ' url - Gives a full URL, an edit URL, and the canonical URL for each page', ' readable - Whether the user can read this page', ' preload - Gives the text returned by EditFormPreloadText', ' displaytitle - Gives the way the page title is actually displayed', @@ -812,72 +848,6 @@ class ApiQueryInfo extends ApiQueryBase { ); } - public function getResultProperties() { - $props = array( - ApiBase::PROP_LIST => false, - '' => array( - 'touched' => 'timestamp', - 'lastrevid' => 'integer', - 'counter' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'length' => 'integer', - 'redirect' => 'boolean', - 'new' => 'boolean', - 'starttimestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ), - 'contentmodel' => 'string', - ), - 'watched' => array( - 'watched' => 'boolean' - ), - 'watchers' => array( - 'watchers' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'notificationtimestamp' => array( - 'notificationtimestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - 'talkid' => array( - 'talkid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'subjectid' => array( - 'subjectid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'url' => array( - 'fullurl' => 'string', - 'editurl' => 'string' - ), - 'readable' => array( - 'readable' => 'boolean' - ), - 'preload' => array( - 'preload' => 'string' - ), - 'displaytitle' => array( - 'displaytitle' => 'string' - ) - ); - - self::addTokenProperties( $props, $this->getTokenFunctions() ); - - return $props; - } - public function getDescription() { return 'Get basic page information such as namespace, title, last touched date, ...'; } diff --git a/includes/api/ApiQueryLangBacklinks.php b/includes/api/ApiQueryLangBacklinks.php index 5bd451b6..34842c63 100644 --- a/includes/api/ApiQueryLangBacklinks.php +++ b/includes/api/ApiQueryLangBacklinks.php @@ -31,7 +31,7 @@ */ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'lbl' ); } @@ -44,7 +44,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ public function run( $resultPageSet = null ) { @@ -92,14 +92,14 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { $this->addOption( 'ORDER BY', array( 'll_title' . $sort, 'll_from' . $sort - )); + ) ); } } else { $this->addOption( 'ORDER BY', array( 'll_lang' . $sort, 'll_title' . $sort, 'll_from' . $sort - )); + ) ); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -111,10 +111,14 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { $count = 0; $result = $this->getResult(); foreach ( $res as $row ) { - if ( ++ $count > $params['limit'] ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... - // Continue string preserved in case the redirect query doesn't pass the limit - $this->setContinueEnumParameter( 'continue', "{$row->ll_lang}|{$row->ll_title}|{$row->ll_from}" ); + if ( ++$count > $params['limit'] ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... Continue string + // preserved in case the redirect query doesn't pass the limit. + $this->setContinueEnumParameter( + 'continue', + "{$row->ll_lang}|{$row->ll_title}|{$row->ll_from}" + ); break; } @@ -140,7 +144,10 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $entry ); if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', "{$row->ll_lang}|{$row->ll_title}|{$row->ll_from}" ); + $this->setContinueEnumParameter( + 'continue', + "{$row->ll_lang}|{$row->ll_title}|{$row->ll_from}" + ); break; } } @@ -202,23 +209,6 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'pageid' => 'integer', - 'ns' => 'namespace', - 'title' => 'string', - 'redirect' => 'boolean' - ), - 'lllang' => array( - 'lllang' => 'string' - ), - 'lltitle' => array( - 'lltitle' => 'string' - ) - ); - } - public function getDescription() { return array( 'Find all pages that link to the given language link.', 'Can be used to find all links with a language code, or', @@ -228,12 +218,6 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'missingparam', 'lang' ), - ) ); - } - public function getExamples() { return array( 'api.php?action=query&list=langbacklinks&lbltitle=Test&lbllang=fr', diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index aa796e31..da05f273 100644 --- a/includes/api/ApiQueryLangLinks.php +++ b/includes/api/ApiQueryLangLinks.php @@ -31,7 +31,7 @@ */ class ApiQueryLangLinks extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'll' ); } @@ -41,11 +41,19 @@ class ApiQueryLangLinks extends ApiQueryBase { } $params = $this->extractRequestParams(); + $prop = array_flip( (array)$params['prop'] ); if ( isset( $params['title'] ) && !isset( $params['lang'] ) ) { $this->dieUsageMsg( array( 'missingparam', 'lang' ) ); } + // Handle deprecated param + $this->requireMaxOneParameter( $params, 'url', 'prop' ); + if ( $params['url'] ) { + $this->logFeatureUsage( 'prop=langlinks&llurl' ); + $prop = array( 'url' => 1 ); + } + $this->addFields( array( 'll_from', 'll_lang', @@ -86,9 +94,9 @@ class ApiQueryLangLinks extends ApiQueryBase { $this->addOption( 'ORDER BY', 'll_lang' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'll_from' . $sort, - 'll_lang' . $sort - )); + 'll_from' . $sort, + 'll_lang' . $sort + ) ); } } @@ -104,12 +112,18 @@ class ApiQueryLangLinks extends ApiQueryBase { break; } $entry = array( 'lang' => $row->ll_lang ); - if ( $params['url'] ) { + if ( isset( $prop['url'] ) ) { $title = Title::newFromText( "{$row->ll_lang}:{$row->ll_title}" ); if ( $title ) { $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); } } + if ( isset( $prop['langname'] ) ) { + $entry['langname'] = Language::fetchLanguageName( $row->ll_lang, $params['inlanguagecode'] ); + } + if ( isset( $prop['autonym'] ) ) { + $entry['autonym'] = Language::fetchLanguageName( $row->ll_lang ); + } ApiResult::setContent( $entry, $row->ll_title ); $fit = $this->addPageSubItem( $row->ll_from, $entry ); if ( !$fit ) { @@ -124,6 +138,7 @@ class ApiQueryLangLinks extends ApiQueryBase { } public function getAllowedParams() { + global $wgContLang; return array( 'limit' => array( ApiBase::PARAM_DFLT => 10, @@ -133,7 +148,18 @@ class ApiQueryLangLinks extends ApiQueryBase { ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 ), 'continue' => null, - 'url' => false, + 'url' => array( + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_DEPRECATED => true, + ), + 'prop' => array( + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => array( + 'url', + 'langname', + 'autonym', + ) + ), 'lang' => null, 'title' => null, 'dir' => array( @@ -143,6 +169,7 @@ class ApiQueryLangLinks extends ApiQueryBase { 'descending' ) ), + 'inlanguagecode' => $wgContLang->getCode(), ); } @@ -150,39 +177,29 @@ class ApiQueryLangLinks extends ApiQueryBase { return array( 'limit' => 'How many langlinks to return', 'continue' => 'When more results are available, use this to continue', - 'url' => 'Whether to get the full URL', + 'url' => "Whether to get the full URL (Cannot be used with {$this->getModulePrefix()}prop)", + 'prop' => array( + 'Which additional properties to get for each interlanguage link', + ' url - Adds the full URL', + ' langname - Adds the localised language name (best effort, use CLDR extension)', + " Use {$this->getModulePrefix()}inlanguagecode to control the language", + ' autonym - Adds the native language name', + ), 'lang' => 'Language code', 'title' => "Link to search for. Must be used with {$this->getModulePrefix()}lang", 'dir' => 'The direction in which to list', - ); - } - - public function getResultProperties() { - return array( - '' => array( - 'lang' => 'string', - 'url' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - '*' => 'string' - ) + 'inlanguagecode' => 'Language code for localised language names', ); } public function getDescription() { - return 'Returns all interlanguage links from the given page(s)'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'missingparam', 'lang' ), - ) ); + return 'Returns all interlanguage links from the given page(s).'; } public function getExamples() { return array( - 'api.php?action=query&prop=langlinks&titles=Main%20Page&redirects=' => 'Get interlanguage links from the [[Main Page]]', + 'api.php?action=query&prop=langlinks&titles=Main%20Page&redirects=' + => 'Get interlanguage links from the [[Main Page]]', ); } diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index 937f4f13..71329c4d 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -36,14 +36,15 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { private $table, $prefix, $description, $helpUrl; - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { switch ( $moduleName ) { case self::LINKS: $this->table = 'pagelinks'; $this->prefix = 'pl'; $this->description = 'link'; $this->titlesParam = 'titles'; - $this->titlesParamDescription = 'Only list links to these titles. Useful for checking whether a certain page links to a certain title.'; + $this->titlesParamDescription = 'Only list links to these titles. Useful ' . + 'for checking whether a certain page links to a certain title.'; $this->helpUrl = 'https://www.mediawiki.org/wiki/API:Properties#links_.2F_pl'; break; case self::TEMPLATES: @@ -51,7 +52,8 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $this->prefix = 'tl'; $this->description = 'template'; $this->titlesParam = 'templates'; - $this->titlesParamDescription = 'Only list these templates. Useful for checking whether a certain page uses a certain template.'; + $this->titlesParamDescription = 'Only list these templates. Useful ' . + 'for checking whether a certain page uses a certain template.'; $this->helpUrl = 'https://www.mediawiki.org/wiki/API:Properties#templates_.2F_tl'; break; default: @@ -74,8 +76,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet - * @return + * @param ApiPageSet $resultPageSet */ private function run( $resultPageSet = null ) { if ( $this->getPageSet()->getGoodTitleCount() == 0 ) { @@ -212,6 +213,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { public function getParamDescription() { $desc = $this->description; + return array( 'namespace' => "Show {$desc}s in this namespace(s) only", 'limit' => "How many {$desc}s to return", @@ -221,26 +223,20 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { - return "Returns all {$this->description}s from the given page(s)"; + return "Returns all {$this->description}s from the given page(s)."; } public function getExamples() { $desc = $this->description; $name = $this->getModuleName(); + return array( "api.php?action=query&prop={$name}&titles=Main%20Page" => "Get {$desc}s from the [[Main Page]]", - "api.php?action=query&generator={$name}&titles=Main%20Page&prop=info" => "Get information about the {$desc} pages in the [[Main Page]]", - "api.php?action=query&prop={$name}&titles=Main%20Page&{$this->prefix}namespace=2|10" => "Get {$desc}s from the Main Page in the User and Template namespaces", + "api.php?action=query&generator={$name}&titles=Main%20Page&prop=info" + => "Get information about the {$desc} pages in the [[Main Page]]", + "api.php?action=query&prop={$name}&titles=Main%20Page&{$this->prefix}namespace=2|10" + => "Get {$desc}s from the Main Page in the User and Template namespaces", ); } diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index ecd117e4..d9dbb5e6 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -31,7 +31,7 @@ */ class ApiQueryLogEvents extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'le' ); } @@ -43,6 +43,7 @@ class ApiQueryLogEvents extends ApiQueryBase { public function execute() { $params = $this->extractRequestParams(); $db = $this->getDB(); + $this->requireMaxOneParameter( $params, 'title', 'prefix', 'namespace' ); $prop = array_flip( $params['prop'] ); @@ -64,26 +65,32 @@ class ApiQueryLogEvents extends ApiQueryBase { // Order is significant here $this->addTables( array( 'logging', 'user', 'page' ) ); - $this->addOption( 'STRAIGHT_JOIN' ); $this->addJoinConds( array( 'user' => array( 'LEFT JOIN', 'user_id=log_user' ), 'page' => array( 'LEFT JOIN', array( 'log_namespace=page_namespace', 'log_title=page_title' ) ) ) ); - $index = array( 'logging' => 'times' ); // default, may change $this->addFields( array( + 'log_id', 'log_type', 'log_action', 'log_timestamp', 'log_deleted', ) ); - $this->addFieldsIf( array( 'log_id', 'page_id' ), $this->fld_ids ); + $this->addFieldsIf( 'page_id', $this->fld_ids ); + // log_page is the page_id saved at log time, whereas page_id is from a + // join at query time. This leads to different results in various + // scenarios, e.g. deletion, recreation. + $this->addFieldsIf( 'log_page', $this->fld_ids ); $this->addFieldsIf( array( 'log_user', 'log_user_text', 'user_name' ), $this->fld_user ); $this->addFieldsIf( 'log_user', $this->fld_userid ); - $this->addFieldsIf( array( 'log_namespace', 'log_title' ), $this->fld_title || $this->fld_parsedcomment ); + $this->addFieldsIf( + array( 'log_namespace', 'log_title' ), + $this->fld_title || $this->fld_parsedcomment + ); $this->addFieldsIf( 'log_comment', $this->fld_comment || $this->fld_parsedcomment ); $this->addFieldsIf( 'log_params', $this->fld_details ); @@ -95,21 +102,59 @@ class ApiQueryLogEvents extends ApiQueryBase { if ( !is_null( $params['tag'] ) ) { $this->addTables( 'change_tag' ); - $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'log_id=ct_log_id' ) ) ) ); + $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', + array( 'log_id=ct_log_id' ) ) ) ); $this->addWhereFld( 'ct_tag', $params['tag'] ); - $index['change_tag'] = 'change_tag_tag_id'; } if ( !is_null( $params['action'] ) ) { - list( $type, $action ) = explode( '/', $params['action'] ); + // Do validation of action param, list of allowed actions can contains wildcards + // Allow the param, when the actions is in the list or a wildcard version is listed. + $logAction = $params['action']; + if ( strpos( $logAction, '/' ) === false ) { + // all items in the list have a slash + $valid = false; + } else { + $logActions = array_flip( $this->getAllowedLogActions() ); + list( $type, $action ) = explode( '/', $logAction, 2 ); + $valid = isset( $logActions[$logAction] ) || isset( $logActions[$type . '/*'] ); + } + + if ( !$valid ) { + $valueName = $this->encodeParamName( 'action' ); + $this->dieUsage( + "Unrecognized value for parameter '$valueName': {$logAction}", + "unknown_$valueName" + ); + } + $this->addWhereFld( 'log_type', $type ); $this->addWhereFld( 'log_action', $action ); } elseif ( !is_null( $params['type'] ) ) { $this->addWhereFld( 'log_type', $params['type'] ); - $index['logging'] = 'type_time'; } - $this->addTimestampWhereRange( 'log_timestamp', $params['dir'], $params['start'], $params['end'] ); + $this->addTimestampWhereRange( + 'log_timestamp', + $params['dir'], + $params['start'], + $params['end'] + ); + // Include in ORDER BY for uniqueness + $this->addWhereRange( 'log_id', $params['dir'], null, null ); + + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 2 ); + $op = ( $params['dir'] === 'newer' ? '>' : '<' ); + $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) ); + $continueId = (int)$cont[1]; + $this->dieContinueUsageIf( $continueId != $cont[1] ); + $this->addWhere( "log_timestamp $op $continueTimestamp OR " . + "(log_timestamp = $continueTimestamp AND " . + "log_id $op= $continueId)" + ); + } $limit = $params['limit']; $this->addOption( 'LIMIT', $limit + 1 ); @@ -117,11 +162,11 @@ class ApiQueryLogEvents extends ApiQueryBase { $user = $params['user']; if ( !is_null( $user ) ) { $userid = User::idFromName( $user ); - if ( !$userid ) { - $this->dieUsage( "User name $user not found", 'param_user' ); + if ( $userid ) { + $this->addWhereFld( 'log_user', $userid ); + } else { + $this->addWhereFld( 'log_user_text', IP::sanitizeIP( $user ) ); } - $this->addWhereFld( 'log_user', $userid ); - $index['logging'] = 'user_time'; } $title = $params['title']; @@ -132,16 +177,16 @@ class ApiQueryLogEvents extends ApiQueryBase { } $this->addWhereFld( 'log_namespace', $titleObj->getNamespace() ); $this->addWhereFld( 'log_title', $titleObj->getDBkey() ); + } - // Use the title index in preference to the user index if there is a conflict - $index['logging'] = is_null( $user ) ? 'page_time' : array( 'page_time', 'user_time' ); + if ( $params['namespace'] !== null ) { + $this->addWhereFld( 'log_namespace', $params['namespace'] ); } $prefix = $params['prefix']; if ( !is_null( $prefix ) ) { - global $wgMiserMode; - if ( $wgMiserMode ) { + if ( $this->getConfig()->get( 'MiserMode' ) ) { $this->dieUsage( 'Prefix search disabled in Miser Mode', 'prefixsearchdisabled' ); } @@ -153,23 +198,34 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->addWhere( 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() ) ); } - $this->addOption( 'USE INDEX', $index ); - // Paranoia: avoid brute force searches (bug 17342) - if ( !is_null( $title ) ) { - $this->addWhere( $db->bitAnd( 'log_deleted', LogPage::DELETED_ACTION ) . ' = 0' ); - } - if ( !is_null( $user ) ) { - $this->addWhere( $db->bitAnd( 'log_deleted', LogPage::DELETED_USER ) . ' = 0' ); + if ( $params['namespace'] !== null || !is_null( $title ) || !is_null( $user ) ) { + if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) { + $titleBits = LogPage::DELETED_ACTION; + $userBits = LogPage::DELETED_USER; + } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $titleBits = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED; + $userBits = LogPage::DELETED_USER | LogPage::DELETED_RESTRICTED; + } else { + $titleBits = 0; + $userBits = 0; + } + if ( ( $params['namespace'] !== null || !is_null( $title ) ) && $titleBits ) { + $this->addWhere( $db->bitAnd( 'log_deleted', $titleBits ) . " != $titleBits" ); + } + if ( !is_null( $user ) && $userBits ) { + $this->addWhere( $db->bitAnd( 'log_deleted', $userBits ) . " != $userBits" ); + } } $count = 0; $res = $this->select( __METHOD__ ); $result = $this->getResult(); foreach ( $res as $row ) { - if ( ++ $count > $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->log_timestamp ) ); + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" ); break; } @@ -179,7 +235,7 @@ class ApiQueryLogEvents extends ApiQueryBase { } $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->log_timestamp ) ); + $this->setContinueEnumParameter( 'continue', "$row->log_timestamp|$row->log_id" ); break; } } @@ -187,16 +243,18 @@ class ApiQueryLogEvents extends ApiQueryBase { } /** - * @param $result ApiResult - * @param $vals array - * @param $params string - * @param $type string - * @param $action string - * @param $ts - * @param $legacy bool + * @param ApiResult $result + * @param array $vals + * @param string $params + * @param string $type + * @param string $action + * @param string $ts + * @param bool $legacy * @return array */ - public static function addLogParams( $result, &$vals, $params, $type, $action, $ts, $legacy = false ) { + public static function addLogParams( $result, &$vals, $params, $type, + $action, $ts, $legacy = false + ) { switch ( $type ) { case 'move': if ( $legacy ) { @@ -284,12 +342,15 @@ class ApiQueryLogEvents extends ApiQueryBase { $result->setIndexedTagName_recursive( $logParams, 'param' ); $vals = array_merge( $vals, $logParams ); } + return $vals; } private function extractRowInfo( $row ) { $logEntry = DatabaseLogEntry::newFromRow( $row ); $vals = array(); + $anyHidden = false; + $user = $this->getUser(); if ( $this->fld_ids ) { $vals['logid'] = intval( $row->log_id ); @@ -299,18 +360,29 @@ class ApiQueryLogEvents extends ApiQueryBase { $title = Title::makeTitle( $row->log_namespace, $row->log_title ); } - if ( $this->fld_title || $this->fld_ids ) { + if ( $this->fld_title || $this->fld_ids || $this->fld_details && $row->log_params !== '' ) { if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) { $vals['actionhidden'] = ''; - } else { - if ( $this->fld_type ) { - $vals['action'] = $row->log_action; - } + $anyHidden = true; + } + if ( LogEventsList::userCan( $row, LogPage::DELETED_ACTION, $user ) ) { if ( $this->fld_title ) { ApiQueryBase::addTitleInfo( $vals, $title ); } if ( $this->fld_ids ) { $vals['pageid'] = intval( $row->page_id ); + $vals['logpage'] = intval( $row->log_page ); + } + if ( $this->fld_details && $row->log_params !== '' ) { + self::addLogParams( + $this->getResult(), + $vals, + $logEntry->getParameters(), + $logEntry->getType(), + $logEntry->getSubtype(), + $logEntry->getTimestamp(), + $logEntry->isLegacy() + ); } } } @@ -320,31 +392,17 @@ class ApiQueryLogEvents extends ApiQueryBase { $vals['action'] = $row->log_action; } - if ( $this->fld_details && $row->log_params !== '' ) { - if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) { - $vals['actionhidden'] = ''; - } else { - self::addLogParams( - $this->getResult(), - $vals, - $logEntry->getParameters(), - $logEntry->getType(), - $logEntry->getSubtype(), - $logEntry->getTimestamp(), - $logEntry->isLegacy() - ); - } - } - if ( $this->fld_user || $this->fld_userid ) { if ( LogEventsList::isDeleted( $row, LogPage::DELETED_USER ) ) { $vals['userhidden'] = ''; - } else { + $anyHidden = true; + } + if ( LogEventsList::userCan( $row, LogPage::DELETED_USER, $user ) ) { if ( $this->fld_user ) { $vals['user'] = $row->user_name === null ? $row->log_user_text : $row->user_name; } if ( $this->fld_userid ) { - $vals['userid'] = $row->log_user; + $vals['userid'] = intval( $row->log_user ); } if ( !$row->log_user ) { @@ -359,7 +417,9 @@ class ApiQueryLogEvents extends ApiQueryBase { if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->log_comment ) ) { if ( LogEventsList::isDeleted( $row, LogPage::DELETED_COMMENT ) ) { $vals['commenthidden'] = ''; - } else { + $anyHidden = true; + } + if ( LogEventsList::userCan( $row, LogPage::DELETED_COMMENT, $user ) ) { if ( $this->fld_comment ) { $vals['comment'] = $row->log_comment; } @@ -380,10 +440,25 @@ class ApiQueryLogEvents extends ApiQueryBase { } } + if ( $anyHidden && LogEventsList::isDeleted( $row, LogPage::DELETED_RESTRICTED ) ) { + $vals['suppressed'] = ''; + } + return $vals; } + /** + * @return array + */ + private function getAllowedLogActions() { + $config = $this->getConfig(); + return array_keys( array_merge( $config->get( 'LogActions' ), $config->get( 'LogActionsHandlers' ) ) ); + } + public function getCacheMode( $params ) { + if ( $this->userCanSeeRevDel() ) { + return 'private'; + } if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) { // formatComment() calls wfMessage() among other things return 'anon-public-user-private'; @@ -396,8 +471,8 @@ class ApiQueryLogEvents extends ApiQueryBase { } } - public function getAllowedParams() { - global $wgLogTypes, $wgLogActions, $wgLogActionsHandlers; + public function getAllowedParams( $flags = 0 ) { + $config = $this->getConfig(); return array( 'prop' => array( ApiBase::PARAM_ISMULTI => true, @@ -416,10 +491,13 @@ class ApiQueryLogEvents extends ApiQueryBase { ) ), 'type' => array( - ApiBase::PARAM_TYPE => $wgLogTypes + ApiBase::PARAM_TYPE => $config->get( 'LogTypes' ) ), 'action' => array( - ApiBase::PARAM_TYPE => array_keys( array_merge( $wgLogActions, $wgLogActionsHandlers ) ) + // validation on request is done in execute() + ApiBase::PARAM_TYPE => ( $flags & ApiBase::GET_VALUES_FOR_HELP ) + ? $this->getAllowedLogActions() + : null ), 'start' => array( ApiBase::PARAM_TYPE => 'timestamp' @@ -436,6 +514,9 @@ class ApiQueryLogEvents extends ApiQueryBase { ), 'user' => null, 'title' => null, + 'namespace' => array( + ApiBase::PARAM_TYPE => 'namespace' + ), 'prefix' => null, 'tag' => null, 'limit' => array( @@ -444,12 +525,14 @@ class ApiQueryLogEvents extends ApiQueryBase { ApiBase::PARAM_MIN => 1, ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 - ) + ), + 'continue' => null, ); } public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'prop' => array( 'Which properties to get', @@ -465,85 +548,25 @@ class ApiQueryLogEvents extends ApiQueryBase { ' tags - Lists tags for the event', ), 'type' => 'Filter log entries to only this type', - 'action' => "Filter log actions to only this type. Overrides {$p}type", + 'action' => array( + "Filter log actions to only this action. Overrides {$p}type", + "Wildcard actions like 'action/*' allows to specify any string for the asterisk" + ), 'start' => 'The timestamp to start enumerating from', 'end' => 'The timestamp to end enumerating', 'dir' => $this->getDirectionDescription( $p ), 'user' => 'Filter entries to those made by the given user', 'title' => 'Filter entries to those related to a page', + 'namespace' => 'Filter entries to those in the given namespace', 'prefix' => 'Filter entries that start with this prefix. Disabled in Miser Mode', 'limit' => 'How many total event entries to return', 'tag' => 'Only list event entries tagged with this tag', - ); - } - - public function getResultProperties() { - global $wgLogTypes; - return array( - 'ids' => array( - 'logid' => 'integer', - 'pageid' => 'integer' - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'type' => array( - 'type' => array( - ApiBase::PROP_TYPE => $wgLogTypes - ), - 'action' => 'string' - ), - 'details' => array( - 'actionhidden' => 'boolean' - ), - 'user' => array( - 'userhidden' => 'boolean', - 'user' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'anon' => 'boolean' - ), - 'userid' => array( - 'userhidden' => 'boolean', - 'userid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'anon' => 'boolean' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'comment' => array( - 'commenthidden' => 'boolean', - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'commenthidden' => 'boolean', - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) + 'continue' => 'When more results are available, use this to continue', ); } public function getDescription() { - return 'Get events from logs'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'param_user', 'info' => 'User name $user not found' ), - array( 'code' => 'param_title', 'info' => 'Bad title value \'title\'' ), - array( 'code' => 'param_prefix', 'info' => 'Bad title value \'prefix\'' ), - array( 'code' => 'prefixsearchdisabled', 'info' => 'Prefix search disabled in Miser Mode' ), - ) ); + return 'Get events from logs.'; } public function getExamples() { diff --git a/includes/api/ApiQueryORM.php b/includes/api/ApiQueryORM.php index a23ff06b..469b2972 100644 --- a/includes/api/ApiQueryORM.php +++ b/includes/api/ApiQueryORM.php @@ -104,7 +104,7 @@ abstract class ApiQueryORM extends ApiQueryBase { protected function getParams() { return array_filter( $this->extractRequestParams(), - function( $prop ) { + function ( $prop ) { return isset( $prop ); } ); @@ -260,5 +260,4 @@ abstract class ApiQueryORM extends ApiQueryBase { return array_merge( $this->getTable()->getFieldDescriptions(), $descriptions ); } - } diff --git a/includes/api/ApiQueryPagePropNames.php b/includes/api/ApiQueryPagePropNames.php index 08c883d8..8cd9c6cf 100644 --- a/includes/api/ApiQueryPagePropNames.php +++ b/includes/api/ApiQueryPagePropNames.php @@ -32,7 +32,7 @@ */ class ApiQueryPagePropNames extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'ppn' ); } @@ -63,7 +63,8 @@ class ApiQueryPagePropNames extends ApiQueryBase { $count = 0; foreach ( $this->select( __METHOD__ ) as $row ) { if ( ++$count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... $this->setContinueEnumParameter( 'continue', $row->pp_propname ); break; } @@ -101,7 +102,7 @@ class ApiQueryPagePropNames extends ApiQueryBase { } public function getDescription() { - return 'List all page prop names in use on the wiki'; + return 'List all page prop names in use on the wiki.'; } public function getExamples() { diff --git a/includes/api/ApiQueryPageProps.php b/includes/api/ApiQueryPageProps.php index 2de57106..e370c39f 100644 --- a/includes/api/ApiQueryPageProps.php +++ b/includes/api/ApiQueryPageProps.php @@ -33,7 +33,7 @@ class ApiQueryPageProps extends ApiQueryBase { private $params; - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'pp' ); } @@ -104,9 +104,9 @@ class ApiQueryPageProps extends ApiQueryBase { * Add page properties to an ApiResult, adding a continue * parameter if it doesn't fit. * - * @param $result ApiResult - * @param $page int - * @param $props array + * @param ApiResult $result + * @param int $page + * @param array $props * @return bool True if it fits in the result */ private function addPageProps( $result, $page, $props ) { @@ -115,6 +115,7 @@ class ApiQueryPageProps extends ApiQueryBase { if ( !$fit ) { $this->setContinueEnumParameter( 'continue', $page ); } + return $fit; } @@ -134,12 +135,13 @@ class ApiQueryPageProps extends ApiQueryBase { public function getParamDescription() { return array( 'continue' => 'When more results are available, use this to continue', - 'prop' => 'Only list these props. Useful for checking whether a certain page uses a certain page prop', + 'prop' => 'Only list these props. Useful for checking whether a ' . + 'certain page uses a certain page prop', ); } public function getDescription() { - return 'Get various properties defined in the page content'; + return 'Get various properties defined in the page content.'; } public function getExamples() { diff --git a/includes/api/ApiQueryPagesWithProp.php b/includes/api/ApiQueryPagesWithProp.php index 6f2f02e4..b6c85253 100644 --- a/includes/api/ApiQueryPagesWithProp.php +++ b/includes/api/ApiQueryPagesWithProp.php @@ -32,7 +32,7 @@ */ class ApiQueryPagesWithProp extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'pwp' ); } @@ -49,7 +49,7 @@ class ApiQueryPagesWithProp extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function run( $resultPageSet = null ) { @@ -92,7 +92,8 @@ class ApiQueryPagesWithProp extends ApiQueryGeneratorBase { $count = 0; foreach ( $this->select( __METHOD__ ) as $row ) { if ( ++$count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... $this->setContinueEnumParameter( 'continue', $row->page_id ); break; } @@ -173,13 +174,15 @@ class ApiQueryPagesWithProp extends ApiQueryGeneratorBase { } public function getDescription() { - return 'List all pages using a given page prop'; + return 'List all pages using a given page prop.'; } public function getExamples() { return array( - 'api.php?action=query&list=pageswithprop&pwppropname=displaytitle&pwpprop=ids|title|value' => 'Get first 10 pages using {{DISPLAYTITLE:}}', - 'api.php?action=query&generator=pageswithprop&gpwppropname=notoc&prop=info' => 'Get page info about first 10 pages using __NOTOC__', + 'api.php?action=query&list=pageswithprop&pwppropname=displaytitle&pwpprop=ids|title|value' + => 'Get first 10 pages using {{DISPLAYTITLE:}}', + 'api.php?action=query&generator=pageswithprop&gpwppropname=notoc&prop=info' + => 'Get page info about first 10 pages using __NOTOC__', ); } diff --git a/includes/api/ApiQueryPrefixSearch.php b/includes/api/ApiQueryPrefixSearch.php new file mode 100644 index 00000000..4abd7f0e --- /dev/null +++ b/includes/api/ApiQueryPrefixSearch.php @@ -0,0 +1,124 @@ +<?php +/** + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.23 + */ + +/** + * @ingroup API + */ +class ApiQueryPrefixSearch extends ApiQueryGeneratorBase { + public function __construct( $query, $moduleName ) { + parent::__construct( $query, $moduleName, 'ps' ); + } + + public function execute() { + $this->run(); + } + + public function executeGenerator( $resultPageSet ) { + $this->run( $resultPageSet ); + } + + /** + * @param ApiPageSet $resultPageSet + */ + private function run( $resultPageSet = null ) { + $params = $this->extractRequestParams(); + $search = $params['search']; + $limit = $params['limit']; + $namespaces = $params['namespace']; + + $searcher = new TitlePrefixSearch; + $titles = $searcher->searchWithVariants( $search, $limit, $namespaces ); + if ( $resultPageSet ) { + $resultPageSet->populateFromTitles( $titles ); + } else { + $result = $this->getResult(); + foreach ( $titles as $title ) { + if ( !$limit-- ) { + break; + } + $vals = array( + 'ns' => intval( $title->getNamespace() ), + 'title' => $title->getPrefixedText(), + ); + if ( $title->isSpecialPage() ) { + $vals['special'] = ''; + } else { + $vals['pageid'] = intval( $title->getArticleId() ); + } + $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); + if ( !$fit ) { + break; + } + } + $result->setIndexedTagName_internal( + array( 'query', $this->getModuleName() ), $this->getModulePrefix() + ); + } + } + + public function getCacheMode( $params ) { + return 'public'; + } + + public function getAllowedParams() { + return array( + 'search' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true, + ), + 'namespace' => array( + ApiBase::PARAM_DFLT => NS_MAIN, + ApiBase::PARAM_TYPE => 'namespace', + ApiBase::PARAM_ISMULTI => true, + ), + 'limit' => array( + ApiBase::PARAM_DFLT => 10, + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MIN => 1, + // Non-standard value for compatibility with action=opensearch + ApiBase::PARAM_MAX => 100, + ApiBase::PARAM_MAX2 => 200, + ), + ); + } + + public function getParamDescription() { + return array( + 'search' => 'Search string', + 'limit' => 'Maximum amount of results to return', + 'namespace' => 'Namespaces to search', + ); + } + + public function getDescription() { + return 'Perform a prefix search for page titles'; + } + + public function getExamples() { + return array( + 'api.php?action=query&list=prefixsearch&pssearch=meaning', + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Prefixsearch'; + } +} diff --git a/includes/api/ApiQueryProtectedTitles.php b/includes/api/ApiQueryProtectedTitles.php index 222ad074..4c88be7a 100644 --- a/includes/api/ApiQueryProtectedTitles.php +++ b/includes/api/ApiQueryProtectedTitles.php @@ -31,7 +31,7 @@ */ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'pt' ); } @@ -44,7 +44,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function run( $resultPageSet = null ) { @@ -63,6 +63,27 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { $this->addWhereFld( 'pt_namespace', $params['namespace'] ); $this->addWhereFld( 'pt_create_perm', $params['level'] ); + // Include in ORDER BY for uniqueness + $this->addWhereRange( 'pt_namespace', $params['dir'], null, null ); + $this->addWhereRange( 'pt_title', $params['dir'], null, null ); + + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 3 ); + $op = ( $params['dir'] === 'newer' ? '>' : '<' ); + $db = $this->getDB(); + $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) ); + $continueNs = (int)$cont[1]; + $this->dieContinueUsageIf( $continueNs != $cont[1] ); + $continueTitle = $db->addQuotes( $cont[2] ); + $this->addWhere( "pt_timestamp $op $continueTimestamp OR " . + "(pt_timestamp = $continueTimestamp AND " . + "(pt_namespace $op $continueNs OR " . + "(pt_namespace = $continueNs AND " . + "pt_title $op= $continueTitle)))" + ); + } + if ( isset( $prop['user'] ) ) { $this->addTables( 'user' ); $this->addFields( 'user_name' ); @@ -80,9 +101,12 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { $titles = array(); foreach ( $res as $row ) { - 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 ) ); + if ( ++$count > $params['limit'] ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'continue', + "$row->pt_timestamp|$row->pt_namespace|$row->pt_title" + ); break; } @@ -121,8 +145,9 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'start', - wfTimestamp( TS_ISO_8601, $row->pt_timestamp ) ); + $this->setContinueEnumParameter( 'continue', + "$row->pt_timestamp|$row->pt_namespace|$row->pt_title" + ); break; } } else { @@ -131,7 +156,10 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { } if ( is_null( $resultPageSet ) ) { - $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), $this->getModulePrefix() ); + $result->setIndexedTagName_internal( + array( 'query', $this->getModuleName() ), + $this->getModulePrefix() + ); } else { $resultPageSet->populateFromTitles( $titles ); } @@ -147,7 +175,6 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { } public function getAllowedParams() { - global $wgRestrictionLevels; return array( 'namespace' => array( ApiBase::PARAM_ISMULTI => true, @@ -155,7 +182,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { ), 'level' => array( ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => array_diff( $wgRestrictionLevels, array( '' ) ) + ApiBase::PARAM_TYPE => array_diff( $this->getConfig()->get( 'RestrictionLevels' ), array( '' ) ) ), 'limit' => array( ApiBase::PARAM_DFLT => 10, @@ -190,6 +217,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { 'level' ) ), + 'continue' => null, ); } @@ -211,48 +239,12 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { ' level - Adds the protection level', ), 'level' => 'Only list titles with these protection levels', - ); - } - - public function getResultProperties() { - global $wgRestrictionLevels; - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'user' => array( - 'user' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'userid' => 'integer' - ), - 'userid' => array( - 'userid' => 'integer' - ), - 'comment' => array( - 'comment' => 'string' - ), - 'parsedcomment' => array( - 'parsedcomment' => 'string' - ), - 'expiry' => array( - 'expiry' => 'timestamp' - ), - 'level' => array( - 'level' => array( - ApiBase::PROP_TYPE => array_diff( $wgRestrictionLevels, array( '' ) ) - ) - ) + 'continue' => 'When more results are available, use this to continue', ); } public function getDescription() { - return 'List all titles protected from creation'; + return 'List all titles protected from creation.'; } public function getExamples() { diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php index 79fe0498..5ddd9450 100644 --- a/includes/api/ApiQueryQueryPage.php +++ b/includes/api/ApiQueryQueryPage.php @@ -32,18 +32,13 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { private $qpMap; - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'qp' ); - // We need to do this to make sure $wgQueryPages is set up - // This SUCKS - global $IP; - require_once "$IP/includes/QueryPage.php"; - // Build mapping from special page names to QueryPage classes - global $wgQueryPages, $wgAPIUselessQueryPages; + $uselessQueryPages = $this->getConfig()->get( 'APIUselessQueryPages' ); $this->qpMap = array(); - foreach ( $wgQueryPages as $page ) { - if ( !in_array( $page[1], $wgAPIUselessQueryPages ) ) { + foreach ( QueryPage::getPages() as $page ) { + if ( !in_array( $page[1], $uselessQueryPages ) ) { $this->qpMap[$page[1]] = $page[0]; } } @@ -58,11 +53,9 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet */ public function run( $resultPageSet = null ) { - global $wgQueryCacheLimit; - $params = $this->extractRequestParams(); $result = $this->getResult(); @@ -82,7 +75,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { if ( $ts ) { $r['cachedtimestamp'] = wfTimestamp( TS_ISO_8601, $ts ); } - $r['maxresults'] = $wgQueryCacheLimit; + $r['maxresults'] = $this->getConfig()->get( 'QueryCacheLimit' ); } } $result->addValue( array( 'query' ), $this->getModuleName(), $r ); @@ -126,7 +119,10 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { } } if ( is_null( $resultPageSet ) ) { - $result->setIndexedTagName_internal( array( 'query', $this->getModuleName(), 'results' ), 'page' ); + $result->setIndexedTagName_internal( + array( 'query', $this->getModuleName(), 'results' ), + 'page' + ); } else { $resultPageSet->populateFromTitles( $titles ); } @@ -138,6 +134,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { if ( $qp->getRestriction() != '' ) { return 'private'; } + return 'public'; } @@ -166,46 +163,8 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - ApiBase::PROP_ROOT => array( - 'name' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => false - ), - 'disabled' => array( - ApiBase::PROP_TYPE => 'boolean', - ApiBase::PROP_NULLABLE => false - ), - 'cached' => array( - ApiBase::PROP_TYPE => 'boolean', - ApiBase::PROP_NULLABLE => false - ), - 'cachedtimestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - '' => array( - 'value' => 'string', - 'timestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ), - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { - return 'Get a list provided by a QueryPage-based special page'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'specialpage-cantexecute' ) - ) ); + return 'Get a list provided by a QueryPage-based special page.'; } public function getExamples() { diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php index 2754bdae..530557e6 100644 --- a/includes/api/ApiQueryRandom.php +++ b/includes/api/ApiQueryRandom.php @@ -30,12 +30,10 @@ * * @ingroup API */ - class ApiQueryRandom extends ApiQueryGeneratorBase { - private $pageIDs; - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'rn' ); } @@ -48,11 +46,11 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { } /** - * @param $randstr - * @param $limit - * @param $namespace - * @param $resultPageSet ApiPageSet - * @param $redirect + * @param string $randstr + * @param int $limit + * @param int $namespace + * @param ApiPageSet $resultPageSet + * @param bool $redirect * @return void */ protected function prepareQuery( $randstr, $limit, $namespace, &$resultPageSet, $redirect ) { @@ -62,7 +60,6 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { $this->addWhereFld( 'page_namespace', $namespace ); $this->addWhereRange( 'page_random', 'newer', $randstr, null ); $this->addWhereFld( 'page_is_redirect', $redirect ); - $this->addOption( 'USE INDEX', 'page_random' ); if ( is_null( $resultPageSet ) ) { $this->addFields( array( 'page_id', 'page_title', 'page_namespace' ) ); } else { @@ -71,7 +68,7 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return int */ protected function runQuery( $resultPageSet = null ) { @@ -83,8 +80,8 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { // Prevent duplicates if ( !in_array( $row->page_id, $this->pageIDs ) ) { $fit = $this->getResult()->addValue( - array( 'query', $this->getModuleName() ), - null, $this->extractRowInfo( $row ) ); + 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 @@ -102,7 +99,7 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ public function run( $resultPageSet = null ) { @@ -110,14 +107,26 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { $result = $this->getResult(); $this->pageIDs = array(); - $this->prepareQuery( wfRandom(), $params['limit'], $params['namespace'], $resultPageSet, $params['redirect'] ); + $this->prepareQuery( + wfRandom(), + $params['limit'], + $params['namespace'], + $resultPageSet, + $params['redirect'] + ); $count = $this->runQuery( $resultPageSet ); if ( $count < $params['limit'] ) { /* We got too few pages, we probably picked a high value * for page_random. We'll just take the lowest ones, see * also the comment in Title::getRandomTitle() */ - $this->prepareQuery( 0, $params['limit'] - $count, $params['namespace'], $resultPageSet, $params['redirect'] ); + $this->prepareQuery( + 0, + $params['limit'] - $count, + $params['namespace'], + $resultPageSet, + $params['redirect'] + ); $this->runQuery( $resultPageSet ); } @@ -131,6 +140,7 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { $vals = array(); $vals['id'] = intval( $row->page_id ); ApiQueryBase::addTitleInfo( $vals, $title ); + return $vals; } @@ -163,22 +173,15 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'id' => 'integer', - 'ns' => 'namespace', - 'title' => 'string' - ) - ); - } - public function getDescription() { return array( - 'Get a set of random pages', - 'NOTE: Pages are listed in a fixed sequence, only the starting point is random. This means that if, for example, "Main Page" is the first ', - ' random page on your list, "List of fictional monkeys" will *always* be second, "List of people on stamps of Vanuatu" third, etc', - 'NOTE: If the number of pages in the namespace is lower than rnlimit, you will get fewer pages. You will not get the same page twice' + 'Get a set of random pages.', + 'NOTE: Pages are listed in a fixed sequence, only the starting point is random.', + ' This means that if, for example, "Main Page" is the first random page on', + ' your list, "List of fictional monkeys" will *always* be second, "List of', + ' people on stamps of Vanuatu" third, etc.', + 'NOTE: If the number of pages in the namespace is lower than rnlimit, you will', + ' get fewer pages. You will not get the same page twice.' ); } diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 6b10bdc6..6f0c5d34 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -32,14 +32,14 @@ */ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'rc' ); } private $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false, - $fld_flags = false, $fld_timestamp = false, $fld_title = false, $fld_ids = false, - $fld_sizes = false, $fld_redirect = false, $fld_patrolled = false, $fld_loginfo = false, - $fld_tags = false, $fld_sha1 = false, $token = array(); + $fld_flags = false, $fld_timestamp = false, $fld_title = false, $fld_ids = false, + $fld_sizes = false, $fld_redirect = false, $fld_patrolled = false, $fld_loginfo = false, + $fld_tags = false, $fld_sha1 = false, $token = array(); private $tokenFunctions; @@ -47,7 +47,8 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { * Get an array mapping token names to their handler functions. * The prototype for a token function is func($pageid, $title, $rc) * it should return a token or false (permission denied) - * @return array array(tokenname => function) + * @deprecated since 1.24 + * @return array Array(tokenname => function) */ protected function getTokenFunctions() { // Don't call the hooks twice @@ -64,14 +65,16 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { 'patrol' => array( 'ApiQueryRecentChanges', 'getPatrolToken' ) ); wfRunHooks( 'APIQueryRecentChangesTokens', array( &$this->tokenFunctions ) ); + return $this->tokenFunctions; } /** - * @param $pageid - * @param $title - * @param $rc RecentChange (optional) - * @return bool|String + * @deprecated since 1.24 + * @param int $pageid + * @param Title $title + * @param RecentChange|null $rc + * @return bool|string */ public static function getPatrolToken( $pageid, $title, $rc = null ) { global $wgUser; @@ -80,32 +83,31 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { if ( $rc ) { if ( ( $wgUser->useRCPatrol() && $rc->getAttribute( 'rc_type' ) == RC_EDIT ) || - ( $wgUser->useNPPatrol() && $rc->getAttribute( 'rc_type' ) == RC_NEW ) ) - { - $validTokenUser = true; - } - } else { - if ( $wgUser->useRCPatrol() || $wgUser->useNPPatrol() ) { + ( $wgUser->useNPPatrol() && $rc->getAttribute( 'rc_type' ) == RC_NEW ) + ) { $validTokenUser = true; } + } elseif ( $wgUser->useRCPatrol() || $wgUser->useNPPatrol() ) { + $validTokenUser = true; } if ( $validTokenUser ) { // The patrol token is always the same, let's exploit that static $cachedPatrolToken = null; + if ( is_null( $cachedPatrolToken ) ) { $cachedPatrolToken = $wgUser->getEditToken( 'patrol' ); } + return $cachedPatrolToken; - } else { - return false; } + return false; } /** * Sets internal state to include the desired properties in the output. - * @param array $prop associative array of properties, only keys are used here + * @param array $prop Associative array of properties, only keys are used here */ public function initProperties( $prop ) { $this->fld_comment = isset( $prop['comment'] ); @@ -135,7 +137,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /** * Generates and outputs the result of this query based upon the provided parameters. * - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet */ public function run( $resultPageSet = null ) { $user = $this->getUser(); @@ -145,7 +147,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /* Build our basic query. Namely, something along the lines of: * SELECT * FROM recentchanges WHERE rc_timestamp > $start * AND rc_timestamp < $end AND rc_namespace = $namespace - * AND rc_deleted = 0 */ $this->addTables( 'recentchanges' ); $index = array( 'recentchanges' => 'rc_timestamp' ); // May change @@ -153,15 +154,12 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { 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' ); - } - - $timestamp = $this->getDB()->addQuotes( wfTimestamp( TS_MW, $cont[0] ) ); + $this->dieContinueUsageIf( count( $cont ) != 2 ); + $db = $this->getDB(); + $timestamp = $db->addQuotes( $db->timestamp( $cont[0] ) ); $id = intval( $cont[1] ); + $this->dieContinueUsageIf( $id != $cont[1] ); $op = $params['dir'] === 'older' ? '<' : '>'; - $this->addWhere( "rc_timestamp $op $timestamp OR " . "(rc_timestamp = $timestamp AND " . @@ -176,10 +174,13 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ) ); $this->addWhereFld( 'rc_namespace', $params['namespace'] ); - $this->addWhereFld( 'rc_deleted', 0 ); if ( !is_null( $params['type'] ) ) { - $this->addWhereFld( 'rc_type', $this->parseRCType( $params['type'] ) ); + try { + $this->addWhereFld( 'rc_type', RecentChange::parseToRCType( $params['type'] ) ); + } catch ( MWException $e ) { + ApiBase::dieDebug( __METHOD__, $e->getMessage() ); + } } if ( !is_null( $params['show'] ) ) { @@ -187,18 +188,26 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /* Check for conflicting parameters. */ if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) ) - || ( isset( $show['bot'] ) && isset( $show['!bot'] ) ) - || ( isset( $show['anon'] ) && isset( $show['!anon'] ) ) - || ( isset( $show['redirect'] ) && isset( $show['!redirect'] ) ) - || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) + || ( isset( $show['bot'] ) && isset( $show['!bot'] ) ) + || ( isset( $show['anon'] ) && isset( $show['!anon'] ) ) + || ( isset( $show['redirect'] ) && isset( $show['!redirect'] ) ) + || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) + || ( isset( $show['patrolled'] ) && isset( $show['unpatrolled'] ) ) + || ( isset( $show['!patrolled'] ) && isset( $show['unpatrolled'] ) ) ) { $this->dieUsageMsg( 'show' ); } // Check permissions - if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) { + if ( isset( $show['patrolled'] ) + || isset( $show['!patrolled'] ) + || isset( $show['unpatrolled'] ) + ) { if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) { - $this->dieUsage( 'You need the patrol right to request the patrolled flag', 'permissiondenied' ); + $this->dieUsage( + 'You need the patrol right to request the patrolled flag', + 'permissiondenied' + ); } } @@ -213,8 +222,21 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) ); $this->addWhereIf( 'page_is_redirect = 1', isset( $show['redirect'] ) ); + if ( isset( $show['unpatrolled'] ) ) { + // See ChangesList:isUnpatrolled + if ( $user->useRCPatrol() ) { + $this->addWhere( 'rc_patrolled = 0' ); + } elseif ( $user->useNPPatrol() ) { + $this->addWhere( 'rc_patrolled = 0' ); + $this->addWhereFld( 'rc_type', RC_NEW ); + } + } + // Don't throw log entries out the window here - $this->addWhereIf( 'page_is_redirect = 0 OR page_is_redirect IS NULL', isset( $show['!redirect'] ) ); + $this->addWhereIf( + 'page_is_redirect = 0 OR page_is_redirect IS NULL', + isset( $show['!redirect'] ) + ); } if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) { @@ -235,6 +257,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /* Add the fields we're concerned with to our query. */ $this->addFields( array( + 'rc_id', 'rc_timestamp', 'rc_namespace', 'rc_title', @@ -252,20 +275,26 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $this->initProperties( $prop ); if ( $this->fld_patrolled && !$user->useRCPatrol() && !$user->useNPPatrol() ) { - $this->dieUsage( 'You need the patrol right to request the patrolled flag', 'permissiondenied' ); + $this->dieUsage( + 'You need the patrol right to request the patrolled flag', + 'permissiondenied' + ); } - $this->addFields( 'rc_id' ); /* Add fields to our query if they are specified as a needed parameter. */ $this->addFieldsIf( array( 'rc_this_oldid', 'rc_last_oldid' ), $this->fld_ids ); $this->addFieldsIf( 'rc_comment', $this->fld_comment || $this->fld_parsedcomment ); - $this->addFieldsIf( 'rc_user', $this->fld_user ); - $this->addFieldsIf( 'rc_user_text', $this->fld_user || $this->fld_userid ); + $this->addFieldsIf( 'rc_user', $this->fld_user || $this->fld_userid ); + $this->addFieldsIf( 'rc_user_text', $this->fld_user ); $this->addFieldsIf( array( 'rc_minor', 'rc_type', 'rc_bot' ), $this->fld_flags ); $this->addFieldsIf( array( 'rc_old_len', 'rc_new_len' ), $this->fld_sizes ); $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled ); - $this->addFieldsIf( array( 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ), $this->fld_loginfo ); - $showRedirects = $this->fld_redirect || isset( $show['redirect'] ) || isset( $show['!redirect'] ); + $this->addFieldsIf( + array( 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ), + $this->fld_loginfo + ); + $showRedirects = $this->fld_redirect || isset( $show['redirect'] ) + || isset( $show['!redirect'] ); } if ( $this->fld_tags ) { @@ -276,13 +305,15 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { if ( $this->fld_sha1 ) { $this->addTables( 'revision' ); - $this->addJoinConds( array( 'revision' => array( 'LEFT JOIN', array( 'rc_this_oldid=rev_id' ) ) ) ); + $this->addJoinConds( array( 'revision' => array( 'LEFT JOIN', + array( 'rc_this_oldid=rev_id' ) ) ) ); $this->addFields( array( 'rev_sha1', 'rev_deleted' ) ); } if ( $params['toponly'] || $showRedirects ) { $this->addTables( 'page' ); - $this->addJoinConds( array( 'page' => array( 'LEFT JOIN', array( 'rc_namespace=page_namespace', 'rc_title=page_title' ) ) ) ); + $this->addJoinConds( array( 'page' => array( 'LEFT JOIN', + array( 'rc_namespace=page_namespace', 'rc_title=page_title' ) ) ) ); $this->addFields( 'page_is_redirect' ); if ( $params['toponly'] ) { @@ -294,7 +325,36 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $this->addTables( 'change_tag' ); $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'rc_id=ct_rc_id' ) ) ) ); $this->addWhereFld( 'ct_tag', $params['tag'] ); - $index['change_tag'] = 'change_tag_tag_id'; + } + + // Paranoia: avoid brute force searches (bug 17342) + if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) { + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $bitmask = Revision::DELETED_USER; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + } else { + $bitmask = 0; + } + if ( $bitmask ) { + $this->addWhere( $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask" ); + } + } + if ( $this->getRequest()->getCheck( 'namespace' ) ) { + // LogPage::DELETED_ACTION hides the affected page, too. + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $bitmask = LogPage::DELETED_ACTION; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED; + } else { + $bitmask = 0; + } + if ( $bitmask ) { + $this->addWhere( $this->getDB()->makeList( array( + 'rc_type != ' . RC_LOG, + $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask", + ), LIST_OR ) ); + } } $this->token = $params['token']; @@ -311,9 +371,10 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /* Iterate through the rows, adding data extracted from them to our query result. */ foreach ( $res as $row ) { - if ( ++ $count > $params['limit'] ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'continue', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) . '|' . $row->rc_id ); + if ( ++$count > $params['limit'] ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" ); break; } @@ -327,7 +388,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) . '|' . $row->rc_id ); + $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" ); break; } } else { @@ -346,69 +407,65 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /** * Extracts from a single sql row the data needed to describe one recent change. * - * @param mixed $row The row from which to extract the data. + * @param stdClass $row The row from which to extract the data. * @return array An array mapping strings (descriptors) to their respective string values. * @access public */ public function extractRowInfo( $row ) { /* Determine the title of the page that has been changed. */ $title = Title::makeTitle( $row->rc_namespace, $row->rc_title ); + $user = $this->getUser(); /* Our output data. */ $vals = array(); $type = intval( $row->rc_type ); + $vals['type'] = RecentChange::parseFromRCType( $type ); - /* Determine what kind of change this was. */ - switch ( $type ) { - case RC_EDIT: - $vals['type'] = 'edit'; - break; - case RC_NEW: - $vals['type'] = 'new'; - break; - case RC_MOVE: - $vals['type'] = 'move'; - break; - case RC_LOG: - $vals['type'] = 'log'; - break; - case RC_EXTERNAL: - $vals['type'] = 'external'; - break; - case RC_MOVE_OVER_REDIRECT: - $vals['type'] = 'move over redirect'; - break; - default: - $vals['type'] = $type; - } + $anyHidden = false; /* Create a new entry in the result for the title. */ - if ( $this->fld_title ) { - ApiQueryBase::addTitleInfo( $vals, $title ); + if ( $this->fld_title || $this->fld_ids ) { + if ( $type === RC_LOG && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) { + $vals['actionhidden'] = ''; + $anyHidden = true; + } + if ( $type !== RC_LOG || + LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) + ) { + if ( $this->fld_title ) { + ApiQueryBase::addTitleInfo( $vals, $title ); + } + if ( $this->fld_ids ) { + $vals['pageid'] = intval( $row->rc_cur_id ); + $vals['revid'] = intval( $row->rc_this_oldid ); + $vals['old_revid'] = intval( $row->rc_last_oldid ); + } + } } - /* Add ids, such as rcid, pageid, revid, and oldid to the change's info. */ if ( $this->fld_ids ) { $vals['rcid'] = intval( $row->rc_id ); - $vals['pageid'] = intval( $row->rc_cur_id ); - $vals['revid'] = intval( $row->rc_this_oldid ); - $vals['old_revid'] = intval( $row->rc_last_oldid ); } - /* Add user data and 'anon' flag, if use is anonymous. */ + /* Add user data and 'anon' flag, if user is anonymous. */ if ( $this->fld_user || $this->fld_userid ) { - - if ( $this->fld_user ) { - $vals['user'] = $row->rc_user_text; + if ( $row->rc_deleted & Revision::DELETED_USER ) { + $vals['userhidden'] = ''; + $anyHidden = true; } + if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_USER, $user ) ) { + if ( $this->fld_user ) { + $vals['user'] = $row->rc_user_text; + } - if ( $this->fld_userid ) { - $vals['userid'] = $row->rc_user; - } + if ( $this->fld_userid ) { + $vals['userid'] = $row->rc_user; + } - if ( !$row->rc_user ) { - $vals['anon'] = ''; + if ( !$row->rc_user ) { + $vals['anon'] = ''; + } } } @@ -437,12 +494,20 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } /* Add edit summary / log summary. */ - if ( $this->fld_comment && isset( $row->rc_comment ) ) { - $vals['comment'] = $row->rc_comment; - } + if ( $this->fld_comment || $this->fld_parsedcomment ) { + if ( $row->rc_deleted & Revision::DELETED_COMMENT ) { + $vals['commenthidden'] = ''; + $anyHidden = true; + } + if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_COMMENT, $user ) ) { + if ( $this->fld_comment && isset( $row->rc_comment ) ) { + $vals['comment'] = $row->rc_comment; + } - if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) { - $vals['parsedcomment'] = Linker::formatComment( $row->rc_comment, $title ); + if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) { + $vals['parsedcomment'] = Linker::formatComment( $row->rc_comment, $title ); + } + } } if ( $this->fld_redirect ) { @@ -456,19 +521,29 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $vals['patrolled'] = ''; } + if ( $this->fld_patrolled && ChangesList::isUnpatrolled( $row, $user ) ) { + $vals['unpatrolled'] = ''; + } + if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) { - $vals['logid'] = intval( $row->rc_logid ); - $vals['logtype'] = $row->rc_log_type; - $vals['logaction'] = $row->rc_log_action; - $logEntry = DatabaseLogEntry::newFromRow( (array)$row ); - ApiQueryLogEvents::addLogParams( - $this->getResult(), - $vals, - $logEntry->getParameters(), - $logEntry->getType(), - $logEntry->getSubtype(), - $logEntry->getTimestamp() - ); + if ( $row->rc_deleted & LogPage::DELETED_ACTION ) { + $vals['actionhidden'] = ''; + $anyHidden = true; + } + if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) { + $vals['logid'] = intval( $row->rc_logid ); + $vals['logtype'] = $row->rc_log_type; + $vals['logaction'] = $row->rc_log_action; + $logEntry = DatabaseLogEntry::newFromRow( (array)$row ); + ApiQueryLogEvents::addLogParams( + $this->getResult(), + $vals, + $logEntry->getParameters(), + $logEntry->getType(), + $logEntry->getSubtype(), + $logEntry->getTimestamp() + ); + } } if ( $this->fld_tags ) { @@ -482,15 +557,16 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } if ( $this->fld_sha1 && $row->rev_sha1 !== null ) { - // The RevDel check should currently never pass due to the - // rc_deleted = 0 condition in the WHERE clause, but in case that - // ever changes we check it here too. if ( $row->rev_deleted & Revision::DELETED_TEXT ) { $vals['sha1hidden'] = ''; - } elseif ( $row->rev_sha1 !== '' ) { - $vals['sha1'] = wfBaseConvert( $row->rev_sha1, 36, 16, 40 ); - } else { - $vals['sha1'] = ''; + $anyHidden = true; + } + if ( Revision::userCanBitfield( $row->rev_deleted, Revision::DELETED_TEXT, $user ) ) { + if ( $row->rev_sha1 !== '' ) { + $vals['sha1'] = wfBaseConvert( $row->rev_sha1, 36, 16, 40 ); + } else { + $vals['sha1'] = ''; + } } } @@ -507,27 +583,11 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } } - return $vals; - } - - private function parseRCType( $type ) { - if ( is_array( $type ) ) { - $retval = array(); - foreach ( $type as $t ) { - $retval[] = $this->parseRCType( $t ); - } - return $retval; - } - switch ( $type ) { - case 'edit': - return RC_EDIT; - case 'new': - return RC_NEW; - case 'log': - return RC_LOG; - case 'external': - return RC_EXTERNAL; + if ( $anyHidden && ( $row->rc_deleted & Revision::DELETED_RESTRICTED ) ) { + $vals['suppressed'] = ''; } + + return $vals; } public function getCacheMode( $params ) { @@ -541,10 +601,14 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { if ( isset( $params['token'] ) ) { return 'private'; } + if ( $this->userCanSeeRevDel() ) { + return 'private'; + } if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) { // formatComment() calls wfMessage() among other things return 'anon-public-user-private'; } + return 'public'; } @@ -595,6 +659,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ) ), 'token' => array( + ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ), ApiBase::PARAM_ISMULTI => true ), @@ -610,7 +675,8 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { 'redirect', '!redirect', 'patrolled', - '!patrolled' + '!patrolled', + 'unpatrolled' ) ), 'limit' => array( @@ -636,6 +702,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'start' => 'The timestamp to start enumerating from', 'end' => 'The timestamp to end enumerating', @@ -655,7 +722,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ' ids - Adds the page ID, recent changes ID and the new and old revision ID', ' sizes - Adds the new and old page length in bytes', ' redirect - Tags edit if page is a redirect', - ' patrolled - Tags edits that have been patrolled', + ' patrolled - Tags patrollable edits as being patrolled or unpatrolled', ' loginfo - Adds log information (logid, logtype, etc) to log entries', ' tags - Lists tags for the entry', ' sha1 - Adds the content checksum for entries associated with a revision', @@ -673,117 +740,8 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ); } - public function getResultProperties() { - global $wgLogTypes; - $props = array( - '' => array( - 'type' => array( - ApiBase::PROP_TYPE => array( - 'edit', - 'new', - 'move', - 'log', - 'move over redirect' - ) - ) - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string', - 'new_ns' => array( - ApiBase::PROP_TYPE => 'namespace', - ApiBase::PROP_NULLABLE => true - ), - 'new_title' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'ids' => array( - 'rcid' => 'integer', - 'pageid' => 'integer', - 'revid' => 'integer', - 'old_revid' => 'integer' - ), - 'user' => array( - 'user' => 'string', - 'anon' => 'boolean' - ), - 'userid' => array( - 'userid' => 'integer', - 'anon' => 'boolean' - ), - 'flags' => array( - 'bot' => 'boolean', - 'new' => 'boolean', - 'minor' => 'boolean' - ), - 'sizes' => array( - 'oldlen' => 'integer', - 'newlen' => 'integer' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'comment' => array( - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'redirect' => array( - 'redirect' => 'boolean' - ), - 'patrolled' => array( - 'patrolled' => 'boolean' - ), - 'loginfo' => array( - 'logid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'logtype' => array( - ApiBase::PROP_TYPE => $wgLogTypes, - ApiBase::PROP_NULLABLE => true - ), - 'logaction' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'sha1' => array( - 'sha1' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'sha1hidden' => array( - ApiBase::PROP_TYPE => 'boolean', - ApiBase::PROP_NULLABLE => true - ), - ), - ); - - self::addTokenProperties( $props, $this->getTokenFunctions() ); - - return $props; - } - public function getDescription() { - return 'Enumerate recent changes'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'show' ), - array( 'code' => 'permissiondenied', 'info' => 'You need the patrol right to request the patrolled flag' ), - array( 'code' => 'user-excludeuser', 'info' => 'user and excludeuser cannot be used together' ), - ) ); + return 'Enumerate recent changes.'; } public function getExamples() { diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index 415288ef..da4ec195 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -25,9 +25,10 @@ */ /** - * A query action to enumerate revisions of a given page, or show top revisions of multiple pages. - * Various pieces of information may be shown - flags, comments, and the actual wiki markup of the rev. - * In the enumeration mode, ranges of revisions may be requested and filtered. + * A query action to enumerate revisions of a given page, or show top revisions + * of multiple pages. Various pieces of information may be shown - flags, + * comments, and the actual wiki markup of the rev. In the enumeration mode, + * ranges of revisions may be requested and filtered. * * @ingroup API */ @@ -36,16 +37,18 @@ class ApiQueryRevisions extends ApiQueryBase { private $diffto, $difftotext, $expandTemplates, $generateXML, $section, $token, $parseContent, $contentFormat; - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'rv' ); } - private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false, $fld_sha1 = false, - $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false, - $fld_content = false, $fld_tags = false, $fld_contentmodel = false; + private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, + $fld_size = false, $fld_sha1 = false, $fld_comment = false, + $fld_parsedcomment = false, $fld_user = false, $fld_userid = false, + $fld_content = false, $fld_tags = false, $fld_contentmodel = false; private $tokenFunctions; + /** @deprecated since 1.24 */ protected function getTokenFunctions() { // tokenname => function // function prototype is func($pageid, $title, $rev) @@ -65,20 +68,23 @@ class ApiQueryRevisions extends ApiQueryBase { 'rollback' => array( 'ApiQueryRevisions', 'getRollbackToken' ) ); wfRunHooks( 'APIQueryRevisionsTokens', array( &$this->tokenFunctions ) ); + return $this->tokenFunctions; } /** - * @param $pageid - * @param $title Title - * @param $rev Revision - * @return bool|String + * @deprecated since 1.24 + * @param int $pageid + * @param Title $title + * @param Revision $rev + * @return bool|string */ public static function getRollbackToken( $pageid, $title, $rev ) { global $wgUser; if ( !$wgUser->isAllowed( 'rollback' ) ) { return false; } + return $wgUser->getEditToken( array( $title->getPrefixedText(), $rev->getUserText() ) ); } @@ -91,9 +97,9 @@ class ApiQueryRevisions extends ApiQueryBase { // Enumerating revisions on multiple pages make it extremely // difficult to manage continuations and require additional SQL indexes $enumRevMode = ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) || - !is_null( $params['limit'] ) || !is_null( $params['startid'] ) || - !is_null( $params['endid'] ) || $params['dir'] === 'newer' || - !is_null( $params['start'] ) || !is_null( $params['end'] ) ); + !is_null( $params['limit'] ) || !is_null( $params['startid'] ) || + !is_null( $params['endid'] ) || $params['dir'] === 'newer' || + !is_null( $params['start'] ) || !is_null( $params['end'] ) ); $pageSet = $this->getPageSet(); $pageCount = $pageSet->getGoodTitleCount(); @@ -105,11 +111,20 @@ class ApiQueryRevisions extends ApiQueryBase { } if ( $revCount > 0 && $enumRevMode ) { - $this->dieUsage( 'The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).', 'revids' ); + $this->dieUsage( + 'The revids= parameter may not be used with the list options ' . + '(limit, startid, endid, dirNewer, start, end).', + 'revids' + ); } 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' ); + $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['difftotext'] ) ) { @@ -119,8 +134,12 @@ class ApiQueryRevisions extends ApiQueryBase { $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' ); + && $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 @@ -130,7 +149,7 @@ class ApiQueryRevisions extends ApiQueryBase { if ( !$difftoRev ) { $this->dieUsageMsg( array( 'nosuchrevid', $params['diffto'] ) ); } - if ( $difftoRev->isDeleted( Revision::DELETED_TEXT ) ) { + if ( !$difftoRev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) { $this->setWarning( "Couldn't diff to r{$difftoRev->getID()}: content is hidden" ); $params['diffto'] = null; } @@ -163,9 +182,6 @@ class ApiQueryRevisions extends ApiQueryBase { $this->contentFormat = $params['contentformat']; } - // Possible indexes used - $index = array(); - $userMax = ( $this->fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 ); $botMax = ( $this->fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 ); $limit = $params['limit']; @@ -181,18 +197,21 @@ class ApiQueryRevisions extends ApiQueryBase { if ( isset( $prop['tags'] ) ) { $this->fld_tags = true; $this->addTables( 'tag_summary' ); - $this->addJoinConds( array( 'tag_summary' => array( 'LEFT JOIN', array( 'rev_id=ts_rev_id' ) ) ) ); + $this->addJoinConds( + array( 'tag_summary' => array( 'LEFT JOIN', array( 'rev_id=ts_rev_id' ) ) ) + ); $this->addFields( 'ts_tags' ); } if ( !is_null( $params['tag'] ) ) { $this->addTables( 'change_tag' ); - $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) ) ); + $this->addJoinConds( + array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) ) + ); $this->addWhereFld( 'ct_tag', $params['tag'] ); - $index['change_tag'] = 'change_tag_tag_id'; } - if ( isset( $prop['content'] ) || !is_null( $this->difftotext ) ) { + if ( isset( $prop['content'] ) || !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) { // For each page we will request, the user must have read rights for that page $user = $this->getUser(); /** @var $title Title */ @@ -299,7 +318,16 @@ class ApiQueryRevisions extends ApiQueryBase { } if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) { // Paranoia: avoid brute force searches (bug 17342) - $this->addWhere( $db->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ); + if ( !$this->getUser()->isAllowed( 'deletedhistory' ) ) { + $bitmask = Revision::DELETED_USER; + } elseif ( !$this->getUser()->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + } else { + $bitmask = 0; + } + if ( $bitmask ) { + $this->addWhere( $db->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" ); + } } } elseif ( $revCount > 0 ) { $max = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; @@ -349,7 +377,7 @@ class ApiQueryRevisions extends ApiQueryBase { $this->addOption( 'ORDER BY', array( 'rev_page', 'rev_id' - )); + ) ); // assumption testing -- we should never get more then $pageCount rows. $limit = $pageCount; @@ -358,14 +386,14 @@ class ApiQueryRevisions extends ApiQueryBase { } $this->addOption( 'LIMIT', $limit + 1 ); - $this->addOption( 'USE INDEX', $index ); $count = 0; $res = $this->select( __METHOD__ ); foreach ( $res as $row ) { - if ( ++ $count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... if ( !$enumRevMode ) { ApiBase::dieDebug( __METHOD__, 'Got more rows then expected' ); // bug report } @@ -391,7 +419,9 @@ class ApiQueryRevisions extends ApiQueryBase { private function extractRowInfo( $row ) { $revision = new Revision( $row ); $title = $revision->getTitle(); + $user = $this->getUser(); $vals = array(); + $anyHidden = false; if ( $this->fld_ids ) { $vals['revid'] = intval( $revision->getId() ); @@ -408,11 +438,13 @@ class ApiQueryRevisions extends ApiQueryBase { if ( $this->fld_user || $this->fld_userid ) { if ( $revision->isDeleted( Revision::DELETED_USER ) ) { $vals['userhidden'] = ''; - } else { + $anyHidden = true; + } + if ( $revision->userCan( Revision::DELETED_USER, $user ) ) { if ( $this->fld_user ) { - $vals['user'] = $revision->getUserText(); + $vals['user'] = $revision->getRawUserText(); } - $userid = $revision->getUser(); + $userid = $revision->getRawUser(); if ( !$userid ) { $vals['anon'] = ''; } @@ -435,14 +467,18 @@ class ApiQueryRevisions extends ApiQueryBase { } } - if ( $this->fld_sha1 && !$revision->isDeleted( Revision::DELETED_TEXT ) ) { - if ( $revision->getSha1() != '' ) { - $vals['sha1'] = wfBaseConvert( $revision->getSha1(), 36, 16, 40 ); - } else { - $vals['sha1'] = ''; + if ( $this->fld_sha1 ) { + if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) { + $vals['sha1hidden'] = ''; + $anyHidden = true; + } + if ( $revision->userCan( Revision::DELETED_TEXT, $user ) ) { + if ( $revision->getSha1() != '' ) { + $vals['sha1'] = wfBaseConvert( $revision->getSha1(), 36, 16, 40 ); + } else { + $vals['sha1'] = ''; + } } - } elseif ( $this->fld_sha1 ) { - $vals['sha1hidden'] = ''; } if ( $this->fld_contentmodel ) { @@ -452,8 +488,10 @@ class ApiQueryRevisions extends ApiQueryBase { if ( $this->fld_comment || $this->fld_parsedcomment ) { if ( $revision->isDeleted( Revision::DELETED_COMMENT ) ) { $vals['commenthidden'] = ''; - } else { - $comment = $revision->getComment(); + $anyHidden = true; + } + if ( $revision->userCan( Revision::DELETED_COMMENT, $user ) ) { + $comment = $revision->getRawComment(); if ( $this->fld_comment ) { $vals['comment'] = $comment; @@ -490,25 +528,38 @@ class ApiQueryRevisions extends ApiQueryBase { $content = null; global $wgParser; if ( $this->fld_content || !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) { - $content = $revision->getContent(); + $content = $revision->getContent( Revision::FOR_THIS_USER, $this->getUser() ); // Expand templates after getting section content because // template-added sections don't count and Parser::preprocess() // will have less input if ( $content && $this->section !== false ) { $content = $content->getSection( $this->section, false ); if ( !$content ) { - $this->dieUsage( "There is no section {$this->section} in r" . $revision->getId(), 'nosuchsection' ); + $this->dieUsage( + "There is no section {$this->section} in r" . $revision->getId(), + 'nosuchsection' + ); } } + if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) { + $vals['texthidden'] = ''; + $anyHidden = true; + } elseif ( !$content ) { + $vals['textmissing'] = ''; + } } - if ( $this->fld_content && $content && !$revision->isDeleted( Revision::DELETED_TEXT ) ) { + if ( $this->fld_content && $content ) { $text = null; if ( $this->generateXML ) { if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { $t = $content->getNativeData(); # note: don't set $text - $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS ); + $wgParser->startExternalParse( + $title, + ParserOptions::newFromContext( $this->getContext() ), + OT_PREPROCESS + ); $dom = $wgParser->preprocessToDom( $t ); if ( is_callable( array( $dom, 'saveXML' ) ) ) { $xml = $dom->saveXML(); @@ -518,8 +569,8 @@ class ApiQueryRevisions extends ApiQueryBase { $vals['parsetree'] = $xml; } else { $this->setWarning( "Conversion to XML is supported for wikitext only, " . - $title->getPrefixedDBkey() . - " uses content model " . $content->getModel() ); + $title->getPrefixedDBkey() . + " uses content model " . $content->getModel() ); } } @@ -528,7 +579,11 @@ class ApiQueryRevisions extends ApiQueryBase { if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { $text = $content->getNativeData(); - $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) ); + $text = $wgParser->preprocess( + $text, + $title, + ParserOptions::newFromContext( $this->getContext() ) + ); } else { $this->setWarning( "Template expansion is supported for wikitext only, " . $title->getPrefixedDBkey() . @@ -538,7 +593,11 @@ class ApiQueryRevisions extends ApiQueryBase { } } if ( $this->parseContent ) { - $po = $content->getParserOutput( $title, $revision->getId(), ParserOptions::newFromContext( $this->getContext() ) ); + $po = $content->getParserOutput( + $title, + $revision->getId(), + ParserOptions::newFromContext( $this->getContext() ) + ); $text = $po->getText(); } @@ -550,7 +609,7 @@ class ApiQueryRevisions extends ApiQueryBase { $name = $title->getPrefixedDBkey(); $this->dieUsage( "The requested format {$this->contentFormat} is not supported " . - "for content model $model used by $name", 'badformat' ); + "for content model $model used by $name", 'badformat' ); } $text = $content->serialize( $format ); @@ -564,21 +623,12 @@ class ApiQueryRevisions extends ApiQueryBase { if ( $text !== false ) { ApiResult::setContent( $vals, $text ); } - } elseif ( $this->fld_content ) { - if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) { - $vals['texthidden'] = ''; - } else { - $vals['textmissing'] = ''; - } } - if ( !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) { - global $wgAPIMaxUncachedDiffs; + if ( $content && ( !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) ) { static $n = 0; // Number of uncached diffs we've had - if ( is_null( $content ) ) { - $vals['textmissing'] = ''; - } elseif ( $n < $wgAPIMaxUncachedDiffs ) { + if ( $n < $this->getConfig()->get( 'APIMaxUncachedDiffs' ) ) { $vals['diff'] = array(); $context = new DerivativeContext( $this->getContext() ); $context->setTitle( $title ); @@ -588,15 +638,21 @@ class ApiQueryRevisions extends ApiQueryBase { $model = $title->getContentModel(); if ( $this->contentFormat - && !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat ) ) { + && !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat ) + ) { $name = $title->getPrefixedDBkey(); $this->dieUsage( "The requested format {$this->contentFormat} is not supported for " . - "content model $model used by $name", 'badformat' ); + "content model $model used by $name", 'badformat' ); } - $difftocontent = ContentHandler::makeContent( $this->difftotext, $title, $model, $this->contentFormat ); + $difftocontent = ContentHandler::makeContent( + $this->difftotext, + $title, + $model, + $this->contentFormat + ); $engine = $handler->createDifferenceEngine( $context ); $engine->setContent( $content, $difftocontent ); @@ -614,6 +670,11 @@ class ApiQueryRevisions extends ApiQueryBase { $vals['diff']['notcached'] = ''; } } + + if ( $anyHidden && $revision->isDeleted( Revision::DELETED_RESTRICTED ) ) { + $vals['suppressed'] = ''; + } + return $vals; } @@ -621,10 +682,14 @@ class ApiQueryRevisions extends ApiQueryBase { if ( isset( $params['token'] ) ) { return 'private'; } + if ( $this->userCanSeeRevDel() ) { + return 'private'; + } if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) { // formatComment() calls wfMessage() among other things return 'anon-public-user-private'; } + return 'public'; } @@ -685,6 +750,7 @@ class ApiQueryRevisions extends ApiQueryBase { 'parse' => false, 'section' => null, 'token' => array( + ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ), ApiBase::PARAM_ISMULTI => true ), @@ -700,6 +766,7 @@ class ApiQueryRevisions extends ApiQueryBase { public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'prop' => array( 'Which properties to get for each revision:', @@ -733,120 +800,47 @@ class ApiQueryRevisions extends ApiQueryBase { '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' ), - 'difftotext' => array( 'Text to diff each revision to. Only diffs a limited number of revisions.', - "Overrides {$p}diffto. If {$p}section is set, only that section will be diffed against this text" ), + 'difftotext' => array( + 'Text to diff each revision to. Only diffs a limited number of revisions.', + "Overrides {$p}diffto. If {$p}section is set, only that section will be", + 'diffed against this text', + ), 'tag' => 'Only list revisions tagged with this tag', 'contentformat' => 'Serialization format used for difftotext and expected for output of content', ); } - public function getResultProperties() { - $props = array( - '' => array(), - 'ids' => array( - 'revid' => 'integer', - 'parentid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'flags' => array( - 'minor' => 'boolean' - ), - 'user' => array( - 'userhidden' => 'boolean', - 'user' => 'string', - 'anon' => 'boolean' - ), - 'userid' => array( - 'userhidden' => 'boolean', - 'userid' => 'integer', - 'anon' => 'boolean' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'size' => array( - 'size' => 'integer' - ), - 'sha1' => array( - 'sha1' => 'string' - ), - 'comment' => array( - 'commenthidden' => 'boolean', - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'commenthidden' => 'boolean', - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'content' => array( - '*' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'texthidden' => 'boolean', - 'textmissing' => 'boolean', - ), - 'contentmodel' => array( - 'contentmodel' => 'string' - ), - ); - - self::addTokenProperties( $props, $this->getTokenFunctions() ); - - return $props; - } - public function getDescription() { return array( - 'Get revision information', + 'Get revision information.', 'May be used in several ways:', - ' 1) Get data about a set of pages (last revision), by setting titles or pageids parameter', - ' 2) Get revisions for one given page, by using titles/pageids with start/end/limit params', - ' 3) Get data about a set of revisions by setting their IDs with revids parameter', - 'All parameters marked as (enum) may only be used with a single page (#2)' + ' 1) Get data about a set of pages (last revision), by setting titles or pageids parameter.', + ' 2) Get revisions for one given page, by using titles/pageids with start/end/limit params.', + ' 3) Get data about a set of revisions by setting their IDs with revids parameter.', + 'All parameters marked as (enum) may only be used with a single page (#2).' ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'nosuchrevid', 'diffto' ), - array( 'code' => 'revids', 'info' => 'The revids= parameter may not be used with the list options ' - . '(limit, startid, endid, dirNewer, start, end).' ), - array( 'code' => 'multpages', 'info' => '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.' ), - array( 'code' => 'diffto', 'info' => 'rvdiffto must be set to a non-negative number, "prev", "next" or "cur"' ), - array( 'code' => 'badparams', 'info' => 'start and startid cannot be used together' ), - array( 'code' => 'badparams', 'info' => 'end and endid cannot be used together' ), - array( 'code' => 'badparams', 'info' => 'user and excludeuser cannot be used together' ), - array( 'code' => 'nosuchsection', 'info' => 'There is no section section in rID' ), - array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied ' - . ' to the page\'s content model' ), - ) ); - } - public function getExamples() { return array( 'Get data with content for the last revision of titles "API" and "Main Page"', - ' api.php?action=query&prop=revisions&titles=API|Main%20Page&rvprop=timestamp|user|comment|content', + ' api.php?action=query&prop=revisions&titles=API|Main%20Page&' . + 'rvprop=timestamp|user|comment|content', 'Get last 5 revisions of the "Main Page"', - ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&' . + 'rvprop=timestamp|user|comment', 'Get first 5 revisions of the "Main Page"', - ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvdir=newer', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&' . + 'rvprop=timestamp|user|comment&rvdir=newer', 'Get first 5 revisions of the "Main Page" made after 2006-05-01', - ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvdir=newer&rvstart=20060501000000', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&' . + 'rvprop=timestamp|user|comment&rvdir=newer&rvstart=20060501000000', 'Get first 5 revisions of the "Main Page" that were not made made by anonymous user "127.0.0.1"', - ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvexcludeuser=127.0.0.1', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&' . + 'rvprop=timestamp|user|comment&rvexcludeuser=127.0.0.1', 'Get first 5 revisions of the "Main Page" that were made by the user "MediaWiki default"', - ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&rvprop=timestamp|user|comment&rvuser=MediaWiki%20default', + ' api.php?action=query&prop=revisions&titles=Main%20Page&rvlimit=5&' . + 'rvprop=timestamp|user|comment&rvuser=MediaWiki%20default', ); } diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index 36b55979..b7dcd0ed 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -39,7 +39,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { */ const BACKEND_NULL_PARAM = 'database-backed'; - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'sr' ); } @@ -52,7 +52,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function run( $resultPageSet = null ) { @@ -63,15 +63,25 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $limit = $params['limit']; $query = $params['search']; $what = $params['what']; + $interwiki = $params['interwiki']; $searchInfo = array_flip( $params['info'] ); $prop = array_flip( $params['prop'] ); + // Deprecated parameters + if ( isset( $prop['hasrelated'] ) ) { + $this->logFeatureUsage( 'action=search&srprop=hasrelated' ); + $this->setWarning( 'srprop=hasrelated has been deprecated' ); + } + if ( isset( $prop['score'] ) ) { + $this->logFeatureUsage( 'action=search&srprop=score' ); + $this->setWarning( 'srprop=score has been deprecated' ); + } + // Create search engine instance and set options $search = isset( $params['backend'] ) && $params['backend'] != self::BACKEND_NULL_PARAM ? SearchEngine::create( $params['backend'] ) : SearchEngine::create(); $search->setLimitOffset( $limit + 1, $params['offset'] ); $search->setNamespaces( $params['namespace'] ); - $search->showRedirects = $params['redirects']; $query = $search->transformSearchTerm( $query ); $query = $search->replacePrefixes( $query ); @@ -112,12 +122,12 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $totalhits = $matches->getTotalHits(); if ( $totalhits !== null ) { $apiResult->addValue( array( 'query', 'searchinfo' ), - 'totalhits', $totalhits ); + 'totalhits', $totalhits ); } } if ( isset( $searchInfo['suggestion'] ) && $matches->hasSuggestion() ) { $apiResult->addValue( array( 'query', 'searchinfo' ), - 'suggestion', $matches->getSuggestionQuery() ); + 'suggestion', $matches->getSuggestionQuery() ); } // Add the search results to the result @@ -127,8 +137,9 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $result = $matches->next(); while ( $result ) { - if ( ++ $count > $limit ) { - // We've reached the one extra which shows that there are additional items to be had. Stop here... + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are + // additional items to be had. Stop here... $this->setContinueEnumParameter( 'offset', $params['offset'] + $params['limit'] ); break; } @@ -156,9 +167,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { if ( isset( $prop['timestamp'] ) ) { $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $result->getTimestamp() ); } - if ( !is_null( $result->getScore() ) && isset( $prop['score'] ) ) { - $vals['score'] = $result->getScore(); - } if ( isset( $prop['titlesnippet'] ) ) { $vals['titlesnippet'] = $result->getTitleSnippet( $terms ); } @@ -178,13 +186,10 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $vals['sectionsnippet'] = $result->getSectionSnippet(); } } - if ( isset( $prop['hasrelated'] ) && $result->hasRelated() ) { - $vals['hasrelated'] = ''; - } // Add item to results and see whether it fits $fit = $apiResult->addValue( array( 'query', $this->getModuleName() ), - null, $vals ); + null, $vals ); if ( !$fit ) { $this->setContinueEnumParameter( 'offset', $params['offset'] + $count - 1 ); break; @@ -196,10 +201,55 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $result = $matches->next(); } + $hasInterwikiResults = false; + if ( $interwiki && $resultPageSet === null && $matches->hasInterwikiResults() ) { + $matches = $matches->getInterwikiResults(); + $hasInterwikiResults = true; + + // Include number of results if requested + if ( isset( $searchInfo['totalhits'] ) ) { + $totalhits = $matches->getTotalHits(); + if ( $totalhits !== null ) { + $apiResult->addValue( array( 'query', 'interwikisearchinfo' ), + 'totalhits', $totalhits ); + } + } + + $result = $matches->next(); + while ( $result ) { + $title = $result->getTitle(); + $vals = array( + 'namespace' => $result->getInterwikiNamespaceText(), + 'title' => $title->getText(), + 'url' => $title->getFullUrl(), + ); + + // Add item to results and see whether it fits + $fit = $apiResult->addValue( + array( 'query', 'interwiki' . $this->getModuleName(), $result->getInterwikiPrefix() ), + null, + $vals + ); + + if ( !$fit ) { + // We hit the limit. We can't really provide any meaningful + // pagination info so just bail out + break; + } + + $result = $matches->next(); + } + } + if ( is_null( $resultPageSet ) ) { $apiResult->setIndexedTagName_internal( array( - 'query', $this->getModuleName() - ), 'p' ); + 'query', $this->getModuleName() + ), 'p' ); + if ( $hasInterwikiResults ) { + $apiResult->setIndexedTagName_internal( array( + 'query', 'interwiki' . $this->getModuleName() + ), 'p' ); + } } else { $resultPageSet->populateFromTitles( $titles ); } @@ -210,8 +260,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } public function getAllowedParams() { - global $wgSearchType; - $params = array( 'search' => array( ApiBase::PARAM_TYPE => 'string', @@ -255,7 +303,6 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { ), ApiBase::PARAM_ISMULTI => true, ), - 'redirects' => false, 'offset' => 0, 'limit' => array( ApiBase::PARAM_DFLT => 10, @@ -263,7 +310,8 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { ApiBase::PARAM_MIN => 1, ApiBase::PARAM_MAX => ApiBase::LIMIT_SML1, ApiBase::PARAM_MAX2 => ApiBase::LIMIT_SML2 - ) + ), + 'interwiki' => false, ); $alternatives = SearchEngine::getSearchTypes(); @@ -272,7 +320,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $alternatives[0] = self::BACKEND_NULL_PARAM; } $params['backend'] = array( - ApiBase::PARAM_DFLT => $wgSearchType, + ApiBase::PARAM_DFLT => $this->getConfig()->get( 'SearchType' ), ApiBase::PARAM_TYPE => $alternatives, ); } @@ -291,18 +339,18 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { ' size - Adds the size of the page in bytes', ' wordcount - Adds the word count of the page', ' timestamp - Adds the timestamp of when the page was last edited', - ' score - Adds the score (if any) from the search engine', + ' score - DEPRECATED and IGNORED', ' snippet - Adds a parsed snippet of the page', ' titlesnippet - Adds a parsed snippet of the page title', ' redirectsnippet - Adds a parsed snippet of the redirect title', ' redirecttitle - Adds the title of the matching redirect', ' sectionsnippet - Adds a parsed snippet of the matching section title', ' sectiontitle - Adds the title of the matching section', - ' hasrelated - Indicates whether a related search is available', + ' hasrelated - DEPRECATED and IGNORED', ), - 'redirects' => 'Include redirect pages in the search', 'offset' => 'Use this value to continue paging (return by query)', - 'limit' => 'How many total pages to return' + 'limit' => 'How many total pages to return', + 'interwiki' => 'Include interwiki results in the search, if available' ); if ( count( SearchEngine::getSearchTypes() ) > 1 ) { @@ -312,73 +360,8 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { return $descriptions; } - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'snippet' => array( - 'snippet' => 'string' - ), - 'size' => array( - 'size' => 'integer' - ), - 'wordcount' => array( - 'wordcount' => 'integer' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'score' => array( - 'score' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'titlesnippet' => array( - 'titlesnippet' => 'string' - ), - 'redirecttitle' => array( - 'redirecttitle' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'redirectsnippet' => array( - 'redirectsnippet' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'sectiontitle' => array( - 'sectiontitle' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'sectionsnippet' => array( - 'sectionsnippet' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'hasrelated' => array( - 'hasrelated' => 'boolean' - ) - ); - } - public function getDescription() { - return 'Perform a full text search'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'search-text-disabled', 'info' => 'text search is disabled' ), - array( 'code' => 'search-title-disabled', 'info' => 'title search is disabled' ), - array( 'code' => 'search-error', 'info' => 'search error has occurred' ), - ) ); + return 'Perform a full text search.'; } public function getExamples() { diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index a7767062..311438fd 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -31,7 +31,7 @@ */ class ApiQuerySiteinfo extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'si' ); } @@ -78,6 +78,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { case 'rightsinfo': $fit = $this->appendRightsInfo( $p ); break; + case 'restrictions': + $fit = $this->appendRestrictions( $p ); + break; case 'languages': $fit = $this->appendLanguages( $p ); break; @@ -99,6 +102,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { case 'protocols': $fit = $this->appendProtocols( $p ); break; + case 'defaultoptions': + $fit = $this->appendDefaultOptions( $p ); + break; default: ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" ); } @@ -106,7 +112,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { // Abuse siprop as a query-continue parameter // and set it to all unprocessed props $this->setContinueEnumParameter( 'prop', implode( '|', - array_diff( $params['prop'], $done ) ) ); + array_diff( $params['prop'], $done ) ) ); break; } $done[] = $p; @@ -114,29 +120,37 @@ class ApiQuerySiteinfo extends ApiQueryBase { } protected function appendGeneralInfo( $property ) { - global $wgContLang, - $wgDisableLangConversion, - $wgDisableTitleConversion; + global $wgContLang; + + $config = $this->getConfig(); $data = array(); $mainPage = Title::newMainPage(); $data['mainpage'] = $mainPage->getPrefixedText(); $data['base'] = wfExpandUrl( $mainPage->getFullURL(), PROTO_CURRENT ); - $data['sitename'] = $GLOBALS['wgSitename']; - $data['logo'] = $GLOBALS['wgLogo']; - $data['generator'] = "MediaWiki {$GLOBALS['wgVersion']}"; - $data['phpversion'] = phpversion(); + $data['sitename'] = $config->get( 'Sitename' ); + + // wgLogo can either be a relative or an absolute path + // make sure we always return an absolute path + $data['logo'] = wfExpandUrl( $config->get( 'Logo' ), PROTO_RELATIVE ); + + $data['generator'] = "MediaWiki {$config->get( 'Version' )}"; + + $data['phpversion'] = PHP_VERSION; $data['phpsapi'] = PHP_SAPI; - $data['dbtype'] = $GLOBALS['wgDBtype']; + if ( defined( 'HHVM_VERSION' ) ) { + $data['hhvmversion'] = HHVM_VERSION; + } + $data['dbtype'] = $config->get( 'DBtype' ); $data['dbversion'] = $this->getDB()->getServerVersion(); $allowFrom = array( '' ); $allowException = true; - if ( !$GLOBALS['wgAllowExternalImages'] ) { - if ( $GLOBALS['wgEnableImageWhitelist'] ) { + if ( !$config->get( 'AllowExternalImages' ) ) { + if ( $config->get( 'EnableImageWhitelist' ) ) { $data['imagewhitelistenabled'] = ''; } - $allowFrom = $GLOBALS['wgAllowExternalImagesFrom']; + $allowFrom = $config->get( 'AllowExternalImagesFrom' ); $allowException = !empty( $allowFrom ); } if ( $allowException ) { @@ -144,17 +158,21 @@ class ApiQuerySiteinfo extends ApiQueryBase { $this->getResult()->setIndexedTagName( $data['externalimages'], 'prefix' ); } - if ( !$wgDisableLangConversion ) { + if ( !$config->get( 'DisableLangConversion' ) ) { $data['langconversion'] = ''; } - if ( !$wgDisableTitleConversion ) { + if ( !$config->get( 'DisableTitleConversion' ) ) { $data['titleconversion'] = ''; } if ( $wgContLang->linkPrefixExtension() ) { - $data['linkprefix'] = wfMessage( 'linkprefix' )->inContentLanguage()->text(); + $linkPrefixCharset = $wgContLang->linkPrefixCharset(); + $data['linkprefixcharset'] = $linkPrefixCharset; + // For backwards compatibility + $data['linkprefix'] = "/^((?>.*[^$linkPrefixCharset]|))(.+)$/sDu"; } else { + $data['linkprefixcharset'] = ''; $data['linkprefix'] = ''; } @@ -165,24 +183,22 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['linktrail'] = ''; } - $git = SpecialVersion::getGitHeadSha1( $GLOBALS['IP'] ); + global $IP; + $git = SpecialVersion::getGitHeadSha1( $IP ); if ( $git ) { $data['git-hash'] = $git; + $data['git-branch'] = + SpecialVersion::getGitCurrentBranch( $GLOBALS['IP'] ); } else { - $svn = SpecialVersion::getSvnRevision( $GLOBALS['IP'] ); + $svn = SpecialVersion::getSvnRevision( $IP ); if ( $svn ) { $data['rev'] = $svn; } } // 'case-insensitive' option is reserved for future - $data['case'] = $GLOBALS['wgCapitalLinks'] ? 'first-letter' : 'case-sensitive'; - - if ( isset( $GLOBALS['wgRightsCode'] ) ) { - $data['rightscode'] = $GLOBALS['wgRightsCode']; - } - $data['rights'] = $GLOBALS['wgRightsText']; - $data['lang'] = $GLOBALS['wgLanguageCode']; + $data['case'] = $config->get( 'CapitalLinks' ) ? 'first-letter' : 'case-sensitive'; + $data['lang'] = $config->get( 'LanguageCode' ); $fallbacks = array(); foreach ( $wgContLang->getFallbackLanguages() as $code ) { @@ -194,7 +210,10 @@ class ApiQuerySiteinfo extends ApiQueryBase { if ( $wgContLang->hasVariants() ) { $variants = array(); foreach ( $wgContLang->getVariants() as $code ) { - $variants[] = array( 'code' => $code ); + $variants[] = array( + 'code' => $code, + 'name' => $wgContLang->getVariantname( $code ), + ); } $data['variants'] = $variants; $this->getResult()->setIndexedTagName( $data['variants'], 'lang' ); @@ -209,12 +228,12 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['readonly'] = ''; $data['readonlyreason'] = wfReadOnlyReason(); } - if ( $GLOBALS['wgEnableWriteAPI'] ) { + if ( $config->get( 'EnableWriteAPI' ) ) { $data['writeapi'] = ''; } - $tz = $GLOBALS['wgLocaltimezone']; - $offset = $GLOBALS['wgLocalTZoffset']; + $tz = $config->get( 'Localtimezone' ); + $offset = $config->get( 'LocalTZoffset' ); if ( is_null( $tz ) ) { $tz = 'UTC'; $offset = 0; @@ -223,20 +242,36 @@ class ApiQuerySiteinfo extends ApiQueryBase { } $data['timezone'] = $tz; $data['timeoffset'] = intval( $offset ); - $data['articlepath'] = $GLOBALS['wgArticlePath']; - $data['scriptpath'] = $GLOBALS['wgScriptPath']; - $data['script'] = $GLOBALS['wgScript']; - $data['variantarticlepath'] = $GLOBALS['wgVariantArticlePath']; - $data['server'] = $GLOBALS['wgServer']; + $data['articlepath'] = $config->get( 'ArticlePath' ); + $data['scriptpath'] = $config->get( 'ScriptPath' ); + $data['script'] = $config->get( 'Script' ); + $data['variantarticlepath'] = $config->get( 'VariantArticlePath' ); + $data['server'] = $config->get( 'Server' ); + $data['servername'] = $config->get( 'ServerName' ); $data['wikiid'] = wfWikiID(); $data['time'] = wfTimestamp( TS_ISO_8601, time() ); - if ( $GLOBALS['wgMiserMode'] ) { + if ( $config->get( 'MiserMode' ) ) { $data['misermode'] = ''; } $data['maxuploadsize'] = UploadBase::getMaxUploadSize(); + $data['thumblimits'] = $config->get( 'ThumbLimits' ); + $this->getResult()->setIndexedTagName( $data['thumblimits'], 'limit' ); + $data['imagelimits'] = array(); + $this->getResult()->setIndexedTagName( $data['imagelimits'], 'limit' ); + foreach ( $config->get( 'ImageLimits' ) as $k => $limit ) { + $data['imagelimits'][$k] = array( 'width' => $limit[0], 'height' => $limit[1] ); + } + + $favicon = $config->get( 'Favicon' ); + if ( !empty( $favicon ) ) { + // wgFavicon can either be a relative or an absolute path + // make sure we always return an absolute path + $data['favicon'] = wfExpandUrl( $favicon, PROTO_RELATIVE ); + } + wfRunHooks( 'APIQuerySiteInfoGeneralInfo', array( $this, &$data ) ); return $this->getResult()->addValue( 'query', $property, $data ); @@ -276,12 +311,14 @@ class ApiQuerySiteinfo extends ApiQueryBase { } $this->getResult()->setIndexedTagName( $data, 'ns' ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendNamespaceAliases( $property ) { - global $wgNamespaceAliases, $wgContLang; - $aliases = array_merge( $wgNamespaceAliases, $wgContLang->getNamespaceAliases() ); + global $wgContLang; + $aliases = array_merge( $this->getConfig()->get( 'NamespaceAliases' ), + $wgContLang->getNamespaceAliases() ); $namespaces = $wgContLang->getNamespaces(); $data = array(); foreach ( $aliases as $title => $ns ) { @@ -299,6 +336,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { sort( $data ); $this->getResult()->setIndexedTagName( $data, 'ns' ); + return $this->getResult()->addValue( 'query', $property, $data ); } @@ -306,7 +344,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { global $wgContLang; $data = array(); $aliases = $wgContLang->getSpecialPageAliases(); - foreach ( SpecialPageFactory::getList() as $specialpage => $stuff ) { + foreach ( SpecialPageFactory::getNames() as $specialpage ) { if ( isset( $aliases[$specialpage] ) ) { $arr = array( 'realname' => $specialpage, 'aliases' => $aliases[$specialpage] ); $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' ); @@ -314,6 +352,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { } } $this->getResult()->setIndexedTagName( $data, 'specialpage' ); + return $this->getResult()->addValue( 'query', $property, $data ); } @@ -330,6 +369,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data[] = $arr; } $this->getResult()->setIndexedTagName( $data, 'magicword' ); + return $this->getResult()->addValue( 'query', $property, $data ); } @@ -348,20 +388,45 @@ class ApiQuerySiteinfo extends ApiQueryBase { $langNames = Language::fetchLanguageNames( $langCode ); $getPrefixes = Interwiki::getAllPrefixes( $local ); + $extraLangPrefixes = $this->getConfig()->get( 'ExtraInterlanguageLinkPrefixes' ); + $localInterwikis = $this->getConfig()->get( 'LocalInterwikis' ); $data = array(); foreach ( $getPrefixes as $row ) { $prefix = $row['iw_prefix']; $val = array(); $val['prefix'] = $prefix; - if ( $row['iw_local'] == '1' ) { + if ( isset( $row['iw_local'] ) && $row['iw_local'] == '1' ) { $val['local'] = ''; } - // $val['trans'] = intval( $row['iw_trans'] ); // should this be exposed? + if ( isset( $row['iw_trans'] ) && $row['iw_trans'] == '1' ) { + $val['trans'] = ''; + } + if ( isset( $langNames[$prefix] ) ) { $val['language'] = $langNames[$prefix]; } + if ( in_array( $prefix, $localInterwikis ) ) { + $val['localinterwiki'] = ''; + } + if ( in_array( $prefix, $extraLangPrefixes ) ) { + $val['extralanglink'] = ''; + + $linktext = wfMessage( "interlanguage-link-$prefix" ); + if ( !$linktext->isDisabled() ) { + $val['linktext'] = $linktext->text(); + } + + $sitename = wfMessage( "interlanguage-link-sitename-$prefix" ); + if ( !$sitename->isDisabled() ) { + $val['sitename'] = $sitename->text(); + } + } + $val['url'] = wfExpandUrl( $row['iw_url'], PROTO_CURRENT ); + if ( substr( $row['iw_url'], 0, 2 ) == '//' ) { + $val['protorel'] = ''; + } if ( isset( $row['iw_wikiid'] ) ) { $val['wikiid'] = $row['iw_wikiid']; } @@ -373,16 +438,20 @@ class ApiQuerySiteinfo extends ApiQueryBase { } $this->getResult()->setIndexedTagName( $data, 'iw' ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendDbReplLagInfo( $property, $includeAll ) { - global $wgShowHostnames; $data = array(); $lb = wfGetLB(); + $showHostnames = $this->getConfig()->get( 'ShowHostnames' ); if ( $includeAll ) { - if ( !$wgShowHostnames ) { - $this->dieUsage( 'Cannot view all servers info unless $wgShowHostnames is true', 'includeAllDenied' ); + if ( !$showHostnames ) { + $this->dieUsage( + 'Cannot view all servers info unless $wgShowHostnames is true', + 'includeAllDenied' + ); } $lags = $lb->getLagTimes(); @@ -395,7 +464,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { } else { list( , $lag, $index ) = $lb->getMaxLag(); $data[] = array( - 'host' => $wgShowHostnames + 'host' => $showHostnames ? $lb->getServerName( $index ) : '', 'lag' => intval( $lag ) @@ -404,15 +473,15 @@ class ApiQuerySiteinfo extends ApiQueryBase { $result = $this->getResult(); $result->setIndexedTagName( $data, 'db' ); + 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() ); - if ( !$wgDisableCounters ) { + if ( !$this->getConfig()->get( 'DisableCounters' ) ) { $data['views'] = intval( SiteStats::views() ); } $data['edits'] = intval( SiteStats::edits() ); @@ -428,40 +497,42 @@ class ApiQuerySiteinfo extends ApiQueryBase { } protected function appendUserGroups( $property, $numberInGroup ) { - global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups; - global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; + $config = $this->getConfig(); $data = array(); $result = $this->getResult(); - foreach ( $wgGroupPermissions as $group => $permissions ) { + $allGroups = User::getAllGroups(); + foreach ( $config->get( 'GroupPermissions' ) as $group => $permissions ) { $arr = array( 'name' => $group, 'rights' => array_keys( $permissions, true ), ); if ( $numberInGroup ) { - global $wgAutopromote; + $autopromote = $config->get( 'Autopromote' ); if ( $group == 'user' ) { $arr['number'] = SiteStats::users(); - // '*' and autopromote groups have no size - } elseif ( $group !== '*' && !isset( $wgAutopromote[$group] ) ) { + } elseif ( $group !== '*' && !isset( $autopromote[$group] ) ) { $arr['number'] = SiteStats::numberInGroup( $group ); } } $groupArr = array( - 'add' => $wgAddGroups, - 'remove' => $wgRemoveGroups, - 'add-self' => $wgGroupsAddToSelf, - 'remove-self' => $wgGroupsRemoveFromSelf + 'add' => $config->get( 'AddGroups' ), + 'remove' => $config->get( 'RemoveGroups' ), + 'add-self' => $config->get( 'GroupsAddToSelf' ), + 'remove-self' => $config->get( 'GroupsRemoveFromSelf' ) ); foreach ( $groupArr as $type => $rights ) { if ( isset( $rights[$group] ) ) { - $arr[$type] = $rights[$group]; - $result->setIndexedTagName( $arr[$type], 'group' ); + $groups = array_intersect( $rights[$group], $allGroups ); + if ( $groups ) { + $arr[$type] = $groups; + $result->setIndexedTagName( $arr[$type], 'group' ); + } } } @@ -470,30 +541,32 @@ class ApiQuerySiteinfo extends ApiQueryBase { } $result->setIndexedTagName( $data, 'group' ); + return $result->addValue( 'query', $property, $data ); } protected function appendFileExtensions( $property ) { - global $wgFileExtensions; - $data = array(); - foreach ( array_unique( $wgFileExtensions ) as $ext ) { + foreach ( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) as $ext ) { $data[] = array( 'ext' => $ext ); } $this->getResult()->setIndexedTagName( $data, 'fe' ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendExtensions( $property ) { - global $wgExtensionCredits; $data = array(); - foreach ( $wgExtensionCredits as $type => $extensions ) { + foreach ( $this->getConfig()->get( 'ExtensionCredits' ) as $type => $extensions ) { foreach ( $extensions as $ext ) { $ret = array(); $ret['type'] = $type; if ( isset( $ext['name'] ) ) { $ret['name'] = $ext['name']; } + if ( isset( $ext['namemsg'] ) ) { + $ret['namemsg'] = $ext['namemsg']; + } if ( isset( $ext['description'] ) ) { $ret['description'] = $ext['description']; } @@ -515,26 +588,63 @@ class ApiQuerySiteinfo extends ApiQueryBase { $ret['url'] = $ext['url']; } if ( isset( $ext['version'] ) ) { - $ret['version'] = $ext['version']; + $ret['version'] = $ext['version']; } elseif ( isset( $ext['svn-revision'] ) && preg_match( '/\$(?:Rev|LastChangedRevision|Revision): *(\d+)/', - $ext['svn-revision'], $m ) ) - { - $ret['version'] = 'r' . $m[1]; + $ext['svn-revision'], $m ) + ) { + $ret['version'] = 'r' . $m[1]; + } + if ( isset( $ext['path'] ) ) { + $extensionPath = dirname( $ext['path'] ); + $gitInfo = new GitInfo( $extensionPath ); + $vcsVersion = $gitInfo->getHeadSHA1(); + if ( $vcsVersion !== false ) { + $ret['vcs-system'] = 'git'; + $ret['vcs-version'] = $vcsVersion; + $ret['vcs-url'] = $gitInfo->getHeadViewUrl(); + $vcsDate = $gitInfo->getHeadCommitDate(); + if ( $vcsDate !== false ) { + $ret['vcs-date'] = wfTimestamp( TS_ISO_8601, $vcsDate ); + } + } else { + $svnInfo = SpecialVersion::getSvnInfo( $extensionPath ); + if ( $svnInfo !== false ) { + $ret['vcs-system'] = 'svn'; + $ret['vcs-version'] = $svnInfo['checkout-rev']; + $ret['vcs-url'] = isset( $svnInfo['viewvc-url'] ) ? $svnInfo['viewvc-url'] : ''; + } + } + + if ( SpecialVersion::getExtLicenseFileName( $extensionPath ) ) { + $ret['license-name'] = isset( $ext['license-name'] ) ? $ext['license-name'] : ''; + $ret['license'] = SpecialPage::getTitleFor( + 'Version', + "License/{$ext['name']}" + )->getLinkURL(); + } + + if ( SpecialVersion::getExtAuthorsFileName( $extensionPath ) ) { + $ret['credits'] = SpecialPage::getTitleFor( + 'Version', + "Credits/{$ext['name']}" + )->getLinkURL(); + } } $data[] = $ret; } } $this->getResult()->setIndexedTagName( $data, 'ext' ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendRightsInfo( $property ) { - global $wgRightsPage, $wgRightsUrl, $wgRightsText; - $title = Title::newFromText( $wgRightsPage ); - $url = $title ? wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ) : $wgRightsUrl; - $text = $wgRightsText; + $config = $this->getConfig(); + $title = Title::newFromText( $config->get( 'RightsPage' ) ); + $url = $title ? wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ) : $config->get( 'RightsUrl' ); + $text = $config->get( 'RightsText' ); if ( !$text && $title ) { $text = $title->getPrefixedText(); } @@ -547,6 +657,23 @@ class ApiQuerySiteinfo extends ApiQueryBase { return $this->getResult()->addValue( 'query', $property, $data ); } + protected function appendRestrictions( $property ) { + $config = $this->getConfig(); + $data = array( + 'types' => $config->get( 'RestrictionTypes' ), + 'levels' => $config->get( 'RestrictionLevels' ), + 'cascadinglevels' => $config->get( 'CascadingRestrictionLevels' ), + 'semiprotectedlevels' => $config->get( 'SemiprotectedRestrictionLevels' ), + ); + + $this->getResult()->setIndexedTagName( $data['types'], 'type' ); + $this->getResult()->setIndexedTagName( $data['levels'], 'level' ); + $this->getResult()->setIndexedTagName( $data['cascadinglevels'], 'level' ); + $this->getResult()->setIndexedTagName( $data['semiprotectedlevels'], 'level' ); + + return $this->getResult()->addValue( 'query', $property, $data ); + } + public function appendLanguages( $property ) { $params = $this->extractRequestParams(); $langCode = isset( $params['inlanguagecode'] ) ? $params['inlanguagecode'] : ''; @@ -560,17 +687,28 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data[] = $lang; } $this->getResult()->setIndexedTagName( $data, 'lang' ); + return $this->getResult()->addValue( 'query', $property, $data ); } public function appendSkins( $property ) { $data = array(); - $usable = Skin::getUsableSkins(); + $allowed = Skin::getAllowedSkins(); $default = Skin::normalizeKey( 'default' ); foreach ( Skin::getSkinNames() as $name => $displayName ) { + $msg = $this->msg( "skinname-{$name}" ); + $code = $this->getParameter( 'inlanguagecode' ); + if ( $code && Language::isValidCode( $code ) ) { + $msg->inLanguage( $code ); + } else { + $msg->inContentLanguage(); + } + if ( $msg->exists() ) { + $displayName = $msg->text(); + } $skin = array( 'code' => $name ); ApiResult::setContent( $skin, $displayName ); - if ( !isset( $usable[$name] ) ) { + if ( !isset( $allowed[$name] ) ) { $skin['unusable'] = ''; } if ( $name === $default ) { @@ -579,6 +717,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data[] = $skin; } $this->getResult()->setIndexedTagName( $data, 'skin' ); + return $this->getResult()->addValue( 'query', $property, $data ); } @@ -587,6 +726,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $wgParser->firstCallInit(); $tags = array_map( array( $this, 'formatParserTags' ), $wgParser->getTags() ); $this->getResult()->setIndexedTagName( $tags, 't' ); + return $this->getResult()->addValue( 'query', $property, $tags ); } @@ -595,37 +735,43 @@ class ApiQuerySiteinfo extends ApiQueryBase { $wgParser->firstCallInit(); $hooks = $wgParser->getFunctionHooks(); $this->getResult()->setIndexedTagName( $hooks, 'h' ); + return $this->getResult()->addValue( 'query', $property, $hooks ); } public function appendVariables( $property ) { $variables = MagicWord::getVariableIDs(); $this->getResult()->setIndexedTagName( $variables, 'v' ); + return $this->getResult()->addValue( 'query', $property, $variables ); } public function appendProtocols( $property ) { - global $wgUrlProtocols; // Make a copy of the global so we don't try to set the _element key of it - bug 45130 - $protocols = array_values( $wgUrlProtocols ); + $protocols = array_values( $this->getConfig()->get( 'UrlProtocols' ) ); $this->getResult()->setIndexedTagName( $protocols, 'p' ); + return $this->getResult()->addValue( 'query', $property, $protocols ); } + public function appendDefaultOptions( $property ) { + return $this->getResult()->addValue( 'query', $property, User::getDefaultOptions() ); + } + private function formatParserTags( $item ) { return "<{$item}>"; } public function appendSubscribedHooks( $property ) { - global $wgHooks; - $myWgHooks = $wgHooks; + $hooks = $this->getConfig()->get( 'Hooks' ); + $myWgHooks = $hooks; ksort( $myWgHooks ); $data = array(); - foreach ( $myWgHooks as $hook => $hooks ) { + foreach ( $myWgHooks as $name => $subscribers ) { $arr = array( - 'name' => $hook, - 'subscribers' => array_map( array( 'SpecialVersion', 'arrayToString' ), $hooks ), + 'name' => $name, + 'subscribers' => array_map( array( 'SpecialVersion', 'arrayToString' ), $subscribers ), ); $this->getResult()->setIndexedTagName( $arr['subscribers'], 's' ); @@ -633,10 +779,20 @@ class ApiQuerySiteinfo extends ApiQueryBase { } $this->getResult()->setIndexedTagName( $data, 'hook' ); + return $this->getResult()->addValue( 'query', $property, $data ); } public function getCacheMode( $params ) { + // Messages for $wgExtraInterlanguageLinkPrefixes depend on user language + if ( + count( $this->getConfig()->get( 'ExtraInterlanguageLinkPrefixes' ) ) && + !is_null( $params['prop'] ) && + in_array( 'interwikimap', $params['prop'] ) + ) { + return 'anon-public-user-private'; + } + return 'public'; } @@ -658,6 +814,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { 'extensions', 'fileextensions', 'rightsinfo', + 'restrictions', 'languages', 'skins', 'extensiontags', @@ -665,6 +822,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { 'showhooks', 'variables', 'protocols', + 'defaultoptions', ) ), 'filteriw' => array( @@ -681,6 +839,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'prop' => array( 'Which sysinfo properties to get:', @@ -690,38 +849,35 @@ class ApiQuerySiteinfo extends ApiQueryBase { ' specialpagealiases - List of special page aliases', ' magicwords - List of magic words and their aliases', ' statistics - Returns site statistics', - " interwikimap - Returns interwiki map " . + ' interwikimap - Returns interwiki map ' . "(optionally filtered, (optionally localised by using {$p}inlanguagecode))", ' 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', - " languages - Returns a list of languages MediaWiki supports" . + ' restrictions - Returns information on available restriction (protection) types', + ' languages - Returns a list of languages MediaWiki supports ' . "(optionally localised by using {$p}inlanguagecode)", - ' skins - Returns a list of all enabled skins', + ' skins - Returns a list of all enabled skins ' . + "(optionally localised by using {$p}inlanguagecode, otherwise in content language)", ' extensiontags - Returns a list of parser extension tags', ' functionhooks - Returns a list of parser function hooks', ' showhooks - Returns a list of all subscribed hooks (contents of $wgHooks)', ' variables - Returns a list of variable IDs', ' protocols - Returns a list of protocols that are allowed in external links.', + ' defaultoptions - Returns the default values for user preferences.', ), 'filteriw' => 'Return only local or only nonlocal entries of the interwiki map', 'showalldb' => 'List all database servers, not just the one lagging the most', 'numberingroup' => 'Lists the number of users in user groups', - 'inlanguagecode' => 'Language code for localised language names (best effort, use CLDR extension)', + 'inlanguagecode' => 'Language code for localised language names ' . + '(best effort, use CLDR extension) and skin names', ); } public function getDescription() { - return 'Return general information about the site'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( array( - 'code' => 'includeAllDenied', - 'info' => 'Cannot view all servers info unless $wgShowHostnames is true' - ), ) ); + return 'Return general information about the site.'; } public function getExamples() { diff --git a/includes/api/ApiQueryStashImageInfo.php b/includes/api/ApiQueryStashImageInfo.php index 6899375a..db928560 100644 --- a/includes/api/ApiQueryStashImageInfo.php +++ b/includes/api/ApiQueryStashImageInfo.php @@ -27,7 +27,7 @@ */ class ApiQueryStashImageInfo extends ApiQueryImageInfo { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'sii' ); } @@ -47,6 +47,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { // Alias sessionkey to filekey, but give an existing filekey precedence. if ( !$params['filekey'] && $params['sessionkey'] ) { + $this->logFeatureUsage( 'prop=stashimageinfo&siisessionkey' ); $params['filekey'] = $params['sessionkey']; } @@ -60,7 +61,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { $result->addValue( array( 'query', $this->getModuleName() ), null, $imageInfo ); $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), $modulePrefix ); } - //TODO: update exception handling here to understand current getFile exceptions + // @todo Update exception handling here to understand current getFile exceptions } catch ( UploadStashNotAvailableException $e ) { $this->dieUsage( "Session not available: " . $e->getMessage(), "nosession" ); } catch ( UploadStashFileNotFoundException $e ) { @@ -72,7 +73,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { private $propertyFilter = array( 'user', 'userid', 'comment', 'parsedcomment', - 'mediatype', 'archivename', + 'mediatype', 'archivename', 'uploadwarning', ); public function getAllowedParams() { @@ -108,10 +109,11 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { /** * Return the API documentation for the parameters. - * @return Array parameter documentation. + * @return array Parameter documentation. */ public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'prop' => self::getPropertyDescriptions( $this->propertyFilter, $p ), 'filekey' => 'Key that identifies a previous upload that was stashed temporarily.', @@ -123,19 +125,15 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { ); } - public function getResultProperties() { - return ApiQueryImageInfo::getResultPropertiesFiltered( $this->propertyFilter ); - } - public function getDescription() { - return 'Returns image information for stashed images'; + return 'Returns image information for stashed images.'; } public function getExamples() { return array( 'api.php?action=query&prop=stashimageinfo&siifilekey=124sd34rsdf567', - 'api.php?action=query&prop=stashimageinfo&siifilekey=b34edoe3|bceffd4&siiurlwidth=120&siiprop=url', + 'api.php?action=query&prop=stashimageinfo&siifilekey=b34edoe3|bceffd4&' . + 'siiurlwidth=120&siiprop=url', ); } - } diff --git a/includes/api/ApiQueryTags.php b/includes/api/ApiQueryTags.php index 732df9a4..31845648 100644 --- a/includes/api/ApiQueryTags.php +++ b/includes/api/ApiQueryTags.php @@ -38,9 +38,9 @@ class ApiQueryTags extends ApiQueryBase { private $limit; private $fld_displayname = false, $fld_description = false, - $fld_hitcount = false; + $fld_hitcount = false; - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'tg' ); } @@ -97,6 +97,7 @@ class ApiQueryTags extends ApiQueryBase { if ( ++$count > $this->limit ) { $this->setContinueEnumParameter( 'continue', $tagName ); + return false; } @@ -121,6 +122,7 @@ class ApiQueryTags extends ApiQueryBase { $fit = $this->result->addValue( array( 'query', $this->getModuleName() ), null, $tag ); if ( !$fit ) { $this->setContinueEnumParameter( 'continue', $tagName ); + return false; } @@ -168,25 +170,8 @@ class ApiQueryTags extends ApiQueryBase { ); } - public function getResultProperties() { - return array( - '' => array( - 'name' => 'string' - ), - 'displayname' => array( - 'displayname' => 'string' - ), - 'description' => array( - 'description' => 'string' - ), - 'hitcount' => array( - 'hitcount' => 'integer' - ) - ); - } - public function getDescription() { - return 'List change tags'; + return 'List change tags.'; } public function getExamples() { diff --git a/includes/api/ApiQueryTokens.php b/includes/api/ApiQueryTokens.php new file mode 100644 index 00000000..ba9c9377 --- /dev/null +++ b/includes/api/ApiQueryTokens.php @@ -0,0 +1,104 @@ +<?php +/** + * Module to fetch tokens via action=query&meta=tokens + * + * Created on August 8, 2014 + * + * Copyright © 2014 Brad Jorsch bjorsch@wikimedia.org + * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.24 + */ + +/** + * Module to fetch tokens via action=query&meta=tokens + * + * @ingroup API + * @since 1.24 + */ +class ApiQueryTokens extends ApiQueryBase { + + public function execute() { + $params = $this->extractRequestParams(); + $res = array(); + + if ( $this->getMain()->getRequest()->getVal( 'callback' ) !== null ) { + $this->setWarning( 'Tokens may not be obtained when using a callback' ); + return; + } + + $salts = self::getTokenTypeSalts(); + foreach ( $params['type'] as $type ) { + $salt = $salts[$type]; + $val = $this->getUser()->getEditToken( $salt, $this->getRequest() ); + $res[$type . 'token'] = $val; + } + + $this->getResult()->addValue( 'query', $this->getModuleName(), $res ); + } + + public static function getTokenTypeSalts() { + static $salts = null; + if ( !$salts ) { + wfProfileIn( __METHOD__ ); + $salts = array( + 'csrf' => '', + 'watch' => 'watch', + 'patrol' => 'patrol', + 'rollback' => 'rollback', + 'userrights' => 'userrights', + ); + wfRunHooks( 'ApiQueryTokensRegisterTypes', array( &$salts ) ); + ksort( $salts ); + wfProfileOut( __METHOD__ ); + } + + return $salts; + } + + public function getAllowedParams() { + return array( + 'type' => array( + ApiBase::PARAM_DFLT => 'csrf', + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => array_keys( self::getTokenTypeSalts() ), + ), + ); + } + + public function getParamDescription() { + return array( + 'type' => 'Type of token(s) to request' + ); + } + + public function getDescription() { + return 'Gets tokens for data-modifying actions.'; + } + + protected function getExamples() { + return array( + 'api.php?action=query&meta=tokens' => 'Retrieve a csrf token (the default)', + 'api.php?action=query&meta=tokens&type=watch|patrol' => 'Retrieve a watch token and a patrol token' + ); + } + + public function getCacheMode( $params ) { + return 'private'; + } +} diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index 9a9be7b2..4b167b8b 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -31,14 +31,14 @@ */ class ApiQueryContributions extends ApiQueryBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'uc' ); } private $params, $prefixMode, $userprefix, $multiUserMode, $usernames, $parentLens; private $fld_ids = false, $fld_title = false, $fld_timestamp = false, - $fld_comment = false, $fld_parsedcomment = false, $fld_flags = false, - $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false; + $fld_comment = false, $fld_parsedcomment = false, $fld_flags = false, + $fld_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false; public function execute() { // Parse some parameters @@ -56,6 +56,11 @@ class ApiQueryContributions extends ApiQueryBase { $this->fld_patrolled = isset( $prop['patrolled'] ); $this->fld_tags = isset( $prop['tags'] ); + // Most of this code will use the 'contributions' group DB, which can map to slaves + // with extra user based indexes or partioning by user. The additional metadata + // queries should use a regular slave since the lookup pattern is not all by user. + $dbSecondary = $this->getDB(); // any random slave + // TODO: if the query is going only against the revision table, should this be done? $this->selectNamedDB( 'contributions', DB_SLAVE, 'contributions' ); @@ -90,7 +95,7 @@ class ApiQueryContributions extends ApiQueryBase { $revIds[] = $row->rev_parent_id; } } - $this->parentLens = Revision::getParentLengths( $this->getDB(), $revIds ); + $this->parentLens = Revision::getParentLengths( $dbSecondary, $revIds ); $res->rewind(); // reset } @@ -100,36 +105,32 @@ class ApiQueryContributions extends ApiQueryBase { // Fetch each row foreach ( $res as $row ) { - if ( ++ $count > $limit ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... - if ( $this->multiUserMode ) { - $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) ); - } else { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->rev_timestamp ) ); - } + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) ); break; } $vals = $this->extractRowInfo( $row ); $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 ) ); - } + $this->setContinueEnumParameter( 'continue', $this->continueStr( $row ) ); break; } } - $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'item' ); + $this->getResult()->setIndexedTagName_internal( + array( 'query', $this->getModuleName() ), + 'item' + ); } /** * Validate the 'user' parameter and set the value to compare * against `revision`.`rev_user_text` * - * @param $user string + * @param string $user */ private function prepareUsername( $user ) { if ( !is_null( $user ) && $user !== '' ) { @@ -158,26 +159,53 @@ class ApiQueryContributions extends ApiQueryBase { $this->addWhere( 'page_id=rev_page' ); // Handle continue parameter - if ( $this->multiUserMode && !is_null( $this->params['continue'] ) ) { + if ( !is_null( $this->params['continue'] ) ) { $continue = explode( '|', $this->params['continue'] ); - $this->dieContinueUsageIf( count( $continue ) != 2 ); $db = $this->getDB(); - $encUser = $db->addQuotes( $continue[0] ); - $encTS = $db->addQuotes( $db->timestamp( $continue[1] ) ); + if ( $this->multiUserMode ) { + $this->dieContinueUsageIf( count( $continue ) != 3 ); + $encUser = $db->addQuotes( array_shift( $continue ) ); + } else { + $this->dieContinueUsageIf( count( $continue ) != 2 ); + } + $encTS = $db->addQuotes( $db->timestamp( $continue[0] ) ); + $encId = (int)$continue[1]; + $this->dieContinueUsageIf( $encId != $continue[1] ); $op = ( $this->params['dir'] == 'older' ? '<' : '>' ); - $this->addWhere( - "rev_user_text $op $encUser OR " . - "(rev_user_text = $encUser AND " . - "rev_timestamp $op= $encTS)" - ); + if ( $this->multiUserMode ) { + $this->addWhere( + "rev_user_text $op $encUser OR " . + "(rev_user_text = $encUser AND " . + "(rev_timestamp $op $encTS OR " . + "(rev_timestamp = $encTS AND " . + "rev_id $op= $encId)))" + ); + } else { + $this->addWhere( + "rev_timestamp $op $encTS OR " . + "(rev_timestamp = $encTS AND " . + "rev_id $op= $encId)" + ); + } } - if ( !$user->isAllowed( 'hideuser' ) ) { - $this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ); + // Don't include any revisions where we're not supposed to be able to + // see the username. + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $bitmask = Revision::DELETED_USER; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + } else { + $bitmask = 0; } + if ( $bitmask ) { + $this->addWhere( $this->getDB()->bitAnd( 'rev_deleted', $bitmask ) . " != $bitmask" ); + } + // We only want pages by the specified users. if ( $this->prefixMode ) { - $this->addWhere( 'rev_user_text' . $this->getDB()->buildLike( $this->userprefix, $this->getDB()->anyString() ) ); + $this->addWhere( 'rev_user_text' . + $this->getDB()->buildLike( $this->userprefix, $this->getDB()->anyString() ) ); } else { $this->addWhereFld( 'rev_user_text', $this->usernames ); } @@ -189,13 +217,24 @@ class ApiQueryContributions extends ApiQueryBase { } $this->addTimestampWhereRange( 'rev_timestamp', $this->params['dir'], $this->params['start'], $this->params['end'] ); + // Include in ORDER BY for uniqueness + $this->addWhereRange( 'rev_id', $this->params['dir'], null, null ); + $this->addWhereFld( 'page_namespace', $this->params['namespace'] ); $show = $this->params['show']; + if ( $this->params['toponly'] ) { // deprecated/old param + $this->logFeatureUsage( 'list=usercontribs&uctoponly' ); + $show[] = 'top'; + } if ( !is_null( $show ) ) { $show = array_flip( $show ); + if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) ) - || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) ) { + || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) + || ( isset( $show['top'] ) && isset( $show['!top'] ) ) + || ( isset( $show['new'] ) && isset( $show['!new'] ) ) + ) { $this->dieUsageMsg( 'show' ); } @@ -203,6 +242,10 @@ class ApiQueryContributions extends ApiQueryBase { $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->addWhereIf( 'rev_id != page_latest', isset( $show['!top'] ) ); + $this->addWhereIf( 'rev_id = page_latest', isset( $show['top'] ) ); + $this->addWhereIf( 'rev_parent_id != 0', isset( $show['!new'] ) ); + $this->addWhereIf( 'rev_parent_id = 0', isset( $show['new'] ) ); } $this->addOption( 'LIMIT', $this->params['limit'] + 1 ); $index = array( 'revision' => 'usertext_timestamp' ); @@ -211,6 +254,7 @@ class ApiQueryContributions extends ApiQueryBase { // ns+title checks if the user has access rights for this page // user_text is necessary if multiple users were specified $this->addFields( array( + 'rev_id', 'rev_timestamp', 'page_namespace', 'page_title', @@ -220,9 +264,13 @@ class ApiQueryContributions extends ApiQueryBase { ) ); if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) || - $this->fld_patrolled ) { + $this->fld_patrolled + ) { if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) { - $this->dieUsage( 'You need the patrol right to request the patrolled flag', 'permissiondenied' ); + $this->dieUsage( + 'You need the patrol right to request the patrolled flag', + 'permissiondenied' + ); } // Use a redundant join condition on both @@ -249,7 +297,6 @@ class ApiQueryContributions extends ApiQueryBase { $this->addTables( $tables ); $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->fld_parsedcomment ); @@ -260,19 +307,18 @@ class ApiQueryContributions extends ApiQueryBase { if ( $this->fld_tags ) { $this->addTables( 'tag_summary' ); - $this->addJoinConds( array( 'tag_summary' => array( 'LEFT JOIN', array( 'rev_id=ts_rev_id' ) ) ) ); + $this->addJoinConds( + array( 'tag_summary' => array( 'LEFT JOIN', array( 'rev_id=ts_rev_id' ) ) ) + ); $this->addFields( 'ts_tags' ); } if ( isset( $this->params['tag'] ) ) { $this->addTables( 'change_tag' ); - $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) ) ); + $this->addJoinConds( + array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) ) + ); $this->addWhereFld( 'ct_tag', $this->params['tag'] ); - $index['change_tag'] = 'change_tag_tag_id'; - } - - if ( $this->params['toponly'] ) { - $this->addWhere( 'rev_id = page_latest' ); } $this->addOption( 'USE INDEX', $index ); @@ -281,16 +327,24 @@ class ApiQueryContributions extends ApiQueryBase { /** * Extract fields from the database row and append them to a result array * - * @param $row + * @param stdClass $row * @return array */ private function extractRowInfo( $row ) { $vals = array(); + $anyHidden = false; + if ( $row->rev_deleted & Revision::DELETED_TEXT ) { + $vals['texthidden'] = ''; + $anyHidden = true; + } + + // Any rows where we can't view the user were filtered out in the query. $vals['userid'] = $row->rev_user; $vals['user'] = $row->rev_user_text; if ( $row->rev_deleted & Revision::DELETED_USER ) { $vals['userhidden'] = ''; + $anyHidden = true; } if ( $this->fld_ids ) { $vals['pageid'] = intval( $row->rev_page ); @@ -327,7 +381,15 @@ class ApiQueryContributions extends ApiQueryBase { if ( ( $this->fld_comment || $this->fld_parsedcomment ) && isset( $row->rev_comment ) ) { if ( $row->rev_deleted & Revision::DELETED_COMMENT ) { $vals['commenthidden'] = ''; - } else { + $anyHidden = true; + } + + $userCanView = Revision::userCanBitfield( + $row->rev_deleted, + Revision::DELETED_COMMENT, $this->getUser() + ); + + if ( $userCanView ) { if ( $this->fld_comment ) { $vals['comment'] = $row->rev_comment; } @@ -346,8 +408,13 @@ class ApiQueryContributions extends ApiQueryBase { $vals['size'] = intval( $row->rev_len ); } - if ( $this->fld_sizediff && !is_null( $row->rev_len ) && !is_null( $row->rev_parent_id ) ) { - $parentLen = isset( $this->parentLens[$row->rev_parent_id] ) ? $this->parentLens[$row->rev_parent_id] : 0; + if ( $this->fld_sizediff + && !is_null( $row->rev_len ) + && !is_null( $row->rev_parent_id ) + ) { + $parentLen = isset( $this->parentLens[$row->rev_parent_id] ) + ? $this->parentLens[$row->rev_parent_id] + : 0; $vals['sizediff'] = intval( $row->rev_len - $parentLen ); } @@ -361,12 +428,19 @@ class ApiQueryContributions extends ApiQueryBase { } } + if ( $anyHidden && $row->rev_deleted & Revision::DELETED_RESTRICTED ) { + $vals['suppressed'] = ''; + } + return $vals; } private function continueStr( $row ) { - return $row->rev_user_text . '|' . - wfTimestamp( TS_ISO_8601, $row->rev_timestamp ); + if ( $this->multiUserMode ) { + return "$row->rev_user_text|$row->rev_timestamp|$row->rev_id"; + } else { + return "$row->rev_timestamp|$row->rev_id"; + } } public function getCacheMode( $params ) { @@ -429,23 +503,34 @@ class ApiQueryContributions extends ApiQueryBase { '!minor', 'patrolled', '!patrolled', + 'top', + '!top', + 'new', + '!new', ) ), 'tag' => null, - 'toponly' => false, + 'toponly' => array( + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_DEPRECATED => true, + ), ); } public function getParamDescription() { - global $wgRCMaxAge; $p = $this->getModulePrefix(); + $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' ); + return array( 'limit' => 'The maximum number of contributions to return', 'start' => 'The start timestamp to return from', 'end' => 'The end timestamp to return to', 'continue' => 'When more results are available, use this to continue', 'user' => 'The users to retrieve contributions for', - 'userprefix' => "Retrieve contributions for all users whose names begin with this value. Overrides {$p}user", + 'userprefix' => array( + "Retrieve contributions for all users whose names begin with this value.", + "Overrides {$p}user", + ), 'dir' => $this->getDirectionDescription( $p ), 'namespace' => 'Only list contributions in these namespaces', 'prop' => array( @@ -461,83 +546,18 @@ class ApiQueryContributions extends ApiQueryBase { ' patrolled - Tags patrolled edits', ' tags - Lists tags for the edit', ), - 'show' => array( "Show only items that meet this criteria, e.g. non minor edits only: {$p}show=!minor", - "NOTE: if {$p}show=patrolled or {$p}show=!patrolled is set, revisions older than \$wgRCMaxAge ($wgRCMaxAge) won't be shown", ), + 'show' => array( + "Show only items that meet thse criteria, e.g. non minor edits only: {$p}show=!minor", + "NOTE: If {$p}show=patrolled or {$p}show=!patrolled is set, revisions older than", + "\$wgRCMaxAge ($RCMaxAge) won't be shown", + ), 'tag' => 'Only list revisions tagged with this tag', 'toponly' => 'Only list changes which are the latest revision', ); } - public function getResultProperties() { - return array( - '' => array( - 'userid' => 'integer', - 'user' => 'string', - 'userhidden' => 'boolean' - ), - 'ids' => array( - 'pageid' => 'integer', - 'revid' => 'integer', - 'parentid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'flags' => array( - 'new' => 'boolean', - 'minor' => 'boolean', - 'top' => 'boolean' - ), - 'comment' => array( - 'commenthidden' => 'boolean', - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'commenthidden' => 'boolean', - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'patrolled' => array( - 'patrolled' => 'boolean' - ), - 'size' => array( - 'size' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'sizediff' => array( - 'sizediff' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { - return 'Get all edits by a user'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'param_user', 'info' => 'User parameter may not be empty.' ), - array( 'code' => 'param_user', 'info' => 'User name user is not valid' ), - array( 'show' ), - array( 'code' => 'permissiondenied', 'info' => 'You need the patrol right to request the patrolled flag' ), - ) ); + return 'Get all edits by a user.'; } public function getExamples() { diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index 3c85ea69..fd5f47b4 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -31,9 +31,11 @@ */ class ApiQueryUserInfo extends ApiQueryBase { + const WL_UNREAD_LIMIT = 1000; + private $prop = array(); - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'ui' ); } @@ -50,7 +52,6 @@ class ApiQueryUserInfo extends ApiQueryBase { } protected function getCurrentUserInfo() { - global $wgHiddenPrefs; $user = $this->getUser(); $result = $this->getResult(); $vals = array(); @@ -68,6 +69,10 @@ class ApiQueryUserInfo extends ApiQueryBase { $vals['blockedby'] = $block->getByName(); $vals['blockedbyid'] = $block->getBy(); $vals['blockreason'] = $user->blockedFor(); + $vals['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $block->mTimestamp ); + $vals['blockexpiry'] = $block->getExpiry() === 'infinity' + ? 'infinite' + : wfTimestamp( TS_ISO_8601, $block->getExpiry() ); } } @@ -103,6 +108,12 @@ class ApiQueryUserInfo extends ApiQueryBase { $vals['options'] = $user->getOptions(); } + if ( isset( $this->prop['preferencestoken'] ) ) { + $p = $this->getModulePrefix(); + $this->setWarning( + "{$p}prop=preferencestoken has been deprecated. Please use action=query&meta=tokens instead." + ); + } if ( isset( $this->prop['preferencestoken'] ) && is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) && $user->isAllowed( 'editmyoptions' ) @@ -120,7 +131,7 @@ class ApiQueryUserInfo extends ApiQueryBase { $vals['ratelimits'] = $this->getRateLimits(); } - if ( isset( $this->prop['realname'] ) && !in_array( 'realname', $wgHiddenPrefs ) ) { + if ( isset( $this->prop['realname'] ) && !in_array( 'realname', $this->getConfig()->get( 'HiddenPrefs' ) ) ) { $vals['realname'] = $user->getRealName(); } @@ -152,11 +163,33 @@ class ApiQueryUserInfo extends ApiQueryBase { $result->setIndexedTagName( $acceptLang, 'lang' ); $vals['acceptlang'] = $acceptLang; } + + if ( isset( $this->prop['unreadcount'] ) ) { + $dbr = $this->getQuery()->getNamedDB( 'watchlist', DB_SLAVE, 'watchlist' ); + + $sql = $dbr->selectSQLText( + 'watchlist', + array( 'dummy' => 1 ), + array( + 'wl_user' => $user->getId(), + 'wl_notificationtimestamp IS NOT NULL', + ), + __METHOD__, + array( 'LIMIT' => self::WL_UNREAD_LIMIT ) + ); + $count = $dbr->selectField( array( 'c' => "($sql)" ), 'COUNT(*)' ); + + if ( $count >= self::WL_UNREAD_LIMIT ) { + $vals['unreadcount'] = self::WL_UNREAD_LIMIT . '+'; + } else { + $vals['unreadcount'] = (int)$count; + } + } + return $vals; } protected function getRateLimits() { - global $wgRateLimits; $user = $this->getUser(); if ( !$user->isPingLimitable() ) { return array(); // No limits @@ -180,7 +213,7 @@ class ApiQueryUserInfo extends ApiQueryBase { // Now get the actual limits $retval = array(); - foreach ( $wgRateLimits as $action => $limits ) { + foreach ( $this->getConfig()->get( 'RateLimits' ) as $action => $limits ) { foreach ( $categories as $cat ) { if ( isset( $limits[$cat] ) && !is_null( $limits[$cat] ) ) { $retval[$action][$cat]['hits'] = intval( $limits[$cat][0] ); @@ -188,6 +221,7 @@ class ApiQueryUserInfo extends ApiQueryBase { } } } + return $retval; } @@ -210,7 +244,8 @@ class ApiQueryUserInfo extends ApiQueryBase { 'email', 'realname', 'acceptlang', - 'registrationdate' + 'registrationdate', + 'unreadcount', ) ) ); @@ -227,76 +262,23 @@ class ApiQueryUserInfo extends ApiQueryBase { ' rights - Lists all the rights the current user has', ' changeablegroups - Lists the groups the current user can add to and remove from', ' options - Lists all preferences the current user has set', - ' preferencestoken - Get a token to change current user\'s preferences', + ' preferencestoken - DEPRECATED! Get a token to change current user\'s preferences', ' editcount - Adds the current user\'s edit count', ' ratelimits - Lists all rate limits applying to the current user', ' realname - Adds the user\'s real name', ' email - Adds the user\'s email address and email authentication date', - ' acceptlang - Echoes the Accept-Language header sent by the client in a structured format', + ' acceptlang - Echoes the Accept-Language header sent by ' . + 'the client in a structured format', ' registrationdate - Adds the user\'s registration date', - ) - ); - } - - public function getResultProperties() { - return array( - ApiBase::PROP_LIST => false, - '' => array( - 'id' => 'integer', - 'name' => 'string', - 'anon' => 'boolean' - ), - 'blockinfo' => array( - 'blockid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'blockedby' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'blockedbyid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'blockedreason' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'hasmsg' => array( - 'messages' => 'boolean' - ), - 'preferencestoken' => array( - 'preferencestoken' => 'string' - ), - 'editcount' => array( - 'editcount' => 'integer' - ), - 'realname' => array( - 'realname' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'email' => array( - 'email' => 'string', - 'emailauthenticated' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - 'registrationdate' => array( - 'registrationdate' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) + ' unreadcount - Adds the count of unread pages on the user\'s watchlist ' . + '(maximum ' . ( self::WL_UNREAD_LIMIT - 1 ) . '; returns "' . + self::WL_UNREAD_LIMIT . '+" if more)', ) ); } public function getDescription() { - return 'Get information about the current user'; + return 'Get information about the current user.'; } public function getExamples() { diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index dccfee67..2f5e4b4b 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -33,7 +33,24 @@ class ApiQueryUsers extends ApiQueryBase { private $tokenFunctions, $prop; - public function __construct( $query, $moduleName ) { + /** + * Properties whose contents does not depend on who is looking at them. If the usprops field + * contains anything not listed here, the cache mode will never be public for logged-in users. + * @var array + */ + protected static $publicProps = array( + // everything except 'blockinfo' which might show hidden records if the user + // making the request has the appropriate permissions + 'groups', + 'implicitgroups', + 'rights', + 'editcount', + 'registration', + 'emailable', + 'gender', + ); + + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'us' ); } @@ -41,7 +58,8 @@ class ApiQueryUsers extends ApiQueryBase { * Get an array mapping token names to their handler functions. * The prototype for a token function is func($user) * it should return a token or false (permission denied) - * @return Array tokenname => function + * @deprecated since 1.24 + * @return array Array of tokenname => function */ protected function getTokenFunctions() { // Don't call the hooks twice @@ -58,15 +76,18 @@ class ApiQueryUsers extends ApiQueryBase { 'userrights' => array( 'ApiQueryUsers', 'getUserrightsToken' ), ); wfRunHooks( 'APIQueryUsersTokens', array( &$this->tokenFunctions ) ); + return $this->tokenFunctions; } /** - * @param $user User - * @return String + * @deprecated since 1.24 + * @param User $user + * @return string */ public static function getUserrightsToken( $user ) { global $wgUser; + // Since the permissions check for userrights is non-trivial, // don't bother with it here return $wgUser->getEditToken( $user->getName() ); @@ -90,10 +111,10 @@ class ApiQueryUsers extends ApiQueryBase { if ( $n === false || $n === '' ) { $vals = array( 'name' => $u, 'invalid' => '' ); $fit = $result->addValue( array( 'query', $this->getModuleName() ), - null, $vals ); + null, $vals ); if ( !$fit ) { $this->setContinueEnumParameter( 'users', - implode( '|', array_diff( $users, $done ) ) ); + implode( '|', array_diff( $users, $done ) ) ); $goodNames = array(); break; } @@ -174,6 +195,7 @@ class ApiQueryUsers extends ApiQueryBase { $data[$name]['blockid'] = $row->ipb_id; $data[$name]['blockedby'] = $row->ipb_by_text; $data[$name]['blockedbyid'] = $row->ipb_by; + $data[$name]['blockedtimestamp'] = wfTimestamp( TS_ISO_8601, $row->ipb_timestamp ); $data[$name]['blockreason'] = $row->ipb_reason; $data[$name]['blockexpiry'] = $row->ipb_expiry; } @@ -244,10 +266,10 @@ class ApiQueryUsers extends ApiQueryBase { } $fit = $result->addValue( array( 'query', $this->getModuleName() ), - null, $data[$u] ); + null, $data[$u] ); if ( !$fit ) { $this->setContinueEnumParameter( 'users', - implode( '|', array_diff( $users, $done ) ) ); + implode( '|', array_diff( $users, $done ) ) ); break; } $done[] = $u; @@ -259,7 +281,7 @@ class ApiQueryUsers extends ApiQueryBase { * Gets all the groups that a user is automatically a member of (implicit groups) * * @deprecated since 1.20; call User::getAutomaticGroups() directly. - * @param $user User + * @param User $user * @return array */ public static function getAutoGroups( $user ) { @@ -271,8 +293,10 @@ class ApiQueryUsers extends ApiQueryBase { public function getCacheMode( $params ) { if ( isset( $params['token'] ) ) { return 'private'; - } else { + } elseif ( array_diff( (array)$params['prop'], static::$publicProps ) ) { return 'anon-public-user-private'; + } else { + return 'public'; } } @@ -296,6 +320,7 @@ class ApiQueryUsers extends ApiQueryBase { ApiBase::PARAM_ISMULTI => true ), 'token' => array( + ApiBase::PARAM_DEPRECATED => true, ApiBase::PARAM_TYPE => array_keys( $this->getTokenFunctions() ), ApiBase::PARAM_ISMULTI => true ), @@ -312,7 +337,8 @@ class ApiQueryUsers extends ApiQueryBase { ' rights - Lists all the rights the user(s) has', ' editcount - Adds the user\'s edit count', ' registration - Adds the user\'s registration timestamp', - ' emailable - Tags if the user can and wants to receive email through [[Special:Emailuser]]', + ' emailable - Tags if the user can and wants to receive ' . + 'email through [[Special:Emailuser]]', ' gender - Tags the gender of the user. Returns "male", "female", or "unknown"', ), 'users' => 'A list of users to obtain the same information for', @@ -320,75 +346,8 @@ class ApiQueryUsers extends ApiQueryBase { ); } - public function getResultProperties() { - $props = array( - '' => array( - 'userid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'name' => 'string', - 'invalid' => 'boolean', - 'hidden' => 'boolean', - 'interwiki' => 'boolean', - 'missing' => 'boolean' - ), - 'editcount' => array( - 'editcount' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ) - ), - 'registration' => array( - 'registration' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - 'blockinfo' => array( - 'blockid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'blockedby' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'blockedbyid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'blockedreason' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'blockedexpiry' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - 'emailable' => array( - 'emailable' => 'boolean' - ), - 'gender' => array( - 'gender' => array( - ApiBase::PROP_TYPE => array( - 'male', - 'female', - 'unknown' - ), - ApiBase::PROP_NULLABLE => true - ) - ) - ); - - self::addTokenProperties( $props, $this->getTokenFunctions() ); - - return $props; - } - public function getDescription() { - return 'Get information about a list of users'; + return 'Get information about a list of users.'; } public function getExamples() { diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index 22843f50..efbe05ee 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -32,7 +32,7 @@ */ class ApiQueryWatchlist extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'wl' ); } @@ -44,12 +44,14 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->run( $resultPageSet ); } - private $fld_ids = false, $fld_title = false, $fld_patrol = false, $fld_flags = false, - $fld_timestamp = false, $fld_user = false, $fld_comment = false, $fld_parsedcomment = false, $fld_sizes = false, - $fld_notificationtimestamp = false, $fld_userid = false, $fld_loginfo = false; + private $fld_ids = false, $fld_title = false, $fld_patrol = false, + $fld_flags = false, $fld_timestamp = false, $fld_user = false, + $fld_comment = false, $fld_parsedcomment = false, $fld_sizes = false, + $fld_notificationtimestamp = false, $fld_userid = false, + $fld_loginfo = false; /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function run( $resultPageSet = null ) { @@ -57,7 +59,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); - $user = $this->getWatchlistUser( $params ); + $user = $this->getUser(); + $wlowner = $this->getWatchlistUser( $params ); if ( !is_null( $params['prop'] ) && is_null( $resultPageSet ) ) { $prop = array_flip( $params['prop'] ); @@ -83,10 +86,12 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } $this->addFields( array( + 'rc_id', 'rc_namespace', 'rc_title', 'rc_timestamp', 'rc_type', + 'rc_deleted', ) ); if ( is_null( $resultPageSet ) ) { @@ -103,7 +108,10 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->addFieldsIf( 'rc_patrolled', $this->fld_patrol ); $this->addFieldsIf( array( 'rc_old_len', 'rc_new_len' ), $this->fld_sizes ); $this->addFieldsIf( 'wl_notificationtimestamp', $this->fld_notificationtimestamp ); - $this->addFieldsIf( array( 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ), $this->fld_loginfo ); + $this->addFieldsIf( + array( 'rc_logid', 'rc_log_type', 'rc_log_action', 'rc_params' ), + $this->fld_loginfo + ); } elseif ( $params['allrev'] ) { $this->addFields( 'rc_this_oldid' ); } else { @@ -115,22 +123,35 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'watchlist', ) ); - $userId = $user->getId(); + $userId = $wlowner->getId(); $this->addJoinConds( array( 'watchlist' => array( 'INNER JOIN', array( 'wl_user' => $userId, 'wl_namespace=rc_namespace', 'wl_title=rc_title' - ) ) ) ); - - $this->addWhere( array( - 'rc_deleted' => 0, - ) ); + ) + ) ) ); $db = $this->getDB(); $this->addTimestampWhereRange( 'rc_timestamp', $params['dir'], $params['start'], $params['end'] ); + // Include in ORDER BY for uniqueness + $this->addWhereRange( 'rc_id', $params['dir'], null, null ); + + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 2 ); + $op = ( $params['dir'] === 'newer' ? '>' : '<' ); + $continueTimestamp = $db->addQuotes( $db->timestamp( $cont[0] ) ); + $continueId = (int)$cont[1]; + $this->dieContinueUsageIf( $continueId != $cont[1] ); + $this->addWhere( "rc_timestamp $op $continueTimestamp OR " . + "(rc_timestamp = $continueTimestamp AND " . + "rc_id $op= $continueId)" + ); + } + $this->addWhereFld( 'wl_namespace', $params['namespace'] ); if ( !$params['allrev'] ) { @@ -144,18 +165,21 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { /* Check for conflicting parameters. */ if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) ) - || ( isset( $show['bot'] ) && isset( $show['!bot'] ) ) - || ( isset( $show['anon'] ) && isset( $show['!anon'] ) ) - || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) + || ( isset( $show['bot'] ) && isset( $show['!bot'] ) ) + || ( isset( $show['anon'] ) && isset( $show['!anon'] ) ) + || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) + || ( isset( $show['unread'] ) && isset( $show['!unread'] ) ) ) { $this->dieUsageMsg( 'show' ); } // Check permissions. if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) ) { - $user = $this->getUser(); if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) { - $this->dieUsage( 'You need the patrol right to request the patrolled flag', 'permissiondenied' ); + $this->dieUsage( + 'You need the patrol right to request the patrolled flag', + 'permissiondenied' + ); } } @@ -168,10 +192,16 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->addWhereIf( 'rc_user != 0', isset( $show['!anon'] ) ); $this->addWhereIf( 'rc_patrolled = 0', isset( $show['!patrolled'] ) ); $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) ); + $this->addWhereIf( 'wl_notificationtimestamp IS NOT NULL', isset( $show['unread'] ) ); + $this->addWhereIf( 'wl_notificationtimestamp IS NULL', isset( $show['!unread'] ) ); } if ( !is_null( $params['type'] ) ) { - $this->addWhereFld( 'rc_type', $this->parseRCType( $params['type'] ) ); + try { + $this->addWhereFld( 'rc_type', RecentChange::parseToRCType( $params['type'] ) ); + } catch ( MWException $e ) { + ApiBase::dieDebug( __METHOD__, $e->getMessage() ); + } } if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) { @@ -185,7 +215,40 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } // This is an index optimization for mysql, as done in the Special:Watchlist page - $this->addWhereIf( "rc_timestamp > ''", !isset( $params['start'] ) && !isset( $params['end'] ) && $db->getType() == 'mysql' ); + $this->addWhereIf( + "rc_timestamp > ''", + !isset( $params['start'] ) && !isset( $params['end'] ) && $db->getType() == 'mysql' + ); + + // Paranoia: avoid brute force searches (bug 17342) + if ( !is_null( $params['user'] ) || !is_null( $params['excludeuser'] ) ) { + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $bitmask = Revision::DELETED_USER; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $bitmask = Revision::DELETED_USER | Revision::DELETED_RESTRICTED; + } else { + $bitmask = 0; + } + if ( $bitmask ) { + $this->addWhere( $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask" ); + } + } + + // LogPage::DELETED_ACTION hides the affected page, too. So hide those + // entirely from the watchlist, or someone could guess the title. + if ( !$user->isAllowed( 'deletedhistory' ) ) { + $bitmask = LogPage::DELETED_ACTION; + } elseif ( !$user->isAllowedAny( 'suppressrevision', 'viewsuppressed' ) ) { + $bitmask = LogPage::DELETED_ACTION | LogPage::DELETED_RESTRICTED; + } else { + $bitmask = 0; + } + if ( $bitmask ) { + $this->addWhere( $this->getDB()->makeList( array( + 'rc_type != ' . RC_LOG, + $this->getDB()->bitAnd( 'rc_deleted', $bitmask ) . " != $bitmask", + ), LIST_OR ) ); + } $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -194,9 +257,10 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $res = $this->select( __METHOD__ ); foreach ( $res as $row ) { - 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->rc_timestamp ) ); + if ( ++$count > $params['limit'] ) { + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" ); break; } @@ -204,8 +268,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $vals = $this->extractRowInfo( $row ); $fit = $this->getResult()->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'start', - wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) ); + $this->setContinueEnumParameter( 'continue', "$row->rc_timestamp|$row->rc_id" ); break; } } else { @@ -218,7 +281,10 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } if ( is_null( $resultPageSet ) ) { - $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'item' ); + $this->getResult()->setIndexedTagName_internal( + array( 'query', $this->getModuleName() ), + 'item' + ); } elseif ( $params['allrev'] ) { $resultPageSet->populateFromRevisionIDs( $ids ); } else { @@ -227,139 +293,142 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } private function extractRowInfo( $row ) { - $vals = array(); - - $type = intval( $row->rc_type ); - - /* Determine what kind of change this was. */ - switch ( $type ) { - case RC_EDIT: - $vals['type'] = 'edit'; - break; - case RC_NEW: - $vals['type'] = 'new'; - break; - case RC_MOVE: - $vals['type'] = 'move'; - break; - case RC_LOG: - $vals['type'] = 'log'; - break; - case RC_EXTERNAL: - $vals['type'] = 'external'; - break; - case RC_MOVE_OVER_REDIRECT: - $vals['type'] = 'move over redirect'; - break; - default: - $vals['type'] = $type; - } - - if ( $this->fld_ids ) { - $vals['pageid'] = intval( $row->rc_cur_id ); - $vals['revid'] = intval( $row->rc_this_oldid ); - $vals['old_revid'] = intval( $row->rc_last_oldid ); - } - + /* Determine the title of the page that has been changed. */ $title = Title::makeTitle( $row->rc_namespace, $row->rc_title ); + $user = $this->getUser(); - if ( $this->fld_title ) { - ApiQueryBase::addTitleInfo( $vals, $title ); + /* Our output data. */ + $vals = array(); + $type = intval( $row->rc_type ); + $vals['type'] = RecentChange::parseFromRCType( $type ); + $anyHidden = false; + + /* Create a new entry in the result for the title. */ + if ( $this->fld_title || $this->fld_ids ) { + // These should already have been filtered out of the query, but just in case. + if ( $type === RC_LOG && ( $row->rc_deleted & LogPage::DELETED_ACTION ) ) { + $vals['actionhidden'] = ''; + $anyHidden = true; + } + if ( $type !== RC_LOG || + LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) + ) { + if ( $this->fld_title ) { + ApiQueryBase::addTitleInfo( $vals, $title ); + } + if ( $this->fld_ids ) { + $vals['pageid'] = intval( $row->rc_cur_id ); + $vals['revid'] = intval( $row->rc_this_oldid ); + $vals['old_revid'] = intval( $row->rc_last_oldid ); + } + } } + /* Add user data and 'anon' flag, if user is anonymous. */ if ( $this->fld_user || $this->fld_userid ) { - - if ( $this->fld_userid ) { - $vals['userid'] = $row->rc_user; - // for backwards compatibility - $vals['user'] = $row->rc_user; + if ( $row->rc_deleted & Revision::DELETED_USER ) { + $vals['userhidden'] = ''; + $anyHidden = true; } + if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_USER, $user ) ) { + if ( $this->fld_userid ) { + $vals['userid'] = $row->rc_user; + // for backwards compatibility + $vals['user'] = $row->rc_user; + } - if ( $this->fld_user ) { - $vals['user'] = $row->rc_user_text; - } + if ( $this->fld_user ) { + $vals['user'] = $row->rc_user_text; + } - if ( !$row->rc_user ) { - $vals['anon'] = ''; + if ( !$row->rc_user ) { + $vals['anon'] = ''; + } } } + /* Add flags, such as new, minor, bot. */ if ( $this->fld_flags ) { + if ( $row->rc_bot ) { + $vals['bot'] = ''; + } if ( $row->rc_type == RC_NEW ) { $vals['new'] = ''; } if ( $row->rc_minor ) { $vals['minor'] = ''; } - if ( $row->rc_bot ) { - $vals['bot'] = ''; - } } - if ( $this->fld_patrol && isset( $row->rc_patrolled ) ) { - $vals['patrolled'] = ''; + /* Add sizes of each revision. (Only available on 1.10+) */ + if ( $this->fld_sizes ) { + $vals['oldlen'] = intval( $row->rc_old_len ); + $vals['newlen'] = intval( $row->rc_new_len ); } + /* Add the timestamp. */ if ( $this->fld_timestamp ) { $vals['timestamp'] = wfTimestamp( TS_ISO_8601, $row->rc_timestamp ); } - if ( $this->fld_sizes ) { - $vals['oldlen'] = intval( $row->rc_old_len ); - $vals['newlen'] = intval( $row->rc_new_len ); - } - if ( $this->fld_notificationtimestamp ) { $vals['notificationtimestamp'] = ( $row->wl_notificationtimestamp == null ) ? '' : wfTimestamp( TS_ISO_8601, $row->wl_notificationtimestamp ); } - if ( $this->fld_comment && isset( $row->rc_comment ) ) { - $vals['comment'] = $row->rc_comment; - } + /* Add edit summary / log summary. */ + if ( $this->fld_comment || $this->fld_parsedcomment ) { + if ( $row->rc_deleted & Revision::DELETED_COMMENT ) { + $vals['commenthidden'] = ''; + $anyHidden = true; + } + if ( Revision::userCanBitfield( $row->rc_deleted, Revision::DELETED_COMMENT, $user ) ) { + if ( $this->fld_comment && isset( $row->rc_comment ) ) { + $vals['comment'] = $row->rc_comment; + } - if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) { - $vals['parsedcomment'] = Linker::formatComment( $row->rc_comment, $title ); + if ( $this->fld_parsedcomment && isset( $row->rc_comment ) ) { + $vals['parsedcomment'] = Linker::formatComment( $row->rc_comment, $title ); + } + } } - if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) { - $vals['logid'] = intval( $row->rc_logid ); - $vals['logtype'] = $row->rc_log_type; - $vals['logaction'] = $row->rc_log_action; - $logEntry = DatabaseLogEntry::newFromRow( (array)$row ); - ApiQueryLogEvents::addLogParams( - $this->getResult(), - $vals, - $logEntry->getParameters(), - $logEntry->getType(), - $logEntry->getSubtype(), - $logEntry->getTimestamp() - ); + /* Add the patrolled flag */ + if ( $this->fld_patrol && $row->rc_patrolled == 1 ) { + $vals['patrolled'] = ''; } - return $vals; - } + if ( $this->fld_patrol && ChangesList::isUnpatrolled( $row, $user ) ) { + $vals['unpatrolled'] = ''; + } - /* Copied from ApiQueryRecentChanges. */ - private function parseRCType( $type ) { - if ( is_array( $type ) ) { - $retval = array(); - foreach ( $type as $t ) { - $retval[] = $this->parseRCType( $t ); + if ( $this->fld_loginfo && $row->rc_type == RC_LOG ) { + if ( $row->rc_deleted & LogPage::DELETED_ACTION ) { + $vals['actionhidden'] = ''; + $anyHidden = true; + } + if ( LogEventsList::userCanBitfield( $row->rc_deleted, LogPage::DELETED_ACTION, $user ) ) { + $vals['logid'] = intval( $row->rc_logid ); + $vals['logtype'] = $row->rc_log_type; + $vals['logaction'] = $row->rc_log_action; + $logEntry = DatabaseLogEntry::newFromRow( (array)$row ); + ApiQueryLogEvents::addLogParams( + $this->getResult(), + $vals, + $logEntry->getParameters(), + $logEntry->getType(), + $logEntry->getSubtype(), + $logEntry->getTimestamp() + ); } - return $retval; } - switch ( $type ) { - case 'edit': - return RC_EDIT; - case 'new': - return RC_NEW; - case 'log': - return RC_LOG; - case 'external': - return RC_EXTERNAL; + + if ( $anyHidden && ( $row->rc_deleted & Revision::DELETED_RESTRICTED ) ) { + $vals['suppressed'] = ''; } + + return $vals; } public function getAllowedParams() { @@ -424,6 +493,8 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { '!anon', 'patrolled', '!patrolled', + 'unread', + '!unread', ) ), 'type' => array( @@ -440,12 +511,14 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { ), 'token' => array( ApiBase::PARAM_TYPE => 'string' - ) + ), + 'continue' => null, ); } public function getParamDescription() { $p = $this->getModulePrefix(); + return array( 'allrev' => 'Include multiple revisions of the same page within given timeframe', 'start' => 'The timestamp to start enumerating from', @@ -482,105 +555,14 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { ' log - Log entries', ), 'owner' => 'The name of the user whose watchlist you\'d like to access', - 'token' => 'Give a security token (settable in preferences) to allow access to another user\'s watchlist' - ); - } - - public function getResultProperties() { - global $wgLogTypes; - return array( - '' => array( - 'type' => array( - ApiBase::PROP_TYPE => array( - 'edit', - 'new', - 'move', - 'log', - 'move over redirect' - ) - ) - ), - 'ids' => array( - 'pageid' => 'integer', - 'revid' => 'integer', - 'old_revid' => 'integer' - ), - 'title' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'user' => array( - 'user' => 'string', - 'anon' => 'boolean' - ), - 'userid' => array( - 'userid' => 'integer', - 'anon' => 'boolean' - ), - 'flags' => array( - 'new' => 'boolean', - 'minor' => 'boolean', - 'bot' => 'boolean' - ), - 'patrol' => array( - 'patrolled' => 'boolean' - ), - 'timestamp' => array( - 'timestamp' => 'timestamp' - ), - 'sizes' => array( - 'oldlen' => 'integer', - 'newlen' => 'integer' - ), - 'notificationtimestamp' => array( - 'notificationtimestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - 'comment' => array( - 'comment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'parsedcomment' => array( - 'parsedcomment' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ), - 'loginfo' => array( - 'logid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'logtype' => array( - ApiBase::PROP_TYPE => $wgLogTypes, - ApiBase::PROP_NULLABLE => true - ), - 'logaction' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) + 'token' => 'Give a security token (settable in preferences) to ' . + 'allow access to another user\'s watchlist', + 'continue' => 'When more results are available, use this to continue', ); } public function getDescription() { - return "Get all recent changes to pages in the logged in user's watchlist"; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'bad_wlowner', 'info' => 'Specified user does not exist' ), - array( 'code' => 'bad_wltoken', 'info' => 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences' ), - array( 'code' => 'notloggedin', 'info' => 'You must be logged-in to have a watchlist' ), - array( 'code' => 'patrol', 'info' => 'patrol property is not available' ), - array( 'show' ), - array( 'code' => 'permissiondenied', 'info' => 'You need the patrol right to request the patrolled flag' ), - array( 'code' => 'user-excludeuser', 'info' => 'user and excludeuser cannot be used together' ), - ) ); + return "Get all recent changes to pages in the logged in user's watchlist."; } public function getExamples() { diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php index ea4e724a..6b2223ac 100644 --- a/includes/api/ApiQueryWatchlistRaw.php +++ b/includes/api/ApiQueryWatchlistRaw.php @@ -32,7 +32,7 @@ */ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { - public function __construct( $query, $moduleName ) { + public function __construct( ApiQuery $query, $moduleName ) { parent::__construct( $query, $moduleName, 'wr' ); } @@ -45,7 +45,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet + * @param ApiPageSet $resultPageSet * @return void */ private function run( $resultPageSet = null ) { @@ -91,7 +91,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { $this->addOption( 'ORDER BY', array( 'wl_namespace' . $sort, 'wl_title' . $sort - )); + ) ); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); $res = $this->select( __METHOD__ ); @@ -100,7 +100,8 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { $count = 0; foreach ( $res as $row ) { if ( ++$count > $params['limit'] ) { - // We've reached the one extra which shows that there are additional pages to be had. Stop here... + // We've reached the one extra which shows that there are + // additional pages to be had. Stop here... $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' . $row->wl_title ); break; } @@ -109,8 +110,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { if ( is_null( $resultPageSet ) ) { $vals = array(); ApiQueryBase::addTitleInfo( $vals, $t ); - if ( isset( $prop['changed'] ) && !is_null( $row->wl_notificationtimestamp ) ) - { + if ( isset( $prop['changed'] ) && !is_null( $row->wl_notificationtimestamp ) ) { $vals['changed'] = wfTimestamp( TS_ISO_8601, $row->wl_notificationtimestamp ); } $fit = $this->getResult()->addValue( $this->getModuleName(), null, $vals ); @@ -183,37 +183,14 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { ), 'show' => 'Only list items that meet these criteria', 'owner' => 'The name of the user whose watchlist you\'d like to access', - 'token' => 'Give a security token (settable in preferences) to allow access to another user\'s watchlist', + 'token' => 'Give a security token (settable in preferences) to allow ' . + 'access to another user\'s watchlist', 'dir' => 'Direction to sort the titles and namespaces in', ); } - public function getResultProperties() { - return array( - '' => array( - 'ns' => 'namespace', - 'title' => 'string' - ), - 'changed' => array( - 'changed' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ) - ); - } - public function getDescription() { - return "Get all pages on the logged in user's watchlist"; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'notloggedin', 'info' => 'You must be logged-in to have a watchlist' ), - array( 'show' ), - array( 'code' => 'bad_wlowner', 'info' => 'Specified user does not exist' ), - array( 'code' => 'bad_wltoken', 'info' => 'Incorrect watchlist token provided -- please set a correct token in Special:Preferences' ), - ) ); + return "Get all pages on the logged in user's watchlist."; } public function getExamples() { diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index 39c114b8..2e80447e 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -26,18 +26,20 @@ /** * This class represents the result of the API operations. - * It simply wraps a nested array() structure, adding some functions to simplify array's modifications. - * As various modules execute, they add different pieces of information to this result, - * structuring it as it will be given to the client. + * It simply wraps a nested array() structure, adding some functions to simplify + * array's modifications. As various modules execute, they add different pieces + * of information to this result, structuring it as it will be given to the client. * * Each subarray may either be a dictionary - key-value pairs with unique keys, * or lists, where the items are added using $data[] = $value notation. * - * There are two special key values that change how XML output is generated: - * '_element' This key sets the tag name for the rest of the elements in the current array. - * It is only inserted if the formatter returned true for getNeedsRawData() - * '*' This key has special meaning only to the XML formatter, and is outputted as is - * for all others. In XML it becomes the content of the current element. + * There are three special key values that change how XML output is generated: + * '_element' This key sets the tag name for the rest of the elements in the current array. + * It is only inserted if the formatter returned true for getNeedsRawData() + * '_subelements' This key causes the specified elements to be returned as subelements rather than attributes. + * It is only inserted if the formatter returned true for getNeedsRawData() + * '*' This key has special meaning only to the XML formatter, and is outputted as is + * for all others. In XML it becomes the content of the current element. * * @ingroup API */ @@ -56,13 +58,27 @@ class ApiResult extends ApiBase { */ const ADD_ON_TOP = 2; + /** + * For addValue() and setElement(), do not check size while adding a value + * Don't use this unless you REALLY know what you're doing. + * Values added while the size checking was disabled will never be counted + * @since 1.24 + */ + const NO_SIZE_CHECK = 4; + private $mData, $mIsRawMode, $mSize, $mCheckingSize; + private $continueAllModules = array(); + private $continueGeneratedModules = array(); + private $continuationData = array(); + private $generatorContinuationData = array(); + private $generatorParams = array(); + private $generatorDone = false; + /** - * Constructor - * @param $main ApiMain object + * @param ApiMain $main */ - public function __construct( $main ) { + public function __construct( ApiMain $main ) { parent::__construct( $main, 'result' ); $this->mIsRawMode = false; $this->mCheckingSize = true; @@ -80,9 +96,11 @@ class ApiResult extends ApiBase { /** * Call this function when special elements such as '_element' * are needed by the formatter, for example in XML printing. + * @since 1.23 $flag parameter added + * @param bool $flag Set the raw mode flag to this state */ - public function setRawMode() { - $this->mIsRawMode = true; + public function setRawMode( $flag = true ) { + $this->mIsRawMode = $flag; } /** @@ -104,7 +122,7 @@ class ApiResult extends ApiBase { /** * 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 + * @param mixed $value * @return int */ public static function size( $value ) { @@ -117,6 +135,7 @@ class ApiResult extends ApiBase { // Objects can't always be cast to string $s = strlen( $value ); } + return $s; } @@ -132,6 +151,7 @@ class ApiResult extends ApiBase { * 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) + * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK */ public function disableSizeCheck() { $this->mCheckingSize = false; @@ -139,6 +159,7 @@ class ApiResult extends ApiBase { /** * Re-enable size checking in addValue() + * @deprecated since 1.24, use ApiResult::NO_SIZE_CHECK */ public function enableSizeCheck() { $this->mCheckingSize = true; @@ -147,17 +168,20 @@ class ApiResult extends ApiBase { /** * Add an output value to the array by name. * Verifies that value with the same name has not been added before. - * @param array $arr to add $value to + * @param array $arr To add $value to * @param string $name Index of $arr to add $value at - * @param $value mixed - * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. This parameter used to be - * boolean, and the value of OVERRIDE=1 was specifically chosen so that it would be backwards - * compatible with the new method signature. + * @param mixed $value + * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. + * This parameter used to be boolean, and the value of OVERRIDE=1 was + * specifically chosen so that it would be backwards compatible with the + * new method signature. * * @since 1.21 int $flags replaced boolean $override */ public static function setElement( &$arr, $name, $value, $flags = 0 ) { - if ( $arr === null || $name === null || $value === null || !is_array( $arr ) || is_array( $name ) ) { + if ( $arr === null || $name === null || $value === null + || !is_array( $arr ) || is_array( $name ) + ) { ApiBase::dieDebug( __METHOD__, 'Bad parameter' ); } @@ -176,16 +200,19 @@ class ApiResult extends ApiBase { ApiBase::dieDebug( __METHOD__, "Attempting to merge element $name" ); } } else { - ApiBase::dieDebug( __METHOD__, "Attempting to add element $name=$value, existing value is {$arr[$name]}" ); + ApiBase::dieDebug( + __METHOD__, + "Attempting to add element $name=$value, existing value is {$arr[$name]}" + ); } } /** * Adds a content element to an array. * Use this function instead of hardcoding the '*' element. - * @param array $arr to add the content element to - * @param $value Mixed - * @param string $subElemName when present, content element is created + * @param array $arr To add the content element to + * @param mixed $value + * @param string $subElemName 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. */ @@ -204,10 +231,34 @@ class ApiResult extends ApiBase { } /** + * Causes the elements with the specified names to be output as + * subelements rather than attributes. + * @param array $arr + * @param array|string $names The element name(s) to be output as subelements + */ + public function setSubelements( &$arr, $names ) { + // In raw mode, add the '_subelements', otherwise just ignore + if ( !$this->getIsRawMode() ) { + return; + } + if ( $arr === null || $names === null || !is_array( $arr ) ) { + ApiBase::dieDebug( __METHOD__, 'Bad parameter' ); + } + if ( !is_array( $names ) ) { + $names = array( $names ); + } + if ( !isset( $arr['_subelements'] ) ) { + $arr['_subelements'] = $names; + } else { + $arr['_subelements'] = array_merge( $arr['_subelements'], $names ); + } + } + + /** * In case the array contains indexed values (in addition to named), * give all indexed values the given tag name. This function MUST be * called on every array that has numerical indexes. - * @param $arr array + * @param array $arr * @param string $tag Tag name */ public function setIndexedTagName( &$arr, $tag ) { @@ -224,7 +275,7 @@ class ApiResult extends ApiBase { /** * Calls setIndexedTagName() on each sub-array of $arr - * @param $arr array + * @param array $arr * @param string $tag Tag name */ public function setIndexedTagName_recursive( &$arr, $tag ) { @@ -245,7 +296,7 @@ class ApiResult extends ApiBase { * Don't specify a path to a value that's not in the result, or * you'll get nasty errors. * @param array $path Path to the array, like addValue()'s $path - * @param $tag string + * @param string $tag */ public function setIndexedTagName_internal( $path, $tag ) { $data = &$this->mData; @@ -268,26 +319,26 @@ class ApiResult extends ApiBase { * If $path is null, the value will be inserted at the data root. * If $name is empty, the $value is added as a next list element data[] = $value. * - * @param $path array|string|null - * @param $name string - * @param $value mixed - * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. This parameter used to be - * boolean, and the value of OVERRIDE=1 was specifically chosen so that it would be backwards - * compatible with the new method signature. + * @param array|string|null $path + * @param string $name + * @param mixed $value + * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. + * This parameter used to be boolean, and the value of OVERRIDE=1 was specifically + * chosen so that it would be backwards compatible with the new method signature. * @return bool True if $value fits in the result, false if not * * @since 1.21 int $flags replaced boolean $override */ public function addValue( $path, $name, $value, $flags = 0 ) { - global $wgAPIMaxResultSize; - $data = &$this->mData; - if ( $this->mCheckingSize ) { + if ( $this->mCheckingSize && !( $flags & ApiResult::NO_SIZE_CHECK ) ) { $newsize = $this->mSize + self::size( $value ); - if ( $newsize > $wgAPIMaxResultSize ) { + $maxResultSize = $this->getConfig()->get( 'APIMaxResultSize' ); + if ( $newsize > $maxResultSize ) { $this->setWarning( "This result was truncated because it would otherwise be larger than the " . - "limit of {$wgAPIMaxResultSize} bytes" ); + "limit of {$maxResultSize} bytes" ); + return false; } $this->mSize = $newsize; @@ -322,14 +373,15 @@ class ApiResult extends ApiBase { // Add named element self::setElement( $data, $name, $value, $flags ); } + return true; } /** * Add a parsed limit=max to the result. * - * @param $moduleName string - * @param $limit int + * @param string $moduleName + * @param int $limit */ public function setParsedLimit( $moduleName, $limit ) { // Add value, allowing overwriting @@ -340,8 +392,8 @@ class ApiResult extends ApiBase { * Unset a value previously added to the result set. * Fails silently if the value isn't found. * For parameters, see addValue() - * @param $path array|null - * @param $name string + * @param array|null $path + * @param string $name */ public function unsetValue( $path, $name ) { $data = &$this->mData; @@ -367,7 +419,7 @@ class ApiResult extends ApiBase { /** * Callback function for cleanUpUTF8() * - * @param $s string + * @param string $s */ private static function cleanUp_helper( &$s ) { if ( !is_string( $s ) ) { @@ -394,10 +446,186 @@ class ApiResult extends ApiBase { $result[] = $error; } $this->setIndexedTagName( $result, $errorType ); + return $result; } public function execute() { ApiBase::dieDebug( __METHOD__, 'execute() is not supported on Result object' ); } + + /** + * Parse a 'continue' parameter and return status information. + * + * This must be balanced by a call to endContinuation(). + * + * @since 1.24 + * @param string|null $continue The "continue" parameter, if any + * @param ApiBase[] $allModules Contains ApiBase instances that will be executed + * @param array $generatedModules Names of modules that depend on the generator + * @return array Two elements: a boolean indicating if the generator is done, + * and an array of modules to actually execute. + */ + public function beginContinuation( + $continue, array $allModules = array(), array $generatedModules = array() + ) { + $this->continueGeneratedModules = $generatedModules + ? array_combine( $generatedModules, $generatedModules ) + : array(); + $this->continuationData = array(); + $this->generatorContinuationData = array(); + $this->generatorParams = array(); + + $skip = array(); + if ( is_string( $continue ) && $continue !== '' ) { + $continue = explode( '||', $continue ); + $this->dieContinueUsageIf( count( $continue ) !== 2 ); + $this->generatorDone = ( $continue[0] === '-' ); + if ( !$this->generatorDone ) { + $this->generatorParams = explode( '|', $continue[0] ); + } + $skip = explode( '|', $continue[1] ); + } + + $this->continueAllModules = array(); + $runModules = array(); + foreach ( $allModules as $module ) { + $name = $module->getModuleName(); + if ( in_array( $name, $skip ) ) { + $this->continueAllModules[$name] = false; + // Prevent spurious "unused parameter" warnings + $module->extractRequestParams(); + } else { + $this->continueAllModules[$name] = true; + $runModules[] = $module; + } + } + + return array( + $this->generatorDone, + $runModules, + ); + } + + /** + * Set the continuation parameter for a module + * + * @since 1.24 + * @param ApiBase $module + * @param string $paramName + * @param string|array $paramValue + */ + public function setContinueParam( ApiBase $module, $paramName, $paramValue ) { + $name = $module->getModuleName(); + if ( !isset( $this->continueAllModules[$name] ) ) { + throw new MWException( + "Module '$name' called ApiResult::setContinueParam but was not " . + 'passed to ApiResult::beginContinuation' + ); + } + if ( !$this->continueAllModules[$name] ) { + throw new MWException( + "Module '$name' was not supposed to have been executed, but " . + 'it was executed anyway' + ); + } + $paramName = $module->encodeParamName( $paramName ); + if ( is_array( $paramValue ) ) { + $paramValue = join( '|', $paramValue ); + } + $this->continuationData[$name][$paramName] = $paramValue; + } + + /** + * Set the continuation parameter for the generator module + * + * @since 1.24 + * @param ApiBase $module + * @param string $paramName + * @param string|array $paramValue + */ + public function setGeneratorContinueParam( ApiBase $module, $paramName, $paramValue ) { + $name = $module->getModuleName(); + $paramName = $module->encodeParamName( $paramName ); + if ( is_array( $paramValue ) ) { + $paramValue = join( '|', $paramValue ); + } + $this->generatorContinuationData[$name][$paramName] = $paramValue; + } + + /** + * Close continuation, writing the data into the result + * + * @since 1.24 + * @param string $style 'standard' for the new style since 1.21, 'raw' for + * the style used in 1.20 and earlier. + */ + public function endContinuation( $style = 'standard' ) { + if ( $style === 'raw' ) { + $key = 'query-continue'; + $data = array_merge_recursive( + $this->continuationData, $this->generatorContinuationData + ); + } else { + $key = 'continue'; + $data = array(); + + $finishedModules = array_diff( + array_keys( $this->continueAllModules ), + array_keys( $this->continuationData ) + ); + + // First, grab the non-generator-using continuation data + $continuationData = array_diff_key( + $this->continuationData, $this->continueGeneratedModules + ); + foreach ( $continuationData as $module => $kvp ) { + $data += $kvp; + } + + // Next, handle the generator-using continuation data + $continuationData = array_intersect_key( + $this->continuationData, $this->continueGeneratedModules + ); + if ( $continuationData ) { + // Some modules are unfinished: include those params, and copy + // the generator params. + foreach ( $continuationData as $module => $kvp ) { + $data += $kvp; + } + $data += array_intersect_key( + $this->getMain()->getRequest()->getValues(), + array_flip( $this->generatorParams ) + ); + } elseif ( $this->generatorContinuationData ) { + // All the generator-using modules are complete, but the + // generator isn't. Continue the generator and restart the + // generator-using modules + $this->generatorParams = array(); + foreach ( $this->generatorContinuationData as $kvp ) { + $this->generatorParams = array_merge( + $this->generatorParams, array_keys( $kvp ) + ); + $data += $kvp; + } + $finishedModules = array_diff( + $finishedModules, $this->continueGeneratedModules + ); + } else { + // Generator and prop modules are all done. Mark it so. + $this->generatorDone = true; + } + + // Set 'continue' if any continuation data is set or if the generator + // still needs to run + if ( $data || !$this->generatorDone ) { + $data['continue'] = + ( $this->generatorDone ? '-' : join( '|', $this->generatorParams ) ) . + '||' . join( '|', $finishedModules ); + } + } + if ( $data ) { + $this->addValue( null, $key, $data, ApiResult::ADD_ON_TOP | ApiResult::NO_SIZE_CHECK ); + } + } } diff --git a/includes/api/ApiRevisionDelete.php b/includes/api/ApiRevisionDelete.php new file mode 100644 index 00000000..cbc30704 --- /dev/null +++ b/includes/api/ApiRevisionDelete.php @@ -0,0 +1,236 @@ +<?php +/** + * Created on Jun 25, 2013 + * + * Copyright © 2013 Brad Jorsch <bjorsch@wikimedia.org> + * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.23 + */ + +/** + * API interface to RevDel. The API equivalent of Special:RevisionDelete. + * Requires API write mode to be enabled. + * + * @ingroup API + */ +class ApiRevisionDelete extends ApiBase { + + public function execute() { + $params = $this->extractRequestParams(); + $user = $this->getUser(); + + if ( !$user->isAllowed( RevisionDeleter::getRestriction( $params['type'] ) ) ) { + $this->dieUsageMsg( 'badaccess-group0' ); + } + + if ( !$params['ids'] ) { + $this->dieUsage( "At least one value is required for 'ids'", 'badparams' ); + } + + $hide = $params['hide'] ?: array(); + $show = $params['show'] ?: array(); + if ( array_intersect( $hide, $show ) ) { + $this->dieUsage( "Mutually exclusive values for 'hide' and 'show'", 'badparams' ); + } elseif ( !$hide && !$show ) { + $this->dieUsage( "At least one value is required for 'hide' or 'show'", 'badparams' ); + } + $bits = array( + 'content' => RevisionDeleter::getRevdelConstant( $params['type'] ), + 'comment' => Revision::DELETED_COMMENT, + 'user' => Revision::DELETED_USER, + ); + $bitfield = array(); + foreach ( $bits as $key => $bit ) { + if ( in_array( $key, $hide ) ) { + $bitfield[$bit] = 1; + } elseif ( in_array( $key, $show ) ) { + $bitfield[$bit] = 0; + } else { + $bitfield[$bit] = -1; + } + } + + if ( $params['suppress'] === 'yes' ) { + if ( !$user->isAllowed( 'suppressrevision' ) ) { + $this->dieUsageMsg( 'badaccess-group0' ); + } + $bitfield[Revision::DELETED_RESTRICTED] = 1; + } elseif ( $params['suppress'] === 'no' ) { + $bitfield[Revision::DELETED_RESTRICTED] = 0; + } else { + $bitfield[Revision::DELETED_RESTRICTED] = -1; + } + + $targetObj = null; + if ( $params['target'] ) { + $targetObj = Title::newFromText( $params['target'] ); + } + $targetObj = RevisionDeleter::suggestTarget( $params['type'], $targetObj, $params['ids'] ); + if ( $targetObj === null ) { + $this->dieUsage( 'A target title is required for this RevDel type', 'needtarget' ); + } + + $list = RevisionDeleter::createList( + $params['type'], $this->getContext(), $targetObj, $params['ids'] + ); + $status = $list->setVisibility( + array( 'value' => $bitfield, 'comment' => $params['reason'], 'perItemStatus' => true ) + ); + + $result = $this->getResult(); + $data = $this->extractStatusInfo( $status ); + $data['target'] = $targetObj->getFullText(); + $data['items'] = array(); + + foreach ( $status->itemStatuses as $id => $s ) { + $data['items'][$id] = $this->extractStatusInfo( $s ); + $data['items'][$id]['id'] = $id; + } + + $list->reloadFromMaster(); + // @codingStandardsIgnoreStart Avoid function calls in a FOR loop test part + for ( $item = $list->reset(); $list->current(); $item = $list->next() ) { + $data['items'][$item->getId()] += $item->getApiData( $this->getResult() ); + } + // @codingStandardsIgnoreEnd + + $data['items'] = array_values( $data['items'] ); + $result->setIndexedTagName( $data['items'], 'i' ); + $result->addValue( null, $this->getModuleName(), $data ); + } + + private function extractStatusInfo( $status ) { + $ret = array( + 'status' => $status->isOK() ? 'Success' : 'Fail', + ); + $errors = $this->formatStatusMessages( $status->getErrorsByType( 'error' ) ); + if ( $errors ) { + $this->getResult()->setIndexedTagName( $errors, 'e' ); + $ret['errors'] = $errors; + } + $warnings = $this->formatStatusMessages( $status->getErrorsByType( 'warning' ) ); + if ( $warnings ) { + $this->getResult()->setIndexedTagName( $warnings, 'w' ); + $ret['warnings'] = $warnings; + } + + return $ret; + } + + private function formatStatusMessages( $messages ) { + if ( !$messages ) { + return array(); + } + $result = $this->getResult(); + $ret = array(); + foreach ( $messages as $m ) { + $message = array(); + if ( $m['message'] instanceof Message ) { + $msg = $m['message']; + $message = array( 'message' => $msg->getKey() ); + if ( $msg->getParams() ) { + $message['params'] = $msg->getParams(); + $result->setIndexedTagName( $message['params'], 'p' ); + } + } else { + $message = array( 'message' => $m['message'] ); + $msg = wfMessage( $m['message'] ); + if ( isset( $m['params'] ) ) { + $message['params'] = $m['params']; + $result->setIndexedTagName( $message['params'], 'p' ); + $msg->params( $m['params'] ); + } + } + $message['rendered'] = $msg->useDatabase( false )->inLanguage( 'en' )->plain(); + $ret[] = $message; + } + + return $ret; + } + + public function mustBePosted() { + return true; + } + + public function isWriteMode() { + return true; + } + + public function getAllowedParams() { + return array( + 'type' => array( + ApiBase::PARAM_TYPE => RevisionDeleter::getTypes(), + ApiBase::PARAM_REQUIRED => true + ), + 'target' => null, + 'ids' => array( + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_REQUIRED => true + ), + 'hide' => array( + ApiBase::PARAM_TYPE => array( 'content', 'comment', 'user' ), + ApiBase::PARAM_ISMULTI => true, + ), + 'show' => array( + ApiBase::PARAM_TYPE => array( 'content', 'comment', 'user' ), + ApiBase::PARAM_ISMULTI => true, + ), + 'suppress' => array( + ApiBase::PARAM_TYPE => array( 'yes', 'no', 'nochange' ), + ApiBase::PARAM_DFLT => 'nochange', + ), + 'reason' => null, + ); + } + + public function getParamDescription() { + return array( + 'type' => 'Type of revision deletion being performed', + 'target' => 'Page title for the revision deletion, if required for the type', + 'ids' => 'Identifiers for the revisions to be deleted', + 'hide' => 'What to hide for each revision', + 'show' => 'What to unhide for each revision', + 'suppress' => 'Whether to suppress data from administrators as well as others', + 'reason' => 'Reason for the deletion/undeletion', + ); + } + + public function getDescription() { + return 'Delete/undelete revisions.'; + } + + public function needsToken() { + return 'csrf'; + } + + public function getExamples() { + return array( + 'api.php?action=revisiondelete&target=Main%20Page&type=revision&ids=12345&' . + 'hide=content&token=123ABC' + => 'Hide content for revision 12345 on the Main Page', + 'api.php?action=revisiondelete&type=logging&ids=67890&hide=content|comment|user&' . + 'reason=BLP%20violation&token=123ABC' + => 'Hide all data on log entry 67890 with the reason "BLP violation"', + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Revisiondelete'; + } +} diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index b9873f49..f4d3c541 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -40,21 +40,44 @@ class ApiRollback extends ApiBase { private $mUser = null; public function execute() { + $user = $this->getUser(); $params = $this->extractRequestParams(); - // User and title already validated in call to getTokenSalt from Main - $titleObj = $this->getRbTitle(); + // WikiPage::doRollback needs a Web UI token, so get one of those if we + // validated based on an API rollback token. + $token = $params['token']; + if ( $user->matchEditToken( $token, 'rollback', $this->getRequest() ) ) { + $token = $this->getUser()->getEditToken( + $this->getWebUITokenSalt( $params ), + $this->getRequest() + ); + } + + $titleObj = $this->getRbTitle( $params ); $pageObj = WikiPage::factory( $titleObj ); $summary = $params['summary']; $details = array(); - $retval = $pageObj->doRollback( $this->getRbUser(), $summary, $params['token'], $params['markbot'], $details, $this->getUser() ); + $retval = $pageObj->doRollback( + $this->getRbUser( $params ), + $summary, + $token, + $params['markbot'], + $details, + $user + ); if ( $retval ) { // We don't care about multiple errors, just report one of them $this->dieUsageMsg( reset( $retval ) ); } - $this->setWatch( $params['watchlist'], $titleObj ); + $watch = 'preferences'; + if ( isset( $params['watchlist'] ) ) { + $watch = $params['watchlist']; + } + + // Watch pages + $this->setWatch( $watch, $titleObj, 'watchrollback' ); $info = array( 'title' => $titleObj->getPrefixedText(), @@ -78,18 +101,14 @@ class ApiRollback extends ApiBase { public function getAllowedParams() { return array( - 'title' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true + 'title' => null, + 'pageid' => array( + ApiBase::PARAM_TYPE => 'integer' ), 'user' => array( ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'summary' => '', 'markbot' => false, 'watchlist' => array( @@ -105,59 +124,51 @@ class ApiRollback extends ApiBase { } public function getParamDescription() { + $p = $this->getModulePrefix(); + return array( - 'title' => 'Title of the page you want to rollback.', - 'user' => 'Name of the user whose edits are to be rolled back. If set incorrectly, you\'ll get a badtoken error.', - 'token' => "A rollback token previously retrieved through {$this->getModulePrefix()}prop=revisions", + 'title' => "Title of the page you want to roll back. Cannot be used together with {$p}pageid", + 'pageid' => "Page ID of the page you want to roll back. Cannot be used together with {$p}title", + 'user' => 'Name of the user whose edits are to be rolled back.', + 'token' => array( + /* Standard description automatically prepended */ + 'For compatibility, the token used in the web UI is also accepted.' + ), 'summary' => 'Custom edit summary. If empty, default summary will be used', 'markbot' => 'Mark the reverted edits and the revert as bot edits', - 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', - ); - } - - public function getResultProperties() { - return array( - '' => array( - 'title' => 'string', - 'pageid' => 'integer', - 'summary' => 'string', - 'revid' => 'integer', - 'old_revid' => 'integer', - 'last_revid' => 'integer' - ) + 'watchlist' => 'Unconditionally add or remove the page from your watchlist, ' . + 'use preferences or do not change watch', ); } public function getDescription() { return array( - 'Undo the last edit to the page. If the last user who edited the page made multiple edits in a row,', - 'they will all be rolled back' + 'Undo the last edit to the page. If the last user who edited the page made', + 'multiple edits in a row, they will all be rolled back.' ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'invalidtitle', 'title' ), - array( 'notanarticle' ), - array( 'invaliduser', 'user' ), - ) ); - } - public function needsToken() { - return true; + return 'rollback'; } - public function getTokenSalt() { - return array( $this->getRbTitle()->getPrefixedText(), $this->getRbUser() ); + protected function getWebUITokenSalt( array $params ) { + return array( + $this->getRbTitle( $params )->getPrefixedText(), + $this->getRbUser( $params ) + ); } - private function getRbUser() { + /** + * @param array $params + * + * @return string + */ + private function getRbUser( array $params ) { if ( $this->mUser !== null ) { return $this->mUser; } - $params = $this->extractRequestParams(); - // We need to be able to revert IPs, but getCanonicalName rejects them $this->mUser = User::isIP( $params['user'] ) ? $params['user'] @@ -170,20 +181,29 @@ class ApiRollback extends ApiBase { } /** + * @param array $params + * * @return Title */ - private function getRbTitle() { + private function getRbTitle( array $params ) { if ( $this->mTitleObj !== null ) { return $this->mTitleObj; } - $params = $this->extractRequestParams(); - - $this->mTitleObj = Title::newFromText( $params['title'] ); - - if ( !$this->mTitleObj || $this->mTitleObj->isExternal() ) { - $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); + $this->requireOnlyOneParameter( $params, 'title', 'pageid' ); + + if ( isset( $params['title'] ) ) { + $this->mTitleObj = Title::newFromText( $params['title'] ); + if ( !$this->mTitleObj || $this->mTitleObj->isExternal() ) { + $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); + } + } elseif ( isset( $params['pageid'] ) ) { + $this->mTitleObj = Title::newFromID( $params['pageid'] ); + if ( !$this->mTitleObj ) { + $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) ); + } } + if ( !$this->mTitleObj->exists() ) { $this->dieUsageMsg( 'notanarticle' ); } @@ -194,7 +214,9 @@ class ApiRollback extends ApiBase { public function getExamples() { return array( 'api.php?action=rollback&title=Main%20Page&user=Catrope&token=123ABC', - 'api.php?action=rollback&title=Main%20Page&user=217.121.114.116&token=123ABC&summary=Reverting%20vandalism&markbot=1' + 'api.php?action=rollback&pageid=122&user=Catrope&token=123ABC', + 'api.php?action=rollback&title=Main%20Page&user=217.121.114.116&' . + 'token=123ABC&summary=Reverting%20vandalism&markbot=1' ); } diff --git a/includes/api/ApiRsd.php b/includes/api/ApiRsd.php index d219c91c..a2771a0c 100644 --- a/includes/api/ApiRsd.php +++ b/includes/api/ApiRsd.php @@ -60,7 +60,7 @@ class ApiRsd extends ApiBase { } public function getDescription() { - return 'Export an RSD (Really Simple Discovery) schema'; + return 'Export an RSD (Really Simple Discovery) schema.'; } public function getExamples() { @@ -69,6 +69,10 @@ class ApiRsd extends ApiBase { ); } + public function isReadMode() { + return false; + } + /** * Builds an internal list of APIs to expose information about. * Normally this only lists the MediaWiki API, with its base URL, @@ -107,6 +111,7 @@ class ApiRsd extends ApiBase { ), ); wfRunHooks( 'ApiRsdServiceApis', array( &$apis ) ); + return $apis; } @@ -149,12 +154,13 @@ class ApiRsd extends ApiBase { } $outputData[] = $data; } + return $outputData; } } class ApiFormatXmlRsd extends ApiFormatXml { - public function __construct( $main, $format ) { + public function __construct( ApiMain $main, $format ) { parent::__construct( $main, $format ); $this->setRootElement( 'rsd' ); } diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php index 53a68fde..5d527fc7 100644 --- a/includes/api/ApiSetNotificationTimestamp.php +++ b/includes/api/ApiSetNotificationTimestamp.php @@ -46,9 +46,14 @@ class ApiSetNotificationTimestamp extends ApiBase { $params = $this->extractRequestParams(); $this->requireMaxOneParameter( $params, 'timestamp', 'torevid', 'newerthanrevid' ); + $this->getResult()->beginContinuation( $params['continue'], array(), array() ); + $pageSet = $this->getPageSet(); if ( $params['entirewatchlist'] && $pageSet->getDataSource() !== null ) { - $this->dieUsage( "Cannot use 'entirewatchlist' at the same time as '{$pageSet->getDataSource()}'", 'multisource' ); + $this->dieUsage( + "Cannot use 'entirewatchlist' at the same time as '{$pageSet->getDataSource()}'", + 'multisource' + ); } $dbw = wfGetDB( DB_MASTER, 'api' ); @@ -67,22 +72,26 @@ class ApiSetNotificationTimestamp extends ApiBase { $this->dieUsage( 'torevid may only be used with a single page', 'multpages' ); } $title = reset( $pageSet->getGoodTitles() ); - $timestamp = Revision::getTimestampFromId( $title, $params['torevid'] ); - if ( $timestamp ) { - $timestamp = $dbw->timestamp( $timestamp ); - } else { - $timestamp = null; + if ( $title ) { + $timestamp = Revision::getTimestampFromId( $title, $params['torevid'] ); + if ( $timestamp ) { + $timestamp = $dbw->timestamp( $timestamp ); + } else { + $timestamp = null; + } } } elseif ( isset( $params['newerthanrevid'] ) ) { if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) { $this->dieUsage( 'newerthanrevid may only be used with a single page', 'multpages' ); } $title = reset( $pageSet->getGoodTitles() ); - $revid = $title->getNextRevisionID( $params['newerthanrevid'] ); - if ( $revid ) { - $timestamp = $dbw->timestamp( Revision::getTimestampFromId( $title, $revid ) ); - } else { - $timestamp = null; + if ( $title ) { + $revid = $title->getNextRevisionID( $params['newerthanrevid'] ); + if ( $revid ) { + $timestamp = $dbw->timestamp( Revision::getTimestampFromId( $title, $revid ) ); + } else { + $timestamp = null; + } } } @@ -95,7 +104,9 @@ class ApiSetNotificationTimestamp extends ApiBase { __METHOD__ ); - $result['notificationtimestamp'] = ( is_null( $timestamp ) ? '' : wfTimestamp( TS_ISO_8601, $timestamp ) ); + $result['notificationtimestamp'] = is_null( $timestamp ) + ? '' + : wfTimestamp( TS_ISO_8601, $timestamp ); } else { // First, log the invalid titles foreach ( $pageSet->getInvalidTitles() as $title ) { @@ -119,49 +130,55 @@ class ApiSetNotificationTimestamp extends ApiBase { $result[] = $rev; } - // Now process the valid titles - $lb = new LinkBatch( $pageSet->getTitles() ); - $dbw->update( 'watchlist', array( 'wl_notificationtimestamp' => $timestamp ), - array( 'wl_user' => $user->getID(), $lb->constructSet( 'wl', $dbw ) ), - __METHOD__ - ); - - // Query the results of our update - $timestamps = array(); - $res = $dbw->select( 'watchlist', array( 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ), - array( 'wl_user' => $user->getID(), $lb->constructSet( 'wl', $dbw ) ), - __METHOD__ - ); - foreach ( $res as $row ) { - $timestamps[$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp; - } + if ( $pageSet->getTitles() ) { + // Now process the valid titles + $lb = new LinkBatch( $pageSet->getTitles() ); + $dbw->update( 'watchlist', array( 'wl_notificationtimestamp' => $timestamp ), + array( 'wl_user' => $user->getID(), $lb->constructSet( 'wl', $dbw ) ), + __METHOD__ + ); - // Now, put the valid titles into the result - /** @var $title Title */ - foreach ( $pageSet->getTitles() as $title ) { - $ns = $title->getNamespace(); - $dbkey = $title->getDBkey(); - $r = array( - 'ns' => intval( $ns ), - 'title' => $title->getPrefixedText(), + // Query the results of our update + $timestamps = array(); + $res = $dbw->select( + 'watchlist', + array( 'wl_namespace', 'wl_title', 'wl_notificationtimestamp' ), + array( 'wl_user' => $user->getID(), $lb->constructSet( 'wl', $dbw ) ), + __METHOD__ ); - if ( !$title->exists() ) { - $r['missing'] = ''; + foreach ( $res as $row ) { + $timestamps[$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp; } - if ( isset( $timestamps[$ns] ) && array_key_exists( $dbkey, $timestamps[$ns] ) ) { - $r['notificationtimestamp'] = ''; - if ( $timestamps[$ns][$dbkey] !== null ) { - $r['notificationtimestamp'] = wfTimestamp( TS_ISO_8601, $timestamps[$ns][$dbkey] ); + + // Now, put the valid titles into the result + /** @var $title Title */ + foreach ( $pageSet->getTitles() as $title ) { + $ns = $title->getNamespace(); + $dbkey = $title->getDBkey(); + $r = array( + 'ns' => intval( $ns ), + 'title' => $title->getPrefixedText(), + ); + if ( !$title->exists() ) { + $r['missing'] = ''; } - } else { - $r['notwatched'] = ''; + if ( isset( $timestamps[$ns] ) && array_key_exists( $dbkey, $timestamps[$ns] ) ) { + $r['notificationtimestamp'] = ''; + if ( $timestamps[$ns][$dbkey] !== null ) { + $r['notificationtimestamp'] = wfTimestamp( TS_ISO_8601, $timestamps[$ns][$dbkey] ); + } + } else { + $r['notwatched'] = ''; + } + $result[] = $r; } - $result[] = $r; } $apiResult->setIndexedTagName( $result, 'page' ); } $apiResult->addValue( null, $this->getModuleName(), $result ); + + $apiResult->endContinuation(); } /** @@ -172,6 +189,7 @@ class ApiSetNotificationTimestamp extends ApiBase { if ( !isset( $this->mPageSet ) ) { $this->mPageSet = new ApiPageSet( $this ); } + return $this->mPageSet; } @@ -184,11 +202,7 @@ class ApiSetNotificationTimestamp extends ApiBase { } public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getAllowedParams( $flags = 0 ) { @@ -196,7 +210,6 @@ class ApiSetNotificationTimestamp extends ApiBase { 'entirewatchlist' => array( ApiBase::PARAM_TYPE => 'boolean' ), - 'token' => null, 'timestamp' => array( ApiBase::PARAM_TYPE => 'timestamp' ), @@ -206,12 +219,13 @@ class ApiSetNotificationTimestamp extends ApiBase { 'newerthanrevid' => array( ApiBase::PARAM_TYPE => 'integer' ), + 'continue' => '', ); if ( $flags ) { $result += $this->getPageSet()->getFinalParams( $flags ); } - return $result; + return $result; } public function getParamDescription() { @@ -220,44 +234,7 @@ class ApiSetNotificationTimestamp extends ApiBase { 'timestamp' => 'Timestamp to which to set the notification timestamp', 'torevid' => 'Revision to set the notification timestamp to (one page only)', 'newerthanrevid' => 'Revision to set the notification timestamp newer than (one page only)', - 'token' => 'A token previously acquired via prop=info', - ); - } - - public function getResultProperties() { - return array( - ApiBase::PROP_LIST => true, - ApiBase::PROP_ROOT => array( - 'notificationtimestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ), - '' => array( - 'ns' => array( - ApiBase::PROP_TYPE => 'namespace', - ApiBase::PROP_NULLABLE => true - ), - 'title' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'pageid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'revid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'invalid' => 'boolean', - 'missing' => 'boolean', - 'notwatched' => 'boolean', - 'notificationtimestamp' => array( - ApiBase::PROP_TYPE => 'timestamp', - ApiBase::PROP_NULLABLE => true - ) - ) + 'continue' => 'When more results are available, use this to continue', ); } @@ -269,28 +246,16 @@ class ApiSetNotificationTimestamp extends ApiBase { ); } - public function getPossibleErrors() { - $ps = $this->getPageSet(); - return array_merge( - parent::getPossibleErrors(), - $ps->getFinalPossibleErrors(), - $this->getRequireMaxOneParameterErrorMessages( - array( 'timestamp', 'torevid', 'newerthanrevid' ) ), - $this->getRequireOnlyOneParameterErrorMessages( - array_merge( array( 'entirewatchlist' ), array_keys( $ps->getFinalParams() ) ) ), - array( - array( 'code' => 'notloggedin', 'info' => 'Anonymous users cannot use watchlist change notifications' ), - array( 'code' => 'multpages', 'info' => 'torevid may only be used with a single page' ), - array( 'code' => 'multpages', 'info' => 'newerthanrevid may only be used with a single page' ), - ) - ); - } - public function getExamples() { return array( - 'api.php?action=setnotificationtimestamp&entirewatchlist=&token=123ABC' => 'Reset the notification status for the entire watchlist', - 'api.php?action=setnotificationtimestamp&titles=Main_page&token=123ABC' => 'Reset the notification status for "Main page"', - 'api.php?action=setnotificationtimestamp&titles=Main_page×tamp=2012-01-01T00:00:00Z&token=123ABC' => 'Set the notification timestamp for "Main page" so all edits since 1 January 2012 are unviewed', + 'api.php?action=setnotificationtimestamp&entirewatchlist=&token=123ABC' + => 'Reset the notification status for the entire watchlist', + 'api.php?action=setnotificationtimestamp&titles=Main_page&token=123ABC' + => 'Reset the notification status for "Main page"', + 'api.php?action=setnotificationtimestamp&titles=Main_page&' . + 'timestamp=2012-01-01T00:00:00Z&token=123ABC' + => 'Set the notification timestamp for "Main page" so all edits ' . + 'since 1 January 2012 are unviewed', ); } diff --git a/includes/api/ApiTokens.php b/includes/api/ApiTokens.php index d220a5e6..9287fe6e 100644 --- a/includes/api/ApiTokens.php +++ b/includes/api/ApiTokens.php @@ -25,11 +25,16 @@ */ /** + * @deprecated since 1.24 * @ingroup API */ class ApiTokens extends ApiBase { public function execute() { + $this->setWarning( + "action=tokens has been deprecated. Please use action=query&meta=tokens instead." + ); + $params = $this->extractRequestParams(); $res = array(); @@ -67,6 +72,7 @@ class ApiTokens extends ApiBase { wfRunHooks( 'ApiTokensGetTokenTypes', array( &$types ) ); ksort( $types ); wfProfileOut( __METHOD__ ); + return $types; } @@ -80,16 +86,6 @@ class ApiTokens extends ApiBase { ); } - public function getResultProperties() { - $props = array( - '' => array(), - ); - - self::addTokenProperties( $props, $this->getTokenTypes() ); - - return $props; - } - public function getParamDescription() { return array( 'type' => 'Type of token(s) to request' @@ -97,7 +93,10 @@ class ApiTokens extends ApiBase { } public function getDescription() { - return 'Gets tokens for data-modifying actions'; + return array( + 'This module is deprecated in favor of action=query&meta=tokens.', + 'Gets tokens for data-modifying actions.' + ); } protected function getExamples() { diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index 6a739a2f..2854a825 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -89,64 +89,28 @@ class ApiUnblock extends ApiBase { ApiBase::PARAM_TYPE => 'integer', ), 'user' => null, - 'token' => null, 'reason' => '', ); } public function getParamDescription() { $p = $this->getModulePrefix(); - return array( - 'id' => "ID of the block you want to unblock (obtained through list=blocks). Cannot be used together with {$p}user", - 'user' => "Username, IP address or IP range you want to unblock. Cannot be used together with {$p}id", - 'token' => "An unblock token previously obtained through prop=info", - 'reason' => 'Reason for unblock', - ); - } - public function getResultProperties() { return array( - '' => array( - 'id' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'user' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'userid' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'reason' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) + 'id' => "ID of the block you want to unblock (obtained through list=blocks). " . + "Cannot be used together with {$p}user", + 'user' => "Username, IP address or IP range you want to unblock. " . + "Cannot be used together with {$p}id", + 'reason' => 'Reason for unblock', ); } public function getDescription() { - return 'Unblock a user'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'unblock-notarget' ), - array( 'unblock-idanduser' ), - array( 'cantunblock' ), - array( 'ipbblocked' ), - array( 'ipbnounblockself' ), - ) ); + return 'Unblock a user.'; } public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php index 4bbe568d..07aad9f5 100644 --- a/includes/api/ApiUndelete.php +++ b/includes/api/ApiUndelete.php @@ -56,11 +56,11 @@ class ApiUndelete extends ApiBase { $params['timestamps'][$i] = wfTimestamp( TS_MW, $ts ); } - $pa = new PageArchive( $titleObj ); + $pa = new PageArchive( $titleObj, $this->getConfig() ); $retval = $pa->undelete( ( isset( $params['timestamps'] ) ? $params['timestamps'] : array() ), $params['reason'], - array(), + $params['fileids'], false, $this->getUser() ); @@ -70,7 +70,7 @@ class ApiUndelete extends ApiBase { if ( $retval[1] ) { wfRunHooks( 'FileUndeleteComplete', - array( $titleObj, array(), $this->getUser(), $params['reason'] ) ); + array( $titleObj, $params['fileids'], $this->getUser(), $params['reason'] ) ); } $this->setWatch( $params['watchlist'], $titleObj ); @@ -96,15 +96,15 @@ class ApiUndelete extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'reason' => '', 'timestamps' => array( ApiBase::PARAM_TYPE => 'timestamp', ApiBase::PARAM_ISMULTI => true, ), + 'fileids' => array( + ApiBase::PARAM_TYPE => 'integer', + ApiBase::PARAM_ISMULTI => true, + ), 'watchlist' => array( ApiBase::PARAM_DFLT => 'preferences', ApiBase::PARAM_TYPE => array( @@ -120,46 +120,30 @@ class ApiUndelete extends ApiBase { public function getParamDescription() { return array( 'title' => 'Title of the page you want to restore', - 'token' => 'An undelete token previously retrieved through list=deletedrevs', 'reason' => 'Reason for restoring', - 'timestamps' => 'Timestamps of the revisions to restore. If not set, all revisions will be restored.', - 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', - ); - } - - public function getResultProperties() { - return array( - '' => array( - 'title' => 'string', - 'revisions' => 'integer', - 'filerevisions' => 'integer', - 'reason' => 'string' - ) + 'timestamps' => array( + 'Timestamps of the revisions to restore.', + 'If both timestamps and fileids are empty, all will be restored.', + ), + 'fileids' => array( + 'IDs of the file revisions to restore.', + 'If both timestamps and fileids are empty, all will be restored.', + ), + 'watchlist' => 'Unconditionally add or remove the page from your ' . + 'watchlist, use preferences or do not change watch', ); } public function getDescription() { return array( - 'Restore certain revisions of a deleted page. A list of deleted revisions (including timestamps) can be', - 'retrieved through list=deletedrevs' + 'Restore certain revisions of a deleted page. A list of deleted revisions ', + '(including timestamps) can be retrieved through list=deletedrevs, and a list', + 'of deleted file ids can be retrieved through list=filearchive.' ); } - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'permdenied-undelete' ), - array( 'blockedtext' ), - array( 'invalidtitle', 'title' ), - array( 'cannotundelete' ), - ) ); - } - public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index 467eccf8..657181b7 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -28,17 +28,12 @@ * @ingroup API */ class ApiUpload extends ApiBase { - - /** - * @var UploadBase - */ + /** @var UploadBase|UploadFromChunks */ protected $mUpload = null; protected $mParams; public function execute() { - global $wgEnableAsyncUploads; - // Check whether upload is enabled if ( !UploadBase::isEnabled() ) { $this->dieUsageMsg( 'uploaddisabled' ); @@ -50,13 +45,14 @@ class ApiUpload extends ApiBase { $this->mParams = $this->extractRequestParams(); $request = $this->getMain()->getRequest(); // Check if async mode is actually supported (jobs done in cli mode) - $this->mParams['async'] = ( $this->mParams['async'] && $wgEnableAsyncUploads ); + $this->mParams['async'] = ( $this->mParams['async'] && $this->getConfig()->get( 'EnableAsyncUploads' ) ); // Add the uploaded file to the params array $this->mParams['file'] = $request->getFileName( 'file' ); $this->mParams['chunk'] = $request->getFileName( 'chunk' ); // Copy the session key to the file key, for backward compatibility. if ( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) { + $this->logFeatureUsage( 'action=upload&sessionkey' ); $this->mParams['filekey'] = $this->mParams['sessionkey']; } @@ -95,7 +91,7 @@ class ApiUpload extends ApiBase { } elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) { // defer verification to background process } else { - wfDebug( __METHOD__ . 'about to verify' ); + wfDebug( __METHOD__ . " about to verify\n" ); $this->verifyUpload(); } @@ -141,6 +137,7 @@ class ApiUpload extends ApiBase { // Stash the file and get stash result return $this->getStashResult( $warnings ); } + // This is the most common case -- a normal upload with no warnings // performUpload will return a formatted properly for the API with status return $this->performUpload( $warnings ); @@ -165,6 +162,7 @@ class ApiUpload extends ApiBase { } catch ( MWException $e ) { $this->dieUsage( $e->getMessage(), 'stashfailed' ); } + return $result; } @@ -185,6 +183,7 @@ class ApiUpload extends ApiBase { } catch ( MWException $e ) { $result['warnings']['stashfailed'] = $e->getMessage(); } + return $result; } @@ -212,11 +211,11 @@ class ApiUpload extends ApiBase { } } else { $filekey = $this->mParams['filekey']; - /** @var $status Status */ $status = $this->mUpload->addChunk( $chunkPath, $chunkSize, $this->mParams['offset'] ); if ( !$status->isGood() ) { $this->dieUsage( $status->getWikiText(), 'stashfailed' ); + return array(); } } @@ -233,7 +232,7 @@ class ApiUpload extends ApiBase { array( 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ) ); - $ok = JobQueueGroup::singleton()->push( new AssembleUploadChunksJob( + JobQueueGroup::singleton()->push( new AssembleUploadChunksJob( Title::makeTitle( NS_FILE, $filekey ), array( 'filename' => $this->mParams['filename'], @@ -241,17 +240,13 @@ class ApiUpload extends ApiBase { 'session' => $this->getContext()->exportSession() ) ) ); - if ( $ok ) { - $result['result'] = 'Poll'; - } else { - UploadBase::setSessionStatus( $filekey, false ); - $this->dieUsage( - "Failed to start AssembleUploadChunks.php", 'stashfailed' ); - } + $result['result'] = 'Poll'; + $result['stage'] = 'queued'; } else { $status = $this->mUpload->concatenateChunks(); if ( !$status->isGood() ) { $this->dieUsage( $status->getWikiText(), 'stashfailed' ); + return array(); } @@ -265,6 +260,7 @@ class ApiUpload extends ApiBase { } $result['filekey'] = $filekey; $result['offset'] = $this->mParams['offset'] + $chunkSize; + return $result; } @@ -272,7 +268,7 @@ class ApiUpload extends ApiBase { * Stash the file and return the file key * Also re-raises exceptions with slightly more informative message strings (useful for API) * @throws MWException - * @return String file key + * @return string File key */ private function performStash() { try { @@ -287,6 +283,7 @@ class ApiUpload extends ApiBase { wfDebug( __METHOD__ . ' ' . $message . "\n" ); throw new MWException( $message ); } + return $fileKey; } @@ -341,6 +338,7 @@ class ApiUpload extends ApiBase { } unset( $progress['status'] ); // remove Status object $this->getResult()->addValue( null, $this->getModuleName(), $progress ); + return false; } @@ -357,6 +355,7 @@ class ApiUpload extends ApiBase { $sessionData['sessionkey'] = $this->mParams['statuskey']; } $this->getResult()->addValue( null, $this->getModuleName(), $sessionData ); + return false; } @@ -440,7 +439,7 @@ class ApiUpload extends ApiBase { /** * Checks that the user has permissions to perform this upload. * Dies with usage message on inadequate permissions. - * @param $user User The user to check. + * @param User $user The user to check. */ protected function checkPermissions( $user ) { // Check whether the user has the appropriate permissions to upload anyway @@ -449,9 +448,9 @@ class ApiUpload extends ApiBase { if ( $permission !== true ) { if ( !$user->isLoggedIn() ) { $this->dieUsageMsg( array( 'mustbeloggedin', 'upload' ) ); - } else { - $this->dieUsageMsg( 'badaccess-groups' ); } + + $this->dieUsageMsg( 'badaccess-groups' ); } } @@ -469,10 +468,9 @@ class ApiUpload extends ApiBase { /** * Performs file verification, dies on error. + * @param array $verification */ protected function checkVerification( array $verification ) { - global $wgFileExtensions; - // @todo Move them to ApiBase's message map switch ( $verification['status'] ) { // Recoverable errors @@ -481,7 +479,7 @@ class ApiUpload extends ApiBase { break; case UploadBase::ILLEGAL_FILENAME: $this->dieRecoverableError( 'illegal-filename', 'filename', - array( 'filename' => $verification['filtered'] ) ); + array( 'filename' => $verification['filtered'] ) ); break; case UploadBase::FILENAME_TOO_LONG: $this->dieRecoverableError( 'filename-toolong', 'filename' ); @@ -504,7 +502,7 @@ class ApiUpload extends ApiBase { case UploadBase::FILETYPE_BADTYPE: $extradata = array( 'filetype' => $verification['finalExt'], - 'allowed' => array_values( array_unique( $wgFileExtensions ) ) + 'allowed' => array_values( array_unique( $this->getConfig()->get( 'FileExtensions' ) ) ) ); $this->getResult()->setIndexedTagName( $extradata['allowed'], 'ext' ); @@ -521,15 +519,15 @@ class ApiUpload extends ApiBase { case UploadBase::VERIFICATION_ERROR: $this->getResult()->setIndexedTagName( $verification['details'], 'detail' ); $this->dieUsage( 'This file did not pass file verification', 'verification-error', - 0, array( 'details' => $verification['details'] ) ); + 0, array( 'details' => $verification['details'] ) ); break; case UploadBase::HOOK_ABORTED: $this->dieUsage( "The modification you tried to make was aborted by an extension hook", - 'hookaborted', 0, array( 'error' => $verification['error'] ) ); + 'hookaborted', 0, array( 'error' => $verification['error'] ) ); break; default: $this->dieUsage( 'An unknown error occurred', 'unknown-error', - 0, array( 'code' => $verification['status'] ) ); + 0, array( 'code' => $verification['status'] ) ); break; } } @@ -555,6 +553,7 @@ class ApiUpload extends ApiBase { if ( isset( $warnings['duplicate'] ) ) { $dupes = array(); + /** @var File $dupe */ foreach ( $warnings['duplicate'] as $dupe ) { $dupes[] = $dupe->getName(); } @@ -565,10 +564,14 @@ class ApiUpload extends ApiBase { if ( isset( $warnings['exists'] ) ) { $warning = $warnings['exists']; unset( $warnings['exists'] ); - $localFile = isset( $warning['normalizedFile'] ) ? $warning['normalizedFile'] : $warning['file']; + /** @var LocalFile $localFile */ + $localFile = isset( $warning['normalizedFile'] ) + ? $warning['normalizedFile'] + : $warning['file']; $warnings[$warning['warning']] = $localFile->getName(); } } + return $warnings; } @@ -587,10 +590,23 @@ class ApiUpload extends ApiBase { /** @var $file File */ $file = $this->mUpload->getLocalFile(); - $watch = $this->getWatchlistValue( $this->mParams['watchlist'], $file->getTitle() ); + + // For preferences mode, we want to watch if 'watchdefault' is set or + // if the *file* doesn't exist and 'watchcreations' is set. But + // getWatchlistValue()'s automatic handling checks if the *title* + // exists or not, so we need to check both prefs manually. + $watch = $this->getWatchlistValue( + $this->mParams['watchlist'], $file->getTitle(), 'watchdefault' + ); + if ( !$watch && $this->mParams['watchlist'] == 'preferences' && !$file->exists() ) { + $watch = $this->getWatchlistValue( + $this->mParams['watchlist'], $file->getTitle(), 'watchcreations' + ); + } // Deprecated parameters if ( $this->mParams['watch'] ) { + $this->logFeatureUsage( 'action=upload&watch' ); $watch = true; } @@ -604,7 +620,7 @@ class ApiUpload extends ApiBase { $this->mParams['filekey'], array( 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ) ); - $ok = JobQueueGroup::singleton()->push( new PublishStashedFileJob( + JobQueueGroup::singleton()->push( new PublishStashedFileJob( Title::makeTitle( NS_FILE, $this->mParams['filename'] ), array( 'filename' => $this->mParams['filename'], @@ -615,13 +631,8 @@ class ApiUpload extends ApiBase { 'session' => $this->getContext()->exportSession() ) ) ); - if ( $ok ) { - $result['result'] = 'Poll'; - } else { - UploadBase::setSessionStatus( $this->mParams['filekey'], false ); - $this->dieUsage( - "Failed to start PublishStashedFile.php", 'publishfailed' ); - } + $result['result'] = 'Poll'; + $result['stage'] = 'queued'; } else { /** @var $status Status */ $status = $this->mUpload->performUpload( $this->mParams['comment'], @@ -637,11 +648,10 @@ class ApiUpload extends ApiBase { 'result' => 'Queued', 'statuskey' => $error[0][1], ); - } else { - $this->getResult()->setIndexedTagName( $error, 'error' ); - - $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error ); } + + $this->getResult()->setIndexedTagName( $error, 'error' ); + $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error ); } $result['result'] = 'Success'; } @@ -658,8 +668,7 @@ class ApiUpload extends ApiBase { * Checks if asynchronous copy uploads are enabled and throws an error if they are not. */ protected function checkAsyncDownloadEnabled() { - global $wgAllowAsyncCopyUploads; - if ( !$wgAllowAsyncCopyUploads ) { + if ( !$this->getConfig()->get( 'AllowAsyncCopyUploads' ) ) { $this->dieUsage( 'Asynchronous copy uploads disabled', 'asynccopyuploaddisabled' ); } } @@ -681,10 +690,6 @@ class ApiUpload extends ApiBase { ApiBase::PARAM_DFLT => '' ), 'text' => null, - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'watch' => array( ApiBase::PARAM_DFLT => false, ApiBase::PARAM_DEPRECATED => true, @@ -728,17 +733,19 @@ class ApiUpload extends ApiBase { public function getParamDescription() { $params = array( 'filename' => 'Target filename', - 'token' => 'Edit token. You can get one of these through prop=info', - 'comment' => 'Upload comment. Also used as the initial page text for new files if "text" is not specified', + 'comment' => 'Upload comment. Also used as the initial page text for new ' . + 'files if "text" is not specified', 'text' => 'Initial page text for new files', 'watch' => 'Watch the page', - 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', + 'watchlist' => 'Unconditionally add or remove the page from your watchlist, ' . + 'use preferences or do not change watch', 'ignorewarnings' => 'Ignore any warnings', 'file' => 'File contents', 'url' => 'URL to fetch the file from', 'filekey' => 'Key that identifies a previous upload that was stashed temporarily.', 'sessionkey' => 'Same as filekey, maintained for backward compatibility.', - 'stash' => 'If set, the server will not add the file to the repository and stash it temporarily.', + 'stash' => 'If set, the server will not add the file to the repository ' . + 'and stash it temporarily.', 'chunk' => 'Chunk contents', 'offset' => 'Offset of chunk in bytes', @@ -752,42 +759,6 @@ class ApiUpload extends ApiBase { ); return $params; - - } - - public function getResultProperties() { - return array( - '' => array( - 'result' => array( - ApiBase::PROP_TYPE => array( - 'Success', - 'Warning', - 'Continue', - 'Queued' - ), - ), - 'filekey' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'sessionkey' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'offset' => array( - ApiBase::PROP_TYPE => 'integer', - ApiBase::PROP_NULLABLE => true - ), - 'statuskey' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), - 'filename' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ) - ) - ); } public function getDescription() { @@ -797,49 +768,20 @@ class ApiUpload extends ApiBase { ' * Have the MediaWiki server fetch a file from a URL, using the "url" parameter', ' * Complete an earlier upload that failed due to warnings, using the "filekey" parameter', 'Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when', - 'sending the "file". Also you must get and send an edit token before doing any upload stuff' - ); - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), - $this->getRequireOnlyOneParameterErrorMessages( array( 'filekey', 'file', 'url', 'statuskey' ) ), - array( - array( 'uploaddisabled' ), - array( 'invalid-file-key' ), - array( 'uploaddisabled' ), - array( 'mustbeloggedin', 'upload' ), - array( 'badaccess-groups' ), - array( 'code' => 'fetchfileerror', 'info' => '' ), - array( 'code' => 'nomodule', 'info' => 'No upload module set' ), - array( 'code' => 'empty-file', 'info' => 'The file you submitted was empty' ), - array( 'code' => 'filetype-missing', 'info' => 'The file is missing an extension' ), - array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ), - array( 'code' => 'overwrite', 'info' => 'Overwriting an existing file is not allowed' ), - array( 'code' => 'stashfailed', 'info' => 'Stashing temporary file failed' ), - array( 'code' => 'publishfailed', 'info' => 'Publishing of stashed file failed' ), - array( 'code' => 'internal-error', 'info' => 'An internal error occurred' ), - array( 'code' => 'asynccopyuploaddisabled', 'info' => 'Asynchronous copy uploads disabled' ), - array( 'code' => 'stasherror', 'info' => 'An upload stash error occurred' ), - array( 'fileexists-forbidden' ), - array( 'fileexists-shared-forbidden' ), - ) + 'sending the "file".', ); } public function needsToken() { - return true; - } - - public function getTokenSalt() { - return ''; + return 'csrf'; } public function getExamples() { return array( - 'api.php?action=upload&filename=Wiki.png&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png' + 'api.php?action=upload&filename=Wiki.png' . + '&url=http%3A//upload.wikimedia.org/wikipedia/en/b/bc/Wiki.png&token=123ABC' => 'Upload from a URL', - 'api.php?action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1' + 'api.php?action=upload&filename=Wiki.png&filekey=filekey&ignorewarnings=1&token=123ABC' => 'Complete an upload that failed due to warnings', ); } diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php index 7d308285..c3ceb345 100644 --- a/includes/api/ApiUserrights.php +++ b/includes/api/ApiUserrights.php @@ -35,16 +35,16 @@ class ApiUserrights extends ApiBase { public function execute() { $params = $this->extractRequestParams(); - $user = $this->getUrUser(); + $user = $this->getUrUser( $params ); $form = new UserrightsPage; $form->setContext( $this->getContext() ); $r['user'] = $user->getName(); $r['userid'] = $user->getId(); - list( $r['added'], $r['removed'] ) = - $form->doSaveUserGroups( - $user, (array)$params['add'], - (array)$params['remove'], $params['reason'] ); + list( $r['added'], $r['removed'] ) = $form->doSaveUserGroups( + $user, (array)$params['add'], + (array)$params['remove'], $params['reason'] + ); $result = $this->getResult(); $result->setIndexedTagName( $r['added'], 'group' ); @@ -53,26 +53,28 @@ class ApiUserrights extends ApiBase { } /** + * @param array $params * @return User */ - private function getUrUser() { + private function getUrUser( array $params ) { if ( $this->mUser !== null ) { return $this->mUser; } - $params = $this->extractRequestParams(); + $this->requireOnlyOneParameter( $params, 'user', 'userid' ); + + $user = isset( $params['user'] ) ? $params['user'] : '#' . $params['userid']; $form = new UserrightsPage; $form->setContext( $this->getContext() ); - $status = $form->fetchUser( $params['user'] ); + $status = $form->fetchUser( $user ); if ( !$status->isOK() ) { $this->dieStatus( $status ); - } else { - $user = $status->value; } - $this->mUser = $user; - return $user; + $this->mUser = $status->value; + + return $status->value; } public function mustBePosted() { @@ -87,7 +89,9 @@ class ApiUserrights extends ApiBase { return array( 'user' => array( ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true + ), + 'userid' => array( + ApiBase::PARAM_TYPE => 'integer', ), 'add' => array( ApiBase::PARAM_TYPE => User::getAllGroups(), @@ -97,10 +101,6 @@ class ApiUserrights extends ApiBase { ApiBase::PARAM_TYPE => User::getAllGroups(), ApiBase::PARAM_ISMULTI => true ), - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), 'reason' => array( ApiBase::PARAM_DFLT => '' ) @@ -110,28 +110,33 @@ class ApiUserrights extends ApiBase { public function getParamDescription() { return array( 'user' => 'User name', + 'userid' => 'User id', 'add' => 'Add the user to these groups', 'remove' => 'Remove the user from these groups', - 'token' => 'A userrights token previously retrieved through list=users', + 'token' => array( + /* Standard description automatically prepended */ + 'For compatibility, the token used in the web UI is also accepted.' + ), 'reason' => 'Reason for the change', ); } public function getDescription() { - return 'Add/remove a user to/from groups'; + return 'Add/remove a user to/from groups.'; } public function needsToken() { - return true; + return 'userrights'; } - public function getTokenSalt() { - return $this->getUrUser()->getName(); + protected function getWebUITokenSalt( array $params ) { + return $this->getUrUser( $params )->getName(); } public function getExamples() { return array( - 'api.php?action=userrights&user=FooBot&add=bot&remove=sysop|bureaucrat&token=123ABC' + 'api.php?action=userrights&user=FooBot&add=bot&remove=sysop|bureaucrat&token=123ABC', + 'api.php?action=userrights&userid=123&add=bot&remove=sysop|bureaucrat&token=123ABC' ); } diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php index c7d636a1..e6a660b3 100644 --- a/includes/api/ApiWatch.php +++ b/includes/api/ApiWatch.php @@ -30,41 +30,105 @@ * @ingroup API */ class ApiWatch extends ApiBase { + private $mPageSet = null; public function execute() { $user = $this->getUser(); if ( !$user->isLoggedIn() ) { $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' ); } + if ( !$user->isAllowed( 'editmywatchlist' ) ) { $this->dieUsage( 'You don\'t have permission to edit your watchlist', 'permissiondenied' ); } $params = $this->extractRequestParams(); - $title = Title::newFromText( $params['title'] ); - if ( !$title || $title->isExternal() || !$title->canExist() ) { - $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); + $this->getResult()->beginContinuation( $params['continue'], array(), array() ); + + $pageSet = $this->getPageSet(); + // by default we use pageset to extract the page to work on. + // title is still supported for backward compatibility + if ( !isset( $params['title'] ) ) { + $pageSet->execute(); + $res = $pageSet->getInvalidTitlesAndRevisions( array( + 'invalidTitles', + 'special', + 'missingIds', + 'missingRevIds', + 'interwikiTitles' + ) ); + + foreach ( $pageSet->getMissingTitles() as $title ) { + $r = $this->watchTitle( $title, $user, $params ); + $r['missing'] = 1; + $res[] = $r; + } + + foreach ( $pageSet->getGoodTitles() as $title ) { + $r = $this->watchTitle( $title, $user, $params ); + $res[] = $r; + } + $this->getResult()->setIndexedTagName( $res, 'w' ); + } else { + // dont allow use of old title parameter with new pageset parameters. + $extraParams = array_keys( array_filter( $pageSet->extractRequestParams(), function ( $x ) { + return $x !== null && $x !== false; + } ) ); + + if ( $extraParams ) { + $p = $this->getModulePrefix(); + $this->dieUsage( + "The parameter {$p}title can not be used with " . implode( ", ", $extraParams ), + 'invalidparammix' + ); + } + + $this->logFeatureUsage( 'action=watch&title' ); + $title = Title::newFromText( $params['title'] ); + if ( !$title || !$title->isWatchable() ) { + $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); + } + $res = $this->watchTitle( $title, $user, $params, true ); + } + $this->getResult()->addValue( null, $this->getModuleName(), $res ); + $this->getResult()->endContinuation(); + } + + private function watchTitle( Title $title, User $user, array $params, + $compatibilityMode = false + ) { + if ( !$title->isWatchable() ) { + return array( 'title' => $title->getPrefixedText(), 'watchable' => 0 ); } $res = array( 'title' => $title->getPrefixedText() ); - // Currently unnecessary, code to act as a safeguard against any change in current behavior of uselang + // Currently unnecessary, code to act as a safeguard against any change + // in current behavior of uselang. // Copy from ApiParse $oldLang = null; - if ( isset( $params['uselang'] ) && $params['uselang'] != $this->getContext()->getLanguage()->getCode() ) { + if ( isset( $params['uselang'] ) && + $params['uselang'] != $this->getContext()->getLanguage()->getCode() + ) { $oldLang = $this->getContext()->getLanguage(); // Backup language $this->getContext()->setLanguage( Language::factory( $params['uselang'] ) ); } if ( $params['unwatch'] ) { - $res['unwatched'] = ''; - $res['message'] = $this->msg( 'removedwatchtext', $title->getPrefixedText() )->title( $title )->parseAsBlock(); $status = UnwatchAction::doUnwatch( $title, $user ); + if ( $status->isOK() ) { + $res['unwatched'] = ''; + $res['message'] = $this->msg( 'removedwatchtext', $title->getPrefixedText() ) + ->title( $title )->parseAsBlock(); + } } else { - $res['watched'] = ''; - $res['message'] = $this->msg( 'addedwatchtext', $title->getPrefixedText() )->title( $title )->parseAsBlock(); $status = WatchAction::doWatch( $title, $user ); + if ( $status->isOK() ) { + $res['watched'] = ''; + $res['message'] = $this->msg( 'addedwatchtext', $title->getPrefixedText() ) + ->title( $title )->parseAsBlock(); + } } if ( !is_null( $oldLang ) ) { @@ -72,9 +136,25 @@ class ApiWatch extends ApiBase { } if ( !$status->isOK() ) { - $this->dieStatus( $status ); + if ( $compatibilityMode ) { + $this->dieStatus( $status ); + } + $res['error'] = $this->getErrorFromStatus( $status ); } - $this->getResult()->addValue( null, $this->getModuleName(), $res ); + + return $res; + } + + /** + * Get a cached instance of an ApiPageSet object + * @return ApiPageSet + */ + private function getPageSet() { + if ( $this->mPageSet === null ) { + $this->mPageSet = new ApiPageSet( $this ); + } + + return $this->mPageSet; } public function mustBePosted() { @@ -86,64 +166,45 @@ class ApiWatch extends ApiBase { } public function needsToken() { - return true; - } - - public function getTokenSalt() { return 'watch'; } - public function getAllowedParams() { - return array( + public function getAllowedParams( $flags = 0 ) { + $result = array( 'title' => array( ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true + ApiBase::PARAM_DEPRECATED => true ), 'unwatch' => false, 'uselang' => null, - 'token' => array( - ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true - ), + 'continue' => '', ); + if ( $flags ) { + $result += $this->getPageSet()->getFinalParams( $flags ); + } + + return $result; } public function getParamDescription() { - return array( - 'title' => 'The page to (un)watch', + $psModule = $this->getPageSet(); + + return $psModule->getParamDescription() + array( + 'title' => 'The page to (un)watch. use titles instead', 'unwatch' => 'If set the page will be unwatched rather than watched', 'uselang' => 'Language to show the message in', - 'token' => 'A token previously acquired via prop=info', - ); - } - - public function getResultProperties() { - return array( - '' => array( - 'title' => 'string', - 'unwatched' => 'boolean', - 'watched' => 'boolean', - 'message' => 'string' - ) + 'continue' => 'When more results are available, use this to continue', ); } public function getDescription() { - return 'Add or remove a page from/to the current user\'s watchlist'; - } - - public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'notloggedin', 'info' => 'You must be logged-in to have a watchlist' ), - array( 'invalidtitle', 'title' ), - array( 'hookaborted' ), - ) ); + return 'Add or remove pages from/to the current user\'s watchlist.'; } public function getExamples() { return array( - 'api.php?action=watch&title=Main_Page' => 'Watch the page "Main Page"', - 'api.php?action=watch&title=Main_Page&unwatch=' => 'Unwatch the page "Main Page"', + 'api.php?action=watch&titles=Main_Page' => 'Watch the page "Main Page"', + 'api.php?action=watch&titles=Main_Page&unwatch=' => 'Unwatch the page "Main Page"', ); } |