diff options
Diffstat (limited to 'includes/api')
95 files changed, 4197 insertions, 2197 deletions
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 875a3814..9351a8d8 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -66,14 +66,22 @@ abstract class ApiBase extends ContextSource { const LIMIT_SML1 = 50; // Slow query, std user limit const LIMIT_SML2 = 500; // Slow query, bot/sysop limit + /** + * getAllowedParams() flag: When set, the result could take longer to generate, + * but should be more thorough. E.g. get the list of generators for ApiSandBox extension + * @since 1.21 + */ + const GET_VALUES_FOR_HELP = 1; + private $mMainModule, $mModuleName, $mModulePrefix; + private $mSlaveDB = null; private $mParamCache = array(); /** * Constructor * @param $mainModule ApiMain object - * @param $moduleName string Name of this module - * @param $modulePrefix string Prefix to use for parameter names + * @param string $moduleName Name of this module + * @param string $modulePrefix Prefix to use for parameter names */ public function __construct( $mainModule, $moduleName, $modulePrefix = '' ) { $this->mMainModule = $mainModule; @@ -105,15 +113,19 @@ abstract class ApiBase extends ContextSource { * The result data should be stored in the ApiResult object available * through getResult(). */ - public abstract function execute(); + abstract public function execute(); /** * Returns a string that identifies the version of the extending class. * Typically includes the class name, the svn revision, timestamp, and * last author. Usually done with SVN's Id keyword * @return string + * @deprecated since 1.21, version string is no longer supported */ - public abstract function getVersion(); + public function getVersion() { + wfDeprecated( __METHOD__, '1.21' ); + return ''; + } /** * Get the name of the module being executed by this instance @@ -124,6 +136,15 @@ abstract class ApiBase extends ContextSource { } /** + * Get the module manager, or null if this module has no sub-modules + * @since 1.21 + * @return ApiModuleManager + */ + public function getModuleManager() { + return null; + } + + /** * Get parameter prefix (usually two letters or an empty string). * @return string */ @@ -168,7 +189,7 @@ abstract class ApiBase extends ContextSource { * @return ApiResult */ public function getResult() { - // Main module has getResult() method overriden + // Main module has getResult() method overridden // Safety - avoid infinite loop: if ( $this->isMain() ) { ApiBase::dieDebug( __METHOD__, 'base method was called on main module. ' ); @@ -203,26 +224,32 @@ abstract class ApiBase extends ContextSource { * section to notice any changes in API. Multiple calls to this * function will result in the warning messages being separated by * newlines - * @param $warning string Warning message + * @param string $warning Warning message */ public function setWarning( $warning ) { $result = $this->getResult(); $data = $result->getData(); - if ( isset( $data['warnings'][$this->getModuleName()] ) ) { + $moduleName = $this->getModuleName(); + if ( isset( $data['warnings'][$moduleName] ) ) { // Don't add duplicate warnings - $warn_regex = preg_quote( $warning, '/' ); - if ( preg_match( "/{$warn_regex}(\\n|$)/", $data['warnings'][$this->getModuleName()]['*'] ) ) { - return; + $oldWarning = $data['warnings'][$moduleName]['*']; + $warnPos = strpos( $oldWarning, $warning ); + // If $warning was found in $oldWarning, check if it starts at 0 or after "\n" + if ( $warnPos !== false && ( $warnPos === 0 || $oldWarning[$warnPos - 1] === "\n" ) ) { + // Check if $warning is followed by "\n" or the end of the $oldWarning + $warnPos += strlen( $warning ); + if ( strlen( $oldWarning ) <= $warnPos || $oldWarning[$warnPos] === "\n" ) { + return; + } } - $oldwarning = $data['warnings'][$this->getModuleName()]['*']; // If there is a warning already, append it to the existing one - $warning = "$oldwarning\n$warning"; - $result->unsetValue( 'warnings', $this->getModuleName() ); + $warning = "$oldWarning\n$warning"; } $msg = array(); ApiResult::setContent( $msg, $warning ); $result->disableSizeCheck(); - $result->addValue( 'warnings', $this->getModuleName(), $msg ); + $result->addValue( 'warnings', $moduleName, + $msg, ApiResult::OVERRIDE | ApiResult::ADD_ON_TOP ); $result->enableSizeCheck(); } @@ -254,6 +281,8 @@ abstract class ApiBase extends ContextSource { } $msg = $lnPrfx . implode( $lnPrfx, $msg ) . "\n"; + $msg .= $this->makeHelpArrayToString( $lnPrfx, false, $this->getHelpUrls() ); + if ( $this->isReadMode() ) { $msg .= "\nThis module requires read rights"; } @@ -297,25 +326,6 @@ abstract class ApiBase extends ContextSource { } } } - - $msg .= $this->makeHelpArrayToString( $lnPrfx, "Help page", $this->getHelpUrls() ); - - if ( $this->getMain()->getShowVersions() ) { - $versions = $this->getVersion(); - $pattern = '/(\$.*) ([0-9a-z_]+\.php) (.*\$)/i'; - $callback = array( $this, 'makeHelpMsg_callback' ); - - if ( is_array( $versions ) ) { - foreach ( $versions as &$v ) { - $v = preg_replace_callback( $pattern, $callback, $v ); - } - $versions = implode( "\n ", $versions ); - } else { - $versions = preg_replace_callback( $pattern, $callback, $versions ); - } - - $msg .= "Version:\n $versions\n"; - } } return $msg; @@ -330,8 +340,8 @@ abstract class ApiBase extends ContextSource { } /** - * @param $prefix string Text to split output items - * @param $title string What is being output + * @param string $prefix Text to split output items + * @param string $title What is being output * @param $input string|array * @return string */ @@ -340,13 +350,15 @@ abstract class ApiBase extends ContextSource { return ''; } if ( !is_array( $input ) ) { - $input = array( - $input - ); + $input = array( $input ); } if ( count( $input ) > 0 ) { - $msg = $title . ( count( $input ) > 1 ? 's' : '' ) . ":\n "; + if ( $title ) { + $msg = $title . ( count( $input ) > 1 ? 's' : '' ) . ":\n "; + } else { + $msg = ' '; + } $msg .= implode( $prefix, $input ) . "\n"; return $msg; } @@ -359,7 +371,7 @@ abstract class ApiBase extends ContextSource { * @return string or false */ public function makeHelpMsgParameters() { - $params = $this->getFinalParams(); + $params = $this->getFinalParams( ApiBase::GET_VALUES_FOR_HELP ); if ( $params ) { $paramsDescription = $this->getFinalParamDescription(); @@ -416,7 +428,7 @@ abstract class ApiBase extends ContextSource { if ( $t === '' ) { $nothingPrompt = 'Can be empty, or '; } else { - $choices[] = $t; + $choices[] = $t; } } $desc .= $paramPrefix . $nothingPrompt . $prompt; @@ -455,6 +467,9 @@ abstract class ApiBase extends ContextSource { $desc .= $paramPrefix . $intRangeStr; } break; + case 'upload': + $desc .= $paramPrefix . "Must be posted as a file upload using multipart/form-data"; + break; } } @@ -487,44 +502,6 @@ abstract class ApiBase extends ContextSource { } /** - * Callback for preg_replace_callback() call in makeHelpMsg(). - * Replaces a source file name with a link to ViewVC - * - * @param $matches array - * @return string - */ - public function makeHelpMsg_callback( $matches ) { - global $wgAutoloadClasses, $wgAutoloadLocalClasses; - - $file = ''; - if ( isset( $wgAutoloadLocalClasses[get_class( $this )] ) ) { - $file = $wgAutoloadLocalClasses[get_class( $this )]; - } elseif ( isset( $wgAutoloadClasses[get_class( $this )] ) ) { - $file = $wgAutoloadClasses[get_class( $this )]; - } - - // Do some guesswork here - $path = strstr( $file, 'includes/api/' ); - if ( $path === false ) { - $path = strstr( $file, 'extensions/' ); - } else { - $path = 'phase3/' . $path; - } - - // Get the filename from $matches[2] instead of $file - // If they're not the same file, they're assumed to be in the - // same directory - // This is necessary to make stuff like ApiMain::getVersion() - // returning the version string for ApiBase work - if ( $path ) { - return "{$matches[0]}\n https://svn.wikimedia.org/" . - "viewvc/mediawiki/trunk/" . dirname( $path ) . - "/{$matches[2]}"; - } - return $matches[0]; - } - - /** * Returns the description string for this module * @return mixed string or array of strings */ @@ -545,15 +522,22 @@ 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. + * + * Some derived classes may choose to handle an integer $flags parameter + * in the overriding methods. Callers of this method can pass zero or + * more OR-ed flags like GET_VALUES_FOR_HELP. + * * @return array|bool */ - protected function getAllowedParams() { + protected function getAllowedParams( /* $flags = 0 */ ) { + // int $flags is not declared because it causes "Strict standards" + // warning. Most derived classes do not implement it. return false; } /** * Returns an array of parameter descriptions. - * Don't call this functon directly: use getFinalParamDescription() to + * Don't call this function directly: use getFinalParamDescription() to * allow hooks to modify descriptions as needed. * @return array|bool False on no parameter descriptions */ @@ -565,11 +549,13 @@ abstract class ApiBase extends ContextSource { * Get final list of parameters, after hooks have had a chance to * tweak it as needed. * + * @param int $flags Zero or more flags like GET_VALUES_FOR_HELP * @return array|Bool False on no parameters + * @since 1.21 $flags param added */ - public function getFinalParams() { - $params = $this->getAllowedParams(); - wfRunHooks( 'APIGetAllowedParams', array( &$this, &$params ) ); + public function getFinalParams( $flags = 0 ) { + $params = $this->getAllowedParams( $flags ); + wfRunHooks( 'APIGetAllowedParams', array( &$this, &$params, $flags ) ); return $params; } @@ -596,7 +582,7 @@ abstract class ApiBase extends ContextSource { * 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 + * Don't call this function directly: use getFinalResultProperties() to * allow hooks to modify descriptions as needed. * * @return array|bool False on no properties @@ -645,7 +631,7 @@ abstract class ApiBase extends ContextSource { /** * This method mangles parameter name based on the prefix supplied to the constructor. * Override this method to change parameter name during runtime - * @param $paramName string Parameter name + * @param string $paramName Parameter name * @return string Prefixed parameter name */ public function encodeParamName( $paramName ) { @@ -680,8 +666,8 @@ abstract class ApiBase extends ContextSource { /** * Get a value for the given parameter - * @param $paramName string Parameter name - * @param $parseLimit bool see extractRequestParams() + * @param string $paramName Parameter name + * @param bool $parseLimit see extractRequestParams() * @return mixed Parameter value */ protected function getParameter( $paramName, $parseLimit = true ) { @@ -692,7 +678,7 @@ abstract class ApiBase extends ContextSource { /** * Die if none or more than one of a certain set of parameters is set and not false. - * @param $params array of parameter names + * @param array $params of parameter names */ public function requireOnlyOneParameter( $params ) { $required = func_get_args(); @@ -703,7 +689,7 @@ abstract class ApiBase extends ContextSource { array( $this, "parameterNotEmpty" ) ) ), $required ); if ( count( $intersection ) > 1 ) { - $this->dieUsage( "The parameters {$p}" . implode( ", {$p}", $intersection ) . ' can not be used together', "{$p}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 {$p}" . implode( ", {$p}", $required ) . ' is required', "{$p}missingparam" ); } @@ -760,7 +746,7 @@ abstract class ApiBase extends ContextSource { /** * @param $params array - * @param $load bool|string Whether load the object's state from the database: + * @param bool|string $load Whether load the object's state from the database: * - false: don't load (if the pageid is given, it will still be loaded) * - 'fromdb': load from a slave database * - 'fromdbmaster': load from the master database @@ -772,9 +758,12 @@ abstract class ApiBase extends ContextSource { $pageObj = null; if ( isset( $params['title'] ) ) { $titleObj = Title::newFromText( $params['title'] ); - if ( !$titleObj ) { + if ( !$titleObj || $titleObj->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); } + if ( !$titleObj->canExist() ) { + $this->dieUsage( "Namespace doesn't allow actual pages", 'pagecannotexist' ); + } $pageObj = WikiPage::factory( $titleObj ); if ( $load !== false ) { $pageObj->loadPageData( $load ); @@ -806,7 +795,7 @@ abstract class ApiBase extends ContextSource { } /** - * Callback function used in requireOnlyOneParameter to check whether reequired parameters are set + * Callback function used in requireOnlyOneParameter to check whether required parameters are set * * @param $x object Parameter to check is not null/false * @return bool @@ -827,9 +816,9 @@ abstract class ApiBase extends ContextSource { /** * Return true if we're to watch the page, false if not, null if no change. - * @param $watchlist String Valid values: 'watch', 'unwatch', 'preferences', 'nochange' + * @param string $watchlist Valid values: 'watch', 'unwatch', 'preferences', 'nochange' * @param $titleObj Title the page under consideration - * @param $userOption String The user option to consider when $watchlist=preferences. + * @param string $userOption The user option to consider when $watchlist=preferences. * If not set will magically default to either watchdefault or watchcreations * @return bool */ @@ -849,13 +838,13 @@ abstract class ApiBase extends ContextSource { if ( $userWatching ) { return true; } - # If no user option was passed, use watchdefault or watchcreation + # If no user option was passed, use watchdefault or watchcreations if ( is_null( $userOption ) ) { $userOption = $titleObj->exists() ? 'watchdefault' : 'watchcreations'; } # Watch the article based on the user preference - return (bool)$this->getUser()->getOption( $userOption ); + return $this->getUser()->getBoolOption( $userOption ); case 'nochange': return $userWatching; @@ -867,9 +856,9 @@ abstract class ApiBase extends ContextSource { /** * Set a watch (or unwatch) based the based on a watchlist parameter. - * @param $watch String Valid values: 'watch', 'unwatch', 'preferences', 'nochange' + * @param string $watch Valid values: 'watch', 'unwatch', 'preferences', 'nochange' * @param $titleObj Title the article's title to change - * @param $userOption String The user option to consider when $watch=preferences + * @param string $userOption The user option to consider when $watch=preferences */ protected function setWatch( $watch, $titleObj, $userOption = null ) { $value = $this->getWatchlistValue( $watch, $titleObj, $userOption ); @@ -888,8 +877,8 @@ abstract class ApiBase extends ContextSource { /** * Using the settings determine the value for the given parameter * - * @param $paramName String: parameter name - * @param $paramSettings array|mixed default value or an array of settings + * @param string $paramName parameter name + * @param array|mixed $paramSettings default value or an array of settings * using PARAM_* constants. * @param $parseLimit Boolean: parse limit? * @return mixed Parameter value @@ -929,9 +918,32 @@ abstract class ApiBase extends ContextSource { ApiBase::dieDebug( __METHOD__, "Boolean param $encParamName's default is set to '$default'. Boolean parameters must default to false." ); } - $value = $this->getRequest()->getCheck( $encParamName ); + $value = $this->getMain()->getCheck( $encParamName ); + } elseif ( $type == 'upload' ) { + if ( isset( $default ) ) { + // Having a default value is not allowed + ApiBase::dieDebug( __METHOD__, "File upload param $encParamName's default is set to '$default'. File upload parameters may not have a default." ); + } + if ( $multi ) { + ApiBase::dieDebug( __METHOD__, "Multi-values not supported for $encParamName" ); + } + $value = $this->getMain()->getUpload( $encParamName ); + if ( !$value->exists() ) { + // This will get the value without trying to normalize it + // (because trying to normalize a large binary file + // accidentally uploaded as a field fails spectacularly) + $value = $this->getMain()->getRequest()->unsetVal( $encParamName ); + if ( $value !== null ) { + $this->dieUsage( + "File upload param $encParamName is not a file upload; " . + "be sure to use multipart/form-data for your POST and include " . + "a filename in the Content-Disposition header.", + "badupload_{$encParamName}" + ); + } + } } else { - $value = $this->getRequest()->getVal( $encParamName, $default ); + $value = $this->getMain()->getVal( $encParamName, $default ); if ( isset( $value ) && $type == 'namespace' ) { $type = MWNamespace::getValidNamespaces(); @@ -953,7 +965,6 @@ abstract class ApiBase extends ContextSource { if ( $required && $value === '' ) { $this->dieUsageMsg( array( 'missingparam', $paramName ) ); } - break; case 'integer': // Force everything using intval() and optionally validate limits $min = isset ( $paramSettings[self::PARAM_MIN] ) ? $paramSettings[self::PARAM_MIN] : null; @@ -1010,29 +1021,23 @@ abstract class ApiBase extends ContextSource { } break; case 'user': - if ( !is_array( $value ) ) { - $value = array( $value ); - } - - foreach ( $value as $key => $val ) { - $title = Title::makeTitleSafe( NS_USER, $val ); - if ( is_null( $title ) ) { - $this->dieUsage( "Invalid value for user parameter $encParamName", "baduser_{$encParamName}" ); + if ( is_array( $value ) ) { + foreach ( $value as $key => $val ) { + $value[$key] = $this->validateUser( $val, $encParamName ); } - $value[$key] = $title->getText(); - } - - if ( !$multi ) { - $value = $value[0]; + } else { + $value = $this->validateUser( $value, $encParamName ); } break; + case 'upload': // nothing to do + break; default: ApiBase::dieDebug( __METHOD__, "Param $encParamName's type is unknown - $type" ); } } // Throw out duplicates if requested - if ( is_array( $value ) && !$dupes ) { + if ( !$dupes && is_array( $value ) ) { $value = array_unique( $value ); } @@ -1051,10 +1056,10 @@ abstract class ApiBase extends ContextSource { * Return an array of values that were given in a 'a|b|c' notation, * after it optionally validates them against the list allowed values. * - * @param $valueName string The name of the parameter (for error + * @param string $valueName The name of the parameter (for error * reporting) * @param $value mixed The value being parsed - * @param $allowMultiple bool Can $value contain more than one value + * @param bool $allowMultiple Can $value contain more than one value * separated by '|'? * @param $allowedValues mixed An array of values to check against. If * null, all values are accepted. @@ -1106,11 +1111,11 @@ abstract class ApiBase extends ContextSource { /** * Validate the value against the minimum and user/bot maximum limits. * Prints usage info on failure. - * @param $paramName string Parameter name - * @param $value int Parameter value - * @param $min int|null Minimum value - * @param $max int|null Maximum value for users - * @param $botMax int Maximum value for sysops/bots + * @param string $paramName Parameter name + * @param int $value Parameter value + * @param int|null $min Minimum value + * @param int|null $max Maximum value for users + * @param int $botMax Maximum value for sysops/bots * @param $enforceLimits Boolean Whether to enforce (die) if value is outside limits */ function validateLimit( $paramName, &$value, $min, $max, $botMax = null, $enforceLimits = false ) { @@ -1144,16 +1149,31 @@ abstract class ApiBase extends ContextSource { } /** - * @param $value string - * @param $paramName string - * @return string + * Validate and normalize of parameters of type 'timestamp' + * @param string $value Parameter value + * @param string $encParamName Parameter name + * @return string Validated and normalized parameter + */ + function validateTimestamp( $value, $encParamName ) { + $unixTimestamp = wfTimestamp( TS_UNIX, $value ); + if ( $unixTimestamp === false ) { + $this->dieUsage( "Invalid value '$value' for timestamp parameter $encParamName", "badtimestamp_{$encParamName}" ); + } + return wfTimestamp( TS_MW, $unixTimestamp ); + } + + /** + * Validate and normalize of parameters of type 'user' + * @param string $value Parameter value + * @param string $encParamName Parameter value + * @return string Validated and normalized parameter */ - function validateTimestamp( $value, $paramName ) { - $value = wfTimestamp( TS_UNIX, $value ); - if ( $value === 0 ) { - $this->dieUsage( "Invalid value '$value' for timestamp parameter $paramName", "badtimestamp_{$paramName}" ); + private function validateUser( $value, $encParamName ) { + $title = Title::makeTitleSafe( NS_USER, $value ); + if ( $title === null ) { + $this->dieUsage( "Invalid value '$value' for user parameter $encParamName", "baduser_{$encParamName}" ); } - return wfTimestamp( TS_MW, $value ); + return $title->getText(); } /** @@ -1172,8 +1192,8 @@ abstract class ApiBase extends ContextSource { /** * Truncate an array to a certain length. - * @param $arr array Array to truncate - * @param $limit int Maximum length + * @param array $arr Array to truncate + * @param int $limit Maximum length * @return bool True if the array was truncated, false otherwise */ public static function truncateArray( &$arr, $limit ) { @@ -1189,12 +1209,12 @@ abstract class ApiBase extends ContextSource { * Throw a UsageException, which will (if uncaught) call the main module's * error handler and die with an error message. * - * @param $description string One-line human-readable description of the + * @param string $description One-line human-readable description of the * error condition, e.g., "The API requires a valid action parameter" - * @param $errorCode string Brief, arbitrary, stable string to allow easy + * @param string $errorCode 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 int $httpRespCode HTTP response code + * @param array $extradata Data to add to the "<error>" element; array in ApiResult format * @throws UsageException */ public function dieUsage( $description, $errorCode, $httpRespCode = 0, $extradata = null ) { @@ -1226,7 +1246,7 @@ abstract class ApiBase extends ContextSource { 'nocreatetext' => array( 'code' => 'cantcreate-anon', 'info' => "Anonymous users can't create new pages" ), 'movenologintext' => array( 'code' => 'cantmove-anon', 'info' => "Anonymous users can't move pages" ), 'movenotallowed' => array( 'code' => 'cantmove', 'info' => "You don't have permission to move pages" ), - 'confirmedittext' => array( 'code' => 'confirmemail', 'info' => "You must confirm your e-mail address before you can edit" ), + 'confirmedittext' => array( 'code' => 'confirmemail', 'info' => "You must confirm your email address before you can edit" ), 'blockedtext' => array( 'code' => 'blocked', 'info' => "You have been blocked from editing" ), 'autoblockedtext' => array( 'code' => 'autoblocked', 'info' => "Your IP address has been blocked automatically, because it was used by a blocked user" ), @@ -1254,15 +1274,15 @@ abstract class ApiBase extends ContextSource { 'badipaddress' => array( 'code' => 'invalidip', 'info' => "Invalid IP address specified" ), 'ipb_expiry_invalid' => array( 'code' => 'invalidexpiry', 'info' => "Invalid expiry time" ), 'ipb_already_blocked' => array( 'code' => 'alreadyblocked', 'info' => "The user you tried to block was already blocked" ), - 'ipb_blocked_as_range' => array( 'code' => 'blockedasrange', 'info' => "IP address \"\$1\" was blocked as part of range \"\$2\". You can't unblock the IP invidually, but you can unblock the range as a whole." ), + 'ipb_blocked_as_range' => array( 'code' => 'blockedasrange', 'info' => "IP address \"\$1\" was blocked as part of range \"\$2\". You can't unblock the IP individually, but you can unblock the range as a whole." ), 'ipb_cant_unblock' => array( 'code' => 'cantunblock', 'info' => "The block you specified was not found. It may have been unblocked already" ), - 'mailnologin' => array( 'code' => 'cantsend', 'info' => "You are not logged in, you do not have a confirmed e-mail address, or you are not allowed to send e-mail to other users, so you cannot send e-mail" ), + 'mailnologin' => array( 'code' => 'cantsend', 'info' => "You are not logged in, you do not have a confirmed email address, or you are not allowed to send email to other users, so you cannot send email" ), 'ipbblocked' => array( 'code' => 'ipbblocked', 'info' => 'You cannot block or unblock users while you are yourself blocked' ), 'ipbnounblockself' => array( 'code' => 'ipbnounblockself', 'info' => 'You are not allowed to unblock yourself' ), 'usermaildisabled' => array( 'code' => 'usermaildisabled', 'info' => "User email has been disabled" ), - 'blockedemailuser' => array( 'code' => 'blockedfrommail', 'info' => "You have been blocked from sending e-mail" ), + 'blockedemailuser' => array( 'code' => 'blockedfrommail', 'info' => "You have been blocked from sending email" ), 'notarget' => array( 'code' => 'notarget', 'info' => "You have not specified a valid target for this action" ), - 'noemail' => array( 'code' => 'noemail', 'info' => "The user has not specified a valid e-mail address, or has chosen not to receive e-mail from other users" ), + 'noemail' => array( 'code' => 'noemail', 'info' => "The user has not specified a valid email address, or has chosen not to receive email from other users" ), 'rcpatroldisabled' => array( 'code' => 'patroldisabled', 'info' => "Patrolling is disabled on this wiki" ), 'markedaspatrollederror-noautopatrol' => array( 'code' => 'noautopatrol', 'info' => "You don't have permission to patrol your own changes" ), 'delete-toobig' => array( 'code' => 'bigdelete', 'info' => "You can't delete this page because it has more than \$1 revisions" ), @@ -1291,7 +1311,7 @@ abstract class ApiBase extends ContextSource { 'missingtitle-createonly' => array( 'code' => 'missingtitle-createonly', 'info' => "Missing titles can only be protected with 'create'" ), 'cantblock' => array( 'code' => 'cantblock', 'info' => "You don't have permission to block users" ), 'canthide' => array( 'code' => 'canthide', 'info' => "You don't have permission to hide user names from the block log" ), - 'cantblock-email' => array( 'code' => 'cantblock-email', 'info' => "You don't have permission to block users from sending e-mail through the wiki" ), + 'cantblock-email' => array( 'code' => 'cantblock-email', 'info' => "You don't have permission to block users from sending email through the wiki" ), 'unblock-notarget' => array( 'code' => 'notarget', 'info' => "Either the id or the user parameter must be set" ), 'unblock-idanduser' => array( 'code' => 'idanduser', 'info' => "The id and user parameters can't be used together" ), 'cantunblock' => array( 'code' => 'permissiondenied', 'info' => "You don't have permission to unblock users" ), @@ -1349,8 +1369,8 @@ abstract class ApiBase extends ContextSource { // uploadMsgs 'invalid-file-key' => array( 'code' => 'invalid-file-key', 'info' => 'Not a valid file key' ), 'nouploadmodule' => array( 'code' => 'nouploadmodule', 'info' => 'No upload module set' ), - 'uploaddisabled' => array( 'code' => 'uploaddisabled', 'info' => 'Uploads are not enabled. Make sure $wgEnableUploads is set to true in LocalSettings.php and the PHP ini setting file_uploads is true' ), - 'copyuploaddisabled' => array( 'code' => 'copyuploaddisabled', 'info' => 'Uploads by URL is not enabled. Make sure $wgAllowCopyUploads is set to true in LocalSettings.php.' ), + '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' ), @@ -1385,8 +1405,41 @@ abstract class ApiBase extends ContextSource { } /** + * Will only set a warning instead of failing if the global $wgDebugAPI + * is set to true. Otherwise behaves exactly as dieUsageMsg(). + * @param $error (array|string) Element of a getUserPermissionsErrors()-style array + * @since 1.21 + */ + public function dieUsageMsgOrDebug( $error ) { + global $wgDebugAPI; + if( $wgDebugAPI !== true ) { + $this->dieUsageMsg( $error ); + } else { + if( is_string( $error ) ) { + $error = array( $error ); + } + $parsed = $this->parseMsg( $error ); + $this->setWarning( '$wgDebugAPI: ' . $parsed['code'] + . ' - ' . $parsed['info'] ); + } + } + + /** + * Die with the $prefix.'badcontinue' error. This call is common enough to make it into the base method. + * @param $condition boolean will only die if this value is true + * @since 1.21 + */ + protected function dieContinueUsageIf( $condition ) { + if ( $condition ) { + $this->dieUsage( + 'Invalid continue param. You should pass the original value returned by the previous query', + 'badcontinue' ); + } + } + + /** * Return the error message related to a certain array - * @param $error array Element of a getUserPermissionsErrors()-style array + * @param array $error Element of a getUserPermissionsErrors()-style array * @return array('code' => code, 'info' => info) */ public function parseMsg( $error ) { @@ -1395,7 +1448,7 @@ abstract class ApiBase extends ContextSource { // Check whether the error array was nested // array( array( <code>, <params> ), array( <another_code>, <params> ) ) - if( is_array( $key ) ){ + if( is_array( $key ) ) { $error = $key; $key = array_shift( $error ); } @@ -1413,8 +1466,8 @@ abstract class ApiBase extends ContextSource { /** * Internal code errors should be reported with this method - * @param $method string Method or function name - * @param $message string Error message + * @param string $method Method or function name + * @param string $message Error message */ protected static function dieDebug( $method, $message ) { wfDebugDieBacktrace( "Internal error in $method: $message" ); @@ -1515,10 +1568,17 @@ abstract class ApiBase extends ContextSource { $params = $this->getFinalParams(); if ( $params ) { foreach ( $params as $paramName => $paramSettings ) { - if ( isset( $paramSettings[ApiBase::PARAM_REQUIRED] ) ) { + if ( isset( $paramSettings[ApiBase::PARAM_REQUIRED] ) && $paramSettings[ApiBase::PARAM_REQUIRED] ) { $ret[] = array( 'missingparam', $paramName ); } } + if ( array_key_exists( 'continue', $params ) ) { + $ret[] = array( + array( + 'code' => 'badcontinue', + 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' + ) ); + } } if ( $this->mustBePosted() ) { @@ -1544,7 +1604,7 @@ abstract class ApiBase extends ContextSource { /** * Parses a list of errors into a standardised format - * @param $errors array List of errors. Items can be in the for array( key, param1, param2, ... ) or array( 'code' => ..., 'info' => ... ) + * @param array $errors List of errors. Items can be in the for array( key, param1, param2, ... ) or array( 'code' => ..., 'info' => ... ) * @return array Parsed list of errors with items in the form array( 'code' => ..., 'info' => ... ) */ public function parseErrors( $errors ) { @@ -1666,17 +1726,23 @@ abstract class ApiBase extends ContextSource { } /** + * Gets a default slave database connection object * @return DatabaseBase */ protected function getDB() { - return wfGetDB( DB_SLAVE, 'api' ); + if ( !isset( $this->mSlaveDB ) ) { + $this->profileDBIn(); + $this->mSlaveDB = wfGetDB( DB_SLAVE, 'api' ); + $this->profileDBOut(); + } + return $this->mSlaveDB; } /** * Debugging function that prints a value and an optional backtrace * @param $value mixed Value to print - * @param $name string Description of the printed value - * @param $backtrace bool If true, print a backtrace + * @param string $name Description of the printed value + * @param bool $backtrace If true, print a backtrace */ public static function debugPrint( $value, $name = 'unknown', $backtrace = false ) { print "\n\n<pre><b>Debugging value '$name':</b>\n\n"; @@ -1686,12 +1752,4 @@ abstract class ApiBase extends ContextSource { } print "\n</pre>\n"; } - - /** - * Returns a string that identifies the version of this class. - * @return string - */ - public static function getBaseVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index c879b35d..90432b95 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -25,17 +25,13 @@ */ /** -* API module that facilitates the blocking of users. Requires API write mode -* to be enabled. -* + * API module that facilitates the blocking of users. Requires API write mode + * to be enabled. + * * @ingroup API */ class ApiBlock extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - /** * Blocks the user specified in the parameters for the given expiry, with the * given reason, and with all other settings provided in the params. If the block @@ -55,6 +51,7 @@ class ApiBlock extends ApiBase { if ( !$user->isAllowed( 'block' ) ) { $this->dieUsageMsg( 'cantblock' ); } + # bug 15810: blocked admins should have limited access here if ( $user->isBlocked() ) { $status = SpecialBlock::checkUnblockSelf( $params['user'], $user ); @@ -62,6 +59,13 @@ class ApiBlock extends ApiBase { $this->dieUsageMsg( array( $status ) ); } } + + $target = User::newFromName( $params['user'] ); + // Bug 38633 - if the target is a user (not an IP address), but it doesn't exist or is unusable, error. + if ( $target instanceof User && ( $target->isAnon() /* doesn't exist */ || !User::isUsableName( $target->getName() ) ) ) { + $this->dieUsageMsg( array( 'nosuchuser', $params['user'] ) ); + } + if ( $params['hidename'] && !$user->isAllowed( 'hideuser' ) ) { $this->dieUsageMsg( 'canthide' ); } @@ -70,6 +74,7 @@ class ApiBlock extends ApiBase { } $data = array( + 'PreviousTarget' => $params['user'], 'Target' => $params['user'], 'Reason' => array( $params['reason'], @@ -83,7 +88,7 @@ class ApiBlock extends ApiBase { 'DisableEmail' => $params['noemail'], 'HideUser' => $params['hidename'], 'DisableUTEdit' => !$params['allowusertalk'], - 'AlreadyBlocked' => $params['reblock'], + 'Reblock' => $params['reblock'], 'Watch' => $params['watchuser'], 'Confirm' => true, ); @@ -99,7 +104,7 @@ class ApiBlock extends ApiBase { $res['userID'] = $target instanceof User ? $target->getId() : 0; $block = Block::newFromTarget( $target ); - if( $block instanceof Block ){ + if( $block instanceof Block ) { $res['expiry'] = $block->mExpiry == $this->getDB()->getInfinity() ? 'infinite' : wfTimestamp( TS_ISO_8601, $block->mExpiry ); @@ -178,7 +183,7 @@ class ApiBlock extends ApiBase { 'anononly' => 'Block anonymous users only (i.e. disable anonymous edits for this IP)', 'nocreate' => 'Prevent account creation', 'autoblock' => 'Automatically block the last used IP address, and any subsequent IP addresses they try to login from', - 'noemail' => 'Prevent user from sending e-mail through the wiki. (Requires the "blockemail" right.)', + 'noemail' => 'Prevent user from sending email through the wiki. (Requires the "blockemail" right.)', 'hidename' => 'Hide the username from the block log. (Requires the "hideuser" right.)', 'allowusertalk' => 'Allow the user to edit their own talk page (depends on $wgBlockAllowsUTEdit)', 'reblock' => 'If the user is already blocked, overwrite the existing block', @@ -256,8 +261,4 @@ class ApiBlock extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Block'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index ed72b29b..79ffcb0a 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -25,17 +25,21 @@ class ApiComparePages extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { $params = $this->extractRequestParams(); $rev1 = $this->revisionOrTitleOrId( $params['fromrev'], $params['fromtitle'], $params['fromid'] ); $rev2 = $this->revisionOrTitleOrId( $params['torev'], $params['totitle'], $params['toid'] ); - $de = new DifferenceEngine( $this->getContext(), + $revision = Revision::newFromId( $rev1 ); + + if ( !$revision ) { + $this->dieUsage( 'The diff cannot be retrieved, ' . + 'one revision does not exist or you do not have permission to view it.', 'baddiff' ); + } + + $contentHandler = $revision->getContentHandler(); + $de = $contentHandler->createDifferenceEngine( $this->getContext(), $rev1, $rev2, null, // rcid @@ -77,11 +81,11 @@ class ApiComparePages extends ApiBase { * @return int */ private function revisionOrTitleOrId( $revision, $titleText, $titleId ) { - if( $revision ){ + if( $revision ) { return $revision; } elseif( $titleText ) { $title = Title::newFromText( $titleText ); - if( !$title ){ + if( !$title || $title->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $titleText ) ); } return $title->getLatestRevID(); @@ -164,8 +168,4 @@ class ApiComparePages extends ApiBase { 'api.php?action=compare&fromrev=1&torev=2' => 'Create a diff between revision 1 and 2', ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiCreateAccount.php b/includes/api/ApiCreateAccount.php new file mode 100644 index 00000000..55c60cce --- /dev/null +++ b/includes/api/ApiCreateAccount.php @@ -0,0 +1,298 @@ +<?php +/** + * Created on August 7, 2012 + * + * Copyright © 2012 Tyler Romeo <tylerromeo@gmail.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Unit to authenticate account registration attempts to the current wiki. + * + * @ingroup API + */ +class ApiCreateAccount extends ApiBase { + public function execute() { + + // $loginForm->addNewaccountInternal will throw exceptions + // if wiki is read only (already handled by api), user is blocked or does not have rights. + // Use userCan in order to hit GlobalBlock checks (according to Special:userlogin) + $loginTitle = SpecialPage::getTitleFor( 'Userlogin' ); + if ( !$loginTitle->userCan( 'createaccount', $this->getUser() ) ) { + $this->dieUsage( 'You do not have the right to create a new account', 'permdenied-createaccount' ); + } + if ( $this->getUser()->isBlockedFromCreateAccount() ) { + $this->dieUsage( 'You cannot create a new account because you are blocked', 'blocked' ); + } + + $params = $this->extractRequestParams(); + + $result = array(); + + // Init session if necessary + if ( session_id() == '' ) { + wfSetupSession(); + } + + if( $params['mailpassword'] && !$params['email'] ) { + $this->dieUsageMsg( 'noemail' ); + } + + $context = new DerivativeContext( $this->getContext() ); + $context->setRequest( new DerivativeRequest( + $this->getContext()->getRequest(), + array( + 'type' => 'signup', + 'uselang' => $params['language'], + 'wpName' => $params['name'], + 'wpPassword' => $params['password'], + 'wpRetype' => $params['password'], + 'wpDomain' => $params['domain'], + 'wpEmail' => $params['email'], + 'wpRealName' => $params['realname'], + 'wpCreateaccountToken' => $params['token'], + 'wpCreateaccount' => $params['mailpassword'] ? null : '1', + 'wpCreateaccountMail' => $params['mailpassword'] ? '1' : null + ) + ) ); + + $loginForm = new LoginForm(); + $loginForm->setContext( $context ); + $loginForm->load(); + + $status = $loginForm->addNewaccountInternal(); + $result = array(); + if( $status->isGood() ) { + // Success! + $user = $status->getValue(); + + // If we showed up language selection links, and one was in use, be + // smart (and sensible) and save that language as the user's preference + global $wgLoginLanguageSelector, $wgEmailAuthentication; + if( $wgLoginLanguageSelector && $params['language'] ) { + $user->setOption( 'language', $params['language'] ); + } + + if( $params['mailpassword'] ) { + // If mailpassword was set, disable the password and send an email. + $user->setPassword( null ); + $status->merge( $loginForm->mailPasswordInternal( $user, false, 'createaccount-title', 'createaccount-text' ) ); + } elseif( $wgEmailAuthentication && Sanitizer::validateEmail( $user->getEmail() ) ) { + // Send out an email authentication message if needed + $status->merge( $user->sendConfirmationMail() ); + } + + // Save settings (including confirmation token) + $user->saveSettings(); + + wfRunHooks( 'AddNewAccount', array( $user, $params['mailpassword'] ) ); + + if ( $params['mailpassword'] ) { + $logAction = 'byemail'; + } elseif ( $this->getUser()->isLoggedIn() ) { + $logAction = 'create2'; + } else { + $logAction = 'create'; + } + $user->addNewUserLogEntry( $logAction, (string)$params['reason'] ); + + // Add username, id, and token to result. + $result['username'] = $user->getName(); + $result['userid'] = $user->getId(); + $result['token'] = $user->getToken(); + } + + $apiResult = $this->getResult(); + + if( $status->hasMessage( 'sessionfailure' ) || $status->hasMessage( 'nocookiesfornew' ) ) { + // Token was incorrect, so add it to result, but don't throw an exception + // since not having the correct token is part of the normal + // flow of events. + $result['token'] = LoginForm::getCreateaccountToken(); + $result['result'] = 'needtoken'; + } elseif( !$status->isOK() ) { + // There was an error. Die now. + // Cannot use dieUsageMsg() directly because extensions + // might return custom error messages. + $errors = $status->getErrorsArray(); + if( $errors[0] instanceof Message ) { + $code = 'aborted'; + $desc = $errors[0]; + } else { + $code = array_shift( $errors[0] ); + $desc = wfMessage( $code, $errors[0] ); + } + $this->dieUsage( $desc, $code ); + } elseif( !$status->isGood() ) { + // Status is not good, but OK. This means warnings. + $result['result'] = 'warning'; + + // Add any warnings to the result + $warnings = $status->getErrorsByType( 'warning' ); + if( $warnings ) { + foreach( $warnings as &$warning ) { + $apiResult->setIndexedTagName( $warning['params'], 'param' ); + } + $apiResult->setIndexedTagName( $warnings, 'warning' ); + $result['warnings'] = $warnings; + } + } else { + // Everything was fine. + $result['result'] = 'success'; + } + + $apiResult->addValue( null, 'createaccount', $result ); + } + + public function getDescription() { + return 'Create a new user account.'; + } + + public function mustBePosted() { + return true; + } + + public function isReadMode() { + return false; + } + + public function isWriteMode() { + return true; + } + + public function getAllowedParams() { + global $wgEmailConfirmToEdit; + return array( + 'name' => array( + ApiBase::PARAM_TYPE => 'user', + ApiBase::PARAM_REQUIRED => true + ), + 'password' => null, + 'domain' => null, + 'token' => null, + 'email' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => $wgEmailConfirmToEdit + ), + 'realname' => null, + 'mailpassword' => array( + ApiBase::PARAM_TYPE => 'boolean', + ApiBase::PARAM_DFLT => false + ), + 'reason' => null, + 'language' => null + ); + } + + public function getParamDescription() { + $p = $this->getModulePrefix(); + return array( + 'name' => 'Username', + 'password' => "Password (ignored if {$p}mailpassword is set)", + 'domain' => 'Domain for external authentication (optional)', + 'token' => 'Account creation token obtained in first request', + 'email' => 'Email address of user (optional)', + 'realname' => 'Real name of user (optional)', + 'mailpassword' => 'If set to any value, a random password will be emailed to the user', + 'reason' => 'Optional reason for creating the account to be put in the logs', + 'language' => 'Language code to set as default for the user (optional, defaults to content language)' + ); + } + + public function getResultProperties() { + return array( + 'createaccount' => array( + 'result' => array( + ApiBase::PROP_TYPE => array( + 'success', + 'warning', + 'needtoken' + ) + ), + 'username' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'userid' => array( + ApiBase::PROP_TYPE => 'int', + ApiBase::PROP_NULLABLE => true + ), + 'token' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + ) + ); + } + + public function getPossibleErrors() { + // Note the following errors aren't possible and don't need to be listed: + // sessionfailure, nocookiesfornew, badretype + $localErrors = array( + 'wrongpassword', // Actually caused by wrong domain field. Riddle me that... + 'sorbs_create_account_reason', + 'noname', + 'userexists', + 'password-name-match', // from User::getPasswordValidity + 'password-login-forbidden', // from User::getPasswordValidity + 'noemailtitle', + 'invalidemailaddress', + 'externaldberror', + 'acct_creation_throttle_hit', + ); + + $errors = parent::getPossibleErrors(); + // All local errors are from LoginForm, which means they're actually message keys. + foreach( $localErrors as $error ) { + $errors[] = array( 'code' => $error, 'info' => wfMessage( $error )->parse() ); + } + + $errors[] = array( + 'code' => 'permdenied-createaccount', + 'info' => 'You do not have the right to create a new account' + ); + $errors[] = array( + 'code' => 'blocked', + 'info' => 'You cannot create a new account because you are blocked' + ); + $errors[] = array( + 'code' => 'aborted', + 'info' => 'Account creation aborted by hook (info may vary)' + ); + + // 'passwordtooshort' has parameters. :( + global $wgMinimalPasswordLength; + $errors[] = array( + 'code' => 'passwordtooshort', + 'info' => wfMessage( 'passwordtooshort', $wgMinimalPasswordLength )->parse() + ); + return $errors; + } + + public function getExamples() { + return array( + 'api.php?action=createaccount&name=testuser&password=test123', + 'api.php?action=createaccount&name=testmailuser&mailpassword=true&reason=MyReason', + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Account_creation'; + } +} diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index 2d36f19a..d1f0806e 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -32,10 +32,6 @@ */ class ApiDelete extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - /** * Extracts the title, token, and reason from the request parameters and invokes * the local delete() function with these as arguments. It does not make use of @@ -61,6 +57,9 @@ class ApiDelete extends ApiBase { $status = self::delete( $pageObj, $user, $params['token'], $reason ); } + if ( is_array( $status ) ) { + $this->dieUsageMsg( $status[0] ); + } if ( !$status->isGood() ) { $errors = $status->getErrorsArray(); $this->dieUsageMsg( $errors[0] ); // We don't care about multiple errors, just report one of them @@ -98,11 +97,11 @@ class ApiDelete extends ApiBase { /** * We have our own delete() function, since Article.php's implementation is split in two phases * - * @param $page WikiPage object to work on + * @param $page Page|WikiPage object to work on * @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 Status + * @param string $token delete token (same as edit token) + * @param string|null $reason reason for the deletion. Autogenerated if NULL + * @return Status|array */ public static function delete( Page $page, User $user, $token, &$reason = null ) { $title = $page->getTitle(); @@ -128,13 +127,13 @@ class ApiDelete extends ApiBase { } /** - * @param $page WikiPage object to work on + * @param $page WikiPage|Page object to work on * @param $user User doing the action * @param $token * @param $oldimage * @param $reason * @param $suppress bool - * @return Status + * @return Status|array */ public static function deleteFile( Page $page, User $user, $token, $oldimage, &$reason = null, $suppress = false ) { $title = $page->getTitle(); @@ -161,7 +160,7 @@ class ApiDelete extends ApiBase { if ( is_null( $reason ) ) { // Log and RC don't like null reasons $reason = ''; } - return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress ); + return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress, $user ); } public function mustBePosted() { @@ -264,8 +263,4 @@ class ApiDelete extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Delete'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiDisabled.php b/includes/api/ApiDisabled.php index 13975aec..e5ef3b7e 100644 --- a/includes/api/ApiDisabled.php +++ b/includes/api/ApiDisabled.php @@ -36,10 +36,6 @@ */ class ApiDisabled extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { $this->dieUsage( "The \"{$this->getModuleName()}\" module has been disabled.", 'moduledisabled' ); } @@ -63,8 +59,4 @@ class ApiDisabled extends ApiBase { public function getExamples() { return array(); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index 0963fe7c..4916145b 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -33,10 +33,6 @@ */ class ApiEditPage extends ApiBase { - public function __construct( $query, $moduleName ) { - parent::__construct( $query, $moduleName ); - } - public function execute() { $user = $this->getUser(); $params = $this->extractRequestParams(); @@ -50,32 +46,28 @@ class ApiEditPage extends ApiBase { $pageObj = $this->getTitleOrPageId( $params ); $titleObj = $pageObj->getTitle(); - if ( $titleObj->isExternal() ) { - $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); - } - $apiResult = $this->getResult(); if ( $params['redirect'] ) { if ( $titleObj->isRedirect() ) { $oldTitle = $titleObj; - $titles = Title::newFromRedirectArray( - Revision::newFromTitle( - $oldTitle, false, Revision::READ_LATEST - )->getText( Revision::FOR_THIS_USER ) - ); + $titles = Revision::newFromTitle( $oldTitle, false, Revision::READ_LATEST ) + ->getContent( Revision::FOR_THIS_USER, $user ) + ->getRedirectChain(); // array_shift( $titles ); $redirValues = array(); + + /** @var $newTitle Title */ foreach ( $titles as $id => $newTitle ) { - if ( !isset( $titles[ $id - 1 ] ) ) { - $titles[ $id - 1 ] = $oldTitle; + if ( !isset( $titles[$id - 1] ) ) { + $titles[$id - 1] = $oldTitle; } $redirValues[] = array( - 'from' => $titles[ $id - 1 ]->getPrefixedText(), + 'from' => $titles[$id - 1]->getPrefixedText(), 'to' => $newTitle->getPrefixedText() ); @@ -84,9 +76,34 @@ class ApiEditPage extends ApiBase { $apiResult->setIndexedTagName( $redirValues, 'r' ); $apiResult->addValue( null, 'redirects', $redirValues ); + + // Since the page changed, update $pageObj + $pageObj = WikiPage::factory( $titleObj ); } } + if ( !isset( $params['contentmodel'] ) || $params['contentmodel'] == '' ) { + $contentHandler = $pageObj->getContentHandler(); + } else { + $contentHandler = ContentHandler::getForModelID( $params['contentmodel'] ); + } + + // @todo ask handler whether direct editing is supported at all! make allowFlatEdit() method or some such + + if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) { + $params['contentformat'] = $contentHandler->getDefaultFormat(); + } + + $contentFormat = $params['contentformat']; + + if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) { + $name = $titleObj->getPrefixedDBkey(); + $model = $contentHandler->getModelID(); + + $this->dieUsage( "The requested format $contentFormat is not supported for content model ". + " $model used by $name", 'badformat' ); + } + if ( $params['createonly'] && $titleObj->exists() ) { $this->dieUsageMsg( 'createonly-exists' ); } @@ -103,31 +120,61 @@ class ApiEditPage extends ApiBase { $this->dieUsageMsg( $errors[0] ); } - $articleObj = Article::newFromTitle( $titleObj, $this->getContext() ); - $toMD5 = $params['text']; if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) ) { - // For non-existent pages, Article::getContent() - // returns an interface message rather than '' - // We do want getContent()'s behavior for non-existent - // MediaWiki: pages, though - if ( $articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI ) { - $content = ''; - } else { - $content = $articleObj->getContent(); + $content = $pageObj->getContent(); + + if ( !$content ) { + if ( $titleObj->getNamespace() == NS_MEDIAWIKI ) { + # If this is a MediaWiki:x message, then load the messages + # and return the message value for x. + $text = $titleObj->getDefaultMessageText(); + if ( $text === false ) { + $text = ''; + } + + try { + $content = ContentHandler::makeContent( $text, $this->getTitle() ); + } catch ( MWContentSerializationException $ex ) { + $this->dieUsage( $ex->getMessage(), 'parseerror' ); + return; + } + } else { + # Otherwise, make a new empty content. + $content = $contentHandler->makeEmptyContent(); + } + } + + // @todo: Add support for appending/prepending to the Content interface + + if ( !( $content instanceof TextContent ) ) { + $mode = $contentHandler->getModelID(); + $this->dieUsage( "Can't append to pages using content model $mode", 'appendnotsupported' ); } if ( !is_null( $params['section'] ) ) { + if ( !$contentHandler->supportsSections() ) { + $modelName = $contentHandler->getModelID(); + $this->dieUsage( "Sections are not supported for this content model: $modelName.", 'sectionsnotsupported' ); + } + // Process the content for section edits - global $wgParser; $section = intval( $params['section'] ); - $content = $wgParser->getSection( $content, $section, false ); - if ( $content === false ) { + $content = $content->getSection( $section ); + + if ( !$content ) { $this->dieUsage( "There is no section {$section}.", 'nosuchsection' ); } } - $params['text'] = $params['prependtext'] . $content . $params['appendtext']; + + if ( !$content ) { + $text = ''; + } else { + $text = $content->serialize( $contentFormat ); + } + + $params['text'] = $params['prependtext'] . $text . $params['appendtext']; $toMD5 = $params['prependtext'] . $params['appendtext']; } @@ -151,18 +198,21 @@ class ApiEditPage extends ApiBase { $this->dieUsageMsg( array( 'nosuchrevid', $params['undoafter'] ) ); } - if ( $undoRev->getPage() != $articleObj->getID() ) { + if ( $undoRev->getPage() != $pageObj->getID() ) { $this->dieUsageMsg( array( 'revwrongpage', $undoRev->getID(), $titleObj->getPrefixedText() ) ); } - if ( $undoafterRev->getPage() != $articleObj->getID() ) { + if ( $undoafterRev->getPage() != $pageObj->getID() ) { $this->dieUsageMsg( array( 'revwrongpage', $undoafterRev->getID(), $titleObj->getPrefixedText() ) ); } - $newtext = $articleObj->getUndoText( $undoRev, $undoafterRev ); - if ( $newtext === false ) { + $newContent = $contentHandler->getUndoContent( $pageObj->getRevision(), $undoRev, $undoafterRev ); + + if ( !$newContent ) { $this->dieUsageMsg( 'undo-failure' ); } - $params['text'] = $newtext; + + $params['text'] = $newContent->serialize( $params['contentformat'] ); + // 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'] ) { @@ -179,6 +229,8 @@ class ApiEditPage extends ApiBase { // That interface kind of sucks, but it's workable $requestArray = array( 'wpTextbox1' => $params['text'], + 'format' => $contentFormat, + 'model' => $contentHandler->getModelID(), 'wpEditToken' => $params['token'], 'wpIgnoreBlankSummary' => '' ); @@ -191,18 +243,23 @@ class ApiEditPage extends ApiBase { $requestArray['wpSectionTitle'] = $params['sectiontitle']; } + // TODO: Pass along information from 'undoafter' as well + if ( $params['undo'] > 0 ) { + $requestArray['wpUndidRevision'] = $params['undo']; + } + // Watch out for basetimestamp == '' // wfTimestamp() treats it as NOW, almost certainly causing an edit conflict if ( !is_null( $params['basetimestamp'] ) && $params['basetimestamp'] != '' ) { $requestArray['wpEdittime'] = wfTimestamp( TS_MW, $params['basetimestamp'] ); } else { - $requestArray['wpEdittime'] = $articleObj->getTimestamp(); + $requestArray['wpEdittime'] = $pageObj->getTimestamp(); } if ( !is_null( $params['starttimestamp'] ) && $params['starttimestamp'] != '' ) { $requestArray['wpStarttime'] = wfTimestamp( TS_MW, $params['starttimestamp'] ); } else { - $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime + $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime } if ( $params['minor'] || ( !$params['notminor'] && $user->getOption( 'minordefault' ) ) ) { @@ -244,7 +301,19 @@ class ApiEditPage extends ApiBase { // TODO: Make them not or check if they still do $wgTitle = $titleObj; - $ep = new EditPage( $articleObj ); + $articleContext = new RequestContext; + $articleContext->setRequest( $req ); + $articleContext->setWikiPage( $pageObj ); + $articleContext->setUser( $this->getUser() ); + + /** @var $articleObject Article */ + $articleObject = Article::newFromWikiPage( $pageObj, $articleContext ); + + $ep = new EditPage( $articleObject ); + + // allow editing of non-textual content. + $ep->allowNonTextContent = true; + $ep->setContextTitle( $titleObj ); $ep->importFormData( $req ); @@ -262,7 +331,7 @@ class ApiEditPage extends ApiBase { } // Do the actual save - $oldRevId = $articleObj->getRevIdFetched(); + $oldRevId = $articleObject->getRevIdFetched(); $result = null; // Fake $wgRequest for some hooks inside EditPage // @todo FIXME: This interface SUCKS @@ -278,6 +347,9 @@ class ApiEditPage extends ApiBase { case EditPage::AS_HOOK_ERROR_EXPECTED: $this->dieUsageMsg( 'hookaborted' ); + case EditPage::AS_PARSE_ERROR: + $this->dieUsage( $status->getMessage(), 'parseerror' ); + case EditPage::AS_IMAGE_REDIRECT_ANON: $this->dieUsageMsg( 'noimageredirect-anon' ); @@ -324,19 +396,21 @@ class ApiEditPage extends ApiBase { case EditPage::AS_SUCCESS_NEW_ARTICLE: $r['new'] = ''; + // fall-through case EditPage::AS_SUCCESS_UPDATE: $r['result'] = 'Success'; $r['pageid'] = intval( $titleObj->getArticleID() ); $r['title'] = $titleObj->getPrefixedText(); - $newRevId = $articleObj->getLatest(); + $r['contentmodel'] = $titleObj->getContentModel(); + $newRevId = $articleObject->getLatest(); if ( $newRevId == $oldRevId ) { $r['nochange'] = ''; } else { $r['oldrevid'] = intval( $oldRevId ); $r['newrevid'] = intval( $newRevId ); $r['newtimestamp'] = wfTimestamp( TS_ISO_8601, - $articleObj->getTimestamp() ); + $pageObj->getTimestamp() ); } break; @@ -380,6 +454,7 @@ class ApiEditPage extends ApiBase { array( 'undo-failure' ), array( 'hashcheckfailed' ), array( 'hookaborted' ), + array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ), array( 'noimageredirect-anon' ), array( 'noimageredirect-logged' ), array( 'spamdetected', 'spam' ), @@ -397,6 +472,13 @@ class ApiEditPage extends ApiBase { array( 'unknownerror', 'retval' ), array( 'code' => 'nosuchsection', 'info' => 'There is no section section.' ), array( 'code' => 'invalidsection', 'info' => 'The section parameter must be set to an integer or \'new\'' ), + array( 'code' => 'sectionsnotsupported', 'info' => 'Sections are not supported for this type of page.' ), + array( 'code' => 'editnotsupported', 'info' => 'Editing of this type of page is not supported using ' + . 'the text based edit API.' ), + array( 'code' => 'appendnotsupported', 'info' => 'This type of page can not be edited by appending ' + . 'or prepending text.' ), + array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied to ' + . 'the page\'s content model' ), array( 'customcssprotected' ), array( 'customjsprotected' ), ) @@ -414,7 +496,6 @@ class ApiEditPage extends ApiBase { 'section' => null, 'sectiontitle' => array( ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => false, ), 'text' => null, 'token' => array( @@ -460,6 +541,12 @@ class ApiEditPage extends ApiBase { ApiBase::PARAM_TYPE => 'boolean', ApiBase::PARAM_DFLT => false, ), + 'contentformat' => array( + ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(), + ), + 'contentmodel' => array( + ApiBase::PARAM_TYPE => ContentHandler::getContentModels(), + ) ); } @@ -490,7 +577,7 @@ class ApiEditPage extends ApiBase { 'watch' => 'Add the page to your watchlist', 'unwatch' => 'Remove the page from your watchlist', 'watchlist' => 'Unconditionally add or remove the page from your watchlist, use preferences or do not change watch', - 'md5' => array( "The MD5 hash of the {$p}text parameter, or the {$p}prependtext and {$p}appendtext parameters concatenated.", + 'md5' => array( "The MD5 hash of the {$p}text parameter, or the {$p}prependtext and {$p}appendtext parameters concatenated.", 'If set, the edit won\'t be done unless the hash is correct' ), 'prependtext' => "Add this text to the beginning of the page. Overrides {$p}text", 'appendtext' => array( "Add this text to the end of the page. Overrides {$p}text.", @@ -498,6 +585,8 @@ class ApiEditPage extends ApiBase { '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', + 'contentformat' => 'Content serialization format used for the input text', + 'contentmodel' => 'Content model of the new content', ); } @@ -546,10 +635,8 @@ class ApiEditPage extends ApiBase { public function getExamples() { return array( - 'api.php?action=edit&title=Test&summary=test%20summary&text=article%20content&basetimestamp=20070824123454&token=%2B\\' => 'Edit a page (anonymous user)', - 'api.php?action=edit&title=Test&summary=NOTOC&minor=&prependtext=__NOTOC__%0A&basetimestamp=20070824123454&token=%2B\\' => 'Prepend __NOTOC__ to a page (anonymous user)', 'api.php?action=edit&title=Test&undo=13585&undoafter=13579&basetimestamp=20070824123454&token=%2B\\' @@ -560,8 +647,4 @@ class ApiEditPage extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Edit'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiEmailUser.php b/includes/api/ApiEmailUser.php index 4fa03434..cd0d0cba 100644 --- a/includes/api/ApiEmailUser.php +++ b/includes/api/ApiEmailUser.php @@ -30,10 +30,6 @@ */ class ApiEmailUser extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { $params = $this->extractRequestParams(); @@ -158,10 +154,6 @@ class ApiEmailUser extends ApiBase { } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:E-mail'; - } - - public function getVersion() { - return __CLASS__ . ': $Id$'; + return 'https://www.mediawiki.org/wiki/API:Email'; } } diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php index 160f5b91..f5898fb3 100644 --- a/includes/api/ApiExpandTemplates.php +++ b/includes/api/ApiExpandTemplates.php @@ -33,10 +33,6 @@ */ class ApiExpandTemplates extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { // Cache may vary on $wgUser because ParserOptions gets data from it $this->getMain()->setCacheMode( 'anon-public-user-private' ); @@ -46,7 +42,7 @@ class ApiExpandTemplates extends ApiBase { // Create title for parser $title_obj = Title::newFromText( $params['title'] ); - if ( !$title_obj ) { + if ( !$title_obj || $title_obj->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); } @@ -130,8 +126,4 @@ class ApiExpandTemplates extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Parsing_wikitext#expandtemplates'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index 1cf760ae..015a9922 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -29,10 +29,6 @@ */ class ApiFeedContributions extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - /** * This module uses a custom feed wrapper printer. * @@ -51,7 +47,7 @@ class ApiFeedContributions extends ApiBase { $this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' ); } - if( !isset( $wgFeedClasses[ $params['feedformat'] ] ) ) { + if( !isset( $wgFeedClasses[$params['feedformat']] ) ) { $this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' ); } @@ -130,10 +126,22 @@ class ApiFeedContributions extends ApiBase { protected function feedItemDesc( $revision ) { if( $revision ) { $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text(); + $content = $revision->getContent(); + + if ( $content instanceof TextContent ) { + // only textual content has a "source view". + $html = nl2br( htmlspecialchars( $content->getNativeData() ) ); + } else { + //XXX: we could get an HTML representation of the content via getParserOutput, but that may + // contain JS magic and generally may not be suitable for inclusion in a feed. + // Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method. + //Compare also FeedUtils::formatDiffRow. + $html = ''; + } + return '<p>' . htmlspecialchars( $revision->getUserText() ) . $msg . htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) . - "</p>\n<hr />\n<div>" . - nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>"; + "</p>\n<hr />\n<div>" . $html . "</div>"; } return ''; } @@ -201,8 +209,4 @@ class ApiFeedContributions extends ApiBase { 'api.php?action=feedcontributions&user=Reedy', ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index 6ccb02fe..6c793b36 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -33,10 +33,6 @@ */ class ApiFeedWatchlist extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - /** * This module uses a custom feed wrapper printer. * @@ -62,12 +58,9 @@ class ApiFeedWatchlist extends ApiBase { $this->dieUsage( 'Syndication feeds are not available', 'feed-unavailable' ); } - if( !isset( $wgFeedClasses[ $params['feedformat'] ] ) ) { + if( !isset( $wgFeedClasses[$params['feedformat']] ) ) { $this->dieUsage( 'Invalid subscription feed type', 'feed-invalid' ); } - if ( !is_null( $params['wlexcludeuser'] ) ) { - $fauxReqArr['wlexcludeuser'] = $params['wlexcludeuser']; - } // limit to the number of hours going from now back $endTime = wfTimestamp( TS_MW, time() - intval( $params['hours'] * 60 * 60 ) ); @@ -84,12 +77,15 @@ class ApiFeedWatchlist extends ApiBase { 'wllimit' => ( 50 > $wgFeedLimit ) ? $wgFeedLimit : 50 ); - if ( !is_null( $params['wlowner'] ) ) { + if ( $params['wlowner'] !== null ) { $fauxReqArr['wlowner'] = $params['wlowner']; } - if ( !is_null( $params['wltoken'] ) ) { + if ( $params['wltoken'] !== null ) { $fauxReqArr['wltoken'] = $params['wltoken']; } + if ( $params['wlexcludeuser'] !== null ) { + $fauxReqArr['wlexcludeuser'] = $params['wlexcludeuser']; + } // Support linking to diffs instead of article if ( $params['linktodiffs'] ) { @@ -233,8 +229,4 @@ class ApiFeedWatchlist extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Watchlist_feed'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFileRevert.php b/includes/api/ApiFileRevert.php index 83d078d2..cbb2ba6a 100644 --- a/includes/api/ApiFileRevert.php +++ b/includes/api/ApiFileRevert.php @@ -37,10 +37,6 @@ class ApiFileRevert extends ApiBase { protected $params; - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { $this->params = $this->extractRequestParams(); // Extract the file and archiveName from the request parameters @@ -50,7 +46,7 @@ class ApiFileRevert extends ApiBase { $this->checkPermissions( $this->getUser() ); $sourceUrl = $this->file->getArchiveVirtualUrl( $this->archiveName ); - $status = $this->file->upload( $sourceUrl, $this->params['comment'], $this->params['comment'] ); + $status = $this->file->upload( $sourceUrl, $this->params['comment'], $this->params['comment'], 0, false, false, $this->getUser() ); if ( $status->isGood() ) { $result = array( 'result' => 'Success' ); @@ -73,8 +69,8 @@ class ApiFileRevert extends ApiBase { protected function checkPermissions( $user ) { $title = $this->file->getTitle(); $permissionErrors = array_merge( - $title->getUserPermissionsErrors( 'edit' , $user ), - $title->getUserPermissionsErrors( 'upload' , $user ) + $title->getUserPermissionsErrors( 'edit', $user ), + $title->getUserPermissionsErrors( 'upload', $user ) ); if ( $permissionErrors ) { @@ -191,12 +187,8 @@ class ApiFileRevert extends ApiBase { public function getExamples() { return array( - 'api.php?action=filerevert&filename=Wiki.png&comment=Revert&archivename=20110305152740!Wiki.png&token=+\\' + 'api.php?action=filerevert&filename=Wiki.png&comment=Revert&archivename=20110305152740!Wiki.png&token=123ABC' => 'Revert Wiki.png to the version of 20110305152740', ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index 8ad9b8ca..d8aa1634 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -38,7 +38,7 @@ abstract class ApiFormatBase extends ApiBase { * Constructor * If $format ends with 'fm', pretty-print the output in HTML. * @param $main ApiMain - * @param $format string Format name + * @param string $format Format name */ public function __construct( $main, $format ) { parent::__construct( $main, $format ); @@ -58,7 +58,7 @@ abstract class ApiFormatBase extends ApiBase { * This method is not called if getIsHtml() returns true. * @return string */ - public abstract function getMimeType(); + abstract public function getMimeType(); /** * Whether this formatter needs raw data such as _element tags @@ -83,7 +83,7 @@ abstract class ApiFormatBase extends ApiBase { * special-case fix that should be removed once the help has been * reworked to use a fully HTML interface. * - * @param $b bool Whether or not ampersands should be escaped. + * @param bool $b Whether or not ampersands should be escaped. */ public function setUnescapeAmps ( $b ) { $this->mUnescapeAmps = $b; @@ -123,11 +123,13 @@ abstract class ApiFormatBase extends ApiBase { /** * Initialize the printer function and prepare the output headers, etc. - * This method must be the first outputing method during execution. - * A help screen's header is printed for the HTML-based output - * @param $isError bool Whether an error message is printed + * This method must be the first outputting method during execution. + * A human-targeted notice about available formats is printed for the HTML-based output, + * except for help screens (caused by either an error in the API parameters, + * the calling of action=help, or requesting the root script api.php). + * @param bool $isHelpScreen Whether a help screen is going to be shown */ - function initPrinter( $isError ) { + function initPrinter( $isHelpScreen ) { if ( $this->mDisabled ) { return; } @@ -164,7 +166,7 @@ abstract class ApiFormatBase extends ApiBase { <?php - if ( !$isError ) { + if ( !$isHelpScreen ) { ?> <br /> <small> @@ -175,15 +177,18 @@ To see the non HTML representation of the <?php echo( $this->mFormat ); ?> forma 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> +<pre style='white-space: pre-wrap;'> <?php - } + } else { // don't wrap the contents of the <pre> for help screens + // because these are actually formatted to rely on + // the monospaced font for layout purposes ?> <pre> <?php - + } } } @@ -248,7 +253,7 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, } /** - * Sets whether the pretty-printer should format *bold* and $italics$ + * Sets whether the pretty-printer should format *bold* * @param $help bool */ public function setHelp( $help = true ) { @@ -264,22 +269,19 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, protected function formatHTML( $text ) { // Escape everything first for full coverage $text = htmlspecialchars( $text ); - // encode all comments or tags as safe blue strings $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( "#(((?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 ) { // make strings inside * bold $text = preg_replace( "#\\*[^<>\n]+\\*#", '<b>\\0</b>', $text ); - // make strings inside $ italic - $text = preg_replace( "#\\$[^<>\n]+\\$#", '<b><i>\\0</i></b>', $text ); } + // identify URLs + $protos = wfUrlProtocolsWithoutProtRel(); + // This regex hacks around bug 13218 (" included in the URL) + $text = preg_replace( "#(((?i)$protos).*?)(")?([ \\'\"<>\n]|<|>|")#", '<a href="\\1">\\1</a>\\3\\4', $text ); /** * Temporary fix for bad links in help messages. As a special case, @@ -308,10 +310,6 @@ See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, public function getDescription() { return $this->getIsHtml() ? ' (pretty-print in HTML)' : ''; } - - public static function getBaseVersion() { - return __CLASS__ . ': $Id$'; - } } /** @@ -328,7 +326,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase { * Call this method to initialize output data. See execute() * @param $result ApiResult * @param $feed object an instance of one of the $wgFeedClasses classes - * @param $feedItems array of FeedItem objects + * @param array $feedItems of FeedItem objects */ public static function setResult( $result, $feed, $feedItems ) { // Store output in the Result data. @@ -381,8 +379,4 @@ class ApiFormatFeedWrapper extends ApiFormatBase { ApiBase::dieDebug( __METHOD__, 'Invalid feed class/item' ); } } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFormatDbg.php b/includes/api/ApiFormatDbg.php index 3d2a39ca..1b2e02c9 100644 --- a/includes/api/ApiFormatDbg.php +++ b/includes/api/ApiFormatDbg.php @@ -30,10 +30,6 @@ */ class ApiFormatDbg extends ApiFormatBase { - public function __construct( $main, $format ) { - parent::__construct( $main, $format ); - } - public function getMimeType() { // This looks like it should be text/plain, but IE7 is so // brain-damaged it tries to parse text/plain as HTML if it @@ -48,8 +44,4 @@ class ApiFormatDbg extends ApiFormatBase { public function getDescription() { return 'Output data in PHP\'s var_export() format' . parent::getDescription(); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFormatDump.php b/includes/api/ApiFormatDump.php index 0f055e13..62253e14 100644 --- a/includes/api/ApiFormatDump.php +++ b/includes/api/ApiFormatDump.php @@ -30,10 +30,6 @@ */ class ApiFormatDump extends ApiFormatBase { - public function __construct( $main, $format ) { - parent::__construct( $main, $format ); - } - public function getMimeType() { // This looks like it should be text/plain, but IE7 is so // brain-damaged it tries to parse text/plain as HTML if it @@ -52,8 +48,4 @@ class ApiFormatDump extends ApiFormatBase { public function getDescription() { return 'Output data in PHP\'s var_dump() format' . parent::getDescription(); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFormatJson.php b/includes/api/ApiFormatJson.php index acbc7d3b..abb63480 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -85,13 +85,9 @@ class ApiFormatJson extends ApiFormatBase { public function getDescription() { if ( $this->mIsRaw ) { - return 'Output data with the debuging elements in JSON format' . parent::getDescription(); + return 'Output data with the debugging elements in JSON format' . parent::getDescription(); } else { return 'Output data in JSON format' . parent::getDescription(); } } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFormatNone.php b/includes/api/ApiFormatNone.php new file mode 100644 index 00000000..78023af3 --- /dev/null +++ b/includes/api/ApiFormatNone.php @@ -0,0 +1,43 @@ +<?php +/** + * + * + * Created on Oct 22, 2006 + * + * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * API Serialized PHP output formatter + * @ingroup API + */ +class ApiFormatNone extends ApiFormatBase { + + public function getMimeType() { + return 'text/plain'; + } + + public function execute() { + } + + public function getDescription() { + return 'Output nothing' . parent::getDescription(); + } +} diff --git a/includes/api/ApiFormatPhp.php b/includes/api/ApiFormatPhp.php index fac2ca58..b2d1f044 100644 --- a/includes/api/ApiFormatPhp.php +++ b/includes/api/ApiFormatPhp.php @@ -30,10 +30,6 @@ */ class ApiFormatPhp extends ApiFormatBase { - public function __construct( $main, $format ) { - parent::__construct( $main, $format ); - } - public function getMimeType() { return 'application/vnd.php.serialized'; } @@ -45,8 +41,4 @@ class ApiFormatPhp extends ApiFormatBase { public function getDescription() { return 'Output data in serialized PHP format' . parent::getDescription(); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFormatRaw.php b/includes/api/ApiFormatRaw.php index 184f0a34..d278efa0 100644 --- a/includes/api/ApiFormatRaw.php +++ b/includes/api/ApiFormatRaw.php @@ -66,8 +66,4 @@ class ApiFormatRaw extends ApiFormatBase { } $this->printText( $data['text'] ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFormatTxt.php b/includes/api/ApiFormatTxt.php index 71414593..4130e70c 100644 --- a/includes/api/ApiFormatTxt.php +++ b/includes/api/ApiFormatTxt.php @@ -30,10 +30,6 @@ */ class ApiFormatTxt extends ApiFormatBase { - public function __construct( $main, $format ) { - parent::__construct( $main, $format ); - } - public function getMimeType() { // This looks like it should be text/plain, but IE7 is so // brain-damaged it tries to parse text/plain as HTML if it @@ -48,8 +44,4 @@ class ApiFormatTxt extends ApiFormatBase { public function getDescription() { return 'Output data in PHP\'s print_r() format' . parent::getDescription(); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php index 65056e44..62b69bb6 100644 --- a/includes/api/ApiFormatWddx.php +++ b/includes/api/ApiFormatWddx.php @@ -30,10 +30,6 @@ */ class ApiFormatWddx extends ApiFormatBase { - public function __construct( $main, $format ) { - parent::__construct( $main, $format ); - } - public function getMimeType() { return 'text/xml'; } @@ -112,8 +108,4 @@ class ApiFormatWddx extends ApiFormatBase { public function getDescription() { return 'Output data in WDDX format' . parent::getDescription(); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php index 5ccf1859..b4e8e330 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -36,10 +36,6 @@ class ApiFormatXml extends ApiFormatBase { private $mIncludeNamespace = false; private $mXslt = null; - public function __construct( $main, $format ) { - parent::__construct( $main, $format ); - } - public function getMimeType() { return 'text/xml'; } @@ -92,7 +88,7 @@ class ApiFormatXml extends ApiFormatBase { * * @par Example: * @verbatim - * name='root', value = array( '_element'=>'page', 'x', 'y', 'z') + * name='root', value = array( '_element'=>'page', 'x', 'y', 'z') * @endverbatim * creates: * @verbatim @@ -105,7 +101,7 @@ class ApiFormatXml extends ApiFormatBase { * * @par Example: * @verbatim - * name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10) + * name='root', value = array( '*'=>'text', 'lang'=>'en', 'id'=>10) * @endverbatim * creates: * @verbatim @@ -205,7 +201,13 @@ class ApiFormatXml extends ApiFormatBase { // ignore break; default: - $retval .= $indstr . Xml::element( $elemName, null, $elemValue ); + // to make sure null value doesn't produce unclosed element, + // which is what Xml::element( $elemName, null, null ) returns + if ( $elemValue === null ) { + $retval .= $indstr . Xml::element( $elemName ); + } else { + $retval .= $indstr . Xml::element( $elemName, null, $elemValue ); + } break; } return $retval; @@ -248,8 +250,4 @@ class ApiFormatXml extends ApiFormatBase { public function getDescription() { return 'Output data in XML format' . parent::getDescription(); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiFormatYaml.php b/includes/api/ApiFormatYaml.php index 730ad8ea..700d4a5e 100644 --- a/includes/api/ApiFormatYaml.php +++ b/includes/api/ApiFormatYaml.php @@ -35,10 +35,6 @@ class ApiFormatYaml extends ApiFormatJson { } public function getDescription() { - return 'Output data in YAML format' . parent::getDescription(); - } - - public function getVersion() { - return __CLASS__ . ': $Id$'; + return 'Output data in YAML format' . ApiFormatBase::getDescription(); } } diff --git a/includes/api/ApiHelp.php b/includes/api/ApiHelp.php index 2b5de21a..9cafc5bb 100644 --- a/includes/api/ApiHelp.php +++ b/includes/api/ApiHelp.php @@ -31,10 +31,6 @@ */ class ApiHelp extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - /** * Module for displaying help */ @@ -47,43 +43,62 @@ class ApiHelp extends ApiBase { } $this->getMain()->setHelp(); - $result = $this->getResult(); - $queryObj = new ApiQuery( $this->getMain(), 'query' ); - $r = array(); - if ( is_array( $params['modules'] ) ) { - $modArr = $this->getMain()->getModules(); - foreach ( $params['modules'] as $m ) { - if ( !isset( $modArr[$m] ) ) { - $r[] = array( 'name' => $m, 'missing' => '' ); - continue; - } - $module = new $modArr[$m]( $this->getMain(), $m ); - - $r[] = $this->buildModuleHelp( $module, 'action' ); - } + if ( is_array( $params['modules'] ) ) { + $modules = $params['modules']; + } else { + $modules = array(); } if ( is_array( $params['querymodules'] ) ) { - $qmodArr = $queryObj->getModules(); + $queryModules = $params['querymodules']; + foreach ( $queryModules as $m ) { + $modules[] = 'query+' . $m; + } + } else { + $queryModules = array(); + } - foreach ( $params['querymodules'] as $qm ) { - if ( !isset( $qmodArr[$qm] ) ) { - $r[] = array( 'name' => $qm, 'missing' => '' ); - continue; + $r = array(); + foreach ( $modules as $m ) { + // sub-modules could be given in the form of "name[+name[+name...]]" + $subNames = explode( '+', $m ); + if ( count( $subNames ) === 1 ) { + // In case the '+' was typed into URL, it resolves as a space + $subNames = explode( ' ', $m ); + } + $module = $this->getMain(); + for ( $i = 0; $i < count( $subNames ); $i++ ) { + $subs = $module->getModuleManager(); + if ( $subs === null ) { + $module = null; + } else { + $module = $subs->getModule( $subNames[$i] ); } - $module = new $qmodArr[$qm]( $this, $qm ); - $type = $queryObj->getModuleType( $qm ); - - if ( $type === null ) { - $r[] = array( 'name' => $qm, 'missing' => '' ); - continue; + if ( $module === null ) { + if ( count( $subNames ) === 2 + && $i === 1 + && $subNames[0] === 'query' + && in_array( $subNames[1], $queryModules ) + ) { + // Legacy: This is one of the renamed 'querymodule=...' parameters, + // do not use '+' notation in the output, use submodule's name instead. + $name = $subNames[1]; + } else { + $name = implode( '+', array_slice( $subNames, 0, $i + 1 ) ); + } + $r[] = array( 'name' => $name, 'missing' => '' ); + break; + } else { + $type = $subs->getModuleGroup( $subNames[$i] ); } - + } + if ( $module !== null ) { $r[] = $this->buildModuleHelp( $module, $type ); } } + $result->setIndexedTagName( $r, 'module' ); $result->addValue( null, $this->getModuleName(), $r ); } @@ -118,15 +133,16 @@ class ApiHelp extends ApiBase { ApiBase::PARAM_ISMULTI => true ), 'querymodules' => array( - ApiBase::PARAM_ISMULTI => true + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_DEPRECATED => true ), ); } public function getParamDescription() { return array( - 'modules' => 'List of module names (value of the action= parameter)', - 'querymodules' => 'List of query module names (value of prop=, meta= or list= parameter)', + 'modules' => 'List of module names (value of the action= parameter). Can specify submodules with a \'+\'', + 'querymodules' => 'Use modules=query+value instead. List of query module names (value of prop=, meta= or list= parameter)', ); } @@ -138,9 +154,8 @@ class ApiHelp extends ApiBase { return array( 'api.php?action=help' => 'Whole help page', 'api.php?action=help&modules=protect' => 'Module (action) help page', - 'api.php?action=help&querymodules=categorymembers' => 'Query (list) modules help page', - 'api.php?action=help&querymodules=info' => 'Query (prop) modules help page', - 'api.php?action=help&querymodules=siteinfo' => 'Query (meta) modules help page', + 'api.php?action=help&modules=query+categorymembers' => 'Help for the query/categorymembers module', + 'api.php?action=help&modules=login|query+info' => 'Help for the login and query/info modules', ); } @@ -151,8 +166,4 @@ class ApiHelp extends ApiBase { 'https://www.mediawiki.org/wiki/API:Quick_start_guide', ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiImageRotate.php b/includes/api/ApiImageRotate.php new file mode 100644 index 00000000..b2d75825 --- /dev/null +++ b/includes/api/ApiImageRotate.php @@ -0,0 +1,232 @@ +<?php +/** + * + * Created on January 3rd, 2013 + * + * 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 + */ + +class ApiImageRotate extends ApiBase { + + private $mPageSet = null; + + public function __construct( $main, $action ) { + parent::__construct( $main, $action ); + } + + /** + * Add all items from $values into the result + * @param array $result output + * @param array $values values to add + * @param string $flag the name of the boolean flag to mark this element + * @param string $name if given, name of the value + */ + private static function addValues( array &$result, $values, $flag = null, $name = null ) { + foreach ( $values as $val ) { + if( $val instanceof Title ) { + $v = array(); + ApiQueryBase::addTitleInfo( $v, $val ); + } elseif( $name !== null ) { + $v = array( $name => $val ); + } else { + $v = $val; + } + if( $flag !== null ) { + $v[$flag] = ''; + } + $result[] = $v; + } + } + + + public function execute() { + $params = $this->extractRequestParams(); + $rotation = $params[ 'rotation' ]; + $user = $this->getUser(); + + $pageSet = $this->getPageSet(); + $pageSet->execute(); + + $result = array(); + $result = array(); + + self::addValues( $result, $pageSet->getInvalidTitles(), 'invalid', 'title' ); + self::addValues( $result, $pageSet->getSpecialTitles(), 'special', 'title' ); + self::addValues( $result, $pageSet->getMissingPageIDs(), 'missing', 'pageid' ); + self::addValues( $result, $pageSet->getMissingRevisionIDs(), 'missing', 'revid' ); + self::addValues( $result, $pageSet->getInterwikiTitlesAsResult() ); + + foreach ( $pageSet->getTitles() as $title ) { + $r = array(); + $r['id'] = $title->getArticleID(); + ApiQueryBase::addTitleInfo( $r, $title ); + if ( !$title->exists() ) { + $r['missing'] = ''; + } + + $file = wfFindFile( $title ); + if ( !$file ) { + $r['result'] = 'Failure'; + $r['errormessage'] = 'File does not exist'; + $result[] = $r; + continue; + } + $handler = $file->getHandler(); + if ( !$handler || !$handler->canRotate() ) { + $r['result'] = 'Failure'; + $r['errormessage'] = 'File type cannot be rotated'; + $result[] = $r; + continue; + } + + // Check whether we're allowed to rotate this file + $permError = $this->checkPermissions( $this->getUser(), $file->getTitle() ); + if ( $permError !== null ) { + $r['result'] = 'Failure'; + $r['errormessage'] = $permError; + $result[] = $r; + continue; + } + + $srcPath = $file->getLocalRefPath(); + if ( $srcPath === false ) { + $r['result'] = 'Failure'; + $r['errormessage'] = 'Cannot get local file path'; + $result[] = $r; + continue; + } + $ext = strtolower( pathinfo( "$srcPath", PATHINFO_EXTENSION ) ); + $tmpFile = TempFSFile::factory( 'rotate_', $ext); + $dstPath = $tmpFile->getPath(); + $err = $handler->rotate( $file, array( + "srcPath" => $srcPath, + "dstPath" => $dstPath, + "rotation"=> $rotation + ) ); + if ( !$err ) { + $comment = wfMessage( 'rotate-comment' )->numParams( $rotation )->text(); + $status = $file->upload( $dstPath, + $comment, $comment, 0, false, false, $this->getUser() ); + if ( $status->isGood() ) { + $r['result'] = 'Success'; + } else { + $r['result'] = 'Failure'; + $r['errormessage'] = $this->getResult()->convertStatusToArray( $status ); + } + } else { + $r['result'] = 'Failure'; + $r['errormessage'] = $err->toText(); + } + $result[] = $r; + } + $apiResult = $this->getResult(); + $apiResult->setIndexedTagName( $result, 'page' ); + $apiResult->addValue( null, $this->getModuleName(), $result ); + } + + /** + * Get a cached instance of an ApiPageSet object + * @return ApiPageSet + */ + private function getPageSet() { + if ( $this->mPageSet === null ) { + $this->mPageSet = new ApiPageSet( $this, 0, NS_FILE ); + } + return $this->mPageSet; + } + + /** + * Checks that the user has permissions to perform rotations. + * @param $user User The user to check. + * @return string|null Permission error message, or null if there is no error + */ + protected function checkPermissions( $user, $title ) { + $permissionErrors = array_merge( + $title->getUserPermissionsErrors( 'edit' , $user ), + $title->getUserPermissionsErrors( 'upload' , $user ) + ); + + if ( $permissionErrors ) { + // Just return the first error + $msg = $this->parseMsg( $permissionErrors[0] ); + return $msg['info']; + } + + return null; + } + + public function mustBePosted() { + return true; + } + + public function isWriteMode() { + return true; + } + + public function getAllowedParams( $flags = 0 ) { + $pageSet = $this->getPageSet(); + $result = array( + 'rotation' => array( + ApiBase::PARAM_TYPE => array( '90', '180', '270' ), + ApiBase::PARAM_REQUIRED => true + ), + 'token' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true + ), + ); + if ( $flags ) { + $result += $this->getPageSet()->getFinalParams( $flags ); + } + return $result; + } + + public function getParamDescription() { + $pageSet = $this->getPageSet(); + return $pageSet->getParamDescription() + array( + 'rotation' => 'Degrees to rotate image clockwise', + 'token' => 'Edit token. You can get one of these through action=tokens', + ); + } + + public function getDescription() { + return 'Rotate one or more images'; + } + + public function needsToken() { + return true; + } + + public function getTokenSalt() { + return ''; + } + + public function getPossibleErrors() { + $pageSet = $this->getPageSet(); + return array_merge( + parent::getPossibleErrors(), + $pageSet->getPossibleErrors() + ); + } + + public function getExamples() { + return array( + 'api.php?action=imagerotate&titles=Example.jpg&rotation=90&token=123ABC', + ); + } +} diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php index 637c1fff..1f0a5fab 100644 --- a/includes/api/ApiImport.php +++ b/includes/api/ApiImport.php @@ -31,10 +31,6 @@ */ class ApiImport extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { $user = $this->getUser(); $params = $this->extractRequestParams(); @@ -109,7 +105,9 @@ class ApiImport extends ApiBase { ApiBase::PARAM_REQUIRED => true ), 'summary' => null, - 'xml' => null, + 'xml' => array( + ApiBase::PARAM_TYPE => 'upload', + ), 'interwikisource' => array( ApiBase::PARAM_TYPE => $wgImportSources ), @@ -150,7 +148,7 @@ class ApiImport extends ApiBase { public function getDescription() { return array( - 'Import a page from another wiki, or an XML file.' , + 'Import a page from another wiki, or an XML file.', 'Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when', 'sending a file for the "xml" parameter.' ); @@ -186,10 +184,6 @@ class ApiImport extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Import'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } /** diff --git a/includes/api/ApiLogin.php b/includes/api/ApiLogin.php index 1f91fe92..b936d3be 100644 --- a/includes/api/ApiLogin.php +++ b/includes/api/ApiLogin.php @@ -38,7 +38,7 @@ class ApiLogin extends ApiBase { /** * Executes the log-in attempt using the parameters passed. If - * the log-in succeeeds, it attaches a cookie to the session + * the log-in succeeds, it attaches a cookie to the session * and outputs the user id, username, and session token. If a * log-in fails, as the result of a bad password, a nonexistent * user, or any other reason, the host is cached with an expiry @@ -147,7 +147,7 @@ class ApiLogin extends ApiBase { case LoginForm::ABORTED: $result['result'] = 'Aborted'; - $result['reason'] = $loginForm->mAbortLoginErrorMsg; + $result['reason'] = $loginForm->mAbortLoginErrorMsg; break; default: @@ -278,8 +278,4 @@ class ApiLogin extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Login'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiLogout.php b/includes/api/ApiLogout.php index b2f634d0..2ba92a63 100644 --- a/includes/api/ApiLogout.php +++ b/includes/api/ApiLogout.php @@ -32,10 +32,6 @@ */ class ApiLogout extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { $user = $this->getUser(); $oldName = $user->getName(); @@ -75,8 +71,4 @@ class ApiLogout extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Logout'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php index 35febd95..80bca2f6 100644 --- a/includes/api/ApiMain.php +++ b/includes/api/ApiMain.php @@ -51,6 +51,7 @@ class ApiMain extends ApiBase { private static $Modules = array( 'login' => 'ApiLogin', 'logout' => 'ApiLogout', + 'createaccount' => 'ApiCreateAccount', 'query' => 'ApiQuery', 'expandtemplates' => 'ApiExpandTemplates', 'parse' => 'ApiParse', @@ -82,6 +83,7 @@ class ApiMain extends ApiBase { 'import' => 'ApiImport', 'userrights' => 'ApiUserrights', 'options' => 'ApiOptions', + 'imagerotate' =>'ApiImageRotate', ); /** @@ -105,6 +107,7 @@ class ApiMain extends ApiBase { 'dbgfm' => 'ApiFormatDbg', 'dump' => 'ApiFormatDump', 'dumpfm' => 'ApiFormatDump', + 'none' => 'ApiFormatNone', ); /** @@ -118,7 +121,7 @@ class ApiMain extends ApiBase { 'msg' => 'Use of the write API', 'params' => array() ), - 'apihighlimits' => array( + 'apihighlimits' => array( 'msg' => 'Use higher limits in API queries (Slow queries: $1 results; Fast queries: $2 results). The limits for slow queries also apply to multivalue parameters.', 'params' => array( ApiBase::LIMIT_SML2, ApiBase::LIMIT_BIG2 ) ) @@ -129,18 +132,20 @@ class ApiMain extends ApiBase { */ private $mPrinter; - private $mModules, $mModuleNames, $mFormats, $mFormatNames; - private $mResult, $mAction, $mShowVersions, $mEnableWrite; + private $mModuleMgr, $mResult; + private $mAction; + private $mEnableWrite; private $mInternalMode, $mSquidMaxage, $mModule; private $mCacheMode = 'private'; private $mCacheControl = array(); + private $mParamsUsed = array(); /** * Constructs an instance of ApiMain that utilizes the module and format specified by $request. * * @param $context IContextSource|WebRequest - if this is an instance of FauxRequest, errors are thrown and no printing occurs - * @param $enableWrite bool should be set to true if the api may modify data + * @param bool $enableWrite should be set to true if the api may modify data */ public function __construct( $context = null, $enableWrite = false ) { if ( $context === null ) { @@ -168,7 +173,7 @@ class ApiMain extends ApiBase { // Remove all modules other than login global $wgUser; - if ( $this->getRequest()->getVal( 'callback' ) !== null ) { + if ( $this->getVal( 'callback' ) !== null ) { // JSON callback allows cross-site reads. // For safety, strip user credentials. wfDebug( "API: stripping user credentials for JSON callback\n" ); @@ -177,15 +182,13 @@ class ApiMain extends ApiBase { } } - global $wgAPIModules; // extension modules - $this->mModules = $wgAPIModules + self::$Modules; - - $this->mModuleNames = array_keys( $this->mModules ); - $this->mFormats = self::$Formats; - $this->mFormatNames = array_keys( $this->mFormats ); + global $wgAPIModules; + $this->mModuleMgr = new ApiModuleManager( $this ); + $this->mModuleMgr->addModules( self::$Modules, 'action' ); + $this->mModuleMgr->addModules( $wgAPIModules, 'action' ); + $this->mModuleMgr->addModules( self::$Formats, 'format' ); $this->mResult = new ApiResult( $this ); - $this->mShowVersions = false; $this->mEnableWrite = $enableWrite; $this->mSquidMaxage = - 1; // flag for executeActionWithErrorHandling() @@ -242,7 +245,7 @@ class ApiMain extends ApiBase { /** * Set the type of caching headers which will be sent. * - * @param $mode String One of: + * @param string $mode One of: * - 'public': Cache this object in public caches, if the maxage or smaxage * parameter is set, or if setCacheMaxAge() was called. If a maximum age is * not provided by any of these means, the object will be private. @@ -271,7 +274,7 @@ class ApiMain extends ApiBase { return; } - if ( !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) ) { + if ( !User::groupHasPermission( '*', 'read' ) ) { // Private wiki, only private headers if ( $mode !== 'private' ) { wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" ); @@ -330,10 +333,11 @@ class ApiMain extends ApiBase { * @return ApiFormatBase */ public function createPrinterByName( $format ) { - if ( !isset( $this->mFormats[$format] ) ) { + $printer = $this->mModuleMgr->getModule( $format, 'format' ); + if ( $printer === null ) { $this->dieUsage( "Unrecognized format: {$format}", 'unknown_format' ); } - return new $this->mFormats[$format] ( $this, $format ); + return $printer; } /** @@ -361,10 +365,17 @@ class ApiMain extends ApiBase { return; } + // Exit here if the request method was OPTIONS + // (assume there will be a followup GET or POST) + if ( $this->getRequest()->getMethod() === 'OPTIONS' ) { + return; + } + // In case an error occurs during data output, // clear the output buffer and print just the error information ob_start(); + $t = microtime( true ); try { $this->executeAction(); } catch ( Exception $e ) { @@ -372,11 +383,16 @@ class ApiMain extends ApiBase { wfRunHooks( 'ApiMain::onException', array( $this, $e ) ); // Log it - if ( !( $e instanceof UsageException ) ) { - wfDebugLog( 'exception', $e->getLogMessage() ); + if ( $e instanceof MWException && !( $e instanceof UsageException ) ) { + global $wgLogExceptionBacktrace; + if ( $wgLogExceptionBacktrace ) { + wfDebugLog( 'exception', $e->getLogMessage() . "\n" . $e->getTraceAsString() . "\n" ); + } else { + wfDebugLog( 'exception', $e->getLogMessage() ); + } } - // Handle any kind of exception by outputing properly formatted error message. + // Handle any kind of exception by outputting properly formatted error message. // If this fails, an unhandled exception should be thrown so that global error // handler will process and log it. @@ -401,6 +417,9 @@ class ApiMain extends ApiBase { $this->printResult( true ); } + // Log the request whether or not there was an error + $this->logRequest( microtime( true ) - $t); + // Send cache headers after any code which might generate an error, to // avoid sending public cache headers for errors. $this->sendCacheHeaders(); @@ -461,9 +480,9 @@ class ApiMain extends ApiBase { /** * 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 + * @param string $value Origin header + * @param array $rules Set of wildcard rules + * @param array $exceptions Set of wildcard rules * @return bool True if $value matches a rule in $rules and doesn't match any rules in $exceptions, false otherwise */ protected static function matchOrigin( $value, $rules, $exceptions ) { @@ -486,7 +505,7 @@ class ApiMain extends ApiBase { * '*' => '.*?' * '?' => '.' * - * @param $wildcard string String with wildcards + * @param string $wildcard String with wildcards * @return string Regular expression */ protected static function wildcardToRegex( $wildcard ) { @@ -593,7 +612,7 @@ class ApiMain extends ApiBase { if ( !isset ( $this->mPrinter ) ) { // The printer has not been created yet. Try to manually get formatter value. $value = $this->getRequest()->getVal( 'format', self::API_DEFAULT_FORMAT ); - if ( !in_array( $value, $this->mFormatNames ) ) { + if ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) { $value = self::API_DEFAULT_FORMAT; } @@ -611,7 +630,6 @@ class ApiMain extends ApiBase { if ( $this->mPrinter->getWantsHelp() || $this->mAction == 'help' ) { ApiResult::setContent( $errMessage, $this->makeHelpMsg() ); } - } else { global $wgShowSQLErrors, $wgShowExceptionDetails; // Something is seriously wrong @@ -628,6 +646,10 @@ class ApiMain extends ApiBase { ApiResult::setContent( $errMessage, $wgShowExceptionDetails ? "\n\n{$e->getTraceAsString()}\n\n" : '' ); } + // Remember all the warnings to re-add them later + $oldResult = $result->getData(); + $warnings = isset( $oldResult['warnings'] ) ? $oldResult['warnings'] : null; + $result->reset(); $result->disableSizeCheck(); // Re-add the id @@ -635,11 +657,13 @@ class ApiMain extends ApiBase { if ( !is_null( $requestid ) ) { $result->addValue( null, 'requestid', $requestid ); } - if ( $wgShowHostnames ) { // servedby is especially useful when debugging errors $result->addValue( null, 'servedby', wfHostName() ); } + if ( $warnings !== null ) { + $result->addValue( null, 'warnings', $warnings ); + } $result->addValue( null, 'error', $errMessage ); @@ -669,7 +693,6 @@ class ApiMain extends ApiBase { $params = $this->extractRequestParams(); - $this->mShowVersions = $params['version']; $this->mAction = $params['action']; if ( !is_string( $this->mAction ) ) { @@ -685,9 +708,10 @@ class ApiMain extends ApiBase { */ protected function setupModule() { // Instantiate the module requested by the user - $module = new $this->mModules[$this->mAction] ( $this, $this->mAction ); - $this->mModule = $module; - + $module = $this->mModuleMgr->getModule( $this->mAction, 'action' ); + if ( $module === null ) { + $this->dieUsage( 'The API requires a valid action parameter', 'unknown_action' ); + } $moduleParams = $module->extractRequestParams(); // Die if token required, but not provided (unless there is a gettoken parameter) @@ -713,7 +737,7 @@ class ApiMain extends ApiBase { /** * Check the max lag if necessary * @param $module ApiBase object: Api module being used - * @param $params Array an array containing the request parameters. + * @param array $params an array containing the request parameters. * @return boolean True on success, false should exit immediately */ protected function checkMaxLag( $module, $params ) { @@ -745,7 +769,7 @@ class ApiMain extends ApiBase { */ protected function checkExecutePermissions( $module ) { $user = $this->getUser(); - if ( $module->isReadMode() && !in_array( 'read', User::getGroupPermissions( array( '*' ) ), true ) && + if ( $module->isReadMode() && !User::groupHasPermission( '*', 'read' ) && !$user->isAllowed( 'read' ) ) { $this->dieUsageMsg( 'readrequired' ); @@ -772,12 +796,13 @@ class ApiMain extends ApiBase { /** * Check POST for external response and setup result printer * @param $module ApiBase An Api module - * @param $params Array an array with the request parameters + * @param array $params an array with the request parameters */ protected function setupExternalResponse( $module, $params ) { - // Ignore mustBePosted() for internal calls - if ( $module->mustBePosted() && !$this->getRequest()->wasPosted() ) { - $this->dieUsageMsg( array( 'mustbeposted', $this->mAction ) ); + if ( !$this->getRequest()->wasPosted() && $module->mustBePosted() ) { + // Module requires POST. GET request might still be allowed + // if $wgDebugApi is true, otherwise fail. + $this->dieUsageMsgOrDebug( array( 'mustbeposted', $this->mAction ) ); } // See if custom printer is used @@ -798,6 +823,7 @@ class ApiMain extends ApiBase { protected function executeAction() { $params = $this->setupExecuteAction(); $module = $this->setupModule(); + $this->mModule = $module; $this->checkExecutePermissions( $module ); @@ -815,6 +841,8 @@ class ApiMain extends ApiBase { wfRunHooks( 'APIAfterExecute', array( &$module ) ); $module->profileOut(); + $this->reportUnusedParams(); + if ( !$this->mInternalMode ) { //append Debug information MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() ); @@ -825,11 +853,120 @@ class ApiMain extends ApiBase { } /** + * Log the preceding request + * @param $time Time in seconds + */ + protected function logRequest( $time ) { + $request = $this->getRequest(); + $milliseconds = $time === null ? '?' : round( $time * 1000 ); + $s = 'API' . + ' ' . $request->getMethod() . + ' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) . + ' ' . $request->getIP() . + ' T=' . $milliseconds .'ms'; + foreach ( $this->getParamsUsed() as $name ) { + $value = $request->getVal( $name ); + if ( $value === null ) { + continue; + } + $s .= ' ' . $name . '='; + if ( strlen( $value ) > 256 ) { + $encValue = $this->encodeRequestLogValue( substr( $value, 0, 256 ) ); + $s .= $encValue . '[...]'; + } else { + $s .= $this->encodeRequestLogValue( $value ); + } + } + $s .= "\n"; + wfDebugLog( 'api', $s, false ); + } + + /** + * Encode a value in a format suitable for a space-separated log line. + */ + protected function encodeRequestLogValue( $s ) { + static $table; + if ( !$table ) { + $chars = ';@$!*(),/:'; + for ( $i = 0; $i < strlen( $chars ); $i++ ) { + $table[rawurlencode( $chars[$i] )] = $chars[$i]; + } + } + return strtr( rawurlencode( $s ), $table ); + } + + /** + * Get the request parameters used in the course of the preceding execute() request + */ + protected function getParamsUsed() { + return array_keys( $this->mParamsUsed ); + } + + /** + * Get a request value, and register the fact that it was used, for logging. + */ + public function getVal( $name, $default = null ) { + $this->mParamsUsed[$name] = true; + return $this->getRequest()->getVal( $name, $default ); + } + + /** + * Get a boolean request value, and register the fact that the parameter + * was used, for logging. + */ + public function getCheck( $name ) { + $this->mParamsUsed[$name] = true; + return $this->getRequest()->getCheck( $name ); + } + + /** + * Get a request upload, and register the fact that it was used, for logging. + * + * @since 1.21 + * @param string $name Parameter name + * @return WebRequestUpload + */ + public function getUpload( $name ) { + $this->mParamsUsed[$name] = true; + return $this->getRequest()->getUpload( $name ); + } + + /** + * Report unused parameters, so the client gets a hint in case it gave us parameters we don't know, + * for example in case of spelling mistakes or a missing 'g' prefix for generators. + */ + protected function reportUnusedParams() { + $paramsUsed = $this->getParamsUsed(); + $allParams = $this->getRequest()->getValueNames(); + + if ( !$this->mInternalMode ) { + // Printer has not yet executed; don't warn that its parameters are unused + $printerParams = array_map( + array( $this->mPrinter, 'encodeParamName' ), + array_keys( $this->mPrinter->getFinalParams() ?: array() ) + ); + $unusedParams = array_diff( $allParams, $paramsUsed, $printerParams ); + } else { + $unusedParams = array_diff( $allParams, $paramsUsed ); + } + + if( count( $unusedParams ) ) { + $s = count( $unusedParams ) > 1 ? 's' : ''; + $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" ); + } + } + + /** * Print results using the current printer * * @param $isError bool */ protected function printResult( $isError ) { + global $wgDebugAPI; + if( $wgDebugAPI !== false ) { + $this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' ); + } + $this->getResult()->cleanUpUTF8(); $printer = $this->mPrinter; $printer->profileIn(); @@ -839,10 +976,10 @@ class ApiMain extends ApiBase { * tell the printer not to escape ampersands so that our links do * not break. */ - $printer->setUnescapeAmps( ( $this->mAction == 'help' || $isError ) - && $printer->getFormat() == 'XML' && $printer->getIsHtml() ); + $isHelp = $isError || $this->mAction == 'help'; + $printer->setUnescapeAmps( $isHelp && $printer->getFormat() == 'XML' && $printer->getIsHtml() ); - $printer->initPrinter( $isError ); + $printer->initPrinter( $isHelp ); $printer->execute(); $printer->closePrinter(); @@ -865,13 +1002,12 @@ class ApiMain extends ApiBase { return array( 'format' => array( ApiBase::PARAM_DFLT => ApiMain::API_DEFAULT_FORMAT, - ApiBase::PARAM_TYPE => $this->mFormatNames + ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'format' ) ), 'action' => array( ApiBase::PARAM_DFLT => 'help', - ApiBase::PARAM_TYPE => $this->mModuleNames + ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'action' ) ), - 'version' => false, 'maxlag' => array( ApiBase::PARAM_TYPE => 'integer' ), @@ -898,12 +1034,11 @@ class ApiMain extends ApiBase { return array( 'format' => 'The format of the output', 'action' => 'What action you would like to perform. See below for module help', - 'version' => 'When showing help, include version for each module', 'maxlag' => array( 'Maximum lag can be used when MediaWiki is installed on a database replicated cluster.', 'To save actions causing any more site replication lag, this parameter can make the client', 'wait until the replication lag is less than the specified value.', - 'In case of a replag error, a HTTP 503 error is returned, with the message like', + 'In case of a replag error, error code "maxlag" is returned, with the message like', '"Waiting for $host: $lag seconds lagged\n".', 'See https://www.mediawiki.org/wiki/Manual:Maxlag_parameter for more information', ), @@ -984,11 +1119,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-2009)', ' 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, 2012-present)', '', 'Please send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org', 'or file a bug report at https://bugzilla.wikimedia.org/' @@ -1014,8 +1149,7 @@ class ApiMain extends ApiBase { $this->setHelp(); // Get help text from cache if present $key = wfMemcKey( 'apihelp', $this->getModuleName(), - SpecialVersion::getVersion( 'nodb' ) . - $this->getShowVersions() ); + SpecialVersion::getVersion( 'nodb' ) ); if ( $wgAPICacheHelpTimeout > 0 ) { $cached = $wgMemc->get( $key ); if ( $cached ) { @@ -1040,9 +1174,11 @@ class ApiMain extends ApiBase { $astriks = str_repeat( '*** ', 14 ); $msg .= "\n\n$astriks Modules $astriks\n\n"; - foreach ( array_keys( $this->mModules ) as $moduleName ) { - $module = new $this->mModules[$moduleName] ( $this, $moduleName ); + + foreach ( $this->mModuleMgr->getNames( 'action' ) as $name ) { + $module = $this->mModuleMgr->getModule( $name ); $msg .= self::makeHelpMsgHeader( $module, 'action' ); + $msg2 = $module->makeHelpMsg(); if ( $msg2 !== false ) { $msg .= $msg2; @@ -1053,14 +1189,13 @@ class ApiMain extends ApiBase { $msg .= "\n$astriks Permissions $astriks\n\n"; foreach ( self::$mRights as $right => $rightMsg ) { $groups = User::getGroupsWithPermission( $right ); - $msg .= "* " . $right . " *\n " . wfMsgReplaceArgs( $rightMsg[ 'msg' ], $rightMsg[ 'params' ] ) . + $msg .= "* " . $right . " *\n " . wfMsgReplaceArgs( $rightMsg['msg'], $rightMsg['params'] ) . "\nGranted to:\n " . str_replace( '*', 'all', implode( ', ', $groups ) ) . "\n\n"; - } $msg .= "\n$astriks Formats $astriks\n\n"; - foreach ( array_keys( $this->mFormats ) as $formatName ) { - $module = $this->createPrinterByName( $formatName ); + foreach ( $this->mModuleMgr->getNames( 'format' ) as $name ) { + $module = $this->mModuleMgr->getModule( $name ); $msg .= self::makeHelpMsgHeader( $module, 'format' ); $msg2 = $module->makeHelpMsg(); if ( $msg2 !== false ) { @@ -1076,7 +1211,7 @@ class ApiMain extends ApiBase { /** * @param $module ApiBase - * @param $paramName String What type of request is this? e.g. action, query, list, prop, meta, format + * @param string $paramName What type of request is this? e.g. action, query, list, prop, meta, format * @return string */ public static function makeHelpMsgHeader( $module, $paramName ) { @@ -1105,25 +1240,19 @@ class ApiMain extends ApiBase { /** * Check whether the user wants us to show version information in the API help * @return bool + * @deprecated since 1.21, always returns false */ public function getShowVersions() { - return $this->mShowVersions; + wfDeprecated( __METHOD__, '1.21' ); + return false; } /** - * Returns the version information of this file, plus it includes - * the versions for all files that are not callable proper API modules - * - * @return array + * Overrides to return this instance's module manager. + * @return ApiModuleManager */ - public function getVersion() { - $vers = array(); - $vers[] = 'MediaWiki: ' . SpecialVersion::getVersion() . "\n https://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/"; - $vers[] = __CLASS__ . ': $Id$'; - $vers[] = ApiBase::getBaseVersion(); - $vers[] = ApiFormatBase::getBaseVersion(); - $vers[] = ApiQueryBase::getBaseVersion(); - return $vers; + public function getModuleManager() { + return $this->mModuleMgr; } /** @@ -1131,40 +1260,44 @@ class ApiMain extends ApiBase { * classes who wish to add their own modules to their lexicon or override the * behavior of inherent ones. * - * @param $mdlName String The identifier for this module. - * @param $mdlClass String The class where this module is implemented. + * @deprecated since 1.21, Use getModuleManager()->addModule() instead. + * @param string $name The identifier for this module. + * @param $class ApiBase The class where this module is implemented. */ - protected function addModule( $mdlName, $mdlClass ) { - $this->mModules[$mdlName] = $mdlClass; + protected function addModule( $name, $class ) { + $this->getModuleManager()->addModule( $name, 'action', $class ); } /** * Add or overwrite an output format for this ApiMain. Intended for use by extending * classes who wish to add to or modify current formatters. * - * @param $fmtName string The identifier for this format. - * @param $fmtClass ApiFormatBase The class implementing this format. + * @deprecated since 1.21, Use getModuleManager()->addModule() instead. + * @param string $name The identifier for this format. + * @param $class ApiFormatBase The class implementing this format. */ - protected function addFormat( $fmtName, $fmtClass ) { - $this->mFormats[$fmtName] = $fmtClass; + protected function addFormat( $name, $class ) { + $this->getModuleManager()->addModule( $name, 'format', $class ); } /** * Get the array mapping module names to class names + * @deprecated since 1.21, Use getModuleManager()'s methods instead. * @return array */ function getModules() { - return $this->mModules; + return $this->getModuleManager()->getNamesWithClasses( 'action' ); } /** * Returns the list of supported formats in form ( 'format' => 'ClassName' ) * * @since 1.18 + * @deprecated since 1.21, Use getModuleManager()'s methods instead. * @return array */ public function getFormats() { - return $this->mFormats; + return $this->getModuleManager()->getNamesWithClasses( 'format' ); } } diff --git a/includes/api/ApiModuleManager.php b/includes/api/ApiModuleManager.php new file mode 100644 index 00000000..100392bf --- /dev/null +++ b/includes/api/ApiModuleManager.php @@ -0,0 +1,171 @@ +<?php +/** + * + * + * Created on Dec 27, 2012 + * + * Copyright © 2012 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.21 + */ + +/** + * This class holds a list of modules and handles instantiation + * + * @since 1.21 + * @ingroup API + */ +class ApiModuleManager extends ContextSource { + + private $mParent; + private $mInstances = array(); + private $mGroups = array(); + private $mModules = array(); + + /** + * Construct new module manager + * @param ApiBase $parentModule Parent module instance will be used during instantiation + */ + public function __construct( ApiBase $parentModule ) { + $this->mParent = $parentModule; + } + + /** + * Add a list of modules to the manager + * @param array $modules A map of ModuleName => ModuleClass + * @param string $group Which group modules belong to (action,format,...) + */ + public function addModules( array $modules, $group ) { + foreach ( $modules as $name => $class ) { + $this->addModule( $name, $group, $class ); + } + } + + /** + * Add or overwrite a module in this ApiMain instance. Intended for use by extending + * classes who wish to add their own modules to their lexicon or override the + * behavior of inherent ones. + * + * @param string $group Name of the module group + * @param string $name The identifier for this module. + * @param string $class The class where this module is implemented. + */ + public function addModule( $name, $group, $class ) { + $this->mGroups[$group] = null; + $this->mModules[$name] = array( $group, $class ); + } + + /** + * Get module instance by name, or instantiate it if it does not exist + * @param string $moduleName module name + * @param string $group optionally validate that the module is in a specific group + * @param bool $ignoreCache if true, force-creates a new instance and does not cache it + * @return mixed the new module instance, or null if failed + */ + public function getModule( $moduleName, $group = null, $ignoreCache = false ) { + if ( !isset( $this->mModules[$moduleName] ) ) { + return null; + } + $grpCls = $this->mModules[$moduleName]; + if ( $group !== null && $grpCls[0] !== $group ) { + return null; + } + if ( !$ignoreCache && isset( $this->mInstances[$moduleName] ) ) { + // already exists + return $this->mInstances[$moduleName]; + } else { + // new instance + $class = $grpCls[1]; + $instance = new $class ( $this->mParent, $moduleName ); + if ( !$ignoreCache ) { + // cache this instance in case it is needed later + $this->mInstances[$moduleName] = $instance; + } + return $instance; + } + } + + /** + * Get an array of modules in a specific group or all if no group is set. + * @param string $group optional group filter + * @return array list of module names + */ + public function getNames( $group = null ) { + if ( $group === null ) { + return array_keys( $this->mModules ); + } + $result = array(); + foreach ( $this->mModules as $name => $grpCls ) { + if ( $grpCls[0] === $group ) { + $result[] = $name; + } + } + return $result; + } + + /** + * Create an array of (moduleName => moduleClass) for a specific group or for all. + * @param string $group name of the group to get or null for all + * @return array name=>class map + */ + public function getNamesWithClasses( $group = null ) { + $result = array(); + foreach ( $this->mModules as $name => $grpCls ) { + if ( $group === null || $grpCls[0] === $group ) { + $result[$name] = $grpCls[1]; + } + } + return $result; + } + + /** + * Returns true if the specific module is defined at all or in a specific group. + * @param string $moduleName module name + * @param string $group group name to check against, or null to check all groups, + * @return boolean true if defined + */ + public function isDefined( $moduleName, $group = null ) { + if ( isset( $this->mModules[$moduleName] ) ) { + return $group === null || $this->mModules[$moduleName][0] === $group; + } else { + return false; + } + } + + /** + * Returns the group name for the given module + * @param string $moduleName + * @return string group name or null if missing + */ + public function getModuleGroup( $moduleName ) { + if ( isset( $this->mModules[$moduleName] ) ) { + return $this->mModules[$moduleName][0]; + } else { + return null; + } + } + + /** + * Get a list of groups this manager contains. + * @return array + */ + public function getGroups() { + return array_keys( $this->mGroups ); + } +} diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php index 9d73562b..3e846e3b 100644 --- a/includes/api/ApiMove.php +++ b/includes/api/ApiMove.php @@ -30,10 +30,6 @@ */ class ApiMove extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { $user = $this->getUser(); $params = $this->extractRequestParams(); @@ -42,7 +38,7 @@ class ApiMove extends ApiBase { if ( isset( $params['from'] ) ) { $fromTitle = Title::newFromText( $params['from'] ); - if ( !$fromTitle ) { + if ( !$fromTitle || $fromTitle->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $params['from'] ) ); } } elseif ( isset( $params['fromid'] ) ) { @@ -58,7 +54,7 @@ class ApiMove extends ApiBase { $fromTalk = $fromTitle->getTalkPage(); $toTitle = Title::newFromText( $params['to'] ); - if ( !$toTitle ) { + if ( !$toTitle || $toTitle->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $params['to'] ) ); } $toTalk = $toTitle->getTalkPage(); @@ -82,9 +78,16 @@ class ApiMove extends ApiBase { } $r = array( 'from' => $fromTitle->getPrefixedText(), 'to' => $toTitle->getPrefixedText(), 'reason' => $params['reason'] ); - if ( !$params['noredirect'] || !$user->isAllowed( 'suppressredirect' ) ) { + + if ( $fromTitle->exists() ) { + //NOTE: we assume that if the old title exists, it's because it was re-created as + // a redirect to the new title. This is not safe, but what we did before was + // even worse: we just determined whether a redirect should have been created, + // and reported that it was created if it should have, without any checks. + // Also note that isRedirect() is unreliable because of bug 37209. $r['redirectcreated'] = ''; } + if( $toTitleExists ) { $r['moveoverredirect'] = ''; } @@ -122,7 +125,7 @@ class ApiMove extends ApiBase { } } - $watch = "preferences"; + $watch = 'preferences'; if ( isset( $params['watchlist'] ) ) { $watch = $params['watchlist']; } elseif ( $params['watch'] ) { @@ -288,15 +291,11 @@ class ApiMove extends ApiBase { public function getExamples() { return array( - 'api.php?action=move&from=Exampel&to=Example&token=123ABC&reason=Misspelled%20title&movetalk=&noredirect=' + 'api.php?action=move&from=Badtitle&to=Goodtitle&token=123ABC&reason=Misspelled%20title&movetalk=&noredirect=' ); } public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Move'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php index ef562741..caf361ac 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -29,10 +29,6 @@ */ class ApiOpenSearch extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function getCustomPrinter() { return $this->getMain()->createPrinterByName( 'json' ); } @@ -123,8 +119,4 @@ class ApiOpenSearch extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Opensearch'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php index 265c2ccb..8c996a26 100644 --- a/includes/api/ApiOptions.php +++ b/includes/api/ApiOptions.php @@ -25,17 +25,13 @@ */ /** -* API module that facilitates the changing of user's preferences. -* Requires API write mode to be enabled. -* + * 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. */ @@ -54,7 +50,7 @@ class ApiOptions extends ApiBase { } if ( $params['reset'] ) { - $user->resetOptions(); + $user->resetOptions( $params['resetkinds'] ); $changed = true; } @@ -74,13 +70,36 @@ class ApiOptions extends ApiBase { } $prefs = Preferences::getPreferences( $user, $this->getContext() ); + $prefsKinds = $user->getOptionKinds( $this->getContext(), $changes ); + foreach ( $changes as $key => $value ) { - if ( !isset( $prefs[$key] ) ) { - $this->setWarning( "Not a valid preference: $key" ); - continue; + switch ( $prefsKinds[$key] ) { + case 'registered': + // Regular option. + $field = HTMLForm::loadInputFromParameters( $key, $prefs[$key] ); + $validation = $field->validate( $value, $user->getOptions() ); + break; + case 'registered-multiselect': + case 'registered-checkmatrix': + // A key for a multiselect or checkmatrix option. + $validation = true; + $value = $value !== null ? (bool) $value : null; + break; + case 'userjs': + // Allow non-default preferences prefixed with 'userjs-', to be set by user scripts + if ( strlen( $key ) > 255 ) { + $validation = "key too long (no more than 255 bytes allowed)"; + } elseif ( preg_match( "/[^a-zA-Z0-9_-]/", $key ) !== 0 ) { + $validation = "invalid key (only a-z, A-Z, 0-9, _, - allowed)"; + } else { + $validation = true; + } + break; + case 'unused': + default: + $validation = "not a valid preference"; + break; } - $field = HTMLForm::loadInputFromParameters( $key, $prefs[$key] ); - $validation = $field->validate( $value, $user->getOptions() ); if ( $validation === true ) { $user->setOption( $key, $value ); $changed = true; @@ -106,12 +125,20 @@ class ApiOptions extends ApiBase { } public function getAllowedParams() { + $optionKinds = User::listOptionKinds(); + $optionKinds[] = 'all'; + return array( 'token' => array( ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), 'reset' => false, + 'resetkinds' => array( + ApiBase::PARAM_TYPE => $optionKinds, + ApiBase::PARAM_DFLT => 'all', + ApiBase::PARAM_ISMULTI => true + ), 'change' => array( ApiBase::PARAM_ISMULTI => true, ), @@ -139,15 +166,20 @@ class ApiOptions extends ApiBase { 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', + 'reset' => 'Resets preferences to the site defaults', + 'resetkinds' => 'List of types of options to reset when the "reset" option is set', + 'change' => 'List of changes, formatted name=value (e.g. skin=vector), value cannot contain pipe characters. If no value is given (not even an equals sign), e.g., optionname|otheroption|..., the option will be reset to its default value', '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'; + return array( + 'Change preferences of the current user', + 'Only options which are registered in core or in one of installed extensions,', + 'or as options with keys prefixed with \'userjs-\' (intended to be used by user scripts), can be set.' + ); } public function getPossibleErrors() { @@ -176,8 +208,4 @@ class ApiOptions extends ApiBase { '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 0f5be6b2..074efe4b 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, 2013 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 @@ -36,52 +36,177 @@ * the second instance for all their work. * * @ingroup API + * @since 1.21 derives from ApiBase instead of ApiQueryBase */ -class ApiPageSet extends ApiQueryBase { +class ApiPageSet extends ApiBase { - private $mAllPages; // [ns][dbkey] => page_id or negative when missing - private $mTitles, $mGoodTitles, $mMissingTitles, $mInvalidTitles; - private $mMissingPageIDs, $mRedirectTitles, $mSpecialTitles; - private $mNormalizedTitles, $mInterwikiTitles; - private $mResolveRedirects, $mPendingRedirectIDs; - private $mConvertTitles, $mConvertedTitles; - private $mGoodRevIDs, $mMissingRevIDs; - private $mFakePageId; - - private $mRequestedPageFields; + /** + * Constructor flag: The new instance of ApiPageSet will ignore the 'generator=' parameter + * @since 1.21 + */ + const DISABLE_GENERATORS = 1; + + private $mDbSource; + private $mParams; + private $mResolveRedirects; + private $mConvertTitles; + private $mAllowGenerator; + + private $mAllPages = array(); // [ns][dbkey] => page_id or negative when missing + private $mTitles = array(); + private $mGoodTitles = array(); + private $mMissingTitles = array(); + private $mInvalidTitles = array(); + private $mMissingPageIDs = array(); + private $mRedirectTitles = array(); + private $mSpecialTitles = array(); + private $mNormalizedTitles = array(); + private $mInterwikiTitles = array(); + private $mPendingRedirectIDs = array(); + private $mConvertedTitles = array(); + private $mGoodRevIDs = array(); + private $mMissingRevIDs = array(); + private $mFakePageId = -1; + private $mCacheMode = 'public'; + private $mRequestedPageFields = array(); + private $mDefaultNamespace = NS_MAIN; /** * Constructor - * @param $query ApiBase - * @param $resolveRedirects bool Whether redirects should be resolved - * @param $convertTitles bool + * @param $dbSource ApiBase Module implementing getDB(). + * Allows PageSet to reuse existing db connection from the shared state like ApiQuery. + * @param int $flags Zero or more flags like DISABLE_GENERATORS + * @param int $defaultNamespace the namespace to use if none is specified by a prefix. + * @since 1.21 accepts $flags instead of two boolean values */ - public function __construct( $query, $resolveRedirects = false, $convertTitles = false ) { - parent::__construct( $query, 'query' ); + public function __construct( ApiBase $dbSource, $flags = 0, $defaultNamespace = NS_MAIN ) { + parent::__construct( $dbSource->getMain(), $dbSource->getModuleName() ); + $this->mDbSource = $dbSource; + $this->mAllowGenerator = ( $flags & ApiPageSet::DISABLE_GENERATORS ) == 0; + $this->mDefaultNamespace = $defaultNamespace; - $this->mAllPages = array(); - $this->mTitles = array(); - $this->mGoodTitles = array(); - $this->mMissingTitles = array(); - $this->mInvalidTitles = array(); - $this->mMissingPageIDs = array(); - $this->mRedirectTitles = array(); - $this->mNormalizedTitles = array(); - $this->mInterwikiTitles = array(); - $this->mGoodRevIDs = array(); - $this->mMissingRevIDs = array(); - $this->mSpecialTitles = array(); + $this->profileIn(); + $this->mParams = $this->extractRequestParams(); + $this->mResolveRedirects = $this->mParams['redirects']; + $this->mConvertTitles = $this->mParams['converttitles']; + $this->profileOut(); + } - $this->mRequestedPageFields = array(); - $this->mResolveRedirects = $resolveRedirects; - if ( $resolveRedirects ) { - $this->mPendingRedirectIDs = array(); - } + /** + * In case execute() is not called, call this method to mark all relevant parameters as used + * This prevents unused parameters from being reported as warnings + */ + public function executeDryRun() { + $this->executeInternal( true ); + } - $this->mConvertTitles = $convertTitles; - $this->mConvertedTitles = array(); + /** + * Populate the PageSet from the request parameters. + */ + public function execute() { + $this->executeInternal( false ); + } + + /** + * Populate the PageSet from the request parameters. + * @param bool $isDryRun If true, instantiates generator, but only to mark relevant parameters as used + */ + private function executeInternal( $isDryRun ) { + $this->profileIn(); - $this->mFakePageId = - 1; + $generatorName = $this->mAllowGenerator ? $this->mParams['generator'] : null; + if ( isset( $generatorName ) ) { + $dbSource = $this->mDbSource; + $isQuery = $dbSource instanceof ApiQuery; + if ( !$isQuery ) { + // If the parent container of this pageset is not ApiQuery, we must create it to run generator + $dbSource = $this->getMain()->getModuleManager()->getModule( 'query' ); + // Enable profiling for query module because it will be used for db sql profiling + $dbSource->profileIn(); + } + $generator = $dbSource->getModuleManager()->getModule( $generatorName, null, true ); + if ( $generator === null ) { + $this->dieUsage( 'Unknown generator=' . $generatorName, 'badgenerator' ); + } + if ( !$generator instanceof ApiQueryGeneratorBase ) { + $this->dieUsage( "Module $generatorName cannot be used as a generator", 'badgenerator' ); + } + // Create a temporary pageset to store generator's output, + // add any additional fields generator may need, and execute pageset to populate titles/pageids + $tmpPageSet = new ApiPageSet( $dbSource, ApiPageSet::DISABLE_GENERATORS ); + $generator->setGeneratorMode( $tmpPageSet ); + $this->mCacheMode = $generator->getCacheMode( $generator->extractRequestParams() ); + + if ( !$isDryRun ) { + $generator->requestExtraData( $tmpPageSet ); + } + $tmpPageSet->executeInternal( $isDryRun ); + + // populate this pageset with the generator output + $this->profileOut(); + $generator->profileIn(); + + if ( !$isDryRun ) { + $generator->executeGenerator( $this ); + wfRunHooks( 'APIQueryGeneratorAfterExecute', array( &$generator, &$this ) ); + $this->resolvePendingRedirects(); + } else { + // Prevent warnings from being reported on these parameters + $main = $this->getMain(); + foreach ( $generator->extractRequestParams() as $paramName => $param ) { + $main->getVal( $generator->encodeParamName( $paramName ) ); + } + } + $generator->profileOut(); + $this->profileIn(); + + if ( !$isQuery ) { + // If this pageset is not part of the query, we called profileIn() above + $dbSource->profileOut(); + } + } else { + // Only one of the titles/pageids/revids is allowed at the same time + $dataSource = null; + if ( isset( $this->mParams['titles'] ) ) { + $dataSource = 'titles'; + } + if ( isset( $this->mParams['pageids'] ) ) { + if ( isset( $dataSource ) ) { + $this->dieUsage( "Cannot use 'pageids' at the same time as '$dataSource'", 'multisource' ); + } + $dataSource = 'pageids'; + } + if ( isset( $this->mParams['revids'] ) ) { + if ( isset( $dataSource ) ) { + $this->dieUsage( "Cannot use 'revids' at the same time as '$dataSource'", 'multisource' ); + } + $dataSource = 'revids'; + } + + if ( !$isDryRun ) { + // Populate page information with the original user input + switch( $dataSource ) { + case 'titles': + $this->initFromTitles( $this->mParams['titles'] ); + break; + case 'pageids': + $this->initFromPageIds( $this->mParams['pageids'] ); + break; + case 'revids': + if ( $this->mResolveRedirects ) { + $this->setWarning( 'Redirect resolution cannot be used together with the revids= parameter. ' . + 'Any redirects the revids= point to have not been resolved.' ); + } + $this->mResolveRedirects = false; + $this->initFromRevIDs( $this->mParams['revids'] ); + break; + default: + // Do nothing - some queries do not need any of the data sources. + break; + } + } + } + $this->profileOut(); } /** @@ -93,9 +218,33 @@ class ApiPageSet extends ApiQueryBase { } /** - * Request an additional field from the page table. Must be called - * before execute() - * @param $fieldName string Field name + * Return the parameter name that is the source of data for this PageSet + * + * If multiple source parameters are specified (e.g. titles and pageids), + * one will be named arbitrarily. + * + * @return string|null + */ + public function getDataSource() { + if ( $this->mAllowGenerator && isset( $this->mParams['generator'] ) ) { + return 'generator'; + } + if ( isset( $this->mParams['titles'] ) ) { + return 'titles'; + } + if ( isset( $this->mParams['pageids'] ) ) { + return 'pageids'; + } + if ( isset( $this->mParams['revids'] ) ) { + return 'revids'; + } + return null; + } + + /** + * Request an additional field from the page table. + * Must be called before execute() + * @param string $fieldName Field name */ public function requestField( $fieldName ) { $this->mRequestedPageFields[$fieldName] = null; @@ -104,7 +253,7 @@ class ApiPageSet extends ApiQueryBase { /** * Get the value of a custom field previously requested through * requestField() - * @param $fieldName string Field name + * @param string $fieldName Field name * @return mixed Field value */ public function getCustomField( $fieldName ) { @@ -207,14 +356,39 @@ class ApiPageSet extends ApiQueryBase { /** * Get a list of redirect resolutions - maps a title to its redirect - * target. - * @return array prefixed_title (string) => Title object + * target, as an array of output-ready arrays + * @return array */ public function getRedirectTitles() { return $this->mRedirectTitles; } /** + * Get a list of redirect resolutions - maps a title to its redirect + * target. + * @param $result ApiResult + * @return array of prefixed_title (string) => Title object + * @since 1.21 + */ + public function getRedirectTitlesAsResult( $result = null ) { + $values = array(); + foreach ( $this->getRedirectTitles() as $titleStrFrom => $titleTo ) { + $r = array( + 'from' => strval( $titleStrFrom ), + 'to' => $titleTo->getPrefixedText(), + ); + if ( $titleTo->getFragment() !== '' ) { + $r['tofragment'] = $titleTo->getFragment(); + } + $values[] = $r; + } + if ( !empty( $values ) && $result ) { + $result->setIndexedTagName( $values, 'r' ); + } + return $values; + } + + /** * Get a list of title normalizations - maps a title to its normalized * version. * @return array raw_prefixed_title (string) => prefixed_title (string) @@ -224,6 +398,27 @@ class ApiPageSet extends ApiQueryBase { } /** + * Get a list of title normalizations - maps a title to its normalized + * version in the form of result array. + * @param $result ApiResult + * @return array of raw_prefixed_title (string) => prefixed_title (string) + * @since 1.21 + */ + public function getNormalizedTitlesAsResult( $result = null ) { + $values = array(); + foreach ( $this->getNormalizedTitles() as $rawTitleStr => $titleStr ) { + $values[] = array( + 'from' => $rawTitleStr, + 'to' => $titleStr + ); + } + if ( !empty( $values ) && $result ) { + $result->setIndexedTagName( $values, 'n' ); + } + return $values; + } + + /** * Get a list of title conversions - maps a title to its converted * version. * @return array raw_prefixed_title (string) => prefixed_title (string) @@ -233,6 +428,27 @@ class ApiPageSet extends ApiQueryBase { } /** + * Get a list of title conversions - maps a title to its converted + * version as a result array. + * @param $result ApiResult + * @return array of (from, to) strings + * @since 1.21 + */ + public function getConvertedTitlesAsResult( $result = null ) { + $values = array(); + foreach ( $this->getConvertedTitles() as $rawTitleStr => $titleStr ) { + $values[] = array( + 'from' => $rawTitleStr, + 'to' => $titleStr + ); + } + if ( !empty( $values ) && $result ) { + $result->setIndexedTagName( $values, 'c' ); + } + return $values; + } + + /** * Get a list of interwiki titles - maps a title to its interwiki * prefix. * @return array raw_prefixed_title (string) => interwiki_prefix (string) @@ -242,6 +458,33 @@ class ApiPageSet extends ApiQueryBase { } /** + * Get a list of interwiki titles - maps a title to its interwiki + * prefix as result. + * @param $result ApiResult + * @param $iwUrl boolean + * @return array raw_prefixed_title (string) => interwiki_prefix (string) + * @since 1.21 + */ + public function getInterwikiTitlesAsResult( $result = null, $iwUrl = false ) { + $values = array(); + foreach ( $this->getInterwikiTitles() as $rawTitleStr => $interwikiStr ) { + $item = array( + 'title' => $rawTitleStr, + 'iw' => $interwikiStr, + ); + if ( $iwUrl ) { + $title = Title::newFromText( $rawTitleStr ); + $item['url'] = $title->getFullURL( '', false, PROTO_CURRENT ); + } + $values[] = $item; + } + if ( !empty( $values ) && $result ) { + $result->setIndexedTagName( $values, 'i' ); + } + return $values; + } + + /** * Get the list of revision IDs (requested with the revids= parameter) * @return array revID (int) => pageID (int) */ @@ -258,6 +501,25 @@ class ApiPageSet extends ApiQueryBase { } /** + * Revision IDs that were not found in the database as result array. + * @param $result ApiResult + * @return array of revision IDs + * @since 1.21 + */ + public function getMissingRevisionIDsAsResult( $result = null ) { + $values = array(); + foreach ( $this->getMissingRevisionIDs() as $revid ) { + $values[$revid] = array( + 'revid' => $revid + ); + } + if ( !empty( $values ) && $result ) { + $result->setIndexedTagName( $values, 'rev' ); + } + return $values; + } + + /** * Get the list of titles with negative namespace * @return array Title */ @@ -274,55 +536,8 @@ class ApiPageSet extends ApiQueryBase { } /** - * Populate the PageSet from the request parameters. - */ - public function execute() { - $this->profileIn(); - $params = $this->extractRequestParams(); - - // Only one of the titles/pageids/revids is allowed at the same time - $dataSource = null; - if ( isset( $params['titles'] ) ) { - $dataSource = 'titles'; - } - if ( isset( $params['pageids'] ) ) { - if ( isset( $dataSource ) ) { - $this->dieUsage( "Cannot use 'pageids' at the same time as '$dataSource'", 'multisource' ); - } - $dataSource = 'pageids'; - } - if ( isset( $params['revids'] ) ) { - if ( isset( $dataSource ) ) { - $this->dieUsage( "Cannot use 'revids' at the same time as '$dataSource'", 'multisource' ); - } - $dataSource = 'revids'; - } - - switch ( $dataSource ) { - case 'titles': - $this->initFromTitles( $params['titles'] ); - break; - case 'pageids': - $this->initFromPageIds( $params['pageids'] ); - break; - case 'revids': - if ( $this->mResolveRedirects ) { - $this->setWarning( 'Redirect resolution cannot be used together with the revids= parameter. ' . - 'Any redirects the revids= point to have not been resolved.' ); - } - $this->mResolveRedirects = false; - $this->initFromRevIDs( $params['revids'] ); - break; - default: - // Do nothing - some queries do not need any of the data sources. - break; - } - $this->profileOut(); - } - - /** * Populate this PageSet from a list of Titles - * @param $titles array of Title objects + * @param array $titles of Title objects */ public function populateFromTitles( $titles ) { $this->profileIn(); @@ -332,7 +547,7 @@ class ApiPageSet extends ApiQueryBase { /** * Populate this PageSet from a list of page IDs - * @param $pageIDs array of page IDs + * @param array $pageIDs of page IDs */ public function populateFromPageIDs( $pageIDs ) { $this->profileIn(); @@ -353,7 +568,7 @@ class ApiPageSet extends ApiQueryBase { /** * Populate this PageSet from a list of revision IDs - * @param $revIDs array of revision IDs + * @param array $revIDs of revision IDs */ public function populateFromRevisionIDs( $revIDs ) { $this->profileIn(); @@ -385,12 +600,11 @@ class ApiPageSet extends ApiQueryBase { } /** - * Resolve redirects, if applicable + * Do not use, does nothing, will be removed + * @deprecated 1.21 */ public function finishPageSetGeneration() { - $this->profileIn(); - $this->resolvePendingRedirects(); - $this->profileOut(); + wfDeprecated( __METHOD__, '1.21' ); } /** @@ -407,7 +621,7 @@ class ApiPageSet extends ApiQueryBase { * #5 Substitute the original LinkBatch object with the new list * #6 Repeat from step #1 * - * @param $titles array of Title objects or strings + * @param array $titles of Title objects or strings */ private function initFromTitles( $titles ) { // Get validated and normalized title objects @@ -434,10 +648,10 @@ class ApiPageSet extends ApiQueryBase { /** * Does the same as initFromTitles(), but is based on page IDs instead - * @param $pageids array of page IDs + * @param array $pageids of page IDs */ private function initFromPageIds( $pageids ) { - if ( !count( $pageids ) ) { + if ( !$pageids ) { return; } @@ -447,7 +661,7 @@ class ApiPageSet extends ApiQueryBase { $pageids = self::getPositiveIntegers( $pageids ); $res = null; - if ( count( $pageids ) ) { + if ( !empty( $pageids ) ) { $set = array( 'page_id' => $pageids ); @@ -460,7 +674,7 @@ class ApiPageSet extends ApiQueryBase { $this->profileDBOut(); } - $this->initFromQueryResult( $res, $remaining, false ); // process PageIDs + $this->initFromQueryResult( $res, $remaining, false ); // process PageIDs // Resolve any found redirects $this->resolvePendingRedirects(); @@ -470,9 +684,9 @@ class ApiPageSet extends ApiQueryBase { * Iterate through the result of the query on 'page' table, * and for each row create and store title object and save any extra fields requested. * @param $res ResultWrapper DB Query result - * @param $remaining array of either pageID or ns/title elements (optional). + * @param array $remaining of either pageID or ns/title elements (optional). * If given, any missing items will go to $mMissingPageIDs and $mMissingTitles - * @param $processTitles bool Must be provided together with $remaining. + * @param bool $processTitles Must be provided together with $remaining. * If true, treat $remaining as an array of [ns][title] * If false, treat it as an array of [pageIDs] */ @@ -499,7 +713,7 @@ class ApiPageSet extends ApiQueryBase { $this->processDbRow( $row ); // Need gender information - if( MWNamespace::hasGenderDistinction( $row->page_namespace ) ) { + if ( MWNamespace::hasGenderDistinction( $row->page_namespace ) ) { $usernames[] = $row->page_title; } } @@ -518,7 +732,7 @@ class ApiPageSet extends ApiQueryBase { $this->mTitles[] = $title; // need gender information - if( MWNamespace::hasGenderDistinction( $ns ) ) { + if ( MWNamespace::hasGenderDistinction( $ns ) ) { $usernames[] = $dbkey; } } @@ -541,10 +755,10 @@ class ApiPageSet extends ApiQueryBase { /** * Does the same as initFromTitles(), but is based on revision IDs * instead - * @param $revids array of revision IDs + * @param array $revids of revision IDs */ private function initFromRevIDs( $revids ) { - if ( !count( $revids ) ) { + if ( !$revids ) { return; } @@ -555,14 +769,14 @@ class ApiPageSet extends ApiQueryBase { $revids = self::getPositiveIntegers( $revids ); - if ( count( $revids ) ) { + if ( !empty( $revids ) ) { $tables = array( 'revision', 'page' ); $fields = array( 'rev_id', 'rev_page' ); $where = array( 'rev_id' => $revids, 'rev_page = page_id' ); // Get pageIDs data from the `page` table $this->profileDBIn(); - $res = $db->select( $tables, $fields, $where, __METHOD__ ); + $res = $db->select( $tables, $fields, $where, __METHOD__ ); foreach ( $res as $row ) { $revid = intval( $row->rev_id ); $pageid = intval( $row->rev_page ); @@ -670,44 +884,63 @@ class ApiPageSet extends ApiQueryBase { } /** + * Get the cache mode for the data generated by this module. + * All PageSet users should take into account whether this returns a more-restrictive + * cache mode than the using module itself. For possible return values and other + * details about cache modes, see ApiMain::setCacheMode() + * + * Public caching will only be allowed if *all* the modules that supply + * data for a given request return a cache mode of public. + * + * @param $params + * @return string + * @since 1.21 + */ + public function getCacheMode( $params = null ) { + return $this->mCacheMode; + } + + /** * Given an array of title strings, convert them into Title objects. - * Alternativelly, an array of Title objects may be given. + * Alternatively, an array of Title objects may be given. * This method validates access rights for the title, * and appends normalization values to the output. * - * @param $titles array of Title objects or strings + * @param array $titles of Title objects or strings * @return LinkBatch */ private function processTitlesArray( $titles ) { - $genderCache = GenderCache::singleton(); - $genderCache->doTitlesArray( $titles, __METHOD__ ); - + $usernames = array(); $linkBatch = new LinkBatch(); foreach ( $titles as $title ) { - $titleObj = is_string( $title ) ? Title::newFromText( $title ) : $title; + if ( is_string( $title ) ) { + $titleObj = Title::newFromText( $title, $this->mDefaultNamespace ); + } else { + $titleObj = $title; + } if ( !$titleObj ) { // Handle invalid titles gracefully - $this->mAllpages[0][$title] = $this->mFakePageId; + $this->mAllPages[0][$title] = $this->mFakePageId; $this->mInvalidTitles[$this->mFakePageId] = $title; $this->mFakePageId--; continue; // There's nothing else we can do } $unconvertedTitle = $titleObj->getPrefixedText(); $titleWasConverted = false; - $iw = $titleObj->getInterwiki(); - if ( strval( $iw ) !== '' ) { + if ( $titleObj->isExternal() ) { // This title is an interwiki link. - $this->mInterwikiTitles[$titleObj->getPrefixedText()] = $iw; + $this->mInterwikiTitles[$unconvertedTitle] = $titleObj->getInterwiki(); } else { // Variants checking global $wgContLang; if ( $this->mConvertTitles && - count( $wgContLang->getVariants() ) > 1 && + count( $wgContLang->getVariants() ) > 1 && !$titleObj->exists() ) { - // Language::findVariantLink will modify titleObj into + // Language::findVariantLink will modify titleText and titleObj into // the canonical variant if possible - $wgContLang->findVariantLink( $title, $titleObj ); + $titleText = is_string( $title ) ? $title : $titleObj->getPrefixedText(); + $wgContLang->findVariantLink( $titleText, $titleObj ); $titleWasConverted = $unconvertedTitle !== $titleObj->getPrefixedText(); } @@ -728,16 +961,36 @@ class ApiPageSet extends ApiQueryBase { // namespace is localized or the capitalization is // different if ( $titleWasConverted ) { - $this->mConvertedTitles[$title] = $titleObj->getPrefixedText(); + $this->mConvertedTitles[$unconvertedTitle] = $titleObj->getPrefixedText(); + // In this case the page can't be Special. + if ( is_string( $title ) && $title !== $unconvertedTitle ) { + $this->mNormalizedTitles[$title] = $unconvertedTitle; + } } elseif ( is_string( $title ) && $title !== $titleObj->getPrefixedText() ) { $this->mNormalizedTitles[$title] = $titleObj->getPrefixedText(); } + + // Need gender information + if ( MWNamespace::hasGenderDistinction( $titleObj->getNamespace() ) ) { + $usernames[] = $titleObj->getText(); + } } + // Get gender information + $genderCache = GenderCache::singleton(); + $genderCache->doQuery( $usernames, __METHOD__ ); return $linkBatch; } /** + * Get the database connection (read-only) + * @return DatabaseBase + */ + protected function getDB() { + return $this->mDbSource->getDB(); + } + + /** * Returns the input array of integers with all values < 0 removed * * @param $array array @@ -747,7 +1000,7 @@ class ApiPageSet extends ApiQueryBase { // bug 25734 API: possible issue with revids validation // It seems with a load of revision rows, MySQL gets upset // Remove any < 0 integers, as they can't be valid - foreach( $array as $i => $int ) { + foreach ( $array as $i => $int ) { if ( $int < 0 ) { unset( $array[$i] ); } @@ -756,8 +1009,8 @@ class ApiPageSet extends ApiQueryBase { return $array; } - public function getAllowedParams() { - return array( + public function getAllowedParams( $flags = 0 ) { + $result = array( 'titles' => array( ApiBase::PARAM_ISMULTI => true ), @@ -768,15 +1021,59 @@ class ApiPageSet extends ApiQueryBase { 'revids' => array( ApiBase::PARAM_TYPE => 'integer', ApiBase::PARAM_ISMULTI => true - ) + ), + 'redirects' => false, + 'converttitles' => false, ); + if ( $this->mAllowGenerator ) { + if ( $flags & ApiBase::GET_VALUES_FOR_HELP ) { + $result['generator'] = array( + ApiBase::PARAM_TYPE => $this->getGenerators() + ); + } else { + $result['generator'] = null; + } + } + return $result; + } + + private static $generators = null; + + /** + * Get an array of all available generators + * @return array + */ + private function getGenerators() { + if ( self::$generators === null ) { + $query = $this->mDbSource; + if ( !( $query instanceof ApiQuery ) ) { + // If the parent container of this pageset is not ApiQuery, + // we must create it to get module manager + $query = $this->getMain()->getModuleManager()->getModule( 'query' ); + } + $gens = array(); + $mgr = $query->getModuleManager(); + foreach ( $mgr->getNamesWithClasses() as $name => $class ) { + if ( is_subclass_of( $class, 'ApiQueryGeneratorBase' ) ) { + $gens[] = $name; + } + } + sort( $gens ); + self::$generators = $gens; + } + return self::$generators; } public function getParamDescription() { return array( 'titles' => 'A list of titles to work on', 'pageids' => 'A list of page IDs to work on', - 'revids' => 'A list of revision IDs to work on' + 'revids' => 'A list of revision IDs to work on', + 'generator' => array( 'Get the list of pages to work on by executing the specified query module.', + 'NOTE: generator parameter names must be prefixed with a \'g\', see examples' ), + 'redirects' => 'Automatically resolve redirects', + 'converttitles' => array( 'Convert titles to other variants if necessary. Only works if the wiki\'s content language supports variant conversion.', + 'Languages that support variant conversion include ' . implode( ', ', LanguageConverter::$languagesWithVariants ) ), ); } @@ -784,10 +1081,7 @@ class ApiPageSet extends ApiQueryBase { return array_merge( parent::getPossibleErrors(), array( array( 'code' => 'multisource', 'info' => "Cannot use 'pageids' at the same time as 'dataSource'" ), array( 'code' => 'multisource', 'info' => "Cannot use 'revids' at the same time as 'dataSource'" ), + array( 'code' => 'badgenerator', 'info' => 'Module $generatorName cannot be used as a generator' ), ) ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php index 343a2625..27f8cefd 100644 --- a/includes/api/ApiParamInfo.php +++ b/includes/api/ApiParamInfo.php @@ -42,42 +42,13 @@ class ApiParamInfo extends ApiBase { public function execute() { // Get parameters $params = $this->extractRequestParams(); - $result = $this->getResult(); + $resultObj = $this->getResult(); $res = array(); - if ( is_array( $params['modules'] ) ) { - $modules = $this->getMain()->getModules(); - $res['modules'] = array(); - foreach ( $params['modules'] as $mod ) { - if ( !isset( $modules[$mod] ) ) { - $res['modules'][] = array( 'name' => $mod, 'missing' => '' ); - continue; - } - $obj = new $modules[$mod]( $this->getMain(), $mod ); - $item = $this->getClassInfo( $obj ); - $item['name'] = $mod; - $res['modules'][] = $item; - } - $result->setIndexedTagName( $res['modules'], 'module' ); - } + $this->addModulesInfo( $params, 'modules', $res, $resultObj ); - if ( is_array( $params['querymodules'] ) ) { - $queryModules = $this->queryObj->getModules(); - $res['querymodules'] = array(); - foreach ( $params['querymodules'] as $qm ) { - if ( !isset( $queryModules[$qm] ) ) { - $res['querymodules'][] = array( 'name' => $qm, 'missing' => '' ); - continue; - } - $obj = new $queryModules[$qm]( $this, $qm ); - $item = $this->getClassInfo( $obj ); - $item['name'] = $qm; - $item['querytype'] = $this->queryObj->getModuleType( $qm ); - $res['querymodules'][] = $item; - } - $result->setIndexedTagName( $res['querymodules'], 'module' ); - } + $this->addModulesInfo( $params, 'querymodules', $res, $resultObj ); if ( $params['mainmodule'] ) { $res['mainmodule'] = $this->getClassInfo( $this->getMain() ); @@ -88,36 +59,57 @@ class ApiParamInfo extends ApiBase { $res['pagesetmodule'] = $this->getClassInfo( $pageSet ); } - if ( is_array( $params['formatmodules'] ) ) { - $formats = $this->getMain()->getFormats(); - $res['formatmodules'] = array(); - foreach ( $params['formatmodules'] as $f ) { - if ( !isset( $formats[$f] ) ) { - $res['formatmodules'][] = array( 'name' => $f, 'missing' => '' ); - continue; - } - $obj = new $formats[$f]( $this, $f ); - $item = $this->getClassInfo( $obj ); - $item['name'] = $f; - $res['formatmodules'][] = $item; + $this->addModulesInfo( $params, 'formatmodules', $res, $resultObj ); + + $resultObj->addValue( null, $this->getModuleName(), $res ); + } + + /** + * If the type is requested in parameters, adds a section to res with module info. + * @param array $params user parameters array + * @param string $type parameter name + * @param array $res store results in this array + * @param ApiResult $resultObj results object to set indexed tag. + */ + private function addModulesInfo( $params, $type, &$res, $resultObj ) { + if ( !is_array( $params[$type] ) ) { + return; + } + $isQuery = ( $type === 'querymodules' ); + if ( $isQuery ) { + $mgr = $this->queryObj->getModuleManager(); + } else { + $mgr = $this->getMain()->getModuleManager(); + } + $res[$type] = array(); + foreach ( $params[$type] as $mod ) { + if ( !$mgr->isDefined( $mod ) ) { + $res[$type][] = array( 'name' => $mod, 'missing' => '' ); + continue; + } + $obj = $mgr->getModule( $mod ); + $item = $this->getClassInfo( $obj ); + $item['name'] = $mod; + if ( $isQuery ) { + $item['querytype'] = $mgr->getModuleGroup( $mod ); } - $result->setIndexedTagName( $res['formatmodules'], 'module' ); + $res[$type][] = $item; } - $result->addValue( null, $this->getModuleName(), $res ); + $resultObj->setIndexedTagName( $res[$type], 'module' ); } /** * @param $obj ApiBase * @return ApiResult */ - function getClassInfo( $obj ) { + private function getClassInfo( $obj ) { $result = $this->getResult(); $retval['classname'] = get_class( $obj ); $retval['description'] = implode( "\n", (array)$obj->getFinalDescription() ); - $retval['examples'] = ''; - $retval['version'] = implode( "\n", (array)$obj->getVersion() ); + // version is deprecated since 1.21, but needs to be returned for v1 + $retval['version'] = ''; $retval['prefix'] = $obj->getModulePrefix(); if ( $obj->isReadMode() ) { @@ -133,7 +125,7 @@ class ApiParamInfo extends ApiBase { $retval['generator'] = ''; } - $allowedParams = $obj->getFinalParams(); + $allowedParams = $obj->getFinalParams( ApiBase::GET_VALUES_FOR_HELP ); if ( !is_array( $allowedParams ) ) { return $retval; } @@ -150,7 +142,7 @@ class ApiParamInfo extends ApiBase { if ( is_string( $examples ) ) { $examples = array( $examples ); } - foreach( $examples as $k => $v ) { + foreach ( $examples as $k => $v ) { if ( strlen( $retval['examples'] ) ) { $retval['examples'] .= ' '; } @@ -181,7 +173,7 @@ class ApiParamInfo extends ApiBase { } //handle shorthand - if( !is_array( $p ) ) { + if ( !is_array( $p ) ) { $p = array( ApiBase::PARAM_DFLT => $p, ); @@ -208,11 +200,11 @@ class ApiParamInfo extends ApiBase { if ( isset( $p[ApiBase::PARAM_DFLT] ) ) { $type = $p[ApiBase::PARAM_TYPE]; - if( $type === 'boolean' ) { + if ( $type === 'boolean' ) { $a['default'] = ( $p[ApiBase::PARAM_DFLT] ? 'true' : 'false' ); - } elseif( $type === 'string' ) { + } elseif ( $type === 'string' ) { $a['default'] = strval( $p[ApiBase::PARAM_DFLT] ); - } elseif( $type === 'integer' ) { + } elseif ( $type === 'integer' ) { $a['default'] = intval( $p[ApiBase::PARAM_DFLT] ); } else { $a['default'] = $p[ApiBase::PARAM_DFLT]; @@ -299,7 +291,7 @@ class ApiParamInfo extends ApiBase { $retval['props'][] = $propResult; } - // default is true for query modules, false for other modules, overriden by ApiBase::PROP_LIST + // default is true for query modules, false for other modules, overridden by ApiBase::PROP_LIST if ( $listResult === true || ( $listResult !== false && $obj instanceof ApiQueryBase ) ) { $retval['listresult'] = ''; } @@ -319,11 +311,11 @@ class ApiParamInfo extends ApiBase { } public function getAllowedParams() { - $modules = array_keys( $this->getMain()->getModules() ); + $modules = $this->getMain()->getModuleManager()->getNames( 'action' ); sort( $modules ); - $querymodules = array_keys( $this->queryObj->getModules() ); + $querymodules = $this->queryObj->getModuleManager()->getNames(); sort( $querymodules ); - $formatmodules = array_keys( $this->getMain()->getFormats() ); + $formatmodules = $this->getMain()->getModuleManager()->getNames( 'format' ); sort( $formatmodules ); return array( 'modules' => array( @@ -366,8 +358,4 @@ class ApiParamInfo extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Parameter_information'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php index db6e2bb8..09b7a882 100644 --- a/includes/api/ApiParse.php +++ b/includes/api/ApiParse.php @@ -26,11 +26,15 @@ * @ingroup API */ class ApiParse extends ApiBase { - private $section, $text, $pstText = null; - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } + /** @var String $section */ + private $section = null; + + /** @var Content $content */ + private $content = null; + + /** @var Content $pstContent */ + private $pstContent = null; public function execute() { // The data is hot but user-dependent, like page views, so we set vary cookies @@ -44,6 +48,9 @@ class ApiParse extends ApiBase { $pageid = $params['pageid']; $oldid = $params['oldid']; + $model = $params['contentmodel']; + $format = $params['contentformat']; + if ( !is_null( $page ) && ( !is_null( $text ) || $title != 'API' ) ) { $this->dieUsage( 'The page parameter cannot be used together with the text and title parameters', 'params' ); } @@ -61,7 +68,7 @@ class ApiParse extends ApiBase { // TODO: Does this still need $wgTitle? global $wgParser, $wgTitle; - // Currently unnecessary, code to act as a safeguard against any change in current behaviour of uselang breaks + // Currently unnecessary, code to act as a safeguard against any change in current behavior of uselang $oldLang = null; if ( isset( $params['uselang'] ) && $params['uselang'] != $this->getContext()->getLanguage()->getCode() ) { $oldLang = $this->getContext()->getLanguage(); // Backup language @@ -91,19 +98,19 @@ class ApiParse extends ApiBase { $popts->enableLimitReport( !$params['disablepp'] ); // If for some reason the "oldid" is actually the current revision, it may be cached - if ( $titleObj->getLatestRevID() === intval( $oldid ) ) { + if ( $rev->isCurrent() ) { // May get from/save to parser cache - $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid, - isset( $prop['wikitext'] ) ) ; + $p_result = $this->getParsedContent( $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() ); + $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() ); if ( $this->section !== false ) { - $this->text = $this->getSectionText( $this->text, 'r' . $rev->getId() ); + $this->content = $this->getSectionContent( $this->content, 'r' . $rev->getId() ); } // Should we save old revision parses to the parser cache? - $p_result = $wgParser->parse( $this->text, $titleObj, $popts ); + $p_result = $this->content->getParserOutput( $titleObj, $rev->getId(), $popts ); } } else { // Not $oldid, but $pageid or $page if ( $params['redirects'] ) { @@ -136,6 +143,9 @@ class ApiParse extends ApiBase { $pageObj = $this->getTitleOrPageId( $pageParams, 'fromdb' ); $titleObj = $pageObj->getTitle(); + if ( !$titleObj || !$titleObj->exists() ) { + $this->dieUsage( "The page you specified doesn't exist", 'missingtitle' ); + } $wgTitle = $titleObj; if ( isset( $prop['revid'] ) ) { @@ -146,46 +156,59 @@ class ApiParse extends ApiBase { $popts->enableLimitReport( !$params['disablepp'] ); // Potentially cached - $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid, - isset( $prop['wikitext'] ) ) ; + $p_result = $this->getParsedContent( $pageObj, $popts, $pageid, + isset( $prop['wikitext'] ) ); } } else { // Not $oldid, $pageid, $page. Hence based on $text - - if ( is_null( $text ) ) { - $this->dieUsage( 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?', 'params' ); - } - $this->text = $text; $titleObj = Title::newFromText( $title ); - if ( !$titleObj ) { + if ( !$titleObj || $titleObj->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $title ) ); } + if ( !$titleObj->canExist() ) { + $this->dieUsage( "Namespace doesn't allow actual pages", 'pagecannotexist' ); + } $wgTitle = $titleObj; $pageObj = WikiPage::factory( $titleObj ); $popts = $pageObj->makeParserOptions( $this->getContext() ); $popts->enableLimitReport( !$params['disablepp'] ); + if ( is_null( $text ) ) { + $this->dieUsage( 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?', 'params' ); + } + + try { + $this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format ); + } catch ( MWContentSerializationException $ex ) { + $this->dieUsage( $ex->getMessage(), 'parseerror' ); + } + if ( $this->section !== false ) { - $this->text = $this->getSectionText( $this->text, $titleObj->getText() ); + $this->content = $this->getSectionContent( $this->content, $titleObj->getText() ); } if ( $params['pst'] || $params['onlypst'] ) { - $this->pstText = $wgParser->preSaveTransform( $this->text, $titleObj, $this->getUser(), $popts ); + $this->pstContent = $this->content->preSaveTransform( $titleObj, $this->getUser(), $popts ); } if ( $params['onlypst'] ) { // Build a result and bail out $result_array = array(); $result_array['text'] = array(); - $result->setContent( $result_array['text'], $this->pstText ); + $result->setContent( $result_array['text'], $this->pstContent->serialize( $format ) ); if ( isset( $prop['wikitext'] ) ) { $result_array['wikitext'] = array(); - $result->setContent( $result_array['wikitext'], $this->text ); + $result->setContent( $result_array['wikitext'], $this->content->serialize( $format ) ); } $result->addValue( null, $this->getModuleName(), $result_array ); return; } + // Not cached (save or load) - $p_result = $wgParser->parse( $params['pst'] ? $this->pstText : $this->text, $titleObj, $popts ); + if ( $params['pst'] ) { + $p_result = $this->pstContent->getParserOutput( $titleObj, null, $popts ); + } else { + $p_result = $this->content->getParserOutput( $titleObj, null, $popts ); + } } $result_array = array(); @@ -275,10 +298,10 @@ class ApiParse extends ApiBase { if ( isset( $prop['wikitext'] ) ) { $result_array['wikitext'] = array(); - $result->setContent( $result_array['wikitext'], $this->text ); - if ( !is_null( $this->pstText ) ) { + $result->setContent( $result_array['wikitext'], $this->content->serialize( $format ) ); + if ( !is_null( $this->pstContent ) ) { $result_array['psttext'] = array(); - $result->setContent( $result_array['psttext'], $this->pstText ); + $result->setContent( $result_array['psttext'], $this->pstContent->serialize( $format ) ); } } if ( isset( $prop['properties'] ) ) { @@ -286,8 +309,12 @@ class ApiParse extends ApiBase { } if ( $params['generatexml'] ) { + if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) { + $this->dieUsage( "generatexml is only supported for wikitext content", "notwikitext" ); + } + $wgParser->startExternalParse( $titleObj, $popts, OT_PREPROCESS ); - $dom = $wgParser->preprocessToDom( $this->text ); + $dom = $wgParser->preprocessToDom( $this->content->getNativeData() ); if ( is_callable( array( $dom, 'saveXML' ) ) ) { $xml = $dom->saveXML(); } else { @@ -325,15 +352,16 @@ class ApiParse extends ApiBase { * @param $getWikitext Bool * @return ParserOutput */ - private function getParsedSectionOrText( $page, $popts, $pageId = null, $getWikitext = false ) { - global $wgParser; + private function getParsedContent( WikiPage $page, $popts, $pageId = null, $getWikitext = false ) { + $this->content = $page->getContent( Revision::RAW ); //XXX: really raw? - if ( $this->section !== false ) { - $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId ) - ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText() ); + if ( $this->section !== false && $this->content !== null ) { + $this->content = $this->getSectionContent( + $this->content, + !is_null( $pageId ) ? 'page id ' . $pageId : $page->getTitle()->getText() ); // Not cached (save or load) - return $wgParser->parse( $this->text, $page->getTitle(), $popts ); + return $this->content->getParserOutput( $page->getTitle(), null, $popts ); } else { // Try the parser cache first // getParserOutput will save to Parser cache if able @@ -342,20 +370,23 @@ class ApiParse extends ApiBase { $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' ); } if ( $getWikitext ) { - $this->text = $page->getRawText(); + $this->content = $page->getContent( Revision::RAW ); } return $pout; } } - private function getSectionText( $text, $what ) { - global $wgParser; + private function getSectionContent( Content $content, $what ) { // Not cached (save or load) - $text = $wgParser->getSection( $text, $this->section, false ); - if ( $text === false ) { + $section = $content->getSection( $this->section ); + if ( $section === false ) { $this->dieUsage( "There is no section {$this->section} in " . $what, 'nosuchsection' ); } - return $text; + if ( $section === null ) { + $this->dieUsage( "Sections are not supported by " . $what, 'nosuchsection' ); + $section = false; + } + return $section; } private function formatLangLinks( $links ) { @@ -415,14 +446,14 @@ class ApiParse extends ApiBase { $text = Language::fetchLanguageName( $nt->getInterwiki() ); $langs[] = Html::element( 'a', - array( 'href' => $nt->getFullURL(), 'title' => $nt->getText(), 'class' => "external" ), + array( 'href' => $nt->getFullURL(), 'title' => $nt->getText(), 'class' => 'external' ), $text == '' ? $l : $text ); } $s .= implode( wfMessage( 'pipe-separator' )->escaped(), $langs ); if ( $wgContLang->isRTL() ) { - $s = Html::rawElement( 'span', array( 'dir' => "LTR" ), $s ); + $s = Html::rawElement( 'span', array( 'dir' => 'LTR' ), $s ); } return $s; @@ -548,6 +579,12 @@ class ApiParse extends ApiBase { 'section' => null, 'disablepp' => false, 'generatexml' => false, + 'contentformat' => array( + ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(), + ), + 'contentmodel' => array( + ApiBase::PARAM_TYPE => ContentHandler::getContentModels(), + ) ); } @@ -592,7 +629,9 @@ 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', + 'generatexml' => 'Generate XML parse tree (requires prop=wikitext)', + 'contentformat' => 'Content serialization format used for the input text', + 'contentmodel' => 'Content model of the new content', ); } @@ -613,6 +652,9 @@ class ApiParse extends ApiBase { array( 'code' => 'nosuchsection', 'info' => 'There is no section sectionnumber in page' ), array( 'nosuchpageid' ), array( 'invalidtitle', 'title' ), + array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ), + array( 'code' => 'notwikitext', 'info' => 'The requested operation is only supported on wikitext content.' ), + array( 'code' => 'pagecannotexist', 'info' => "Namespace doesn't allow actual pages" ), ) ); } @@ -625,8 +667,4 @@ class ApiParse extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Parsing_wikitext#parse'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php index cb5e081a..4d4fbba9 100644 --- a/includes/api/ApiPatrol.php +++ b/includes/api/ApiPatrol.php @@ -30,10 +30,6 @@ */ class ApiPatrol extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - /** * Patrols the article or provides the reason the patrol failed. */ @@ -120,8 +116,4 @@ class ApiPatrol extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Patrol'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php index b3ca67e6..503c6920 100644 --- a/includes/api/ApiProtect.php +++ b/includes/api/ApiProtect.php @@ -29,10 +29,6 @@ */ class ApiProtect extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { global $wgRestrictionLevels; $params = $this->extractRequestParams(); @@ -178,7 +174,7 @@ class ApiProtect extends ApiBase { 'token' => 'A protect token previously retrieved through prop=info', 'protections' => 'List of protection levels, formatted action=group (e.g. edit=sysop)', 'expiry' => array( 'Expiry timestamps. If only one timestamp is set, it\'ll be used for all protections.', - 'Use \'infinite\', \'indefinite\' or \'never\', for a neverexpiring protection.' ), + 'Use \'infinite\', \'indefinite\' or \'never\', for a never-expiring protection.' ), 'reason' => 'Reason for (un)protecting', 'cascade' => array( 'Enable cascading protection (i.e. protect pages included in this page)', 'Ignored if not all protection levels are \'sysop\' or \'protect\'' ), @@ -234,8 +230,4 @@ class ApiProtect extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Protect'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php index 9fedaf1b..134f4a0d 100644 --- a/includes/api/ApiPurge.php +++ b/includes/api/ApiPurge.php @@ -31,69 +31,69 @@ */ class ApiPurge extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); + private $mPageSet; + + /** + * Add all items from $values into the result + * @param array $result output + * @param array $values values to add + * @param string $flag the name of the boolean flag to mark this element + * @param string $name if given, name of the value + */ + private static function addValues( array &$result, $values, $flag = null, $name = null ) { + foreach ( $values as $val ) { + if( $val instanceof Title ) { + $v = array(); + ApiQueryBase::addTitleInfo( $v, $val ); + } elseif( $name !== null ) { + $v = array( $name => $val ); + } else { + $v = $val; + } + if( $flag !== null ) { + $v[$flag] = ''; + } + $result[] = $v; + } } /** * Purges the cache of a page */ public function execute() { - $user = $this->getUser(); $params = $this->extractRequestParams(); - if ( !$user->isAllowed( 'purge' ) && !$this->getMain()->isInternalMode() && - !$this->getRequest()->wasPosted() ) { - $this->dieUsageMsg( array( 'mustbeposted', $this->getModuleName() ) ); - } $forceLinkUpdate = $params['forcelinkupdate']; - $pageSet = new ApiPageSet( $this ); + $pageSet = $this->getPageSet(); $pageSet->execute(); $result = array(); - 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'] = ''; - $result[] = $page; - } - foreach( $pageSet->getMissingRevisionIDs() as $r ) { - $rev = array(); - $rev['revid'] = $r; - $rev['missing'] = ''; - $result[] = $rev; - } - - foreach ( $pageSet->getTitles() as $title ) { + self::addValues( $result, $pageSet->getInvalidTitles(), 'invalid', 'title' ); + self::addValues( $result, $pageSet->getSpecialTitles(), 'special', 'title' ); + self::addValues( $result, $pageSet->getMissingPageIDs(), 'missing', 'pageid' ); + self::addValues( $result, $pageSet->getMissingRevisionIDs(), 'missing', 'revid' ); + self::addValues( $result, $pageSet->getMissingTitles(), 'missing' ); + self::addValues( $result, $pageSet->getInterwikiTitlesAsResult() ); + + foreach ( $pageSet->getGoodTitles() as $title ) { $r = array(); - ApiQueryBase::addTitleInfo( $r, $title ); - if ( !$title->exists() ) { - $r['missing'] = ''; - $result[] = $r; - continue; - } - $page = WikiPage::factory( $title ); $page->doPurge(); // Directly purge and skip the UI part of purge(). $r['purged'] = ''; - if( $forceLinkUpdate ) { - if ( !$user->pingLimiter() ) { - global $wgParser, $wgEnableParserCache; + if ( $forceLinkUpdate ) { + if ( !$this->getUser()->pingLimiter() ) { + global $wgEnableParserCache; $popts = $page->makeParserOptions( 'canonical' ); - $p_result = $wgParser->parse( $page->getRawText(), $title, $popts, - true, true, $page->getLatest() ); + + # Parse content; note that HTML generation is only needed if we want to cache the result. + $content = $page->getContent( Revision::RAW ); + $p_result = $content->getParserOutput( $title, $page->getLatest(), $popts, $wgEnableParserCache ); # Update the links tables - $updates = $p_result->getSecondaryDataUpdates( $title ); + $updates = $content->getSecondaryDataUpdates( $title, null, true, $p_result ); DataUpdate::runUpdates( $updates ); $r['linkupdate'] = ''; @@ -114,24 +114,52 @@ class ApiPurge extends ApiBase { $apiResult = $this->getResult(); $apiResult->setIndexedTagName( $result, 'page' ); $apiResult->addValue( null, $this->getModuleName(), $result ); + + $values = $pageSet->getNormalizedTitlesAsResult( $apiResult ); + if ( $values ) { + $apiResult->addValue( null, 'normalized', $values ); + } + $values = $pageSet->getConvertedTitlesAsResult( $apiResult ); + if ( $values ) { + $apiResult->addValue( null, 'converted', $values ); + } + $values = $pageSet->getRedirectTitlesAsResult( $apiResult ); + if ( $values ) { + $apiResult->addValue( null, 'redirects', $values ); + } + } + + /** + * Get a cached instance of an ApiPageSet object + * @return ApiPageSet + */ + private function getPageSet() { + if ( !isset( $this->mPageSet ) ) { + $this->mPageSet = new ApiPageSet( $this ); + } + return $this->mPageSet; } public function isWriteMode() { return true; } - public function getAllowedParams() { - $psModule = new ApiPageSet( $this ); - return $psModule->getAllowedParams() + array( - 'forcelinkupdate' => false, - ); + public function mustBePosted() { + // Anonymous users are not allowed a non-POST request + return !$this->getUser()->isAllowed( 'purge' ); + } + + public function getAllowedParams( $flags = 0 ) { + $result = array( 'forcelinkupdate' => false ); + if ( $flags ) { + $result += $this->getPageSet()->getFinalParams( $flags ); + } + return $result; } public function getParamDescription() { - $psModule = new ApiPageSet( $this ); - return $psModule->getParamDescription() + array( - 'forcelinkupdate' => 'Update the links tables', - ); + return $this->getPageSet()->getParamDescription() + + array( 'forcelinkupdate' => 'Update the links tables' ); } public function getResultProperties() { @@ -155,9 +183,14 @@ class ApiPurge extends ApiBase { ApiBase::PROP_NULLABLE => true ), 'invalid' => 'boolean', + 'special' => 'boolean', 'missing' => 'boolean', 'purged' => 'boolean', - 'linkupdate' => 'boolean' + 'linkupdate' => 'boolean', + 'iw' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), ) ); } @@ -169,10 +202,9 @@ class ApiPurge extends ApiBase { } public function getPossibleErrors() { - $psModule = new ApiPageSet( $this ); return array_merge( parent::getPossibleErrors(), - $psModule->getPossibleErrors() + $this->getPageSet()->getPossibleErrors() ); } @@ -185,8 +217,4 @@ class ApiPurge extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Purge'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php index 554aae5a..f69ad234 100644 --- a/includes/api/ApiQuery.php +++ b/includes/api/ApiQuery.php @@ -37,16 +37,11 @@ */ class ApiQuery extends ApiBase { - private $mPropModuleNames, $mListModuleNames, $mMetaModuleNames; - /** - * @var ApiPageSet + * List of Api Query prop modules + * @var array */ - private $mPageSet; - - private $params, $redirects, $convertTitles, $iwUrl; - - private $mQueryPropModules = array( + private static $QueryPropModules = array( 'categories' => 'ApiQueryCategories', 'categoryinfo' => 'ApiQueryCategoryInfo', 'duplicatefiles' => 'ApiQueryDuplicateFiles', @@ -63,11 +58,16 @@ class ApiQuery extends ApiBase { 'templates' => 'ApiQueryLinks', ); - private $mQueryListModules = array( + /** + * List of Api Query list modules + * @var array + */ + private static $QueryListModules = array( 'allcategories' => 'ApiQueryAllCategories', 'allimages' => 'ApiQueryAllImages', 'alllinks' => 'ApiQueryAllLinks', 'allpages' => 'ApiQueryAllPages', + 'alltransclusions' => 'ApiQueryAllLinks', 'allusers' => 'ApiQueryAllUsers', 'backlinks' => 'ApiQueryBacklinks', 'blocks' => 'ApiQueryBlocks', @@ -80,6 +80,8 @@ class ApiQuery extends ApiBase { 'iwbacklinks' => 'ApiQueryIWBacklinks', 'langbacklinks' => 'ApiQueryLangBacklinks', 'logevents' => 'ApiQueryLogEvents', + 'pageswithprop' => 'ApiQueryPagesWithProp', + 'pagepropnames' => 'ApiQueryPagePropNames', 'protectedtitles' => 'ApiQueryProtectedTitles', 'querypage' => 'ApiQueryQueryPage', 'random' => 'ApiQueryRandom', @@ -92,16 +94,26 @@ class ApiQuery extends ApiBase { 'watchlistraw' => 'ApiQueryWatchlistRaw', ); - private $mQueryMetaModules = array( + /** + * List of Api Query meta modules + * @var array + */ + private static $QueryMetaModules = array( 'allmessages' => 'ApiQueryAllMessages', 'siteinfo' => 'ApiQuerySiteinfo', 'userinfo' => 'ApiQueryUserInfo', ); - private $mSlaveDB = null; - private $mNamedDB = array(); + /** + * @var ApiPageSet + */ + private $mPageSet; - protected $mAllowedGenerators = array(); + private $mParams; + private $mNamedDB = array(); + private $mModuleMgr; + private $mGeneratorContinue; + private $mUseLegacyContinue; /** * @param $main ApiMain @@ -110,59 +122,27 @@ class ApiQuery extends ApiBase { public function __construct( $main, $action ) { parent::__construct( $main, $action ); - // Allow custom modules to be added in LocalSettings.php - global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules, - $wgMemc, $wgAPICacheHelpTimeout; - self::appendUserModules( $this->mQueryPropModules, $wgAPIPropModules ); - self::appendUserModules( $this->mQueryListModules, $wgAPIListModules ); - self::appendUserModules( $this->mQueryMetaModules, $wgAPIMetaModules ); - - $this->mPropModuleNames = array_keys( $this->mQueryPropModules ); - $this->mListModuleNames = array_keys( $this->mQueryListModules ); - $this->mMetaModuleNames = array_keys( $this->mQueryMetaModules ); - - // 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 ); + $this->mModuleMgr = new ApiModuleManager( $this ); - if ( $wgAPICacheHelpTimeout > 0 ) { - $wgMemc->set( $key, $this->mAllowedGenerators, $wgAPICacheHelpTimeout ); - } + // Allow custom modules to be added in LocalSettings.php + global $wgAPIPropModules, $wgAPIListModules, $wgAPIMetaModules; + $this->mModuleMgr->addModules( self::$QueryPropModules, 'prop' ); + $this->mModuleMgr->addModules( $wgAPIPropModules, 'prop' ); + $this->mModuleMgr->addModules( self::$QueryListModules, 'list' ); + $this->mModuleMgr->addModules( $wgAPIListModules, 'list' ); + $this->mModuleMgr->addModules( self::$QueryMetaModules, 'meta' ); + $this->mModuleMgr->addModules( $wgAPIMetaModules, 'meta' ); + + // Create PageSet that will process titles/pageids/revids/generator + $this->mPageSet = new ApiPageSet( $this ); } /** - * Helper function to append any add-in modules to the list - * @param $modules array Module array - * @param $newModules array Module array to add to $modules + * Overrides to return this instance's module manager. + * @return ApiModuleManager */ - private static function appendUserModules( &$modules, $newModules ) { - if ( is_array( $newModules ) ) { - foreach ( $newModules as $moduleName => $moduleClass ) { - $modules[$moduleName] = $moduleClass; - } - } - } - - /** - * Gets a default slave database connection object - * @return DatabaseBase - */ - public function getDB() { - if ( !isset( $this->mSlaveDB ) ) { - $this->profileDBIn(); - $this->mSlaveDB = wfGetDB( DB_SLAVE, 'api' ); - $this->profileDBOut(); - } - return $this->mSlaveDB; + public function getModuleManager() { + return $this->mModuleMgr; } /** @@ -170,9 +150,9 @@ class ApiQuery extends ApiBase { * If no such connection has been requested before, it will be created. * Subsequent calls with the same $name will return the same connection * as the first, regardless of the values of $db and $groups - * @param $name string Name to assign to the database connection - * @param $db int One of the DB_* constants - * @param $groups array Query groups + * @param string $name Name to assign to the database connection + * @param int $db One of the DB_* constants + * @param array $groups Query groups * @return DatabaseBase */ public function getNamedDB( $name, $db, $groups ) { @@ -194,31 +174,38 @@ class ApiQuery extends ApiBase { /** * Get the array mapping module names to class names + * @deprecated since 1.21, use getModuleManager()'s methods instead + * @return array array(modulename => classname) + */ + public function getModules() { + wfDeprecated( __METHOD__, '1.21' ); + return $this->getModuleManager()->getNamesWithClasses(); + } + + /** + * Get the generators array mapping module names to class names + * @deprecated since 1.21, list of generators is maintained by ApiPageSet * @return array array(modulename => classname) */ - function getModules() { - return array_merge( $this->mQueryPropModules, $this->mQueryListModules, $this->mQueryMetaModules ); + public function getGenerators() { + wfDeprecated( __METHOD__, '1.21' ); + $gens = array(); + foreach ( $this->mModuleMgr->getNamesWithClasses() as $name => $class ) { + if ( is_subclass_of( $class, 'ApiQueryGeneratorBase' ) ) { + $gens[$name] = $class; + } + } + return $gens; } /** * Get whether the specified module is a prop, list or a meta query module - * @param $moduleName string Name of the module to find type for + * @deprecated since 1.21, use getModuleManager()->getModuleGroup() + * @param string $moduleName Name of the module to find type for * @return mixed string or null */ function getModuleType( $moduleName ) { - if ( isset( $this->mQueryPropModules[$moduleName] ) ) { - return 'prop'; - } - - if ( isset( $this->mQueryListModules[$moduleName] ) ) { - return 'list'; - } - - if ( isset( $this->mQueryMetaModules[$moduleName] ) ) { - return 'meta'; - } - - return null; + return $this->getModuleManager()->getModuleGroup( $moduleName ); } /** @@ -247,42 +234,37 @@ class ApiQuery extends ApiBase { * #5 Execute all requested modules */ public function execute() { - $this->params = $this->extractRequestParams(); - $this->redirects = $this->params['redirects']; - $this->convertTitles = $this->params['converttitles']; - $this->iwUrl = $this->params['iwurl']; + $this->mParams = $this->extractRequestParams(); - // Create PageSet - $this->mPageSet = new ApiPageSet( $this, $this->redirects, $this->convertTitles ); + // $pagesetParams is a array of parameter names used by the pageset generator + // or null if pageset has already finished and is no longer needed + // $completeModules is a set of complete modules with the name as key + $this->initContinue( $pagesetParams, $completeModules ); // Instantiate requested modules - $modules = array(); - $this->instantiateModules( $modules, 'prop', $this->mQueryPropModules ); - $this->instantiateModules( $modules, 'list', $this->mQueryListModules ); - $this->instantiateModules( $modules, 'meta', $this->mQueryMetaModules ); - - $cacheMode = 'public'; - - // If given, execute generator to substitute user supplied data with generated data. - if ( isset( $this->params['generator'] ) ) { - $generator = $this->newGenerator( $this->params['generator'] ); - $params = $generator->extractRequestParams(); - $cacheMode = $this->mergeCacheMode( $cacheMode, - $generator->getCacheMode( $params ) ); - $this->executeGeneratorModule( $generator, $modules ); - } else { - // Append custom fields and populate page/revision information - $this->addCustomFldsToPageSet( $modules, $this->mPageSet ); + $allModules = array(); + $this->instantiateModules( $allModules, 'prop' ); + $propModules = $allModules; // Keep a copy + $this->instantiateModules( $allModules, 'list' ); + $this->instantiateModules( $allModules, 'meta' ); + + // Filter modules based on continue parameter + $modules = $this->initModules( $allModules, $completeModules, $pagesetParams !== null ); + + // Execute pageset if in legacy mode or if pageset is not done + if ( $completeModules === null || $pagesetParams !== null ) { + // Populate page/revision information $this->mPageSet->execute(); + // Record page information (title, namespace, if exists, etc) + $this->outputGeneralPageInfo(); + } else { + $this->mPageSet->executeDryRun(); } - // Record page information (title, namespace, if exists, etc) - $this->outputGeneralPageInfo(); + $cacheMode = $this->mPageSet->getCacheMode(); - // Execute all requested modules. - /** - * @var $module ApiQueryBase - */ + // Execute all unfinished modules + /** @var $module ApiQueryBase */ foreach ( $modules as $module ) { $params = $module->extractRequestParams(); $cacheMode = $this->mergeCacheMode( @@ -295,6 +277,136 @@ class ApiQuery extends ApiBase { // Set the cache mode $this->getMain()->setCacheMode( $cacheMode ); + + if ( $completeModules === null ) { + return; // Legacy continue, we are done + } + + // Reformat query-continue result section + $result = $this->getResult(); + $qc = $result->getData(); + if ( isset( $qc['query-continue'] ) ) { + $qc = $qc['query-continue']; + $result->unsetValue( null, 'query-continue' ); + } elseif ( $this->mGeneratorContinue !== null ) { + $qc = array(); + } else { + // no more "continue"s, we are done! + return; + } + + // we are done with all the modules that do not have result in query-continue + $completeModules = array_merge( $completeModules, array_diff_key( $modules, $qc ) ); + if ( $pagesetParams !== null ) { + // The pageset is still in use, check if all props have finished + $incompleteProps = array_intersect_key( $propModules, $qc ); + if ( count( $incompleteProps ) > 0 ) { + // Properties are not done, continue with the same pageset state - copy current parameters + $main = $this->getMain(); + $contValues = array(); + foreach ( $pagesetParams as $param ) { + // The param name is already prefix-encoded + $contValues[$param] = $main->getVal( $param ); + } + } elseif ( $this->mGeneratorContinue !== null ) { + // Move to the next set of pages produced by pageset, properties need to be restarted + $contValues = $this->mGeneratorContinue; + $pagesetParams = array_keys( $contValues ); + $completeModules = array_diff_key( $completeModules, $propModules ); + } else { + // Done with the pageset, finish up with the the lists and meta modules + $pagesetParams = null; + } + } + + $continue = '||' . implode( '|', array_keys( $completeModules ) ); + if ( $pagesetParams !== null ) { + // list of all pageset parameters to use in the next request + $continue = implode( '|', $pagesetParams ) . $continue; + } else { + // we are done with the pageset + $contValues = array(); + $continue = '-' . $continue; + } + $contValues['continue'] = $continue; + foreach ( $qc as $qcModule ) { + foreach ( $qcModule as $qcKey => $qcValue ) { + $contValues[$qcKey] = $qcValue; + } + } + $this->getResult()->addValue( null, 'continue', $contValues ); + } + + /** + * Parse 'continue' parameter into the list of complete modules and a list of generator parameters + * @param array|null $pagesetParams returns list of generator params or null if pageset is done + * @param array|null $completeModules returns list of finished modules (as keys), or null if legacy + */ + private function initContinue( &$pagesetParams, &$completeModules ) { + $pagesetParams = array(); + $continue = $this->mParams['continue']; + if ( $continue !== null ) { + $this->mUseLegacyContinue = false; + if ( $continue !== '' ) { + // Format: ' pagesetParam1 | pagesetParam2 || module1 | module2 | module3 | ... + // If pageset is done, use '-' + $continue = explode( '||', $continue ); + $this->dieContinueUsageIf( count( $continue ) !== 2 ); + if ( $continue[0] === '-' ) { + $pagesetParams = null; // No need to execute pageset + } elseif ( $continue[0] !== '' ) { + // list of pageset params that might need to be repeated + $pagesetParams = explode( '|', $continue[0] ); + } + $continue = $continue[1]; + } + if ( $continue !== '' ) { + $completeModules = array_flip( explode( '|', $continue ) ); + } else { + $completeModules = array(); + } + } else { + $this->mUseLegacyContinue = true; + $completeModules = null; + } + } + + /** + * Validate sub-modules, filter out completed ones, and do requestExtraData() + * @param array $allModules An dict of name=>instance of all modules requested by the client + * @param array|null $completeModules list of finished modules, or null if legacy continue + * @param bool $usePageset True if pageset will be executed + * @return array of modules to be processed during this execution + */ + private function initModules( $allModules, $completeModules, $usePageset ) { + $modules = $allModules; + $tmp = $completeModules; + $wasPosted = $this->getRequest()->wasPosted(); + $main = $this->getMain(); + + /** @var $module ApiQueryBase */ + foreach ( $allModules as $moduleName => $module ) { + if ( !$wasPosted && $module->mustBePosted() ) { + $this->dieUsageMsgOrDebug( array( 'mustbeposted', $moduleName ) ); + } + if ( $completeModules !== null && array_key_exists( $moduleName, $completeModules ) ) { + // If this module is done, mark all its params as used + $module->extractRequestParams(); + // Make sure this module is not used during execution + unset( $modules[$moduleName] ); + unset( $tmp[$moduleName] ); + } elseif ( $completeModules === null || $usePageset ) { + // Query modules may optimize data requests through the $this->getPageSet() + // object by adding extra fields from the page table. + // This function will gather all the extra request fields from the modules. + $module->requestExtraData( $this->mPageSet ); + } else { + // Error - this prop module must have finished before generator is done + $this->dieContinueUsageIf( $this->mModuleMgr->getModuleGroup( $moduleName ) === 'prop' ); + } + } + $this->dieContinueUsageIf( $completeModules !== null && count( $tmp ) !== 0 ); + return $modules; } /** @@ -320,32 +432,21 @@ class ApiQuery extends ApiBase { } /** - * Query modules may optimize data requests through the $this->getPageSet() object - * by adding extra fields from the page table. - * This function will gather all the extra request fields from the modules. - * @param $modules array of module objects - * @param $pageSet ApiPageSet - */ - private function addCustomFldsToPageSet( $modules, $pageSet ) { - // Query all requested modules. - /** - * @var $module ApiQueryBase - */ - foreach ( $modules as $module ) { - $module->requestExtraData( $pageSet ); - } - } - - /** * Create instances of all modules requested by the client - * @param $modules Array to append instantiated modules to - * @param $param string Parameter name to read modules from - * @param $moduleList Array array(modulename => classname) + * @param array $modules to append instantiated modules to + * @param string $param Parameter name to read modules from */ - private function instantiateModules( &$modules, $param, $moduleList ) { - if ( isset( $this->params[$param] ) ) { - foreach ( $this->params[$param] as $moduleName ) { - $modules[] = new $moduleList[$moduleName] ( $this, $moduleName ); + private function instantiateModules( &$modules, $param ) { + if ( isset( $this->mParams[$param] ) ) { + foreach ( $this->mParams[$param] as $moduleName ) { + $instance = $this->mModuleMgr->getModule( $moduleName, $param ); + if ( $instance === null ) { + ApiBase::dieDebug( __METHOD__, 'Error instantiating module' ); + } + // Ignore duplicates. TODO 2.0: die()? + if ( !array_key_exists( $moduleName, $modules ) ) { + $modules[$moduleName] = $instance; + } } } } @@ -363,85 +464,25 @@ class ApiQuery extends ApiBase { // more than 380K. The maximum revision size is in the megabyte range, // and the maximum result size must be even higher than that. - // Title normalizations - $normValues = array(); - foreach ( $pageSet->getNormalizedTitles() as $rawTitleStr => $titleStr ) { - $normValues[] = array( - 'from' => $rawTitleStr, - 'to' => $titleStr - ); + $values = $pageSet->getNormalizedTitlesAsResult( $result ); + if ( $values ) { + $result->addValue( 'query', 'normalized', $values ); } - - if ( count( $normValues ) ) { - $result->setIndexedTagName( $normValues, 'n' ); - $result->addValue( 'query', 'normalized', $normValues ); + $values = $pageSet->getConvertedTitlesAsResult( $result ); + if ( $values ) { + $result->addValue( 'query', 'converted', $values ); } - - // Title conversions - $convValues = array(); - foreach ( $pageSet->getConvertedTitles() as $rawTitleStr => $titleStr ) { - $convValues[] = array( - 'from' => $rawTitleStr, - 'to' => $titleStr - ); + $values = $pageSet->getInterwikiTitlesAsResult( $result, $this->mParams['iwurl'] ); + if ( $values ) { + $result->addValue( 'query', 'interwiki', $values ); } - - if ( count( $convValues ) ) { - $result->setIndexedTagName( $convValues, 'c' ); - $result->addValue( 'query', 'converted', $convValues ); + $values = $pageSet->getRedirectTitlesAsResult( $result ); + if ( $values ) { + $result->addValue( 'query', 'redirects', $values ); } - - // Interwiki titles - $intrwValues = array(); - foreach ( $pageSet->getInterwikiTitles() as $rawTitleStr => $interwikiStr ) { - $item = array( - 'title' => $rawTitleStr, - 'iw' => $interwikiStr, - ); - if ( $this->iwUrl ) { - $title = Title::newFromText( $rawTitleStr ); - $item['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); - } - $intrwValues[] = $item; - } - - if ( count( $intrwValues ) ) { - $result->setIndexedTagName( $intrwValues, 'i' ); - $result->addValue( 'query', 'interwiki', $intrwValues ); - } - - // Show redirect information - $redirValues = array(); - /** - * @var $titleTo Title - */ - foreach ( $pageSet->getRedirectTitles() as $titleStrFrom => $titleTo ) { - $r = array( - 'from' => strval( $titleStrFrom ), - 'to' => $titleTo->getPrefixedText(), - ); - if ( $titleTo->getFragment() !== '' ) { - $r['tofragment'] = $titleTo->getFragment(); - } - $redirValues[] = $r; - } - - if ( count( $redirValues ) ) { - $result->setIndexedTagName( $redirValues, 'r' ); - $result->addValue( 'query', 'redirects', $redirValues ); - } - - // Missing revision elements - $missingRevIDs = $pageSet->getMissingRevisionIDs(); - if ( count( $missingRevIDs ) ) { - $revids = array(); - foreach ( $missingRevIDs as $revid ) { - $revids[$revid] = array( - 'revid' => $revid - ); - } - $result->setIndexedTagName( $revids, 'rev' ); - $result->addValue( 'query', 'badrevids', $revids ); + $values = $pageSet->getMissingRevisionIDsAsResult( $result ); + if ( $values ) { + $result->addValue( 'query', 'badrevids', $values ); } // Page elements @@ -466,6 +507,7 @@ class ApiQuery extends ApiBase { ); } // Report special pages + /** @var $title Title */ foreach ( $pageSet->getSpecialTitles() as $fakeId => $title ) { $vals = array(); ApiQueryBase::addTitleInfo( $vals, $title ); @@ -489,7 +531,7 @@ class ApiQuery extends ApiBase { } if ( count( $pages ) ) { - if ( $this->params['indexpageids'] ) { + if ( $this->mParams['indexpageids'] ) { $pageIDs = array_keys( $pages ); // json treats all map keys as strings - converting to match $pageIDs = array_map( 'strval', $pageIDs ); @@ -500,21 +542,44 @@ class ApiQuery extends ApiBase { $result->setIndexedTagName( $pages, 'page' ); $result->addValue( 'query', 'pages', $pages ); } - if ( $this->params['export'] ) { + if ( $this->mParams['export'] ) { $this->doExport( $pageSet, $result ); } } /** - * @param $pageSet ApiPageSet Pages to be exported - * @param $result ApiResult Result to output to + * This method is called by the generator base when generator in the smart-continue + * mode tries to set 'query-continue' value. ApiQuery stores those values separately + * until the post-processing when it is known if the generation should continue or repeat. + * @param ApiQueryGeneratorBase $module generator module + * @param string $paramName + * @param mixed $paramValue + * @return bool true if processed, false if this is a legacy continue + */ + public function setGeneratorContinue( $module, $paramName, $paramValue ) { + if ( $this->mUseLegacyContinue ) { + return false; + } + $paramName = $module->encodeParamName( $paramName ); + if ( $this->mGeneratorContinue === null ) { + $this->mGeneratorContinue = array(); + } + $this->mGeneratorContinue[$paramName] = $paramValue; + return true; + } + + /** + * @param $pageSet ApiPageSet Pages to be exported + * @param $result ApiResult Result to output to */ - private function doExport( $pageSet, $result ) { + private function doExport( $pageSet, $result ) { $exportTitles = array(); $titles = $pageSet->getGoodTitles(); if ( count( $titles ) ) { + $user = $this->getUser(); + /** @var $title Title */ foreach ( $titles as $title ) { - if ( $title->userCan( 'read' ) ) { + if ( $title->userCan( 'read', $user ) ) { $exportTitles[] = $title; } } @@ -536,7 +601,7 @@ class ApiQuery extends ApiBase { // It's not continuable, so it would cause more // problems than it'd solve $result->disableSizeCheck(); - if ( $this->params['exportnowrap'] ) { + if ( $this->mParams['exportnowrap'] ) { $result->reset(); // Raw formatter will handle this $result->addValue( null, 'text', $exportxml ); @@ -549,80 +614,30 @@ class ApiQuery extends ApiBase { $result->enableSizeCheck(); } - /** - * Create a generator object of the given type and return it - * @param $generatorName string Module name - * @return ApiQueryGeneratorBase - */ - public function newGenerator( $generatorName ) { - // Find class that implements requested generator - if ( isset( $this->mQueryListModules[$generatorName] ) ) { - $className = $this->mQueryListModules[$generatorName]; - } elseif ( isset( $this->mQueryPropModules[$generatorName] ) ) { - $className = $this->mQueryPropModules[$generatorName]; - } else { - ApiBase::dieDebug( __METHOD__, "Unknown generator=$generatorName" ); - } - $generator = new $className ( $this, $generatorName ); - if ( !$generator instanceof ApiQueryGeneratorBase ) { - $this->dieUsage( "Module $generatorName cannot be used as a generator", 'badgenerator' ); - } - $generator->setGeneratorMode(); - return $generator; - } - - /** - * For generator mode, execute generator, and use its output as new - * ApiPageSet - * @param $generator ApiQueryGeneratorBase Generator Module - * @param $modules array of module objects - */ - protected function executeGeneratorModule( $generator, $modules ) { - // Generator results - $resultPageSet = new ApiPageSet( $this, $this->redirects, $this->convertTitles ); - - // Add any additional fields modules may need - $generator->requestExtraData( $this->mPageSet ); - $this->addCustomFldsToPageSet( $modules, $resultPageSet ); - - // Populate page information with the original user input - $this->mPageSet->execute(); - - // populate resultPageSet with the generator output - $generator->profileIn(); - $generator->executeGenerator( $resultPageSet ); - wfRunHooks( 'APIQueryGeneratorAfterExecute', array( &$generator, &$resultPageSet ) ); - $resultPageSet->finishPageSetGeneration(); - $generator->profileOut(); - - // Swap the resulting pageset back in - $this->mPageSet = $resultPageSet; - } - - public function getAllowedParams() { - return array( + public function getAllowedParams( $flags = 0 ) { + $result = array( 'prop' => array( ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => $this->mPropModuleNames + ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'prop' ) ), 'list' => array( ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => $this->mListModuleNames + ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'list' ) ), 'meta' => array( ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => $this->mMetaModuleNames + ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'meta' ) ), - 'generator' => array( - ApiBase::PARAM_TYPE => $this->mAllowedGenerators - ), - 'redirects' => false, - 'converttitles' => false, 'indexpageids' => false, 'export' => false, 'exportnowrap' => false, 'iwurl' => false, + 'continue' => null, ); + if ( $flags ) { + $result += $this->getPageSet()->getFinalParams( $flags ); + } + return $result; } /** @@ -630,42 +645,40 @@ class ApiQuery extends ApiBase { * @return string */ public function makeHelpMsg() { - // Make sure the internal object is empty - // (just in case a sub-module decides to optimize during instantiation) - $this->mPageSet = null; + + // Use parent to make default message for the query module + $msg = parent::makeHelpMsg(); $querySeparator = str_repeat( '--- ', 12 ); $moduleSeparator = str_repeat( '*** ', 14 ); - $msg = "\n$querySeparator Query: Prop $querySeparator\n\n"; - $msg .= $this->makeHelpMsgHelper( $this->mQueryPropModules, 'prop' ); + $msg .= "\n$querySeparator Query: Prop $querySeparator\n\n"; + $msg .= $this->makeHelpMsgHelper( 'prop' ); $msg .= "\n$querySeparator Query: List $querySeparator\n\n"; - $msg .= $this->makeHelpMsgHelper( $this->mQueryListModules, 'list' ); + $msg .= $this->makeHelpMsgHelper( 'list' ); $msg .= "\n$querySeparator Query: Meta $querySeparator\n\n"; - $msg .= $this->makeHelpMsgHelper( $this->mQueryMetaModules, 'meta' ); + $msg .= $this->makeHelpMsgHelper( 'meta' ); $msg .= "\n\n$moduleSeparator Modules: continuation $moduleSeparator\n\n"; - // Use parent to make default message for the query module - $msg = parent::makeHelpMsg() . $msg; - return $msg; } /** - * For all modules in $moduleList, generate help messages and join them together - * @param $moduleList Array array(modulename => classname) - * @param $paramName string Parameter name + * For all modules of a given group, generate help messages and join them together + * @param string $group Module group * @return string */ - private function makeHelpMsgHelper( $moduleList, $paramName ) { + private function makeHelpMsgHelper( $group ) { $moduleDescriptions = array(); - foreach ( $moduleList as $moduleName => $moduleClass ) { + $moduleNames = $this->mModuleMgr->getNames( $group ); + sort( $moduleNames ); + foreach ( $moduleNames as $name ) { /** * @var $module ApiQueryBase */ - $module = new $moduleClass( $this, $moduleName, null ); + $module = $this->mModuleMgr->getModule( $name ); - $msg = ApiMain::makeHelpMsgHeader( $module, $paramName ); + $msg = ApiMain::makeHelpMsgHeader( $module, $group ); $msg2 = $module->makeHelpMsg(); if ( $msg2 !== false ) { $msg .= $msg2; @@ -679,46 +692,23 @@ class ApiQuery extends ApiBase { return implode( "\n", $moduleDescriptions ); } - /** - * 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 - */ - public function makeHelpMsgParameters() { - $psModule = new ApiPageSet( $this ); - return $psModule->makeHelpMsgParameters() . parent::makeHelpMsgParameters(); - } - public function shouldCheckMaxlag() { return true; } public function getParamDescription() { - return array( + return $this->getPageSet()->getParamDescription() + array( 'prop' => 'Which properties to get for the titles/revisions/pageids. Module help is available below', 'list' => 'Which lists to get. Module help is available below', 'meta' => 'Which metadata to get about the site. Module help is available below', - 'generator' => array( 'Use the output of a list as the input for other prop/list/meta items', - 'NOTE: generator parameter names must be prefixed with a \'g\', see examples' ), - 'redirects' => 'Automatically resolve redirects', - 'converttitles' => array( "Convert titles to other variants if necessary. Only works if the wiki's content language supports variant conversion.", - 'Languages that support variant conversion include ' . implode( ', ', LanguageConverter::$languagesWithVariants ) ), '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', 'iwurl' => 'Whether to get the full URL if the title is an interwiki link', + 'continue' => array( + 'When present, formats query-continue as key-value pairs that should simply be merged into the original request.', + 'This parameter must be set to an empty string in the initial query.', + 'This parameter is recommended for all new development, and will be made default in the next API version.' ), ); } @@ -731,15 +721,16 @@ class ApiQuery extends ApiBase { } public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'badgenerator', 'info' => 'Module $generatorName cannot be used as a generator' ), - ) ); + return array_merge( + parent::getPossibleErrors(), + $this->getPageSet()->getPossibleErrors() + ); } public function getExamples() { return array( - 'api.php?action=query&prop=revisions&meta=siteinfo&titles=Main%20Page&rvprop=user|comment', - 'api.php?action=query&generator=allpages&gapprefix=API/&prop=revisions', + 'api.php?action=query&prop=revisions&meta=siteinfo&titles=Main%20Page&rvprop=user|comment&continue=', + 'api.php?action=query&generator=allpages&gapprefix=API/&prop=revisions&continue=', ); } @@ -750,12 +741,4 @@ class ApiQuery extends ApiBase { 'https://www.mediawiki.org/wiki/API:Lists', ); } - - public function getVersion() { - $psModule = new ApiPageSet( $this ); - $vers = array(); - $vers[] = __CLASS__ . ': $Id$'; - $vers[] = $psModule->getVersion(); - return $vers; - } } diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php index 4f4c77f0..496a0eb8 100644 --- a/includes/api/ApiQueryAllCategories.php +++ b/includes/api/ApiQueryAllCategories.php @@ -60,10 +60,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { 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" ); - } + $this->dieContinueUsageIf( count( $cont ) != 1 ); $op = $params['dir'] == 'descending' ? '<' : '>'; $cont_from = $db->addQuotes( $cont[0] ); $this->addWhere( "cat_title $op= $cont_from" ); @@ -81,7 +78,6 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { } else { $this->addWhereRange( 'cat_pages', 'older', $max, $min); } - if ( isset( $params['prefix'] ) ) { $this->addWhere( 'cat_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); @@ -225,12 +221,6 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { 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', @@ -241,8 +231,4 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Allcategories'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php index b562da8e..e24b162c 100644 --- a/includes/api/ApiQueryAllImages.php +++ b/includes/api/ApiQueryAllImages.php @@ -41,8 +41,8 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { } /** - * 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. + * Override parent method to make sure the repo's DB is used + * which may not necessarily be the same as the local DB. * * TODO: allow querying non-local repos. * @return DatabaseBase @@ -93,7 +93,10 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { $prop = array_flip( $params['prop'] ); $this->addFields( LocalFile::selectFields() ); - $dir = ( in_array( $params['dir'], array( 'descending', 'older' ) ) ? 'older' : 'newer' ); + $ascendingOrder = true; + if ( $params['dir'] == 'descending' || $params['dir'] == 'older' ) { + $ascendingOrder = false; + } if ( $params['sort'] == 'name' ) { // Check mutually exclusive params @@ -110,19 +113,16 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { // 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" ); + $this->dieContinueUsageIf( count( $cont ) != 1 ); + $op = ( $ascendingOrder ? '>' : '<' ); + $continueFrom = $db->addQuotes( $cont[0] ); + $this->addWhere( "img_name $op= $continueFrom" ); } // 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 ); + $this->addWhereRange( 'img_name', ( $ascendingOrder ? 'newer' : 'older' ), $from, $to ); if ( isset( $params['prefix'] ) ) { $this->addWhere( 'img_name' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); @@ -135,13 +135,13 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { $this->dieUsage( "Parameter '{$prefix}{$pname}' can only be used with {$prefix}sort=name", 'badparams' ); } } - if (!is_null( $params['user'] ) && $params['filterbots'] != 'all') { + 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' ); + $this->dieUsage( "Parameters '{$prefix}user' and '{$prefix}filterbots' cannot be used together", 'badparams' ); } // Pagination - $this->addTimestampWhereRange( 'img_timestamp', $dir, $params['start'], $params['end'] ); + $this->addTimestampWhereRange( 'img_timestamp', ( $ascendingOrder ? 'newer' : 'older' ), $params['start'], $params['end'] ); // Image filters if ( !is_null( $params['user'] ) ) { @@ -149,8 +149,6 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { } 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( @@ -158,6 +156,8 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { 'ug_user = img_user' ) ) ) ); + $groupCond = ( $params['filterbots'] == 'nobots' ? 'NULL': 'NOT NULL' ); + $this->addWhere( "ug_group IS $groupCond" ); } } @@ -172,12 +172,13 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { $sha1 = false; if ( isset( $params['sha1'] ) ) { - if ( !$this->validateSha1Hash( $params['sha1'] ) ) { + $sha1 = strtolower( $params['sha1'] ); + if ( !$this->validateSha1Hash( $sha1 ) ) { $this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' ); } - $sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 ); + $sha1 = wfBaseConvert( $sha1, 16, 36, 31 ); } elseif ( isset( $params['sha1base36'] ) ) { - $sha1 = $params['sha1base36']; + $sha1 = strtolower( $params['sha1base36'] ); if ( !$this->validateSha1Base36Hash( $sha1 ) ) { $this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' ); } @@ -200,16 +201,19 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { $limit = $params['limit']; $this->addOption( 'LIMIT', $limit + 1 ); - $sort = ( $dir == 'older' ? ' DESC' : '' ); + $sortFlag = ''; + if ( !$ascendingOrder ) { + $sortFlag = ' 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( 'ORDER BY', 'img_timestamp' . $sortFlag ); + if ( !is_null( $params['user'] ) ) { $this->addOption( 'USE INDEX', array( 'image' => 'img_usertext_timestamp' ) ); + } else { + $this->addOption( 'USE INDEX', array( 'image' => 'img_timestamp' ) ); } } else { - $this->addOption( 'ORDER BY', 'img_name' . $sort ); + $this->addOption( 'ORDER BY', 'img_name' . $sortFlag ); } $res = $this->select( __METHOD__ ); @@ -272,7 +276,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { 'descending', // sort=timestamp 'newer', - 'older', + 'older' ) ), 'from' => null, @@ -373,12 +377,11 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { 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' => 'badparams', 'info' => "Parameters '{$p}user' and '{$p}filterbots' cannot be used together" ), array( 'code' => 'unsupportedrepo', 'info' => 'Local file repository does not support querying all images' ), array( 'code' => 'mimesearchdisabled', 'info' => 'MIME search disabled in Miser Mode' ), array( 'code' => 'invalidsha1hash', 'info' => 'The SHA1 hash provided is not valid' ), array( 'code' => 'invalidsha1base36hash', 'info' => 'The SHA1Base36 hash provided is not valid' ), - array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), ) ); } @@ -402,8 +405,4 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { 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 da4840f0..e355f8b0 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -32,7 +32,34 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { public function __construct( $query, $moduleName ) { - parent::__construct( $query, $moduleName, 'al' ); + switch ( $moduleName ) { + case 'alllinks': + $prefix = 'al'; + $this->table = 'pagelinks'; + $this->tablePrefix = 'pl_'; + $this->dfltNamespace = NS_MAIN; + $this->indexTag = 'l'; + $this->description = 'Enumerate all links that point to a given namespace'; + $this->descriptionLink = 'link'; + $this->descriptionLinked = 'linked'; + $this->descriptionLinking = 'linking'; + break; + case 'alltransclusions': + $prefix = 'at'; + $this->table = 'templatelinks'; + $this->tablePrefix = 'tl_'; + $this->dfltNamespace = NS_TEMPLATE; + $this->indexTag = 't'; + $this->description = 'List all transclusions (pages embedded using {{x}}), including non-existing'; + $this->descriptionLink = 'transclusion'; + $this->descriptionLinked = 'transcluded'; + $this->descriptionLinking = 'transcluding'; + break; + default: + ApiBase::dieDebug( __METHOD__, 'Unknown module name' ); + } + + parent::__construct( $query, $moduleName, $prefix ); } public function execute() { @@ -55,75 +82,71 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $db = $this->getDB(); $params = $this->extractRequestParams(); + $pfx = $this->tablePrefix; $prop = array_flip( $params['prop'] ); $fld_ids = isset( $prop['ids'] ); $fld_title = isset( $prop['title'] ); if ( $params['unique'] ) { - if ( !is_null( $resultPageSet ) ) { - $this->dieUsage( $this->getModuleName() . ' cannot be used as a generator in unique links mode', 'params' ); - } if ( $fld_ids ) { - $this->dieUsage( $this->getModuleName() . ' cannot return corresponding page ids in unique links mode', 'params' ); + $this->dieUsage( + "{$this->getModuleName()} cannot return corresponding page ids in unique {$this->descriptionLink}s mode", + 'params' ); } $this->addOption( 'DISTINCT' ); } - $this->addTables( 'pagelinks' ); - $this->addWhereFld( 'pl_namespace', $params['namespace'] ); + $this->addTables( $this->table ); + $this->addWhereFld( $pfx . 'namespace', $params['namespace'] ); - if ( !is_null( $params['from'] ) && !is_null( $params['continue'] ) ) { - $this->dieUsage( 'alcontinue and alfrom cannot be used together', 'params' ); - } - if ( !is_null( $params['continue'] ) ) { + $continue = !is_null( $params['continue'] ); + if ( $continue ) { $continueArr = explode( '|', $params['continue'] ); $op = $params['dir'] == 'descending' ? '<' : '>'; if ( $params['unique'] ) { - if ( count( $continueArr ) != 1 ) { - $this->dieUsage( 'Invalid continue parameter', 'badcontinue' ); - } + $this->dieContinueUsageIf( count( $continueArr ) != 1 ); $continueTitle = $db->addQuotes( $continueArr[0] ); - $this->addWhere( "pl_title $op= $continueTitle" ); + $this->addWhere( "{$pfx}title $op= $continueTitle" ); } else { - if ( count( $continueArr ) != 2 ) { - $this->dieUsage( 'Invalid continue parameter', 'badcontinue' ); - } + $this->dieContinueUsageIf( count( $continueArr ) != 2 ); $continueTitle = $db->addQuotes( $continueArr[0] ); $continueFrom = intval( $continueArr[1] ); $this->addWhere( - "pl_title $op $continueTitle OR " . - "(pl_title = $continueTitle AND " . - "pl_from $op= $continueFrom)" + "{$pfx}title $op $continueTitle OR " . + "({$pfx}title = $continueTitle AND " . + "{$pfx}from $op= $continueFrom)" ); } } - $from = ( is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); + // 'continue' always overrides 'from' + $from = ( $continue || is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) ); $to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) ); - $this->addWhereRange( 'pl_title', 'newer', $from, $to ); + $this->addWhereRange( $pfx . 'title', 'newer', $from, $to ); if ( isset( $params['prefix'] ) ) { - $this->addWhere( 'pl_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); + $this->addWhere( $pfx . 'title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); } - $this->addFields( 'pl_title' ); - $this->addFieldsIf( 'pl_from', !$params['unique'] ); + $this->addFields( array( 'pl_title' => $pfx . 'title' ) ); + $this->addFieldsIf( array( 'pl_from' => $pfx . 'from' ), !$params['unique'] ); - $this->addOption( 'USE INDEX', 'pl_namespace' ); + $this->addOption( 'USE INDEX', $pfx . 'namespace' ); $limit = $params['limit']; $this->addOption( 'LIMIT', $limit + 1 ); $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); $orderBy = array(); - $orderBy[] = 'pl_title' . $sort; + $orderBy[] = $pfx . 'title' . $sort; if ( !$params['unique'] ) { - $orderBy[] = 'pl_from' . $sort; + $orderBy[] = $pfx . 'from' . $sort; } $this->addOption( 'ORDER BY', $orderBy ); $res = $this->select( __METHOD__ ); $pageids = array(); + $titles = array(); $count = 0; $result = $this->getResult(); foreach ( $res as $row ) { @@ -132,7 +155,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { if ( $params['unique'] ) { $this->setContinueEnumParameter( 'continue', $row->pl_title ); } else { - $this->setContinueEnumParameter( 'continue', $row->pl_title . "|" . $row->pl_from ); + $this->setContinueEnumParameter( 'continue', $row->pl_title . '|' . $row->pl_from ); } break; } @@ -151,17 +174,21 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { if ( $params['unique'] ) { $this->setContinueEnumParameter( 'continue', $row->pl_title ); } else { - $this->setContinueEnumParameter( 'continue', $row->pl_title . "|" . $row->pl_from ); + $this->setContinueEnumParameter( 'continue', $row->pl_title . '|' . $row->pl_from ); } break; } + } elseif ( $params['unique'] ) { + $titles[] = Title::makeTitle( $params['namespace'], $row->pl_title ); } else { $pageids[] = $row->pl_from; } } if ( is_null( $resultPageSet ) ) { - $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'l' ); + $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), $this->indexTag ); + } elseif ( $params['unique'] ) { + $resultPageSet->populateFromTitles( $titles ); } else { $resultPageSet->populateFromPageIDs( $pageids ); } @@ -183,7 +210,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { ) ), 'namespace' => array( - ApiBase::PARAM_DFLT => 0, + ApiBase::PARAM_DFLT => $this->dfltNamespace, ApiBase::PARAM_TYPE => 'namespace' ), 'limit' => array( @@ -205,18 +232,23 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { public function getParamDescription() { $p = $this->getModulePrefix(); + $link = $this->descriptionLink; + $linking = $this->descriptionLinking; return array( - 'from' => 'The page title to start enumerating from', - 'to' => 'The page title to stop enumerating at', - 'prefix' => 'Search for all page titles that begin with this value', - 'unique' => "Only show unique links. Cannot be used with generator or {$p}prop=ids", + 'from' => "The title of the $link to start enumerating from", + 'to' => "The title of the $link to stop enumerating at", + 'prefix' => "Search for all $link titles that begin with this value", + 'unique' => array( + "Only show distinct $link titles. Cannot be used with {$p}prop=ids.", + 'When used as a generator, yields target pages instead of source pages.', + ), 'prop' => array( 'What pieces of information to include', - " ids - Adds pageid of where the link is from (Cannot be used with {$p}unique)", - ' title - Adds the title of the link', + " ids - Adds the pageid of the $linking page (Cannot be used with {$p}unique)", + " title - Adds the title of the $link", ), 'namespace' => 'The namespace to enumerate', - 'limit' => 'How many total links to return', + 'limit' => "How many total items to return", 'continue' => 'When more results are available, use this to continue', 'dir' => 'The direction in which to list', ); @@ -235,30 +267,34 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { } public function getDescription() { - return 'Enumerate all links that point to a given namespace'; + return $this->description; } public function getPossibleErrors() { $m = $this->getModuleName(); + $link = $this->descriptionLink; return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'params', 'info' => "{$m} cannot be used as a generator in unique links mode" ), - array( 'code' => 'params', 'info' => "{$m} cannot return corresponding page ids in unique links mode" ), - array( 'code' => 'params', 'info' => 'alcontinue and alfrom cannot be used together' ), - array( 'code' => 'badcontinue', 'info' => 'Invalid continue parameter' ), + array( 'code' => 'params', 'info' => "{$m} cannot return corresponding page ids in unique {$link}s mode" ), ) ); } public function getExamples() { + $p = $this->getModulePrefix(); + $link = $this->descriptionLink; + $linked = $this->descriptionLinked; return array( - 'api.php?action=query&list=alllinks&alunique=&alfrom=B', + "api.php?action=query&list=all{$link}s&{$p}from=B&{$p}prop=ids|title" + => "List $linked titles with page ids they are from, including missing ones. Start at B", + "api.php?action=query&list=all{$link}s&{$p}unique=&{$p}from=B" + => "List unique $linked titles", + "api.php?action=query&generator=all{$link}s&g{$p}unique=&g{$p}from=B" + => "Gets all $link targets, marking the missing ones", + "api.php?action=query&generator=all{$link}s&g{$p}from=B" + => "Gets pages containing the {$link}s", ); } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Alllinks'; - } - - public function getVersion() { - return __CLASS__ . ': $Id$'; + return "https://www.mediawiki.org/wiki/API:All{$this->descriptionLink}s"; } } diff --git a/includes/api/ApiQueryAllMessages.php b/includes/api/ApiQueryAllMessages.php index f5e1146b..c9811b0d 100644 --- a/includes/api/ApiQueryAllMessages.php +++ b/includes/api/ApiQueryAllMessages.php @@ -39,8 +39,9 @@ class ApiQueryAllMessages extends ApiQueryBase { $params = $this->extractRequestParams(); if ( is_null( $params['lang'] ) ) { - global $wgLang; - $langObj = $wgLang; + $langObj = $this->getLanguage(); + } elseif ( !Language::isValidCode( $params['lang'] ) ) { + $this->dieUsage( 'Invalid language code for parameter lang', 'invalidlang' ); } else { $langObj = Language::factory( $params['lang'] ); } @@ -48,7 +49,7 @@ class ApiQueryAllMessages extends ApiQueryBase { if ( $params['enableparser'] ) { if ( !is_null( $params['title'] ) ) { $title = Title::newFromText( $params['title'] ); - if ( !$title ) { + if ( !$title || $title->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); } } else { @@ -116,7 +117,7 @@ class ApiQueryAllMessages extends ApiQueryBase { $lang = $langObj->getCode(); $customisedMessages = AllmessagesTablePager::getCustomisedStatuses( - array_map( array( $langObj, 'ucfirst'), $messages_target ), $lang, $lang != $wgContLang->getCode() ); + array_map( array( $langObj, 'ucfirst' ), $messages_target ), $lang, $lang != $wgContLang->getCode() ); $customised = $params['customised'] === 'modified'; } @@ -143,7 +144,7 @@ class ApiQueryAllMessages extends ApiQueryBase { } if ( $customiseFilterEnabled ) { - $messageIsCustomised = isset( $customisedMessages['pages'][ $langObj->ucfirst( $message ) ] ); + $messageIsCustomised = isset( $customisedMessages['pages'][$langObj->ucfirst( $message )] ); if ( $customised === $messageIsCustomised ) { if ( $customised ) { $a['customised'] = ''; @@ -256,6 +257,12 @@ class ApiQueryAllMessages extends ApiQueryBase { ); } + public function getPossibleErrors() { + return array_merge( parent::getPossibleErrors(), array( + array( 'code' => 'invalidlang', 'info' => 'Invalid language code for parameter lang' ), + ) ); + } + public function getResultProperties() { return array( '' => array( @@ -291,8 +298,4 @@ class ApiQueryAllMessages extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Meta#allmessages_.2F_am'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryAllPages.php b/includes/api/ApiQueryAllPages.php index 16cc31d2..d718b967 100644 --- a/includes/api/ApiQueryAllPages.php +++ b/includes/api/ApiQueryAllPages.php @@ -69,10 +69,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { 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" ); - } + $this->dieContinueUsageIf( count( $cont ) != 1 ); $op = $params['dir'] == 'descending' ? '<' : '>'; $cont_from = $db->addQuotes( $cont[0] ); $this->addWhere( "page_title $op= $cont_from" ); @@ -120,7 +117,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { if ( count( $params['prtype'] ) || $params['prexpiry'] != 'all' ) { $this->addTables( 'page_restrictions' ); $this->addWhere( 'page_id=pr_page' ); - $this->addWhere( 'pr_expiry>' . $db->addQuotes( $db->timestamp() ) ); + $this->addWhere( "pr_expiry > {$db->addQuotes( $db->timestamp() )} OR pr_expiry IS NULL" ); if ( count( $params['prtype'] ) ) { $this->addWhereFld( 'pr_type', $params['prtype'] ); @@ -138,8 +135,6 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { } elseif ( $params['prfiltercascade'] == 'noncascading' ) { $this->addWhereFld( 'pr_cascade', 0 ); } - - $this->addOption( 'DISTINCT' ); } $forceNameTitleIndex = false; @@ -149,6 +144,8 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { $this->addWhere( "pr_expiry != {$db->addQuotes( $db->getInfinity() )}" ); } + $this->addOption( 'DISTINCT' ); + } elseif ( isset( $params['prlevel'] ) ) { $this->dieUsage( 'prlevel may not be used without prtype', 'params' ); } @@ -226,7 +223,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { 'to' => null, 'prefix' => null, 'namespace' => array( - ApiBase::PARAM_DFLT => 0, + ApiBase::PARAM_DFLT => NS_MAIN, ApiBase::PARAM_TYPE => 'namespace', ), 'filterredir' => array( @@ -336,7 +333,6 @@ 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' ), ) ); } @@ -351,7 +347,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { 'Show info about 4 pages starting at the letter "T"', ), 'api.php?action=query&generator=allpages&gaplimit=2&gapfilterredir=nonredirects&gapfrom=Re&prop=revisions&rvprop=content' => array( - 'Show content of first 2 non-redirect pages begining at "Re"', + 'Show content of first 2 non-redirect pages beginning at "Re"', ) ); } @@ -359,8 +355,4 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Allpages'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php index 7f50cbad..7283aa00 100644 --- a/includes/api/ApiQueryAllUsers.php +++ b/includes/api/ApiQueryAllUsers.php @@ -37,7 +37,7 @@ class ApiQueryAllUsers extends ApiQueryBase { /** * This function converts the user name to a canonical form * which is stored in the database. - * @param String $name + * @param string $name * @return String */ private function getCanonicalUserName( $name ) { @@ -81,12 +81,18 @@ class ApiQueryAllUsers extends ApiQueryBase { $db->buildLike( $this->getCanonicalUserName( $params['prefix'] ), $db->anyString() ) ); } - if ( !is_null( $params['rights'] ) ) { + if ( !is_null( $params['rights'] ) && count( $params['rights'] ) ) { $groups = array(); foreach( $params['rights'] as $r ) { $groups = array_merge( $groups, User::getGroupsWithPermission( $r ) ); } + // no group with the given right(s) exists, no need for a query + if( !count( $groups ) ) { + $this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), '' ); + return; + } + $groups = array_unique( $groups ); if ( is_null( $params['group'] ) ) { @@ -155,7 +161,7 @@ class ApiQueryAllUsers extends ApiQueryBase { $this->addFields( array( 'recentedits' => 'COUNT(*)' ) ); $this->addWhere( 'rc_log_type IS NULL OR rc_log_type != ' . $db->addQuotes( 'newusers' ) ); - $timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays*24*3600 ); + $timestamp = $db->timestamp( wfTimestamp( TS_UNIX ) - $wgActiveUserDays * 24 * 3600 ); $this->addWhere( 'rc_timestamp >= ' . $db->addQuotes( $timestamp ) ); $this->addOption( 'GROUP BY', $userFieldToSort ); @@ -273,7 +279,7 @@ class ApiQueryAllUsers extends ApiQueryBase { if ( $fld_rights ) { if ( !isset( $lastUserData['rights'] ) ) { if ( $lastUserObj ) { - $lastUserData['rights'] = User::getGroupPermissions( $lastUserObj->getAutomaticGroups() ); + $lastUserData['rights'] = User::getGroupPermissions( $lastUserObj->getAutomaticGroups() ); } else { // This should not normally happen $lastUserData['rights'] = array(); @@ -438,8 +444,4 @@ class ApiQueryAllUsers extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Allusers'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php index 06db87bf..3ef6b840 100644 --- a/includes/api/ApiQueryBacklinks.php +++ b/includes/api/ApiQueryBacklinks.php @@ -188,6 +188,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $titleWhere = array(); $allRedirNs = array(); $allRedirDBkey = array(); + /** @var $t Title */ foreach ( $this->redirTitles as $t ) { $redirNs = $t->getNamespace(); $redirDBkey = $t->getDBkey(); @@ -201,6 +202,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if ( !is_null( $this->redirID ) ) { $op = $this->params['dir'] == 'descending' ? '<' : '>'; + /** @var $first Title */ $first = $this->redirTitles[0]; $title = $db->addQuotes( $first->getDBkey() ); $ns = $first->getNamespace(); @@ -246,7 +248,7 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $this->params = $this->extractRequestParams( false ); $this->redirect = isset( $this->params['redirect'] ) && $this->params['redirect']; $userMax = ( $this->redirect ? ApiBase::LIMIT_BIG1 / 2 : ApiBase::LIMIT_BIG1 ); - $botMax = ( $this->redirect ? ApiBase::LIMIT_BIG2 / 2 : ApiBase::LIMIT_BIG2 ); + $botMax = ( $this->redirect ? ApiBase::LIMIT_BIG2 / 2 : ApiBase::LIMIT_BIG2 ); $result = $this->getResult(); @@ -406,20 +408,14 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { // null stuff out now so we know what's set and what isn't $this->rootTitle = $this->contID = $this->redirID = null; $rootNs = intval( $continueList[0] ); - if ( $rootNs === 0 && $continueList[0] !== '0' ) { - // Illegal continue parameter - $this->dieUsage( 'Invalid continue param. You should pass the original value returned by the previous query', '_badcontinue' ); - } + $this->dieContinueUsageIf( $rootNs === 0 && $continueList[0] !== '0' ); + $this->rootTitle = Title::makeTitleSafe( $rootNs, $continueList[1] ); + $this->dieContinueUsageIf( !$this->rootTitle ); - if ( !$this->rootTitle ) { - $this->dieUsage( 'Invalid continue param. You should pass the original value returned by the previous query', '_badcontinue' ); - } $contID = intval( $continueList[2] ); + $this->dieContinueUsageIf( $contID === 0 && $continueList[2] !== '0' ); - if ( $contID === 0 && $continueList[2] !== '0' ) { - $this->dieUsage( 'Invalid continue param. You should pass the original value returned by the previous query', '_badcontinue' ); - } $this->contID = $contID; $id2 = isset( $continueList[3] ) ? $continueList[3] : null; $redirID = intval( $id2 ); @@ -455,12 +451,12 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => 'namespace' ), - 'dir' => array(
- ApiBase::PARAM_DFLT => 'ascending',
- ApiBase::PARAM_TYPE => array(
- 'ascending',
- 'descending'
- )
+ 'dir' => array( + ApiBase::PARAM_DFLT => 'ascending', + ApiBase::PARAM_TYPE => array( + 'ascending', + 'descending' + ) ), 'filterredir' => array( ApiBase::PARAM_DFLT => 'all', @@ -535,7 +531,6 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $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' ), ) ); } @@ -562,8 +557,4 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { public function getHelpUrls() { return $this->helpUrl; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php index 2c48aca0..7819ead4 100644 --- a/includes/api/ApiQueryBase.php +++ b/includes/api/ApiQueryBase.php @@ -112,7 +112,7 @@ abstract class ApiQueryBase extends ApiBase { /** * Add a set of fields to select to the internal array - * @param $value array|string Field name or array of field names + * @param array|string $value Field name or array of field names */ protected function addFields( $value ) { if ( is_array( $value ) ) { @@ -124,8 +124,8 @@ abstract class ApiQueryBase extends ApiBase { /** * Same as addFields(), but add the fields only if a condition is met - * @param $value array|string See addFields() - * @param $condition bool If false, do nothing + * @param array|string $value See addFields() + * @param bool $condition If false, do nothing * @return bool $condition */ protected function addFieldsIf( $value, $condition ) { @@ -162,7 +162,7 @@ abstract class ApiQueryBase extends ApiBase { /** * Same as addWhere(), but add the WHERE clauses only if a condition is met * @param $value mixed See addWhere() - * @param $condition bool If false, do nothing + * @param bool $condition If false, do nothing * @return bool $condition */ protected function addWhereIf( $value, $condition ) { @@ -175,8 +175,8 @@ abstract class ApiQueryBase extends ApiBase { /** * Equivalent to addWhere(array($field => $value)) - * @param $field string Field name - * @param $value string Value; ignored if null or empty array; + * @param string $field Field name + * @param string $value Value; ignored if null or empty array; */ protected function addWhereFld( $field, $value ) { // Use count() to its full documented capabilities to simultaneously @@ -189,14 +189,14 @@ abstract class ApiQueryBase extends ApiBase { /** * Add a WHERE clause corresponding to a range, and an ORDER BY * clause to sort in the right direction - * @param $field string Field name - * @param $dir string If 'newer', sort in ascending order, otherwise + * @param string $field Field name + * @param string $dir If 'newer', sort in ascending order, otherwise * sort in descending order - * @param $start string Value to start the list at. If $dir == 'newer' + * @param string $start Value to start the list at. If $dir == 'newer' * this is the lower boundary, otherwise it's the upper boundary - * @param $end string Value to end the list at. If $dir == 'newer' this + * @param string $end Value to end the list at. If $dir == 'newer' this * is the upper boundary, otherwise it's the lower boundary - * @param $sort bool If false, don't add an ORDER BY clause + * @param bool $sort If false, don't add an ORDER BY clause */ protected function addWhereRange( $field, $dir, $start, $end, $sort = true ) { $isDirNewer = ( $dir === 'newer' ); @@ -240,8 +240,8 @@ abstract class ApiQueryBase extends ApiBase { /** * Add an option such as LIMIT or USE INDEX. If an option was set * before, the old value will be overwritten - * @param $name string Option name - * @param $value string Option value + * @param string $name Option name + * @param string $value Option value */ protected function addOption( $name, $value = null ) { if ( is_null( $value ) ) { @@ -253,9 +253,9 @@ abstract class ApiQueryBase extends ApiBase { /** * Execute a SELECT query based on the values in the internal arrays - * @param $method string Function the query should be attributed to. + * @param string $method Function the query should be attributed to. * You should usually use __METHOD__ here - * @param $extraQuery array Query data to add but not store in the object + * @param array $extraQuery Query data to add but not store in the object * Format is array( 'tables' => ..., 'fields' => ..., 'where' => ..., 'options' => ..., 'join_conds' => ... ) * @return ResultWrapper */ @@ -298,9 +298,9 @@ abstract class ApiQueryBase extends ApiBase { /** * Add information (title and namespace) about a Title object to a * result array - * @param $arr array Result array à la ApiResult + * @param array $arr Result array à la ApiResult * @param $title Title - * @param $prefix string Module prefix + * @param string $prefix Module prefix */ public static function addTitleInfo( &$arr, $title, $prefix = '' ) { $arr[$prefix . 'ns'] = intval( $title->getNamespace() ); @@ -325,8 +325,8 @@ abstract class ApiQueryBase extends ApiBase { /** * Add a sub-element under the page element with the given page ID - * @param $pageId int Page ID - * @param $data array Data array à la ApiResult + * @param int $pageId Page ID + * @param array $data Data array à la ApiResult * @return bool Whether the element fit in the result */ protected function addPageSubItems( $pageId, $data ) { @@ -339,9 +339,9 @@ abstract class ApiQueryBase extends ApiBase { /** * Same as addPageSubItems(), but one element of $data at a time - * @param $pageId int Page ID - * @param $item array Data array à la ApiResult - * @param $elemname string XML element name. If null, getModuleName() + * @param int $pageId Page ID + * @param array $item Data array à la ApiResult + * @param string $elemname XML element name. If null, getModuleName() * is used * @return bool Whether the element fit in the result */ @@ -351,7 +351,7 @@ abstract class ApiQueryBase extends ApiBase { } $result = $this->getResult(); $fit = $result->addValue( array( 'query', 'pages', $pageId, - $this->getModuleName() ), null, $item ); + $this->getModuleName() ), null, $item ); if ( !$fit ) { return false; } @@ -362,15 +362,15 @@ abstract class ApiQueryBase extends ApiBase { /** * Set a query-continue value - * @param $paramName string Parameter name - * @param $paramValue string Parameter value + * @param string $paramName Parameter name + * @param string $paramValue Parameter value */ protected function setContinueEnumParameter( $paramName, $paramValue ) { $paramName = $this->encodeParamName( $paramName ); $msg = array( $paramName => $paramValue ); $result = $this->getResult(); $result->disableSizeCheck(); - $result->addValue( 'query-continue', $this->getModuleName(), $msg ); + $result->addValue( 'query-continue', $this->getModuleName(), $msg, ApiResult::ADD_ON_TOP ); $result->enableSizeCheck(); } @@ -380,8 +380,7 @@ abstract class ApiQueryBase extends ApiBase { */ protected function getDB() { if ( is_null( $this->mDb ) ) { - $apiQuery = $this->getQuery(); - $this->mDb = $apiQuery->getDB(); + $this->mDb = $this->getQuery()->getDB(); } return $this->mDb; } @@ -389,9 +388,9 @@ abstract class ApiQueryBase extends ApiBase { /** * Selects the query database connection with the given name. * See ApiQuery::getNamedDB() for more information - * @param $name string Name to assign to the database connection - * @param $db int One of the DB_* constants - * @param $groups array Query groups + * @param string $name Name to assign to the database connection + * @param int $db One of the DB_* constants + * @param array $groups Query groups * @return DatabaseBase */ public function selectNamedDB( $name, $db, $groups ) { @@ -408,7 +407,7 @@ abstract class ApiQueryBase extends ApiBase { /** * Convert a title to a DB key - * @param $title string Page title with spaces + * @param string $title Page title with spaces * @return string Page title with underscores */ public function titleToKey( $title ) { @@ -420,12 +419,12 @@ abstract class ApiQueryBase extends ApiBase { if ( !$t ) { $this->dieUsageMsg( array( 'invalidtitle', $title ) ); } - return $t->getPrefixedDbKey(); + return $t->getPrefixedDBkey(); } /** * The inverse of titleToKey() - * @param $key string Page title with underscores + * @param string $key Page title with underscores * @return string Page title with spaces */ public function keyToTitle( $key ) { @@ -443,7 +442,7 @@ abstract class ApiQueryBase extends ApiBase { /** * An alternative to titleToKey() that doesn't trim trailing spaces - * @param $titlePart string Title part with spaces + * @param string $titlePart Title part with spaces * @return string Title part with underscores */ public function titlePartToKey( $titlePart ) { @@ -452,7 +451,7 @@ abstract class ApiQueryBase extends ApiBase { /** * An alternative to keyToTitle() that doesn't trim trailing spaces - * @param $keyPart string Key part with spaces + * @param string $keyPart Key part with spaces * @return string Key part with underscores */ public function keyPartToTitle( $keyPart ) { @@ -534,7 +533,7 @@ abstract class ApiQueryBase extends ApiBase { * @return bool */ public function validateSha1Hash( $hash ) { - return preg_match( '/[a-fA-F0-9]{40}/', $hash ); + return preg_match( '/^[a-f0-9]{40}$/', $hash ); } /** @@ -542,25 +541,19 @@ abstract class ApiQueryBase extends ApiBase { * @return bool */ public function validateSha1Base36Hash( $hash ) { - return preg_match( '/[a-zA-Z0-9]{31}/', $hash ); + return preg_match( '/^[a-z0-9]{31}$/', $hash ); } /** * @return array */ public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( + $errors = parent::getPossibleErrors(); + $errors = array_merge( $errors, array( array( 'invalidtitle', 'title' ), array( 'invalidtitle', 'key' ), ) ); - } - - /** - * Get version string for use in the API help output - * @return string - */ - public static function getBaseVersion() { - return __CLASS__ . ': $Id$'; + return $errors; } } @@ -569,33 +562,41 @@ abstract class ApiQueryBase extends ApiBase { */ abstract class ApiQueryGeneratorBase extends ApiQueryBase { - private $mIsGenerator; + private $mGeneratorPageSet = null; /** - * @param $query ApiBase - * @param $moduleName string - * @param $paramPrefix string + * Switch this module to generator mode. By default, generator mode is + * switched off and the module acts like a normal query module. + * @since 1.21 requires pageset parameter + * @param $generatorPageSet ApiPageSet object that the module will get + * by calling getPageSet() when in generator mode. */ - public function __construct( $query, $moduleName, $paramPrefix = '' ) { - parent::__construct( $query, $moduleName, $paramPrefix ); - $this->mIsGenerator = false; + public function setGeneratorMode( ApiPageSet $generatorPageSet ) { + if ( $generatorPageSet === null ) { + ApiBase::dieDebug( __METHOD__, 'Required parameter missing - $generatorPageSet' ); + } + $this->mGeneratorPageSet = $generatorPageSet; } /** - * Switch this module to generator mode. By default, generator mode is - * switched off and the module acts like a normal query module. + * Get the PageSet object to work on. + * If this module is generator, the pageSet object is different from other module's + * @return ApiPageSet */ - public function setGeneratorMode() { - $this->mIsGenerator = true; + protected function getPageSet() { + if ( $this->mGeneratorPageSet !== null ) { + return $this->mGeneratorPageSet; + } + return parent::getPageSet(); } /** * Overrides base class to prepend 'g' to every generator parameter - * @param $paramName string Parameter name + * @param string $paramName Parameter name * @return string Prefixed parameter name */ public function encodeParamName( $paramName ) { - if ( $this->mIsGenerator ) { + if ( $this->mGeneratorPageSet !== null ) { return 'g' . parent::encodeParamName( $paramName ); } else { return parent::encodeParamName( $paramName ); @@ -603,9 +604,24 @@ abstract class ApiQueryGeneratorBase extends ApiQueryBase { } /** + * Overrides base in case of generator & smart continue to + * notify ApiQueryMain instead of adding them to the result right away. + * @param string $paramName Parameter name + * @param string $paramValue Parameter value + */ + protected function setContinueEnumParameter( $paramName, $paramValue ) { + // If this is a generator and query->setGeneratorContinue() returns false, treat as before + if ( $this->mGeneratorPageSet === null + || !$this->getQuery()->setGeneratorContinue( $this, $paramName, $paramValue ) + ) { + parent::setContinueEnumParameter( $paramName, $paramValue ); + } + } + + /** * Execute this module as a generator * @param $resultPageSet ApiPageSet: All output should be appended to * this object */ - public abstract function executeGenerator( $resultPageSet ); + abstract public function executeGenerator( $resultPageSet ); } diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php index 96b86962..d9be9f28 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -132,10 +132,10 @@ class ApiQueryBlocks extends ApiQueryBase { $this->addWhereIf( 'ipb_user != 0', isset( $show['account'] ) ); $this->addWhereIf( 'ipb_user != 0 OR ipb_range_end > ipb_range_start', isset( $show['!ip'] ) ); $this->addWhereIf( 'ipb_user = 0 AND ipb_range_end = ipb_range_start', isset( $show['ip'] ) ); - $this->addWhereIf( 'ipb_expiry = '.$db->addQuotes($db->getInfinity()), isset( $show['!temp'] ) ); - $this->addWhereIf( 'ipb_expiry != '.$db->addQuotes($db->getInfinity()), isset( $show['temp'] ) ); - $this->addWhereIf( "ipb_range_end = ipb_range_start", isset( $show['!range'] ) ); - $this->addWhereIf( "ipb_range_end > ipb_range_start", isset( $show['range'] ) ); + $this->addWhereIf( 'ipb_expiry = ' . $db->addQuotes( $db->getInfinity() ), isset( $show['!temp'] ) ); + $this->addWhereIf( 'ipb_expiry != ' . $db->addQuotes( $db->getInfinity() ), isset( $show['temp'] ) ); + $this->addWhereIf( 'ipb_range_end = ipb_range_start', isset( $show['!range'] ) ); + $this->addWhereIf( 'ipb_range_end > ipb_range_start', isset( $show['range'] ) ); } if ( !$this->getUser()->isAllowed( 'hideuser' ) ) { @@ -182,8 +182,8 @@ class ApiQueryBlocks extends ApiQueryBase { $block['reason'] = $row->ipb_reason; } if ( $fld_range && !$row->ipb_auto ) { - $block['rangestart'] = IP::hexToQuad( $row->ipb_range_start ); - $block['rangeend'] = IP::hexToQuad( $row->ipb_range_end ); + $block['rangestart'] = IP::formatHex( $row->ipb_range_start ); + $block['rangeend'] = IP::formatHex( $row->ipb_range_end ); } if ( $fld_flags ) { // For clarity, these flags use the same names as their action=block counterparts @@ -301,7 +301,7 @@ class ApiQueryBlocks extends ApiQueryBase { 'dir' => $this->getDirectionDescription( $p ), '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.', + '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', 'prop' => array( @@ -404,8 +404,4 @@ class ApiQueryBlocks extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Blocks'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php index 309c2ce9..69a64415 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -53,7 +53,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { */ private function run( $resultPageSet = null ) { if ( $this->getPageSet()->getGoodTitleCount() == 0 ) { - return; // nothing to do + return; // nothing to do } $params = $this->extractRequestParams(); @@ -85,10 +85,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { if ( !is_null( $params['continue'] ) ) { $cont = explode( '|', $params['continue'] ); - if ( count( $cont ) != 2 ) { - $this->dieUsage( "Invalid continue param. You should pass the " . - "original value returned by the previous query", "_badcontinue" ); - } + $this->dieContinueUsageIf( count( $cont ) != 2 ); $op = $params['dir'] == 'descending' ? '<' : '>'; $clfrom = intval( $cont[0] ); $clto = $this->getDB()->addQuotes( $cont[1] ); @@ -276,8 +273,4 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Properties#categories_.2F_cl'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryCategoryInfo.php b/includes/api/ApiQueryCategoryInfo.php index 31517fab..a889272e 100644 --- a/includes/api/ApiQueryCategoryInfo.php +++ b/includes/api/ApiQueryCategoryInfo.php @@ -48,6 +48,7 @@ class ApiQueryCategoryInfo extends ApiQueryBase { $this->getPageSet()->getMissingTitles(); $cattitles = array(); foreach ( $categories as $c ) { + /** @var $t Title */ $t = $titles[$c]; $cattitles[$c] = $t->getDBkey(); } @@ -146,8 +147,4 @@ class ApiQueryCategoryInfo extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Properties#categoryinfo_.2F_ci'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php index 55ce0234..9dbd8593 100644 --- a/includes/api/ApiQueryCategoryMembers.php +++ b/includes/api/ApiQueryCategoryMembers.php @@ -78,7 +78,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $this->addFieldsIf( 'cl_timestamp', $fld_timestamp || $params['sort'] == 'timestamp' ); - $this->addTables( array( 'page', 'categorylinks' ) ); // must be in this order for 'USE INDEX' + $this->addTables( array( 'page', 'categorylinks' ) ); // must be in this order for 'USE INDEX' $this->addWhereFld( 'cl_to', $categoryTitle->getDBkey() ); $queryTypes = $params['type']; @@ -106,11 +106,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { } else { if ( $params['continue'] ) { $cont = explode( '|', $params['continue'], 3 ); - if ( count( $cont ) != 3 ) { - $this->dieUsage( 'Invalid continue param. You should pass the original value returned '. - 'by the previous query', '_badcontinue' - ); - } + $this->dieContinueUsageIf( count( $cont ) != 3 ); // Remove the types to skip from $queryTypes $contTypeIndex = array_search( $cont[0], $queryTypes ); @@ -118,7 +114,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { // Add a WHERE clause for sortkey and from // pack( "H*", $foo ) is used to convert hex back to binary - $escSortkey = $this->getDB()->addQuotes( pack( "H*", $cont[1] ) ); + $escSortkey = $this->getDB()->addQuotes( pack( 'H*', $cont[1] ) ); $from = intval( $cont[2] ); $op = $dir == 'newer' ? '>' : '<'; // $contWhere is used further down @@ -247,7 +243,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { if ( is_null( $resultPageSet ) ) { $result->setIndexedTagName_internal( - array( 'query', $this->getModuleName() ), 'cm' ); + array( 'query', $this->getModuleName() ), 'cm' ); } } @@ -403,7 +399,6 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { $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' ), ) ); } @@ -418,8 +413,4 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Categorymembers'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php index e69ccbd6..31ca1ef5 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -74,15 +74,15 @@ class ApiQueryDeletedrevs extends ApiQueryBase { if ( $mode == 'revs' || $mode == 'user' ) { // Ignore namespace and unique due to inability to know whether they were purposely set - foreach( array( 'from', 'to', 'prefix', /*'namespace',*/ 'continue', /*'unique'*/ ) as $p ) { + foreach( array( 'from', 'to', 'prefix', /*'namespace', 'unique'*/ ) as $p ) { if ( !is_null( $params[$p] ) ) { - $this->dieUsage( "The '{$p}' parameter cannot be used in modes 1 or 2", 'badparams'); + $this->dieUsage( "The '{$p}' parameter cannot be used in modes 1 or 2", 'badparams' ); } } } else { foreach( array( 'start', 'end' ) as $p ) { if ( !is_null( $params[$p] ) ) { - $this->dieUsage( "The {$p} parameter cannot be used in mode 3", 'badparams'); + $this->dieUsage( "The {$p} parameter cannot be used in mode 3", 'badparams' ); } } } @@ -116,7 +116,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { } // Check limits $userMax = $fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1; - $botMax = $fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2; + $botMax = $fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2; $limit = $params['limit']; @@ -160,10 +160,9 @@ class ApiQueryDeletedrevs extends ApiQueryBase { if ( !is_null( $params['continue'] ) && ( $mode == 'all' || $mode == 'revs' ) ) { $cont = explode( '|', $params['continue'] ); - if ( count( $cont ) != 3 ) { - $this->dieUsage( 'Invalid continue param. You should pass the original value returned by the previous query', 'badcontinue' ); - } + $this->dieContinueUsageIf( count( $cont ) != 3 ); $ns = intval( $cont[0] ); + $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] ); $title = $db->addQuotes( $cont[1] ); $ts = $db->addQuotes( $db->timestamp( $cont[2] ) ); $op = ( $dir == 'newer' ? '>' : '<' ); @@ -307,7 +306,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { ), 'namespace' => array( ApiBase::PARAM_TYPE => 'namespace', - ApiBase::PARAM_DFLT => 0, + ApiBase::PARAM_DFLT => NS_MAIN, ), 'limit' => array( ApiBase::PARAM_DFLT => 10, @@ -362,7 +361,7 @@ class ApiQueryDeletedrevs extends ApiQueryBase { 'namespace' => 'Only list pages in this namespace (3)', 'user' => 'Only list revisions by this user', 'excludeuser' => 'Don\'t list revisions by this user', - 'continue' => 'When more results are available, use this to continue (3)', + 'continue' => 'When more results are available, use this to continue (1, 3)', 'unique' => 'List only one revision for each page (3)', ); } @@ -397,11 +396,9 @@ class ApiQueryDeletedrevs extends ApiQueryBase { array( 'code' => 'permissiondenied', 'info' => 'You don\'t have permission to view deleted revision information' ), array( 'code' => 'badparams', 'info' => 'user and excludeuser cannot be used together' ), array( 'code' => 'permissiondenied', 'info' => 'You don\'t have permission to view deleted revision content' ), - array( 'code' => 'badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), array( 'code' => 'badparams', 'info' => "The 'from' parameter cannot be used in modes 1 or 2" ), array( 'code' => 'badparams', 'info' => "The 'to' parameter cannot be used in modes 1 or 2" ), array( 'code' => 'badparams', 'info' => "The 'prefix' parameter cannot be used in modes 1 or 2" ), - array( 'code' => 'badparams', 'info' => "The 'continue' parameter cannot be used in modes 1 or 2" ), array( 'code' => 'badparams', 'info' => "The 'start' parameter cannot be used in mode 3" ), array( 'code' => 'badparams', 'info' => "The 'end' parameter cannot be used in mode 3" ), ) ); @@ -423,8 +420,4 @@ class ApiQueryDeletedrevs extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Deletedrevs'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryDisabled.php b/includes/api/ApiQueryDisabled.php index 6715969a..cf0d841e 100644 --- a/includes/api/ApiQueryDisabled.php +++ b/includes/api/ApiQueryDisabled.php @@ -36,10 +36,6 @@ */ class ApiQueryDisabled extends ApiQueryBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { $this->setWarning( "The \"{$this->getModuleName()}\" module has been disabled." ); } @@ -61,8 +57,4 @@ class ApiQueryDisabled extends ApiQueryBase { public function getExamples() { return array(); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php index 8f0fd3be..18dcba85 100644 --- a/includes/api/ApiQueryDuplicateFiles.php +++ b/includes/api/ApiQueryDuplicateFiles.php @@ -66,10 +66,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { $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' ); - } + $this->dieContinueUsageIf( count( $cont ) != 2 ); $fromImage = $cont[0]; $skipUntilThisDup = $cont[1]; // Filter out any images before $fromImage @@ -95,6 +92,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { $sha1s = array(); foreach ( $files as $file ) { + /** @var $file File */ $sha1s[$file->getName()] = $file->getSha1(); } @@ -116,6 +114,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { if( $params['dir'] == 'descending' ) { $dupFiles = array_reverse( $dupFiles ); } + /** @var $dupFile File */ foreach ( $dupFiles as $dupFile ) { $dupName = $dupFile->getName(); if( $image == $dupName && $dupFile->isLocal() ) { @@ -133,7 +132,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { break; } if ( !is_null( $resultPageSet ) ) { - $titles[] = $file->getTitle(); + $titles[] = $dupFile->getTitle(); } else { $r = array( 'name' => $dupName, @@ -204,12 +203,6 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { return 'List all files that are duplicates of the given file(s) based on hash values'; } - 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&titles=File:Albert_Einstein_Head.jpg&prop=duplicatefiles', @@ -220,8 +213,4 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Properties#duplicatefiles_.2F_df'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php index 42b398ba..eb9cdf9e 100644 --- a/includes/api/ApiQueryExtLinksUsage.php +++ b/includes/api/ApiQueryExtLinksUsage.php @@ -55,7 +55,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { $query = $params['query']; $protocol = self::getProtocolPrefix( $params['protocol'] ); - $this->addTables( array( 'page', 'externallinks' ) ); // must be in this order for 'USE INDEX' + $this->addTables( array( 'page', 'externallinks' ) ); // must be in this order for 'USE INDEX' $this->addOption( 'USE INDEX', 'el_index' ); $this->addWhere( 'page_id=el_from' ); @@ -121,8 +121,12 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { ApiQueryBase::addTitleInfo( $vals, $title ); } if ( $fld_url ) { - // We *could* run this through wfExpandUrl() but I think it's better to output the link verbatim, even if it's protocol-relative --Roan - $vals['url'] = $row->el_to; + $to = $row->el_to; + // expand protocol-relative urls + if( $params['expandurl'] ) { + $to = wfExpandUrl( $to, PROTO_CANONICAL ); + } + $vals['url'] = $to; } $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { @@ -169,7 +173,8 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { ApiBase::PARAM_MIN => 1, ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 - ) + ), + 'expandurl' => false, ); } @@ -218,7 +223,8 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { ), 'query' => 'Search string without protocol. See [[Special:LinkSearch]]. Leave empty to list all external links', 'namespace' => 'The page namespace(s) to enumerate.', - 'limit' => 'How many pages to return.' + 'limit' => 'How many pages to return.', + 'expandurl' => 'Expand protocol-relative urls with the canonical protocol', ); if ( $wgMiserMode ) { @@ -266,8 +272,4 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Exturlusage'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php index 9365a9b8..761b49ea 100644 --- a/includes/api/ApiQueryExternalLinks.php +++ b/includes/api/ApiQueryExternalLinks.php @@ -86,8 +86,12 @@ class ApiQueryExternalLinks extends ApiQueryBase { break; } $entry = array(); - // We *could* run this through wfExpandUrl() but I think it's better to output the link verbatim, even if it's protocol-relative --Roan - ApiResult::setContent( $entry, $row->el_to ); + $to = $row->el_to; + // expand protocol-relative urls + if( $params['expandurl'] ) { + $to = wfExpandUrl( $to, PROTO_CANONICAL ); + } + ApiResult::setContent( $entry, $to ); $fit = $this->addPageSubItem( $row->el_from, $entry ); if ( !$fit ) { $this->setContinueEnumParameter( 'offset', $offset + $count - 1 ); @@ -117,6 +121,7 @@ class ApiQueryExternalLinks extends ApiQueryBase { ApiBase::PARAM_DFLT => '', ), 'query' => null, + 'expandurl' => false, ); } @@ -130,6 +135,7 @@ class ApiQueryExternalLinks extends ApiQueryBase { "Leave both this and {$p}query empty to list all external links" ), 'query' => 'Search string without protocol. Useful for checking whether a certain page contains a certain external url', + 'expandurl' => 'Expand protocol-relative urls with the canonical protocol', ); } @@ -142,7 +148,7 @@ class ApiQueryExternalLinks extends ApiQueryBase { } public function getDescription() { - return 'Returns all external urls (not interwikies) from the given page(s)'; + return 'Returns all external urls (not interwikis) from the given page(s)'; } public function getPossibleErrors() { @@ -160,8 +166,4 @@ class ApiQueryExternalLinks extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Properties#extlinks_.2F_el'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php index a5486ef4..021074a9 100644 --- a/includes/api/ApiQueryFilearchive.php +++ b/includes/api/ApiQueryFilearchive.php @@ -64,7 +64,7 @@ class ApiQueryFilearchive extends ApiQueryBase { $this->addTables( 'filearchive' ); $this->addFields( array( 'fa_name', 'fa_deleted' ) ); - $this->addFieldsIf( 'fa_storage_key', $fld_sha1 ); + $this->addFieldsIf( 'fa_sha1', $fld_sha1 ); $this->addFieldsIf( 'fa_timestamp', $fld_timestamp ); $this->addFieldsIf( array( 'fa_user', 'fa_user_text' ), $fld_user ); $this->addFieldsIf( array( 'fa_height', 'fa_width', 'fa_size' ), $fld_dimensions || $fld_size ); @@ -77,10 +77,7 @@ class ApiQueryFilearchive extends ApiQueryBase { 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" ); - } + $this->dieContinueUsageIf( count( $cont ) != 1 ); $op = $params['dir'] == 'descending' ? '<' : '>'; $cont_from = $db->addQuotes( $cont[0] ); $this->addWhere( "fa_name $op= $cont_from" ); @@ -101,25 +98,21 @@ class ApiQueryFilearchive extends ApiQueryBase { $sha1Set = isset( $params['sha1'] ); $sha1base36Set = isset( $params['sha1base36'] ); if ( $sha1Set || $sha1base36Set ) { - global $wgMiserMode; - if ( $wgMiserMode ) { - $this->dieUsage( 'Search by hash disabled in Miser Mode', 'hashsearchdisabled' ); - } - $sha1 = false; if ( $sha1Set ) { - if ( !$this->validateSha1Hash( $params['sha1'] ) ) { + $sha1 = strtolower( $params['sha1'] ); + if ( !$this->validateSha1Hash( $sha1 ) ) { $this->dieUsage( 'The SHA1 hash provided is not valid', 'invalidsha1hash' ); } - $sha1 = wfBaseConvert( $params['sha1'], 16, 36, 31 ); + $sha1 = wfBaseConvert( $sha1, 16, 36, 31 ); } elseif ( $sha1base36Set ) { - if ( !$this->validateSha1Base36Hash( $params['sha1base36'] ) ) { + $sha1 = strtolower( $params['sha1base36'] ); + if ( !$this->validateSha1Base36Hash( $sha1 ) ) { $this->dieUsage( 'The SHA1Base36 hash provided is not valid', 'invalidsha1base36hash' ); } - $sha1 = $params['sha1base36']; } if ( $sha1 ) { - $this->addWhere( 'fa_storage_key ' . $db->buildLike( "{$sha1}.", $db->anyString() ) ); + $this->addWhereFld( 'fa_sha1', $sha1 ); } } @@ -155,7 +148,7 @@ class ApiQueryFilearchive extends ApiQueryBase { self::addTitleInfo( $file, $title ); if ( $fld_sha1 ) { - $file['sha1'] = wfBaseConvert( LocalRepo::getHashFromKey( $row->fa_storage_key ), 36, 16, 40 ); + $file['sha1'] = wfBaseConvert( $row->fa_sha1, 36, 16, 40 ); } if ( $fld_timestamp ) { $file['timestamp'] = wfTimestamp( TS_ISO_8601, $row->fa_timestamp ); @@ -214,7 +207,6 @@ class ApiQueryFilearchive extends ApiQueryBase { $file['suppressed'] = ''; } - $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $file ); if ( !$fit ) { $this->setContinueEnumParameter( 'continue', $row->fa_name ); @@ -276,8 +268,8 @@ class ApiQueryFilearchive extends ApiQueryBase { 'prefix' => 'Search for all image titles that begin with this value', 'dir' => 'The direction in which to list', 'limit' => 'How many images to return in total', - 'sha1' => "SHA1 hash of image. Overrides {$this->getModulePrefix()}sha1base36. Disabled in Miser Mode", - 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki). Disabled in Miser Mode', + 'sha1' => "SHA1 hash of image. Overrides {$this->getModulePrefix()}sha1base36", + 'sha1base36' => 'SHA1 hash of image in base 36 (used in MediaWiki)', 'prop' => array( 'What image information to get:', ' sha1 - Adds SHA-1 hash for the image', @@ -370,7 +362,6 @@ 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' ), ) ); } @@ -382,8 +373,4 @@ class ApiQueryFilearchive extends ApiQueryBase { ), ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryIWBacklinks.php b/includes/api/ApiQueryIWBacklinks.php index c5012f08..b47d31f2 100644 --- a/includes/api/ApiQueryIWBacklinks.php +++ b/includes/api/ApiQueryIWBacklinks.php @@ -56,10 +56,7 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { if ( !is_null( $params['continue'] ) ) { $cont = explode( '|', $params['continue'] ); - if ( count( $cont ) != 3 ) { - $this->dieUsage( 'Invalid continue param. You should pass the ' . - 'original value returned by the previous query', '_badcontinue' ); - } + $this->dieContinueUsageIf( count( $cont ) != 3 ); $db = $this->getDB(); $op = $params['dir'] == 'descending' ? '<' : '>'; @@ -233,7 +230,6 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { public function getPossibleErrors() { return array_merge( parent::getPossibleErrors(), array( array( 'missingparam', 'prefix' ), - array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), ) ); } @@ -243,8 +239,4 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { 'api.php?action=query&generator=iwbacklinks&giwbltitle=Test&giwblprefix=wikibooks&prop=info' ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryIWLinks.php b/includes/api/ApiQueryIWLinks.php index 30c7f5a8..fc77b4e6 100644 --- a/includes/api/ApiQueryIWLinks.php +++ b/includes/api/ApiQueryIWLinks.php @@ -58,10 +58,7 @@ class ApiQueryIWLinks extends ApiQueryBase { if ( !is_null( $params['continue'] ) ) { $cont = explode( '|', $params['continue'] ); - if ( count( $cont ) != 3 ) { - $this->dieUsage( 'Invalid continue param. You should pass the ' . - 'original value returned by the previous query', '_badcontinue' ); - } + $this->dieContinueUsageIf( count( $cont ) != 3 ); $op = $params['dir'] == 'descending' ? '<' : '>'; $db = $this->getDB(); $iwlfrom = intval( $cont[0] ); @@ -187,7 +184,6 @@ class ApiQueryIWLinks extends ApiQueryBase { public function getPossibleErrors() { return array_merge( parent::getPossibleErrors(), array( array( 'missingparam', 'prefix' ), - array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), ) ); } @@ -196,8 +192,4 @@ class ApiQueryIWLinks extends ApiQueryBase { 'api.php?action=query&prop=iwlinks&titles=Main%20Page' => 'Get interwiki links from the [[Main Page]]', ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index d822eed5..95c2745a 100644 --- a/includes/api/ApiQueryImageInfo.php +++ b/includes/api/ApiQueryImageInfo.php @@ -30,6 +30,8 @@ * @ingroup API */ class ApiQueryImageInfo extends ApiQueryBase { + const TRANSFORM_LIMIT = 50; + private static $transformCount = 0; public function __construct( $query, $moduleName, $prefix = 'ii' ) { // We allow a subclass to override the prefix, to create a related API module. @@ -52,14 +54,10 @@ class ApiQueryImageInfo extends ApiQueryBase { $titles = array_keys( $pageIds[NS_FILE] ); asort( $titles ); // Ensure the order is always the same - $skip = false; + $fromTitle = null; if ( !is_null( $params['continue'] ) ) { - $skip = true; $cont = explode( '|', $params['continue'] ); - if ( count( $cont ) != 2 ) { - $this->dieUsage( 'Invalid continue param. You should pass the original ' . - 'value returned by the previous query', '_badcontinue' ); - } + $this->dieContinueUsageIf( count( $cont ) != 2 ); $fromTitle = strval( $cont[0] ); $fromTimestamp = $cont[1]; // Filter out any titles before $fromTitle @@ -79,14 +77,34 @@ class ApiQueryImageInfo extends ApiQueryBase { } else { $images = RepoGroup::singleton()->findFiles( $titles ); } - foreach ( $images as $img ) { - // Skip redirects - if ( $img->getOriginalTitle()->isRedirect() ) { + foreach ( $titles as $title ) { + $pageId = $pageIds[NS_FILE][$title]; + $start = $title === $fromTitle ? $fromTimestamp : $params['start']; + + if ( !isset( $images[$title] ) ) { + $result->addValue( + array( 'query', 'pages', intval( $pageId ) ), + 'imagerepository', '' + ); + // The above can't fail because it doesn't increase the result size continue; } - $start = $skip ? $fromTimestamp : $params['start']; - $pageId = $pageIds[NS_FILE][ $img->getOriginalTitle()->getDBkey() ]; + /** @var $img File */ + $img = $images[$title]; + + if ( self::getTransformCount() >= self::TRANSFORM_LIMIT ) { + if ( count( $pageIds[NS_FILE] ) == 1 ) { + // See the 'the user is screwed' comment below + $this->setContinueEnumParameter( 'start', + $start !== null ? $start : wfTimestamp( TS_ISO_8601, $img->getTimestamp() ) + ); + } else { + $this->setContinueEnumParameter( 'continue', + $this->getContinueStr( $img, $start ) ); + } + break; + } $fit = $result->addValue( array( 'query', 'pages', intval( $pageId ) ), @@ -100,10 +118,11 @@ class ApiQueryImageInfo extends ApiQueryBase { // thing again. When the violating queries have been // out-continued, the result will get through $this->setContinueEnumParameter( 'start', - wfTimestamp( TS_ISO_8601, $img->getTimestamp() ) ); + $start !== null ? $start : wfTimestamp( TS_ISO_8601, $img->getTimestamp() ) + ); } else { $this->setContinueEnumParameter( 'continue', - $this->getContinueStr( $img ) ); + $this->getContinueStr( $img, $start ) ); } break; } @@ -140,6 +159,7 @@ class ApiQueryImageInfo extends ApiQueryBase { // Get one more to facilitate query-continue functionality $count = ( $gotOne ? 1 : 0 ); $oldies = $img->getHistory( $params['limit'] - $count + 1, $start, $params['end'] ); + /** @var $oldie File */ foreach ( $oldies as $oldie ) { if ( ++$count > $params['limit'] ) { // We've reached the extra one which shows that there are additional pages to be had. Stop here... @@ -167,25 +187,13 @@ class ApiQueryImageInfo extends ApiQueryBase { if ( !$fit ) { break; } - $skip = false; - } - - $data = $this->getResultData(); - foreach ( $data['query']['pages'] as $pageid => $arr ) { - if ( !isset( $arr['imagerepository'] ) ) { - $result->addValue( - array( 'query', 'pages', $pageid ), - 'imagerepository', '' - ); - } - // The above can't fail because it doesn't increase the result size } } } /** * From parameters, construct a 'scale' array - * @param $params Array: Parameters passed to api. + * @param array $params Parameters passed to api. * @return Array or Null: key-val array of 'width' and 'height', or null */ public function getScale( $params ) { @@ -216,8 +224,8 @@ class ApiQueryImageInfo extends ApiQueryBase { * We do this later than getScale, since we need the image * to know which handler, since handlers can make their own parameters. * @param File $image Image that params are for. - * @param Array $thumbParams thumbnail parameters from getScale - * @param String $otherParams of otherParams (iiurlparam). + * @param array $thumbParams thumbnail parameters from getScale + * @param string $otherParams of otherParams (iiurlparam). * @return Array of parameters for transform. */ protected function mergeThumbParams ( $image, $thumbParams, $otherParams ) { @@ -264,10 +272,10 @@ class ApiQueryImageInfo extends ApiQueryBase { * Get result information for an image revision * * @param $file File object - * @param $prop Array of properties to get (in the keys) + * @param array $prop of properties to get (in the keys) * @param $result ApiResult object - * @param $thumbParams Array containing 'width' and 'height' items, or null - * @param $version string Version of image metadata (for things like jpeg which have different versions). + * @param array $thumbParams containing 'width' and 'height' items, or null + * @param string $version Version of image metadata (for things like jpeg which have different versions). * @return Array: result array */ static function getInfo( $file, $prop, $result, $thumbParams = null, $version = 'latest' ) { @@ -346,6 +354,7 @@ class ApiQueryImageInfo extends ApiQueryBase { if ( $url ) { if ( !is_null( $thumbParams ) ) { $mto = $file->transform( $thumbParams ); + self::$transformCount++; if ( $mto && !$mto->isError() ) { $vals['thumburl'] = wfExpandUrl( $mto->getUrl(), PROTO_CURRENT ); @@ -360,7 +369,7 @@ class ApiQueryImageInfo extends ApiQueryBase { } if ( isset( $prop['thumbmime'] ) && $file->getHandler() ) { - list( $ext, $mime ) = $file->getHandler()->getThumbType( + list( , $mime ) = $file->getHandler()->getThumbType( $mto->getExtension(), $file->getMimeType(), $thumbParams ); $vals['thumbmime'] = $mime; } @@ -377,8 +386,10 @@ class ApiQueryImageInfo extends ApiQueryBase { } if ( $meta ) { + wfSuppressWarnings(); $metadata = unserialize( $file->getMetadata() ); - if ( $version !== 'latest' ) { + wfRestoreWarnings(); + if ( $metadata && $version !== 'latest' ) { $metadata = $file->convertMetadataVersion( $metadata, $version ); } $vals['metadata'] = $metadata ? self::processMetaData( $metadata, $result ) : null; @@ -404,6 +415,17 @@ class ApiQueryImageInfo extends ApiQueryBase { } /** + * Get the count of image transformations performed + * + * If this is >= TRANSFORM_LIMIT, you should probably stop processing images. + * + * @return integer count + */ + static function getTransformCount() { + return self::$transformCount; + } + + /** * * @param $metadata Array * @param $result ApiResult @@ -432,11 +454,14 @@ class ApiQueryImageInfo extends ApiQueryBase { /** * @param $img File + * @param null|string $start * @return string */ - protected function getContinueStr( $img ) { - return $img->getOriginalTitle()->getText() . - '|' . $img->getTimestamp(); + protected function getContinueStr( $img, $start = null ) { + if ( $start === null ) { + $start = $img->getTimestamp(); + } + return $img->getOriginalTitle()->getText() . '|' . $start; } public function getAllowedParams() { @@ -494,6 +519,7 @@ class ApiQueryImageInfo extends ApiQueryBase { /** * Returns array key value pairs of properties and their descriptions * + * @param string $modulePrefix * @return array */ private static function getProperties( $modulePrefix = '' ) { @@ -540,7 +566,7 @@ class ApiQueryImageInfo extends ApiQueryBase { return array( 'prop' => self::getPropertyDescriptions( array(), $p ), 'urlwidth' => array( "If {$p}prop=url is set, a URL to an image scaled to this width will be returned.", - 'Only the current version of the image can be scaled' ), + 'Only the current version of the image can be scaled' ), 'urlheight' => "Similar to {$p}urlwidth. Cannot be used without {$p}urlwidth", 'urlparam' => array( "A handler specific parameter string. For example, pdf's ", "might use 'page15-100px'. {$p}urlwidth must be used and be consistent with {$p}urlparam" ), @@ -578,6 +604,15 @@ class ApiQueryImageInfo extends ApiQueryBase { ApiBase::PROP_NULLABLE => true ) ), + 'dimensions' => array( + 'size' => 'integer', + 'width' => 'integer', + 'height' => 'integer', + 'pagecount' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ), 'comment' => array( 'commenthidden' => 'boolean', 'comment' => array( @@ -633,6 +668,13 @@ class ApiQueryImageInfo extends ApiQueryBase { ApiBase::PROP_NULLABLE => true ) ), + 'thumbmime' => array( + 'filehidden' => 'boolean', + 'thumbmime' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ) + ), 'mediatype' => array( 'filehidden' => 'boolean', 'mediatype' => array( @@ -672,7 +714,7 @@ class ApiQueryImageInfo extends ApiQueryBase { array( 'code' => "{$p}urlwidth", 'info' => "{$p}urlheight cannot be used without {$p}urlwidth" ), array( 'code' => 'urlparam', 'info' => "Invalid value for {$p}urlparam" ), array( 'code' => 'urlparam_no_width', 'info' => "{$p}urlparam requires {$p}urlwidth" ), - array( 'code' => 'urlparam_urlwidth_mismatch', 'info' => "The width set in {$p}urlparm doesnt't " . + array( 'code' => 'urlparam_urlwidth_mismatch', 'info' => "The width set in {$p}urlparm doesn't " . "match the one in {$p}urlwidth" ), ) ); } @@ -687,8 +729,4 @@ class ApiQueryImageInfo extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Properties#imageinfo_.2F_ii'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryImages.php b/includes/api/ApiQueryImages.php index 6052a75f..f2bf0a7b 100644 --- a/includes/api/ApiQueryImages.php +++ b/includes/api/ApiQueryImages.php @@ -49,7 +49,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { */ private function run( $resultPageSet = null ) { if ( $this->getPageSet()->getGoodTitleCount() == 0 ) { - return; // nothing to do + return; // nothing to do } $params = $this->extractRequestParams(); @@ -62,10 +62,7 @@ class ApiQueryImages extends ApiQueryGeneratorBase { $this->addWhereFld( 'il_from', array_keys( $this->getPageSet()->getGoodTitles() ) ); if ( !is_null( $params['continue'] ) ) { $cont = explode( '|', $params['continue'] ); - if ( count( $cont ) != 2 ) { - $this->dieUsage( 'Invalid continue param. You should pass the ' . - 'original value returned by the previous query', '_badcontinue' ); - } + $this->dieContinueUsageIf( count( $cont ) != 2 ); $op = $params['dir'] == 'descending' ? '<' : '>'; $ilfrom = intval( $cont[0] ); $ilto = $this->getDB()->addQuotes( $cont[1] ); @@ -185,12 +182,6 @@ class ApiQueryImages extends ApiQueryGeneratorBase { return 'Returns all images contained on the given page(s)'; } - 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&prop=images&titles=Main%20Page' => 'Get a list of images used in the [[Main Page]]', @@ -201,8 +192,4 @@ class ApiQueryImages extends ApiQueryGeneratorBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Properties#images_.2F_im'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php index 5d4f0346..37cd9159 100644 --- a/includes/api/ApiQueryInfo.php +++ b/includes/api/ApiQueryInfo.php @@ -33,7 +33,8 @@ class ApiQueryInfo extends ApiQueryBase { private $fld_protection = false, $fld_talkid = false, $fld_subjectid = false, $fld_url = false, - $fld_readable = false, $fld_watched = false, $fld_notificationtimestamp = false, + $fld_readable = false, $fld_watched = false, $fld_watchers = false, + $fld_notificationtimestamp = false, $fld_preload = false, $fld_displaytitle = false; private $params, $titles, $missing, $everything, $pageCounter; @@ -41,7 +42,8 @@ class ApiQueryInfo extends ApiQueryBase { private $pageRestrictions, $pageIsRedir, $pageIsNew, $pageTouched, $pageLatest, $pageLength; - private $protections, $watched, $notificationtimestamps, $talkids, $subjectids, $displaytitles; + private $protections, $watched, $watchers, $notificationtimestamps, $talkids, $subjectids, $displaytitles; + private $showZeroWatchers = false; private $tokenFunctions; @@ -96,7 +98,7 @@ class ApiQueryInfo extends ApiQueryBase { 'unblock' => array( 'ApiQueryInfo', 'getUnblockToken' ), 'email' => array( 'ApiQueryInfo', 'getEmailToken' ), 'import' => array( 'ApiQueryInfo', 'getImportToken' ), - 'watch' => array( 'ApiQueryInfo', 'getWatchToken'), + 'watch' => array( 'ApiQueryInfo', 'getWatchToken' ), ); wfRunHooks( 'APIQueryInfoTokens', array( &$this->tokenFunctions ) ); return $this->tokenFunctions; @@ -118,11 +120,11 @@ class ApiQueryInfo extends ApiQueryBase { } // The token is always the same, let's exploit that - if ( !isset( ApiQueryInfo::$cachedTokens[ 'edit' ] ) ) { - ApiQueryInfo::$cachedTokens[ 'edit' ] = $wgUser->getEditToken(); + if ( !isset( ApiQueryInfo::$cachedTokens['edit'] ) ) { + ApiQueryInfo::$cachedTokens['edit'] = $wgUser->getEditToken(); } - return ApiQueryInfo::$cachedTokens[ 'edit' ]; + return ApiQueryInfo::$cachedTokens['edit']; } public static function getDeleteToken( $pageid, $title ) { @@ -132,11 +134,11 @@ class ApiQueryInfo extends ApiQueryBase { } // The token is always the same, let's exploit that - if ( !isset( ApiQueryInfo::$cachedTokens[ 'delete' ] ) ) { - ApiQueryInfo::$cachedTokens[ 'delete' ] = $wgUser->getEditToken(); + if ( !isset( ApiQueryInfo::$cachedTokens['delete'] ) ) { + ApiQueryInfo::$cachedTokens['delete'] = $wgUser->getEditToken(); } - return ApiQueryInfo::$cachedTokens[ 'delete' ]; + return ApiQueryInfo::$cachedTokens['delete']; } public static function getProtectToken( $pageid, $title ) { @@ -146,11 +148,11 @@ class ApiQueryInfo extends ApiQueryBase { } // The token is always the same, let's exploit that - if ( !isset( ApiQueryInfo::$cachedTokens[ 'protect' ] ) ) { - ApiQueryInfo::$cachedTokens[ 'protect' ] = $wgUser->getEditToken(); + if ( !isset( ApiQueryInfo::$cachedTokens['protect'] ) ) { + ApiQueryInfo::$cachedTokens['protect'] = $wgUser->getEditToken(); } - return ApiQueryInfo::$cachedTokens[ 'protect' ]; + return ApiQueryInfo::$cachedTokens['protect']; } public static function getMoveToken( $pageid, $title ) { @@ -160,11 +162,11 @@ class ApiQueryInfo extends ApiQueryBase { } // The token is always the same, let's exploit that - if ( !isset( ApiQueryInfo::$cachedTokens[ 'move' ] ) ) { - ApiQueryInfo::$cachedTokens[ 'move' ] = $wgUser->getEditToken(); + if ( !isset( ApiQueryInfo::$cachedTokens['move'] ) ) { + ApiQueryInfo::$cachedTokens['move'] = $wgUser->getEditToken(); } - return ApiQueryInfo::$cachedTokens[ 'move' ]; + return ApiQueryInfo::$cachedTokens['move']; } public static function getBlockToken( $pageid, $title ) { @@ -174,11 +176,11 @@ class ApiQueryInfo extends ApiQueryBase { } // The token is always the same, let's exploit that - if ( !isset( ApiQueryInfo::$cachedTokens[ 'block' ] ) ) { - ApiQueryInfo::$cachedTokens[ 'block' ] = $wgUser->getEditToken(); + if ( !isset( ApiQueryInfo::$cachedTokens['block'] ) ) { + ApiQueryInfo::$cachedTokens['block'] = $wgUser->getEditToken(); } - return ApiQueryInfo::$cachedTokens[ 'block' ]; + return ApiQueryInfo::$cachedTokens['block']; } public static function getUnblockToken( $pageid, $title ) { @@ -193,11 +195,11 @@ class ApiQueryInfo extends ApiQueryBase { } // The token is always the same, let's exploit that - if ( !isset( ApiQueryInfo::$cachedTokens[ 'email' ] ) ) { - ApiQueryInfo::$cachedTokens[ 'email' ] = $wgUser->getEditToken(); + if ( !isset( ApiQueryInfo::$cachedTokens['email'] ) ) { + ApiQueryInfo::$cachedTokens['email'] = $wgUser->getEditToken(); } - return ApiQueryInfo::$cachedTokens[ 'email' ]; + return ApiQueryInfo::$cachedTokens['email']; } public static function getImportToken( $pageid, $title ) { @@ -207,11 +209,11 @@ class ApiQueryInfo extends ApiQueryBase { } // The token is always the same, let's exploit that - if ( !isset( ApiQueryInfo::$cachedTokens[ 'import' ] ) ) { - ApiQueryInfo::$cachedTokens[ 'import' ] = $wgUser->getEditToken(); + if ( !isset( ApiQueryInfo::$cachedTokens['import'] ) ) { + ApiQueryInfo::$cachedTokens['import'] = $wgUser->getEditToken(); } - return ApiQueryInfo::$cachedTokens[ 'import' ]; + return ApiQueryInfo::$cachedTokens['import']; } public static function getWatchToken( $pageid, $title ) { @@ -221,11 +223,11 @@ class ApiQueryInfo extends ApiQueryBase { } // The token is always the same, let's exploit that - if ( !isset( ApiQueryInfo::$cachedTokens[ 'watch' ] ) ) { - ApiQueryInfo::$cachedTokens[ 'watch' ] = $wgUser->getEditToken( 'watch' ); + if ( !isset( ApiQueryInfo::$cachedTokens['watch'] ) ) { + ApiQueryInfo::$cachedTokens['watch'] = $wgUser->getEditToken( 'watch' ); } - return ApiQueryInfo::$cachedTokens[ 'watch' ]; + return ApiQueryInfo::$cachedTokens['watch']; } public static function getOptionsToken( $pageid, $title ) { @@ -235,11 +237,11 @@ class ApiQueryInfo extends ApiQueryBase { } // The token is always the same, let's exploit that - if ( !isset( ApiQueryInfo::$cachedTokens[ 'options' ] ) ) { - ApiQueryInfo::$cachedTokens[ 'options' ] = $wgUser->getEditToken(); + if ( !isset( ApiQueryInfo::$cachedTokens['options'] ) ) { + ApiQueryInfo::$cachedTokens['options'] = $wgUser->getEditToken(); } - return ApiQueryInfo::$cachedTokens[ 'options' ]; + return ApiQueryInfo::$cachedTokens['options']; } public function execute() { @@ -248,6 +250,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_watchers = isset( $prop['watchers'] ); $this->fld_notificationtimestamp = isset( $prop['notificationtimestamp'] ); $this->fld_talkid = isset( $prop['talkid'] ); $this->fld_subjectid = isset( $prop['subjectid'] ); @@ -268,10 +271,7 @@ class ApiQueryInfo extends ApiQueryBase { // Throw away any titles we're gonna skip so they don't // clutter queries $cont = explode( '|', $this->params['continue'] ); - if ( count( $cont ) != 2 ) { - $this->dieUsage( 'Invalid continue param. You should pass the original ' . - 'value returned by the previous query', '_badcontinue' ); - } + $this->dieContinueUsageIf( count( $cont ) != 2 ); $conttitle = Title::makeTitleSafe( $cont[0], $cont[1] ); foreach ( $this->everything as $pageid => $title ) { if ( Title::compare( $title, $conttitle ) >= 0 ) { @@ -308,6 +308,10 @@ class ApiQueryInfo extends ApiQueryBase { $this->getWatchedInfo(); } + if ( $this->fld_watchers ) { + $this->getWatcherInfo(); + } + // Run the talkid/subjectid query if requested if ( $this->fld_talkid || $this->fld_subjectid ) { $this->getTSIDs(); @@ -317,6 +321,7 @@ class ApiQueryInfo extends ApiQueryBase { $this->getDisplayTitle(); } + /** @var $title Title */ foreach ( $this->everything as $pageid => $title ) { $pageInfo = $this->extractPageInfo( $pageid, $title ); $fit = $result->addValue( array( @@ -334,7 +339,7 @@ class ApiQueryInfo extends ApiQueryBase { /** * Get a result array with information about a title - * @param $pageid int Page ID (negative for missing titles) + * @param int $pageid Page ID (negative for missing titles) * @param $title Title object * @return array */ @@ -349,7 +354,7 @@ class ApiQueryInfo extends ApiQueryBase { $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] ); $pageInfo['lastrevid'] = intval( $this->pageLatest[$pageid] ); $pageInfo['counter'] = $wgDisableCounters - ? "" + ? '' : intval( $this->pageCounter[$pageid] ); $pageInfo['length'] = intval( $this->pageLength[$pageid] ); @@ -387,6 +392,14 @@ class ApiQueryInfo extends ApiQueryBase { $pageInfo['watched'] = ''; } + if ( $this->fld_watchers ) { + if ( isset( $this->watchers[$ns][$dbkey] ) ) { + $pageInfo['watchers'] = $this->watchers[$ns][$dbkey]; + } elseif ( $this->showZeroWatchers ) { + $pageInfo['watchers'] = 0; + } + } + if ( $this->fld_notificationtimestamp ) { $pageInfo['notificationtimestamp'] = ''; if ( isset( $this->notificationtimestamps[$ns][$dbkey] ) ) { @@ -394,7 +407,7 @@ class ApiQueryInfo extends ApiQueryBase { } } - if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) { + if ( $this->fld_talkid && isset( $this->talkids[$ns][$dbkey] ) ) { $pageInfo['talkid'] = $this->talkids[$ns][$dbkey]; } @@ -406,7 +419,7 @@ class ApiQueryInfo extends ApiQueryBase { $pageInfo['fullurl'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); $pageInfo['editurl'] = wfExpandUrl( $title->getFullURL( 'action=edit' ), PROTO_CURRENT ); } - if ( $this->fld_readable && $title->userCan( 'read' ) ) { + if ( $this->fld_readable && $title->userCan( 'read', $this->getUser() ) ) { $pageInfo['readable'] = ''; } @@ -450,6 +463,7 @@ class ApiQueryInfo extends ApiQueryBase { $res = $this->select( __METHOD__ ); foreach ( $res as $row ) { + /** @var $title Title */ $title = $this->titles[$row->pr_page]; $a = array( 'type' => $row->pr_type, @@ -585,6 +599,7 @@ class ApiQueryInfo extends ApiQueryBase { private function getTSIDs() { $getTitles = $this->talkids = $this->subjectids = array(); + /** @var $t Title */ foreach ( $this->everything as $t ) { if ( MWNamespace::isTalk( $t->getNamespace() ) ) { if ( $this->fld_subjectid ) { @@ -678,6 +693,46 @@ class ApiQueryInfo extends ApiQueryBase { } } + /** + * Get the count of watchers and put it in $this->watchers + */ + private function getWatcherInfo() { + global $wgUnwatchedPageThreshold; + + if ( count( $this->everything ) == 0 ) { + return; + } + + $user = $this->getUser(); + $canUnwatchedpages = $user->isAllowed( 'unwatchedpages' ); + if ( !$canUnwatchedpages && !is_int( $wgUnwatchedPageThreshold ) ) { + return; + } + + $this->watchers = array(); + $this->showZeroWatchers = $canUnwatchedpages; + $db = $this->getDB(); + + $lb = new LinkBatch( $this->everything ); + + $this->resetQueryParams(); + $this->addTables( array( 'watchlist' ) ); + $this->addFields( array( 'wl_title', 'wl_namespace', 'count' => 'COUNT(*)' ) ); + $this->addWhere( array( + $lb->constructSet( 'wl', $db ) + ) ); + $this->addOption( 'GROUP BY', array( 'wl_namespace', 'wl_title' ) ); + if ( !$canUnwatchedpages ) { + $this->addOption( 'HAVING', "COUNT(*) >= $wgUnwatchedPageThreshold" ); + } + + $res = $this->select( __METHOD__ ); + + foreach ( $res as $row ) { + $this->watchers[$row->wl_namespace][$row->wl_title] = (int)$row->count; + } + } + public function getCacheMode( $params ) { $publicProps = array( 'protection', @@ -709,6 +764,7 @@ class ApiQueryInfo extends ApiQueryBase { 'protection', 'talkid', 'watched', # private + 'watchers', # private 'notificationtimestamp', # private 'subjectid', 'url', @@ -734,6 +790,7 @@ class ApiQueryInfo extends ApiQueryBase { ' 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', + ' watchers - The number of watchers, if allowed', ' notificationtimestamp - The watchlist notification timestamp of each page', ' subjectid - The page ID of the parent page for each talk page', ' url - Gives a full URL to the page, and also an edit URL', @@ -767,6 +824,12 @@ class ApiQueryInfo extends ApiQueryBase { 'watched' => array( 'watched' => 'boolean' ), + 'watchers' => array( + 'watchers' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ), 'notificationtimestamp' => array( 'notificationtimestamp' => array( ApiBase::PROP_TYPE => 'timestamp', @@ -809,12 +872,6 @@ class ApiQueryInfo extends ApiQueryBase { return 'Get basic page information such as namespace, title, last touched date, ...'; } - 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&prop=info&titles=Main%20Page', @@ -825,8 +882,4 @@ class ApiQueryInfo extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Properties#info_.2F_in'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryLangBacklinks.php b/includes/api/ApiQueryLangBacklinks.php index 3920407b..7a4880a4 100644 --- a/includes/api/ApiQueryLangBacklinks.php +++ b/includes/api/ApiQueryLangBacklinks.php @@ -56,10 +56,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { if ( !is_null( $params['continue'] ) ) { $cont = explode( '|', $params['continue'] ); - if ( count( $cont ) != 3 ) { - $this->dieUsage( 'Invalid continue param. You should pass the ' . - 'original value returned by the previous query', '_badcontinue' ); - } + $this->dieContinueUsageIf( count( $cont ) != 3 ); $db = $this->getDB(); $op = $params['dir'] == 'descending' ? '<' : '>'; @@ -233,7 +230,6 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { public function getPossibleErrors() { return array_merge( parent::getPossibleErrors(), array( array( 'missingparam', 'lang' ), - array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), ) ); } @@ -243,8 +239,4 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { 'api.php?action=query&generator=langbacklinks&glbltitle=Test&glbllang=fr&prop=info' ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index 3109a090..ac65d2d2 100644 --- a/includes/api/ApiQueryLangLinks.php +++ b/includes/api/ApiQueryLangLinks.php @@ -25,7 +25,7 @@ */ /** - * A query module to list all langlinks (links to correspanding foreign language pages). + * A query module to list all langlinks (links to corresponding foreign language pages). * * @ingroup API */ @@ -56,10 +56,7 @@ class ApiQueryLangLinks extends ApiQueryBase { $this->addWhereFld( 'll_from', array_keys( $this->getPageSet()->getGoodTitles() ) ); if ( !is_null( $params['continue'] ) ) { $cont = explode( '|', $params['continue'] ); - if ( count( $cont ) != 2 ) { - $this->dieUsage( 'Invalid continue param. You should pass the ' . - 'original value returned by the previous query', '_badcontinue' ); - } + $this->dieContinueUsageIf( count( $cont ) != 2 ); $op = $params['dir'] == 'descending' ? '<' : '>'; $llfrom = intval( $cont[0] ); $lllang = $this->getDB()->addQuotes( $cont[1] ); @@ -179,7 +176,6 @@ class ApiQueryLangLinks extends ApiQueryBase { public function getPossibleErrors() { return array_merge( parent::getPossibleErrors(), array( array( 'missingparam', 'lang' ), - array( 'code' => '_badcontinue', 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' ), ) ); } @@ -192,8 +188,4 @@ class ApiQueryLangLinks extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Properties#langlinks_.2F_ll'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryLinks.php b/includes/api/ApiQueryLinks.php index 9e4b7ebb..937f4f13 100644 --- a/includes/api/ApiQueryLinks.php +++ b/includes/api/ApiQueryLinks.php @@ -79,7 +79,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { */ private function run( $resultPageSet = null ) { if ( $this->getPageSet()->getGoodTitleCount() == 0 ) { - return; // nothing to do + return; // nothing to do } $params = $this->extractRequestParams(); @@ -112,10 +112,7 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { if ( !is_null( $params['continue'] ) ) { $cont = explode( '|', $params['continue'] ); - if ( count( $cont ) != 3 ) { - $this->dieUsage( 'Invalid continue param. You should pass the ' . - 'original value returned by the previous query', '_badcontinue' ); - } + $this->dieContinueUsageIf( count( $cont ) != 3 ); $op = $params['dir'] == 'descending' ? '<' : '>'; $plfrom = intval( $cont[0] ); $plns = intval( $cont[1] ); @@ -241,17 +238,13 @@ class ApiQueryLinks extends ApiQueryGeneratorBase { $desc = $this->description; $name = $this->getModuleName(); return array( - "api.php?action=query&prop={$name}&titles=Main%20Page" => "Get {$desc}s from the [[Main Page]]:", - "api.php?action=query&generator={$name}&titles=Main%20Page&prop=info" => "Get information about the {$desc} pages in the [[Main Page]]:", - "api.php?action=query&prop={$name}&titles=Main%20Page&{$this->prefix}namespace=2|10" => "Get {$desc}s from the Main Page in the User and Template namespaces:", + "api.php?action=query&prop={$name}&titles=Main%20Page" => "Get {$desc}s from the [[Main Page]]", + "api.php?action=query&generator={$name}&titles=Main%20Page&prop=info" => "Get information about the {$desc} pages in the [[Main Page]]", + "api.php?action=query&prop={$name}&titles=Main%20Page&{$this->prefix}namespace=2|10" => "Get {$desc}s from the Main Page in the User and Template namespaces", ); } public function getHelpUrls() { return $this->helpUrl; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php index 5d85c221..73dcea49 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -58,7 +58,7 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->fld_details = isset( $prop['details'] ); $this->fld_tags = isset( $prop['tags'] ); - $hideLogs = LogEventsList::getExcludeClause( $db ); + $hideLogs = LogEventsList::getExcludeClause( $db, 'user', $this->getUser() ); if ( $hideLogs !== false ) { $this->addWhere( $hideLogs ); } @@ -70,7 +70,7 @@ class ApiQueryLogEvents extends ApiQueryBase { 'user' => array( 'JOIN', 'user_id=log_user' ), 'page' => array( 'LEFT JOIN', - array( 'log_namespace=page_namespace', + array( 'log_namespace=page_namespace', 'log_title=page_title' ) ) ) ); $index = array( 'logging' => 'times' ); // default, may change @@ -151,7 +151,7 @@ class ApiQueryLogEvents extends ApiQueryBase { if ( is_null( $title ) ) { $this->dieUsage( "Bad title value '$prefix'", 'param_prefix' ); } - $this->addWhereFld( 'log_namespace', $title->getNamespace() ); + $this->addWhereFld( 'log_namespace', $title->getNamespace() ); $this->addWhere( 'log_title ' . $db->buildLike( $title->getDBkey(), $db->anyString() ) ); } @@ -201,7 +201,7 @@ class ApiQueryLogEvents extends ApiQueryBase { public static function addLogParams( $result, &$vals, $params, $type, $action, $ts, $legacy = false ) { switch ( $type ) { case 'move': - if ( $legacy ){ + if ( $legacy ) { $targetKey = 0; $noredirKey = 1; } else { @@ -209,21 +209,21 @@ class ApiQueryLogEvents extends ApiQueryBase { $noredirKey = '5::noredir'; } - if ( isset( $params[ $targetKey ] ) ) { - $title = Title::newFromText( $params[ $targetKey ] ); + if ( isset( $params[$targetKey] ) ) { + $title = Title::newFromText( $params[$targetKey] ); if ( $title ) { $vals2 = array(); ApiQueryBase::addTitleInfo( $vals2, $title, 'new_' ); $vals[$type] = $vals2; } } - if ( isset( $params[ $noredirKey ] ) && $params[ $noredirKey ] ) { + if ( isset( $params[$noredirKey] ) && $params[$noredirKey] ) { $vals[$type]['suppressedredirect'] = ''; } $params = null; break; case 'patrol': - if ( $legacy ){ + if ( $legacy ) { $cur = 0; $prev = 1; $auto = 2; @@ -241,7 +241,12 @@ class ApiQueryLogEvents extends ApiQueryBase { break; case 'rights': $vals2 = array(); - list( $vals2['old'], $vals2['new'] ) = $params; + if( $legacy ) { + list( $vals2['old'], $vals2['new'] ) = $params; + } else { + $vals2['new'] = implode( ', ', $params['5::newgroups'] ); + $vals2['old'] = implode( ', ', $params['4::oldgroups'] ); + } $vals[$type] = $vals2; $params = null; break; @@ -262,9 +267,19 @@ class ApiQueryLogEvents extends ApiQueryBase { break; } if ( !is_null( $params ) ) { - $result->setIndexedTagName( $params, 'param' ); - $result->setIndexedTagName_recursive( $params, 'param' ); - $vals = array_merge( $vals, $params ); + $logParams = array(); + // Keys like "4::paramname" can't be used for output so we change them to "paramname" + foreach ( $params as $key => $value ) { + if ( strpos( $key, ':' ) === false ) { + $logParams[$key] = $value; + continue; + } + $logParam = explode( ':', $key, 3 ); + $logParams[$logParam[2]] = $value; + } + $result->setIndexedTagName( $logParams, 'param' ); + $result->setIndexedTagName_recursive( $logParams, 'param' ); + $vals = array_merge( $vals, $logParams ); } return $vals; } @@ -362,8 +377,12 @@ class ApiQueryLogEvents extends ApiQueryBase { if ( !is_null( $params['prop'] ) && in_array( 'parsedcomment', $params['prop'] ) ) { // formatComment() calls wfMessage() among other things return 'anon-public-user-private'; - } else { + } elseif ( LogEventsList::getExcludeClause( $this->getDB(), 'user', $this->getUser() ) + === LogEventsList::getExcludeClause( $this->getDB(), 'public' ) + ) { // Output can only contain public data. return 'public'; + } else { + return 'anon-public-user-private'; } } @@ -432,7 +451,7 @@ class ApiQueryLogEvents extends ApiQueryBase { ' timestamp - Adds the timestamp for the event', ' comment - Adds the comment of the event', ' parsedcomment - Adds the parsed comment of the event', - ' details - Lists addtional details about the event', + ' details - Lists additional details about the event', ' tags - Lists tags for the event', ), 'type' => 'Filter log entries to only this type', @@ -526,8 +545,4 @@ class ApiQueryLogEvents extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Logevents'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryORM.php b/includes/api/ApiQueryORM.php new file mode 100644 index 00000000..41d8f11c --- /dev/null +++ b/includes/api/ApiQueryORM.php @@ -0,0 +1,264 @@ +<?php + +/** + * Base query module for querying results from ORMTables. + * + * 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 + * + * @since 1.21 + * + * @file + * @ingroup API + * + * @license GNU GPL v2+ + * @author Jeroen De Dauw < jeroendedauw@gmail.com > + */ +abstract class ApiQueryORM extends ApiQueryBase { + + /** + * Returns an instance of the IORMTable table being queried. + * + * @since 1.21 + * + * @return IORMTable + */ + abstract protected function getTable(); + + /** + * Returns the name of the individual rows. + * For example: page, user, contest, campaign, etc. + * This is used to appropriately name elements in XML. + * Deriving classes typically override this method. + * + * @since 1.21 + * + * @return string + */ + protected function getRowName() { + return 'item'; + } + + /** + * Returns the name of the list of rows. + * For example: pages, users, contests, campaigns, etc. + * This is used to appropriately name nodes in the output. + * Deriving classes typically override this method. + * + * @since 1.21 + * + * @return string + */ + protected function getListName() { + return 'items'; + } + + /** + * Returns the path to where the items results should be added in the result. + * + * @since 1.21 + * + * @return null|string|array + */ + protected function getResultPath() { + return null; + } + + /** + * Get the parameters, find out what the conditions for the query are, + * run it, and add the results. + * + * @since 1.21 + */ + public function execute() { + $params = $this->getParams(); + + if ( !in_array( 'id', $params['props'] ) ) { + $params['props'][] = 'id'; + } + + $results = $this->getResults( $params, $this->getConditions( $params ) ); + $this->addResults( $params, $results ); + } + + /** + * Get the request parameters and remove all params set + * to null (ie those that are not actually provided). + * + * @since 1.21 + * + * @return array + */ + protected function getParams() { + return array_filter( + $this->extractRequestParams(), + function( $prop ) { + return isset( $prop ); + } + ); + } + + /** + * Get the conditions for the query. These will be provided as + * regular parameters, together with limit, props, continue, + * and possibly others which we need to get rid off. + * + * @since 1.21 + * + * @param array $params + * + * @return array + */ + protected function getConditions( array $params ) { + $conditions = array(); + $fields = $this->getTable()->getFields(); + + foreach ( $params as $name => $value ) { + if ( array_key_exists( $name, $fields ) ) { + $conditions[$name] = $value; + } + } + + return $conditions; + } + + /** + * Get the actual results. + * + * @since 1.21 + * + * @param array $params + * @param array $conditions + * + * @return ORMResult + */ + protected function getResults( array $params, array $conditions ) { + return $this->getTable()->select( + $params['props'], + $conditions, + array( + 'LIMIT' => $params['limit'] + 1, + 'ORDER BY' => $this->getTable()->getPrefixedField( 'id' ) . ' ASC', + ), + __METHOD__ + ); + } + + /** + * Serialize the results and add them to the result object. + * + * @since 1.21 + * + * @param array $params + * @param ORMResult $results + */ + protected function addResults( array $params, ORMResult $results ) { + $serializedResults = array(); + $count = 0; + + foreach ( $results as /* IORMRow */ $result ) { + 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', $result->getId() ); + break; + } + + $serializedResults[] = $this->formatRow( $result, $params ); + } + + $this->setIndexedTagNames( $serializedResults ); + $this->addSerializedResults( $serializedResults ); + } + + /** + * Formats a row to it's desired output format. + * + * @since 1.21 + * + * @param IORMRow $result + * @param array $params + * + * @return mixed + */ + protected function formatRow( IORMRow $result, array $params ) { + return $result->toArray( $params['props'] ); + } + + /** + * Set the tag names for formats such as XML. + * + * @since 1.21 + * + * @param array $serializedResults + */ + protected function setIndexedTagNames( array &$serializedResults ) { + $this->getResult()->setIndexedTagName( $serializedResults, $this->getRowName() ); + } + + /** + * Add the serialized results to the result object. + * + * @since 1.21 + * + * @param array $serializedResults + */ + protected function addSerializedResults( array $serializedResults ) { + $this->getResult()->addValue( + $this->getResultPath(), + $this->getListName(), + $serializedResults + ); + } + + /** + * @see ApiBase::getAllowedParams() + * @return array + */ + public function getAllowedParams() { + $params = array ( + 'props' => array( + ApiBase::PARAM_TYPE => $this->getTable()->getFieldNames(), + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_REQUIRED => true, + ), + 'limit' => array( + ApiBase::PARAM_DFLT => 20, + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_MIN => 1, + ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, + ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 + ), + 'continue' => null, + ); + + return array_merge( $this->getTable()->getAPIParams(), $params ); + } + + /** + * @see ApiBase::getParamDescription() + * @return array + */ + public function getParamDescription() { + $descriptions = array ( + 'props' => 'Fields to query', + 'continue' => 'Offset number from where to continue the query', + 'limit' => 'Max amount of rows to return', + ); + + return array_merge( $this->getTable()->getFieldDescriptions(), $descriptions ); + } + +} diff --git a/includes/api/ApiQueryPagePropNames.php b/includes/api/ApiQueryPagePropNames.php new file mode 100644 index 00000000..08c883d8 --- /dev/null +++ b/includes/api/ApiQueryPagePropNames.php @@ -0,0 +1,116 @@ +<?php +/** + * Created on January 21, 2013 + * + * Copyright © 2013 Brad Jorsch <bjorsch@wikimedia.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.21 + * @author Brad Jorsch + */ + +/** + * A query module to list used page props + * + * @ingroup API + * @since 1.21 + */ +class ApiQueryPagePropNames extends ApiQueryBase { + + public function __construct( $query, $moduleName ) { + parent::__construct( $query, $moduleName, 'ppn' ); + } + + public function getCacheMode( $params ) { + return 'public'; + } + + public function execute() { + $params = $this->extractRequestParams(); + + $this->addTables( 'page_props' ); + $this->addFields( 'pp_propname' ); + $this->addOption( 'DISTINCT' ); + $this->addOption( 'ORDER BY', 'pp_propname' ); + + if ( $params['continue'] ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 1 ); + + // Add a WHERE clause + $this->addWhereRange( 'pp_propname', 'newer', $cont[0], null ); + } + + $limit = $params['limit']; + $this->addOption( 'LIMIT', $limit + 1 ); + + $result = $this->getResult(); + $count = 0; + foreach ( $this->select( __METHOD__ ) as $row ) { + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'continue', $row->pp_propname ); + break; + } + + $vals = array(); + $vals['propname'] = $row->pp_propname; + $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); + if ( !$fit ) { + $this->setContinueEnumParameter( 'continue', $row->pp_propname ); + break; + } + } + + $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'p' ); + } + + public function getAllowedParams() { + return array( + 'continue' => null, + 'limit' => array( + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_DFLT => 10, + ApiBase::PARAM_MIN => 1, + ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1, + ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2 + ), + ); + } + + public function getParamDescription() { + return array( + 'continue' => 'When more results are available, use this to continue', + 'limit' => 'The maximum number of pages to return', + ); + } + + public function getDescription() { + return 'List all page prop names in use on the wiki'; + } + + public function getExamples() { + return array( + 'api.php?action=query&list=pagepropnames' => 'Get first 10 prop names', + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Pagepropnames'; + } +} diff --git a/includes/api/ApiQueryPageProps.php b/includes/api/ApiQueryPageProps.php index 1eef67e6..2de57106 100644 --- a/includes/api/ApiQueryPageProps.php +++ b/includes/api/ApiQueryPageProps.php @@ -49,7 +49,7 @@ class ApiQueryPageProps extends ApiQueryBase { $this->addTables( 'page_props' ); $this->addFields( array( 'pp_page', 'pp_propname', 'pp_value' ) ); - $this->addWhereFld( 'pp_page', array_keys( $pages ) ); + $this->addWhereFld( 'pp_page', array_keys( $pages ) ); if ( $this->params['continue'] ) { $this->addWhere( 'pp_page >=' . intval( $this->params['continue'] ) ); @@ -60,7 +60,10 @@ class ApiQueryPageProps extends ApiQueryBase { } # Force a sort order to ensure that properties are grouped by page - $this->addOption( 'ORDER BY', 'pp_page' ); + # But only if pp_page is not constant in the WHERE clause. + if ( count( $pages ) > 1 ) { + $this->addOption( 'ORDER BY', 'pp_page' ); + } $res = $this->select( __METHOD__ ); $currentPage = 0; # Id of the page currently processed @@ -122,14 +125,16 @@ class ApiQueryPageProps extends ApiQueryBase { public function getAllowedParams() { return array( 'continue' => null, - 'prop' => null, + 'prop' => array( + ApiBase::PARAM_ISMULTI => true, + ), ); } public function getParamDescription() { return array( 'continue' => 'When more results are available, use this to continue', - 'prop' => 'Page prop to look on the page for. Useful for checking whether a certain page uses a certain page prop.' + 'prop' => 'Only list these props. Useful for checking whether a certain page uses a certain page prop', ); } @@ -146,8 +151,4 @@ class ApiQueryPageProps extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Properties#pageprops_.2F_pp'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryPagesWithProp.php b/includes/api/ApiQueryPagesWithProp.php new file mode 100644 index 00000000..0132fc3e --- /dev/null +++ b/includes/api/ApiQueryPagesWithProp.php @@ -0,0 +1,189 @@ +<?php +/** + * Created on December 31, 2012 + * + * Copyright © 2012 Brad Jorsch <bjorsch@wikimedia.org> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + * @since 1.21 + * @author Brad Jorsch + */ + +/** + * A query module to enumerate pages that use a particular prop + * + * @ingroup API + * @since 1.21 + */ +class ApiQueryPagesWithProp extends ApiQueryGeneratorBase { + + public function __construct( $query, $moduleName ) { + parent::__construct( $query, $moduleName, 'pwp' ); + } + + public function execute() { + $this->run(); + } + + public function getCacheMode( $params ) { + return 'public'; + } + + public function executeGenerator( $resultPageSet ) { + $this->run( $resultPageSet ); + } + + /** + * @param $resultPageSet ApiPageSet + * @return void + */ + private function run( $resultPageSet = null ) { + $params = $this->extractRequestParams(); + + $prop = array_flip( $params['prop'] ); + $fld_ids = isset( $prop['ids'] ); + $fld_title = isset( $prop['title'] ); + $fld_value = isset( $prop['value'] ); + + if ( $resultPageSet === null ) { + $this->addFields( array( 'page_id' ) ); + $this->addFieldsIf( array( 'page_title', 'page_namespace' ), $fld_title ); + $this->addFieldsIf( 'pp_value', $fld_value ); + } else { + $this->addFields( $resultPageSet->getPageTableFields() ); + } + $this->addTables( array( 'page_props', 'page' ) ); + $this->addWhere( 'pp_page=page_id' ); + $this->addWhereFld( 'pp_propname', $params['propname'] ); + + $dir = ( $params['dir'] == 'ascending' ) ? 'newer' : 'older'; + + if ( $params['continue'] ) { + $cont = explode( '|', $params['continue'] ); + $this->dieContinueUsageIf( count( $cont ) != 1 ); + + // Add a WHERE clause + $from = (int)$cont[0]; + $this->addWhereRange( 'pp_page', $dir, $from, null ); + } + + $sort = ( $params['dir'] === 'descending' ? ' DESC' : '' ); + $this->addOption( 'ORDER BY', 'pp_page' . $sort ); + + $limit = $params['limit']; + $this->addOption( 'LIMIT', $limit + 1 ); + + $result = $this->getResult(); + $count = 0; + foreach ( $this->select( __METHOD__ ) as $row ) { + if ( ++$count > $limit ) { + // We've reached the one extra which shows that there are additional pages to be had. Stop here... + $this->setContinueEnumParameter( 'continue', $row->page_id ); + break; + } + + if ( $resultPageSet === null ) { + $vals = array(); + if ( $fld_ids ) { + $vals['pageid'] = (int)$row->page_id; + } + if ( $fld_title ) { + $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + ApiQueryBase::addTitleInfo( $vals, $title ); + } + if ( $fld_value ) { + $vals['value'] = $row->pp_value; + } + $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); + if ( !$fit ) { + $this->setContinueEnumParameter( 'continue', $row->page_id ); + break; + } + } else { + $resultPageSet->processDbRow( $row ); + } + } + + if ( $resultPageSet === null ) { + $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'page' ); + } + } + + public function getAllowedParams() { + return array( + 'propname' => array( + ApiBase::PARAM_TYPE => 'string', + ApiBase::PARAM_REQUIRED => true, + ), + 'prop' => array( + ApiBase::PARAM_DFLT => 'ids|title', + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => array ( + 'ids', + 'title', + 'value', + ) + ), + 'continue' => null, + 'limit' => array( + ApiBase::PARAM_TYPE => 'limit', + ApiBase::PARAM_DFLT => 10, + 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', + ) + ), + ); + } + + public function getParamDescription() { + return array( + 'propname' => 'Page prop for which to enumerate pages', + 'prop' => array( + 'What pieces of information to include', + ' ids - Adds the page ID', + ' title - Adds the title and namespace ID of the page', + ' value - Adds the value of the page prop', + ), + 'dir' => 'In which direction to sort', + 'continue' => 'When more results are available, use this to continue', + 'limit' => 'The maximum number of pages to return', + ); + } + + public function getDescription() { + return 'List all pages using a given page prop'; + } + + public function getExamples() { + return array( + 'api.php?action=query&list=pageswithprop&pwppropname=displaytitle&pwpprop=ids|title|value' => 'Get first 10 pages using {{DISPLAYTITLE:}}', + 'api.php?action=query&generator=pageswithprop&gpwppropname=notoc&prop=info' => 'Get page info about first 10 pages using __NOTOC__', + ); + } + + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Pageswithprop'; + } +} diff --git a/includes/api/ApiQueryProtectedTitles.php b/includes/api/ApiQueryProtectedTitles.php index 14aed28d..4aa00007 100644 --- a/includes/api/ApiQueryProtectedTitles.php +++ b/includes/api/ApiQueryProtectedTitles.php @@ -98,7 +98,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { $vals['user'] = $row->user_name; } - if ( isset( $prop['user'] ) ) { + if ( isset( $prop['userid'] ) || /*B/C*/isset( $prop['user'] ) ) { $vals['userid'] = $row->pt_user; } @@ -231,6 +231,9 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { ), 'userid' => 'integer' ), + 'userid' => array( + 'userid' => 'integer' + ), 'comment' => array( 'comment' => 'string' ), @@ -261,8 +264,4 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Protectedtitles'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php index a8be26d3..b03bdfb8 100644 --- a/includes/api/ApiQueryQueryPage.php +++ b/includes/api/ApiQueryQueryPage.php @@ -75,6 +75,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { $params = $this->extractRequestParams(); $result = $this->getResult(); + /** @var $qp QueryPage */ $qp = new $this->qpMap[$params['page']](); if ( !$qp->userCanExecute( $this->getUser() ) ) { $this->dieUsageMsg( 'specialpage-cantexecute' ); @@ -141,6 +142,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { } public function getCacheMode( $params ) { + /** @var $qp QueryPage */ $qp = new $this->qpMap[$params['page']](); if ( $qp->getRestriction() != '' ) { return 'private'; @@ -211,7 +213,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { public function getPossibleErrors() { return array_merge( parent::getPossibleErrors(), array( - array( 'specialpage-cantexecute' ) + array( 'specialpage-cantexecute' ) ) ); } @@ -220,8 +222,4 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { 'api.php?action=query&list=querypage&qppage=Ancientpages' ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php index ddf5841b..ae3bb893 100644 --- a/includes/api/ApiQueryRandom.php +++ b/includes/api/ApiQueryRandom.php @@ -33,6 +33,8 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { + private $pageIDs; + public function __construct( $query, $moduleName ) { parent::__construct( $query, $moduleName, 'rn' ); } @@ -183,8 +185,4 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { public function getExamples() { return 'api.php?action=query&list=random&rnnamespace=0&rnlimit=2'; } - - public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRandom.php overlordq$'; - } } diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 7ae4f371..8aceab22 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -105,7 +105,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /** * Sets internal state to include the desired properties in the output. - * @param $prop Array associative array of properties, only keys are used here + * @param array $prop associative array of properties, only keys are used here */ public function initProperties( $prop ) { $this->fld_comment = isset( $prop['comment'] ); @@ -149,6 +149,31 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $this->addTables( 'recentchanges' ); $index = array( 'recentchanges' => 'rc_timestamp' ); // May change $this->addTimestampWhereRange( 'rc_timestamp', $params['dir'], $params['start'], $params['end'] ); + + if ( !is_null( $params['continue'] ) ) { + $cont = explode( '|', $params['continue'] ); + if ( count( $cont ) != 2 ) { + $this->dieUsage( 'Invalid continue param. You should pass the ' . + 'original value returned by the previous query', '_badcontinue' ); + } + + $timestamp = $this->getDB()->addQuotes( wfTimestamp( TS_MW, $cont[0] ) ); + $id = intval( $cont[1] ); + $op = $params['dir'] === 'older' ? '<' : '>'; + + $this->addWhere( + "rc_timestamp $op $timestamp OR " . + "(rc_timestamp = $timestamp AND " . + "rc_id $op= $id)" + ); + } + + $order = $params['dir'] === 'older' ? 'DESC' : 'ASC'; + $this->addOption( 'ORDER BY', array( + "rc_timestamp $order", + "rc_id $order", + ) ); + $this->addWhereFld( 'rc_namespace', $params['namespace'] ); $this->addWhereFld( 'rc_deleted', 0 ); @@ -214,8 +239,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { 'rc_title', 'rc_cur_id', 'rc_type', - 'rc_moved_to_ns', - 'rc_moved_to_title', 'rc_deleted' ) ); @@ -231,12 +254,13 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $this->dieUsage( 'You need the patrol right to request the patrolled flag', 'permissiondenied' ); } + $this->addFields( 'rc_id' ); /* Add fields to our query if they are specified as a needed parameter. */ - $this->addFieldsIf( array( 'rc_id', 'rc_this_oldid', 'rc_last_oldid' ), $this->fld_ids ); + $this->addFieldsIf( array( 'rc_this_oldid', 'rc_last_oldid' ), $this->fld_ids ); $this->addFieldsIf( 'rc_comment', $this->fld_comment || $this->fld_parsedcomment ); $this->addFieldsIf( 'rc_user', $this->fld_user ); $this->addFieldsIf( 'rc_user_text', $this->fld_user || $this->fld_userid ); - $this->addFieldsIf( array( 'rc_minor', 'rc_type', '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 ); @@ -262,7 +286,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { if ( !is_null( $params['tag'] ) ) { $this->addTables( 'change_tag' ); $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'rc_id=ct_rc_id' ) ) ) ); - $this->addWhereFld( 'ct_tag' , $params['tag'] ); + $this->addWhereFld( 'ct_tag', $params['tag'] ); global $wgOldChangeTagsIndex; $index['change_tag'] = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id'; } @@ -283,7 +307,7 @@ class ApiQueryRecentChanges 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( 'start', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) ); + $this->setContinueEnumParameter( 'continue', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) . '|' . $row->rc_id ); break; } @@ -297,7 +321,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); if ( !$fit ) { - $this->setContinueEnumParameter( 'start', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) ); + $this->setContinueEnumParameter( 'continue', wfTimestamp( TS_ISO_8601, $row->rc_timestamp ) . '|' . $row->rc_id ); break; } } else { @@ -316,17 +340,11 @@ 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. + * @param mixed $row The row from which to extract the data. * @return array An array mapping strings (descriptors) to their respective string values. * @access public */ public function extractRowInfo( $row ) { - /* If page was moved somewhere, get the title of the move target. */ - $movedToTitle = false; - if ( isset( $row->rc_moved_to_title ) && $row->rc_moved_to_title !== '' ) { - $movedToTitle = Title::makeTitle( $row->rc_moved_to_ns, $row->rc_moved_to_title ); - } - /* Determine the title of the page that has been changed. */ $title = Title::makeTitle( $row->rc_namespace, $row->rc_title ); @@ -349,6 +367,9 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { case RC_LOG: $vals['type'] = 'log'; break; + case RC_EXTERNAL: + $vals['type'] = 'external'; + break; case RC_MOVE_OVER_REDIRECT: $vals['type'] = 'move over redirect'; break; @@ -359,9 +380,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { /* Create a new entry in the result for the title. */ if ( $this->fld_title ) { ApiQueryBase::addTitleInfo( $vals, $title ); - if ( $movedToTitle ) { - ApiQueryBase::addTitleInfo( $vals, $movedToTitle, 'new_' ); - } } /* Add ids, such as rcid, pageid, revid, and oldid to the change's info. */ @@ -488,6 +506,8 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { return RC_NEW; case 'log': return RC_LOG; + case 'external': + return RC_EXTERNAL; } } @@ -584,11 +604,13 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => array( 'edit', + 'external', 'new', 'log' ) ), 'toponly' => false, + 'continue' => null, ); } @@ -626,6 +648,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { 'limit' => 'How many total changes to return', 'tag' => 'Only list changes tagged with this tag', 'toponly' => 'Only list changes which are the latest revision', + 'continue' => 'When more results are available, use this to continue', ); } @@ -741,8 +764,4 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Recentchanges'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php index b89a8ea9..192fe873 100644 --- a/includes/api/ApiQueryRevisions.php +++ b/includes/api/ApiQueryRevisions.php @@ -34,15 +34,15 @@ class ApiQueryRevisions extends ApiQueryBase { private $diffto, $difftotext, $expandTemplates, $generateXML, $section, - $token, $parseContent; + $token, $parseContent, $contentFormat; public function __construct( $query, $moduleName ) { parent::__construct( $query, $moduleName, 'rv' ); } - private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false, + private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false, $fld_sha1 = false, $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false, - $fld_content = false, $fld_tags = false; + $fld_content = false, $fld_tags = false, $fld_contentmodel = false; private $tokenFunctions; @@ -95,7 +95,6 @@ class ApiQueryRevisions extends ApiQueryBase { !is_null( $params['endid'] ) || $params['dir'] === 'newer' || !is_null( $params['start'] ) || !is_null( $params['end'] ) ); - $pageSet = $this->getPageSet(); $pageCount = $pageSet->getGoodTitleCount(); $revCount = $pageSet->getRevisionCount(); @@ -155,15 +154,20 @@ class ApiQueryRevisions extends ApiQueryBase { $this->fld_parsedcomment = isset ( $prop['parsedcomment'] ); $this->fld_size = isset ( $prop['size'] ); $this->fld_sha1 = isset ( $prop['sha1'] ); + $this->fld_contentmodel = isset ( $prop['contentmodel'] ); $this->fld_userid = isset( $prop['userid'] ); $this->fld_user = isset ( $prop['user'] ); $this->token = $params['token']; + if ( !empty( $params['contentformat'] ) ) { + $this->contentFormat = $params['contentformat']; + } + // Possible indexes used $index = array(); $userMax = ( $this->fld_content ? ApiBase::LIMIT_SML1 : ApiBase::LIMIT_BIG1 ); - $botMax = ( $this->fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 ); + $botMax = ( $this->fld_content ? ApiBase::LIMIT_SML2 : ApiBase::LIMIT_BIG2 ); $limit = $params['limit']; if ( $limit == 'max' ) { $limit = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; @@ -184,15 +188,17 @@ class ApiQueryRevisions extends ApiQueryBase { if ( !is_null( $params['tag'] ) ) { $this->addTables( 'change_tag' ); $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) ) ); - $this->addWhereFld( 'ct_tag' , $params['tag'] ); + $this->addWhereFld( 'ct_tag', $params['tag'] ); global $wgOldChangeTagsIndex; $index['change_tag'] = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id'; } if ( isset( $prop['content'] ) || !is_null( $this->difftotext ) ) { // For each page we will request, the user must have read rights for that page + $user = $this->getUser(); + /** @var $title Title */ foreach ( $pageSet->getGoodTitles() as $title ) { - if ( !$title->userCan( 'read' ) ) { + if ( !$title->userCan( 'read', $user ) ) { $this->dieUsage( 'The current user is not allowed to read ' . $title->getPrefixedText(), 'accessdenied' ); @@ -255,7 +261,7 @@ class ApiQueryRevisions extends ApiQueryBase { // rvstart and rvstartid when that is supplied. if ( !is_null( $params['continue'] ) ) { $params['startid'] = $params['continue']; - unset( $params['start'] ); + $params['start'] = null; } // This code makes an assumption that sorting by rev_id and rev_timestamp produces @@ -332,10 +338,7 @@ class ApiQueryRevisions extends ApiQueryBase { if ( !is_null( $params['continue'] ) ) { $cont = explode( '|', $params['continue'] ); - if ( count( $cont ) != 2 ) { - $this->dieUsage( 'Invalid continue param. You should pass the original ' . - 'value returned by the previous query', '_badcontinue' ); - } + $this->dieContinueUsageIf( count( $cont ) != 2 ); $pageid = intval( $cont[0] ); $revid = intval( $cont[1] ); $this->addWhere( @@ -433,12 +436,18 @@ class ApiQueryRevisions extends ApiQueryBase { } } - if ( $this->fld_sha1 ) { + if ( $this->fld_sha1 && !$revision->isDeleted( Revision::DELETED_TEXT ) ) { if ( $revision->getSha1() != '' ) { $vals['sha1'] = wfBaseConvert( $revision->getSha1(), 36, 16, 40 ); } else { $vals['sha1'] = ''; } + } elseif ( $this->fld_sha1 ) { + $vals['sha1hidden'] = ''; + } + + if ( $this->fld_contentmodel ) { + $vals['contentmodel'] = $revision->getContentModel(); } if ( $this->fld_comment || $this->fld_parsedcomment ) { @@ -479,55 +488,121 @@ class ApiQueryRevisions extends ApiQueryBase { } } - $text = null; + $content = null; global $wgParser; - if ( $this->fld_content || !is_null( $this->difftotext ) ) { - $text = $revision->getText(); + if ( $this->fld_content || !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) { + $content = $revision->getContent(); // Expand templates after getting section content because // template-added sections don't count and Parser::preprocess() // will have less input - if ( $this->section !== false ) { - $text = $wgParser->getSection( $text, $this->section, false ); - if ( $text === false ) { + if ( $content && $this->section !== false ) { + $content = $content->getSection( $this->section, false ); + if ( !$content ) { $this->dieUsage( "There is no section {$this->section} in r" . $revision->getId(), 'nosuchsection' ); } } } - if ( $this->fld_content && !$revision->isDeleted( Revision::DELETED_TEXT ) ) { + if ( $this->fld_content && $content && !$revision->isDeleted( Revision::DELETED_TEXT ) ) { + $text = null; + if ( $this->generateXML ) { - $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS ); - $dom = $wgParser->preprocessToDom( $text ); - if ( is_callable( array( $dom, 'saveXML' ) ) ) { - $xml = $dom->saveXML(); + if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { + $t = $content->getNativeData(); # note: don't set $text + + $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS ); + $dom = $wgParser->preprocessToDom( $t ); + if ( is_callable( array( $dom, 'saveXML' ) ) ) { + $xml = $dom->saveXML(); + } else { + $xml = $dom->__toString(); + } + $vals['parsetree'] = $xml; } else { - $xml = $dom->__toString(); + $this->setWarning( "Conversion to XML is supported for wikitext only, " . + $title->getPrefixedDBkey() . + " uses content model " . $content->getModel() . ")" ); } - $vals['parsetree'] = $xml; - } + if ( $this->expandTemplates && !$this->parseContent ) { - $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) ); + #XXX: implement template expansion for all content types in ContentHandler? + if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) { + $text = $content->getNativeData(); + + $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) ); + } else { + $this->setWarning( "Template expansion is supported for wikitext only, " . + $title->getPrefixedDBkey() . + " uses content model " . $content->getModel() . ")" ); + + $text = false; + } } if ( $this->parseContent ) { - $text = $wgParser->parse( $text, $title, ParserOptions::newFromContext( $this->getContext() ) )->getText(); + $po = $content->getParserOutput( $title, $revision->getId(), ParserOptions::newFromContext( $this->getContext() ) ); + $text = $po->getText(); + } + + if ( $text === null ) { + $format = $this->contentFormat ? $this->contentFormat : $content->getDefaultFormat(); + $model = $content->getModel(); + + if ( !$content->isSupportedFormat( $format ) ) { + $name = $title->getPrefixedDBkey(); + + $this->dieUsage( "The requested format {$this->contentFormat} is not supported ". + "for content model $model used by $name", 'badformat' ); + } + + $text = $content->serialize( $format ); + + // always include format and model. + // Format is needed to deserialize, model is needed to interpret. + $vals['contentformat'] = $format; + $vals['contentmodel'] = $model; + } + + if ( $text !== false ) { + ApiResult::setContent( $vals, $text ); } - ApiResult::setContent( $vals, $text ); } elseif ( $this->fld_content ) { - $vals['texthidden'] = ''; + if ( $revision->isDeleted( Revision::DELETED_TEXT ) ) { + $vals['texthidden'] = ''; + } else { + $vals['textmissing'] = ''; + } } if ( !is_null( $this->diffto ) || !is_null( $this->difftotext ) ) { global $wgAPIMaxUncachedDiffs; static $n = 0; // Number of uncached diffs we've had - if ( $n < $wgAPIMaxUncachedDiffs ) { + + if ( is_null( $content ) ) { + $vals['textmissing'] = ''; + } elseif ( $n < $wgAPIMaxUncachedDiffs ) { $vals['diff'] = array(); $context = new DerivativeContext( $this->getContext() ); $context->setTitle( $title ); + $handler = $revision->getContentHandler(); + if ( !is_null( $this->difftotext ) ) { - $engine = new DifferenceEngine( $context ); - $engine->setText( $text, $this->difftotext ); + $model = $title->getContentModel(); + + if ( $this->contentFormat + && !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat ) ) { + + $name = $title->getPrefixedDBkey(); + + $this->dieUsage( "The requested format {$this->contentFormat} is not supported for ". + "content model $model used by $name", 'badformat' ); + } + + $difftocontent = ContentHandler::makeContent( $this->difftotext, $title, $model, $this->contentFormat ); + + $engine = $handler->createDifferenceEngine( $context ); + $engine->setContent( $content, $difftocontent ); } else { - $engine = new DifferenceEngine( $context, $revision->getID(), $this->diffto ); + $engine = $handler->createDifferenceEngine( $context, $revision->getID(), $this->diffto ); $vals['diff']['from'] = $engine->getOldid(); $vals['diff']['to'] = $engine->getNewid(); } @@ -567,6 +642,7 @@ class ApiQueryRevisions extends ApiQueryBase { 'userid', 'size', 'sha1', + 'contentmodel', 'comment', 'parsedcomment', 'content', @@ -616,6 +692,10 @@ class ApiQueryRevisions extends ApiQueryBase { 'continue' => null, 'diffto' => null, 'difftotext' => null, + 'contentformat' => array( + ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(), + ApiBase::PARAM_DFLT => null + ), ); } @@ -631,6 +711,7 @@ class ApiQueryRevisions extends ApiQueryBase { ' userid - User id of revision creator', ' size - Length (bytes) of the revision', ' sha1 - SHA-1 (base 16) of the revision', + ' contentmodel - Content model id', ' comment - Comment by the user for revision', ' parsedcomment - Parsed comment by the user for the revision', ' content - Text of the revision', @@ -644,9 +725,10 @@ class ApiQueryRevisions extends ApiQueryBase { 'dir' => $this->getDirectionDescription( $p, ' (enum)' ), 'user' => 'Only include revisions made by user (enum)', 'excludeuser' => 'Exclude revisions made by user (enum)', - 'expandtemplates' => 'Expand templates in revision content', - 'generatexml' => 'Generate XML parse tree for revision content', - 'parse' => 'Parse revision content. For performance reasons if this option is used, rvlimit is enforced to 1.', + 'expandtemplates' => "Expand templates in revision content (requires {$p}prop=content)", + 'generatexml' => "Generate XML parse tree for revision content (requires {$p}prop=content)", + 'parse' => array( "Parse revision content (requires {$p}prop=content).", + 'For performance reasons if this option is used, rvlimit is enforced to 1.' ), 'section' => 'Only retrieve the content of this section number', 'token' => 'Which tokens to obtain for each revision', 'continue' => 'When more results are available, use this to continue', @@ -655,6 +737,7 @@ class ApiQueryRevisions extends ApiQueryBase { 'difftotext' => array( 'Text to diff each revision to. Only diffs a limited number of revisions.', "Overrides {$p}diffto. If {$p}section is set, only that section will be diffed against this text" ), 'tag' => 'Only list revisions tagged with this tag', + 'contentformat' => 'Serialization format used for difftotext and expected for output of content', ); } @@ -709,8 +792,12 @@ class ApiQueryRevisions extends ApiQueryBase { ApiBase::PROP_TYPE => 'string', ApiBase::PROP_NULLABLE => true ), - 'texthidden' => 'boolean' - ) + 'texthidden' => 'boolean', + 'textmissing' => 'boolean', + ), + 'contentmodel' => array( + 'contentmodel' => 'string' + ), ); self::addTokenProperties( $props, $this->getTokenFunctions() ); @@ -732,13 +819,18 @@ class ApiQueryRevisions extends ApiQueryBase { public function getPossibleErrors() { return array_merge( parent::getPossibleErrors(), array( array( 'nosuchrevid', 'diffto' ), - array( 'code' => 'revids', 'info' => 'The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).' ), - array( 'code' => 'multpages', 'info' => 'titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start and end parameters may only be used on a single page.' ), + array( 'code' => 'revids', 'info' => 'The revids= parameter may not be used with the list options ' + . '(limit, startid, endid, dirNewer, start, end).' ), + array( 'code' => 'multpages', 'info' => 'titles, pageids or a generator was used to supply multiple pages, ' + . ' but the limit, startid, endid, dirNewer, user, excludeuser, ' + . 'start and end parameters may only be used on a single page.' ), array( 'code' => 'diffto', 'info' => 'rvdiffto must be set to a non-negative number, "prev", "next" or "cur"' ), array( 'code' => 'badparams', 'info' => 'start and startid cannot be used together' ), array( 'code' => 'badparams', 'info' => 'end and endid cannot be used together' ), array( 'code' => 'badparams', 'info' => 'user and excludeuser cannot be used together' ), array( 'code' => 'nosuchsection', 'info' => 'There is no section section in rID' ), + array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied ' + . ' to the page\'s content model' ), ) ); } @@ -762,8 +854,4 @@ class ApiQueryRevisions extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Properties#revisions_.2F_rv'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php index 364433d5..86183391 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -168,7 +168,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } } if ( isset( $prop['hasrelated'] ) && $result->hasRelated() ) { - $vals['hasrelated'] = ""; + $vals['hasrelated'] = ''; } // Add item to results and see whether it fits @@ -205,7 +205,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { ApiBase::PARAM_REQUIRED => true ), 'namespace' => array( - ApiBase::PARAM_DFLT => 0, + ApiBase::PARAM_DFLT => NS_MAIN, ApiBase::PARAM_TYPE => 'namespace', ApiBase::PARAM_ISMULTI => true, ), @@ -359,8 +359,4 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Search'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php index ec503d64..810e1d6b 100644 --- a/includes/api/ApiQuerySiteinfo.php +++ b/includes/api/ApiQuerySiteinfo.php @@ -96,6 +96,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { case 'variables': $fit = $this->appendVariables( $p ); break; + case 'protocols': + $fit = $this->appendProtocols( $p ); + break; default: ApiBase::dieDebug( __METHOD__, "Unknown prop=$p" ); } @@ -111,7 +114,9 @@ class ApiQuerySiteinfo extends ApiQueryBase { } protected function appendGeneralInfo( $property ) { - global $wgContLang; + global $wgContLang, + $wgDisableLangConversion, + $wgDisableTitleConversion; $data = array(); $mainPage = Title::newMainPage(); @@ -120,10 +125,31 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['sitename'] = $GLOBALS['wgSitename']; $data['generator'] = "MediaWiki {$GLOBALS['wgVersion']}"; $data['phpversion'] = phpversion(); - $data['phpsapi'] = php_sapi_name(); + $data['phpsapi'] = PHP_SAPI; $data['dbtype'] = $GLOBALS['wgDBtype']; $data['dbversion'] = $this->getDB()->getServerVersion(); + if ( !$wgDisableLangConversion ) { + $data['langconversion'] = ''; + } + + if ( !$wgDisableTitleConversion ) { + $data['titleconversion'] = ''; + } + + if ( $wgContLang->linkPrefixExtension() ) { + $data['linkprefix'] = wfMessage( 'linkprefix' )->inContentLanguage()->text(); + } else { + $data['linkprefix'] = ''; + } + + $linktrail = $wgContLang->linkTrail(); + if ( $linktrail ) { + $data['linktrail'] = $linktrail; + } else { + $data['linktrail'] = ''; + } + $git = SpecialVersion::getGitHeadSha1( $GLOBALS['IP'] ); if ( $git ) { $data['git-hash'] = $git; @@ -227,6 +253,11 @@ class ApiQuerySiteinfo extends ApiQueryBase { if ( MWNamespace::isNonincludable( $ns ) ) { $data[$ns]['nonincludable'] = ''; } + + $contentmodel = MWNamespace::getNamespaceContentModel( $ns ); + if ( $contentmodel ) { + $data[$ns]['defaultcontentmodel'] = $contentmodel; + } } $this->getResult()->setIndexedTagName( $data, 'ns' ); @@ -345,7 +376,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { ); } } else { - list( $host, $lag, $index ) = $lb->getMaxLag(); + list( , $lag, $index ) = $lb->getMaxLag(); $data[] = array( 'host' => $wgShowHostnames ? $lb->getServerName( $index ) @@ -457,7 +488,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { } if ( isset( $ext['author'] ) ) { $ret['author'] = is_array( $ext['author'] ) ? - implode( ', ', $ext['author' ] ) : $ext['author']; + implode( ', ', $ext['author'] ) : $ext['author']; } if ( isset( $ext['url'] ) ) { $ret['url'] = $ext['url']; @@ -525,7 +556,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { public function appendExtensionTags( $property ) { global $wgParser; $wgParser->firstCallInit(); - $tags = array_map( array( $this, 'formatParserTags'), $wgParser->getTags() ); + $tags = array_map( array( $this, 'formatParserTags' ), $wgParser->getTags() ); $this->getResult()->setIndexedTagName( $tags, 't' ); return $this->getResult()->addValue( 'query', $property, $tags ); } @@ -544,6 +575,14 @@ class ApiQuerySiteinfo extends ApiQueryBase { return $this->getResult()->addValue( 'query', $property, $variables ); } + public function appendProtocols( $property ) { + global $wgUrlProtocols; + // Make a copy of the global so we don't try to set the _element key of it - bug 45130 + $protocols = array_values( $wgUrlProtocols ); + $this->getResult()->setIndexedTagName( $protocols, 'p' ); + return $this->getResult()->addValue( 'query', $property, $protocols ); + } + private function formatParserTags( $item ) { return "<{$item}>"; } @@ -554,7 +593,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { ksort( $myWgHooks ); $data = array(); - foreach ( $myWgHooks as $hook => $hooks ) { + foreach ( $myWgHooks as $hook => $hooks ) { $arr = array( 'name' => $hook, 'subscribers' => array_map( array( 'SpecialVersion', 'arrayToString' ), $hooks ), @@ -596,6 +635,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { 'functionhooks', 'showhooks', 'variables', + 'protocols', ) ), 'filteriw' => array( @@ -633,6 +673,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { ' functionhooks - Returns a list of parser function hooks', ' showhooks - Returns a list of all subscribed hooks (contents of $wgHooks)', ' variables - Returns a list of variable IDs', + ' protocols - Returns a list of protocols that are allowed in external links.', ), 'filteriw' => 'Return only local or only nonlocal entries of the interwiki map', 'showalldb' => 'List all database servers, not just the one lagging the most', @@ -662,8 +703,4 @@ class ApiQuerySiteinfo extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Meta#siteinfo_.2F_si'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryStashImageInfo.php b/includes/api/ApiQueryStashImageInfo.php index a310d109..6899375a 100644 --- a/includes/api/ApiQueryStashImageInfo.php +++ b/includes/api/ApiQueryStashImageInfo.php @@ -42,7 +42,7 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { $result = $this->getResult(); if ( !$params['filekey'] && !$params['sessionkey'] ) { - $this->dieUsage( "One of filekey or sessionkey must be supplied", 'nofilekey'); + $this->dieUsage( "One of filekey or sessionkey must be supplied", 'nofilekey' ); } // Alias sessionkey to filekey, but give an existing filekey precedence. @@ -138,9 +138,4 @@ class ApiQueryStashImageInfo extends ApiQueryImageInfo { ); } - public function getVersion() { - return __CLASS__ . ': $Id$'; - } - } - diff --git a/includes/api/ApiQueryTags.php b/includes/api/ApiQueryTags.php index f97c1b2a..e0637ff7 100644 --- a/includes/api/ApiQueryTags.php +++ b/includes/api/ApiQueryTags.php @@ -162,7 +162,7 @@ class ApiQueryTags extends ApiQueryBase { 'prop' => array( 'Which properties to get', ' name - Adds name of tag', - ' displayname - Adds system messsage for the tag', + ' displayname - Adds system message for the tag', ' description - Adds description of the tag', ' hitcount - Adds the amount of revisions that have this tag', ), @@ -195,8 +195,4 @@ class ApiQueryTags extends ApiQueryBase { 'api.php?action=query&list=tags&tgprop=displayname|description|hitcount' ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index f30b1325..597c412d 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -160,10 +160,7 @@ class ApiQueryContributions extends ApiQueryBase { // Handle continue parameter if ( $this->multiUserMode && !is_null( $this->params['continue'] ) ) { $continue = explode( '|', $this->params['continue'] ); - if ( count( $continue ) != 2 ) { - $this->dieUsage( 'Invalid continue param. You should pass the original ' . - 'value returned by the previous query', '_badcontinue' ); - } + $this->dieContinueUsageIf( count( $continue ) != 2 ); $db = $this->getDB(); $encUser = $db->addQuotes( $continue[0] ); $encTS = $db->addQuotes( $db->timestamp( $continue[1] ) ); @@ -223,7 +220,7 @@ class ApiQueryContributions extends ApiQueryBase { ) ); if ( isset( $show['patrolled'] ) || isset( $show['!patrolled'] ) || - $this->fld_patrolled ) { + $this->fld_patrolled ) { if ( !$user->useRCPatrol() && !$user->useNPPatrol() ) { $this->dieUsage( 'You need the patrol right to request the patrolled flag', 'permissiondenied' ); } @@ -445,7 +442,7 @@ class ApiQueryContributions extends ApiQueryBase { 'end' => 'The end timestamp to return to', 'continue' => 'When more results are available, use this to continue', 'user' => 'The users to retrieve contributions for', - 'userprefix' => "Retrieve contibutions for all users whose names begin with this value. Overrides {$p}user", + 'userprefix' => "Retrieve contributions for all users whose names begin with this value. Overrides {$p}user", 'dir' => $this->getDirectionDescription( $p ), 'namespace' => 'Only list contributions in these namespaces', 'prop' => array( @@ -546,8 +543,4 @@ class ApiQueryContributions extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Usercontribs'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php index 66906659..1a491eca 100644 --- a/includes/api/ApiQueryUserInfo.php +++ b/includes/api/ApiQueryUserInfo.php @@ -77,18 +77,18 @@ class ApiQueryUserInfo extends ApiQueryBase { if ( isset( $this->prop['groups'] ) ) { $vals['groups'] = $user->getEffectiveGroups(); - $result->setIndexedTagName( $vals['groups'], 'g' ); // even if empty + $result->setIndexedTagName( $vals['groups'], 'g' ); // even if empty } if ( isset( $this->prop['implicitgroups'] ) ) { $vals['implicitgroups'] = $user->getAutomaticGroups(); - $result->setIndexedTagName( $vals['implicitgroups'], 'g' ); // even if empty + $result->setIndexedTagName( $vals['implicitgroups'], 'g' ); // even if empty } if ( isset( $this->prop['rights'] ) ) { // User::getRights() may return duplicate values, strip them $vals['rights'] = array_values( array_unique( $user->getRights() ) ); - $result->setIndexedTagName( $vals['rights'], 'r' ); // even if empty + $result->setIndexedTagName( $vals['rights'], 'r' ); // even if empty } if ( isset( $this->prop['changeablegroups'] ) ) { @@ -303,8 +303,4 @@ class ApiQueryUserInfo extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Meta#userinfo_.2F_ui'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php index bf438d1d..72ab7866 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -110,19 +110,39 @@ class ApiQueryUsers extends ApiQueryBase { $this->addFields( User::selectFields() ); $this->addWhereFld( 'user_name', $goodNames ); - if ( isset( $this->prop['groups'] ) || isset( $this->prop['rights'] ) ) { - $this->addTables( 'user_groups' ); - $this->addJoinConds( array( 'user_groups' => array( 'LEFT JOIN', 'ug_user=user_id' ) ) ); - $this->addFields( 'ug_group' ); - } - $this->showHiddenUsersAddBlockInfo( isset( $this->prop['blockinfo'] ) ); $data = array(); $res = $this->select( __METHOD__ ); + $this->resetQueryParams(); + + // get user groups if needed + if ( isset( $this->prop['groups'] ) || isset( $this->prop['rights'] ) ) { + $userGroups = array(); + + $this->addTables( 'user' ); + $this->addWhereFld( 'user_name', $goodNames ); + $this->addTables( 'user_groups' ); + $this->addJoinConds( array( 'user_groups' => array( 'INNER JOIN', 'ug_user=user_id' ) ) ); + $this->addFields( array( 'user_name', 'ug_group' ) ); + $userGroupsRes = $this->select( __METHOD__ ); + + foreach( $userGroupsRes as $row ) { + $userGroups[$row->user_name][] = $row->ug_group; + } + } foreach ( $res as $row ) { - $user = User::newFromRow( $row ); + // create user object and pass along $userGroups if set + // that reduces the number of database queries needed in User dramatically + if ( !isset( $userGroups ) ) { + $user = User::newFromRow( $row ); + } else { + if ( !isset( $userGroups[$row->user_name] ) || !is_array( $userGroups[$row->user_name] ) ) { + $userGroups[$row->user_name] = array(); + } + $user = User::newFromRow( $row, array( 'user_groups' => $userGroups[$row->user_name] ) ); + } $name = $user->getName(); $data[$name]['userid'] = $user->getId(); @@ -137,29 +157,15 @@ class ApiQueryUsers extends ApiQueryBase { } if ( isset( $this->prop['groups'] ) ) { - if ( !isset( $data[$name]['groups'] ) ) { - $data[$name]['groups'] = $user->getAutomaticGroups(); - } - - if ( !is_null( $row->ug_group ) ) { - // This row contains only one group, others will be added from other rows - $data[$name]['groups'][] = $row->ug_group; - } + $data[$name]['groups'] = $user->getEffectiveGroups(); } - if ( isset( $this->prop['implicitgroups'] ) && !isset( $data[$name]['implicitgroups'] ) ) { - $data[$name]['implicitgroups'] = $user->getAutomaticGroups(); + if ( isset( $this->prop['implicitgroups'] ) ) { + $data[$name]['implicitgroups'] = $user->getAutomaticGroups(); } if ( isset( $this->prop['rights'] ) ) { - if ( !isset( $data[$name]['rights'] ) ) { - $data[$name]['rights'] = User::getGroupPermissions( $user->getAutomaticGroups() ); - } - - if ( !is_null( $row->ug_group ) ) { - $data[$name]['rights'] = array_unique( array_merge( $data[$name]['rights'], - User::getGroupPermissions( array( $row->ug_group ) ) ) ); - } + $data[$name]['rights'] = $user->getRights(); } if ( $row->ipb_deleted ) { $data[$name]['hidden'] = ''; @@ -244,16 +250,16 @@ class ApiQueryUsers extends ApiQueryBase { } $done[] = $u; } - return $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'user' ); + $result->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), 'user' ); } /** - * 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 - */ + * 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 ) { wfDeprecated( __METHOD__, '1.20' ); @@ -304,7 +310,7 @@ class ApiQueryUsers extends ApiQueryBase { ' rights - Lists all the rights the user(s) has', ' editcount - Adds the user\'s edit count', ' registration - Adds the user\'s registration timestamp', - ' emailable - Tags if the user can and wants to receive e-mail through [[Special:Emailuser]]', + ' emailable - Tags if the user can and wants to receive email through [[Special:Emailuser]]', ' gender - Tags the gender of the user. Returns "male", "female", or "unknown"', ), 'users' => 'A list of users to obtain the same information for', @@ -390,8 +396,4 @@ class ApiQueryUsers extends ApiQueryBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Users'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php index a1a33728..90b12c14 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -116,7 +116,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { ) ); $userId = $user->getId(); - $this->addJoinConds( array( 'watchlist' => array('INNER JOIN', + $this->addJoinConds( array( 'watchlist' => array( 'INNER JOIN', array( 'wl_user' => $userId, 'wl_namespace=rc_namespace', @@ -240,14 +240,16 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { if ( $this->fld_user || $this->fld_userid ) { - if ( $this->fld_user ) { - $vals['user'] = $row->rc_user_text; - } - if ( $this->fld_userid ) { + $vals['userid'] = $row->rc_user; + // for backwards compatibility $vals['user'] = $row->rc_user; } + if ( $this->fld_user ) { + $vals['user'] = $row->rc_user_text; + } + if ( !$row->rc_user ) { $vals['anon'] = ''; } @@ -511,15 +513,11 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'api.php?action=query&list=watchlist&wlallrev=&wlprop=ids|title|timestamp|user|comment', 'api.php?action=query&generator=watchlist&prop=info', 'api.php?action=query&generator=watchlist&gwlallrev=&prop=revisions&rvprop=timestamp|user', - 'api.php?action=query&list=watchlist&wlowner=Bob_Smith&wltoken=d8d562e9725ea1512894cdab28e5ceebc7f20237' + 'api.php?action=query&list=watchlist&wlowner=Bob_Smith&wltoken=123ABC' ); } public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Watchlist'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php index 6b24aef3..2cb4d9eb 100644 --- a/includes/api/ApiQueryWatchlistRaw.php +++ b/includes/api/ApiQueryWatchlistRaw.php @@ -71,11 +71,9 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { 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" ); - } + $this->dieContinueUsageIf( count( $cont ) != 2 ); $ns = intval( $cont[0] ); + $this->dieContinueUsageIf( strval( $ns ) !== $cont[0] ); $title = $this->getDB()->addQuotes( $cont[1] ); $op = $params['dir'] == 'ascending' ? '>' : '<'; $this->addWhere( @@ -224,8 +222,4 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { 'api.php?action=query&generator=watchlistraw&gwrshow=changed&prop=revisions', ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiResult.php b/includes/api/ApiResult.php index 91e20812..39c114b8 100644 --- a/includes/api/ApiResult.php +++ b/includes/api/ApiResult.php @@ -36,13 +36,26 @@ * There are two special key values that change how XML output is generated: * '_element' This key sets the tag name for the rest of the elements in the current array. * It is only inserted if the formatter returned true for getNeedsRawData() - * '*' This key has special meaning only to the XML formatter, and is outputed as is - * for all others. In XML it becomes the content of the current element. + * '*' This key has special meaning only to the XML formatter, and is outputted as is + * for all others. In XML it becomes the content of the current element. * * @ingroup API */ class ApiResult extends ApiBase { + /** + * override existing value in addValue() and setElement() + * @since 1.21 + */ + const OVERRIDE = 1; + + /** + * For addValue() and setElement(), if the value does not exist, add it as the first element. + * In case the new value has no name (numerical index), all indexes will be renumbered. + * @since 1.21 + */ + const ADD_ON_TOP = 2; + private $mData, $mIsRawMode, $mSize, $mCheckingSize; /** @@ -134,18 +147,27 @@ class ApiResult extends ApiBase { /** * Add an output value to the array by name. * Verifies that value with the same name has not been added before. - * @param $arr array to add $value to - * @param $name string Index of $arr to add $value at + * @param array $arr to add $value to + * @param string $name Index of $arr to add $value at * @param $value mixed - * @param $overwrite bool Whether overwriting an existing element is allowed + * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. This parameter used to be + * boolean, and the value of OVERRIDE=1 was specifically chosen so that it would be backwards + * compatible with the new method signature. + * + * @since 1.21 int $flags replaced boolean $override */ - public static function setElement( &$arr, $name, $value, $overwrite = false ) { + public static function setElement( &$arr, $name, $value, $flags = 0 ) { if ( $arr === null || $name === null || $value === null || !is_array( $arr ) || is_array( $name ) ) { ApiBase::dieDebug( __METHOD__, 'Bad parameter' ); } - if ( !isset ( $arr[$name] ) || $overwrite ) { - $arr[$name] = $value; + $exists = isset( $arr[$name] ); + if ( !$exists || ( $flags & ApiResult::OVERRIDE ) ) { + if ( !$exists && ( $flags & ApiResult::ADD_ON_TOP ) ) { + $arr = array( $name => $value ) + $arr; + } else { + $arr[$name] = $value; + } } elseif ( is_array( $arr[$name] ) && is_array( $value ) ) { $merged = array_intersect_key( $arr[$name], $value ); if ( !count( $merged ) ) { @@ -161,9 +183,9 @@ class ApiResult extends ApiBase { /** * Adds a content element to an array. * Use this function instead of hardcoding the '*' element. - * @param $arr array to add the content element to + * @param array $arr to add the content element to * @param $value Mixed - * @param $subElemName string when present, content element is created + * @param string $subElemName when present, content element is created * as a sub item of $arr. Use this parameter to create elements in * format "<elem>text</elem>" without attributes. */ @@ -186,7 +208,7 @@ class ApiResult extends ApiBase { * give all indexed values the given tag name. This function MUST be * called on every array that has numerical indexes. * @param $arr array - * @param $tag string Tag name + * @param string $tag Tag name */ public function setIndexedTagName( &$arr, $tag ) { // In raw mode, add the '_element', otherwise just ignore @@ -203,7 +225,7 @@ class ApiResult extends ApiBase { /** * Calls setIndexedTagName() on each sub-array of $arr * @param $arr array - * @param $tag string Tag name + * @param string $tag Tag name */ public function setIndexedTagName_recursive( &$arr, $tag ) { if ( !is_array( $arr ) ) { @@ -222,7 +244,7 @@ class ApiResult extends ApiBase { * Calls setIndexedTagName() on an array already in the result. * Don't specify a path to a value that's not in the result, or * you'll get nasty errors. - * @param $path array Path to the array, like addValue()'s $path + * @param array $path Path to the array, like addValue()'s $path * @param $tag string */ public function setIndexedTagName_internal( $path, $tag ) { @@ -249,11 +271,14 @@ class ApiResult extends ApiBase { * @param $path array|string|null * @param $name string * @param $value mixed - * @param $overwrite bool - * + * @param int $flags Zero or more OR-ed flags like OVERRIDE | ADD_ON_TOP. This parameter used to be + * boolean, and the value of OVERRIDE=1 was specifically chosen so that it would be backwards + * compatible with the new method signature. * @return bool True if $value fits in the result, false if not + * + * @since 1.21 int $flags replaced boolean $override */ - public function addValue( $path, $name, $value, $overwrite = false ) { + public function addValue( $path, $name, $value, $flags = 0 ) { global $wgAPIMaxResultSize; $data = &$this->mData; @@ -268,26 +293,34 @@ class ApiResult extends ApiBase { $this->mSize = $newsize; } - if ( !is_null( $path ) ) { - if ( is_array( $path ) ) { - foreach ( $path as $p ) { - if ( !isset( $data[$p] ) ) { + $addOnTop = $flags & ApiResult::ADD_ON_TOP; + if ( $path !== null ) { + foreach ( (array)$path as $p ) { + if ( !isset( $data[$p] ) ) { + if ( $addOnTop ) { + $data = array( $p => array() ) + $data; + $addOnTop = false; + } else { $data[$p] = array(); } - $data = &$data[$p]; - } - } else { - if ( !isset( $data[$path] ) ) { - $data[$path] = array(); } - $data = &$data[$path]; + $data = &$data[$p]; } } if ( !$name ) { - $data[] = $value; // Add list element + // Add list element + if ( $addOnTop ) { + // This element needs to be inserted in the beginning + // Numerical indexes will be renumbered + array_unshift( $data, $value ); + } else { + // Add new value at the end + $data[] = $value; + } } else { - self::setElement( $data, $name, $value, $overwrite ); // Add named element + // Add named element + self::setElement( $data, $name, $value, $flags ); } return true; } @@ -300,19 +333,19 @@ class ApiResult extends ApiBase { */ public function setParsedLimit( $moduleName, $limit ) { // Add value, allowing overwriting - $this->addValue( 'limits', $moduleName, $limit, true ); + $this->addValue( 'limits', $moduleName, $limit, ApiResult::OVERRIDE ); } /** * Unset a value previously added to the result set. * Fails silently if the value isn't found. * For parameters, see addValue() - * @param $path array + * @param $path array|null * @param $name string */ public function unsetValue( $path, $name ) { $data = &$this->mData; - if ( !is_null( $path ) ) { + if ( $path !== null ) { foreach ( (array)$path as $p ) { if ( !isset( $data[$p] ) ) { return; @@ -367,8 +400,4 @@ class ApiResult extends ApiBase { public function execute() { ApiBase::dieDebug( __METHOD__, 'execute() is not supported on Result object' ); } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiRollback.php b/includes/api/ApiRollback.php index 677df16a..b9873f49 100644 --- a/includes/api/ApiRollback.php +++ b/includes/api/ApiRollback.php @@ -29,10 +29,6 @@ */ class ApiRollback extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - /** * @var Title */ @@ -185,7 +181,7 @@ class ApiRollback extends ApiBase { $this->mTitleObj = Title::newFromText( $params['title'] ); - if ( !$this->mTitleObj ) { + if ( !$this->mTitleObj || $this->mTitleObj->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); } if ( !$this->mTitleObj->exists() ) { @@ -205,8 +201,4 @@ class ApiRollback extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Rollback'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiRsd.php b/includes/api/ApiRsd.php index f0e1fad6..c4a1328c 100644 --- a/includes/api/ApiRsd.php +++ b/includes/api/ApiRsd.php @@ -31,10 +31,6 @@ */ class ApiRsd extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { $result = $this->getResult(); @@ -155,10 +151,6 @@ class ApiRsd extends ApiBase { } return $outputData; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } class ApiFormatXmlRsd extends ApiFormatXml { @@ -170,8 +162,4 @@ class ApiFormatXmlRsd extends ApiFormatXml { public function getMimeType() { return 'application/rsd+xml'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php index 098b1a66..58d5d9ab 100644 --- a/includes/api/ApiSetNotificationTimestamp.php +++ b/includes/api/ApiSetNotificationTimestamp.php @@ -31,9 +31,7 @@ */ class ApiSetNotificationTimestamp extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } + private $mPageSet; public function execute() { $user = $this->getUser(); @@ -45,11 +43,12 @@ class ApiSetNotificationTimestamp extends ApiBase { $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 ); + $pageSet = $this->getPageSet(); + if ( $params['entirewatchlist'] && $pageSet->getDataSource() !== null ) { + $this->dieUsage( "Cannot use 'entirewatchlist' at the same time as '{$pageSet->getDataSource()}'", 'multisource' ); + } - $dbw = $this->getDB( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER, 'api' ); $timestamp = null; if ( isset( $params['timestamp'] ) ) { @@ -96,20 +95,20 @@ class ApiSetNotificationTimestamp extends ApiBase { $result['notificationtimestamp'] = ( is_null( $timestamp ) ? '' : wfTimestamp( TS_ISO_8601, $timestamp ) ); } else { // First, log the invalid titles - foreach( $pageSet->getInvalidTitles() as $title ) { + foreach ( $pageSet->getInvalidTitles() as $title ) { $r = array(); $r['title'] = $title; $r['invalid'] = ''; $result[] = $r; } - foreach( $pageSet->getMissingPageIDs() as $p ) { + foreach ( $pageSet->getMissingPageIDs() as $p ) { $page = array(); $page['pageid'] = $p; $page['missing'] = ''; $page['notwatched'] = ''; $result[] = $page; } - foreach( $pageSet->getMissingRevisionIDs() as $r ) { + foreach ( $pageSet->getMissingRevisionIDs() as $r ) { $rev = array(); $rev['revid'] = $r; $rev['missing'] = ''; @@ -135,6 +134,7 @@ class ApiSetNotificationTimestamp extends ApiBase { } // Now, put the valid titles into the result + /** @var $title Title */ foreach ( $pageSet->getTitles() as $title ) { $ns = $title->getNamespace(); $dbkey = $title->getDBkey(); @@ -161,6 +161,17 @@ class ApiSetNotificationTimestamp extends ApiBase { $apiResult->addValue( null, $this->getModuleName(), $result ); } + /** + * Get a cached instance of an ApiPageSet object + * @return ApiPageSet + */ + private function getPageSet() { + if ( !isset( $this->mPageSet ) ) { + $this->mPageSet = new ApiPageSet( $this ); + } + return $this->mPageSet; + } + public function mustBePosted() { return true; } @@ -177,9 +188,8 @@ class ApiSetNotificationTimestamp extends ApiBase { return ''; } - public function getAllowedParams() { - $psModule = new ApiPageSet( $this ); - return $psModule->getAllowedParams() + array( + public function getAllowedParams( $flags = 0 ) { + $result = array( 'entirewatchlist' => array( ApiBase::PARAM_TYPE => 'boolean' ), @@ -194,11 +204,15 @@ class ApiSetNotificationTimestamp extends ApiBase { ApiBase::PARAM_TYPE => 'integer' ), ); + if ( $flags ) { + $result += $this->getPageSet()->getFinalParams( $flags ); + } + return $result; + } public function getParamDescription() { - $psModule = new ApiPageSet( $this ); - return $psModule->getParamDescription() + array( + return $this->getPageSet()->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)', @@ -247,18 +261,20 @@ class ApiSetNotificationTimestamp extends ApiBase { 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', + 'and the sending of email when the "Email me when a page on my watchlist is', 'changed" preference is enabled.' ); } public function getPossibleErrors() { - $psModule = new ApiPageSet( $this ); + $ps = $this->getPageSet(); return array_merge( parent::getPossibleErrors(), - $psModule->getPossibleErrors(), - $this->getRequireMaxOneParameterErrorMessages( array( 'timestamp', 'torevid', 'newerthanrevid' ) ), - $this->getRequireOnlyOneParameterErrorMessages( array_merge( array( 'entirewatchlist' ), array_keys( $psModule->getAllowedParams() ) ) ), + $ps->getPossibleErrors(), + $this->getRequireMaxOneParameterErrorMessages( + array( 'timestamp', 'torevid', 'newerthanrevid' ) ), + $this->getRequireOnlyOneParameterErrorMessages( + array_merge( array( 'entirewatchlist' ), array_keys( $ps->getFinalParams() ) ) ), array( array( 'code' => 'notloggedin', 'info' => 'Anonymous users cannot use watchlist change notifications' ), array( 'code' => 'multpages', 'info' => 'torevid may only be used with a single page' ), @@ -269,17 +285,13 @@ class ApiSetNotificationTimestamp extends ApiBase { 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', + 'api.php?action=setnotificationtimestamp&entirewatchlist=&token=123ABC' => 'Reset the notification status for the entire watchlist', + 'api.php?action=setnotificationtimestamp&titles=Main_page&token=123ABC' => 'Reset the notification status for "Main page"', + 'api.php?action=setnotificationtimestamp&titles=Main_page×tamp=2012-01-01T00:00:00Z&token=123ABC' => 'Set the notification timestamp for "Main page" so all edits since 1 January 2012 are unviewed', ); } 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 index 2c9b482c..7080f547 100644 --- a/includes/api/ApiTokens.php +++ b/includes/api/ApiTokens.php @@ -24,25 +24,17 @@ * @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 ) { @@ -53,7 +45,6 @@ class ApiTokens extends ApiBase { } $this->getResult()->addValue( null, $this->getModuleName(), $res ); - wfProfileOut( __METHOD__ ); } private function getTokenTypes() { @@ -62,11 +53,11 @@ class ApiTokens extends ApiBase { return $types; } wfProfileIn( __METHOD__ ); - $types = array( 'patrol' => 'ApiQueryRecentChanges::getPatrolToken' ); + $types = array( 'patrol' => array( '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'; + $types[$name] = array( 'ApiQueryInfo', 'get' . ucfirst( $name ) . 'Token' ); } wfRunHooks( 'ApiTokensGetTokenTypes', array( &$types ) ); ksort( $types ); @@ -85,54 +76,13 @@ class ApiTokens extends ApiBase { } 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 - ) - ) + $props = array( + '' => array(), ); + + self::addTokenProperties( $props, $this->getTokenTypes() ); + + return $props; } public function getParamDescription() { @@ -151,8 +101,4 @@ class ApiTokens extends ApiBase { '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 ff9ac474..55e7331d 100644 --- a/includes/api/ApiUnblock.php +++ b/includes/api/ApiUnblock.php @@ -32,10 +32,6 @@ */ class ApiUnblock extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - /** * Unblocks the specified user or provides the reason the unblock failed. */ @@ -178,8 +174,4 @@ class ApiUnblock extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Block'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiUndelete.php b/includes/api/ApiUndelete.php index c9962517..4bbe568d 100644 --- a/includes/api/ApiUndelete.php +++ b/includes/api/ApiUndelete.php @@ -29,10 +29,6 @@ */ class ApiUndelete extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { $params = $this->extractRequestParams(); @@ -45,7 +41,7 @@ class ApiUndelete extends ApiBase { } $titleObj = Title::newFromText( $params['title'] ); - if ( !$titleObj ) { + if ( !$titleObj || $titleObj->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); } @@ -61,7 +57,13 @@ class ApiUndelete extends ApiBase { } $pa = new PageArchive( $titleObj ); - $retval = $pa->undelete( ( isset( $params['timestamps'] ) ? $params['timestamps'] : array() ), $params['reason'] ); + $retval = $pa->undelete( + ( isset( $params['timestamps'] ) ? $params['timestamps'] : array() ), + $params['reason'], + array(), + false, + $this->getUser() + ); if ( !is_array( $retval ) ) { $this->dieUsageMsg( 'cannotundelete' ); } @@ -170,8 +172,4 @@ class ApiUndelete extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Undelete'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php index e7a7849b..7d67aa6e 100644 --- a/includes/api/ApiUpload.php +++ b/includes/api/ApiUpload.php @@ -36,11 +36,9 @@ class ApiUpload extends ApiBase { protected $mParams; - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { + global $wgEnableAsyncUploads; + // Check whether upload is enabled if ( !UploadBase::isEnabled() ) { $this->dieUsageMsg( 'uploaddisabled' ); @@ -51,6 +49,8 @@ class ApiUpload extends ApiBase { // Parameter handling $this->mParams = $this->extractRequestParams(); $request = $this->getMain()->getRequest(); + // Check if async mode is actually supported (jobs done in cli mode) + $this->mParams['async'] = ( $this->mParams['async'] && $wgEnableAsyncUploads ); // Add the uploaded file to the params array $this->mParams['file'] = $request->getFileName( 'file' ); $this->mParams['chunk'] = $request->getFileName( 'chunk' ); @@ -62,17 +62,16 @@ class ApiUpload extends ApiBase { // Select an upload module if ( !$this->selectUploadModule() ) { - // This is not a true upload, but a status request or similar - return; - } - if ( !isset( $this->mUpload ) ) { + return; // not a true upload, but a status request or similar + } elseif ( !isset( $this->mUpload ) ) { $this->dieUsage( 'No upload module set', 'nomodule' ); } // First check permission to upload $this->checkPermissions( $user ); - // Fetch the file + // Fetch the file (usually a no-op) + /** @var $status Status */ $status = $this->mUpload->fetchFile(); if ( !$status->isGood() ) { $errors = $status->getErrorsArray(); @@ -82,26 +81,32 @@ class ApiUpload extends ApiBase { // Check if the uploaded file is sane if ( $this->mParams['chunk'] ) { - $maxSize = $this->mUpload->getMaxUploadSize( ); + $maxSize = $this->mUpload->getMaxUploadSize(); if( $this->mParams['filesize'] > $maxSize ) { $this->dieUsage( 'The file you submitted was too large', 'file-too-large' ); } + if ( !$this->mUpload->getTitle() ) { + $this->dieUsage( 'Invalid file title supplied', 'internal-error' ); + } + } elseif ( $this->mParams['async'] && $this->mParams['filekey'] ) { + // defer verification to background process } else { + wfDebug( __METHOD__ . 'about to verify' ); $this->verifyUpload(); } - + // Check if the user has the rights to modify or overwrite the requested title // (This check is irrelevant if stashing is already requested, since the errors // can always be fixed by changing the title) - if ( ! $this->mParams['stash'] ) { + if ( !$this->mParams['stash'] ) { $permErrors = $this->mUpload->verifyTitlePermissions( $user ); if ( $permErrors !== true ) { $this->dieRecoverableError( $permErrors[0], 'filename' ); } } - // Get the result based on the current upload context: - $result = $this->getContextResult(); + // Get the result based on the current upload context: + $result = $this->getContextResult(); if ( $result['result'] === 'Success' ) { $result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() ); } @@ -111,14 +116,15 @@ class ApiUpload extends ApiBase { // Cleanup any temporary mess $this->mUpload->cleanupTempFile(); } + /** - * Get an uplaod result based on upload context + * Get an upload result based on upload context * @return array */ - private function getContextResult(){ + private function getContextResult() { $warnings = $this->getApiWarnings(); if ( $warnings && !$this->mParams['ignorewarnings'] ) { - // Get warnings formated in result array format + // Get warnings formatted in result array format return $this->getWarningsResult( $warnings ); } elseif ( $this->mParams['chunk'] ) { // Add chunk, and get result @@ -131,12 +137,13 @@ class ApiUpload extends ApiBase { // performUpload will return a formatted properly for the API with status return $this->performUpload( $warnings ); } + /** - * Get Stash Result, throws an expetion if the file could not be stashed. - * @param $warnings array Array of Api upload warnings + * Get Stash Result, throws an exception if the file could not be stashed. + * @param array $warnings Array of Api upload warnings * @return array */ - private function getStashResult( $warnings ){ + 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 @@ -152,12 +159,13 @@ class ApiUpload extends ApiBase { } return $result; } + /** * Get Warnings Result - * @param $warnings array Array of Api upload warnings + * @param array $warnings Array of Api upload warnings * @return array */ - private function getWarningsResult( $warnings ){ + private function getWarningsResult( $warnings ) { $result = array(); $result['result'] = 'Warning'; $result['warnings'] = $warnings; @@ -171,12 +179,13 @@ class ApiUpload extends ApiBase { } return $result; } + /** * Get the result of a chunk upload. - * @param $warnings array Array of Api upload warnings + * @param array $warnings Array of Api upload warnings * @return array */ - private function getChunkResult( $warnings ){ + private function getChunkResult( $warnings ) { $result = array(); $result['result'] = 'Continue'; @@ -186,55 +195,78 @@ class ApiUpload extends ApiBase { $request = $this->getMain()->getRequest(); $chunkPath = $request->getFileTempname( 'chunk' ); $chunkSize = $request->getUpload( 'chunk' )->getSize(); - if ($this->mParams['offset'] == 0) { + if ( $this->mParams['offset'] == 0 ) { try { - $result['filekey'] = $this->performStash(); + $filekey = $this->performStash(); } catch ( MWException $e ) { // FIXME: Error handling here is wrong/different from rest of this $this->dieUsage( $e->getMessage(), 'stashfailed' ); } } else { - $status = $this->mUpload->addChunk($chunkPath, $chunkSize, - $this->mParams['offset']); + $filekey = $this->mParams['filekey']; + /** @var $status Status */ + $status = $this->mUpload->addChunk( + $chunkPath, $chunkSize, $this->mParams['offset'] ); if ( !$status->isGood() ) { $this->dieUsage( $status->getWikiText(), 'stashfailed' ); return array(); } + } - // Check we added the last chunk: - if( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) { + // Check we added the last chunk: + if ( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) { + if ( $this->mParams['async'] ) { + $progress = UploadBase::getSessionStatus( $this->mParams['filekey'] ); + if ( $progress && $progress['result'] === 'Poll' ) { + $this->dieUsage( "Chunk assembly already in progress.", 'stashfailed' ); + } + UploadBase::setSessionStatus( + $this->mParams['filekey'], + array( 'result' => 'Poll', + 'stage' => 'queued', 'status' => Status::newGood() ) + ); + $ok = JobQueueGroup::singleton()->push( new AssembleUploadChunksJob( + Title::makeTitle( NS_FILE, $this->mParams['filekey'] ), + array( + 'filename' => $this->mParams['filename'], + 'filekey' => $this->mParams['filekey'], + 'session' => $this->getContext()->exportSession() + ) + ) ); + if ( $ok ) { + $result['result'] = 'Poll'; + } else { + UploadBase::setSessionStatus( $this->mParams['filekey'], false ); + $this->dieUsage( + "Failed to start AssembleUploadChunks.php", 'stashfailed' ); + } + } else { $status = $this->mUpload->concatenateChunks(); - if ( !$status->isGood() ) { $this->dieUsage( $status->getWikiText(), 'stashfailed' ); 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'] ); + // The fully concatenated file has a new filekey. So remove + // the old filekey and fetch the new one. + $this->mUpload->stash->removeFile( $filekey ); + $filekey = $this->mUpload->getLocalFile()->getFileKey(); $result['result'] = 'Success'; - - } else { - - // Continue passing through the filekey for adding further chunks. - $result['filekey'] = $this->mParams['filekey']; } } + $result['filekey'] = $filekey; $result['offset'] = $this->mParams['offset'] + $chunkSize; return $result; } - + /** * Stash the file and return the file key * Also re-raises exceptions with slightly more informative message strings (useful for API) * @throws MWException * @return String file key */ - function performStash() { + private function performStash() { try { $stashFile = $this->mUpload->stashFile(); @@ -244,7 +276,7 @@ class ApiUpload extends ApiBase { $fileKey = $stashFile->getFileKey(); } catch ( MWException $e ) { $message = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage(); - wfDebug( __METHOD__ . ' ' . $message . "\n"); + wfDebug( __METHOD__ . ' ' . $message . "\n" ); throw new MWException( $message ); } return $fileKey; @@ -254,12 +286,12 @@ class ApiUpload extends ApiBase { * Throw an error that the user can recover from by providing a better * value for $parameter * - * @param $error array Error array suitable for passing to dieUsageMsg() - * @param $parameter string Parameter that needs revising - * @param $data array Optional extra data to pass to the user + * @param array $error Error array suitable for passing to dieUsageMsg() + * @param string $parameter Parameter that needs revising + * @param array $data Optional extra data to pass to the user * @throws UsageException */ - function dieRecoverableError( $error, $parameter, $data = array() ) { + private function dieRecoverableError( $error, $parameter, $data = array() ) { try { $data['filekey'] = $this->performStash(); $data['sessionkey'] = $data['filekey']; @@ -283,11 +315,27 @@ class ApiUpload extends ApiBase { $request = $this->getMain()->getRequest(); // chunk or one and only one of the following parameters is needed - if( !$this->mParams['chunk'] ) { + if ( !$this->mParams['chunk'] ) { $this->requireOnlyOneParameter( $this->mParams, 'filekey', 'file', 'url', 'statuskey' ); } + // Status report for "upload to stash"/"upload from stash" + if ( $this->mParams['filekey'] && $this->mParams['checkstatus'] ) { + $progress = UploadBase::getSessionStatus( $this->mParams['filekey'] ); + if ( !$progress ) { + $this->dieUsage( 'No result in status data', 'missingresult' ); + } elseif ( !$progress['status']->isGood() ) { + $this->dieUsage( $progress['status']->getWikiText(), 'stashfailed' ); + } + if ( isset( $progress['status']->value['verification'] ) ) { + $this->checkVerification( $progress['status']->value['verification'] ); + } + unset( $progress['status'] ); // remove Status object + $this->getResult()->addValue( null, $this->getModuleName(), $progress ); + return false; + } + if ( $this->mParams['statuskey'] ) { $this->checkAsyncDownloadEnabled(); @@ -302,7 +350,6 @@ class ApiUpload extends ApiBase { } $this->getResult()->addValue( null, $this->getModuleName(), $sessionData ); return false; - } // The following modules all require the filename parameter to be set @@ -311,9 +358,11 @@ class ApiUpload extends ApiBase { } if ( $this->mParams['chunk'] ) { + $this->checkChunkedEnabled(); + // Chunk upload $this->mUpload = new UploadFromChunks(); - if( isset( $this->mParams['filekey'] ) ){ + if( isset( $this->mParams['filekey'] ) ) { // handle new chunk $this->mUpload->continueChunks( $this->mParams['filename'], @@ -334,8 +383,11 @@ class ApiUpload extends ApiBase { } $this->mUpload = new UploadFromStash( $this->getUser() ); - - $this->mUpload->initialize( $this->mParams['filekey'], $this->mParams['filename'] ); + // This will not download the temp file in initialize() in async mode. + // We still have enough information to call checkWarnings() and such. + $this->mUpload->initialize( + $this->mParams['filekey'], $this->mParams['filename'], !$this->mParams['async'] + ); } elseif ( isset( $this->mParams['file'] ) ) { $this->mUpload = new UploadFromFile(); $this->mUpload->initialize( @@ -396,13 +448,20 @@ class ApiUpload extends ApiBase { /** * Performs file verification, dies on error. */ - protected function verifyUpload( ) { - global $wgFileExtensions; - - $verification = $this->mUpload->verifyUpload( ); + protected function verifyUpload() { + $verification = $this->mUpload->verifyUpload(); if ( $verification['status'] === UploadBase::OK ) { return; + } else { + return $this->checkVerification( $verification ); } + } + + /** + * Performs file verification, dies on error. + */ + protected function checkVerification( array $verification ) { + global $wgFileExtensions; // TODO: Move them to ApiBase's message map switch( $verification['status'] ) { @@ -460,12 +519,11 @@ class ApiUpload extends ApiBase { break; default: $this->dieUsage( 'An unknown error occurred', 'unknown-error', - 0, array( 'code' => $verification['status'] ) ); + 0, array( 'code' => $verification['status'] ) ); break; } } - /** * Check warnings. * Returns a suitable array for inclusion into API results if there were warnings @@ -503,12 +561,11 @@ class ApiUpload extends ApiBase { return $warnings; } - /** * Perform the actual upload. Returns a suitable result array on success; * dies on failure. * - * @param $warnings array Array of Api upload warnings + * @param array $warnings Array of Api upload warnings * @return array */ protected function performUpload( $warnings ) { @@ -517,6 +574,7 @@ class ApiUpload extends ApiBase { $this->mParams['text'] = $this->mParams['comment']; } + /** @var $file File */ $file = $this->mUpload->getLocalFile(); $watch = $this->getWatchlistValue( $this->mParams['watchlist'], $file->getTitle() ); @@ -526,29 +584,57 @@ class ApiUpload extends ApiBase { } // No errors, no warnings: do the upload - $status = $this->mUpload->performUpload( $this->mParams['comment'], - $this->mParams['text'], $watch, $this->getUser() ); - - if ( !$status->isGood() ) { - $error = $status->getErrorsArray(); - - if ( count( $error ) == 1 && $error[0][0] == 'async' ) { - // The upload can not be performed right now, because the user - // requested so - return array( - 'result' => 'Queued', - 'statuskey' => $error[0][1], - ); + if ( $this->mParams['async'] ) { + $progress = UploadBase::getSessionStatus( $this->mParams['filekey'] ); + if ( $progress && $progress['result'] === 'Poll' ) { + $this->dieUsage( "Upload from stash already in progress.", 'publishfailed' ); + } + UploadBase::setSessionStatus( + $this->mParams['filekey'], + array( 'result' => 'Poll', 'stage' => 'queued', 'status' => Status::newGood() ) + ); + $ok = JobQueueGroup::singleton()->push( new PublishStashedFileJob( + Title::makeTitle( NS_FILE, $this->mParams['filename'] ), + array( + 'filename' => $this->mParams['filename'], + 'filekey' => $this->mParams['filekey'], + 'comment' => $this->mParams['comment'], + 'text' => $this->mParams['text'], + 'watch' => $watch, + 'session' => $this->getContext()->exportSession() + ) + ) ); + if ( $ok ) { + $result['result'] = 'Poll'; } else { - $this->getResult()->setIndexedTagName( $error, 'error' ); + UploadBase::setSessionStatus( $this->mParams['filekey'], false ); + $this->dieUsage( + "Failed to start PublishStashedFile.php", 'publishfailed' ); + } + } else { + /** @var $status Status */ + $status = $this->mUpload->performUpload( $this->mParams['comment'], + $this->mParams['text'], $watch, $this->getUser() ); - $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error ); + if ( !$status->isGood() ) { + $error = $status->getErrorsArray(); + + if ( count( $error ) == 1 && $error[0][0] == 'async' ) { + // The upload can not be performed right now, because the user + // requested so + return array( + 'result' => 'Queued', + 'statuskey' => $error[0][1], + ); + } else { + $this->getResult()->setIndexedTagName( $error, 'error' ); + + $this->dieUsage( 'An internal error occurred', 'internal-error', 0, $error ); + } } + $result['result'] = 'Success'; } - $file = $this->mUpload->getLocalFile(); - - $result['result'] = 'Success'; $result['filename'] = $file->getName(); if ( $warnings && count( $warnings ) > 0 ) { $result['warnings'] = $warnings; @@ -563,7 +649,14 @@ class ApiUpload extends ApiBase { protected function checkAsyncDownloadEnabled() { global $wgAllowAsyncCopyUploads; if ( !$wgAllowAsyncCopyUploads ) { - $this->dieUsage( 'Asynchronous copy uploads disabled', 'asynccopyuploaddisabled'); + $this->dieUsage( 'Asynchronous copy uploads disabled', 'asynccopyuploaddisabled' ); + } + } + + protected function checkChunkedEnabled() { + global $wgAllowChunkedUploads; + if ( !$wgAllowChunkedUploads ) { + $this->dieUsage( 'Chunked uploads disabled', 'chunkeduploaddisabled' ); } } @@ -601,7 +694,9 @@ class ApiUpload extends ApiBase { ), ), 'ignorewarnings' => false, - 'file' => null, + 'file' => array( + ApiBase::PARAM_TYPE => 'upload', + ), 'url' => null, 'filekey' => null, 'sessionkey' => array( @@ -612,11 +707,15 @@ class ApiUpload extends ApiBase { 'filesize' => null, 'offset' => null, - 'chunk' => null, + 'chunk' => array( + ApiBase::PARAM_TYPE => 'upload', + ), + 'async' => false, 'asyncdownload' => false, 'leavemessage' => false, 'statuskey' => null, + 'checkstatus' => false, ); return $params; @@ -641,9 +740,11 @@ class ApiUpload extends ApiBase { 'offset' => 'Offset of chunk in bytes', 'filesize' => 'Filesize of entire upload', + 'async' => 'Make potentially large file operations asynchronous when possible', 'asyncdownload' => 'Make fetching a URL asynchronous', 'leavemessage' => 'If asyncdownload is used, leave a message on the user talk page if finished', - 'statuskey' => 'Fetch the upload status for this file key', + 'statuskey' => 'Fetch the upload status for this file key (upload by URL)', + 'checkstatus' => 'Only fetch the upload status for the given file key', ); return $params; @@ -692,7 +793,7 @@ class ApiUpload extends ApiBase { ' * Have the MediaWiki server fetch a file from a URL, using the "url" parameter', ' * Complete an earlier upload that failed due to warnings, using the "filekey" parameter', 'Note that the HTTP POST must be done as a file upload (i.e. using multipart/form-data) when', - 'sending the "file". Also you must get and send an edit token before doing any upload stuff' + 'sending the "file". Also you must get and send an edit token before doing any upload stuff' ); } @@ -712,8 +813,10 @@ class ApiUpload extends ApiBase { array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ), array( 'code' => 'overwrite', 'info' => 'Overwriting an existing file is not allowed' ), array( 'code' => 'stashfailed', 'info' => 'Stashing temporary file failed' ), + array( 'code' => 'publishfailed', 'info' => 'Publishing of stashed file failed' ), array( 'code' => 'internal-error', 'info' => 'An internal error occurred' ), array( 'code' => 'asynccopyuploaddisabled', 'info' => 'Asynchronous copy uploads disabled' ), + array( 'code' => 'chunkeduploaddisabled', 'info' => 'Chunked uploads disabled' ), array( 'fileexists-forbidden' ), array( 'fileexists-shared-forbidden' ), ) @@ -740,8 +843,4 @@ class ApiUpload extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Upload'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php index cbb66a41..b9b1eeda 100644 --- a/includes/api/ApiUserrights.php +++ b/includes/api/ApiUserrights.php @@ -30,10 +30,6 @@ */ class ApiUserrights extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - private $mUser = null; public function execute() { @@ -141,8 +137,4 @@ class ApiUserrights extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:User_group_membership'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php index 0509f1f8..3e51299f 100644 --- a/includes/api/ApiWatch.php +++ b/includes/api/ApiWatch.php @@ -31,10 +31,6 @@ */ class ApiWatch extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { $user = $this->getUser(); if ( !$user->isLoggedIn() ) { @@ -44,12 +40,20 @@ class ApiWatch extends ApiBase { $params = $this->extractRequestParams(); $title = Title::newFromText( $params['title'] ); - if ( !$title || $title->getNamespace() < 0 ) { + if ( !$title || $title->isExternal() || !$title->canExist() ) { $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) ); } $res = array( 'title' => $title->getPrefixedText() ); + // Currently unnecessary, code to act as a safeguard against any change in current behavior of uselang + // Copy from ApiParse + $oldLang = null; + if ( isset( $params['uselang'] ) && $params['uselang'] != $this->getContext()->getLanguage()->getCode() ) { + $oldLang = $this->getContext()->getLanguage(); // Backup language + $this->getContext()->setLanguage( Language::factory( $params['uselang'] ) ); + } + if ( $params['unwatch'] ) { $res['unwatched'] = ''; $res['message'] = $this->msg( 'removedwatchtext', $title->getPrefixedText() )->title( $title )->parseAsBlock(); @@ -59,6 +63,11 @@ class ApiWatch extends ApiBase { $res['message'] = $this->msg( 'addedwatchtext', $title->getPrefixedText() )->title( $title )->parseAsBlock(); $success = WatchAction::doWatch( $title, $user ); } + + if ( !is_null( $oldLang ) ) { + $this->getContext()->setLanguage( $oldLang ); // Reset language to $oldLang + } + if ( !$success ) { $this->dieUsageMsg( 'hookaborted' ); } @@ -88,6 +97,7 @@ class ApiWatch extends ApiBase { ApiBase::PARAM_REQUIRED => true ), 'unwatch' => false, + 'uselang' => null, 'token' => array( ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true @@ -99,6 +109,7 @@ class ApiWatch extends ApiBase { return array( 'title' => 'The page to (un)watch', 'unwatch' => 'If set the page will be unwatched rather than watched', + 'uselang' => 'Language to show the message in', 'token' => 'A token previously acquired via prop=info', ); } @@ -136,8 +147,4 @@ class ApiWatch extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Watch'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } |