diff options
Diffstat (limited to 'includes/api')
86 files changed, 4464 insertions, 1168 deletions
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index a586f688..875a3814 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -4,7 +4,7 @@ * * Created on Sep 5, 2006 * - * Copyright © 2006, 2010 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2006, 2010 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 @@ -51,9 +51,16 @@ abstract class ApiBase extends ContextSource { 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) + /// @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 + const LIMIT_BIG1 = 500; // Fast query, std user limit const LIMIT_BIG2 = 5000; // Fast query, bot/sysop limit const LIMIT_SML1 = 50; // Slow query, std user limit @@ -127,7 +134,7 @@ abstract class ApiBase extends ContextSource { /** * Get the name of the module as shown in the profiler log * - * @param $db DatabaseBase + * @param $db DatabaseBase|bool * * @return string */ @@ -280,12 +287,12 @@ abstract class ApiBase extends ContextSource { if ( is_numeric( $k ) ) { $msg .= " $v\n"; } else { - $v .= ":"; 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"; } } @@ -365,27 +372,38 @@ abstract class ApiBase extends ContextSource { $desc = implode( $paramPrefix, $desc ); } + //handle shorthand if ( !is_array( $paramSettings ) ) { $paramSettings = array( self::PARAM_DFLT => $paramSettings, ); } - $deprecated = isset( $paramSettings[self::PARAM_DEPRECATED] ) ? - $paramSettings[self::PARAM_DEPRECATED] : false; - if ( $deprecated ) { + //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"; } - $required = isset( $paramSettings[self::PARAM_REQUIRED] ) ? - $paramSettings[self::PARAM_REQUIRED] : false; - if ( $required ) { + 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 ) ) { - if ( isset( $paramSettings[self::PARAM_ISMULTI] ) && $paramSettings[self::PARAM_ISMULTI] ) { + $hintPipeSeparated = true; + $multi = isset( $paramSettings[self::PARAM_ISMULTI] ) ? $paramSettings[self::PARAM_ISMULTI] : false; + if ( $multi ) { $prompt = 'Values (separate with \'|\'): '; } else { $prompt = 'One value: '; @@ -393,7 +411,7 @@ abstract class ApiBase extends ContextSource { if ( is_array( $type ) ) { $choices = array(); - $nothingPrompt = false; + $nothingPrompt = ''; foreach ( $type as $t ) { if ( $t === '' ) { $nothingPrompt = 'Can be empty, or '; @@ -404,6 +422,7 @@ abstract class ApiBase extends ContextSource { $desc .= $paramPrefix . $nothingPrompt . $prompt; $choicesstring = implode( ', ', $choices ); $desc .= wordwrap( $choicesstring, 100, $descWordwrap ); + $hintPipeSeparated = false; } else { switch ( $type ) { case 'namespace': @@ -411,6 +430,7 @@ abstract class ApiBase extends ContextSource { $desc .= $paramPrefix . $prompt; $desc .= wordwrap( implode( ', ', MWNamespace::getValidNamespaces() ), 100, $descWordwrap ); + $hintPipeSeparated = false; break; case 'limit': $desc .= $paramPrefix . "No more than {$paramSettings[self :: PARAM_MAX]}"; @@ -420,37 +440,39 @@ abstract class ApiBase extends ContextSource { $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 must be no less than {$paramSettings[self::PARAM_MIN]}"; + $intRangeStr = "The value$s must be no less than {$paramSettings[self::PARAM_MIN]}"; } elseif ( !$hasMin ) { - $intRangeStr = "The value must be no more than {$paramSettings[self::PARAM_MAX]}"; + $intRangeStr = "The value$s must be no more than {$paramSettings[self::PARAM_MAX]}"; } else { - $intRangeStr = "The value must be between {$paramSettings[self::PARAM_MIN]} and {$paramSettings[self::PARAM_MAX]}"; + $intRangeStr = "The value$s must be between {$paramSettings[self::PARAM_MIN]} and {$paramSettings[self::PARAM_MAX]}"; } $desc .= $paramPrefix . $intRangeStr; } break; } + } - if ( isset( $paramSettings[self::PARAM_ISMULTI] ) ) { - $isArray = is_array( $paramSettings[self::PARAM_TYPE] ); + if ( $multi ) { + if ( $hintPipeSeparated ) { + $desc .= $paramPrefix . "Separate values with '|'"; + } - if ( !$isArray - || $isArray && count( $paramSettings[self::PARAM_TYPE] ) > self::LIMIT_SML1 ) { - $desc .= $paramPrefix . "Maximum number of values " . - self::LIMIT_SML1 . " (" . self::LIMIT_SML2 . " for bots)"; - } + $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 = is_array( $paramSettings ) - ? ( isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null ) - : $paramSettings; + $default = isset( $paramSettings[self::PARAM_DFLT] ) ? $paramSettings[self::PARAM_DFLT] : null; if ( !is_null( $default ) && $default !== false ) { $desc .= $paramPrefix . "Default: $default"; } @@ -512,7 +534,7 @@ abstract class ApiBase extends ContextSource { /** * Returns usage examples for this module. Return false if no examples are available. - * @return false|string|array + * @return bool|string|array */ protected function getExamples() { return false; @@ -523,7 +545,7 @@ abstract class ApiBase extends ContextSource { * value) or (parameter name) => (array with PARAM_* constants as keys) * Don't call this function directly: use getFinalParams() to allow * hooks to modify parameters as needed. - * @return array or false + * @return array|bool */ protected function getAllowedParams() { return false; @@ -533,7 +555,7 @@ abstract class ApiBase extends ContextSource { * Returns an array of parameter descriptions. * Don't call this functon directly: use getFinalParamDescription() to * allow hooks to modify descriptions as needed. - * @return array or false + * @return array|bool False on no parameter descriptions */ protected function getParamDescription() { return false; @@ -543,7 +565,7 @@ abstract class ApiBase extends ContextSource { * Get final list of parameters, after hooks have had a chance to * tweak it as needed. * - * @return array or false + * @return array|Bool False on no parameters */ public function getFinalParams() { $params = $this->getAllowedParams(); @@ -555,7 +577,7 @@ abstract class ApiBase extends ContextSource { * Get final parameter descriptions, after hooks have had a chance to tweak it as * needed. * - * @return array + * @return array|bool False on no parameter descriptions */ public function getFinalParamDescription() { $desc = $this->getParamDescription(); @@ -564,11 +586,56 @@ abstract class ApiBase extends ContextSource { } /** - * Get final module description, after hooks have had a chance to tweak it as + * 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 functon 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 + ); + } + } + + /** + * 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 ) ); @@ -630,14 +697,15 @@ abstract class ApiBase extends ContextSource { public function requireOnlyOneParameter( $params ) { $required = func_get_args(); array_shift( $required ); + $p = $this->getModulePrefix(); $intersection = array_intersect( array_keys( array_filter( $params, array( $this, "parameterNotEmpty" ) ) ), $required ); if ( count( $intersection ) > 1 ) { - $this->dieUsage( 'The parameters ' . implode( ', ', $intersection ) . ' can not be used together', 'invalidparammix' ); + $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', "{$p}invalidparammix" ); } elseif ( count( $intersection ) == 0 ) { - $this->dieUsage( 'One of the parameters ' . implode( ', ', $required ) . ' is required', 'missingparam' ); + $this->dieUsage( "One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required', "{$p}missingparam" ); } } @@ -665,12 +733,13 @@ abstract class ApiBase extends ContextSource { public function requireMaxOneParameter( $params ) { $required = func_get_args(); array_shift( $required ); + $p = $this->getModulePrefix(); $intersection = array_intersect( array_keys( array_filter( $params, array( $this, "parameterNotEmpty" ) ) ), $required ); if ( count( $intersection ) > 1 ) { - $this->dieUsage( 'The parameters ' . implode( ', ', $intersection ) . ' can not be used together', 'invalidparammix' ); + $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', "{$p}invalidparammix" ); } } @@ -690,6 +759,53 @@ abstract class ApiBase extends ContextSource { } /** + * @param $params array + * @param $load bool|string 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 + * - 'fromdbmaster': load from the master database + * @return WikiPage + */ + public function getTitleOrPageId( $params, $load = false ) { + $this->requireOnlyOneParameter( $params, 'title', 'pageid' ); + + $pageObj = null; + if ( isset( $params['title'] ) ) { + $titleObj = Title::newFromText( $params['title'] ); + if ( !$titleObj ) { + $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); + } + $pageObj = WikiPage::factory( $titleObj ); + if ( $load !== false ) { + $pageObj->loadPageData( $load ); + } + } elseif ( isset( $params['pageid'] ) ) { + if ( $load === false ) { + $load = 'fromdb'; + } + $pageObj = WikiPage::newFromID( $params['pageid'], $load ); + if ( !$pageObj ) { + $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) ); + } + } + + return $pageObj; + } + + /** + * @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 reequired parameters are set * * @param $x object Parameter to check is not null/false @@ -719,7 +835,7 @@ abstract class ApiBase extends ContextSource { */ protected function getWatchlistValue ( $watchlist, $titleObj, $userOption = null ) { - $userWatching = $titleObj->userIsWatching(); + $userWatching = $this->getUser()->isWatched( $titleObj ); switch ( $watchlist ) { case 'watch': @@ -773,7 +889,7 @@ abstract class ApiBase extends ContextSource { * Using the settings determine the value for the given parameter * * @param $paramName String: parameter name - * @param $paramSettings Mixed: default value or an array of settings + * @param $paramSettings array|mixed default value or an array of settings * using PARAM_* constants. * @param $parseLimit Boolean: parse limit? * @return mixed Parameter value @@ -809,8 +925,8 @@ abstract class ApiBase extends ContextSource { if ( $type == 'boolean' ) { if ( isset( $default ) && $default !== false ) { - // Having a default value of anything other than 'false' is pointless - ApiBase::dieDebug( __METHOD__, "Boolean param $encParamName's default is set to '$default'" ); + // 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." ); } $value = $this->getRequest()->getCheck( $encParamName ); @@ -1078,7 +1194,8 @@ abstract class ApiBase extends ContextSource { * @param $errorCode string Brief, arbitrary, stable string to allow easy * automated identification of the error, e.g., 'unknown_action' * @param $httpRespCode int HTTP response code - * @param $extradata array Data to add to the <error> element; array in ApiResult format + * @param $extradata array Data to add to the "<error>" element; array in ApiResult format + * @throws UsageException */ public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) { Profiler::instance()->close(); @@ -1155,6 +1272,8 @@ abstract class ApiBase extends ContextSource { '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' ), // API-specific messages 'readrequired' => array( 'code' => 'readapidenied', 'info' => "You need read permission to use this module" ), @@ -1186,7 +1305,6 @@ abstract class ApiBase extends ContextSource { '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" ), - 'nouploadmodule' => array( 'code' => 'nomodule', 'info' => 'No upload module set' ), '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' ), @@ -1202,12 +1320,14 @@ abstract class ApiBase extends ContextSource { '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\"" ), - 'filtered' => array( 'code' => 'filtered', 'info' => "The filter callback function refused your edit" ), 'contenttoobig' => array( 'code' => 'contenttoobig', 'info' => "The content you supplied exceeds the article size limit of \$1 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" ), @@ -1227,10 +1347,11 @@ abstract class ApiBase extends ContextSource { 'edit-already-exists' => array( 'code' => 'edit-already-exists', 'info' => "It seems the page you tried to create already exist" ), // uploadMsgs - 'invalid-session-key' => array( 'code' => 'invalid-session-key', 'info' => 'Not a valid session key' ), + '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.' ), 'filename-tooshort' => array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ), 'filename-toolong' => array( 'code' => 'filename-toolong', 'info' => 'The filename is too long' ), @@ -1280,10 +1401,9 @@ abstract class ApiBase extends ContextSource { } if ( isset( self::$messageMap[$key] ) ) { - return array( 'code' => - wfMsgReplaceArgs( self::$messageMap[$key]['code'], $error ), - 'info' => - wfMsgReplaceArgs( self::$messageMap[$key]['info'], $error ) + return array( + 'code' => wfMsgReplaceArgs( self::$messageMap[$key]['code'], $error ), + 'info' => wfMsgReplaceArgs( self::$messageMap[$key]['info'], $error ) ); } @@ -1332,7 +1452,9 @@ abstract class ApiBase extends ContextSource { } /** - * Returns whether this module requires a Token to execute + * 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() { @@ -1340,8 +1462,12 @@ abstract class ApiBase extends ContextSource { } /** - * 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 - * @return bool|string + * 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; @@ -1373,7 +1499,7 @@ abstract class ApiBase extends ContextSource { } /** - * @return false|string|array Returns a false if the module has no help url, else returns a (array of) string + * @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; diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index 351ac6b7..c879b35d 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -4,7 +4,7 @@ * * Created on Sep 4, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<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 @@ -47,7 +47,7 @@ class ApiBlock extends ApiBase { $params = $this->extractRequestParams(); if ( $params['gettoken'] ) { - $res['blocktoken'] = $user->getEditToken( '', $this->getMain()->getRequest() ); + $res['blocktoken'] = $user->getEditToken(); $this->getResult()->addValue( null, $this->getModuleName(), $res ); return; } @@ -72,9 +72,9 @@ class ApiBlock extends ApiBase { $data = array( 'Target' => $params['user'], 'Reason' => array( - is_null( $params['reason'] ) ? '' : $params['reason'], + $params['reason'], 'other', - is_null( $params['reason'] ) ? '' : $params['reason'] + $params['reason'] ), 'Expiry' => $params['expiry'] == 'never' ? 'infinite' : $params['expiry'], 'HardBlock' => !$params['anononly'], @@ -100,12 +100,14 @@ class ApiBlock extends ApiBase { $block = Block::newFromTarget( $target ); if( $block instanceof Block ){ - $res['expiry'] = $block->mExpiry == wfGetDB( DB_SLAVE )->getInfinity() + $res['expiry'] = $block->mExpiry == $this->getDB()->getInfinity() ? 'infinite' : wfTimestamp( TS_ISO_8601, $block->mExpiry ); + $res['id'] = $block->getId(); } else { # should be unreachable $res['expiry'] = ''; + $res['id'] = ''; } $res['reason'] = $params['reason']; @@ -149,9 +151,12 @@ class ApiBlock extends ApiBase { ApiBase::PARAM_REQUIRED => true ), 'token' => null, - 'gettoken' => false, + 'gettoken' => array( + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_DEPRECATED => true, + ), 'expiry' => 'never', - 'reason' => null, + 'reason' => '', 'anononly' => false, 'nocreate' => false, 'autoblock' => false, @@ -166,10 +171,10 @@ 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 the gettoken parameter or prop=info', + 'token' => 'A block token previously obtained through prop=info', 'gettoken' => 'If set, a block token will be returned, and no other action will be taken', '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 (optional)', + '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', @@ -181,6 +186,44 @@ class ApiBlock extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'blocktoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + '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'; } diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index 4bb94c4a..ed72b29b 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -32,8 +32,8 @@ class ApiComparePages extends ApiBase { public function execute() { $params = $this->extractRequestParams(); - $rev1 = $this->revisionOrTitle( $params['fromrev'], $params['fromtitle'] ); - $rev2 = $this->revisionOrTitle( $params['torev'], $params['totitle'] ); + $rev1 = $this->revisionOrTitleOrId( $params['fromrev'], $params['fromtitle'], $params['fromid'] ); + $rev2 = $this->revisionOrTitleOrId( $params['torev'], $params['totitle'], $params['toid'] ); $de = new DifferenceEngine( $this->getContext(), $rev1, @@ -46,10 +46,16 @@ class ApiComparePages extends ApiBase { if ( isset( $params['fromtitle'] ) ) { $vals['fromtitle'] = $params['fromtitle']; } + if ( isset( $params['fromid'] ) ) { + $vals['fromid'] = $params['fromid']; + } $vals['fromrevid'] = $rev1; if ( isset( $params['totitle'] ) ) { $vals['totitle'] = $params['totitle']; } + if ( isset( $params['toid'] ) ) { + $vals['toid'] = $params['toid']; + } $vals['torevid'] = $rev2; $difftext = $de->getDiffBody(); @@ -67,9 +73,10 @@ class ApiComparePages extends ApiBase { /** * @param $revision int * @param $titleText string + * @param $titleId int * @return int */ - private function revisionOrTitle( $revision, $titleText ) { + private function revisionOrTitleOrId( $revision, $titleText, $titleId ) { if( $revision ){ return $revision; } elseif( $titleText ) { @@ -78,17 +85,29 @@ class ApiComparePages extends ApiBase { $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 or a revision number is needed for both the from and the to parameters' ); + $this->dieUsage( 'inputneeded', 'A title, a page ID, or a revision number is needed for both the from and the to parameters' ); } public function getAllowedParams() { return array( 'fromtitle' => null, + 'fromid' => array( + ApiBase::PARAM_TYPE => 'integer' + ), 'fromrev' => array( ApiBase::PARAM_TYPE => 'integer' ), 'totitle' => null, + 'toid' => array( + ApiBase::PARAM_TYPE => 'integer' + ), 'torev' => array( ApiBase::PARAM_TYPE => 'integer' ), @@ -98,15 +117,36 @@ class ApiComparePages extends ApiBase { public function getParamDescription() { return array( 'fromtitle' => 'First title to compare', + 'fromid' => 'First page ID to compare', 'fromrev' => 'First revision to compare', 'totitle' => 'Second title to compare', + 'toid' => 'Second page ID to compare', 'torev' => 'Second revision to compare', ); } + + 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 for each part (1 and 2)' + 'You must pass a revision number or a page title or a page ID id for each part (1 and 2)' ); } @@ -114,6 +154,7 @@ class ApiComparePages extends ApiBase { 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.' ), ) ); } diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index cfaf6cc1..2d36f19a 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -4,7 +4,7 @@ * * Created on Jun 30, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<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 @@ -46,35 +46,24 @@ class ApiDelete extends ApiBase { public function execute() { $params = $this->extractRequestParams(); - $this->requireOnlyOneParameter( $params, 'title', 'pageid' ); - - if ( isset( $params['title'] ) ) { - $titleObj = Title::newFromText( $params['title'] ); - if ( !$titleObj ) { - $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); - } - } elseif ( isset( $params['pageid'] ) ) { - $titleObj = Title::newFromID( $params['pageid'] ); - if ( !$titleObj ) { - $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) ); - } - } - if ( !$titleObj->exists() ) { + $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' ); + if ( !$pageObj->exists() ) { $this->dieUsageMsg( 'notanarticle' ); } - $reason = ( isset( $params['reason'] ) ? $params['reason'] : null ); - $pageObj = WikiPage::factory( $titleObj ); + $titleObj = $pageObj->getTitle(); + $reason = $params['reason']; $user = $this->getUser(); if ( $titleObj->getNamespace() == NS_FILE ) { - $retval = self::deleteFile( $pageObj, $user, $params['token'], $params['oldimage'], $reason, false ); + $status = self::deleteFile( $pageObj, $user, $params['token'], $params['oldimage'], $reason, false ); } else { - $retval = self::delete( $pageObj, $user, $params['token'], $reason ); + $status = self::delete( $pageObj, $user, $params['token'], $reason ); } - if ( count( $retval ) ) { - $this->dieUsageMsg( reset( $retval ) ); // We don't care about multiple errors, just report one of them + if ( !$status->isGood() ) { + $errors = $status->getErrorsArray(); + $this->dieUsageMsg( $errors[0] ); // We don't care about multiple errors, just report one of them } // Deprecated parameters @@ -87,7 +76,11 @@ class ApiDelete extends ApiBase { } $this->setWatch( $watch, $titleObj, 'watchdeletion' ); - $r = array( 'title' => $titleObj->getPrefixedText(), 'reason' => $reason ); + $r = array( + 'title' => $titleObj->getPrefixedText(), + 'reason' => $reason, + 'logid' => $status->value + ); $this->getResult()->addValue( null, $this->getModuleName(), $r ); } @@ -109,7 +102,7 @@ class ApiDelete extends ApiBase { * @param $user User doing the action * @param $token String: delete token (same as edit token) * @param $reason String: reason for the deletion. Autogenerated if NULL - * @return Title::getUserPermissionsErrors()-like array + * @return Status */ public static function delete( Page $page, User $user, $token, &$reason = null ) { $title = $page->getTitle(); @@ -131,11 +124,7 @@ class ApiDelete extends ApiBase { $error = ''; // Luckily, Article.php provides a reusable delete function that does the hard work for us - if ( $page->doDeleteArticle( $reason, false, 0, true, $error ) ) { - return array(); - } else { - return array( array( 'cannotdelete', $title->getPrefixedText() ) ); - } + return $page->doDeleteArticleReal( $reason, false, 0, true, $error ); } /** @@ -145,7 +134,7 @@ class ApiDelete extends ApiBase { * @param $oldimage * @param $reason * @param $suppress bool - * @return \type|array|Title + * @return Status */ public static function deleteFile( Page $page, User $user, $token, $oldimage, &$reason = null, $suppress = false ) { $title = $page->getTitle(); @@ -167,19 +156,12 @@ class ApiDelete extends ApiBase { if ( !$oldfile->exists() || !$oldfile->isLocal() || $oldfile->getRedirected() ) { return array( array( 'nodeleteablefile' ) ); } - } else { - $oldfile = false; } if ( is_null( $reason ) ) { // Log and RC don't like null reasons $reason = ''; } - $status = FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress ); - if ( !$status->isGood() ) { - return array( array( 'cannotdelete', $title->getPrefixedText() ) ); - } - - return array(); + return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress ); } public function mustBePosted() { @@ -196,7 +178,10 @@ class ApiDelete extends ApiBase { 'pageid' => array( ApiBase::PARAM_TYPE => 'integer' ), - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'reason' => null, 'watch' => array( ApiBase::PARAM_DFLT => false, @@ -233,16 +218,24 @@ class ApiDelete extends ApiBase { ); } + 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->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ), + $this->getTitleOrPageIdErrorMessage(), array( - array( 'invalidtitle', 'title' ), - array( 'nosuchpageid', 'pageid' ), array( 'notanarticle' ), array( 'hookaborted', 'error' ), array( 'delete-toobig', 'limit' ), diff --git a/includes/api/ApiDisabled.php b/includes/api/ApiDisabled.php index 55754896..13975aec 100644 --- a/includes/api/ApiDisabled.php +++ b/includes/api/ApiDisabled.php @@ -4,7 +4,7 @@ * * Created on Sep 25, 2008 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<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 diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index 9ed6d08d..0963fe7c 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -4,7 +4,7 @@ * * Created on August 16, 2007 * - * Copyright © 2007 Iker Labarga <Firstname><Lastname>@gmail.com + * Copyright © 2007 Iker Labarga "<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 @@ -48,8 +48,9 @@ class ApiEditPage extends ApiBase { $this->dieUsageMsg( 'missingtext' ); } - $titleObj = Title::newFromText( $params['title'] ); - if ( !$titleObj || $titleObj->isExternal() ) { + $pageObj = $this->getTitleOrPageId( $params ); + $titleObj = $pageObj->getTitle(); + if ( $titleObj->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); } @@ -59,7 +60,11 @@ class ApiEditPage extends ApiBase { if ( $titleObj->isRedirect() ) { $oldTitle = $titleObj; - $titles = Title::newFromRedirectArray( Revision::newFromTitle( $oldTitle )->getText( Revision::FOR_THIS_USER ) ); + $titles = Title::newFromRedirectArray( + Revision::newFromTitle( + $oldTitle, false, Revision::READ_LATEST + )->getText( Revision::FOR_THIS_USER ) + ); // array_shift( $titles ); $redirValues = array(); @@ -161,7 +166,7 @@ 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'] = wfMsgForContent( 'undo-summary', $params['undo'], $undoRev->getUserText() ); + $params['summary'] = wfMessage( 'undo-summary', $params['undo'], $undoRev->getUserText() )->inContentLanguage()->text(); } } @@ -181,7 +186,7 @@ class ApiEditPage extends ApiBase { if ( !is_null( $params['summary'] ) ) { $requestArray['wpSummary'] = $params['summary']; } - + if ( !is_null( $params['sectiontitle'] ) ) { $requestArray['wpSectionTitle'] = $params['sectiontitle']; } @@ -282,9 +287,6 @@ class ApiEditPage extends ApiBase { case EditPage::AS_SPAM_ERROR: $this->dieUsageMsg( array( 'spamdetected', $result['spam'] ) ); - case EditPage::AS_FILTERING: - $this->dieUsageMsg( 'filtered' ); - case EditPage::AS_BLOCKED_PAGE_FOR_USER: $this->dieUsageMsg( 'blockedtext' ); @@ -342,16 +344,11 @@ class ApiEditPage extends ApiBase { $this->dieUsageMsg( 'summaryrequired' ); case EditPage::AS_END: + default: // $status came from WikiPage::doEdit() $errors = $status->getErrorsArray(); $this->dieUsageMsg( $errors[0] ); // TODO: Add new errors to message map break; - default: - if ( is_string( $status->value ) && strlen( $status->value ) ) { - $this->dieUsage( "An unknown return value was returned by Editpage. The code returned was \"{$status->value}\"" , $status->value ); - } else { - $this->dieUsageMsg( array( 'unknownerror', $status->value ) ); - } } $apiResult->addValue( null, $this->getModuleName(), $r ); } @@ -371,45 +368,48 @@ class ApiEditPage extends ApiBase { public function getPossibleErrors() { global $wgMaxArticleSize; - return array_merge( parent::getPossibleErrors(), array( - array( 'missingtext' ), - array( 'invalidtitle', 'title' ), - array( 'createonly-exists' ), - array( 'nocreate-missing' ), - array( 'nosuchrevid', 'undo' ), - array( 'nosuchrevid', 'undoafter' ), - array( 'revwrongpage', 'id', 'text' ), - array( 'undo-failure' ), - array( 'hashcheckfailed' ), - array( 'hookaborted' ), - array( 'noimageredirect-anon' ), - array( 'noimageredirect-logged' ), - array( 'spamdetected', 'spam' ), - array( 'summaryrequired' ), - array( 'filtered' ), - 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( 'customcssprotected' ), - array( 'customjsprotected' ), - ) ); + 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( '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( 'customcssprotected' ), + array( 'customjsprotected' ), + ) + ); } public function getAllowedParams() { return array( 'title' => array( ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true + ), + 'pageid' => array( + ApiBase::PARAM_TYPE => 'integer', ), 'section' => null, 'sectiontitle' => array( @@ -417,7 +417,10 @@ class ApiEditPage extends ApiBase { ApiBase::PARAM_REQUIRED => false, ), 'text' => null, - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'summary' => null, 'minor' => false, 'notminor' => false, @@ -463,19 +466,20 @@ class ApiEditPage extends ApiBase { public function getParamDescription() { $p = $this->getModulePrefix(); return array( - 'title' => 'Page title', + '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 text parameter' + "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 section=new', + '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.' + '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' @@ -489,13 +493,49 @@ class ApiEditPage extends ApiBase { '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' => "Add this text to the end 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" ), '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', ); } + 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; } diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php index d9eed60c..4fa03434 100644 --- a/includes/api/ApiEmailUser.php +++ b/includes/api/ApiEmailUser.php @@ -55,7 +55,7 @@ class ApiEmailUser extends ApiBase { 'Subject' => $params['subject'], 'CCMe' => $params['ccme'], ); - $retval = SpecialEmailUser::submit( $data ); + $retval = SpecialEmailUser::submit( $data, $this->getContext() ); if ( $retval instanceof Status ) { // SpecialEmailUser sometimes returns a status @@ -98,7 +98,10 @@ class ApiEmailUser extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'ccme' => false, ); } @@ -113,6 +116,23 @@ class ApiEmailUser extends ApiBase { ); } + 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.'; } diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php index d570534d..160f5b91 100644 --- a/includes/api/ApiExpandTemplates.php +++ b/includes/api/ApiExpandTemplates.php @@ -4,7 +4,7 @@ * * Created on Oct 05, 2007 * - * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2007 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 @@ -103,6 +103,14 @@ class ApiExpandTemplates extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + '*' => 'string' + ) + ); + } + public function getDescription() { return 'Expands all templates in wikitext'; } diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index 4e70bde2..1cf760ae 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -60,7 +60,7 @@ class ApiFeedContributions extends ApiBase { $this->dieUsage( 'Size difference is disabled in Miser Mode', 'sizediffdisabled' ); } - $msg = wfMsgForContent( 'Contributions' ); + $msg = wfMessage( 'Contributions' )->inContentLanguage()->text(); $feedTitle = $wgSitename . ' - ' . $msg . ' [' . $wgLanguageCode . ']'; $feedUrl = SpecialPage::getTitleFor( 'Contributions', $params['user'] )->getFullURL(); @@ -96,7 +96,7 @@ class ApiFeedContributions extends ApiBase { } protected function feedItem( $row ) { - $title = Title::MakeTitle( intval( $row->page_namespace ), $row->page_title ); + $title = Title::makeTitle( intval( $row->page_namespace ), $row->page_title ); if( $title ) { $date = $row->rev_timestamp; $comments = $title->getTalkPage()->getFullURL(); @@ -129,7 +129,8 @@ class ApiFeedContributions extends ApiBase { */ protected function feedItemDesc( $revision ) { if( $revision ) { - return '<p>' . htmlspecialchars( $revision->getUserText() ) . wfMsgForContent( 'colon-separator' ) . + $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text(); + return '<p>' . htmlspecialchars( $revision->getUserText() ) . $msg . htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) . "</p>\n<hr />\n<div>" . nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>"; @@ -150,8 +151,7 @@ class ApiFeedContributions extends ApiBase { ApiBase::PARAM_REQUIRED => true, ), 'namespace' => array( - ApiBase::PARAM_TYPE => 'namespace', - ApiBase::PARAM_ISMULTI => true + ApiBase::PARAM_TYPE => 'namespace' ), 'year' => array( ApiBase::PARAM_TYPE => 'integer' diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index eee8fa19..6ccb02fe 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -4,7 +4,7 @@ * * Created on Oct 13, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -117,7 +117,7 @@ class ApiFeedWatchlist extends ApiBase { $feedItems[] = $this->createFeedItem( $info ); } - $msg = wfMsgForContent( 'watchlist' ); + $msg = wfMessage( 'watchlist' )->inContentLanguage()->text(); $feedTitle = $wgSitename . ' - ' . $msg . ' [' . $wgLanguageCode . ']'; $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL(); @@ -131,11 +131,12 @@ class ApiFeedWatchlist extends ApiBase { // Error results should not be cached $this->getMain()->setCacheMaxAge( 0 ); - $feedTitle = $wgSitename . ' - Error - ' . wfMsgForContent( 'watchlist' ) . ' [' . $wgLanguageCode . ']'; + $feedTitle = $wgSitename . ' - Error - ' . wfMessage( 'watchlist' )->inContentLanguage()->text() . ' [' . $wgLanguageCode . ']'; $feedUrl = SpecialPage::getTitleFor( 'Watchlist' )->getFullURL(); $feedFormat = isset( $params['feedformat'] ) ? $params['feedformat'] : 'rss'; - $feed = new $wgFeedClasses[$feedFormat] ( $feedTitle, htmlspecialchars( wfMsgForContent( 'watchlist' ) ), $feedUrl ); + $msg = wfMessage( 'watchlist' )->inContentLanguage()->escaped(); + $feed = new $wgFeedClasses[$feedFormat] ( $feedTitle, $msg, $feedUrl ); if ( $e instanceof UsageException ) { $errorCode = $e->getCodeString(); diff --git a/includes/api/ApiFileRevert.php b/includes/api/ApiFileRevert.php index 7ef1da0a..83d078d2 100644 --- a/includes/api/ApiFileRevert.php +++ b/includes/api/ApiFileRevert.php @@ -71,9 +71,10 @@ class ApiFileRevert extends ApiBase { * @param $user User The user to check. */ protected function checkPermissions( $user ) { + $title = $this->file->getTitle(); $permissionErrors = array_merge( - $this->file->getTitle()->getUserPermissionsErrors( 'edit' , $user ), - $this->file->getTitle()->getUserPermissionsErrors( 'upload' , $user ) + $title->getUserPermissionsErrors( 'edit' , $user ), + $title->getUserPermissionsErrors( 'upload' , $user ) ); if ( $permissionErrors ) { @@ -91,15 +92,17 @@ class ApiFileRevert extends ApiBase { if ( is_null( $title ) ) { $this->dieUsageMsg( array( 'invalidtitle', $this->params['filename'] ) ); } + $localRepo = RepoGroup::singleton()->getLocalRepo(); + // Check if the file really exists - $this->file = wfLocalFile( $title ); + $this->file = $localRepo->newFile( $title ); if ( !$this->file->exists() ) { $this->dieUsageMsg( 'notanarticle' ); } // Check if the archivename is valid for this file $this->archiveName = $this->params['archivename']; - $oldFile = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $this->archiveName ); + $oldFile = $localRepo->newFromArchiveName( $title, $this->archiveName ); if ( !$oldFile->exists() ) { $this->dieUsageMsg( 'filerevert-badversion' ); } @@ -126,21 +129,38 @@ class ApiFileRevert extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true, ), - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), ); } public function getParamDescription() { - $params = array( - 'filename' => 'Target filename', + 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', ); + } - return $params; - + 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() { diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index 1eee717a..8ad9b8ca 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -4,7 +4,7 @@ * * Created on Sep 19, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -169,8 +169,10 @@ abstract class ApiFormatBase extends ApiBase { <br /> <small> You are looking at the HTML representation of the <?php echo( $this->mFormat ); ?> format.<br /> -HTML is good for debugging, but probably is not suitable for your application.<br /> -See <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, or +HTML is good for debugging, but is unsuitable for application use.<br /> +Specify the format parameter to change the output format.<br /> +To see the non HTML representation of the <?php echo( $this->mFormat ); ?> format, set format=<?php echo( strtolower( $this->mFormat ) ); ?>.<br /> +See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, or <a href='<?php echo( $script ); ?>'>API help</a> for more information. </small> <?php @@ -264,11 +266,12 @@ See <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, or $text = htmlspecialchars( $text ); // encode all comments or tags as safe blue strings - $text = preg_replace( '/\<(!--.*?--|.*?)\>/', '<span style="color:blue;"><\1></span>', $text ); + $text = str_replace( '<', '<span style="color:blue;"><', $text ); + $text = str_replace( '>', '></span>', $text ); // identify URLs $protos = wfUrlProtocolsWithoutProtRel(); // This regex hacks around bug 13218 (" included in the URL) - $text = preg_replace( "#(($protos).*?)(")?([ \\'\"<>\n]|<|>|")#", '<a href="\\1">\\1</a>\\3\\4', $text ); + $text = preg_replace( "#(((?i)$protos).*?)(")?([ \\'\"<>\n]|<|>|")#", '<a href="\\1">\\1</a>\\3\\4', $text ); // identify requests to api.php $text = preg_replace( "#api\\.php\\?[^ <\n\t]+#", '<a href="\\0">\\0</a>', $text ); if ( $this->mHelp ) { @@ -329,7 +332,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase { */ public static function setResult( $result, $feed, $feedItems ) { // Store output in the Result data. - // This way we can check during execution if any error has occured + // 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 @@ -374,7 +377,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase { } $feed->outFooter(); } else { - // Error has occured, print something useful + // Error has occurred, print something useful ApiBase::dieDebug( __METHOD__, 'Invalid feed class/item' ); } } diff --git a/includes/api/ApiFormatDbg.php b/includes/api/ApiFormatDbg.php index 92619f76..3d2a39ca 100644 --- a/includes/api/ApiFormatDbg.php +++ b/includes/api/ApiFormatDbg.php @@ -4,7 +4,7 @@ * * Created on Oct 22, 2006 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<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 diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index e728d057..acbc7d3b 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -4,7 +4,7 @@ * * Created on Sep 19, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php index 60552c40..fac2ca58 100644 --- a/includes/api/ApiFormatPhp.php +++ b/includes/api/ApiFormatPhp.php @@ -4,7 +4,7 @@ * * Created on Oct 22, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 diff --git a/includes/api/ApiFormatRaw.php b/includes/api/ApiFormatRaw.php index db81aacd..184f0a34 100644 --- a/includes/api/ApiFormatRaw.php +++ b/includes/api/ApiFormatRaw.php @@ -4,7 +4,7 @@ * * Created on Feb 2, 2009 * - * Copyright © 2009 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2009 Roan Kattouw "<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 diff --git a/includes/api/ApiFormatTxt.php b/includes/api/ApiFormatTxt.php index e26b82b0..71414593 100644 --- a/includes/api/ApiFormatTxt.php +++ b/includes/api/ApiFormatTxt.php @@ -4,7 +4,7 @@ * * Created on Oct 22, 2006 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<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 diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php index 1bc9d025..65056e44 100644 --- a/includes/api/ApiFormatWddx.php +++ b/includes/api/ApiFormatWddx.php @@ -4,7 +4,7 @@ * * Created on Oct 22, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php index 8f4abc15..5ccf1859 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -4,7 +4,7 @@ * * Created on Sep 19, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -83,16 +83,40 @@ class ApiFormatXml extends ApiFormatBase { /** * This method takes an array and converts it to XML. + * * There are several noteworthy cases: * - * If array contains a key '_element', then the code assumes that ALL other keys are not important and replaces them with the value['_element']. - * Example: name='root', value = array( '_element'=>'page', 'x', 'y', 'z') creates <root> <page>x</page> <page>y</page> <page>z</page> </root> + * If array contains a key '_element', then the code assumes that ALL + * other keys are not important and replaces them with the + * value['_element']. + * + * @par Example: + * @verbatim + * name='root', value = array( '_element'=>'page', 'x', 'y', 'z') + * @endverbatim + * creates: + * @verbatim + * <root> <page>x</page> <page>y</page> <page>z</page> </root> + * @endverbatim + * + * If any of the array's element key is '*', then the code treats all + * other key->value pairs as attributes, and the value['*'] as the + * element's content. + * + * @par Example: + * @verbatim + * name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10) + * @endverbatim + * creates: + * @verbatim + * <root lang='en' id='10'>text</root> + * @endverbatim * - * If any of the array's element key is '*', then the code treats all other key->value pairs as attributes, and the value['*'] as the element's content. - * Example: name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10) creates <root lang='en' id='10'>text</root> + * Finally neither key is found, all keys become element names, and values + * become element content. * - * If neither key is found, all keys become element names, and values become element content. - * The method is recursive, so the same rules apply to any sub-arrays. + * @note The method is recursive, so the same rules apply to any + * sub-arrays. * * @param $elemName * @param $elemValue @@ -215,7 +239,8 @@ class ApiFormatXml extends ApiFormatBase { public function getParamDescription() { return array( 'xmldoublequote' => 'If specified, double quotes all attributes and content', - 'xslt' => 'If specified, adds <xslt> as stylesheet', + 'xslt' => 'If specified, adds <xslt> as stylesheet. This should be a wiki page ' + . 'in the MediaWiki namespace whose page name ends with ".xsl"', 'includexmlnamespace' => 'If specified, adds an XML namespace' ); } diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php index dbcdb21c..730ad8ea 100644 --- a/includes/api/ApiFormatYaml.php +++ b/includes/api/ApiFormatYaml.php @@ -4,7 +4,7 @@ * * Created on Sep 19, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index 97da786b..2b5de21a 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -4,7 +4,7 @@ * * Created on Sep 6, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php index ade9f1f3..637c1fff 100644 --- a/includes/api/ApiImport.php +++ b/includes/api/ApiImport.php @@ -4,7 +4,7 @@ * * Created on Feb 4, 2009 * - * Copyright © 2009 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2009 Roan Kattouw "<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 @@ -68,6 +68,12 @@ class ApiImport extends ApiBase { if ( isset( $params['namespace'] ) ) { $importer->setTargetNamespace( $params['namespace'] ); } + if ( isset( $params['rootpage'] ) ) { + $statusRootPage = $importer->setTargetRootPage( $params['rootpage'] ); + if( !$statusRootPage->isGood() ) { + $this->dieUsageMsg( $statusRootPage->getErrorsArray() ); + } + } $reporter = new ApiImportReporter( $importer, $isUpload, @@ -98,7 +104,10 @@ class ApiImport extends ApiBase { public function getAllowedParams() { global $wgImportSources; return array( - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'summary' => null, 'xml' => null, 'interwikisource' => array( @@ -109,7 +118,8 @@ class ApiImport extends ApiBase { 'templates' => false, 'namespace' => array( ApiBase::PARAM_TYPE => 'namespace' - ) + ), + 'rootpage' => null, ); } @@ -123,6 +133,18 @@ class ApiImport extends ApiBase { 'fullhistory' => 'For interwiki imports: import the full history, not just the current version', 'templates' => 'For interwiki imports: import all included templates as well', 'namespace' => 'For interwiki imports: import to this namespace', + 'rootpage' => 'Import as subpage of this page', + ); + } + + public function getResultProperties() { + return array( + ApiBase::PROP_LIST => true, + '' => array( + 'ns' => 'namespace', + 'title' => 'string', + 'revisions' => 'integer' + ) ); } @@ -141,6 +163,8 @@ class ApiImport extends ApiBase { array( 'cantimport-upload' ), array( 'import-unknownerror', 'source' ), array( 'import-unknownerror', 'result' ), + array( 'import-rootpage-nosubpage', 'namespace' ), + array( 'import-rootpage-invalid' ), ) ); } @@ -186,8 +210,16 @@ class ApiImportReporter extends ImportReporter { function reportPage( $title, $origTitle, $revisionCount, $successCount, $pageInfo ) { // Add a result entry $r = array(); - ApiQueryBase::addTitleInfo( $r, $title ); - $r['revisions'] = intval( $successCount ); + + if ( $title === null ) { + # Invalid or non-importable title + $r['title'] = $pageInfo['title']; + $r['invalid'] = ''; + } else { + ApiQueryBase::addTitleInfo( $r, $title ); + $r['revisions'] = intval( $successCount ); + } + $this->mResultArr[] = $r; // Piggyback on the parent to do the logging diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index aa570cbc..1f91fe92 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -4,7 +4,7 @@ * * Created on Sep 19, 2006 * - * Copyright © 2006-2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com, + * Copyright © 2006-2007 Yuri Astrakhan "<Firstname><Lastname>@gmail.com", * Daniel Cannon (cannon dot danielc at gmail dot com) * * This program is free software; you can redistribute it and/or modify @@ -79,6 +79,8 @@ class ApiLogin extends ApiBase { $user->setOption( 'rememberpassword', 1 ); $user->setCookies( $this->getRequest() ); + ApiQueryInfo::resetTokenCache(); + // Run hooks. // @todo FIXME: Split back and frontend from this hook. // @todo FIXME: This hook should be placed in the backend @@ -181,6 +183,66 @@ 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. ', diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php index 81a054a6..b2f634d0 100644 --- a/includes/api/ApiLogout.php +++ b/includes/api/ApiLogout.php @@ -4,7 +4,7 @@ * * Created on Jan 4, 2008 * - * Copyright © 2008 Yuri Astrakhan <Firstname><Lastname>@gmail.com, + * Copyright © 2008 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 @@ -54,6 +54,10 @@ class ApiLogout extends ApiBase { return array(); } + public function getResultProperties() { + return array(); + } + public function getParamDescription() { return array(); } diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index fa95cfca..35febd95 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -4,7 +4,7 @@ * * Created on Sep 4, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -61,9 +61,11 @@ class ApiMain extends ApiBase { 'paraminfo' => 'ApiParamInfo', 'rsd' => 'ApiRsd', 'compare' => 'ApiComparePages', + 'tokens' => 'ApiTokens', // Write modules 'purge' => 'ApiPurge', + 'setnotificationtimestamp' => 'ApiSetNotificationTimestamp', 'rollback' => 'ApiRollback', 'delete' => 'ApiDelete', 'undelete' => 'ApiUndelete', @@ -79,6 +81,7 @@ class ApiMain extends ApiBase { 'patrol' => 'ApiPatrol', 'import' => 'ApiImport', 'userrights' => 'ApiUserrights', + 'options' => 'ApiOptions', ); /** @@ -352,6 +355,12 @@ class ApiMain extends ApiBase { * have been accumulated, and replace it with an error message and a help screen. */ protected function executeActionWithErrorHandling() { + // Verify the CORS header before executing the action + if ( !$this->handleCORS() ) { + // handleCORS() has sent a 403, abort + return; + } + // In case an error occurs during data output, // clear the output buffer and print just the error information ob_start(); @@ -359,8 +368,11 @@ 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 MWException ) { + if ( !( $e instanceof UsageException ) ) { wfDebugLog( 'exception', $e->getLogMessage() ); } @@ -384,7 +396,7 @@ class ApiMain extends ApiBase { // Reset and print just the error message ob_clean(); - // If the error occured during printing, do a printer->profileOut() + // If the error occurred during printing, do a printer->profileOut() $this->mPrinter->safeProfileOut(); $this->printResult( true ); } @@ -400,9 +412,101 @@ class ApiMain extends ApiBase { ob_end_flush(); } + /** + * Check the &origin= query parameter against the Origin: HTTP header and respond appropriately. + * + * If no origin parameter is present, nothing happens. + * If an origin parameter is present but doesn't match the Origin header, a 403 status code + * is set and false is returned. + * If the parameter and the header do match, the header is checked against $wgCrossSiteAJAXdomains + * and $wgCrossSiteAJAXdomainExceptions, and if the origin qualifies, the appropriate CORS + * headers are set. + * + * @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 + return true; + } + + $request = $this->getRequest(); + $response = $request->response(); + // Origin: header is a space-separated list of origins, check all of them + $originHeader = $request->getHeader( 'Origin' ); + if ( $originHeader === false ) { + $origins = array(); + } else { + $origins = explode( ' ', $originHeader ); + } + if ( !in_array( $originParam, $origins ) ) { + // origin parameter set but incorrect + // Send a 403 response + $message = HttpStatus::getMessage( 403 ); + $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 ) ) { + $response->header( "Access-Control-Allow-Origin: $originParam" ); + $response->header( 'Access-Control-Allow-Credentials: true' ); + $this->getOutput()->addVaryHeader( 'Origin' ); + } + return true; + } + + /** + * Attempt to match an Origin header against a set of rules and a set of exceptions + * @param $value string Origin header + * @param $rules array Set of wildcard rules + * @param $exceptions array Set of wildcard rules + * @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 ) { + if ( preg_match( self::wildcardToRegex( $rule ), $value ) ) { + // Rule matches, check exceptions + foreach ( $exceptions as $exc ) { + if ( preg_match( self::wildcardToRegex( $exc ), $value ) ) { + return false; + } + } + return true; + } + } + return false; + } + + /** + * Helper function to convert wildcard string into a regex + * '*' => '.*?' + * '?' => '.' + * + * @param $wildcard string String with wildcards + * @return string Regular expression + */ + protected static function wildcardToRegex( $wildcard ) { + $wildcard = preg_quote( $wildcard, '/' ); + $wildcard = str_replace( + array( '\*', '\?' ), + array( '.*?', '.' ), + $wildcard + ); + return "/https?:\/\/$wildcard/"; + } + protected function sendCacheHeaders() { global $wgUseXVO, $wgVaryOnXFP; $response = $this->getRequest()->response(); + $out = $this->getOutput(); + + if ( $wgVaryOnXFP ) { + $out->addVaryHeader( 'X-Forwarded-Proto' ); + } if ( $this->mCacheMode == 'private' ) { $response->header( 'Cache-Control: private' ); @@ -410,13 +514,9 @@ class ApiMain extends ApiBase { } if ( $this->mCacheMode == 'anon-public-user-private' ) { - $xfp = $wgVaryOnXFP ? ', X-Forwarded-Proto' : ''; - $response->header( 'Vary: Accept-Encoding, Cookie' . $xfp ); + $out->addVaryHeader( 'Cookie' ); + $response->header( $out->getVaryHeader() ); if ( $wgUseXVO ) { - $out = $this->getOutput(); - if ( $wgVaryOnXFP ) { - $out->addVaryHeader( 'X-Forwarded-Proto' ); - } $response->header( $out->getXVO() ); if ( $out->haveCacheVaryCookies() ) { // Logged in, mark this request private @@ -433,12 +533,9 @@ class ApiMain extends ApiBase { } // Send public headers - if ( $wgVaryOnXFP ) { - $response->header( 'Vary: Accept-Encoding, X-Forwarded-Proto' ); - if ( $wgUseXVO ) { - // Bleeeeegh. Our header setting system sucks - $response->header( 'X-Vary-Options: Accept-Encoding;list-contains=gzip, X-Forwarded-Proto' ); - } + $response->header( $out->getVaryHeader() ); + if ( $wgUseXVO ) { + $response->header( $out->getXVO() ); } // If nobody called setCacheMaxAge(), use the (s)maxage parameters @@ -605,7 +702,7 @@ class ApiMain extends ApiBase { if ( !isset( $moduleParams['token'] ) ) { $this->dieUsageMsg( array( 'missingparam', 'token' ) ); } else { - if ( !$this->getUser()->matchEditToken( $moduleParams['token'], $salt, $this->getRequest() ) ) { + if ( !$this->getUser()->matchEditToken( $moduleParams['token'], $salt, $this->getContext()->getRequest() ) ) { $this->dieUsageMsg( 'sessionfailure' ); } } @@ -664,6 +761,12 @@ class ApiMain extends ApiBase { $this->dieReadOnly(); } } + + // Allow extensions to stop execution for arbitrary reasons. + $message = false; + if( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) { + $this->dieUsageMsg( $message ); + } } /** @@ -713,6 +816,9 @@ class ApiMain extends ApiBase { $module->profileOut(); if ( !$this->mInternalMode ) { + //append Debug information + MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() ); + // Print result data $this->printResult( false ); } @@ -779,6 +885,7 @@ class ApiMain extends ApiBase { ), 'requestid' => null, 'servedby' => false, + 'origin' => null, ); } @@ -804,6 +911,12 @@ class ApiMain extends ApiBase { 'maxage' => 'Set the max-age header to this many seconds. Errors are never cached', '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', + 'origin' => array( + 'When accessing the API using a cross-domain AJAX request (CORS), set this to the originating domain.', + '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.', + ), ); } @@ -871,11 +984,11 @@ class ApiMain extends ApiBase { protected function getCredits() { return array( 'API developers:', - ' Roan Kattouw <Firstname>.<Lastname>@gmail.com (lead developer Sep 2007-present)', + ' Roan Kattouw "<Firstname>.<Lastname>@gmail.com" (lead developer Sep 2007-present)', ' Victor Vasiliev - vasilvv at gee mail dot 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)', + ' Yuri Astrakhan "<Firstname><Lastname>@gmail.com" (creator, lead developer Sep 2006-Sep 2007)', '', 'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org', 'or file a bug report at https://bugzilla.wikimedia.org/' @@ -1061,11 +1174,21 @@ class ApiMain extends ApiBase { * * @ingroup API */ -class UsageException extends Exception { +class UsageException extends MWException { private $mCodestr; + + /** + * @var null|array + */ private $mExtraData; + /** + * @param $message string + * @param $codestr string + * @param $code int + * @param $extradata array|null + */ public function __construct( $message, $codestr, $code = 0, $extradata = null ) { parent::__construct( $message, $code ); $this->mCodestr = $codestr; diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index f0a25e4a..9d73562b 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -4,7 +4,7 @@ * * Created on Oct 31, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<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 @@ -37,9 +37,6 @@ class ApiMove extends ApiBase { public function execute() { $user = $this->getUser(); $params = $this->extractRequestParams(); - if ( is_null( $params['reason'] ) ) { - $params['reason'] = ''; - } $this->requireOnlyOneParameter( $params, 'from', 'fromid' ); @@ -78,6 +75,7 @@ class ApiMove extends ApiBase { } // Move the page + $toTitleExists = $toTitle->exists(); $retval = $fromTitle->moveTo( $toTitle, true, $params['reason'], !$params['noredirect'] ); if ( $retval !== true ) { $this->dieUsageMsg( reset( $retval ) ); @@ -87,13 +85,20 @@ class ApiMove extends ApiBase { if ( !$params['noredirect'] || !$user->isAllowed( 'suppressredirect' ) ) { $r['redirectcreated'] = ''; } + if( $toTitleExists ) { + $r['moveoverredirect'] = ''; + } // Move the talk page if ( $params['movetalk'] && $fromTalk->exists() && !$fromTitle->isTalkPage() ) { + $toTalkExists = $toTalk->exists(); $retval = $fromTalk->moveTo( $toTalk, true, $params['reason'], !$params['noredirect'] ); if ( $retval === true ) { $r['talkfrom'] = $fromTalk->getPrefixedText(); $r['talkto'] = $toTalk->getPrefixedText(); + if( $toTalkExists ) { + $r['talkmoveoverredirect'] = ''; + } } else { // We're not gonna dieUsage() on failure, since we already changed something $parsed = $this->parseMsg( reset( $retval ) ); @@ -180,8 +185,11 @@ class ApiMove extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => null, - 'reason' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), + 'reason' => '', 'movetalk' => false, 'movesubpages' => false, 'noredirect' => false, @@ -213,7 +221,7 @@ class ApiMove extends ApiBase { '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 (optional)', + 'reason' => 'Reason for the move', 'movetalk' => 'Move the talk page, if it exists', 'movesubpages' => 'Move subpages, if applicable', 'noredirect' => 'Don\'t create a redirect', @@ -224,6 +232,35 @@ class ApiMove extends ApiBase { ); } + 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'; } diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index 0727cffd..ef562741 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -4,7 +4,7 @@ * * Created on Oct 13, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -45,7 +45,7 @@ class ApiOpenSearch extends ApiBase { $namespaces = $params['namespace']; $suggest = $params['suggest']; - // MWSuggest or similar hit + // Some script that was loaded regardless of wgEnableOpenSearchSuggest, likely cached. if ( $suggest && !$wgEnableOpenSearchSuggest ) { $searches = array(); } else { diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php new file mode 100644 index 00000000..265c2ccb --- /dev/null +++ b/includes/api/ApiOptions.php @@ -0,0 +1,183 @@ +<?php +/** + * + * + * Created on Apr 15, 2012 + * + * Copyright © 2012 Szymon Świerkosz beau@adres.pl + * + * 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 facilitates the changing of user's preferences. +* Requires API write mode to be enabled. +* + * @ingroup API + */ +class ApiOptions extends ApiBase { + + public function __construct( $main, $action ) { + parent::__construct( $main, $action ); + } + + /** + * Changes preferences of the current user. + */ + public function execute() { + $user = $this->getUser(); + + if ( $user->isAnon() ) { + $this->dieUsage( 'Anonymous users cannot change preferences', 'notloggedin' ); + } + + $params = $this->extractRequestParams(); + $changed = false; + + if ( isset( $params['optionvalue'] ) && !isset( $params['optionname'] ) ) { + $this->dieUsageMsg( array( 'missingparam', 'optionname' ) ); + } + + if ( $params['reset'] ) { + $user->resetOptions(); + $changed = true; + } + + $changes = array(); + if ( count( $params['change'] ) ) { + foreach ( $params['change'] as $entry ) { + $array = explode( '=', $entry, 2 ); + $changes[$array[0]] = isset( $array[1] ) ? $array[1] : null; + } + } + if ( isset( $params['optionname'] ) ) { + $newValue = isset( $params['optionvalue'] ) ? $params['optionvalue'] : null; + $changes[$params['optionname']] = $newValue; + } + if ( !$changed && !count( $changes ) ) { + $this->dieUsage( 'No changes were requested', 'nochanges' ); + } + + $prefs = Preferences::getPreferences( $user, $this->getContext() ); + foreach ( $changes as $key => $value ) { + if ( !isset( $prefs[$key] ) ) { + $this->setWarning( "Not a valid preference: $key" ); + continue; + } + $field = HTMLForm::loadInputFromParameters( $key, $prefs[$key] ); + $validation = $field->validate( $value, $user->getOptions() ); + if ( $validation === true ) { + $user->setOption( $key, $value ); + $changed = true; + } else { + $this->setWarning( "Validation error for '$key': $validation" ); + } + } + + if ( $changed ) { + // Commit changes + $user->saveSettings(); + } + + $this->getResult()->addValue( null, $this->getModuleName(), 'success' ); + } + + public function mustBePosted() { + return true; + } + + public function isWriteMode() { + return true; + } + + public function getAllowedParams() { + return array( + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), + 'reset' => false, + 'change' => array( + ApiBase::PARAM_ISMULTI => true, + ), + 'optionname' => array( + ApiBase::PARAM_TYPE => 'string', + ), + 'optionvalue' => array( + ApiBase::PARAM_TYPE => 'string', + ), + ); + } + + 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 all preferences to the site defaults', + 'change' => 'List of changes, formatted name=value (e.g. skin=vector), value cannot contain pipe characters', + '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', + ); + } + + public function getDescription() { + return 'Change preferences of the current user'; + } + + 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 ''; + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Options'; + } + + public function getExamples() { + 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', + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php index 7b84c473..0f5be6b2 100644 --- a/includes/api/ApiPageSet.php +++ b/includes/api/ApiPageSet.php @@ -4,7 +4,7 @@ * * Created on Sep 24, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -52,7 +52,7 @@ class ApiPageSet extends ApiQueryBase { /** * Constructor - * @param $query ApiQueryBase + * @param $query ApiBase * @param $resolveRedirects bool Whether redirects should be resolved * @param $convertTitles bool */ @@ -266,8 +266,8 @@ class ApiPageSet extends ApiQueryBase { } /** - * Returns the number of revisions (requested with revids= parameter)\ - * @return int + * Returns the number of revisions (requested with revids= parameter). + * @return int Number of revisions. */ public function getRevisionCount() { return count( $this->getRevisionIDs() ); @@ -342,7 +342,7 @@ class ApiPageSet extends ApiQueryBase { /** * Populate this PageSet from a rowset returned from the database - * @param $db Database object + * @param $db DatabaseBase object * @param $queryResult ResultWrapper Query result object */ public function populateFromQueryResult( $db, $queryResult ) { @@ -367,7 +367,7 @@ class ApiPageSet extends ApiQueryBase { */ public function processDbRow( $row ) { // Store Title object in various data structures - $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $title = Title::newFromRow( $row ); $pageId = intval( $row->page_id ); $this->mAllPages[$row->page_namespace][$row->page_title] = $pageId; @@ -481,6 +481,7 @@ class ApiPageSet extends ApiQueryBase { ApiBase::dieDebug( __METHOD__, 'Missing $processTitles parameter when $remaining is provided' ); } + $usernames = array(); if ( $res ) { foreach ( $res as $row ) { $pageId = intval( $row->page_id ); @@ -496,6 +497,11 @@ class ApiPageSet extends ApiQueryBase { // Store any extra fields requested by modules $this->processDbRow( $row ); + + // Need gender information + if( MWNamespace::hasGenderDistinction( $row->page_namespace ) ) { + $usernames[] = $row->page_title; + } } } @@ -510,6 +516,11 @@ class ApiPageSet extends ApiQueryBase { $this->mMissingTitles[$this->mFakePageId] = $title; $this->mFakePageId--; $this->mTitles[] = $title; + + // need gender information + if( MWNamespace::hasGenderDistinction( $ns ) ) { + $usernames[] = $dbkey; + } } } } else { @@ -521,6 +532,10 @@ class ApiPageSet extends ApiQueryBase { } } } + + // Get gender information + $genderCache = GenderCache::singleton(); + $genderCache->doQuery( $usernames, __METHOD__ ); } /** @@ -664,6 +679,9 @@ class ApiPageSet extends ApiQueryBase { * @return LinkBatch */ private function processTitlesArray( $titles ) { + $genderCache = GenderCache::singleton(); + $genderCache->doTitlesArray( $titles, __METHOD__ ); + $linkBatch = new LinkBatch(); foreach ( $titles as $title ) { diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index f2263476..343a2625 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -4,7 +4,7 @@ * * Created on Dec 01, 2007 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<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 @@ -251,6 +251,62 @@ 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, overriden 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->getPossibleErrors() ); $result->setIndexedTagName( $retval['errors'], 'error' ); diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index 893491b9..db6e2bb8 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -2,7 +2,7 @@ /** * Created on Dec 01, 2007 * - * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2007 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 @@ -59,19 +59,15 @@ class ApiParse extends ApiBase { // The parser needs $wgTitle to be set, apparently the // $title parameter in Parser::parse isn't enough *sigh* // TODO: Does this still need $wgTitle? - global $wgParser, $wgTitle, $wgLang; + global $wgParser, $wgTitle; // Currently unnecessary, code to act as a safeguard against any change in current behaviour of uselang breaks $oldLang = null; - if ( isset( $params['uselang'] ) && $params['uselang'] != $wgLang->getCode() ) { - $oldLang = $wgLang; // Backup wgLang - $wgLang = Language::factory( $params['uselang'] ); + if ( isset( $params['uselang'] ) && $params['uselang'] != $this->getContext()->getLanguage()->getCode() ) { + $oldLang = $this->getContext()->getLanguage(); // Backup language + $this->getContext()->setLanguage( Language::factory( $params['uselang'] ) ); } - $popts = ParserOptions::newFromContext( $this->getContext() ); - $popts->setTidy( true ); - $popts->enableLimitReport( !$params['disablepp'] ); - $redirValues = null; // Return result @@ -89,13 +85,15 @@ class ApiParse extends ApiBase { } $titleObj = $rev->getTitle(); - $wgTitle = $titleObj; + $pageObj = WikiPage::factory( $titleObj ); + $popts = $pageObj->makeParserOptions( $this->getContext() ); + $popts->enableLimitReport( !$params['disablepp'] ); // If for some reason the "oldid" is actually the current revision, it may be cached if ( $titleObj->getLatestRevID() === intval( $oldid ) ) { // May get from/save to parser cache - $p_result = $this->getParsedSectionOrText( $titleObj, $popts, $pageid, + $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid, isset( $prop['wikitext'] ) ) ; } else { // This is an old revision, so get the text differently $this->text = $rev->getText( Revision::FOR_THIS_USER, $this->getUser() ); @@ -129,32 +127,26 @@ class ApiParse extends ApiBase { foreach ( (array)$redirValues as $r ) { $to = $r['to']; } - $titleObj = Title::newFromText( $to ); - } else { - if ( !is_null ( $pageid ) ) { - $reqParams['pageids'] = $pageid; - $titleObj = Title::newFromID( $pageid ); - } else { // $page - $to = $page; - $titleObj = Title::newFromText( $to ); - } - } - if ( !is_null ( $pageid ) ) { - if ( !$titleObj ) { - // Still throw nosuchpageid error if pageid was provided - $this->dieUsageMsg( array( 'nosuchpageid', $pageid ) ); - } - } elseif ( !$titleObj || !$titleObj->exists() ) { - $this->dieUsage( "The page you specified doesn't exist", 'missingtitle' ); + $pageParams = array( 'title' => $to ); + } elseif ( !is_null( $pageid ) ) { + $pageParams = array( 'pageid' => $pageid ); + } else { // $page + $pageParams = array( 'title' => $page ); } + + $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' ); + $titleObj = $pageObj->getTitle(); $wgTitle = $titleObj; if ( isset( $prop['revid'] ) ) { - $oldid = $titleObj->getLatestRevID(); + $oldid = $pageObj->getLatest(); } + $popts = $pageObj->makeParserOptions( $this->getContext() ); + $popts->enableLimitReport( !$params['disablepp'] ); + // Potentially cached - $p_result = $this->getParsedSectionOrText( $titleObj, $popts, $pageid, + $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid, isset( $prop['wikitext'] ) ) ; } } else { // Not $oldid, $pageid, $page. Hence based on $text @@ -168,6 +160,10 @@ class ApiParse extends ApiBase { $this->dieUsageMsg( array( 'invalidtitle', $title ) ); } $wgTitle = $titleObj; + $pageObj = WikiPage::factory( $titleObj ); + + $popts = $pageObj->makeParserOptions( $this->getContext() ); + $popts->enableLimitReport( !$params['disablepp'] ); if ( $this->section !== false ) { $this->text = $this->getSectionText( $this->text, $titleObj->getText() ); @@ -285,6 +281,21 @@ class ApiParse extends ApiBase { $result->setContent( $result_array['psttext'], $this->pstText ); } } + if ( isset( $prop['properties'] ) ) { + $result_array['properties'] = $this->formatProperties( $p_result->getProperties() ); + } + + if ( $params['generatexml'] ) { + $wgParser->startExternalParse( $titleObj, $popts, OT_PREPROCESS ); + $dom = $wgParser->preprocessToDom( $this->text ); + if ( is_callable( array( $dom, 'saveXML' ) ) ) { + $xml = $dom->saveXML(); + } else { + $xml = $dom->__toString(); + } + $result_array['parsetree'] = array(); + $result->setContent( $result_array['parsetree'], $xml ); + } $result_mapping = array( 'redirects' => 'r', @@ -297,37 +308,39 @@ class ApiParse extends ApiBase { 'iwlinks' => 'iw', 'sections' => 's', 'headitems' => 'hi', + 'properties' => 'pp', ); $this->setIndexedTagNames( $result_array, $result_mapping ); $result->addValue( null, $this->getModuleName(), $result_array ); if ( !is_null( $oldLang ) ) { - $wgLang = $oldLang; // Reset $wgLang to $oldLang + $this->getContext()->setLanguage( $oldLang ); // Reset language to $oldLang } } /** - * @param $titleObj Title + * @param $page WikiPage * @param $popts ParserOptions * @param $pageId Int * @param $getWikitext Bool * @return ParserOutput */ - private function getParsedSectionOrText( $titleObj, $popts, $pageId = null, $getWikitext = false ) { + private function getParsedSectionOrText( $page, $popts, $pageId = null, $getWikitext = false ) { global $wgParser; - $page = WikiPage::factory( $titleObj ); - if ( $this->section !== false ) { $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId ) - ? 'page id ' . $pageId : $titleObj->getText() ); + ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText() ); // Not cached (save or load) - return $wgParser->parse( $this->text, $titleObj, $popts ); + return $wgParser->parse( $this->text, $page->getTitle(), $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->text = $page->getRawText(); } @@ -394,19 +407,19 @@ class ApiParse extends ApiBase { return ''; } - $s = htmlspecialchars( wfMsg( 'otherlanguages' ) . wfMsg( 'colon-separator' ) ); + $s = htmlspecialchars( wfMessage( 'otherlanguages' )->text() . wfMessage( 'colon-separator' )->text() ); $langs = array(); foreach ( $languages as $l ) { $nt = Title::newFromText( $l ); - $text = $wgContLang->getLanguageName( $nt->getInterwiki() ); + $text = Language::fetchLanguageName( $nt->getInterwiki() ); $langs[] = Html::element( 'a', array( 'href' => $nt->getFullURL(), 'title' => $nt->getText(), 'class' => "external" ), $text == '' ? $l : $text ); } - $s .= implode( htmlspecialchars( wfMsgExt( 'pipe-separator', 'escapenoentities' ) ), $langs ); + $s .= implode( wfMessage( 'pipe-separator' )->escaped(), $langs ); if ( $wgContLang->isRTL() ) { $s = Html::rawElement( 'span', array( 'dir' => "LTR" ), $s ); @@ -461,6 +474,17 @@ class ApiParse extends ApiBase { return $result; } + private function formatProperties( $properties ) { + $result = array(); + foreach ( $properties as $name => $value ) { + $entry = array(); + $entry['name'] = $name; + $this->getResult()->setContent( $entry, $value ); + $result[] = $entry; + } + return $result; + } + private function formatCss( $css ) { $result = array(); foreach ( $css as $file => $link ) { @@ -496,7 +520,7 @@ class ApiParse extends ApiBase { ApiBase::PARAM_TYPE => 'integer', ), 'prop' => array( - ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid|displaytitle', + ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid|displaytitle|iwlinks|properties', ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => array( 'text', @@ -515,6 +539,7 @@ class ApiParse extends ApiBase { 'headhtml', 'iwlinks', 'wikitext', + 'properties', ) ), 'pst' => false, @@ -522,6 +547,7 @@ class ApiParse extends ApiBase { 'uselang' => null, 'section' => null, 'disablepp' => false, + 'generatexml' => false, ); } @@ -553,6 +579,7 @@ class ApiParse extends ApiBase { ' headhtml - Gives parsed <head> of 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', ), 'pst' => array( 'Do a pre-save transform on the input before parsing it', @@ -565,11 +592,15 @@ 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', + 'generatexml' => 'Generate XML parse tree', ); } public function getDescription() { - return 'Parses wikitext and returns parser output'; + return array( + 'Parses wikitext and returns parser output', + 'See the various prop-Modules of action=query to get information from the current version of a page', + ); } public function getPossibleErrors() { diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php index 1332f263..cb5e081a 100644 --- a/includes/api/ApiPatrol.php +++ b/includes/api/ApiPatrol.php @@ -65,7 +65,10 @@ class ApiPatrol extends ApiBase { public function getAllowedParams() { return array( - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'rcid' => array( ApiBase::PARAM_TYPE => 'integer', ApiBase::PARAM_REQUIRED => true @@ -80,6 +83,16 @@ class ApiPatrol extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'rcid' => 'integer', + 'ns' => 'namespace', + 'title' => 'string' + ) + ); + } + public function getDescription() { return 'Patrol a page or revision'; } diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php index fb225d86..b3ca67e6 100644 --- a/includes/api/ApiProtect.php +++ b/includes/api/ApiProtect.php @@ -4,7 +4,7 @@ * * Created on Sep 1, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<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 @@ -37,10 +37,8 @@ class ApiProtect extends ApiBase { global $wgRestrictionLevels; $params = $this->extractRequestParams(); - $titleObj = Title::newFromText( $params['title'] ); - if ( !$titleObj ) { - $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); - } + $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' ); + $titleObj = $pageObj->getTitle(); $errors = $titleObj->getUserPermissionsErrors( 'protect', $this->getUser() ); if ( $errors ) { @@ -58,7 +56,7 @@ class ApiProtect extends ApiBase { } $restrictionTypes = $titleObj->getRestrictionTypes(); - $dbr = wfGetDB( DB_SLAVE ); + $db = $this->getDB(); $protections = array(); $expiryarray = array(); @@ -82,7 +80,7 @@ class ApiProtect extends ApiBase { } if ( in_array( $expiry[$i], array( 'infinite', 'indefinite', 'never' ) ) ) { - $expiryarray[$p[0]] = $dbr->getInfinity(); + $expiryarray[$p[0]] = $db->getInfinity(); } else { $exp = strtotime( $expiry[$i] ); if ( $exp < 0 || !$exp ) { @@ -96,7 +94,7 @@ class ApiProtect extends ApiBase { $expiryarray[$p[0]] = $exp; } $resultProtections[] = array( $p[0] => $protections[$p[0]], - 'expiry' => ( $expiryarray[$p[0]] == $dbr->getInfinity() ? + 'expiry' => ( $expiryarray[$p[0]] == $db->getInfinity() ? 'infinite' : wfTimestamp( TS_ISO_8601, $expiryarray[$p[0]] ) ) ); } @@ -106,7 +104,6 @@ class ApiProtect extends ApiBase { $watch = $params['watch'] ? 'watch' : $params['watchlist']; $this->setWatch( $watch, $titleObj ); - $pageObj = WikiPage::factory( $titleObj ); $status = $pageObj->doUpdateRestrictions( $protections, $expiryarray, $cascade, $params['reason'], $this->getUser() ); if ( !$status->isOK() ) { @@ -138,9 +135,14 @@ class ApiProtect extends ApiBase { return array( 'title' => array( ApiBase::PARAM_TYPE => 'string', + ), + 'pageid' => array( + ApiBase::PARAM_TYPE => 'integer', + ), + 'token' => array( + ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => null, 'protections' => array( ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_REQUIRED => true, @@ -169,13 +171,15 @@ class ApiProtect extends ApiBase { } public function getParamDescription() { + $p = $this->getModulePrefix(); return array( - 'title' => 'Title of the page you want to (un)protect', + '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' => 'Pipe-separated list of protection levels, formatted action=group (e.g. edit=sysop)', + '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 neverexpiring protection.' ), - 'reason' => 'Reason for (un)protecting (optional)', + '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\'' ), 'watch' => 'If set, add the page being (un)protected to your watchlist', @@ -183,21 +187,33 @@ class ApiProtect extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'title' => 'string', + 'reason' => 'string', + 'cascade' => 'boolean' + ) + ); + } + public function getDescription() { return 'Change the protection level of a page'; } public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'invalidtitle', 'title' ), - 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 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' ), + ) + ); } public function needsToken() { diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php index 9e9320fb..9fedaf1b 100644 --- a/includes/api/ApiPurge.php +++ b/includes/api/ApiPurge.php @@ -88,13 +88,13 @@ class ApiPurge extends ApiBase { if ( !$user->pingLimiter() ) { global $wgParser, $wgEnableParserCache; - $popts = ParserOptions::newFromContext( $this->getContext() ); + $popts = $page->makeParserOptions( 'canonical' ); $p_result = $wgParser->parse( $page->getRawText(), $title, $popts, true, true, $page->getLatest() ); # Update the links tables - $u = new LinksUpdate( $title, $p_result ); - $u->doUpdate(); + $updates = $p_result->getSecondaryDataUpdates( $title ); + DataUpdate::runUpdates( $updates ); $r['linkupdate'] = ''; @@ -103,7 +103,8 @@ class ApiPurge extends ApiBase { $pcache->save( $p_result, $page, $popts ); } } else { - $this->setWarning( $this->parseMsg( array( 'actionthrottledtext' ) ) ); + $error = $this->parseMsg( array( 'actionthrottledtext' ) ); + $this->setWarning( $error['info'] ); $forceLinkUpdate = false; } } @@ -133,6 +134,34 @@ class ApiPurge extends ApiBase { ); } + 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', + 'missing' => 'boolean', + 'purged' => 'boolean', + 'linkupdate' => 'boolean' + ) + ); + } + public function getDescription() { return array( 'Purge the cache for the given titles.', 'Requires a POST request if the user is not logged in.' @@ -143,7 +172,6 @@ class ApiPurge extends ApiBase { $psModule = new ApiPageSet( $this ); return array_merge( parent::getPossibleErrors(), - array( array( 'cantpurge' ), ), $psModule->getPossibleErrors() ); } diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index cd54a7da..554aae5a 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -4,7 +4,7 @@ * * Created on Sep 7, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -47,55 +47,55 @@ class ApiQuery extends ApiBase { private $params, $redirects, $convertTitles, $iwUrl; private $mQueryPropModules = array( + 'categories' => 'ApiQueryCategories', + 'categoryinfo' => 'ApiQueryCategoryInfo', + 'duplicatefiles' => 'ApiQueryDuplicateFiles', + 'extlinks' => 'ApiQueryExternalLinks', + 'images' => 'ApiQueryImages', + 'imageinfo' => 'ApiQueryImageInfo', 'info' => 'ApiQueryInfo', - 'revisions' => 'ApiQueryRevisions', 'links' => 'ApiQueryLinks', 'iwlinks' => 'ApiQueryIWLinks', 'langlinks' => 'ApiQueryLangLinks', - 'images' => 'ApiQueryImages', - 'imageinfo' => 'ApiQueryImageInfo', + 'pageprops' => 'ApiQueryPageProps', + 'revisions' => 'ApiQueryRevisions', 'stashimageinfo' => 'ApiQueryStashImageInfo', 'templates' => 'ApiQueryLinks', - 'categories' => 'ApiQueryCategories', - 'extlinks' => 'ApiQueryExternalLinks', - 'categoryinfo' => 'ApiQueryCategoryInfo', - 'duplicatefiles' => 'ApiQueryDuplicateFiles', - 'pageprops' => 'ApiQueryPageProps', ); private $mQueryListModules = array( - 'allimages' => 'ApiQueryAllimages', - 'allpages' => 'ApiQueryAllpages', - 'alllinks' => 'ApiQueryAllLinks', 'allcategories' => 'ApiQueryAllCategories', + 'allimages' => 'ApiQueryAllImages', + 'alllinks' => 'ApiQueryAllLinks', + 'allpages' => 'ApiQueryAllPages', 'allusers' => 'ApiQueryAllUsers', 'backlinks' => 'ApiQueryBacklinks', 'blocks' => 'ApiQueryBlocks', 'categorymembers' => 'ApiQueryCategoryMembers', 'deletedrevs' => 'ApiQueryDeletedrevs', 'embeddedin' => 'ApiQueryBacklinks', + 'exturlusage' => 'ApiQueryExtLinksUsage', 'filearchive' => 'ApiQueryFilearchive', 'imageusage' => 'ApiQueryBacklinks', 'iwbacklinks' => 'ApiQueryIWBacklinks', 'langbacklinks' => 'ApiQueryLangBacklinks', 'logevents' => 'ApiQueryLogEvents', + 'protectedtitles' => 'ApiQueryProtectedTitles', + 'querypage' => 'ApiQueryQueryPage', + 'random' => 'ApiQueryRandom', 'recentchanges' => 'ApiQueryRecentChanges', 'search' => 'ApiQuerySearch', 'tags' => 'ApiQueryTags', 'usercontribs' => 'ApiQueryContributions', + 'users' => 'ApiQueryUsers', 'watchlist' => 'ApiQueryWatchlist', 'watchlistraw' => 'ApiQueryWatchlistRaw', - 'exturlusage' => 'ApiQueryExtLinksUsage', - 'users' => 'ApiQueryUsers', - 'random' => 'ApiQueryRandom', - 'protectedtitles' => 'ApiQueryProtectedTitles', - 'querypage' => 'ApiQueryQueryPage', ); private $mQueryMetaModules = array( + 'allmessages' => 'ApiQueryAllMessages', 'siteinfo' => 'ApiQuerySiteinfo', 'userinfo' => 'ApiQueryUserInfo', - 'allmessages' => 'ApiQueryAllmessages', ); private $mSlaveDB = null; @@ -103,11 +103,16 @@ class ApiQuery extends ApiBase { protected $mAllowedGenerators = array(); + /** + * @param $main ApiMain + * @param $action string + */ public function __construct( $main, $action ) { parent::__construct( $main, $action ); // Allow custom modules to be added in LocalSettings.php - global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules; + global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules, + $wgMemc, $wgAPICacheHelpTimeout; self::appendUserModules( $this->mQueryPropModules, $wgAPIPropModules ); self::appendUserModules( $this->mQueryListModules, $wgAPIListModules ); self::appendUserModules( $this->mQueryMetaModules, $wgAPIMetaModules ); @@ -116,8 +121,22 @@ class ApiQuery extends ApiBase { $this->mListModuleNames = array_keys( $this->mQueryListModules ); $this->mMetaModuleNames = array_keys( $this->mQueryMetaModules ); - $this->makeHelpMsgHelper( $this->mQueryPropModules, 'prop' ); - $this->makeHelpMsgHelper( $this->mQueryListModules, 'list' ); + // Get array of query generators from cache if present + $key = wfMemcKey( 'apiquerygenerators', SpecialVersion::getVersion( 'nodb' ) ); + + if ( $wgAPICacheHelpTimeout > 0 ) { + $cached = $wgMemc->get( $key ); + if ( $cached ) { + $this->mAllowedGenerators = $cached; + return; + } + } + $this->makeGeneratorList( $this->mQueryPropModules ); + $this->makeGeneratorList( $this->mQueryListModules ); + + if ( $wgAPICacheHelpTimeout > 0 ) { + $wgMemc->set( $key, $this->mAllowedGenerators, $wgAPICacheHelpTimeout ); + } } /** @@ -135,7 +154,7 @@ class ApiQuery extends ApiBase { /** * Gets a default slave database connection object - * @return Database + * @return DatabaseBase */ public function getDB() { if ( !isset( $this->mSlaveDB ) ) { @@ -154,7 +173,7 @@ class ApiQuery extends ApiBase { * @param $name string Name to assign to the database connection * @param $db int One of the DB_* constants * @param $groups array Query groups - * @return Database + * @return DatabaseBase */ public function getNamedDB( $name, $db, $groups ) { if ( !array_key_exists( $name, $this->mNamedDB ) ) { @@ -202,6 +221,9 @@ class ApiQuery extends ApiBase { return null; } + /** + * @return ApiFormatRaw|null + */ public function getCustomPrinter() { // If &exportnowrap is set, use the raw formatter if ( $this->getParameter( 'export' ) && @@ -258,6 +280,9 @@ class ApiQuery extends ApiBase { $this->outputGeneralPageInfo(); // Execute all requested modules. + /** + * @var $module ApiQueryBase + */ foreach ( $modules as $module ) { $params = $module->extractRequestParams(); $cacheMode = $this->mergeCacheMode( @@ -303,6 +328,9 @@ class ApiQuery extends ApiBase { */ private function addCustomFldsToPageSet( $modules, $pageSet ) { // Query all requested modules. + /** + * @var $module ApiQueryBase + */ foreach ( $modules as $module ) { $module->requestExtraData( $pageSet ); } @@ -384,6 +412,9 @@ class ApiQuery extends ApiBase { // Show redirect information $redirValues = array(); + /** + * @var $titleTo Title + */ foreach ( $pageSet->getRedirectTitles() as $titleStrFrom => $titleTo ) { $r = array( 'from' => strval( $titleStrFrom ), @@ -602,7 +633,6 @@ class ApiQuery extends ApiBase { // Make sure the internal object is empty // (just in case a sub-module decides to optimize during instantiation) $this->mPageSet = null; - $this->mAllowedGenerators = array(); // Will be repopulated $querySeparator = str_repeat( '--- ', 12 ); $moduleSeparator = str_repeat( '*** ', 14 ); @@ -614,8 +644,6 @@ class ApiQuery extends ApiBase { $msg .= $this->makeHelpMsgHelper( $this->mQueryMetaModules, 'meta' ); $msg .= "\n\n$moduleSeparator Modules: continuation $moduleSeparator\n\n"; - // Perform the base call last because the $this->mAllowedGenerators - // will be updated inside makeHelpMsgHelper() // Use parent to make default message for the query module $msg = parent::makeHelpMsg() . $msg; @@ -643,7 +671,6 @@ class ApiQuery extends ApiBase { $msg .= $msg2; } if ( $module instanceof ApiQueryGeneratorBase ) { - $this->mAllowedGenerators[] = $moduleName; $msg .= "Generator:\n This module may be used as a generator\n"; } $moduleDescriptions[] = $msg; @@ -653,6 +680,19 @@ class ApiQuery extends ApiBase { } /** + * Adds any classes that are a subclass of ApiQueryGeneratorBase + * to the allowed generator list + * @param $moduleList array() + */ + private function makeGeneratorList( $moduleList ) { + foreach( $moduleList as $moduleName => $moduleClass ) { + if ( is_subclass_of( $moduleClass, 'ApiQueryGeneratorBase' ) ) { + $this->mAllowedGenerators[] = $moduleName; + } + } + } + + /** * Override to add extra parameters from PageSet * @return string */ @@ -674,7 +714,7 @@ class ApiQuery extends ApiBase { '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 gan, iu, kk, ku, shi, sr, tg, zh' ), + 'Languages that support variant conversion include ' . implode( ', ', LanguageConverter::$languagesWithVariants ) ), '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', diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php index 78367a45..4f4c77f0 100644 --- a/includes/api/ApiQueryAllCategories.php +++ b/includes/api/ApiQueryAllCategories.php @@ -4,7 +4,7 @@ * * Created on December 12, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<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 @@ -58,6 +58,17 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { $this->addTables( 'category' ); $this->addFields( 'cat_title' ); + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + if ( count( $cont ) != 1 ) { + $this->dieUsage( "Invalid continue param. You should pass the " . + "original value returned by the previous query", "_badcontinue" ); + } + $op = $params['dir'] == 'descending' ? '<' : '>'; + $cont_from = $db->addQuotes( $cont[0] ); + $this->addWhere( "cat_title $op= $cont_from" ); + } + $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'] ) ); @@ -65,14 +76,20 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { $min = $params['min']; $max = $params['max']; - $this->addWhereRange( 'cat_pages', $dir, $min, $max ); + if ( $dir == 'newer' ) { + $this->addWhereRange( 'cat_pages', 'newer', $min, $max ); + } else { + $this->addWhereRange( 'cat_pages', 'older', $max, $min); + } + if ( isset( $params['prefix'] ) ) { $this->addWhere( 'cat_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); - $this->addOption( 'ORDER BY', 'cat_title' . ( $params['dir'] == 'descending' ? ' DESC' : '' ) ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $this->addOption( 'ORDER BY', 'cat_title' . $sort ); $prop = array_flip( $params['prop'] ); $this->addFieldsIf( array( 'cat_pages', 'cat_subcats', 'cat_files' ), isset( $prop['size'] ) ); @@ -86,7 +103,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { 'pp_page=page_id', 'pp_propname' => 'hiddencat' ) ), ) ); - $this->addFields( 'pp_propname AS cat_hidden' ); + $this->addFields( array( 'cat_hidden' => 'pp_propname' ) ); } $res = $this->select( __METHOD__ ); @@ -98,15 +115,14 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { 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... - // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->cat_title ) ); + $this->setContinueEnumParameter( 'continue', $row->cat_title ); break; } // Normalize titles $titleObj = Title::makeTitle( NS_CATEGORY, $row->cat_title ); if ( !is_null( $resultPageSet ) ) { - $pages[] = $titleObj->getPrefixedText(); + $pages[] = $titleObj; } else { $item = array(); $result->setContent( $item, $titleObj->getText() ); @@ -121,7 +137,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { } $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $item ); if ( !$fit ) { - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->cat_title ) ); + $this->setContinueEnumParameter( 'continue', $row->cat_title ); break; } } @@ -137,6 +153,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { public function getAllowedParams() { return array( 'from' => null, + 'continue' => null, 'to' => null, 'prefix' => null, 'dir' => array( @@ -172,6 +189,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { public function getParamDescription() { return array( 'from' => 'The category to start enumerating from', + 'continue' => 'When more results are available, use this to continue', 'to' => 'The category to stop enumerating at', 'prefix' => 'Search for all category titles that begin with this value', 'dir' => 'Direction to sort in', @@ -186,10 +204,33 @@ 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'; } + public function getPossibleErrors() { + return array_merge( parent::getPossibleErrors(), array( + array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), + ) ); + } + public function getExamples() { return array( 'api.php?action=query&list=allcategories&acprop=size', diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php new file mode 100644 index 00000000..b562da8e --- /dev/null +++ b/includes/api/ApiQueryAllImages.php @@ -0,0 +1,409 @@ +<?php + +/** + * API for MediaWiki 1.12+ + * + * Created on Mar 16, 2008 + * + * Copyright © 2008 Vasiliev Victor vasilvv@gmail.com, + * based on ApiQueryAllPages.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 + */ + +/** + * Query module to enumerate all available pages. + * + * @ingroup API + */ +class ApiQueryAllImages extends ApiQueryGeneratorBase { + + protected $mRepo; + + public function __construct( $query, $moduleName ) { + parent::__construct( $query, $moduleName, 'ai' ); + $this->mRepo = RepoGroup::singleton()->getLocalRepo(); + } + + /** + * Override parent method to make sure to make sure the repo's DB is used + * which may not necesarilly be the same as the local DB. + * + * TODO: allow querying non-local repos. + * @return DatabaseBase + */ + protected function getDB() { + return $this->mRepo->getSlaveDB(); + } + + public function execute() { + $this->run(); + } + + public function getCacheMode( $params ) { + return 'public'; + } + + /** + * @param $resultPageSet ApiPageSet + * @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->run( $resultPageSet ); + } + + /** + * @param $resultPageSet ApiPageSet + * @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' ); + } + + $prefix = $this->getModulePrefix(); + + $db = $this->getDB(); + + $params = $this->extractRequestParams(); + + // Table and return fields + $this->addTables( 'image' ); + + $prop = array_flip( $params['prop'] ); + $this->addFields( LocalFile::selectFields() ); + + $dir = ( in_array( $params['dir'], array( 'descending', 'older' ) ) ? 'older' : 'newer' ); + + if ( $params['sort'] == 'name' ) { + // Check mutually exclusive params + $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' ); + } + } + if ( $params['filterbots'] != 'all' ) { + $this->dieUsage( "Parameter '{$prefix}filterbots' can only be used with {$prefix}sort=timestamp", 'badparams' ); + } + + // Pagination + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + if ( count( $cont ) != 1 ) { + $this->dieUsage( 'Invalid continue param. You should pass the ' . + 'original value returned by the previous query', '_badcontinue' ); + } + $op = ( $dir == 'older' ? '<' : '>' ); + $cont_from = $db->addQuotes( $cont[0] ); + $this->addWhere( "img_name $op= $cont_from" ); + } + + // Image filters + $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); + $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) ); + $this->addWhereRange( 'img_name', $dir, $from, $to ); + + if ( isset( $params['prefix'] ) ) { + $this->addWhere( 'img_name' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $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' ); + } + } + 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 'user' and 'filterbots' cannot be used together", 'badparams' ); + } + + // Pagination + $this->addTimestampWhereRange( 'img_timestamp', $dir, $params['start'], $params['end'] ); + + // Image filters + if ( !is_null( $params['user'] ) ) { + $this->addWhereFld( 'img_user_text', $params['user'] ); + } + if ( $params['filterbots'] != 'all' ) { + $this->addTables( 'user_groups' ); + $groupCond = ( $params['filterbots'] == 'nobots' ? 'NULL': 'NOT NULL' ); + $this->addWhere( "ug_group IS $groupCond" ); + $this->addJoinConds( array( 'user_groups' => array( + 'LEFT JOIN', + array( + 'ug_group' => User::getGroupsWithPermission( 'bot' ), + 'ug_user = img_user' + ) + ) ) ); + } + } + + // Filters not depending on sort + if ( isset( $params['minsize'] ) ) { + $this->addWhere( 'img_size>=' . intval( $params['minsize'] ) ); + } + + if ( isset( $params['maxsize'] ) ) { + $this->addWhere( 'img_size<=' . intval( $params['maxsize'] ) ); + } + + $sha1 = false; + if ( isset( $params['sha1'] ) ) { + if ( !$this->validateSha1Hash( $params['sha1'] ) ) { + $this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' ); + } + $sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 ); + } elseif ( isset( $params['sha1base36'] ) ) { + $sha1 = $params['sha1base36']; + if ( !$this->validateSha1Base36Hash( $sha1 ) ) { + $this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' ); + } + } + if ( $sha1 ) { + $this->addWhereFld( 'img_sha1', $sha1 ); + } + + if ( !is_null( $params['mime'] ) ) { + global $wgMiserMode; + if ( $wgMiserMode ) { + $this->dieUsage( 'MIME search disabled in Miser Mode', 'mimesearchdisabled' ); + } + + list( $major, $minor ) = File::splitMime( $params['mime'] ); + + $this->addWhereFld( 'img_major_mime', $major ); + $this->addWhereFld( 'img_minor_mime', $minor ); + } + + $limit = $params['limit']; + $this->addOption( 'LIMIT', $limit + 1 ); + $sort = ( $dir == 'older' ? ' DESC' : '' ); + if ( $params['sort'] == 'timestamp' ) { + $this->addOption( 'ORDER BY', 'img_timestamp' . $sort ); + if ( $params['filterbots'] == 'all' ) { + $this->addOption( 'USE INDEX', array( 'image' => 'img_timestamp' ) ); + } else { + $this->addOption( 'USE INDEX', array( 'image' => 'img_usertext_timestamp' ) ); + } + } else { + $this->addOption( 'ORDER BY', 'img_name' . $sort ); + } + + $res = $this->select( __METHOD__ ); + + $titles = array(); + $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 ( $params['sort'] == 'name' ) { + $this->setContinueEnumParameter( 'continue', $row->img_name ); + } else { + $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->img_timestamp ) ); + } + break; + } + + if ( is_null( $resultPageSet ) ) { + $file = $repo->newFileFromRow( $row ); + $info = array_merge( array( 'name' => $row->img_name ), + ApiQueryImageInfo::getInfo( $file, $prop, $result ) ); + self::addTitleInfo( $info, $file->getTitle() ); + + $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $info ); + if ( !$fit ) { + if ( $params['sort'] == 'name' ) { + $this->setContinueEnumParameter( 'continue', $row->img_name ); + } else { + $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->img_timestamp ) ); + } + break; + } + } else { + $titles[] = Title::makeTitle( NS_FILE, $row->img_name ); + } + } + + if ( is_null( $resultPageSet ) ) { + $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'img' ); + } else { + $resultPageSet->populateFromTitles( $titles ); + } + } + + public function getAllowedParams() { + return array ( + 'sort' => array( + ApiBase::PARAM_DFLT => 'name', + ApiBase::PARAM_TYPE => array( + 'name', + 'timestamp' + ) + ), + 'dir' => array( + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => array( + // sort=name + 'ascending', + 'descending', + // sort=timestamp + 'newer', + 'older', + ) + ), + 'from' => null, + 'to' => null, + 'continue' => null, + 'start' => array( + ApiBase::PARAM_TYPE => 'timestamp' + ), + 'end' => array( + ApiBase::PARAM_TYPE => 'timestamp' + ), + 'prop' => array( + ApiBase::PARAM_TYPE => ApiQueryImageInfo::getPropertyNames( $this->propertyFilter ), + ApiBase::PARAM_DFLT => 'timestamp|url', + ApiBase::PARAM_ISMULTI => true + ), + 'prefix' => null, + 'minsize' => array( + ApiBase::PARAM_TYPE => 'integer', + ), + 'maxsize' => array( + ApiBase::PARAM_TYPE => 'integer', + ), + 'sha1' => null, + 'sha1base36' => null, + 'user' => array( + ApiBase::PARAM_TYPE => 'user' + ), + 'filterbots' => array( + ApiBase::PARAM_DFLT => 'all', + ApiBase::PARAM_TYPE => array( + 'all', + 'bots', + 'nobots' + ) + ), + 'mime' => null, + '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 + ), + ); + } + + public function getParamDescription() { + $p = $this->getModulePrefix(); + return array( + 'sort' => 'Property to sort by', + 'dir' => 'The direction in which to list', + 'from' => "The image title to start enumerating from. Can only be used with {$p}sort=name", + 'to' => "The image title to stop enumerating at. Can only be used with {$p}sort=name", + 'continue' => 'When more results are available, use this to continue', + '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", + '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", + '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 ) + ); + } + + 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 'user' and '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' ), + array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), + ) ); + } + + public function getExamples() { + return array( + 'api.php?action=query&list=allimages&aifrom=B' => array( + '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( + '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( + 'Using as Generator', + 'Show info about 4 files starting at the letter "T"', + ), + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Allimages'; + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php index 903f144f..da4840f0 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -4,7 +4,7 @@ * * Created on July 7, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -76,17 +76,26 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $this->dieUsage( 'alcontinue and alfrom cannot be used together', 'params' ); } if ( !is_null( $params['continue'] ) ) { - $arr = explode( '|', $params['continue'] ); - if ( count( $arr ) != 2 ) { - $this->dieUsage( 'Invalid continue parameter', 'badcontinue' ); + $continueArr = explode( '|', $params['continue'] ); + $op = $params['dir'] == 'descending' ? '<' : '>'; + if ( $params['unique'] ) { + if ( count( $continueArr ) != 1 ) { + $this->dieUsage( 'Invalid continue parameter', 'badcontinue' ); + } + $continueTitle = $db->addQuotes( $continueArr[0] ); + $this->addWhere( "pl_title $op= $continueTitle" ); + } else { + if ( count( $continueArr ) != 2 ) { + $this->dieUsage( 'Invalid continue parameter', 'badcontinue' ); + } + $continueTitle = $db->addQuotes( $continueArr[0] ); + $continueFrom = intval( $continueArr[1] ); + $this->addWhere( + "pl_title $op $continueTitle OR " . + "(pl_title = $continueTitle AND " . + "pl_from $op= $continueFrom)" + ); } - $from = $this->getDB()->strencode( $this->titleToKey( $arr[0] ) ); - $id = intval( $arr[1] ); - $this->addWhere( - "pl_title > '$from' OR " . - "(pl_title = '$from' AND " . - "pl_from > $id)" - ); } $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); @@ -104,9 +113,13 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $limit = $params['limit']; $this->addOption( 'LIMIT', $limit + 1 ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $orderBy = array(); + $orderBy[] = 'pl_title' . $sort; if ( !$params['unique'] ) { - $this->addOption( 'ORDER BY', 'pl_title, pl_from' ); + $orderBy[] = 'pl_from' . $sort; } + $this->addOption( 'ORDER BY', $orderBy ); $res = $this->select( __METHOD__ ); @@ -116,11 +129,10 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { 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... - // TODO: Security issue - if the user has no right to view next title, it will still be shown if ( $params['unique'] ) { - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->pl_title ) ); + $this->setContinueEnumParameter( 'continue', $row->pl_title ); } else { - $this->setContinueEnumParameter( 'continue', $this->keyToTitle( $row->pl_title ) . "|" . $row->pl_from ); + $this->setContinueEnumParameter( 'continue', $row->pl_title . "|" . $row->pl_from ); } break; } @@ -137,9 +149,9 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { if ( $params['unique'] ) { - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->pl_title ) ); + $this->setContinueEnumParameter( 'continue', $row->pl_title ); } else { - $this->setContinueEnumParameter( 'continue', $this->keyToTitle( $row->pl_title ) . "|" . $row->pl_from ); + $this->setContinueEnumParameter( 'continue', $row->pl_title . "|" . $row->pl_from ); } break; } @@ -180,7 +192,14 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { ApiBase::PARAM_MIN => 1, ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 - ) + ), + 'dir' => array( + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => array( + 'ascending', + 'descending' + ) + ), ); } @@ -199,6 +218,19 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { 'namespace' => 'The namespace to enumerate', 'limit' => 'How many total links to return', 'continue' => 'When more results are available, use this to continue', + 'dir' => 'The direction in which to list', + ); + } + + public function getResultProperties() { + return array( + 'ids' => array( + 'fromid' => 'integer' + ), + 'title' => array( + 'ns' => 'namespace', + 'title' => 'string' + ) ); } diff --git a/includes/api/ApiQueryAllmessages.php b/includes/api/ApiQueryAllMessages.php index 44774927..f5e1146b 100644 --- a/includes/api/ApiQueryAllmessages.php +++ b/includes/api/ApiQueryAllMessages.php @@ -4,7 +4,7 @@ * * Created on Dec 1, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -29,7 +29,7 @@ * * @ingroup API */ -class ApiQueryAllmessages extends ApiQueryBase { +class ApiQueryAllMessages extends ApiQueryBase { public function __construct( $query, $moduleName ) { parent::__construct( $query, $moduleName, 'am' ); @@ -256,6 +256,27 @@ class ApiQueryAllmessages extends ApiQueryBase { ); } + 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'; } diff --git a/includes/api/ApiQueryAllpages.php b/includes/api/ApiQueryAllPages.php index e003ee91..16cc31d2 100644 --- a/includes/api/ApiQueryAllpages.php +++ b/includes/api/ApiQueryAllPages.php @@ -4,7 +4,7 @@ * * Created on Sep 25, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -29,7 +29,7 @@ * * @ingroup API */ -class ApiQueryAllpages extends ApiQueryGeneratorBase { +class ApiQueryAllPages extends ApiQueryGeneratorBase { public function __construct( $query, $moduleName ) { parent::__construct( $query, $moduleName, 'ap' ); @@ -67,6 +67,17 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { // Page filters $this->addTables( 'page' ); + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + if ( count( $cont ) != 1 ) { + $this->dieUsage( "Invalid continue param. You should pass the " . + "original value returned by the previous query", "_badcontinue" ); + } + $op = $params['dir'] == 'descending' ? '<' : '>'; + $cont_from = $db->addQuotes( $cont[0] ); + $this->addWhere( "page_title $op= $cont_from" ); + } + if ( $params['filterredir'] == 'redirects' ) { $this->addWhereFld( 'page_is_redirect', 1 ); } elseif ( $params['filterredir'] == 'nonredirects' ) { @@ -153,7 +164,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $this->addOption( 'STRAIGHT_JOIN' ); // We have to GROUP BY all selected fields to stop // PostgreSQL from whining - $this->addOption( 'GROUP BY', implode( ', ', $selectFields ) ); + $this->addOption( 'GROUP BY', $selectFields ); $forceNameTitleIndex = false; } @@ -165,13 +176,22 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $this->addOption( 'LIMIT', $limit + 1 ); $res = $this->select( __METHOD__ ); + //Get gender information + if( MWNamespace::hasGenderDistinction( $params['namespace'] ) ) { + $users = array(); + foreach ( $res as $row ) { + $users[] = $row->page_title; + } + GenderCache::singleton()->doQuery( $users, __METHOD__ ); + $res->rewind(); //reset + } + $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... - // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->page_title ) ); + $this->setContinueEnumParameter( 'continue', $row->page_title ); break; } @@ -184,7 +204,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { ); $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->page_title ) ); + $this->setContinueEnumParameter( 'continue', $row->page_title ); break; } } else { @@ -202,6 +222,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { return array( 'from' => null, + 'continue' => null, 'to' => null, 'prefix' => null, 'namespace' => array( @@ -275,6 +296,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { $p = $this->getModulePrefix(); return array( 'from' => 'The page title to start enumerating from', + 'continue' => 'When more results are available, use this to continue', 'to' => 'The page title to stop enumerating at', 'prefix' => 'Search for all page titles that begin with this value', 'namespace' => 'The namespace to enumerate', @@ -296,6 +318,16 @@ 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'; } @@ -304,6 +336,7 @@ class ApiQueryAllpages extends ApiQueryGeneratorBase { 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' ), + array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), ) ); } diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index ac112ef9..7f50cbad 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -4,7 +4,7 @@ * * Created on July 7, 2007 * - * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2007 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 @@ -34,6 +34,16 @@ class ApiQueryAllUsers extends ApiQueryBase { parent::__construct( $query, $moduleName, 'au' ); } + /** + * This function converts the user name to a canonical form + * which is stored in the database. + * @param String $name + * @return String + */ + private function getCanonicalUserName( $name ) { + return str_replace( '_', ' ', $name ); + } + public function execute() { $db = $this->getDB(); $params = $this->extractRequestParams(); @@ -57,8 +67,8 @@ class ApiQueryAllUsers extends ApiQueryBase { $useIndex = true; $dir = ( $params['dir'] == 'descending' ? 'older' : 'newer' ); - $from = is_null( $params['from'] ) ? null : $this->keyToTitle( $params['from'] ); - $to = is_null( $params['to'] ) ? null : $this->keyToTitle( $params['to'] ); + $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. @@ -68,7 +78,7 @@ class ApiQueryAllUsers extends ApiQueryBase { if ( !is_null( $params['prefix'] ) ) { $this->addWhere( $userFieldToSort . - $db->buildLike( $this->keyToTitle( $params['prefix'] ), $db->anyString() ) ); + $db->buildLike( $this->getCanonicalUserName( $params['prefix'] ), $db->anyString() ) ); } if ( !is_null( $params['rights'] ) ) { @@ -142,11 +152,11 @@ class ApiQueryAllUsers extends ApiQueryBase { 'INNER JOIN', 'rc_user_text=user_name' ) ) ); - $this->addFields( 'COUNT(*) AS recentedits' ); + $this->addFields( array( 'recentedits' => 'COUNT(*)' ) ); - $this->addWhere( "rc_log_type IS NULL OR rc_log_type != 'newusers'" ); + $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->addWhere( 'rc_timestamp >= ' . $db->addQuotes( $timestamp ) ); $this->addOption( 'GROUP BY', $userFieldToSort ); } @@ -190,15 +200,14 @@ class ApiQueryAllUsers extends ApiQueryBase { $lastUserData = null; if ( !$fit ) { - $this->setContinueEnumParameter( 'from', - $this->keyToTitle( $lastUserData['name'] ) ); + $this->setContinueEnumParameter( 'from', $lastUserData['name'] ); break; } } if ( $count > $limit ) { // We've reached the one extra which shows that there are additional pages to be had. Stop here... - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->user_name ) ); + $this->setContinueEnumParameter( 'from', $row->user_name ); break; } @@ -209,7 +218,9 @@ class ApiQueryAllUsers extends ApiQueryBase { 'name' => $lastUser, ); if ( $fld_blockinfo && !is_null( $row->ipb_by_text ) ) { + $lastUserData['blockid'] = $row->ipb_id; $lastUserData['blockedby'] = $row->ipb_by_text; + $lastUserData['blockedbyid'] = $row->ipb_by; $lastUserData['blockreason'] = $row->ipb_reason; $lastUserData['blockexpiry'] = $row->ipb_expiry; } @@ -235,32 +246,45 @@ class ApiQueryAllUsers extends ApiQueryBase { 'MediaWiki configuration error: the database contains more user groups than known to User::getAllGroups() function' ); } - $lastUserObj = User::newFromName( $lastUser ); + $lastUserObj = User::newFromId( $row->user_id ); // Add user's group info if ( $fld_groups ) { - if ( !isset( $lastUserData['groups'] ) && $lastUserObj ) { - $lastUserData['groups'] = ApiQueryUsers::getAutoGroups( $lastUserObj ); + if ( !isset( $lastUserData['groups'] ) ) { + if ( $lastUserObj ) { + $lastUserData['groups'] = $lastUserObj->getAutomaticGroups(); + } else { + // This should not normally happen + $lastUserData['groups'] = array(); + } } if ( !is_null( $row->ug_group2 ) ) { $lastUserData['groups'][] = $row->ug_group2; } + $result->setIndexedTagName( $lastUserData['groups'], 'g' ); } if ( $fld_implicitgroups && !isset( $lastUserData['implicitgroups'] ) && $lastUserObj ) { - $lastUserData['implicitgroups'] = ApiQueryUsers::getAutoGroups( $lastUserObj ); + $lastUserData['implicitgroups'] = $lastUserObj->getAutomaticGroups(); $result->setIndexedTagName( $lastUserData['implicitgroups'], 'g' ); } if ( $fld_rights ) { - if ( !isset( $lastUserData['rights'] ) && $lastUserObj ) { - $lastUserData['rights'] = User::getGroupPermissions( $lastUserObj->getAutomaticGroups() ); + if ( !isset( $lastUserData['rights'] ) ) { + if ( $lastUserObj ) { + $lastUserData['rights'] = User::getGroupPermissions( $lastUserObj->getAutomaticGroups() ); + } else { + // This should not normally happen + $lastUserData['rights'] = array(); + } } + if ( !is_null( $row->ug_group2 ) ) { $lastUserData['rights'] = array_unique( array_merge( $lastUserData['rights'], User::getGroupPermissions( array( $row->ug_group2 ) ) ) ); } + $result->setIndexedTagName( $lastUserData['rights'], 'r' ); } } @@ -269,8 +293,7 @@ class ApiQueryAllUsers extends ApiQueryBase { $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $lastUserData ); if ( !$fit ) { - $this->setContinueEnumParameter( 'from', - $this->keyToTitle( $lastUserData['name'] ) ); + $this->setContinueEnumParameter( 'from', $lastUserData['name'] ); } } @@ -338,7 +361,7 @@ 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)', + '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', @@ -354,6 +377,48 @@ class ApiQueryAllUsers extends ApiQueryBase { ); } + 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' + ) + ); + } + public function getDescription() { return 'Enumerate all registered users'; } diff --git a/includes/api/ApiQueryAllimages.php b/includes/api/ApiQueryAllimages.php deleted file mode 100644 index ca344f73..00000000 --- a/includes/api/ApiQueryAllimages.php +++ /dev/null @@ -1,267 +0,0 @@ -<?php - -/** - * API for MediaWiki 1.12+ - * - * Created on Mar 16, 2008 - * - * Copyright © 2008 Vasiliev Victor vasilvv@gmail.com, - * based on ApiQueryAllpages.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 - */ - -/** - * Query module to enumerate all available pages. - * - * @ingroup API - */ -class ApiQueryAllimages extends ApiQueryGeneratorBase { - - protected $mRepo; - - public function __construct( $query, $moduleName ) { - parent::__construct( $query, $moduleName, 'ai' ); - $this->mRepo = RepoGroup::singleton()->getLocalRepo(); - } - - /** - * Override parent method to make sure to make sure the repo's DB is used - * which may not necesarilly be the same as the local DB. - * - * TODO: allow querying non-local repos. - * @return DatabaseBase - */ - protected function getDB() { - return $this->mRepo->getSlaveDB(); - } - - public function execute() { - $this->run(); - } - - public function getCacheMode( $params ) { - return 'public'; - } - - /** - * @param $resultPageSet ApiPageSet - * @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->run( $resultPageSet ); - } - - /** - * @param $resultPageSet ApiPageSet - * @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' ); - } - - $db = $this->getDB(); - - $params = $this->extractRequestParams(); - - // Image filters - $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'] ) ); - $this->addWhereRange( 'img_name', $dir, $from, $to ); - - if ( isset( $params['prefix'] ) ) - $this->addWhere( 'img_name' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); - - if ( isset( $params['minsize'] ) ) { - $this->addWhere( 'img_size>=' . intval( $params['minsize'] ) ); - } - - if ( isset( $params['maxsize'] ) ) { - $this->addWhere( 'img_size<=' . intval( $params['maxsize'] ) ); - } - - $sha1 = false; - if ( isset( $params['sha1'] ) ) { - if ( !$this->validateSha1Hash( $params['sha1'] ) ) { - $this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' ); - } - $sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 ); - } elseif ( isset( $params['sha1base36'] ) ) { - $sha1 = $params['sha1base36']; - if ( !$this->validateSha1Base36Hash( $sha1 ) ) { - $this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' ); - } - } - if ( $sha1 ) { - $this->addWhereFld( 'img_sha1', $sha1 ); - } - - if ( !is_null( $params['mime'] ) ) { - global $wgMiserMode; - if ( $wgMiserMode ) { - $this->dieUsage( 'MIME search disabled in Miser Mode', 'mimesearchdisabled' ); - } - - list( $major, $minor ) = File::splitMime( $params['mime'] ); - - $this->addWhereFld( 'img_major_mime', $major ); - $this->addWhereFld( 'img_minor_mime', $minor ); - } - - $this->addTables( 'image' ); - - $prop = array_flip( $params['prop'] ); - $this->addFields( LocalFile::selectFields() ); - - $limit = $params['limit']; - $this->addOption( 'LIMIT', $limit + 1 ); - $this->addOption( 'ORDER BY', 'img_name' . - ( $params['dir'] == 'descending' ? ' DESC' : '' ) ); - - $res = $this->select( __METHOD__ ); - - $titles = array(); - $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... - // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->img_name ) ); - break; - } - - if ( is_null( $resultPageSet ) ) { - $file = $repo->newFileFromRow( $row ); - $info = array_merge( array( 'name' => $row->img_name ), - ApiQueryImageInfo::getInfo( $file, $prop, $result ) ); - self::addTitleInfo( $info, $file->getTitle() ); - - $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $info ); - if ( !$fit ) { - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->img_name ) ); - break; - } - } else { - $titles[] = Title::makeTitle( NS_IMAGE, $row->img_name ); - } - } - - if ( is_null( $resultPageSet ) ) { - $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'img' ); - } else { - $resultPageSet->populateFromTitles( $titles ); - } - } - - public function getAllowedParams() { - return array ( - 'from' => null, - 'to' => null, - 'prefix' => null, - 'minsize' => array( - ApiBase::PARAM_TYPE => 'integer', - ), - 'maxsize' => array( - ApiBase::PARAM_TYPE => 'integer', - ), - 'limit' => array( - ApiBase::PARAM_DFLT => 10, - ApiBase::PARAM_TYPE => 'limit', - ApiBase::PARAM_MIN => 1, - ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, - ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 - ), - 'dir' => array( - ApiBase::PARAM_DFLT => 'ascending', - ApiBase::PARAM_TYPE => array( - 'ascending', - 'descending' - ) - ), - 'sha1' => null, - 'sha1base36' => null, - 'prop' => array( - ApiBase::PARAM_TYPE => ApiQueryImageInfo::getPropertyNames( $this->propertyFilter ), - ApiBase::PARAM_DFLT => 'timestamp|url', - ApiBase::PARAM_ISMULTI => true - ), - 'mime' => null, - ); - } - - public function getParamDescription() { - return array( - 'from' => 'The image title to start enumerating from', - 'to' => 'The image title to stop enumerating at', - 'prefix' => 'Search for all image titles that begin with this value', - 'dir' => 'The direction in which to list', - 'minsize' => 'Limit to images with at least this many bytes', - 'maxsize' => 'Limit to images with at most this many bytes', - 'limit' => 'How many images in total to return', - 'sha1' => "SHA1 hash of image. Overrides {$this->getModulePrefix()}sha1base36", - 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki)', - 'prop' => ApiQueryImageInfo::getPropertyDescriptions( $this->propertyFilter ), - 'mime' => 'What MIME type to search for. e.g. image/jpeg. Disabled in Miser Mode', - ); - } - - private $propertyFilter = array( 'archivename' ); - - public function getDescription() { - return 'Enumerate all images sequentially'; - } - - public function getPossibleErrors() { - 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' => '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' ), - ) ); - } - - public function getExamples() { - return array( - 'api.php?action=query&list=allimages&aifrom=B' => array( - 'Simple Use', - 'Show a list of images starting at the letter "B"', - ), - 'api.php?action=query&generator=allimages&gailimit=4&gaifrom=T&prop=imageinfo' => array( - 'Using as Generator', - 'Show info about 4 images starting at the letter "T"', - ), - ); - } - - public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Allimages'; - } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } -} diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index 381ef550..06db87bf 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -4,7 +4,7 @@ * * Created on Oct 16, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -40,7 +40,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { private $rootTitle; private $params, $contID, $redirID, $redirect; - private $bl_ns, $bl_from, $bl_table, $bl_code, $bl_title, $bl_sort, $bl_fields, $hasNS; + private $bl_ns, $bl_from, $bl_table, $bl_code, $bl_title, $bl_fields, $hasNS; /** * Maps ns and title to pageid @@ -91,14 +91,12 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->hasNS = $moduleName !== 'imageusage'; if ( $this->hasNS ) { $this->bl_title = $prefix . '_title'; - $this->bl_sort = "{$this->bl_ns}, {$this->bl_title}, {$this->bl_from}"; $this->bl_fields = array( $this->bl_ns, $this->bl_title ); } else { $this->bl_title = $prefix . '_to'; - $this->bl_sort = "{$this->bl_title}, {$this->bl_from}"; $this->bl_fields = array( $this->bl_title ); @@ -144,7 +142,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->addWhereFld( 'page_namespace', $this->params['namespace'] ); if ( !is_null( $this->contID ) ) { - $this->addWhere( "{$this->bl_from}>={$this->contID}" ); + $op = $this->params['dir'] == 'descending' ? '<' : '>'; + $this->addWhere( "{$this->bl_from}$op={$this->contID}" ); } if ( $this->params['filterredir'] == 'redirects' ) { @@ -155,7 +154,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } $this->addOption( 'LIMIT', $this->params['limit'] + 1 ); - $this->addOption( 'ORDER BY', $this->bl_from ); + $sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' ); + $this->addOption( 'ORDER BY', $this->bl_from . $sort ); $this->addOption( 'STRAIGHT_JOIN' ); } @@ -186,28 +186,35 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { // We can't use LinkBatch here because $this->hasNS may be false $titleWhere = array(); + $allRedirNs = array(); + $allRedirDBkey = array(); foreach ( $this->redirTitles as $t ) { - $titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $t->getDBkey() ) . - ( $this->hasNS ? " AND {$this->bl_ns} = '{$t->getNamespace()}'" : '' ); + $redirNs = $t->getNamespace(); + $redirDBkey = $t->getDBkey(); + $titleWhere[] = "{$this->bl_title} = " . $db->addQuotes( $redirDBkey ) . + ( $this->hasNS ? " AND {$this->bl_ns} = {$redirNs}" : '' ); + $allRedirNs[] = $redirNs; + $allRedirDBkey[] = $redirDBkey; } $this->addWhere( $db->makeList( $titleWhere, LIST_OR ) ); $this->addWhereFld( 'page_namespace', $this->params['namespace'] ); if ( !is_null( $this->redirID ) ) { + $op = $this->params['dir'] == 'descending' ? '<' : '>'; $first = $this->redirTitles[0]; - $title = $db->strencode( $first->getDBkey() ); + $title = $db->addQuotes( $first->getDBkey() ); $ns = $first->getNamespace(); $from = $this->redirID; if ( $this->hasNS ) { - $this->addWhere( "{$this->bl_ns} > $ns OR " . + $this->addWhere( "{$this->bl_ns} $op $ns OR " . "({$this->bl_ns} = $ns AND " . - "({$this->bl_title} > '$title' OR " . - "({$this->bl_title} = '$title' AND " . - "{$this->bl_from} >= $from)))" ); + "({$this->bl_title} $op $title OR " . + "({$this->bl_title} = $title AND " . + "{$this->bl_from} $op= $from)))" ); } else { - $this->addWhere( "{$this->bl_title} > '$title' OR " . - "({$this->bl_title} = '$title' AND " . - "{$this->bl_from} >= $from)" ); + $this->addWhere( "{$this->bl_title} $op $title OR " . + "({$this->bl_title} = $title AND " . + "{$this->bl_from} $op= $from)" ); } } if ( $this->params['filterredir'] == 'redirects' ) { @@ -217,7 +224,17 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } $this->addOption( 'LIMIT', $this->params['limit'] + 1 ); - $this->addOption( 'ORDER BY', $this->bl_sort ); + $orderBy = array(); + $sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' ); + // Don't order by namespace/title if it's constant in the WHERE clause + if( $this->hasNS && count( array_unique( $allRedirNs ) ) != 1 ) { + $orderBy[] = $this->bl_ns . $sort; + } + if( count( array_unique( $allRedirDBkey ) ) != 1 ) { + $orderBy[] = $this->bl_title . $sort; + } + $orderBy[] = $this->bl_from . $sort; + $this->addOption( 'ORDER BY', $orderBy ); $this->addOption( 'USE INDEX', array( 'page' => 'PRIMARY' ) ); } @@ -277,7 +294,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if ( $this->hasNS ) { $parentID = $this->pageMap[$row-> { $this->bl_ns } ][$row-> { $this->bl_title } ]; } else { - $parentID = $this->pageMap[NS_IMAGE][$row-> { $this->bl_title } ]; + $parentID = $this->pageMap[NS_FILE][$row-> { $this->bl_title } ]; } $this->continueStr = $this->getContinueRedirStr( $parentID, $row->page_id ); break; @@ -369,14 +386,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if ( !is_null( $this->params['continue'] ) ) { $this->parseContinueParam(); } else { - if ( $this->params['title'] !== '' ) { - $title = Title::newFromText( $this->params['title'] ); - if ( !$title ) { - $this->dieUsageMsg( array( 'invalidtitle', $this->params['title'] ) ); - } else { - $this->rootTitle = $title; - } - } + $this->rootTitle = $this->getTitleOrPageId( $this->params )->getTitle(); } // only image titles are allowed for the root in imageinfo mode @@ -436,13 +446,22 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $retval = array( 'title' => array( ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => true + ), + 'pageid' => array( + ApiBase::PARAM_TYPE => 'integer', ), 'continue' => null, 'namespace' => array( ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => 'namespace' ), + 'dir' => array(
+ ApiBase::PARAM_DFLT => 'ascending',
+ ApiBase::PARAM_TYPE => array(
+ 'ascending',
+ 'descending'
+ )
+ ), 'filterredir' => array( ApiBase::PARAM_DFLT => 'all', ApiBase::PARAM_TYPE => array( @@ -468,9 +487,11 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { public function getParamDescription() { $retval = array( - 'title' => 'Title to search', + 'title' => "Title to search. Cannot be used together with {$this->bl_code}pageid", + 'pageid' => "Pageid to search. Cannot be used together with {$this->bl_code}title", 'continue' => 'When more results are available, use this to continue', 'namespace' => 'The namespace to enumerate', + 'dir' => 'The direction in which to list', ); if ( $this->getModuleName() != 'embeddedin' ) { return array_merge( $retval, array( @@ -485,6 +506,17 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { ) ); } + public function getResultProperties() { + return array( + '' => array( + 'pageid' => 'integer', + 'ns' => 'namespace', + 'title' => 'string', + 'redirect' => 'boolean' + ) + ); + } + public function getDescription() { switch ( $this->getModuleName() ) { case 'backlinks': @@ -499,11 +531,13 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { } public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'invalidtitle', 'title' ), - array( 'code' => 'bad_image_title', 'info' => "The title for {$this->getModuleName()} query must be an image" ), - array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), - ) ); + return array_merge( parent::getPossibleErrors(), + $this->getTitleOrPageIdErrorMessage(), + array( + array( 'code' => 'bad_image_title', 'info' => "The title for {$this->getModuleName()} query must be an image" ), + array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), + ) + ); } public function getExamples() { diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 4fe82de0..2c48aca0 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -4,7 +4,7 @@ * * Created on Sep 7, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -233,7 +233,7 @@ abstract class ApiQueryBase extends ApiBase { */ protected function addTimestampWhereRange( $field, $dir, $start, $end, $sort = true ) { $db = $this->getDb(); - return $this->addWhereRange( $field, $dir, + $this->addWhereRange( $field, $dir, $db->timestampOrNull( $start ), $db->timestampOrNull( $end ), $sort ); } @@ -392,7 +392,7 @@ abstract class ApiQueryBase extends ApiBase { * @param $name string Name to assign to the database connection * @param $db int One of the DB_* constants * @param $groups array Query groups - * @return Database + * @return DatabaseBase */ public function selectNamedDB( $name, $db, $groups ) { $this->mDb = $this->getQuery()->getNamedDB( $name, $db, $groups ); @@ -519,7 +519,7 @@ abstract class ApiQueryBase extends ApiBase { $this->addFields( 'ipb_deleted' ); if ( $showBlockInfo ) { - $this->addFields( array( 'ipb_reason', 'ipb_by_text', 'ipb_expiry' ) ); + $this->addFields( array( 'ipb_id', 'ipb_by', 'ipb_by_text', 'ipb_reason', 'ipb_expiry' ) ); } // Don't show hidden names @@ -571,6 +571,11 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { private $mIsGenerator; + /** + * @param $query ApiBase + * @param $moduleName string + * @param $paramPrefix string + */ public function __construct( $query, $moduleName, $paramPrefix = '' ) { parent::__construct( $query, $moduleName, $paramPrefix ); $this->mIsGenerator = false; diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index bebb5a7d..96b86962 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -4,7 +4,7 @@ * * Created on Sep 10, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<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 @@ -77,6 +77,9 @@ class ApiQueryBlocks extends ApiQueryBase { $this->addOption( 'LIMIT', $params['limit'] + 1 ); $this->addTimestampWhereRange( 'ipb_timestamp', $params['dir'], $params['start'], $params['end'] ); + + $db = $this->getDB(); + if ( isset( $params['ids'] ) ) { $this->addWhereFld( 'ipb_id', $params['ids'] ); } @@ -87,7 +90,6 @@ class ApiQueryBlocks extends ApiQueryBase { $this->addWhereFld( 'ipb_address', $this->usernames ); $this->addWhereFld( 'ipb_auto', 0 ); } - $db = $this->getDB(); if ( isset( $params['ip'] ) ) { list( $ip, $range ) = IP::parseCIDR( $params['ip'] ); if ( $ip && $range ) { @@ -101,10 +103,15 @@ class ApiQueryBlocks extends ApiQueryBase { } $prefix = substr( $lower, 0, 4 ); + # Fairly hard to make a malicious SQL statement out of hex characters, + # but it is good practice to add quotes + $lower = $db->addQuotes( $lower ); + $upper = $db->addQuotes( $upper ); + $this->addWhere( array( 'ipb_range_start' . $db->buildLike( $prefix, $db->anyString() ), - "ipb_range_start <= '$lower'", - "ipb_range_end >= '$upper'", + 'ipb_range_start <= ' . $lower, + 'ipb_range_end >= ' . $upper, 'ipb_auto' => 0 ) ); } @@ -292,8 +299,8 @@ class ApiQueryBlocks extends ApiQueryBase { 'start' => 'The timestamp to start enumerating from', 'end' => 'The timestamp to stop enumerating at', 'dir' => $this->getDirectionDescription( $p ), - 'ids' => 'Pipe-separated list of block IDs to list (optional)', - 'users' => 'Pipe-separated list of users to search for (optional)', + 'ids' => 'List of block IDs to list (optional)', + 'users' => 'List of users to search for (optional)', '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 /16 are not accepted' ), 'limit' => 'The maximum amount of blocks to list', @@ -317,18 +324,74 @@ class ApiQueryBlocks extends ApiQueryBase { ); } + 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' + ) + ); + } + public function getDescription() { return 'List all blocked users and IP addresses'; } public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( + return array_merge( parent::getPossibleErrors(), $this->getRequireOnlyOneParameterErrorMessages( array( 'users', 'ip' ) ), - array( 'code' => 'cidrtoobroad', 'info' => 'CIDR ranges broader than /16 are not accepted' ), - 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( + array( 'code' => 'cidrtoobroad', 'info' => 'CIDR ranges broader than /16 are not accepted' ), + array( 'code' => 'param_user', 'info' => 'User parameter may not be empty' ), + array( 'code' => 'param_user', 'info' => 'User name user is not valid' ), + array( 'show' ), + ) + ); } public function getExamples() { diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index 1c1f1550..309c2ce9 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -4,7 +4,7 @@ * * Created on May 13, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -89,12 +89,13 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $this->dieUsage( "Invalid continue param. You should pass the " . "original value returned by the previous query", "_badcontinue" ); } + $op = $params['dir'] == 'descending' ? '<' : '>'; $clfrom = intval( $cont[0] ); - $clto = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); + $clto = $this->getDB()->addQuotes( $cont[1] ); $this->addWhere( - "cl_from > $clfrom OR " . + "cl_from $op $clfrom OR " . "(cl_from = $clfrom AND " . - "cl_to >= '$clto')" + "cl_to $op= $clto)" ); } @@ -123,14 +124,14 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $this->addOption( 'USE INDEX', array( 'categorylinks' => 'cl_from' ) ); - $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); // Don't order by cl_from if it's constant in the WHERE clause if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) { - $this->addOption( 'ORDER BY', 'cl_to' . $dir ); + $this->addOption( 'ORDER BY', 'cl_to' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'cl_from' . $dir, - 'cl_to' . $dir + 'cl_from' . $sort, + 'cl_to' . $sort )); } @@ -142,8 +143,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { 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->cl_from . - '|' . $this->keyToTitle( $row->cl_to ) ); + $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to ); break; } @@ -163,8 +163,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { $fit = $this->addPageSubItem( $row->cl_from, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', $row->cl_from . - '|' . $this->keyToTitle( $row->cl_to ) ); + $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to ); break; } } @@ -174,8 +173,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { 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->cl_from . - '|' . $this->keyToTitle( $row->cl_to ) ); + $this->setContinueEnumParameter( 'continue', $row->cl_from . '|' . $row->cl_to ); break; } @@ -239,6 +237,25 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { ); } + 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'; } diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php index c5070e87..31517fab 100644 --- a/includes/api/ApiQueryCategoryInfo.php +++ b/includes/api/ApiQueryCategoryInfo.php @@ -4,7 +4,7 @@ * * Created on May 13, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -25,7 +25,8 @@ */ /** - * This query adds the <categories> subelement to all pages with the list of categories the page is in + * This query adds the "<categories>" subelement to all pages with the list of + * categories the page is in. * * @ingroup API */ @@ -61,7 +62,7 @@ class ApiQueryCategoryInfo extends ApiQueryBase { 'pp_propname' => 'hiddencat' ) ), ) ); - $this->addFields( array( 'cat_title', 'cat_pages', 'cat_subcats', 'cat_files', 'pp_propname AS cat_hidden' ) ); + $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'] ) ) { @@ -106,6 +107,34 @@ 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'; } diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index 4b19b7e8..55ce0234 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -4,7 +4,7 @@ * * Created on June 14, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -54,22 +54,9 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { private function run( $resultPageSet = null ) { $params = $this->extractRequestParams(); - $this->requireOnlyOneParameter( $params, 'title', 'pageid' ); - - if ( isset( $params['title'] ) ) { - $categoryTitle = Title::newFromText( $params['title'] ); - - if ( is_null( $categoryTitle ) || $categoryTitle->getNamespace() != NS_CATEGORY ) { - $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' ); - } - } elseif( isset( $params['pageid'] ) ) { - $categoryTitle = Title::newFromID( $params['pageid'] ); - - if ( !$categoryTitle ) { - $this->dieUsageMsg( array( 'nosuchpageid', $params['pageid'] ) ); - } elseif ( $categoryTitle->getNamespace() != NS_CATEGORY ) { - $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' ); - } + $categoryTitle = $this->getTitleOrPageId( $params )->getTitle(); + if ( $categoryTitle->getNamespace() != NS_CATEGORY ) { + $this->dieUsage( 'The category name you entered is not valid', 'invalidcategory' ); } $prop = array_flip( $params['prop'] ); @@ -107,10 +94,10 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $this->addWhereFld( 'page_namespace', $params['namespace'] ); } - $dir = $params['dir'] == 'asc' ? 'newer' : 'older'; + $dir = in_array( $params['dir'], array( 'asc', 'ascending', 'newer' ) ) ? 'newer' : 'older'; if ( $params['sort'] == 'timestamp' ) { - $this->addWhereRange( 'cl_timestamp', + $this->addTimestampWhereRange( 'cl_timestamp', $dir, $params['start'], $params['end'] ); @@ -313,10 +300,15 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { ) ), 'dir' => array( - ApiBase::PARAM_DFLT => 'asc', + ApiBase::PARAM_DFLT => 'ascending', ApiBase::PARAM_TYPE => array( 'asc', - 'desc' + 'desc', + // Normalising with other modules + 'ascending', + 'descending', + 'newer', + 'older', ) ), 'start' => array( @@ -357,7 +349,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { '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", - 'continue' => 'For large categories, give the value retured from previous query', + 'continue' => 'For large categories, give the value returned from previous query', 'limit' => 'The maximum number of pages to return.', ); @@ -372,17 +364,46 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { 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' + ) + ); + } + public function getDescription() { return 'List all pages in a given category'; } public function getPossibleErrors() { return array_merge( parent::getPossibleErrors(), - $this->getRequireOnlyOneParameterErrorMessages( array( 'title', 'pageid' ) ), + $this->getTitleOrPageIdErrorMessage(), array( array( 'code' => 'invalidcategory', 'info' => 'The category name you entered is not valid' ), array( 'code' => 'badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), - array( 'nosuchpageid', 'pageid' ), ) ); } diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index 0a0cc93d..e69ccbd6 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -4,7 +4,7 @@ * * Created on Jul 2, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<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 @@ -155,7 +155,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $this->addWhereFld( 'ar_user_text', $params['user'] ); } elseif ( !is_null( $params['excludeuser'] ) ) { $this->addWhere( 'ar_user_text != ' . - $this->getDB()->addQuotes( $params['excludeuser'] ) ); + $db->addQuotes( $params['excludeuser'] ) ); } if ( !is_null( $params['continue'] ) && ( $mode == 'all' || $mode == 'revs' ) ) { @@ -164,14 +164,14 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $this->dieUsage( 'Invalid continue param. You should pass the original value returned by the previous query', 'badcontinue' ); } $ns = intval( $cont[0] ); - $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); - $ts = $this->getDB()->strencode( $cont[2] ); + $title = $db->addQuotes( $cont[1] ); + $ts = $db->addQuotes( $db->timestamp( $cont[2] ) ); $op = ( $dir == 'newer' ? '>' : '<' ); $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_title $op $title OR " . + "(ar_title = $title AND " . + "ar_timestamp $op= $ts)))" ); } $this->addOption( 'LIMIT', $limit + 1 ); @@ -180,7 +180,11 @@ class ApiQueryDeletedrevs extends ApiQueryBase { if ( $params['unique'] ) { $this->addOption( 'GROUP BY', 'ar_title' ); } else { - $this->addOption( 'ORDER BY', 'ar_title, ar_timestamp' ); + $sort = ( $dir == 'newer' ? '' : ' DESC' ); + $this->addOption( 'ORDER BY', array( + 'ar_title' . $sort, + 'ar_timestamp' . $sort + )); } } else { if ( $mode == 'revs' ) { @@ -199,7 +203,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { // We've had enough if ( $mode == 'all' || $mode == 'revs' ) { $this->setContinueEnumParameter( 'continue', intval( $row->ar_namespace ) . '|' . - $this->keyToTitle( $row->ar_title ) . '|' . $row->ar_timestamp ); + $row->ar_title . '|' . $row->ar_timestamp ); } else { $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ar_timestamp ) ); } @@ -265,7 +269,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { if ( !$fit ) { if ( $mode == 'all' || $mode == 'revs' ) { $this->setContinueEnumParameter( 'continue', intval( $row->ar_namespace ) . '|' . - $this->keyToTitle( $row->ar_title ) . '|' . $row->ar_timestamp ); + $row->ar_title . '|' . $row->ar_timestamp ); } else { $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->ar_timestamp ) ); } @@ -334,8 +338,8 @@ class ApiQueryDeletedrevs extends ApiQueryBase { public function getParamDescription() { return array( - 'start' => 'The timestamp to start enumerating from (1,2)', - 'end' => 'The timestamp to stop enumerating at (1,2)', + 'start' => 'The timestamp to start enumerating from (1, 2)', + 'end' => 'The timestamp to stop enumerating at (1, 2)', 'dir' => $this->getDirectionDescription( $this->getModulePrefix(), ' (1, 3)' ), 'from' => 'Start listing at this title (3)', 'to' => 'Stop listing at this title (3)', @@ -363,6 +367,18 @@ class ApiQueryDeletedrevs extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'ns' => 'namespace', + 'title' => 'string' + ), + 'token' => array( + 'token' => 'string' + ) + ); + } + public function getDescription() { $p = $this->getModulePrefix(); return array( diff --git a/includes/api/ApiQueryDisabled.php b/includes/api/ApiQueryDisabled.php index d68480c3..6715969a 100644 --- a/includes/api/ApiQueryDisabled.php +++ b/includes/api/ApiQueryDisabled.php @@ -4,7 +4,7 @@ * * Created on Sep 25, 2008 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<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 diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php index beca5879..8f0fd3be 100644 --- a/includes/api/ApiQueryDuplicateFiles.php +++ b/includes/api/ApiQueryDuplicateFiles.php @@ -4,7 +4,7 @@ * * Created on Sep 27, 2008 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<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 @@ -59,67 +59,99 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { } $images = $namespaces[NS_FILE]; - $this->addTables( 'image', 'i1' ); - $this->addTables( 'image', 'i2' ); - $this->addFields( array( - 'i1.img_name AS orig_name', - 'i2.img_name AS dup_name', - 'i2.img_user_text AS dup_user_text', - 'i2.img_timestamp AS dup_timestamp' - ) ); - - $this->addWhere( array( - 'i1.img_name' => array_keys( $images ), - 'i1.img_sha1 = i2.img_sha1', - 'i1.img_name != i2.img_name', - ) ); + if( $params['dir'] == 'descending' ) { + $images = array_reverse( $images ); + } + $skipUntilThisDup = false; if ( isset( $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' ); } - $orig = $this->getDB()->strencode( $this->titleTokey( $cont[0] ) ); - $dup = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); - $this->addWhere( - "i1.img_name > '$orig' OR " . - "(i1.img_name = '$orig' AND " . - "i2.img_name >= '$dup')" - ); + $fromImage = $cont[0]; + $skipUntilThisDup = $cont[1]; + // Filter out any images before $fromImage + foreach ( $images as $image => $pageId ) { + if ( $image < $fromImage ) { + unset( $images[$image] ); + } else { + break; + } + } } - $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' ); - $this->addOption( 'ORDER BY', 'i1.img_name' . $dir ); - $this->addOption( 'LIMIT', $params['limit'] + 1 ); + $filesToFind = array_keys( $images ); + if( $params['localonly'] ) { + $files = RepoGroup::singleton()->getLocalRepo()->findFiles( $filesToFind ); + } else { + $files = RepoGroup::singleton()->findFiles( $filesToFind ); + } - $res = $this->select( __METHOD__ ); + $fit = true; $count = 0; $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( 'continue', - $this->keyToTitle( $row->orig_name ) . '|' . - $this->keyToTitle( $row->dup_name ) ); - break; + + $sha1s = array(); + foreach ( $files as $file ) { + $sha1s[$file->getName()] = $file->getSha1(); + } + + // 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 ); + } else { + $filesBySha1s = RepoGroup::singleton()->findBySha1s( $filesToFindBySha1s ); + } + + // iterate over $images to handle continue param correct + foreach( $images as $image => $pageId ) { + if( !isset( $sha1s[$image] ) ) { + continue; //file does not exist + } + $sha1 = $sha1s[$image]; + $dupFiles = $filesBySha1s[$sha1]; + if( $params['dir'] == 'descending' ) { + $dupFiles = array_reverse( $dupFiles ); } - if ( !is_null( $resultPageSet ) ) { - $titles[] = Title::makeTitle( NS_FILE, $row->dup_name ); - } else { - $r = array( - 'name' => $row->dup_name, - 'user' => $row->dup_user_text, - 'timestamp' => wfTimestamp( TS_ISO_8601, $row->dup_timestamp ) - ); - $fit = $this->addPageSubItem( $images[$row->orig_name], $r ); - if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', - $this->keyToTitle( $row->orig_name ) . '|' . - $this->keyToTitle( $row->dup_name ) ); + foreach ( $dupFiles as $dupFile ) { + $dupName = $dupFile->getName(); + if( $image == $dupName && $dupFile->isLocal() ) { + continue; //ignore the local file itself + } + if( $skipUntilThisDup !== false && $dupName < $skipUntilThisDup ) { + continue; //skip to pos after the image from continue param + } + $skipUntilThisDup = false; + if ( ++$count > $params['limit'] ) { + $fit = false; //break outer loop + // We're one over limit which shows that + // there are additional images to be had. Stop here... + $this->setContinueEnumParameter( 'continue', $image . '|' . $dupName ); break; } + if ( !is_null( $resultPageSet ) ) { + $titles[] = $file->getTitle(); + } else { + $r = array( + 'name' => $dupName, + 'user' => $dupFile->getUser( 'text' ), + 'timestamp' => wfTimestamp( TS_ISO_8601, $dupFile->getTimestamp() ) + ); + if( !$dupFile->isLocal() ) { + $r['shared'] = ''; + } + $fit = $this->addPageSubItem( $pageId, $r ); + if ( !$fit ) { + $this->setContinueEnumParameter( 'continue', $image . '|' . $dupName ); + break; + } + } + } + if( !$fit ) { + break; } } if ( !is_null( $resultPageSet ) ) { @@ -144,19 +176,32 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { 'descending' ) ), + 'localonly' => false, ); } public function getParamDescription() { return array( - 'limit' => 'How many files to return', + 'limit' => 'How many duplicate files to return', 'continue' => 'When more results are available, use this to continue', 'dir' => 'The direction in which to list', + 'localonly' => 'Look only for files in the local repository', + ); + } + + 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)'; + return 'List all files that are duplicates of the given file(s) based on hash values'; } public function getPossibleErrors() { diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index 93c71e2f..42b398ba 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -4,7 +4,7 @@ * * Created on July 7, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -232,6 +232,21 @@ 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'; } diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php index a9fbc839..9365a9b8 100644 --- a/includes/api/ApiQueryExternalLinks.php +++ b/includes/api/ApiQueryExternalLinks.php @@ -4,7 +4,7 @@ * * Created on May 13, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -133,6 +133,14 @@ class ApiQueryExternalLinks extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + '' => array( + '*' => 'string' + ) + ); + } + public function getDescription() { return 'Returns all external urls (not interwikies) from the given page(s)'; } diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php index be995f30..a5486ef4 100644 --- a/includes/api/ApiQueryFilearchive.php +++ b/includes/api/ApiQueryFilearchive.php @@ -6,7 +6,7 @@ * * Copyright © 2010 Sam Reed * Copyright © 2008 Vasiliev Victor vasilvv@gmail.com, - * based on ApiQueryAllpages.php + * based on ApiQueryAllPages.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 @@ -56,8 +56,10 @@ class ApiQueryFilearchive extends ApiQueryBase { $fld_dimensions = isset( $prop['dimensions'] ); $fld_description = isset( $prop['description'] ) || isset( $prop['parseddescription'] ); $fld_mime = isset( $prop['mime'] ); + $fld_mediatype = isset( $prop['mediatype'] ); $fld_metadata = isset( $prop['metadata'] ); $fld_bitdepth = isset( $prop['bitdepth'] ); + $fld_archivename = isset( $prop['archivename'] ); $this->addTables( 'filearchive' ); @@ -68,12 +70,28 @@ class ApiQueryFilearchive extends ApiQueryBase { $this->addFieldsIf( array( 'fa_height', 'fa_width', 'fa_size' ), $fld_dimensions || $fld_size ); $this->addFieldsIf( 'fa_description', $fld_description ); $this->addFieldsIf( array( 'fa_major_mime', 'fa_minor_mime' ), $fld_mime ); + $this->addFieldsIf( 'fa_media_type', $fld_mediatype ); $this->addFieldsIf( 'fa_metadata', $fld_metadata ); $this->addFieldsIf( 'fa_bits', $fld_bitdepth ); + $this->addFieldsIf( 'fa_archive_name', $fld_archivename ); + + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + if ( count( $cont ) != 1 ) { + $this->dieUsage( "Invalid continue param. You should pass the " . + "original value returned by the previous query", "_badcontinue" ); + } + $op = $params['dir'] == 'descending' ? '<' : '>'; + $cont_from = $db->addQuotes( $cont[0] ); + $this->addWhere( "fa_name $op= $cont_from" ); + } // 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'] ) ); $this->addWhereRange( 'fa_name', $dir, $from, $to ); if ( isset( $params['prefix'] ) ) { @@ -117,8 +135,8 @@ class ApiQueryFilearchive extends ApiQueryBase { $limit = $params['limit']; $this->addOption( 'LIMIT', $limit + 1 ); - $this->addOption( 'ORDER BY', 'fa_name' . - ( $params['dir'] == 'descending' ? ' DESC' : '' ) ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $this->addOption( 'ORDER BY', 'fa_name' . $sort ); $res = $this->select( __METHOD__ ); @@ -127,8 +145,7 @@ class ApiQueryFilearchive extends ApiQueryBase { 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... - // TODO: Security issue - if the user has no right to view next title, it will still be shown - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->fa_name ) ); + $this->setContinueEnumParameter( 'continue', $row->fa_name ); break; } @@ -165,6 +182,9 @@ class ApiQueryFilearchive extends ApiQueryBase { $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 ) @@ -176,6 +196,9 @@ class ApiQueryFilearchive extends ApiQueryBase { if ( $fld_mime ) { $file['mime'] = "$row->fa_major_mime/$row->fa_minor_mime"; } + if ( $fld_archivename && !is_null( $row->fa_archive_name ) ) { + $file['archivename'] = $row->fa_archive_name; + } if ( $row->fa_deleted & File::DELETED_FILE ) { $file['filehidden'] = ''; @@ -194,7 +217,7 @@ class ApiQueryFilearchive extends ApiQueryBase { $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $file ); if ( !$fit ) { - $this->setContinueEnumParameter( 'from', $this->keyToTitle( $row->fa_name ) ); + $this->setContinueEnumParameter( 'continue', $row->fa_name ); break; } } @@ -205,6 +228,7 @@ class ApiQueryFilearchive extends ApiQueryBase { public function getAllowedParams() { return array ( 'from' => null, + 'continue' => null, 'to' => null, 'prefix' => null, 'limit' => array( @@ -235,8 +259,10 @@ class ApiQueryFilearchive extends ApiQueryBase { 'description', 'parseddescription', 'mime', + 'mediatype', 'metadata', - 'bitdepth' + 'bitdepth', + 'archivename', ), ), ); @@ -245,6 +271,7 @@ class ApiQueryFilearchive extends ApiQueryBase { public function getParamDescription() { return array( 'from' => 'The image title to start enumerating from', + 'continue' => 'When more results are available, use this to continue', 'to' => 'The image title to stop enumerating at', 'prefix' => 'Search for all image titles that begin with this value', 'dir' => 'The direction in which to list', @@ -261,9 +288,75 @@ class ApiQueryFilearchive extends ApiQueryBase { ' description - Adds description the image version', ' parseddescription - Parse the description on the version', ' mime - Adds MIME of the image', + ' mediatype - Adds the media type of the image', ' metadata - Lists EXIF metadata for the version of the image', ' bitdepth - Adds the bit depth of the version', - ), + ' archivename - Adds the file name of the archive version for non-latest versions' + ), + ); + } + + 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' + ), ); } @@ -277,6 +370,7 @@ class ApiQueryFilearchive extends ApiQueryBase { 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' ), + array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), ) ); } diff --git a/includes/api/ApiQueryIWBacklinks.php b/includes/api/ApiQueryIWBacklinks.php index feda1779..c5012f08 100644 --- a/includes/api/ApiQueryIWBacklinks.php +++ b/includes/api/ApiQueryIWBacklinks.php @@ -5,7 +5,7 @@ * Created on May 14, 2010 * * Copyright © 2010 Sam Reed - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -61,15 +61,17 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { 'original value returned by the previous query', '_badcontinue' ); } - $prefix = $this->getDB()->strencode( $cont[0] ); - $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); + $db = $this->getDB(); + $op = $params['dir'] == 'descending' ? '<' : '>'; + $prefix = $db->addQuotes( $cont[0] ); + $title = $db->addQuotes( $cont[1] ); $from = intval( $cont[2] ); $this->addWhere( - "iwl_prefix > '$prefix' OR " . - "(iwl_prefix = '$prefix' AND " . - "(iwl_title > '$title' OR " . - "(iwl_title = '$title' AND " . - "iwl_from >= $from)))" + "iwl_prefix $op $prefix OR " . + "(iwl_prefix = $prefix AND " . + "(iwl_title $op $title OR " . + "(iwl_title = $title AND " . + "iwl_from $op= $from)))" ); } @@ -83,16 +85,24 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { $this->addFields( array( 'page_id', 'page_title', 'page_namespace', 'page_is_redirect', 'iwl_from', 'iwl_prefix', 'iwl_title' ) ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); if ( isset( $params['prefix'] ) ) { $this->addWhereFld( 'iwl_prefix', $params['prefix'] ); if ( isset( $params['title'] ) ) { $this->addWhereFld( 'iwl_title', $params['title'] ); - $this->addOption( 'ORDER BY', 'iwl_from' ); + $this->addOption( 'ORDER BY', 'iwl_from' . $sort ); } else { - $this->addOption( 'ORDER BY', 'iwl_title, iwl_from' ); + $this->addOption( 'ORDER BY', array( + 'iwl_title' . $sort, + 'iwl_from' . $sort + )); } } else { - $this->addOption( 'ORDER BY', 'iwl_prefix, iwl_title, iwl_from' ); + $this->addOption( 'ORDER BY', array( + 'iwl_prefix' . $sort, + 'iwl_title' . $sort, + 'iwl_from' . $sort + )); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -170,6 +180,13 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { 'iwtitle', ), ), + 'dir' => array( + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => array( + 'ascending', + 'descending' + ) + ), ); } @@ -184,6 +201,24 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { ' iwtitle - Adds the title of the interwiki', ), 'limit' => 'How many total pages to return', + 'dir' => 'The direction in which to list', + ); + } + + public function getResultProperties() { + return array( + '' => array( + 'pageid' => 'integer', + 'ns' => 'namespace', + 'title' => 'string', + 'redirect' => 'boolean' + ), + 'iwprefix' => array( + 'iwprefix' => 'string' + ), + 'iwtitle' => array( + 'iwtitle' => 'string' + ) ); } @@ -205,7 +240,7 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { public function getExamples() { return array( 'api.php?action=query&list=iwbacklinks&iwbltitle=Test&iwblprefix=wikibooks', - 'api.php?action=query&generator=iwbacklinks&giwbltitle=Test&iwblprefix=wikibooks&prop=info' + 'api.php?action=query&generator=iwbacklinks&giwbltitle=Test&giwblprefix=wikibooks&prop=info' ); } diff --git a/includes/api/ApiQueryIWLinks.php b/includes/api/ApiQueryIWLinks.php index 13256ad8..30c7f5a8 100644 --- a/includes/api/ApiQueryIWLinks.php +++ b/includes/api/ApiQueryIWLinks.php @@ -5,7 +5,7 @@ * Created on May 14, 2010 * * Copyright © 2010 Sam Reed - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -62,38 +62,40 @@ class ApiQueryIWLinks extends ApiQueryBase { $this->dieUsage( 'Invalid continue param. You should pass the ' . 'original value returned by the previous query', '_badcontinue' ); } + $op = $params['dir'] == 'descending' ? '<' : '>'; + $db = $this->getDB(); $iwlfrom = intval( $cont[0] ); - $iwlprefix = $this->getDB()->strencode( $cont[1] ); - $iwltitle = $this->getDB()->strencode( $this->titleToKey( $cont[2] ) ); + $iwlprefix = $db->addQuotes( $cont[1] ); + $iwltitle = $db->addQuotes( $cont[2] ); $this->addWhere( - "iwl_from > $iwlfrom OR " . + "iwl_from $op $iwlfrom OR " . "(iwl_from = $iwlfrom AND " . - "(iwl_prefix > '$iwlprefix' OR " . - "(iwl_prefix = '$iwlprefix' AND " . - "iwl_title >= '$iwltitle')))" + "(iwl_prefix $op $iwlprefix OR " . + "(iwl_prefix = $iwlprefix AND " . + "iwl_title $op= $iwltitle)))" ); } - $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); if ( isset( $params['prefix'] ) ) { $this->addWhereFld( 'iwl_prefix', $params['prefix'] ); if ( isset( $params['title'] ) ) { $this->addWhereFld( 'iwl_title', $params['title'] ); - $this->addOption( 'ORDER BY', 'iwl_from' . $dir ); + $this->addOption( 'ORDER BY', 'iwl_from' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'iwl_title' . $dir, - 'iwl_from' . $dir + 'iwl_title' . $sort, + 'iwl_from' . $sort )); } } else { // Don't order by iwl_from if it's constant in the WHERE clause if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) { - $this->addOption( 'ORDER BY', 'iwl_prefix' . $dir ); + $this->addOption( 'ORDER BY', 'iwl_prefix' . $sort ); } else { $this->addOption( 'ORDER BY', array ( - 'iwl_from' . $dir, - 'iwl_prefix' . $dir + 'iwl_from' . $sort, + 'iwl_prefix' . $sort )); } } @@ -165,6 +167,19 @@ 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)'; } diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index 03a24821..d822eed5 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -4,7 +4,7 @@ * * Created on July 6, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -73,7 +73,12 @@ class ApiQueryImageInfo extends ApiQueryBase { } $result = $this->getResult(); - $images = RepoGroup::singleton()->findFiles( $titles ); + //search only inside the local repo + if( $params['localonly'] ) { + $images = RepoGroup::singleton()->getLocalRepo()->findFiles( $titles ); + } else { + $images = RepoGroup::singleton()->findFiles( $titles ); + } foreach ( $images as $img ) { // Skip redirects if ( $img->getOriginalTitle()->isRedirect() ) { @@ -81,14 +86,14 @@ class ApiQueryImageInfo extends ApiQueryBase { } $start = $skip ? $fromTimestamp : $params['start']; - $pageId = $pageIds[NS_IMAGE][ $img->getOriginalTitle()->getDBkey() ]; + $pageId = $pageIds[NS_FILE][ $img->getOriginalTitle()->getDBkey() ]; $fit = $result->addValue( array( 'query', 'pages', intval( $pageId ) ), 'imagerepository', $img->getRepoName() ); if ( !$fit ) { - if ( count( $pageIds[NS_IMAGE] ) == 1 ) { + if ( count( $pageIds[NS_FILE] ) == 1 ) { // The user is screwed. imageinfo can't be solely // responsible for exceeding the limit in this case, // so set a query-continue that just returns the same @@ -119,7 +124,7 @@ class ApiQueryImageInfo extends ApiQueryBase { self::getInfo( $img, $prop, $result, $finalThumbParams, $params['metadataversion'] ) ); if ( !$fit ) { - if ( count( $pageIds[NS_IMAGE] ) == 1 ) { + if ( count( $pageIds[NS_FILE] ) == 1 ) { // See the 'the user is screwed' comment above $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $img->getTimestamp() ) ); @@ -149,7 +154,7 @@ class ApiQueryImageInfo extends ApiQueryBase { self::getInfo( $oldie, $prop, $result, $finalThumbParams, $params['metadataversion'] ) ); if ( !$fit ) { - if ( count( $pageIds[NS_IMAGE] ) == 1 ) { + if ( count( $pageIds[NS_FILE] ) == 1 ) { $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $oldie->getTimestamp() ) ); } else { @@ -356,8 +361,7 @@ class ApiQueryImageInfo extends ApiQueryBase { if ( isset( $prop['thumbmime'] ) && $file->getHandler() ) { list( $ext, $mime ) = $file->getHandler()->getThumbType( - substr( $mto->getPath(), strrpos( $mto->getPath(), '.' ) + 1 ), - $file->getMimeType(), $thumbParams ); + $mto->getExtension(), $file->getMimeType(), $thumbParams ); $vals['thumbmime'] = $mime; } } elseif ( $mto && $mto->isError() ) { @@ -430,7 +434,7 @@ class ApiQueryImageInfo extends ApiQueryBase { * @param $img File * @return string */ - private function getContinueStr( $img ) { + protected function getContinueStr( $img ) { return $img->getOriginalTitle()->getText() . '|' . $img->getTimestamp(); } @@ -472,6 +476,7 @@ class ApiQueryImageInfo extends ApiQueryBase { ApiBase::PARAM_TYPE => 'string', ), 'continue' => null, + 'localonly' => false, ); } @@ -491,7 +496,7 @@ class ApiQueryImageInfo extends ApiQueryBase { * * @return array */ - private static function getProperties() { + private static function getProperties( $modulePrefix = '' ) { return array( 'timestamp' => ' timestamp - Adds timestamp for the uploaded version', 'user' => ' user - Adds the user who uploaded the image version', @@ -503,7 +508,8 @@ class ApiQueryImageInfo extends ApiQueryBase { 'dimensions' => ' dimensions - Alias for size', // For backwards compatibility 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)', + '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', @@ -518,10 +524,10 @@ class ApiQueryImageInfo extends ApiQueryBase { * * @return array */ - public static function getPropertyDescriptions( $filter = array() ) { + public static function getPropertyDescriptions( $filter = array(), $modulePrefix = '' ) { return array_merge( array( 'What image information to get:' ), - array_values( array_diff_key( self::getProperties(), array_flip( $filter ) ) ) + array_values( array_diff_key( self::getProperties( $modulePrefix ), array_flip( $filter ) ) ) ); } @@ -532,7 +538,7 @@ class ApiQueryImageInfo extends ApiQueryBase { public function getParamDescription() { $p = $this->getModulePrefix(); return array( - 'prop' => self::getPropertyDescriptions(), + '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.", 'Only the current version of the image can be scaled' ), 'urlheight' => "Similar to {$p}urlwidth. Cannot be used without {$p}urlwidth", @@ -543,10 +549,119 @@ class ApiQueryImageInfo extends ApiQueryBase { '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' + '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 + ) + ), + '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 + ) + ), + '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 + ) + ), + ); + return array_diff_key( $props, array_flip( $filter ) ); + } + + public function getResultProperties() { + return self::getResultPropertiesFiltered(); + } + public function getDescription() { return 'Returns image information and upload history'; } diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php index f03b2874..6052a75f 100644 --- a/includes/api/ApiQueryImages.php +++ b/includes/api/ApiQueryImages.php @@ -4,7 +4,7 @@ * * Created on May 13, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -25,7 +25,8 @@ */ /** - * This query adds an <images> subelement to all pages with the list of images embedded into those pages. + * This query adds an "<images>" subelement to all pages with the list of + * images embedded into those pages. * * @ingroup API */ @@ -65,23 +66,24 @@ class ApiQueryImages extends ApiQueryGeneratorBase { $this->dieUsage( 'Invalid continue param. You should pass the ' . 'original value returned by the previous query', '_badcontinue' ); } + $op = $params['dir'] == 'descending' ? '<' : '>'; $ilfrom = intval( $cont[0] ); - $ilto = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); + $ilto = $this->getDB()->addQuotes( $cont[1] ); $this->addWhere( - "il_from > $ilfrom OR " . + "il_from $op $ilfrom OR " . "(il_from = $ilfrom AND " . - "il_to >= '$ilto')" + "il_to $op= $ilto)" ); } - $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); // Don't order by il_from if it's constant in the WHERE clause if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) { - $this->addOption( 'ORDER BY', 'il_to' . $dir ); + $this->addOption( 'ORDER BY', 'il_to' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'il_from' . $dir, - 'il_to' . $dir + 'il_from' . $sort, + 'il_to' . $sort )); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -107,16 +109,14 @@ class ApiQueryImages extends ApiQueryGeneratorBase { 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->il_from . - '|' . $this->keyToTitle( $row->il_to ) ); + $this->setContinueEnumParameter( 'continue', $row->il_from . '|' . $row->il_to ); break; } $vals = array(); ApiQueryBase::addTitleInfo( $vals, Title::makeTitle( NS_FILE, $row->il_to ) ); $fit = $this->addPageSubItem( $row->il_from, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', $row->il_from . - '|' . $this->keyToTitle( $row->il_to ) ); + $this->setContinueEnumParameter( 'continue', $row->il_from . '|' . $row->il_to ); break; } } @@ -127,8 +127,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { 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->il_from . - '|' . $this->keyToTitle( $row->il_to ) ); + $this->setContinueEnumParameter( 'continue', $row->il_from . '|' . $row->il_to ); break; } $titles[] = Title::makeTitle( NS_FILE, $row->il_to ); @@ -173,6 +172,15 @@ class ApiQueryImages extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'ns' => 'namespace', + 'title' => 'string' + ) + ); + } + public function getDescription() { return 'Returns all images contained on the given page(s)'; } diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index f0d0faa3..5d4f0346 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -4,7 +4,7 @@ * * Created on Sep 25, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -33,7 +33,7 @@ class ApiQueryInfo extends ApiQueryBase { private $fld_protection = false, $fld_talkid = false, $fld_subjectid = false, $fld_url = false, - $fld_readable = false, $fld_watched = false, + $fld_readable = false, $fld_watched = false, $fld_notificationtimestamp = false, $fld_preload = false, $fld_displaytitle = false; private $params, $titles, $missing, $everything, $pageCounter; @@ -41,7 +41,7 @@ class ApiQueryInfo extends ApiQueryBase { private $pageRestrictions, $pageIsRedir, $pageIsNew, $pageTouched, $pageLatest, $pageLength; - private $protections, $watched, $talkids, $subjectids, $displaytitles; + private $protections, $watched, $notificationtimestamps, $talkids, $subjectids, $displaytitles; private $tokenFunctions; @@ -57,7 +57,10 @@ class ApiQueryInfo extends ApiQueryBase { global $wgDisableCounters; $pageSet->requestField( 'page_restrictions' ); - $pageSet->requestField( 'page_is_redirect' ); + // when resolving redirects, no page will have this field + if( !$pageSet->isResolvingRedirects() ) { + $pageSet->requestField( 'page_is_redirect' ); + } $pageSet->requestField( 'page_is_new' ); if ( !$wgDisableCounters ) { $pageSet->requestField( 'page_counter' ); @@ -99,6 +102,12 @@ class ApiQueryInfo extends ApiQueryBase { return $this->tokenFunctions; } + static $cachedTokens = array(); + + public static function resetTokenCache() { + ApiQueryInfo::$cachedTokens = array(); + } + public static function getEditToken( $pageid, $title ) { // We could check for $title->userCan('edit') here, // but that's too expensive for this purpose @@ -108,14 +117,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - // The edit token is always the same, let's exploit that - static $cachedEditToken = null; - if ( !is_null( $cachedEditToken ) ) { - return $cachedEditToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'edit' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'edit' ] = $wgUser->getEditToken(); } - $cachedEditToken = $wgUser->getEditToken(); - return $cachedEditToken; + return ApiQueryInfo::$cachedTokens[ 'edit' ]; } public static function getDeleteToken( $pageid, $title ) { @@ -124,13 +131,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedDeleteToken = null; - if ( !is_null( $cachedDeleteToken ) ) { - return $cachedDeleteToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'delete' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'delete' ] = $wgUser->getEditToken(); } - $cachedDeleteToken = $wgUser->getEditToken(); - return $cachedDeleteToken; + return ApiQueryInfo::$cachedTokens[ 'delete' ]; } public static function getProtectToken( $pageid, $title ) { @@ -139,13 +145,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedProtectToken = null; - if ( !is_null( $cachedProtectToken ) ) { - return $cachedProtectToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'protect' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'protect' ] = $wgUser->getEditToken(); } - $cachedProtectToken = $wgUser->getEditToken(); - return $cachedProtectToken; + return ApiQueryInfo::$cachedTokens[ 'protect' ]; } public static function getMoveToken( $pageid, $title ) { @@ -154,13 +159,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedMoveToken = null; - if ( !is_null( $cachedMoveToken ) ) { - return $cachedMoveToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'move' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'move' ] = $wgUser->getEditToken(); } - $cachedMoveToken = $wgUser->getEditToken(); - return $cachedMoveToken; + return ApiQueryInfo::$cachedTokens[ 'move' ]; } public static function getBlockToken( $pageid, $title ) { @@ -169,13 +173,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedBlockToken = null; - if ( !is_null( $cachedBlockToken ) ) { - return $cachedBlockToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'block' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'block' ] = $wgUser->getEditToken(); } - $cachedBlockToken = $wgUser->getEditToken(); - return $cachedBlockToken; + return ApiQueryInfo::$cachedTokens[ 'block' ]; } public static function getUnblockToken( $pageid, $title ) { @@ -189,13 +192,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedEmailToken = null; - if ( !is_null( $cachedEmailToken ) ) { - return $cachedEmailToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'email' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'email' ] = $wgUser->getEditToken(); } - $cachedEmailToken = $wgUser->getEditToken(); - return $cachedEmailToken; + return ApiQueryInfo::$cachedTokens[ 'email' ]; } public static function getImportToken( $pageid, $title ) { @@ -204,13 +206,12 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedImportToken = null; - if ( !is_null( $cachedImportToken ) ) { - return $cachedImportToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'import' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'import' ] = $wgUser->getEditToken(); } - $cachedImportToken = $wgUser->getEditToken(); - return $cachedImportToken; + return ApiQueryInfo::$cachedTokens[ 'import' ]; } public static function getWatchToken( $pageid, $title ) { @@ -219,13 +220,26 @@ class ApiQueryInfo extends ApiQueryBase { return false; } - static $cachedWatchToken = null; - if ( !is_null( $cachedWatchToken ) ) { - return $cachedWatchToken; + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'watch' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'watch' ] = $wgUser->getEditToken( 'watch' ); } - $cachedWatchToken = $wgUser->getEditToken( 'watch' ); - return $cachedWatchToken; + return ApiQueryInfo::$cachedTokens[ 'watch' ]; + } + + public static function getOptionsToken( $pageid, $title ) { + global $wgUser; + if ( !$wgUser->isLoggedIn() ) { + return false; + } + + // The token is always the same, let's exploit that + if ( !isset( ApiQueryInfo::$cachedTokens[ 'options' ] ) ) { + ApiQueryInfo::$cachedTokens[ 'options' ] = $wgUser->getEditToken(); + } + + return ApiQueryInfo::$cachedTokens[ 'options' ]; } public function execute() { @@ -234,6 +248,7 @@ class ApiQueryInfo extends ApiQueryBase { $prop = array_flip( $this->params['prop'] ); $this->fld_protection = isset( $prop['protection'] ); $this->fld_watched = isset( $prop['watched'] ); + $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] ); $this->fld_talkid = isset( $prop['talkid'] ); $this->fld_subjectid = isset( $prop['subjectid'] ); $this->fld_url = isset( $prop['url'] ); @@ -269,7 +284,10 @@ class ApiQueryInfo extends ApiQueryBase { } $this->pageRestrictions = $pageSet->getCustomField( 'page_restrictions' ); - $this->pageIsRedir = $pageSet->getCustomField( 'page_is_redirect' ); + // when resolving redirects, no page will have this field + $this->pageIsRedir = !$pageSet->isResolvingRedirects() + ? $pageSet->getCustomField( 'page_is_redirect' ) + : array(); $this->pageIsNew = $pageSet->getCustomField( 'page_is_new' ); global $wgDisableCounters; @@ -286,7 +304,7 @@ class ApiQueryInfo extends ApiQueryBase { $this->getProtectionInfo(); } - if ( $this->fld_watched ) { + if ( $this->fld_watched || $this->fld_notificationtimestamp ) { $this->getWatchedInfo(); } @@ -322,7 +340,10 @@ class ApiQueryInfo extends ApiQueryBase { */ private function extractPageInfo( $pageid, $title ) { $pageInfo = array(); - if ( $title->exists() ) { + $titleExists = $pageid > 0; //$title->exists() needs pageid, which is not set for all title objects + $ns = $title->getNamespace(); + $dbkey = $title->getDBkey(); + if ( $titleExists ) { global $wgDisableCounters; $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] ); @@ -332,7 +353,7 @@ class ApiQueryInfo extends ApiQueryBase { : intval( $this->pageCounter[$pageid] ); $pageInfo['length'] = intval( $this->pageLength[$pageid] ); - if ( $this->pageIsRedir[$pageid] ) { + if ( isset( $this->pageIsRedir[$pageid] ) && $this->pageIsRedir[$pageid] ) { $pageInfo['redirect'] = ''; } if ( $this->pageIsNew[$pageid] ) { @@ -355,23 +376,30 @@ class ApiQueryInfo extends ApiQueryBase { if ( $this->fld_protection ) { $pageInfo['protection'] = array(); - if ( isset( $this->protections[$title->getNamespace()][$title->getDBkey()] ) ) { + if ( isset( $this->protections[$ns][$dbkey] ) ) { $pageInfo['protection'] = - $this->protections[$title->getNamespace()][$title->getDBkey()]; + $this->protections[$ns][$dbkey]; } $this->getResult()->setIndexedTagName( $pageInfo['protection'], 'pr' ); } - if ( $this->fld_watched && isset( $this->watched[$title->getNamespace()][$title->getDBkey()] ) ) { + if ( $this->fld_watched && isset( $this->watched[$ns][$dbkey] ) ) { $pageInfo['watched'] = ''; } - if ( $this->fld_talkid && isset( $this->talkids[$title->getNamespace()][$title->getDBkey()] ) ) { - $pageInfo['talkid'] = $this->talkids[$title->getNamespace()][$title->getDBkey()]; + if ( $this->fld_notificationtimestamp ) { + $pageInfo['notificationtimestamp'] = ''; + if ( isset( $this->notificationtimestamps[$ns][$dbkey] ) ) { + $pageInfo['notificationtimestamp'] = wfTimestamp( TS_ISO_8601, $this->notificationtimestamps[$ns][$dbkey] ); + } + } + + if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) { + $pageInfo['talkid'] = $this->talkids[$ns][$dbkey]; } - if ( $this->fld_subjectid && isset( $this->subjectids[$title->getNamespace()][$title->getDBkey()] ) ) { - $pageInfo['subjectid'] = $this->subjectids[$title->getNamespace()][$title->getDBkey()]; + if ( $this->fld_subjectid && isset( $this->subjectids[$ns][$dbkey] ) ) { + $pageInfo['subjectid'] = $this->subjectids[$ns][$dbkey]; } if ( $this->fld_url ) { @@ -383,7 +411,7 @@ class ApiQueryInfo extends ApiQueryBase { } if ( $this->fld_preload ) { - if ( $title->exists() ) { + if ( $titleExists ) { $pageInfo['preload'] = ''; } else { $text = null; @@ -394,8 +422,8 @@ class ApiQueryInfo extends ApiQueryBase { } if ( $this->fld_displaytitle ) { - if ( isset( $this->displaytitles[$title->getArticleId()] ) ) { - $pageInfo['displaytitle'] = $this->displaytitles[$title->getArticleId()]; + if ( isset( $this->displaytitles[$pageid] ) ) { + $pageInfo['displaytitle'] = $this->displaytitles[$pageid]; } else { $pageInfo['displaytitle'] = $title->getPrefixedText(); } @@ -415,15 +443,14 @@ class ApiQueryInfo extends ApiQueryBase { // Get normal protections for existing titles if ( count( $this->titles ) ) { $this->resetQueryParams(); - $this->addTables( array( 'page_restrictions', 'page' ) ); - $this->addWhere( 'page_id=pr_page' ); + $this->addTables( 'page_restrictions' ); $this->addFields( array( 'pr_page', 'pr_type', 'pr_level', - 'pr_expiry', 'pr_cascade', 'page_namespace', - 'page_title' ) ); + 'pr_expiry', 'pr_cascade' ) ); $this->addWhereFld( 'pr_page', array_keys( $this->titles ) ); $res = $this->select( __METHOD__ ); foreach ( $res as $row ) { + $title = $this->titles[$row->pr_page]; $a = array( 'type' => $row->pr_type, 'level' => $row->pr_level, @@ -432,11 +459,14 @@ class ApiQueryInfo extends ApiQueryBase { if ( $row->pr_cascade ) { $a['cascade'] = ''; } - $this->protections[$row->page_namespace][$row->page_title][] = $a; - - // Also check old restrictions - if ( $this->pageRestrictions[$row->pr_page] ) { - $restrictions = explode( ':', trim( $this->pageRestrictions[$row->pr_page] ) ); + $this->protections[$title->getNamespace()][$title->getDBkey()][] = $a; + } + // Also check old restrictions + foreach( $this->titles as $pageId => $title ) { + if ( $this->pageRestrictions[$pageId] ) { + $namespace = $title->getNamespace(); + $dbKey = $title->getDBkey(); + $restrictions = explode( ':', trim( $this->pageRestrictions[$pageId] ) ); foreach ( $restrictions as $restrict ) { $temp = explode( '=', trim( $restrict ) ); if ( count( $temp ) == 1 ) { @@ -446,12 +476,12 @@ class ApiQueryInfo extends ApiQueryBase { if ( $restriction == '' ) { continue; } - $this->protections[$row->page_namespace][$row->page_title][] = array( + $this->protections[$namespace][$dbKey][] = array( 'type' => 'edit', 'level' => $restriction, 'expiry' => 'infinity', ); - $this->protections[$row->page_namespace][$row->page_title][] = array( + $this->protections[$namespace][$dbKey][] = array( 'type' => 'move', 'level' => $restriction, 'expiry' => 'infinity', @@ -461,7 +491,7 @@ class ApiQueryInfo extends ApiQueryBase { if ( $restriction == '' ) { continue; } - $this->protections[$row->page_namespace][$row->page_title][] = array( + $this->protections[$namespace][$dbKey][] = array( 'type' => $temp[0], 'level' => $restriction, 'expiry' => 'infinity', @@ -612,6 +642,7 @@ class ApiQueryInfo extends ApiQueryBase { /** * Get information about watched status and put it in $this->watched + * and $this->notificationtimestamps */ private function getWatchedInfo() { $user = $this->getUser(); @@ -621,6 +652,7 @@ class ApiQueryInfo extends ApiQueryBase { } $this->watched = array(); + $this->notificationtimestamps = array(); $db = $this->getDB(); $lb = new LinkBatch( $this->everything ); @@ -628,6 +660,7 @@ class ApiQueryInfo extends ApiQueryBase { $this->resetQueryParams(); $this->addTables( array( 'watchlist' ) ); $this->addFields( array( 'wl_title', 'wl_namespace' ) ); + $this->addFieldsIf( 'wl_notificationtimestamp', $this->fld_notificationtimestamp ); $this->addWhere( array( $lb->constructSet( 'wl', $db ), 'wl_user' => $user->getID() @@ -636,7 +669,12 @@ class ApiQueryInfo extends ApiQueryBase { $res = $this->select( __METHOD__ ); foreach ( $res as $row ) { - $this->watched[$row->wl_namespace][$row->wl_title] = true; + if ( $this->fld_watched ) { + $this->watched[$row->wl_namespace][$row->wl_title] = true; + } + if ( $this->fld_notificationtimestamp ) { + $this->notificationtimestamps[$row->wl_namespace][$row->wl_title] = $row->wl_notificationtimestamp; + } } } @@ -671,6 +709,7 @@ class ApiQueryInfo extends ApiQueryBase { 'protection', 'talkid', 'watched', # private + 'notificationtimestamp', # private 'subjectid', 'url', 'readable', # private @@ -692,20 +731,80 @@ class ApiQueryInfo extends ApiQueryBase { return array( 'prop' => array( 'Which additional properties to get:', - ' protection - List the protection level of each page', - ' talkid - The page ID of the talk page for each non-talk page', - ' watched - List the watched status 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', - ' 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', + ' protection - List the protection level of each page', + ' talkid - The page ID of the talk page for each non-talk page', + ' watched - List the watched status of each page', + ' 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', + ' 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', ), 'token' => 'Request a token to perform a data-modifying action on a page', 'continue' => 'When more results are available, use this to continue', ); } + public function 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 + ) + ), + 'watched' => array( + 'watched' => 'boolean' + ), + '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 15734944..3920407b 100644 --- a/includes/api/ApiQueryLangBacklinks.php +++ b/includes/api/ApiQueryLangBacklinks.php @@ -5,7 +5,7 @@ * Created on May 14, 2011 * * Copyright © 2011 Sam Reed - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -61,15 +61,17 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { 'original value returned by the previous query', '_badcontinue' ); } - $prefix = $this->getDB()->strencode( $cont[0] ); - $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); + $db = $this->getDB(); + $op = $params['dir'] == 'descending' ? '<' : '>'; + $prefix = $db->addQuotes( $cont[0] ); + $title = $db->addQuotes( $cont[1] ); $from = intval( $cont[2] ); $this->addWhere( - "ll_lang > '$prefix' OR " . - "(ll_lang = '$prefix' AND " . - "(ll_title > '$title' OR " . - "(ll_title = '$title' AND " . - "ll_from >= $from)))" + "ll_lang $op $prefix OR " . + "(ll_lang = $prefix AND " . + "(ll_title $op $title OR " . + "(ll_title = $title AND " . + "ll_from $op= $from)))" ); } @@ -83,16 +85,24 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { $this->addFields( array( 'page_id', 'page_title', 'page_namespace', 'page_is_redirect', 'll_from', 'll_lang', 'll_title' ) ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); if ( isset( $params['lang'] ) ) { $this->addWhereFld( 'll_lang', $params['lang'] ); if ( isset( $params['title'] ) ) { $this->addWhereFld( 'll_title', $params['title'] ); - $this->addOption( 'ORDER BY', 'll_from' ); + $this->addOption( 'ORDER BY', 'll_from' . $sort ); } else { - $this->addOption( 'ORDER BY', 'll_title, ll_from' ); + $this->addOption( 'ORDER BY', array( + 'll_title' . $sort, + 'll_from' . $sort + )); } } else { - $this->addOption( 'ORDER BY', 'll_lang, ll_title, ll_from' ); + $this->addOption( 'ORDER BY', array( + 'll_lang' . $sort, + 'll_title' . $sort, + 'll_from' . $sort + )); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -170,6 +180,13 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { 'lltitle', ), ), + 'dir' => array( + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => array( + 'ascending', + 'descending' + ) + ), ); } @@ -184,6 +201,24 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { ' lltitle - Adds the title of the language ink', ), 'limit' => 'How many total pages to return', + 'dir' => 'The direction in which to list', + ); + } + + public function getResultProperties() { + return array( + '' => array( + 'pageid' => 'integer', + 'ns' => 'namespace', + 'title' => 'string', + 'redirect' => 'boolean' + ), + 'lllang' => array( + 'lllang' => 'string' + ), + 'lltitle' => array( + 'lltitle' => 'string' + ) ); } @@ -205,7 +240,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { public function getExamples() { return array( 'api.php?action=query&list=langbacklinks&lbltitle=Test&lbllang=fr', - 'api.php?action=query&generator=langbacklinks&glbltitle=Test&lbllang=fr&prop=info' + 'api.php?action=query&generator=langbacklinks&glbltitle=Test&glbllang=fr&prop=info' ); } diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index fdba8465..3109a090 100644 --- a/includes/api/ApiQueryLangLinks.php +++ b/includes/api/ApiQueryLangLinks.php @@ -4,7 +4,7 @@ * * Created on May 13, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -60,35 +60,36 @@ class ApiQueryLangLinks extends ApiQueryBase { $this->dieUsage( 'Invalid continue param. You should pass the ' . 'original value returned by the previous query', '_badcontinue' ); } + $op = $params['dir'] == 'descending' ? '<' : '>'; $llfrom = intval( $cont[0] ); - $lllang = $this->getDB()->strencode( $cont[1] ); + $lllang = $this->getDB()->addQuotes( $cont[1] ); $this->addWhere( - "ll_from > $llfrom OR " . + "ll_from $op $llfrom OR " . "(ll_from = $llfrom AND " . - "ll_lang >= '$lllang')" + "ll_lang $op= $lllang)" ); } - $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' ); - if ( isset( $params['lang'] ) ) { + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + if ( isset( $params['lang'] ) ) { $this->addWhereFld( 'll_lang', $params['lang'] ); if ( isset( $params['title'] ) ) { $this->addWhereFld( 'll_title', $params['title'] ); - $this->addOption( 'ORDER BY', 'll_from' . $dir ); + $this->addOption( 'ORDER BY', 'll_from' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'll_title' . $dir, - 'll_from' . $dir + 'll_title' . $sort, + 'll_from' . $sort )); } } else { // Don't order by ll_from if it's constant in the WHERE clause if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) { - $this->addOption( 'ORDER BY', 'll_lang' . $dir ); + $this->addOption( 'ORDER BY', 'll_lang' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'll_from' . $dir, - 'll_lang' . $dir + 'll_from' . $sort, + 'll_lang' . $sort )); } } @@ -158,6 +159,19 @@ class ApiQueryLangLinks extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'lang' => 'string', + 'url' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + '*' => 'string' + ) + ); + } + public function getDescription() { return 'Returns all interlanguage links from the given page(s)'; } diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index 0377eddb..9e4b7ebb 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -4,7 +4,7 @@ * * Created on May 12, 2007 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -85,9 +85,9 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); $this->addFields( array( - $this->prefix . '_from AS pl_from', - $this->prefix . '_namespace AS pl_namespace', - $this->prefix . '_title AS pl_title' + 'pl_from' => $this->prefix . '_from', + 'pl_namespace' => $this->prefix . '_namespace', + 'pl_title' => $this->prefix . '_title' ) ); $this->addTables( $this->table ); @@ -116,19 +116,20 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $this->dieUsage( 'Invalid continue param. You should pass the ' . 'original value returned by the previous query', '_badcontinue' ); } + $op = $params['dir'] == 'descending' ? '<' : '>'; $plfrom = intval( $cont[0] ); $plns = intval( $cont[1] ); - $pltitle = $this->getDB()->strencode( $this->titleToKey( $cont[2] ) ); + $pltitle = $this->getDB()->addQuotes( $cont[2] ); $this->addWhere( - "{$this->prefix}_from > $plfrom OR " . + "{$this->prefix}_from $op $plfrom OR " . "({$this->prefix}_from = $plfrom AND " . - "({$this->prefix}_namespace > $plns OR " . + "({$this->prefix}_namespace $op $plns OR " . "({$this->prefix}_namespace = $plns AND " . - "{$this->prefix}_title >= '$pltitle')))" + "{$this->prefix}_title $op= $pltitle)))" ); } - $dir = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); // Here's some MySQL craziness going on: if you use WHERE foo='bar' // and later ORDER BY foo MySQL doesn't notice the ORDER BY is pointless // but instead goes and filesorts, because the index for foo was used @@ -136,13 +137,13 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { // clause from the ORDER BY clause $order = array(); if ( count( $this->getPageSet()->getGoodTitles() ) != 1 ) { - $order[] = $this->prefix . '_from' . $dir; + $order[] = $this->prefix . '_from' . $sort; } if ( count( $params['namespace'] ) != 1 ) { - $order[] = $this->prefix . '_namespace' . $dir; + $order[] = $this->prefix . '_namespace' . $sort; } - $order[] = $this->prefix . "_title" . $dir; + $order[] = $this->prefix . '_title' . $sort; $this->addOption( 'ORDER BY', $order ); $this->addOption( 'USE INDEX', $this->prefix . '_from' ); $this->addOption( 'LIMIT', $params['limit'] + 1 ); @@ -156,8 +157,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { // We've reached the one extra which shows that // there are additional pages to be had. Stop here... $this->setContinueEnumParameter( 'continue', - "{$row->pl_from}|{$row->pl_namespace}|" . - $this->keyToTitle( $row->pl_title ) ); + "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" ); break; } $vals = array(); @@ -165,8 +165,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $fit = $this->addPageSubItem( $row->pl_from, $vals ); if ( !$fit ) { $this->setContinueEnumParameter( 'continue', - "{$row->pl_from}|{$row->pl_namespace}|" . - $this->keyToTitle( $row->pl_title ) ); + "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" ); break; } } @@ -178,8 +177,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { // We've reached the one extra which shows that // there are additional pages to be had. Stop here... $this->setContinueEnumParameter( 'continue', - "{$row->pl_from}|{$row->pl_namespace}|" . - $this->keyToTitle( $row->pl_title ) ); + "{$row->pl_from}|{$row->pl_namespace}|{$row->pl_title}" ); break; } $titles[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); @@ -226,6 +224,15 @@ 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)"; } diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 0d07a254..5d85c221 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -4,7 +4,7 @@ * * Created on Oct 16, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -195,6 +195,7 @@ class ApiQueryLogEvents extends ApiQueryBase { * @param $type string * @param $action string * @param $ts + * @param $legacy bool * @return array */ public static function addLogParams( $result, &$vals, $params, $type, $action, $ts, $legacy = false ) { @@ -262,6 +263,7 @@ class ApiQueryLogEvents extends ApiQueryBase { } if ( !is_null( $params ) ) { $result->setIndexedTagName( $params, 'param' ); + $result->setIndexedTagName_recursive( $params, 'param' ); $vals = array_merge( $vals, $params ); } return $vals; @@ -358,7 +360,7 @@ class ApiQueryLogEvents extends ApiQueryBase { public function getCacheMode( $params ) { if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) { - // formatComment() calls wfMsg() among other things + // formatComment() calls wfMessage() among other things return 'anon-public-user-private'; } else { return 'public'; @@ -366,7 +368,7 @@ class ApiQueryLogEvents extends ApiQueryBase { } public function getAllowedParams() { - global $wgLogTypes, $wgLogActions; + global $wgLogTypes, $wgLogActions, $wgLogActionsHandlers; return array( 'prop' => array( ApiBase::PARAM_ISMULTI => true, @@ -388,7 +390,7 @@ class ApiQueryLogEvents extends ApiQueryBase { ApiBase::PARAM_TYPE => $wgLogTypes ), 'action' => array( - ApiBase::PARAM_TYPE => array_keys( $wgLogActions ) + ApiBase::PARAM_TYPE => array_keys( array_merge( $wgLogActions, $wgLogActionsHandlers ) ) ), 'start' => array( ApiBase::PARAM_TYPE => 'timestamp' @@ -446,6 +448,62 @@ class ApiQueryLogEvents extends ApiQueryBase { ); } + 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 + ) + ) + ); + } + public function getDescription() { return 'Get events from logs'; } diff --git a/includes/api/ApiQueryProtectedTitles.php b/includes/api/ApiQueryProtectedTitles.php index 44cc1d32..14aed28d 100644 --- a/includes/api/ApiQueryProtectedTitles.php +++ b/includes/api/ApiQueryProtectedTitles.php @@ -4,7 +4,7 @@ * * Created on Feb 13, 2009 * - * Copyright © 2009 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2009 Roan Kattouw "<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 @@ -139,7 +139,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { public function getCacheMode( $params ) { if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) { - // formatComment() calls wfMsg() among other things + // formatComment() calls wfMessage() among other things return 'anon-public-user-private'; } else { return 'public'; @@ -214,6 +214,40 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { ); } + 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' + ), + 'comment' => array( + 'comment' => 'string' + ), + 'parsedcomment' => array( + 'parsedcomment' => 'string' + ), + 'expiry' => array( + 'expiry' => 'timestamp' + ), + 'level' => array( + 'level' => array( + ApiBase::PROP_TYPE => array_diff( $wgRestrictionLevels, array( '' ) ) + ) + ) + ); + } + public function getDescription() { return 'List all titles protected from creation'; } diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php index 5eba0de6..a8be26d3 100644 --- a/includes/api/ApiQueryQueryPage.php +++ b/includes/api/ApiQueryQueryPage.php @@ -4,7 +4,7 @@ * * Created on Dec 22, 2010 * - * Copyright © 2010 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2010 Roan Kattouw "<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 @@ -70,6 +70,8 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { * @param $resultPageSet ApiPageSet */ public function run( $resultPageSet = null ) { + global $wgQueryCacheLimit; + $params = $this->extractRequestParams(); $result = $this->getResult(); @@ -88,6 +90,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { if ( $ts ) { $r['cachedtimestamp'] = wfTimestamp( TS_ISO_8601, $ts ); } + $r['maxresults'] = $wgQueryCacheLimit; } } $result->addValue( array( 'query' ), $this->getModuleName(), $r ); @@ -170,6 +173,38 @@ 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'; } diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php index 2e9e2dd5..ddf5841b 100644 --- a/includes/api/ApiQueryRandom.php +++ b/includes/api/ApiQueryRandom.php @@ -161,6 +161,16 @@ 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', diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index bf5bbd9b..7ae4f371 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -4,7 +4,7 @@ * * Created on Oct 19, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -70,24 +70,37 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /** * @param $pageid * @param $title - * @param $rc RecentChange + * @param $rc RecentChange (optional) * @return bool|String */ - public static function getPatrolToken( $pageid, $title, $rc ) { + public static function getPatrolToken( $pageid, $title, $rc = null ) { global $wgUser; - if ( !$wgUser->useRCPatrol() && ( !$wgUser->useNPPatrol() || - $rc->getAttribute( 'rc_type' ) != RC_NEW ) ) - { - return false; + + $validTokenUser = false; + + 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() ) { + $validTokenUser = true; + } } - // The patrol token is always the same, let's exploit that - static $cachedPatrolToken = null; - if ( is_null( $cachedPatrolToken ) ) { - $cachedPatrolToken = $wgUser->getEditToken( 'patrol' ); + 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 $cachedPatrolToken; } /** @@ -131,7 +144,7 @@ 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' + * AND rc_deleted = 0 */ $this->addTables( 'recentchanges' ); $index = array( 'recentchanges' => 'rc_timestamp' ); // May change @@ -223,7 +236,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $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( array( 'rc_minor', 'rc_new', 'rc_bot' ) , $this->fld_flags ); + $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 ); @@ -304,7 +317,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { * Extracts from a single sql row the data needed to describe one recent change. * * @param $row The row from which to extract the data. - * @return An array mapping strings (descriptors) to their respective string values. + * @return array An array mapping strings (descriptors) to their respective string values. * @access public */ public function extractRowInfo( $row ) { @@ -380,7 +393,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { if ( $row->rc_bot ) { $vals['bot'] = ''; } - if ( $row->rc_new ) { + if ( $row->rc_type == RC_NEW ) { $vals['new'] = ''; } if ( $row->rc_minor ) { @@ -423,13 +436,14 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $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, - $row->rc_params, - $row->rc_log_action, - $row->rc_log_type, - $row->rc_timestamp + $logEntry->getParameters(), + $logEntry->getType(), + $logEntry->getSubtype(), + $logEntry->getTimestamp() ); } @@ -489,7 +503,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { return 'private'; } if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) { - // formatComment() calls wfMsg() among other things + // formatComment() calls wfMessage() among other things return 'anon-public-user-private'; } return 'public'; @@ -615,6 +629,97 @@ 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 + ) + ) + ); + + self::addTokenProperties( $props, $this->getTokenFunctions() ); + + return $props; + } + public function getDescription() { return 'Enumerate recent changes'; } diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index fa58bdf0..b89a8ea9 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -4,7 +4,7 @@ * * Created on Sep 7, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -224,6 +224,13 @@ class ApiQueryRevisions extends ApiQueryBase { } } + // add user name, if needed + if ( $this->fld_user ) { + $this->addTables( 'user' ); + $this->addJoinConds( array( 'user' => Revision::userJoinCond() ) ); + $this->addFields( Revision::selectUserFields() ); + } + // Bug 24166 - API error when using rvprop=tags $this->addTables( 'revision' ); @@ -241,6 +248,16 @@ class ApiQueryRevisions extends ApiQueryBase { $this->dieUsage( 'user and excludeuser cannot be used together', 'badparams' ); } + // Continuing effectively uses startid. But we can't use rvstartid + // directly, because there is no way to tell the client to ''not'' + // send rvstart if it sent it in the original query. So instead we + // send the continuation startid as rvcontinue, and ignore both + // rvstart and rvstartid when that is supplied. + if ( !is_null( $params['continue'] ) ) { + $params['startid'] = $params['continue']; + unset( $params['start'] ); + } + // This code makes an assumption that sorting by rev_id and rev_timestamp produces // the same result. This way users may request revisions starting at a given time, // but to page through results use the rev_id returned after each page. @@ -290,7 +307,7 @@ class ApiQueryRevisions extends ApiQueryBase { $this->addWhereFld( 'rev_id', array_keys( $revs ) ); if ( !is_null( $params['continue'] ) ) { - $this->addWhere( "rev_id >= '" . intval( $params['continue'] ) . "'" ); + $this->addWhere( 'rev_id >= ' . intval( $params['continue'] ) ); } $this->addOption( 'ORDER BY', 'rev_id' ); @@ -322,12 +339,15 @@ class ApiQueryRevisions extends ApiQueryBase { $pageid = intval( $cont[0] ); $revid = intval( $cont[1] ); $this->addWhere( - "rev_page > '$pageid' OR " . - "(rev_page = '$pageid' AND " . - "rev_id >= '$revid')" + "rev_page > $pageid OR " . + "(rev_page = $pageid AND " . + "rev_id >= $revid)" ); } - $this->addOption( 'ORDER BY', 'rev_page, rev_id' ); + $this->addOption( 'ORDER BY', array( + 'rev_page', + 'rev_id' + )); // assumption testing -- we should never get more then $pageCount rows. $limit = $pageCount; @@ -347,14 +367,14 @@ class ApiQueryRevisions extends ApiQueryBase { if ( !$enumRevMode ) { ApiBase::dieDebug( __METHOD__, 'Got more rows then expected' ); // bug report } - $this->setContinueEnumParameter( 'startid', intval( $row->rev_id ) ); + $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) ); break; } $fit = $this->addPageSubItem( $row->rev_page, $this->extractRowInfo( $row ), 'rev' ); if ( !$fit ) { if ( $enumRevMode ) { - $this->setContinueEnumParameter( 'startid', intval( $row->rev_id ) ); + $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) ); } elseif ( $revCount > 0 ) { $this->setContinueEnumParameter( 'continue', intval( $row->rev_id ) ); } else { @@ -528,7 +548,7 @@ class ApiQueryRevisions extends ApiQueryBase { return 'private'; } if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) { - // formatComment() calls wfMsg() among other things + // formatComment() calls wfMessage() among other things return 'anon-public-user-private'; } return 'public'; @@ -638,6 +658,66 @@ class ApiQueryRevisions extends ApiQueryBase { ); } + 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' + ) + ); + + self::addTokenProperties( $props, $this->getTokenFunctions() ); + + return $props; + } + public function getDescription() { return array( 'Get revision information', diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index 40aac050..364433d5 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -4,7 +4,7 @@ * * Created on July 30, 2007 * - * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2007 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 @@ -280,6 +280,63 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { ); } + 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'; } diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index e2580ac6..ec503d64 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -4,7 +4,7 @@ * * Created on Sep 25, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -93,6 +93,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { case 'showhooks': $fit = $this->appendSubscribedHooks( $p ); break; + case 'variables': + $fit = $this->appendVariables( $p ); + break; default: ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" ); } @@ -121,9 +124,14 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['dbtype'] = $GLOBALS['wgDBtype']; $data['dbversion'] = $this->getDB()->getServerVersion(); - $svn = SpecialVersion::getSvnRevision( $GLOBALS['IP'] ); - if ( $svn ) { - $data['rev'] = $svn; + $git = SpecialVersion::getGitHeadSha1( $GLOBALS['IP'] ); + if ( $git ) { + $data['git-hash'] = $git; + } else { + $svn = SpecialVersion::getSvnRevision( $GLOBALS['IP'] ); + if ( $svn ) { + $data['rev'] = $svn; + } } // 'case-insensitive' option is reserved for future @@ -142,6 +150,15 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['fallback'] = $fallbacks; $this->getResult()->setIndexedTagName( $data['fallback'], 'lang' ); + if( $wgContLang->hasVariants() ) { + $variants = array(); + foreach( $wgContLang->getVariants() as $code ) { + $variants[] = array( 'code' => $code ); + } + $data['variants'] = $variants; + $this->getResult()->setIndexedTagName( $data['variants'], 'lang' ); + } + if ( $wgContLang->isRTL() ) { $data['rtl'] = ''; } @@ -177,6 +194,8 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['misermode'] = ''; } + $data['maxuploadsize'] = UploadBase::getMaxUploadSize(); + wfRunHooks( 'APIQuerySiteInfoGeneralInfo', array( $this, &$data ) ); return $this->getResult()->addValue( 'query', $property, $data ); @@ -204,6 +223,10 @@ class ApiQuerySiteinfo extends ApiQueryBase { if ( MWNamespace::isContent( $ns ) ) { $data[$ns]['content'] = ''; } + + if ( MWNamespace::isNonincludable( $ns ) ) { + $data[$ns]['nonincludable'] = ''; + } } $this->getResult()->setIndexedTagName( $data, 'ns' ); @@ -234,10 +257,13 @@ class ApiQuerySiteinfo extends ApiQueryBase { protected function appendSpecialPageAliases( $property ) { global $wgContLang; $data = array(); - foreach ( $wgContLang->getSpecialPageAliases() as $specialpage => $aliases ) { - $arr = array( 'realname' => $specialpage, 'aliases' => $aliases ); - $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' ); - $data[] = $arr; + $aliases = $wgContLang->getSpecialPageAliases(); + foreach ( SpecialPageFactory::getList() as $specialpage => $stuff ) { + if ( isset( $aliases[$specialpage] ) ) { + $arr = array( 'realname' => $specialpage, 'aliases' => $aliases[$specialpage] ); + $this->getResult()->setIndexedTagName( $arr['aliases'], 'alias' ); + $data[] = $arr; + } } $this->getResult()->setIndexedTagName( $data, 'specialpage' ); return $this->getResult()->addValue( 'query', $property, $data ); @@ -271,12 +297,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $params = $this->extractRequestParams(); $langCode = isset( $params['inlanguagecode'] ) ? $params['inlanguagecode'] : ''; - - if( $langCode ) { - $langNames = Language::getTranslatedLanguageNames( $langCode ); - } else { - $langNames = Language::getLanguageNames(); - } + $langNames = Language::fetchLanguageNames( $langCode ); $getPrefixes = Interwiki::getAllPrefixes( $local ); $data = array(); @@ -477,12 +498,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { public function appendLanguages( $property ) { $params = $this->extractRequestParams(); $langCode = isset( $params['inlanguagecode'] ) ? $params['inlanguagecode'] : ''; - - if( $langCode ) { - $langNames = Language::getTranslatedLanguageNames( $langCode ); - } else { - $langNames = Language::getLanguageNames(); - } + $langNames = Language::fetchLanguageNames( $langCode ); $data = array(); @@ -522,6 +538,12 @@ class ApiQuerySiteinfo extends ApiQueryBase { 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 ); + } + private function formatParserTags( $item ) { return "<{$item}>"; } @@ -573,6 +595,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { 'extensiontags', 'functionhooks', 'showhooks', + 'variables', ) ), 'filteriw' => array( @@ -608,7 +631,8 @@ class ApiQuerySiteinfo extends ApiQueryBase { ' skins - Returns a list of all enabled skins', ' 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)' + ' showhooks - Returns a list of all subscribed hooks (contents of $wgHooks)', + ' variables - Returns a list of variable IDs', ), 'filteriw' => 'Return only local or only nonlocal entries of the interwiki map', 'showalldb' => 'List all database servers, not just the one lagging the most', diff --git a/includes/api/ApiQueryStashImageInfo.php b/includes/api/ApiQueryStashImageInfo.php index 4501ec58..a310d109 100644 --- a/includes/api/ApiQueryStashImageInfo.php +++ b/includes/api/ApiQueryStashImageInfo.php @@ -113,7 +113,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { public function getParamDescription() { $p = $this->getModulePrefix(); return array( - 'prop' => self::getPropertyDescriptions( $this->propertyFilter ), + 'prop' => self::getPropertyDescriptions( $this->propertyFilter, $p ), 'filekey' => 'Key that identifies a previous upload that was stashed temporarily.', 'sessionkey' => 'Alias for filekey, for backward compatibility.', 'urlwidth' => "If {$p}prop=url is set, a URL to an image scaled to this width will be returned.", @@ -123,6 +123,10 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { ); } + public function getResultProperties() { + return ApiQueryImageInfo::getResultPropertiesFiltered( $this->propertyFilter ); + } + public function getDescription() { return 'Returns image information for stashed images'; } diff --git a/includes/api/ApiQueryTags.php b/includes/api/ApiQueryTags.php index 12cea1d7..f97c1b2a 100644 --- a/includes/api/ApiQueryTags.php +++ b/includes/api/ApiQueryTags.php @@ -59,7 +59,7 @@ class ApiQueryTags extends ApiQueryBase { $this->addTables( 'change_tag' ); $this->addFields( 'ct_tag' ); - $this->addFieldsIf( 'count(*) AS hitcount', $this->fld_hitcount ); + $this->addFieldsIf( array( 'hitcount' => 'COUNT(*)' ), $this->fld_hitcount ); $this->addOption( 'LIMIT', $this->limit + 1 ); $this->addOption( 'GROUP BY', 'ct_tag' ); @@ -73,7 +73,7 @@ class ApiQueryTags extends ApiQueryBase { if ( !$ok ) { break; } - $ok = $this->doTag( $row->ct_tag, $row->hitcount ); + $ok = $this->doTag( $row->ct_tag, $this->fld_hitcount ? $row->hitcount : 0 ); } // include tags with no hits yet @@ -169,6 +169,23 @@ 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'; } diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index 8e2f20db..f30b1325 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -4,7 +4,7 @@ * * Created on Oct 16, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -35,10 +35,10 @@ class ApiQueryContributions extends ApiQueryBase { parent::__construct( $query, $moduleName, 'uc' ); } - private $params, $prefixMode, $userprefix, $multiUserMode, $usernames; + 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_patrolled = false, $fld_tags = false, $fld_size = false, $fld_sizediff = false; public function execute() { // Parse some parameters @@ -50,6 +50,7 @@ class ApiQueryContributions extends ApiQueryBase { $this->fld_comment = isset( $prop['comment'] ); $this->fld_parsedcomment = isset ( $prop['parsedcomment'] ); $this->fld_size = isset( $prop['size'] ); + $this->fld_sizediff = isset( $prop['sizediff'] ); $this->fld_flags = isset( $prop['flags'] ); $this->fld_timestamp = isset( $prop['timestamp'] ); $this->fld_patrolled = isset( $prop['patrolled'] ); @@ -82,6 +83,17 @@ class ApiQueryContributions extends ApiQueryBase { // Do the actual query. $res = $this->select( __METHOD__ ); + if( $this->fld_sizediff ) { + $revIds = array(); + foreach ( $res as $row ) { + if( $row->rev_parent_id ) { + $revIds[] = $row->rev_parent_id; + } + } + $this->parentLens = Revision::getParentLengths( $this->getDB(), $revIds ); + $res->rewind(); // reset + } + // Initialise some variables $count = 0; $limit = $this->params['limit']; @@ -152,13 +164,14 @@ class ApiQueryContributions extends ApiQueryBase { $this->dieUsage( 'Invalid continue param. You should pass the original ' . 'value returned by the previous query', '_badcontinue' ); } - $encUser = $this->getDB()->strencode( $continue[0] ); - $encTS = wfTimestamp( TS_MW, $continue[1] ); + $db = $this->getDB(); + $encUser = $db->addQuotes( $continue[0] ); + $encTS = $db->addQuotes( $db->timestamp( $continue[1] ) ); $op = ( $this->params['dir'] == 'older' ? '<' : '>' ); $this->addWhere( - "rev_user_text $op '$encUser' OR " . - "(rev_user_text = '$encUser' AND " . - "rev_timestamp $op= '$encTS')" + "rev_user_text $op $encUser OR " . + "(rev_user_text = $encUser AND " . + "rev_timestamp $op= $encTS)" ); } @@ -185,7 +198,7 @@ class ApiQueryContributions extends ApiQueryBase { 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'] ) ) ) { $this->dieUsageMsg( 'show' ); } @@ -243,8 +256,9 @@ class ApiQueryContributions extends ApiQueryBase { $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 ); - $this->addFieldsIf( 'rev_len', $this->fld_size ); - $this->addFieldsIf( array( 'rev_minor_edit', 'rev_parent_id' ), $this->fld_flags ); + $this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff ); + $this->addFieldsIf( 'rev_minor_edit', $this->fld_flags ); + $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff ); $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled ); if ( $this->fld_tags ) { @@ -332,6 +346,11 @@ 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; + $vals['sizediff'] = intval( $row->rev_len - $parentLen ); + } + if ( $this->fld_tags ) { if ( $row->ts_tags ) { $tags = explode( ',', $row->ts_tags ); @@ -397,6 +416,7 @@ class ApiQueryContributions extends ApiQueryBase { 'comment', 'parsedcomment', 'size', + 'sizediff', 'flags', 'patrolled', 'tags' @@ -435,7 +455,8 @@ class ApiQueryContributions extends ApiQueryBase { ' timestamp - Adds the timestamp of the edit', ' comment - Adds the comment of the edit', ' parsedcomment - Adds the parsed comment of the edit', - ' size - Adds the size of the page', + ' size - Adds the new size of the edit', + ' sizediff - Adds the size delta of the edit against its parent', ' flags - Adds flags of the edit', ' patrolled - Tags patrolled edits', ' tags - Lists tags for the edit', @@ -447,6 +468,61 @@ class ApiQueryContributions extends ApiQueryBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'userid' => 'integer', + 'user' => 'string', + 'userhidden' => 'boolean' + ), + 'ids' => array( + 'pageid' => 'integer', + 'revid' => 'integer' + ), + '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'; } diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index a0ee227f..66906659 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -4,7 +4,7 @@ * * Created on July 30, 2007 * - * Copyright © 2007 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * Copyright © 2007 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 @@ -50,7 +50,7 @@ class ApiQueryUserInfo extends ApiQueryBase { } protected function getCurrentUserInfo() { - global $wgRequest, $wgHiddenPrefs; + global $wgHiddenPrefs; $user = $this->getUser(); $result = $this->getResult(); $vals = array(); @@ -63,7 +63,10 @@ class ApiQueryUserInfo extends ApiQueryBase { if ( isset( $this->prop['blockinfo'] ) ) { if ( $user->isBlocked() ) { - $vals['blockedby'] = User::whoIs( $user->blockedBy() ); + $block = $user->getBlock(); + $vals['blockid'] = $block->getId(); + $vals['blockedby'] = $block->getByName(); + $vals['blockedbyid'] = $block->getBy(); $vals['blockreason'] = $user->blockedFor(); } } @@ -73,14 +76,12 @@ class ApiQueryUserInfo extends ApiQueryBase { } if ( isset( $this->prop['groups'] ) ) { - $autolist = ApiQueryUsers::getAutoGroups( $user ); - - $vals['groups'] = array_merge( $autolist, $user->getGroups() ); + $vals['groups'] = $user->getEffectiveGroups(); $result->setIndexedTagName( $vals['groups'], 'g' ); // even if empty } if ( isset( $this->prop['implicitgroups'] ) ) { - $vals['implicitgroups'] = ApiQueryUsers::getAutoGroups( $user ); + $vals['implicitgroups'] = $user->getAutomaticGroups(); $result->setIndexedTagName( $vals['implicitgroups'], 'g' ); // even if empty } @@ -136,7 +137,7 @@ class ApiQueryUserInfo extends ApiQueryBase { } if ( isset( $this->prop['acceptlang'] ) ) { - $langs = $wgRequest->getAcceptLang(); + $langs = $this->getRequest()->getAcceptLang(); $acceptLang = array(); foreach ( $langs as $lang => $val ) { $r = array( 'q' => $val ); @@ -231,6 +232,63 @@ class ApiQueryUserInfo extends ApiQueryBase { ); } + 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 + ) + ) + ); + } + public function getDescription() { return 'Get information about the current user'; } diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index 31624bdf..bf438d1d 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -4,7 +4,7 @@ * * Created on July 30, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<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 @@ -61,10 +61,10 @@ class ApiQueryUsers extends ApiQueryBase { return $this->tokenFunctions; } - /** - * @param $user User - * @return String - */ + /** + * @param $user User + * @return String + */ public static function getUserrightsToken( $user ) { global $wgUser; // Since the permissions check for userrights is non-trivial, @@ -107,7 +107,7 @@ class ApiQueryUsers extends ApiQueryBase { if ( count( $goodNames ) ) { $this->addTables( 'user' ); - $this->addFields( '*' ); + $this->addFields( User::selectFields() ); $this->addWhereFld( 'user_name', $goodNames ); if ( isset( $this->prop['groups'] ) || isset( $this->prop['rights'] ) ) { @@ -138,7 +138,7 @@ class ApiQueryUsers extends ApiQueryBase { if ( isset( $this->prop['groups'] ) ) { if ( !isset( $data[$name]['groups'] ) ) { - $data[$name]['groups'] = self::getAutoGroups( $user ); + $data[$name]['groups'] = $user->getAutomaticGroups(); } if ( !is_null( $row->ug_group ) ) { @@ -148,7 +148,7 @@ class ApiQueryUsers extends ApiQueryBase { } if ( isset( $this->prop['implicitgroups'] ) && !isset( $data[$name]['implicitgroups'] ) ) { - $data[$name]['implicitgroups'] = self::getAutoGroups( $user ); + $data[$name]['implicitgroups'] = $user->getAutomaticGroups(); } if ( isset( $this->prop['rights'] ) ) { @@ -165,7 +165,9 @@ class ApiQueryUsers extends ApiQueryBase { $data[$name]['hidden'] = ''; } if ( isset( $this->prop['blockinfo'] ) && !is_null( $row->ipb_by_text ) ) { + $data[$name]['blockid'] = $row->ipb_id; $data[$name]['blockedby'] = $row->ipb_by_text; + $data[$name]['blockedbyid'] = $row->ipb_by; $data[$name]['blockreason'] = $row->ipb_reason; $data[$name]['blockexpiry'] = $row->ipb_expiry; } @@ -247,18 +249,15 @@ 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 * @return array */ public static function getAutoGroups( $user ) { - $groups = array(); - $groups[] = '*'; + wfDeprecated( __METHOD__, '1.20' ); - if ( !$user->isAnon() ) { - $groups[] = 'user'; - } - - return array_merge( $groups, Autopromote::getAutopromoteGroups( $user ) ); + return $user->getAutomaticGroups(); } public function getCacheMode( $params ) { @@ -313,6 +312,73 @@ 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'; } diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index ea56fcd9..a1a33728 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -4,7 +4,7 @@ * * Created on Sep 25, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -96,7 +96,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'rc_last_oldid', ) ); - $this->addFieldsIf( array( 'rc_new', 'rc_minor', 'rc_bot' ), $this->fld_flags ); + $this->addFieldsIf( array( 'rc_type', 'rc_minor', 'rc_bot' ), $this->fld_flags ); $this->addFieldsIf( 'rc_user', $this->fld_user || $this->fld_userid ); $this->addFieldsIf( 'rc_user_text', $this->fld_user ); $this->addFieldsIf( 'rc_comment', $this->fld_comment || $this->fld_parsedcomment ); @@ -254,7 +254,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { } if ( $this->fld_flags ) { - if ( $row->rc_new ) { + if ( $row->rc_type == RC_NEW ) { $vals['new'] = ''; } if ( $row->rc_minor ) { @@ -296,13 +296,14 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $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, - $row->rc_params, - $row->rc_log_type, - $row->rc_log_action, - $row->rc_timestamp + $logEntry->getParameters(), + $logEntry->getType(), + $logEntry->getSubtype(), + $logEntry->getTimestamp() ); } @@ -417,6 +418,76 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { ); } + public function getResultProperties() { + global $wgLogTypes; + return array( + '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 + ) + ) + ); + } + public function getDescription() { return "Get all recent changes to pages in the logged in user's watchlist"; } diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php index 506944f0..6b24aef3 100644 --- a/includes/api/ApiQueryWatchlistRaw.php +++ b/includes/api/ApiQueryWatchlistRaw.php @@ -4,7 +4,7 @@ * * Created on Oct 4, 2008 * - * Copyright © 2008 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2008 Roan Kattouw "<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 @@ -76,19 +76,24 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { "original value returned by the previous query", "_badcontinue" ); } $ns = intval( $cont[0] ); - $title = $this->getDB()->strencode( $this->titleToKey( $cont[1] ) ); + $title = $this->getDB()->addQuotes( $cont[1] ); + $op = $params['dir'] == 'ascending' ? '>' : '<'; $this->addWhere( - "wl_namespace > '$ns' OR " . - "(wl_namespace = '$ns' AND " . - "wl_title >= '$title')" + "wl_namespace $op $ns OR " . + "(wl_namespace = $ns AND " . + "wl_title $op= $title)" ); } + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); // Don't ORDER BY wl_namespace if it's constant in the WHERE clause if ( count( $params['namespace'] ) == 1 ) { - $this->addOption( 'ORDER BY', 'wl_title' ); + $this->addOption( 'ORDER BY', 'wl_title' . $sort ); } else { - $this->addOption( 'ORDER BY', 'wl_namespace, wl_title' ); + $this->addOption( 'ORDER BY', array( + 'wl_namespace' . $sort, + 'wl_title' . $sort + )); } $this->addOption( 'LIMIT', $params['limit'] + 1 ); $res = $this->select( __METHOD__ ); @@ -98,8 +103,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { 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->wl_namespace . '|' . - $this->keyToTitle( $row->wl_title ) ); + $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' . $row->wl_title ); break; } $t = Title::makeTitle( $row->wl_namespace, $row->wl_title ); @@ -113,8 +117,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { } $fit = $this->getResult()->addValue( $this->getModuleName(), null, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' . - $this->keyToTitle( $row->wl_title ) ); + $this->setContinueEnumParameter( 'continue', $row->wl_namespace . '|' . $row->wl_title ); break; } } else { @@ -160,7 +163,14 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { ), 'token' => array( ApiBase::PARAM_TYPE => 'string' - ) + ), + 'dir' => array( + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => array( + 'ascending', + 'descending' + ), + ), ); } @@ -176,6 +186,22 @@ 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', + '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 + ) + ) ); } diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index 798b2275..91e20812 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -4,7 +4,7 @@ * * Created on Sep 4, 2006 * - * Copyright © 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com + * 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 @@ -165,7 +165,7 @@ class ApiResult extends ApiBase { * @param $value Mixed * @param $subElemName string when present, content element is created * as a sub item of $arr. Use this parameter to create elements in - * format <elem>text</elem> without attributes + * format "<elem>text</elem>" without attributes. */ public static function setContent( &$arr, $value, $subElemName = null ) { if ( is_array( $value ) ) { diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index 436c392b..677df16a 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -4,7 +4,7 @@ * * Created on Jun 20, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<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 @@ -49,7 +49,7 @@ class ApiRollback extends ApiBase { // User and title already validated in call to getTokenSalt from Main $titleObj = $this->getRbTitle(); $pageObj = WikiPage::factory( $titleObj ); - $summary = ( isset( $params['summary'] ) ? $params['summary'] : '' ); + $summary = $params['summary']; $details = array(); $retval = $pageObj->doRollback( $this->getRbUser(), $summary, $params['token'], $params['markbot'], $details, $this->getUser() ); @@ -90,8 +90,11 @@ class ApiRollback extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => null, - 'summary' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), + 'summary' => '', 'markbot' => false, 'watchlist' => array( ApiBase::PARAM_DFLT => 'preferences', @@ -110,12 +113,25 @@ class ApiRollback extends ApiBase { '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", - 'summary' => 'Custom edit summary. If not set, default summary will be used', + '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' + ) + ); + } + 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,', diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php new file mode 100644 index 00000000..098b1a66 --- /dev/null +++ b/includes/api/ApiSetNotificationTimestamp.php @@ -0,0 +1,285 @@ +<?php + +/** + * API for MediaWiki 1.14+ + * + * Created on Jun 18, 2012 + * + * Copyright © 2012 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 + */ + +/** + * API interface for setting the wl_notificationtimestamp field + * @ingroup API + */ +class ApiSetNotificationTimestamp extends ApiBase { + + public function __construct( $main, $action ) { + parent::__construct( $main, $action ); + } + + public function execute() { + $user = $this->getUser(); + + if ( $user->isAnon() ) { + $this->dieUsage( 'Anonymous users cannot use watchlist change notifications', 'notloggedin' ); + } + + $params = $this->extractRequestParams(); + $this->requireMaxOneParameter( $params, 'timestamp', 'torevid', 'newerthanrevid' ); + + $pageSet = new ApiPageSet( $this ); + $args = array_merge( array( $params, 'entirewatchlist' ), array_keys( $pageSet->getAllowedParams() ) ); + call_user_func_array( array( $this, 'requireOnlyOneParameter' ), $args ); + + $dbw = $this->getDB( DB_MASTER ); + + $timestamp = null; + if ( isset( $params['timestamp'] ) ) { + $timestamp = $dbw->timestamp( $params['timestamp'] ); + } + + if ( !$params['entirewatchlist'] ) { + $pageSet->execute(); + } + + if ( isset( $params['torevid'] ) ) { + if ( $params['entirewatchlist'] || $pageSet->getGoodTitleCount() > 1 ) { + $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; + } + } 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; + } + } + + $apiResult = $this->getResult(); + $result = array(); + if ( $params['entirewatchlist'] ) { + // Entire watchlist mode: Just update the thing and return a success indicator + $dbw->update( 'watchlist', array( 'wl_notificationtimestamp' => $timestamp ), + array( 'wl_user' => $user->getID() ), + __METHOD__ + ); + + $result['notificationtimestamp'] = ( is_null( $timestamp ) ? '' : wfTimestamp( TS_ISO_8601, $timestamp ) ); + } else { + // First, log the invalid titles + foreach( $pageSet->getInvalidTitles() as $title ) { + $r = array(); + $r['title'] = $title; + $r['invalid'] = ''; + $result[] = $r; + } + foreach( $pageSet->getMissingPageIDs() as $p ) { + $page = array(); + $page['pageid'] = $p; + $page['missing'] = ''; + $page['notwatched'] = ''; + $result[] = $page; + } + foreach( $pageSet->getMissingRevisionIDs() as $r ) { + $rev = array(); + $rev['revid'] = $r; + $rev['missing'] = ''; + $rev['notwatched'] = ''; + $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; + } + + // Now, put the valid titles into the result + foreach ( $pageSet->getTitles() as $title ) { + $ns = $title->getNamespace(); + $dbkey = $title->getDBkey(); + $r = array( + 'ns' => intval( $ns ), + 'title' => $title->getPrefixedText(), + ); + if ( !$title->exists() ) { + $r['missing'] = ''; + } + 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; + } + + $apiResult->setIndexedTagName( $result, 'page' ); + } + $apiResult->addValue( null, $this->getModuleName(), $result ); + } + + public function mustBePosted() { + return true; + } + + public function isWriteMode() { + return true; + } + + public function needsToken() { + return true; + } + + public function getTokenSalt() { + return ''; + } + + public function getAllowedParams() { + $psModule = new ApiPageSet( $this ); + return $psModule->getAllowedParams() + array( + 'entirewatchlist' => array( + ApiBase::PARAM_TYPE => 'boolean' + ), + 'token' => null, + 'timestamp' => array( + ApiBase::PARAM_TYPE => 'timestamp' + ), + 'torevid' => array( + ApiBase::PARAM_TYPE => 'integer' + ), + 'newerthanrevid' => array( + ApiBase::PARAM_TYPE => 'integer' + ), + ); + } + + public function getParamDescription() { + $psModule = new ApiPageSet( $this ); + return $psModule->getParamDescription() + array( + 'entirewatchlist' => 'Work on all watched pages', + '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 + ) + ) + ); + } + + public function getDescription() { + return array( 'Update the notification timestamp for watched pages.', + 'This affects the highlighting of changed pages in the watchlist and history,', + 'and the sending of email when the "E-mail me when a page on my watchlist is', + 'changed" preference is enabled.' + ); + } + + public function getPossibleErrors() { + $psModule = new ApiPageSet( $this ); + return array_merge( + parent::getPossibleErrors(), + $psModule->getPossibleErrors(), + $this->getRequireMaxOneParameterErrorMessages( array( 'timestamp', 'torevid', 'newerthanrevid' ) ), + $this->getRequireOnlyOneParameterErrorMessages( array_merge( array( 'entirewatchlist' ), array_keys( $psModule->getAllowedParams() ) ) ), + 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=ABC123' => 'Reset the notification status for the entire watchlist', + 'api.php?action=setnotificationtimestamp&titles=Main_page&token=ABC123' => 'Reset the notification status for "Main page"', + 'api.php?action=setnotificationtimestamp&titles=Main_page×tamp=2012-01-01T00:00:00Z&token=ABC123' => 'Set the notification timestamp for "Main page" so all edits since 1 January 2012 are unviewed', + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:SetNotificationTimestamp'; + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} diff --git a/includes/api/ApiTokens.php b/includes/api/ApiTokens.php new file mode 100644 index 00000000..2c9b482c --- /dev/null +++ b/includes/api/ApiTokens.php @@ -0,0 +1,158 @@ +<?php +/** + * + * + * Created on Jul 29, 2011 + * + * Copyright © 2011 John Du Hart john@johnduhart.me + * + * 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 + */ + + +/** + * @ingroup API + */ +class ApiTokens extends ApiBase { + + public function __construct( $main, $action ) { + parent::__construct( $main, $action ); + } + + public function execute() { + wfProfileIn( __METHOD__ ); + $params = $this->extractRequestParams(); + $res = array(); + + $types = $this->getTokenTypes(); + foreach ( $params['type'] as $type ) { + $type = strtolower( $type ); + + $val = call_user_func( $types[$type], null, null ); + + if ( $val === false ) { + $this->setWarning( "Action '$type' is not allowed for the current user" ); + } else { + $res[$type . 'token'] = $val; + } + } + + $this->getResult()->addValue( null, $this->getModuleName(), $res ); + wfProfileOut( __METHOD__ ); + } + + private function getTokenTypes() { + static $types = null; + if ( $types ) { + return $types; + } + wfProfileIn( __METHOD__ ); + $types = array( 'patrol' => 'ApiQueryRecentChanges::getPatrolToken' ); + $names = array( 'edit', 'delete', 'protect', 'move', 'block', 'unblock', + 'email', 'import', 'watch', 'options' ); + foreach ( $names as $name ) { + $types[$name] = 'ApiQueryInfo::get' . ucfirst( $name ) . 'Token'; + } + wfRunHooks( 'ApiTokensGetTokenTypes', array( &$types ) ); + ksort( $types ); + wfProfileOut( __METHOD__ ); + return $types; + } + + public function getAllowedParams() { + return array( + 'type' => array( + ApiBase::PARAM_DFLT => 'edit', + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => array_keys( $this->getTokenTypes() ), + ), + ); + } + + public function getResultProperties() { + return array( + '' => array( + 'patroltoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'edittoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'deletetoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'protecttoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'movetoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'blocktoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'unblocktoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'emailtoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'importtoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'watchtoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'optionstoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ) + ); + } + + 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=tokens' => 'Retrieve an edit token (the default)', + 'api.php?action=tokens&type=email|move' => 'Retrieve an email token and a move token' + ); + } + + public function getVersion() { + return __CLASS__ . ': $Id$'; + } +} diff --git a/includes/api/ApiUnblock.php b/includes/api/ApiUnblock.php index db94fd5b..e34771fc 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -4,7 +4,7 @@ * * Created on Sep 7, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<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 @@ -44,7 +44,7 @@ class ApiUnblock extends ApiBase { $params = $this->extractRequestParams(); if ( $params['gettoken'] ) { - $res['unblocktoken'] = $user->getEditToken( '', $this->getMain()->getRequest() ); + $res['unblocktoken'] = $user->getEditToken(); $this->getResult()->addValue( null, $this->getModuleName(), $res ); return; } @@ -69,7 +69,7 @@ class ApiUnblock extends ApiBase { $data = array( 'Target' => is_null( $params['id'] ) ? $params['user'] : "#{$params['id']}", - 'Reason' => is_null( $params['reason'] ) ? '' : $params['reason'] + 'Reason' => $params['reason'] ); $block = Block::newFromTarget( $data['Target'] ); $retval = SpecialUnblock::processUnblock( $data, $this->getContext() ); @@ -78,7 +78,9 @@ class ApiUnblock extends ApiBase { } $res['id'] = $block->getId(); - $res['user'] = $block->getType() == Block::TYPE_AUTO ? '' : $block->getTarget(); + $target = $block->getType() == Block::TYPE_AUTO ? '' : $block->getTarget(); + $res['user'] = $target; + $res['userid'] = $target instanceof User ? $target->getId() : 0; $res['reason'] = $params['reason']; $this->getResult()->addValue( null, $this->getModuleName(), $res ); } @@ -98,8 +100,11 @@ class ApiUnblock extends ApiBase { ), 'user' => null, 'token' => null, - 'gettoken' => false, - 'reason' => null, + 'gettoken' => array( + ApiBase::PARAM_DFLT => false, + ApiBase::PARAM_DEPRECATED => true, + ), + 'reason' => '', ); } @@ -108,9 +113,36 @@ class ApiUnblock extends ApiBase { 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 the gettoken parameter or {$p}prop=info", + 'token' => "An unblock token previously obtained through prop=info", 'gettoken' => 'If set, an unblock token will be returned, and no other action will be taken', - 'reason' => 'Reason for unblock (optional)', + 'reason' => 'Reason for unblock', + ); + } + + public function getResultProperties() { + return array( + '' => array( + 'unblocktoken' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + '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 + ) + ) ); } diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php index d3429972..c9962517 100644 --- a/includes/api/ApiUndelete.php +++ b/includes/api/ApiUndelete.php @@ -4,7 +4,7 @@ * * Created on Jul 3, 2007 * - * Copyright © 2007 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2007 Roan Kattouw "<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 @@ -76,7 +76,7 @@ class ApiUndelete extends ApiBase { $info['title'] = $titleObj->getPrefixedText(); $info['revisions'] = intval( $retval[0] ); $info['fileversions'] = intval( $retval[1] ); - $info['reason'] = intval( $retval[2] ); + $info['reason'] = $retval[2]; $this->getResult()->addValue( null, $this->getModuleName(), $info ); } @@ -94,7 +94,10 @@ class ApiUndelete extends ApiBase { ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'reason' => '', 'timestamps' => array( ApiBase::PARAM_TYPE => 'timestamp', @@ -116,12 +119,23 @@ class ApiUndelete extends ApiBase { return array( 'title' => 'Title of the page you want to restore', 'token' => 'An undelete token previously retrieved through list=deletedrevs', - 'reason' => 'Reason for restoring (optional)', + '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' + ) + ); + } + public function getDescription() { return array( 'Restore certain revisions of a deleted page. A list of deleted revisions (including timestamps) can be', diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index fdc1eff0..3a9b5c56 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -113,27 +113,30 @@ class ApiUpload extends ApiBase { } /** * Get an uplaod result based on upload context + * @return array */ private function getContextResult(){ $warnings = $this->getApiWarnings(); - if ( $warnings ) { + if ( $warnings && !$this->mParams['ignorewarnings'] ) { // Get warnings formated in result array format return $this->getWarningsResult( $warnings ); } elseif ( $this->mParams['chunk'] ) { // Add chunk, and get result - return $this->getChunkResult(); + return $this->getChunkResult( $warnings ); } elseif ( $this->mParams['stash'] ) { // Stash the file and get stash result - return $this->getStashResult(); + 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(); + return $this->performUpload( $warnings ); } /** - * Get Stash Result, throws an expetion if the file could not be stashed. + * Get Stash Result, throws an expetion if the file could not be stashed. + * @param $warnings array Array of Api upload warnings + * @return array */ - private function getStashResult(){ + private function getStashResult( $warnings ){ $result = array (); // Some uploads can request they be stashed, so as not to publish them immediately. // In this case, a failure to stash ought to be fatal @@ -141,6 +144,9 @@ class ApiUpload extends ApiBase { $result['result'] = 'Success'; $result['filekey'] = $this->performStash(); $result['sessionkey'] = $result['filekey']; // backwards compatibility + if ( $warnings && count( $warnings ) > 0 ) { + $result['warnings'] = $warnings; + } } catch ( MWException $e ) { $this->dieUsage( $e->getMessage(), 'stashfailed' ); } @@ -148,7 +154,8 @@ class ApiUpload extends ApiBase { } /** * Get Warnings Result - * @param $warnings Array of Api upload warnings + * @param $warnings array Array of Api upload warnings + * @return array */ private function getWarningsResult( $warnings ){ $result = array(); @@ -165,12 +172,17 @@ class ApiUpload extends ApiBase { return $result; } /** - * Get the result of a chunk upload. + * Get the result of a chunk upload. + * @param $warnings array Array of Api upload warnings + * @return array */ - private function getChunkResult(){ + private function getChunkResult( $warnings ){ $result = array(); - + $result['result'] = 'Continue'; + if ( $warnings && count( $warnings ) > 0 ) { + $result['warnings'] = $warnings; + } $request = $this->getMain()->getRequest(); $chunkPath = $request->getFileTempname( 'chunk' ); $chunkSize = $request->getUpload( 'chunk' )->getSize(); @@ -181,17 +193,30 @@ class ApiUpload extends ApiBase { $this->mParams['offset']); if ( !$status->isGood() ) { $this->dieUsage( $status->getWikiText(), 'stashfailed' ); - return ; + return array(); } - $result['filekey'] = $this->mParams['filekey']; + // Check we added the last chunk: if( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) { $status = $this->mUpload->concatenateChunks(); + if ( !$status->isGood() ) { $this->dieUsage( $status->getWikiText(), 'stashfailed' ); - return ; + return array(); } + + // We have a new filekey for the fully concatenated file. + $result['filekey'] = $this->mUpload->getLocalFile()->getFileKey(); + + // Remove chunk from stash. (Checks against user ownership of chunks.) + $this->mUpload->stash->removeFile( $this->mParams['filekey'] ); + $result['result'] = 'Success'; + + } else { + + // Continue passing through the filekey for adding further chunks. + $result['filekey'] = $this->mParams['filekey']; } } $result['offset'] = $this->mParams['offset'] + $chunkSize; @@ -318,6 +343,10 @@ class ApiUpload extends ApiBase { $this->dieUsageMsg( 'copyuploaddisabled' ); } + if ( !UploadFromUrl::isAllowedHost( $this->mParams['url'] ) ) { + $this->dieUsageMsg( 'copyuploadbaddomain' ); + } + $async = false; if ( $this->mParams['asyncdownload'] ) { $this->checkAsyncDownloadEnabled(); @@ -399,11 +428,21 @@ class ApiUpload extends ApiBase { break; case UploadBase::FILETYPE_BADTYPE: - $this->dieUsage( 'This type of file is banned', 'filetype-banned', - 0, array( - 'filetype' => $verification['finalExt'], - 'allowed' => $wgFileExtensions - ) ); + $extradata = array( + 'filetype' => $verification['finalExt'], + 'allowed' => $wgFileExtensions + ); + $this->getResult()->setIndexedTagName( $extradata['allowed'], 'ext' ); + + $msg = "Filetype not permitted: "; + if ( isset( $verification['blacklistedExt'] ) ) { + $msg .= join( ', ', $verification['blacklistedExt'] ); + $extradata['blacklisted'] = array_values( $verification['blacklistedExt'] ); + $this->getResult()->setIndexedTagName( $extradata['blacklisted'], 'ext' ); + } else { + $msg .= $verification['finalExt']; + } + $this->dieUsage( $msg, 'filetype-banned', 0, $extradata ); break; case UploadBase::VERIFICATION_ERROR: $this->getResult()->setIndexedTagName( $verification['details'], 'detail' ); @@ -423,18 +462,15 @@ class ApiUpload extends ApiBase { /** - * Check warnings if ignorewarnings is not set. + * Check warnings. * Returns a suitable array for inclusion into API results if there were warnings * Returns the empty array if there were no warnings * * @return array */ protected function getApiWarnings() { - $warnings = array(); + $warnings = $this->mUpload->checkWarnings(); - if ( !$this->mParams['ignorewarnings'] ) { - $warnings = $this->mUpload->checkWarnings(); - } return $this->transformWarnings( $warnings ); } @@ -467,9 +503,10 @@ class ApiUpload extends ApiBase { * Perform the actual upload. Returns a suitable result array on success; * dies on failure. * + * @param $warnings array Array of Api upload warnings * @return array */ - protected function performUpload() { + protected function performUpload( $warnings ) { // Use comment as initial page text by default if ( is_null( $this->mParams['text'] ) ) { $this->mParams['text'] = $this->mParams['comment']; @@ -508,6 +545,9 @@ class ApiUpload extends ApiBase { $result['result'] = 'Success'; $result['filename'] = $file->getName(); + if ( $warnings && count( $warnings ) > 0 ) { + $result['warnings'] = $warnings; + } return $result; } @@ -539,7 +579,10 @@ class ApiUpload extends ApiBase { ApiBase::PARAM_DFLT => '' ), 'text' => null, - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'watch' => array( ApiBase::PARAM_DFLT => false, ApiBase::PARAM_DEPRECATED => true, @@ -602,6 +645,41 @@ class ApiUpload extends ApiBase { } + 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() { return array( 'Upload a file, or get the status of pending uploads. Several methods are available:', @@ -631,6 +709,8 @@ class ApiUpload extends ApiBase { array( 'code' => 'stashfailed', 'info' => 'Stashing temporary file failed' ), array( 'code' => 'internal-error', 'info' => 'An internal error occurred' ), array( 'code' => 'asynccopyuploaddisabled', 'info' => 'Asynchronous copy uploads disabled' ), + array( 'fileexists-forbidden' ), + array( 'fileexists-shared-forbidden' ), ) ); } diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php index 191dd3ec..cbb66a41 100644 --- a/includes/api/ApiUserrights.php +++ b/includes/api/ApiUserrights.php @@ -5,7 +5,7 @@ * * Created on Mar 24, 2009 * - * Copyright © 2009 Roan Kattouw <Firstname>.<Lastname>@gmail.com + * Copyright © 2009 Roan Kattouw "<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 @@ -43,6 +43,7 @@ class ApiUserrights extends ApiBase { $form = new UserrightsPage; $r['user'] = $user->getName(); + $r['userid'] = $user->getId(); list( $r['added'], $r['removed'] ) = $form->doSaveUserGroups( $user, (array)$params['add'], @@ -99,7 +100,10 @@ class ApiUserrights extends ApiBase { ApiBase::PARAM_TYPE => User::getAllGroups(), ApiBase::PARAM_ISMULTI => true ), - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), 'reason' => array( ApiBase::PARAM_DFLT => '' ) diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php index fa382b3b..0509f1f8 100644 --- a/includes/api/ApiWatch.php +++ b/includes/api/ApiWatch.php @@ -4,7 +4,7 @@ * * Created on Jan 4, 2008 * - * Copyright © 2008 Yuri Astrakhan <Firstname><Lastname>@gmail.com, + * Copyright © 2008 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 @@ -88,7 +88,10 @@ class ApiWatch extends ApiBase { ApiBase::PARAM_REQUIRED => true ), 'unwatch' => false, - 'token' => null, + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), ); } @@ -100,6 +103,17 @@ class ApiWatch extends ApiBase { ); } + public function getResultProperties() { + return array( + '' => array( + 'title' => 'string', + 'unwatched' => 'boolean', + 'watched' => 'boolean', + 'message' => 'string' + ) + ); + } + public function getDescription() { return 'Add or remove a page from/to the current user\'s watchlist'; } |