diff options
Diffstat (limited to 'includes/api')
96 files changed, 5351 insertions, 2663 deletions
diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 875a3814..ce6ecda6 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"; } @@ -275,15 +304,14 @@ abstract class ApiBase extends ContextSource { } $examples = $this->getExamples(); - if ( $examples !== false && $examples !== '' ) { + if ( $examples ) { if ( !is_array( $examples ) ) { $examples = array( $examples ); } $msg .= "Example" . ( count( $examples ) > 1 ? 's' : '' ) . ":\n"; - foreach( $examples as $k => $v ) { - + foreach ( $examples as $k => $v ) { if ( is_numeric( $k ) ) { $msg .= " $v\n"; } else { @@ -297,25 +325,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 +339,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 +349,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 +370,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 +427,7 @@ abstract class ApiBase extends ContextSource { if ( $t === '' ) { $nothingPrompt = 'Can be empty, or '; } else { - $choices[] = $t; + $choices[] = $t; } } $desc .= $paramPrefix . $nothingPrompt . $prompt; @@ -433,7 +444,7 @@ abstract class ApiBase extends ContextSource { $hintPipeSeparated = false; break; case 'limit': - $desc .= $paramPrefix . "No more than {$paramSettings[self :: PARAM_MAX]}"; + $desc .= $paramPrefix . "No more than {$paramSettings[self::PARAM_MAX]}"; if ( isset( $paramSettings[self::PARAM_MAX2] ) ) { $desc .= " ({$paramSettings[self::PARAM_MAX2]} for bots)"; } @@ -455,6 +466,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 +501,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 +521,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 +548,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 +581,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 +630,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 +665,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 +677,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,9 +688,9 @@ 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', 'invalidparammix' ); } elseif ( count( $intersection ) == 0 ) { - $this->dieUsage( "One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required', "{$p}missingparam" ); + $this->dieUsage( "One of the parameters {$p}" . implode( ", {$p}", $required ) . ' is required', 'missingparam' ); } } @@ -739,7 +724,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', 'invalidparammix' ); } } @@ -760,7 +745,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 +757,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 +794,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,15 +815,15 @@ 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 */ - protected function getWatchlistValue ( $watchlist, $titleObj, $userOption = null ) { + protected function getWatchlistValue( $watchlist, $titleObj, $userOption = null ) { - $userWatching = $this->getUser()->isWatched( $titleObj ); + $userWatching = $this->getUser()->isWatched( $titleObj, WatchedItem::IGNORE_USER_RIGHTS ); switch ( $watchlist ) { case 'watch': @@ -849,13 +837,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 +855,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 ); @@ -877,19 +865,14 @@ abstract class ApiBase extends ContextSource { return; } - $user = $this->getUser(); - if ( $value ) { - WatchAction::doWatch( $titleObj, $user ); - } else { - WatchAction::doUnwatch( $titleObj, $user ); - } + WatchAction::doWatchOrUnwatch( $value, $titleObj, $this->getUser() ); } /** * 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 +912,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,12 +959,11 @@ 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; - $max = isset ( $paramSettings[self::PARAM_MAX] ) ? $paramSettings[self::PARAM_MAX] : null; - $enforceLimits = isset ( $paramSettings[self::PARAM_RANGE_ENFORCE] ) + $min = isset( $paramSettings[self::PARAM_MIN] ) ? $paramSettings[self::PARAM_MIN] : null; + $max = isset( $paramSettings[self::PARAM_MAX] ) ? $paramSettings[self::PARAM_MAX] : null; + $enforceLimits = isset( $paramSettings[self::PARAM_RANGE_ENFORCE] ) ? $paramSettings[self::PARAM_RANGE_ENFORCE] : false; if ( is_array( $value ) ) { @@ -1010,29 +1015,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 +1050,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. @@ -1076,7 +1075,7 @@ abstract class ApiBase extends ContextSource { if ( !$allowMultiple && count( $valuesList ) != 1 ) { // Bug 33482 - Allow entries with | in them for non-multiple values - if ( in_array( $value, $allowedValues ) ) { + if ( in_array( $value, $allowedValues, true ) ) { return $value; } @@ -1106,11 +1105,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 +1143,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 name + * @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 +1186,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 +1203,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 ) { @@ -1203,6 +1217,44 @@ abstract class ApiBase extends ContextSource { } /** + * Throw a UsageException based on the errors in the Status object. + * + * @since 1.22 + * @param Status $status Status object + * @throws UsageException + */ + public function dieStatus( $status ) { + if ( $status->isGood() ) { + throw new MWException( 'Successful status passed to ApiBase::dieStatus' ); + } + + $errors = $status->getErrorsArray(); + if ( !$errors ) { + // No errors? Assume the warnings should be treated as errors + $errors = $status->getWarningsArray(); + } + if ( !$errors ) { + // Still no errors? Punt + $errors = array( array( 'unknownerror-nocode' ) ); + } + + // Cannot use dieUsageMsg() because extensions might return custom + // error messages. + if ( $errors[0] instanceof Message ) { + $msg = $errors[0]; + $code = $msg->getKey(); + } else { + $code = array_shift( $errors[0] ); + $msg = wfMessage( $code, $errors[0] ); + } + if ( isset( ApiBase::$messageMap[$code] ) ) { + // Translate message to code, for backwards compatability + $code = ApiBase::$messageMap[$code]['code']; + } + $this->dieUsage( $msg->inLanguage( 'en' )->useDatabase( false )->plain(), $code ); + } + + /** * Array that maps message keys to error messages. $1 and friends are replaced. */ public static $messageMap = array( @@ -1226,7 +1278,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 +1306,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 +1343,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,9 +1401,10 @@ 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.' ), + 'copyuploadbadurl' => array( 'code' => 'copyuploadbadurl', 'info' => 'Upload not allowed from this URL.' ), 'filename-tooshort' => array( 'code' => 'filename-tooshort', 'info' => 'The filename is too short' ), 'filename-toolong' => array( 'code' => 'filename-toolong', 'info' => 'The filename is too long' ), @@ -1377,7 +1430,7 @@ abstract class ApiBase extends ContextSource { public function dieUsageMsg( $error ) { # most of the time we send a 1 element, so we might as well send it as # a string and make this an array here. - if( is_string( $error ) ) { + if ( is_string( $error ) ) { $error = array( $error ); } $parsed = $this->parseMsg( $error ); @@ -1385,8 +1438,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 +1481,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,11 +1499,11 @@ 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" ); + throw new MWException( "Internal error in $method: $message" ); } /** @@ -1482,7 +1568,7 @@ abstract class ApiBase extends ContextSource { public function getWatchlistUser( $params ) { if ( !is_null( $params['owner'] ) && !is_null( $params['token'] ) ) { $user = User::newFromName( $params['owner'], false ); - if ( !($user && $user->getId()) ) { + if ( !( $user && $user->getId() ) ) { $this->dieUsage( 'Specified user does not exist', 'bad_wlowner' ); } $token = $user->getOption( 'watchlisttoken' ); @@ -1493,6 +1579,9 @@ abstract class ApiBase extends ContextSource { if ( !$this->getUser()->isLoggedIn() ) { $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' ); } + if ( !$this->getUser()->isAllowed( 'viewmywatchlist' ) ) { + $this->dieUsage( 'You don\'t have permission to view your watchlist', 'permissiondenied' ); + } $user = $this->getUser(); } return $user; @@ -1507,6 +1596,10 @@ abstract class ApiBase extends ContextSource { /** * Returns a list of all possible errors returned by the module + * + * Don't call this function directly: use getFinalPossibleErrors() to allow + * hooks to modify parameters as needed. + * * @return array in the format of array( key, param1, param2, ... ) or array( 'code' => ..., 'info' => ... ) */ public function getPossibleErrors() { @@ -1515,10 +1608,16 @@ 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( + 'code' => 'badcontinue', + 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' + ); + } } if ( $this->mustBePosted() ) { @@ -1535,7 +1634,12 @@ abstract class ApiBase extends ContextSource { } if ( $this->needsToken() ) { - $ret[] = array( 'missingparam', 'token' ); + if ( !isset( $params['token'][ApiBase::PARAM_REQUIRED] ) + || !$params['token'][ApiBase::PARAM_REQUIRED] + ) { + // Add token as possible missing parameter, if not already done + $ret[] = array( 'missingparam', 'token' ); + } $ret[] = array( 'sessionfailure' ); } @@ -1543,8 +1647,21 @@ abstract class ApiBase extends ContextSource { } /** + * Get final list of possible errors, after hooks have had a chance to + * tweak it as needed. + * + * @return array + * @since 1.22 + */ + public function getFinalPossibleErrors() { + $possibleErrors = $this->getPossibleErrors(); + wfRunHooks( 'APIGetPossibleErrors', array( $this, &$possibleErrors ) ); + return $possibleErrors; + } + + /** * 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 +1783,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 +1809,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..975153ac 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 @@ -46,15 +42,10 @@ class ApiBlock extends ApiBase { $user = $this->getUser(); $params = $this->extractRequestParams(); - if ( $params['gettoken'] ) { - $res['blocktoken'] = $user->getEditToken(); - $this->getResult()->addValue( null, $this->getModuleName(), $res ); - return; - } - 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 +53,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 +68,7 @@ class ApiBlock extends ApiBase { } $data = array( + 'PreviousTarget' => $params['user'], 'Target' => $params['user'], 'Reason' => array( $params['reason'], @@ -83,7 +82,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 +98,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 ); @@ -151,10 +150,6 @@ class ApiBlock extends ApiBase { ApiBase::PARAM_REQUIRED => true ), 'token' => null, - 'gettoken' => array( - ApiBase::PARAM_DFLT => false, - ApiBase::PARAM_DEPRECATED => true, - ), 'expiry' => 'never', 'reason' => '', 'anononly' => false, @@ -172,13 +167,12 @@ class ApiBlock extends ApiBase { return array( 'user' => 'Username, IP address or IP range you want to block', 'token' => 'A block token previously obtained through prop=info', - 'gettoken' => 'If set, a block token will be returned, and no other action will be taken', 'expiry' => 'Relative expiry time, e.g. \'5 months\' or \'2 weeks\'. If set to \'infinite\', \'indefinite\' or \'never\', the block will never expire.', 'reason' => 'Reason for block', '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', @@ -189,10 +183,6 @@ class ApiBlock extends ApiBase { public function getResultProperties() { return array( '' => array( - 'blocktoken' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), 'user' => array( ApiBase::PROP_TYPE => 'string', ApiBase::PROP_NULLABLE => true @@ -256,8 +246,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..1e35c349 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,17 +81,17 @@ class ApiComparePages extends ApiBase { * @return int */ private function revisionOrTitleOrId( $revision, $titleText, $titleId ) { - if( $revision ){ + if ( $revision ) { return $revision; - } elseif( $titleText ) { + } elseif ( $titleText ) { $title = Title::newFromText( $titleText ); - if( !$title ){ + if ( !$title || $title->isExternal() ) { $this->dieUsageMsg( array( 'invalidtitle', $titleText ) ); } return $title->getLatestRevID(); } elseif ( $titleId ) { $title = Title::newFromID( $titleId ); - if( !$title ) { + if ( !$title ) { $this->dieUsageMsg( array( 'nosuchpageid', $titleId ) ); } 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..0e752c56 --- /dev/null +++ b/includes/api/ApiCreateAccount.php @@ -0,0 +1,296 @@ +<?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() { + // If we're in JSON callback mode, no tokens can be obtained + if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) ) { + $this->dieUsage( 'Cannot create account when using a callback', 'aborted' ); + } + + // $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(); + + // Init session if necessary + if ( session_id() == '' ) { + wfSetupSession(); + } + + if ( $params['mailpassword'] && !$params['email'] ) { + $this->dieUsageMsg( 'noemail' ); + } + + if ( $params['language'] && !Language::isSupportedLanguage( $params['language'] ) ) { + $this->dieUsage( 'Invalid language parameter', 'langinvalid' ); + } + + $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! + global $wgEmailAuthentication; + $user = $status->getValue(); + + if ( $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. + $this->dieStatus( $status ); + } 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 )->inLanguage( 'en' )->useDatabase( false )->parse() ); + } + + $errors[] = array( + 'code' => 'permdenied-createaccount', + 'info' => 'You do not have the right to create a new account' + ); + $errors[] = array( + 'code' => 'blocked', + 'info' => 'You cannot create a new account because you are blocked' + ); + $errors[] = array( + 'code' => 'aborted', + 'info' => 'Account creation aborted by hook (info may vary)' + ); + $errors[] = array( + 'code' => 'langinvalid', + 'info' => 'Invalid language parameter' + ); + + // 'passwordtooshort' has parameters. :( + global $wgMinimalPasswordLength; + $errors[] = array( + 'code' => 'passwordtooshort', + 'info' => wfMessage( 'passwordtooshort', $wgMinimalPasswordLength )->inLanguage( 'en' )->useDatabase( false )->parse() + ); + 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..aea10482 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,9 +57,11 @@ 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 + $this->dieStatus( $status ); } // Deprecated parameters @@ -98,11 +96,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 +126,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 +159,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 +262,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..bd61895b 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,66 @@ 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'] ) ) { - // Process the content for section edits - global $wgParser; - $section = intval( $params['section'] ); - $content = $wgParser->getSection( $content, $section, false ); - if ( $content === false ) { - $this->dieUsage( "There is no section {$section}.", 'nosuchsection' ); + if ( !$contentHandler->supportsSections() ) { + $modelName = $contentHandler->getModelID(); + $this->dieUsage( "Sections are not supported for this content model: $modelName.", 'sectionsnotsupported' ); + } + + if ( $params['section'] == 'new' ) { + // DWIM if they're trying to prepend/append to a new section. + $content = null; + } else { + // Process the content for section edits + $section = intval( $params['section'] ); + $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 +203,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 +234,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,21 +248,26 @@ 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' ) ) ) { + if ( $params['minor'] || ( !$params['notminor'] && $user->getOption( 'minordefault' ) ) ) { $requestArray['wpMinoredit'] = ''; } @@ -218,6 +280,10 @@ class ApiEditPage extends ApiBase { if ( $section == 0 && $params['section'] != '0' && $params['section'] != 'new' ) { $this->dieUsage( "The section parameter must be set to an integer or 'new'", "invalidsection" ); } + $content = $pageObj->getContent(); + if ( $section !== 0 && ( !$content || !$content->getSection( $section ) ) ) { + $this->dieUsage( "There is no section {$section}.", 'nosuchsection' ); + } $requestArray['wpSection'] = $params['section']; } else { $requestArray['wpSection'] = ''; @@ -236,6 +302,10 @@ class ApiEditPage extends ApiBase { $requestArray['wpWatchthis'] = ''; } + // Pass through anything else we might have been given, to support extensions + // This is kind of a hack but it's the best we can do to make extensions work + $requestArray += $this->getRequest()->getValues(); + global $wgTitle, $wgRequest; $req = new DerivativeRequest( $this->getRequest(), $requestArray, true ); @@ -244,14 +314,52 @@ 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 ); + $content = $ep->textbox1; + + // The following is needed to give the hook the full content of the + // new revision rather than just the current section. (Bug 52077) + if ( !is_null( $params['section'] ) && $contentHandler->supportsSections() && $titleObj->exists() ) { + + $sectionTitle = ''; + // If sectiontitle is set, use it, otherwise use the summary as the section title (for + // backwards compatibility with old forms/bots). + if ( $ep->sectiontitle !== '' ) { + $sectionTitle = $ep->sectiontitle; + } else { + $sectionTitle = $ep->summary; + } + + $contentObj = $contentHandler->unserializeContent( $content, $contentFormat ); + + $fullContentObj = $articleObject->replaceSectionContent( $params['section'], $contentObj, $sectionTitle ); + if ( $fullContentObj ) { + $content = $fullContentObj->serialize( $contentFormat ); + } else { + // This most likely means we have an edit conflict which means that the edit + // wont succeed anyway. + $this->dieUsageMsg( 'editconflict' ); + } + } // Run hooks // Handle APIEditBeforeSave parameters $r = array(); - if ( !wfRunHooks( 'APIEditBeforeSave', array( $ep, $ep->textbox1, &$r ) ) ) { + if ( !wfRunHooks( 'APIEditBeforeSave', array( $ep, $content, &$r ) ) ) { if ( count( $r ) ) { $r['result'] = 'Failure'; $apiResult->addValue( null, $this->getModuleName(), $r ); @@ -262,7 +370,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 @@ -273,11 +381,14 @@ class ApiEditPage extends ApiBase { $wgRequest = $oldRequest; global $wgMaxArticleSize; - switch( $status->value ) { + switch ( $status->value ) { case EditPage::AS_HOOK_ERROR: 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 +435,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 +493,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 +511,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 +535,6 @@ class ApiEditPage extends ApiBase { 'section' => null, 'sectiontitle' => array( ApiBase::PARAM_TYPE => 'string', - ApiBase::PARAM_REQUIRED => false, ), 'text' => null, 'token' => array( @@ -460,6 +580,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 +616,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 +624,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 +674,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 +686,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..d5c789c3 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'] ) ); } @@ -69,14 +65,14 @@ class ApiExpandTemplates extends ApiBase { $xml = $dom->__toString(); } $xml_result = array(); - $result->setContent( $xml_result, $xml ); + ApiResult::setContent( $xml_result, $xml ); $result->addValue( null, 'parsetree', $xml_result ); } $retval = $wgParser->preprocess( $params['text'], $title_obj, $options ); // Return result $retval_array = array(); - $result->setContent( $retval_array, $retval ); + ApiResult::setContent( $retval_array, $retval ); $result->addValue( null, $this->getModuleName(), $retval_array ); } @@ -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..05691093 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. * @@ -47,11 +43,11 @@ class ApiFeedContributions extends ApiBase { global $wgFeed, $wgFeedClasses, $wgSitename, $wgLanguageCode; - if( !$wgFeed ) { + if ( !$wgFeed ) { $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' ); } @@ -86,7 +82,7 @@ class ApiFeedContributions extends ApiBase { ) ); $feedItems = array(); - if( $pager->getNumRows() > 0 ) { + if ( $pager->getNumRows() > 0 ) { foreach ( $pager->mResult as $row ) { $feedItems[] = $this->feedItem( $row ); } @@ -97,7 +93,7 @@ class ApiFeedContributions extends ApiBase { protected function feedItem( $row ) { $title = Title::makeTitle( intval( $row->page_namespace ), $row->page_title ); - if( $title ) { + if ( $title && $title->userCan( 'read', $this->getUser() ) ) { $date = $row->rev_timestamp; $comments = $title->getTalkPage()->getFullURL(); $revision = Revision::newFromRow( $row ); @@ -110,9 +106,8 @@ class ApiFeedContributions extends ApiBase { $this->feedItemAuthor( $revision ), $comments ); - } else { - return null; } + return null; } /** @@ -128,12 +123,24 @@ class ApiFeedContributions extends ApiBase { * @return string */ protected function feedItemDesc( $revision ) { - if( $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 ''; } @@ -141,7 +148,7 @@ class ApiFeedContributions extends ApiBase { public function getAllowedParams() { global $wgFeedClasses; $feedFormatNames = array_keys( $wgFeedClasses ); - return array ( + return array( 'feedformat' => array( ApiBase::PARAM_DFLT => 'rss', ApiBase::PARAM_TYPE => $feedFormatNames @@ -201,8 +208,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..fbb70fbc 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -33,9 +33,9 @@ */ class ApiFeedWatchlist extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } + private $watchlistModule = null; + private $linkToDiffs = false; + private $linkToSections = false; /** * This module uses a custom feed wrapper printer. @@ -46,8 +46,6 @@ class ApiFeedWatchlist extends ApiBase { return new ApiFormatFeedWrapper( $this->getMain() ); } - private $linkToDiffs = false; - /** * Make a nested call to the API to request watchlist items in the last $hours. * Wrap the result as an RSS/Atom feed. @@ -58,16 +56,13 @@ class ApiFeedWatchlist extends ApiBase { try { $params = $this->extractRequestParams(); - if( !$wgFeed ) { + if ( !$wgFeed ) { $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 ) ); @@ -81,15 +76,24 @@ class ApiFeedWatchlist extends ApiBase { 'wlprop' => 'title|user|comment|timestamp', 'wldir' => 'older', // reverse order - from newest to oldest 'wlend' => $endTime, // stop at this time - 'wllimit' => ( 50 > $wgFeedLimit ) ? $wgFeedLimit : 50 + 'wllimit' => min( 50, $wgFeedLimit ) ); - 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']; + } + if ( $params['wlshow'] !== null ) { + $fauxReqArr['wlshow'] = $params['wlshow']; + } + if ( $params['wltype'] !== null ) { + $fauxReqArr['wltype'] = $params['wltype']; + } // Support linking to diffs instead of article if ( $params['linktodiffs'] ) { @@ -97,6 +101,12 @@ class ApiFeedWatchlist extends ApiBase { $fauxReqArr['wlprop'] .= '|ids'; } + // Support linking directly to sections when possible + // (possible only if section name is present in comment) + if ( $params['linktosections'] ) { + $this->linkToSections = true; + } + // Check for 'allrev' parameter, and if found, show all revisions to each page on wl. if ( $params['allrev'] ) { $fauxReqArr['wlallrev'] = ''; @@ -164,6 +174,18 @@ class ApiFeedWatchlist extends ApiBase { $titleUrl = $title->getFullURL(); } $comment = isset( $info['comment'] ) ? $info['comment'] : null; + + // Create an anchor to section. + // The anchor won't work for sections that have dupes on page + // as there's no way to strip that info from ApiWatchlist (apparently?). + // RegExp in the line below is equal to Linker::formatAutocomments(). + if ( $this->linkToSections && $comment !== null && preg_match( '!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment, $matches ) ) { + global $wgParser; + $sectionTitle = $wgParser->stripSectionName( $matches[2] ); + $sectionTitle = Sanitizer::normalizeSectionNameWhitespace( $sectionTitle ); + $titleUrl .= Title::newFromText( '#' . $sectionTitle )->getFragmentForURL(); + } + $timestamp = $info['timestamp']; $user = $info['user']; @@ -172,10 +194,18 @@ class ApiFeedWatchlist extends ApiBase { return new FeedItem( $titleStr, $completeText, $titleUrl, $timestamp, $user ); } - public function getAllowedParams() { + private function getWatchlistModule() { + if ( $this->watchlistModule === null ) { + $this->watchlistModule = $this->getMain()->getModuleManager()->getModule( 'query' ) + ->getModuleManager()->getModule( 'watchlist' ); + } + return $this->watchlistModule; + } + + public function getAllowedParams( $flags = 0 ) { global $wgFeedClasses; $feedFormatNames = array_keys( $wgFeedClasses ); - return array ( + $ret = array( 'feedformat' => array( ApiBase::PARAM_DFLT => 'rss', ApiBase::PARAM_TYPE => $feedFormatNames @@ -186,29 +216,41 @@ class ApiFeedWatchlist extends ApiBase { ApiBase::PARAM_MIN => 1, ApiBase::PARAM_MAX => 72, ), - 'allrev' => false, - 'wlowner' => array( - ApiBase::PARAM_TYPE => 'user' - ), - 'wltoken' => array( - ApiBase::PARAM_TYPE => 'string' - ), - 'wlexcludeuser' => array( - ApiBase::PARAM_TYPE => 'user' - ), 'linktodiffs' => false, + 'linktosections' => false, ); + if ( $flags ) { + $wlparams = $this->getWatchlistModule()->getAllowedParams( $flags ); + $ret['allrev'] = $wlparams['allrev']; + $ret['wlowner'] = $wlparams['owner']; + $ret['wltoken'] = $wlparams['token']; + $ret['wlshow'] = $wlparams['show']; + $ret['wltype'] = $wlparams['type']; + $ret['wlexcludeuser'] = $wlparams['excludeuser']; + } else { + $ret['allrev'] = null; + $ret['wlowner'] = null; + $ret['wltoken'] = null; + $ret['wlshow'] = null; + $ret['wltype'] = null; + $ret['wlexcludeuser'] = null; + } + return $ret; } public function getParamDescription() { + $wldescr = $this->getWatchlistModule()->getParamDescription(); return array( 'feedformat' => 'The format of the feed', - 'hours' => 'List pages modified within this many hours from now', - 'allrev' => 'Include multiple revisions of the same page within given timeframe', - 'wlowner' => "The user whose watchlist you want (must be accompanied by {$this->getModulePrefix()}wltoken if it's not you)", - 'wltoken' => 'Security token that requested user set in their preferences', - 'wlexcludeuser' => 'A user whose edits should not be shown in the watchlist', + 'hours' => 'List pages modified within this many hours from now', 'linktodiffs' => 'Link to change differences instead of article pages', + 'linktosections' => 'Link directly to changed sections if possible', + 'allrev' => $wldescr['allrev'], + 'wlowner' => $wldescr['owner'], + 'wltoken' => $wldescr['token'], + 'wlshow' => $wldescr['show'], + 'wltype' => $wldescr['type'], + 'wlexcludeuser' => $wldescr['excludeuser'], ); } @@ -233,8 +275,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..b89fb3a7 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,9 +83,9 @@ 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 ) { + 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,26 +166,29 @@ abstract class ApiFormatBase extends ApiBase { <?php - if ( !$isError ) { + if ( !$isHelpScreen ) { ?> <br /> <small> -You are looking at the HTML representation of the <?php echo( $this->mFormat ); ?> format.<br /> +You are looking at the HTML representation of the <?php echo $this->mFormat; ?> format.<br /> HTML is good for debugging, but is unsuitable for application use.<br /> Specify the format parameter to change the output format.<br /> -To see the non HTML representation of the <?php echo( $this->mFormat ); ?> format, set format=<?php echo( strtolower( $this->mFormat ) ); ?>.<br /> +To see the non HTML representation of the <?php echo $this->mFormat; ?> format, set format=<?php echo strtolower( $this->mFormat ); ?>.<br /> See the <a href='https://www.mediawiki.org/wiki/API'>complete documentation</a>, or -<a href='<?php echo( $script ); ?>'>API help</a> for more information. +<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..342a580f 100644 --- a/includes/api/ApiFormatJson.php +++ b/includes/api/ApiFormatJson.php @@ -56,42 +56,40 @@ class ApiFormatJson extends ApiFormatBase { } public function execute() { - $prefix = $suffix = ''; - $params = $this->extractRequestParams(); + $json = FormatJson::encode( + $this->getResultData(), + $this->getIsHtml(), + $params['utf8'] ? FormatJson::ALL_OK : FormatJson::XMLMETA_OK + ); $callback = $params['callback']; - if ( !is_null( $callback ) ) { - $prefix = preg_replace( "/[^][.\\'\\\"_A-Za-z0-9]/", '', $callback ) . '('; - $suffix = ')'; + if ( $callback !== null ) { + $callback = preg_replace( "/[^][.\\'\\\"_A-Za-z0-9]/", '', $callback ); + $this->printText( "$callback($json)" ); + } else { + $this->printText( $json ); } - $this->printText( - $prefix . - FormatJson::encode( $this->getResultData(), $this->getIsHtml() ) . - $suffix - ); } public function getAllowedParams() { return array( - 'callback' => null, + 'callback' => null, + 'utf8' => false, ); } public function getParamDescription() { return array( 'callback' => 'If specified, wraps the output into a given function call. For safety, all user-specific data will be restricted.', + 'utf8' => 'If specified, encodes most (but not all) non-ASCII characters as UTF-8 instead of replacing them with hexadecimal escape sequences.', ); } 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..5685d937 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'; } @@ -50,7 +46,7 @@ class ApiFormatWddx extends ApiFormatBase { } else { // Don't do newlines and indentation if we weren't asked // for pretty output - $nl = ( $this->getIsHtml() ? '' : "\n" ); + $nl = ( $this->getIsHtml() ? "\n" : '' ); $indstr = ' '; $this->printText( "<?xml version=\"1.0\"?>$nl" ); $this->printText( "<wddxPacket version=\"1.0\">$nl" ); @@ -68,52 +64,47 @@ class ApiFormatWddx extends ApiFormatBase { * @param $indent int */ function slowWddxPrinter( $elemValue, $indent = 0 ) { - $indstr = ( $this->getIsHtml() ? '' : str_repeat( ' ', $indent ) ); - $indstr2 = ( $this->getIsHtml() ? '' : str_repeat( ' ', $indent + 2 ) ); - $nl = ( $this->getIsHtml() ? '' : "\n" ); - switch ( gettype( $elemValue ) ) { - case 'array': - // Check whether we've got an associative array (<struct>) - // or a regular array (<array>) - $cnt = count( $elemValue ); - if ( $cnt == 0 || array_keys( $elemValue ) === range( 0, $cnt - 1 ) ) { - // Regular array - $this->printText( $indstr . Xml::element( 'array', array( - 'length' => $cnt ), null ) . $nl ); - foreach ( $elemValue as $subElemValue ) { - $this->slowWddxPrinter( $subElemValue, $indent + 2 ); - } - $this->printText( "$indstr</array>$nl" ); - } else { - // Associative array (<struct>) - $this->printText( "$indstr<struct>$nl" ); - foreach ( $elemValue as $subElemName => $subElemValue ) { - $this->printText( $indstr2 . Xml::element( 'var', array( - 'name' => $subElemName - ), null ) . $nl ); - $this->slowWddxPrinter( $subElemValue, $indent + 4 ); - $this->printText( "$indstr2</var>$nl" ); - } - $this->printText( "$indstr</struct>$nl" ); + $indstr = ( $this->getIsHtml() ? str_repeat( ' ', $indent ) : '' ); + $indstr2 = ( $this->getIsHtml() ? str_repeat( ' ', $indent + 2 ) : '' ); + $nl = ( $this->getIsHtml() ? "\n" : '' ); + if ( is_array( $elemValue ) ) { + // Check whether we've got an associative array (<struct>) + // or a regular array (<array>) + $cnt = count( $elemValue ); + if ( $cnt == 0 || array_keys( $elemValue ) === range( 0, $cnt - 1 ) ) { + // Regular array + $this->printText( $indstr . Xml::element( 'array', array( + 'length' => $cnt ), null ) . $nl ); + foreach ( $elemValue as $subElemValue ) { + $this->slowWddxPrinter( $subElemValue, $indent + 2 ); + } + $this->printText( "$indstr</array>$nl" ); + } else { + // Associative array (<struct>) + $this->printText( "$indstr<struct>$nl" ); + foreach ( $elemValue as $subElemName => $subElemValue ) { + $this->printText( $indstr2 . Xml::element( 'var', array( + 'name' => $subElemName + ), null ) . $nl ); + $this->slowWddxPrinter( $subElemValue, $indent + 4 ); + $this->printText( "$indstr2</var>$nl" ); } - break; - case 'integer': - case 'double': - $this->printText( $indstr . Xml::element( 'number', null, $elemValue ) . $nl ); - break; - case 'string': - $this->printText( $indstr . Xml::element( 'string', null, $elemValue ) . $nl ); - break; - default: - ApiBase::dieDebug( __METHOD__, 'Unknown type ' . gettype( $elemValue ) ); + $this->printText( "$indstr</struct>$nl" ); + } + } elseif ( is_int( $elemValue ) || is_float( $elemValue ) ) { + $this->printText( $indstr . Xml::element( 'number', null, $elemValue ) . $nl ); + } elseif ( is_string( $elemValue ) ) { + $this->printText( $indstr . Xml::element( 'string', null, $elemValue ) . $nl ); + } elseif ( is_bool( $elemValue ) ) { + $this->printText( $indstr . Xml::element( 'boolean', + array( 'value' => $elemValue ? 'true' : 'false' ) ) . $nl + ); + } else { + ApiBase::dieDebug( __METHOD__, 'Unknown type ' . gettype( $elemValue ) ); } } 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..4ec149c0 100644 --- a/includes/api/ApiFormatXml.php +++ b/includes/api/ApiFormatXml.php @@ -32,14 +32,9 @@ class ApiFormatXml extends ApiFormatBase { private $mRootElemName = 'api'; public static $namespace = 'http://www.mediawiki.org/xml/api/'; - private $mDoubleQuote = false; private $mIncludeNamespace = false; private $mXslt = null; - public function __construct( $main, $format ) { - parent::__construct( $main, $format ); - } - public function getMimeType() { return 'text/xml'; } @@ -54,7 +49,6 @@ class ApiFormatXml extends ApiFormatBase { public function execute() { $params = $this->extractRequestParams(); - $this->mDoubleQuote = $params['xmldoublequote']; $this->mIncludeNamespace = $params['includexmlnamespace']; $this->mXslt = $params['xslt']; @@ -75,8 +69,7 @@ class ApiFormatXml extends ApiFormatBase { $this->printText( self::recXmlPrint( $this->mRootElemName, $data, - $this->getIsHtml() ? - 2 : null, - $this->mDoubleQuote + $this->getIsHtml() ? - 2 : null ) ); } @@ -92,7 +85,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 +98,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 @@ -121,11 +114,10 @@ class ApiFormatXml extends ApiFormatBase { * @param $elemName * @param $elemValue * @param $indent - * @param $doublequote bool * * @return string */ - public static function recXmlPrint( $elemName, $elemValue, $indent, $doublequote = false ) { + public static function recXmlPrint( $elemName, $elemValue, $indent ) { $retval = ''; if ( !is_null( $indent ) ) { $indent += 2; @@ -135,78 +127,71 @@ class ApiFormatXml extends ApiFormatBase { } $elemName = str_replace( ' ', '_', $elemName ); - switch ( gettype( $elemValue ) ) { - case 'array': - if ( isset( $elemValue['*'] ) ) { - $subElemContent = $elemValue['*']; - if ( $doublequote ) { - $subElemContent = Sanitizer::encodeAttribute( $subElemContent ); - } - unset( $elemValue['*'] ); - - // Add xml:space="preserve" to the - // element so XML parsers will leave - // whitespace in the content alone - $elemValue['xml:space'] = 'preserve'; - } else { - $subElemContent = null; + if ( is_array( $elemValue ) ) { + if ( isset( $elemValue['*'] ) ) { + $subElemContent = $elemValue['*']; + unset( $elemValue['*'] ); + + // Add xml:space="preserve" to the + // element so XML parsers will leave + // whitespace in the content alone + $elemValue['xml:space'] = 'preserve'; + } else { + $subElemContent = null; + } + + if ( isset( $elemValue['_element'] ) ) { + $subElemIndName = $elemValue['_element']; + unset( $elemValue['_element'] ); + } else { + $subElemIndName = null; + } + + $indElements = array(); + $subElements = array(); + foreach ( $elemValue as $subElemId => & $subElemValue ) { + if ( is_int( $subElemId ) ) { + $indElements[] = $subElemValue; + unset( $elemValue[$subElemId] ); + } elseif ( is_array( $subElemValue ) ) { + $subElements[$subElemId] = $subElemValue; + unset( $elemValue[$subElemId] ); } + } - if ( isset( $elemValue['_element'] ) ) { - $subElemIndName = $elemValue['_element']; - unset( $elemValue['_element'] ); - } else { - $subElemIndName = null; - } + if ( is_null( $subElemIndName ) && count( $indElements ) ) { + ApiBase::dieDebug( __METHOD__, "($elemName, ...) has integer keys without _element value. Use ApiResult::setIndexedTagName()." ); + } - $indElements = array(); - $subElements = array(); - foreach ( $elemValue as $subElemId => & $subElemValue ) { - if ( is_string( $subElemValue ) && $doublequote ) { - $subElemValue = Sanitizer::encodeAttribute( $subElemValue ); - } - - if ( gettype( $subElemId ) === 'integer' ) { - $indElements[] = $subElemValue; - unset( $elemValue[$subElemId] ); - } elseif ( is_array( $subElemValue ) ) { - $subElements[$subElemId] = $subElemValue; - unset ( $elemValue[$subElemId] ); - } - } + if ( count( $subElements ) && count( $indElements ) && !is_null( $subElemContent ) ) { + ApiBase::dieDebug( __METHOD__, "($elemName, ...) has content and subelements" ); + } - if ( is_null( $subElemIndName ) && count( $indElements ) ) { - ApiBase::dieDebug( __METHOD__, "($elemName, ...) has integer keys without _element value. Use ApiResult::setIndexedTagName()." ); - } + if ( !is_null( $subElemContent ) ) { + $retval .= $indstr . Xml::element( $elemName, $elemValue, $subElemContent ); + } elseif ( !count( $indElements ) && !count( $subElements ) ) { + $retval .= $indstr . Xml::element( $elemName, $elemValue ); + } else { + $retval .= $indstr . Xml::element( $elemName, $elemValue, null ); - if ( count( $subElements ) && count( $indElements ) && !is_null( $subElemContent ) ) { - ApiBase::dieDebug( __METHOD__, "($elemName, ...) has content and subelements" ); + foreach ( $subElements as $subElemId => & $subElemValue ) { + $retval .= self::recXmlPrint( $subElemId, $subElemValue, $indent ); } - if ( !is_null( $subElemContent ) ) { - $retval .= $indstr . Xml::element( $elemName, $elemValue, $subElemContent ); - } elseif ( !count( $indElements ) && !count( $subElements ) ) { - $retval .= $indstr . Xml::element( $elemName, $elemValue ); - } else { - $retval .= $indstr . Xml::element( $elemName, $elemValue, null ); - - foreach ( $subElements as $subElemId => & $subElemValue ) { - $retval .= self::recXmlPrint( $subElemId, $subElemValue, $indent ); - } - - foreach ( $indElements as &$subElemValue ) { - $retval .= self::recXmlPrint( $subElemIndName, $subElemValue, $indent ); - } - - $retval .= $indstr . Xml::closeElement( $elemName ); + foreach ( $indElements as &$subElemValue ) { + $retval .= self::recXmlPrint( $subElemIndName, $subElemValue, $indent ); } - break; - case 'object': - // ignore - break; - default: + + $retval .= $indstr . Xml::closeElement( $elemName ); + } + } elseif ( !is_object( $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; } @@ -230,7 +215,6 @@ class ApiFormatXml extends ApiFormatBase { public function getAllowedParams() { return array( - 'xmldoublequote' => false, 'xslt' => null, 'includexmlnamespace' => false, ); @@ -238,7 +222,6 @@ class ApiFormatXml extends ApiFormatBase { public function getParamDescription() { return array( - 'xmldoublequote' => 'If specified, double quotes all attributes and content', 'xslt' => 'If specified, adds <xslt> as stylesheet. This should be a wiki page ' . 'in the MediaWiki namespace whose page name ends with ".xsl"', 'includexmlnamespace' => 'If specified, adds an XML namespace' @@ -248,8 +231,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..7a60e831 --- /dev/null +++ b/includes/api/ApiImageRotate.php @@ -0,0 +1,230 @@ +<?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']; + + $pageSet = $this->getPageSet(); + $pageSet->execute(); + + $result = array(); + + self::addValues( $result, $pageSet->getInvalidTitles(), 'invalid', 'title' ); + self::addValues( $result, $pageSet->getSpecialTitles(), 'special', 'title' ); + self::addValues( $result, $pageSet->getMissingPageIDs(), 'missing', 'pageid' ); + self::addValues( $result, $pageSet->getMissingRevisionIDs(), 'missing', 'revid' ); + self::addValues( $result, $pageSet->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 )->inContentLanguage()->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 + * @param Title $title + * @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 ) { + $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->getFinalParamDescription() + 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->getFinalPossibleErrors() + ); + } + + 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..f48a822e 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(); @@ -61,7 +57,7 @@ class ApiImport extends ApiBase { $source = ImportStreamSource::newFromUpload( 'xml' ); } if ( !$source->isOK() ) { - $this->dieUsageMsg( $source->getErrorsArray() ); + $this->dieStatus( $source ); } $importer = new WikiImporter( $source->value ); @@ -70,8 +66,8 @@ class ApiImport extends ApiBase { } if ( isset( $params['rootpage'] ) ) { $statusRootPage = $importer->setTargetRootPage( $params['rootpage'] ); - if( !$statusRootPage->isGood() ) { - $this->dieUsageMsg( $statusRootPage->getErrorsArray() ); + if ( !$statusRootPage->isGood() ) { + $this->dieStatus( $statusRootPage ); } } $reporter = new ApiImportReporter( @@ -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..b51d441d 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 @@ -46,6 +46,15 @@ class ApiLogin extends ApiBase { * is reached. The expiry is $this->mLoginThrottle. */ public function execute() { + // If we're in JSON callback mode, no tokens can be obtained + if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) ) { + $this->getResult()->addValue( null, 'login', array( + 'result' => 'Aborted', + 'reason' => 'Cannot log in when using a callback', + ) ); + return; + } + $params = $this->extractRequestParams(); $result = array(); @@ -147,7 +156,7 @@ class ApiLogin extends ApiBase { case LoginForm::ABORTED: $result['result'] = 'Aborted'; - $result['reason'] = $loginForm->mAbortLoginErrorMsg; + $result['reason'] = $loginForm->mAbortLoginErrorMsg; break; default: @@ -278,8 +287,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..c11f16cb 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::isEveryoneAllowed( '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 ) { @@ -373,10 +384,10 @@ class ApiMain extends ApiBase { // Log it if ( !( $e instanceof UsageException ) ) { - wfDebugLog( 'exception', $e->getLogMessage() ); + MWExceptionHandler::logException( $e ); } - // 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 +412,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 +475,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 +500,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 ) { @@ -590,10 +604,10 @@ class ApiMain extends ApiBase { $result = $this->getResult(); // Printer may not be initialized if the extractRequestParams() fails for the main module - if ( !isset ( $this->mPrinter ) ) { + 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 +625,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 +641,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 +652,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 +688,6 @@ class ApiMain extends ApiBase { $params = $this->extractRequestParams(); - $this->mShowVersions = $params['version']; $this->mAction = $params['action']; if ( !is_string( $this->mAction ) ) { @@ -685,20 +703,15 @@ 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; - - $moduleParams = $module->extractRequestParams(); - - // Die if token required, but not provided (unless there is a gettoken parameter) - if ( isset( $moduleParams['gettoken'] ) ) { - $gettoken = $moduleParams['gettoken']; - } else { - $gettoken = false; + $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 $salt = $module->getTokenSalt(); - if ( $salt !== false && !$gettoken ) { + if ( $salt !== false ) { if ( !isset( $moduleParams['token'] ) ) { $this->dieUsageMsg( array( 'missingparam', 'token' ) ); } else { @@ -713,7 +726,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 +758,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::isEveryoneAllowed( 'read' ) && !$user->isAllowed( 'read' ) ) { $this->dieUsageMsg( 'readrequired' ); @@ -764,7 +777,7 @@ class ApiMain extends ApiBase { // Allow extensions to stop execution for arbitrary reasons. $message = false; - if( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) { + if ( !wfRunHooks( 'ApiCheckCanExecute', array( $module, $user, &$message ) ) ) { $this->dieUsageMsg( $message ); } } @@ -772,12 +785,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 +812,7 @@ class ApiMain extends ApiBase { protected function executeAction() { $params = $this->setupExecuteAction(); $module = $this->setupModule(); + $this->mModule = $module; $this->checkExecutePermissions( $module ); @@ -815,6 +830,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 +842,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 +965,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,14 +991,13 @@ 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( + 'maxlag' => array( ApiBase::PARAM_TYPE => 'integer' ), 'smaxage' => array( @@ -884,7 +1009,7 @@ class ApiMain extends ApiBase { ApiBase::PARAM_DFLT => 0 ), 'requestid' => null, - 'servedby' => false, + 'servedby' => false, 'origin' => null, ); } @@ -898,12 +1023,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', ), @@ -913,6 +1037,7 @@ class ApiMain extends ApiBase { 'servedby' => 'Include the hostname that served the request in the results. Unconditionally shown on error', 'origin' => array( 'When accessing the API using a cross-domain AJAX request (CORS), set this to the originating domain.', + 'This must be included in any pre-flight request, and therefore must be part of the request URI (not the POST body).', 'This must match one of the origins in the Origin: header exactly, so it has to be set to something like http://en.wikipedia.org or https://meta.wikimedia.org .', 'If this parameter does not match the Origin: header, a 403 response will be returned.', 'If this parameter matches the Origin: header and the origin is whitelisted, an Access-Control-Allow-Origin header will be set.', @@ -984,11 +1109,11 @@ class ApiMain extends ApiBase { protected function getCredits() { return array( 'API developers:', - ' Roan Kattouw "<Firstname>.<Lastname>@gmail.com" (lead developer Sep 2007-present)', - ' Victor Vasiliev - vasilvv at gee mail dot com', + ' Roan Kattouw "<Firstname>.<Lastname>@gmail.com" (lead developer Sep 2007-2009)', + ' Victor Vasiliev - vasilvv @ gmail . com', ' Bryan Tong Minh - bryan . tongminh @ gmail . com', ' Sam Reed - sam @ reedyboy . net', - ' Yuri Astrakhan "<Firstname><Lastname>@gmail.com" (creator, lead developer Sep 2006-Sep 2007)', + ' 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 +1139,7 @@ class ApiMain extends ApiBase { $this->setHelp(); // Get help text from cache if present $key = wfMemcKey( 'apihelp', $this->getModuleName(), - SpecialVersion::getVersion( 'nodb' ) . - $this->getShowVersions() ); + str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) ) ); if ( $wgAPICacheHelpTimeout > 0 ) { $cached = $wgMemc->get( $key ); if ( $cached ) { @@ -1040,9 +1164,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 +1179,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 +1201,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 +1230,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 +1250,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..c18036cf 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,10 +78,17 @@ 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 ) { + + if ( $toTitleExists ) { $r['moveoverredirect'] = ''; } @@ -96,7 +99,7 @@ class ApiMove extends ApiBase { if ( $retval === true ) { $r['talkfrom'] = $fromTalk->getPrefixedText(); $r['talkto'] = $toTalk->getPrefixedText(); - if( $toTalkExists ) { + if ( $toTalkExists ) { $r['talkmoveoverredirect'] = ''; } } else { @@ -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..315ace37 100644 --- a/includes/api/ApiOpenSearch.php +++ b/includes/api/ApiOpenSearch.php @@ -1,7 +1,5 @@ <?php /** - * - * * Created on Oct 13, 2006 * * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" @@ -29,12 +27,20 @@ */ class ApiOpenSearch extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - + /** + * Override built-in handling of format parameter. + * Only JSON is supported. + * + * @return ApiFormatBase + */ public function getCustomPrinter() { - return $this->getMain()->createPrinterByName( 'json' ); + $params = $this->extractRequestParams(); + $format = $params['format']; + $allowed = array( 'json', 'jsonfm' ); + if ( in_array( $format, $allowed ) ) { + return $this->getMain()->createPrinterByName( $format ); + } + return $this->getMain()->createPrinterByName( $allowed[0] ); } public function execute() { @@ -98,6 +104,10 @@ class ApiOpenSearch extends ApiBase { ApiBase::PARAM_ISMULTI => true ), 'suggest' => false, + 'format' => array( + ApiBase::PARAM_DFLT => 'json', + ApiBase::PARAM_TYPE => array( 'json', 'jsonfm' ), + ) ); } @@ -107,6 +117,7 @@ class ApiOpenSearch extends ApiBase { 'limit' => 'Maximum amount of results to return', 'namespace' => 'Namespaces to search', 'suggest' => 'Do nothing if $wgEnableOpenSearchSuggest is false', + 'format' => 'The format of the output', ); } @@ -123,8 +134,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..7256066d 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. */ @@ -46,6 +42,10 @@ class ApiOptions extends ApiBase { $this->dieUsage( 'Anonymous users cannot change preferences', 'notloggedin' ); } + if ( !$user->isAllowed( 'editmyoptions' ) ) { + $this->dieUsage( 'You don\'t have permission to edit your options', 'permissiondenied' ); + } + $params = $this->extractRequestParams(); $changed = false; @@ -54,7 +54,7 @@ class ApiOptions extends ApiBase { } if ( $params['reset'] ) { - $user->resetOptions(); + $user->resetOptions( $params['resetkinds'], $this->getContext() ); $changed = true; } @@ -74,13 +74,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 +129,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 +170,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 +212,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..b05cb2b6 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,183 @@ * 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(); + /** + * @var int + */ + 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 ); + } + + /** + * 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(); + + $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 ) ); + } 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 ( !$isDryRun ) { + $this->resolvePendingRedirects(); + } - $this->mConvertTitles = $convertTitles; - $this->mConvertedTitles = array(); + 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'; + } - $this->mFakePageId = - 1; + 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 +224,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 +259,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 +362,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 +404,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 +434,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 +464,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 +507,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 +542,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 +553,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 +574,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(); @@ -380,17 +601,16 @@ class ApiPageSet extends ApiQueryBase { } foreach ( $this->mRequestedPageFields as $fieldName => &$fieldValues ) { - $fieldValues[$pageId] = $row-> $fieldName; + $fieldValues[$pageId] = $row->$fieldName; } } /** - * Resolve redirects, if applicable + * Do not use, does nothing, will be removed + * @deprecated since 1.21 */ public function finishPageSetGeneration() { - $this->profileIn(); - $this->resolvePendingRedirects(); - $this->profileOut(); + wfDeprecated( __METHOD__, '1.21' ); } /** @@ -407,7 +627,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 +654,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 +667,7 @@ class ApiPageSet extends ApiQueryBase { $pageids = self::getPositiveIntegers( $pageids ); $res = null; - if ( count( $pageids ) ) { + if ( !empty( $pageids ) ) { $set = array( 'page_id' => $pageids ); @@ -460,7 +680,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 +690,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 +719,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 +738,7 @@ class ApiPageSet extends ApiQueryBase { $this->mTitles[] = $title; // need gender information - if( MWNamespace::hasGenderDistinction( $ns ) ) { + if ( MWNamespace::hasGenderDistinction( $ns ) ) { $usernames[] = $dbkey; } } @@ -541,10 +761,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 +775,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 ); @@ -645,7 +865,7 @@ class ApiPageSet extends ApiQueryBase { $from = $this->mPendingRedirectIDs[$rdfrom]->getPrefixedText(); $to = Title::makeTitle( $row->rd_namespace, $row->rd_title, $row->rd_fragment, $row->rd_interwiki ); unset( $this->mPendingRedirectIDs[$rdfrom] ); - if ( !isset( $this->mAllPages[$row->rd_namespace][$row->rd_title] ) ) { + if ( !$to->isExternal() && !isset( $this->mAllPages[$row->rd_namespace][$row->rd_title] ) ) { $lb->add( $row->rd_namespace, $row->rd_title ); } $this->mRedirectTitles[$from] = $to; @@ -670,44 +890,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 +967,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 +1006,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 +1015,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 +1027,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 +1087,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..3e1a7531 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,14 +142,14 @@ 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'] .= ' '; } $item = array(); if ( is_numeric( $k ) ) { $retval['examples'] .= $v; - $result->setContent( $item, $v ); + ApiResult::setContent( $item, $v ); } else { if ( !is_array( $v ) ) { $item['description'] = $v; @@ -165,7 +157,7 @@ class ApiParamInfo extends ApiBase { $item['description'] = implode( $v, "\n" ); } $retval['examples'] .= $item['description'] . ' ' . $k; - $result->setContent( $item, $k ); + ApiResult::setContent( $item, $k ); } $retval['allexamples'][] = $item; } @@ -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'] = ''; } @@ -308,7 +300,7 @@ class ApiParamInfo extends ApiBase { } // Errors - $retval['errors'] = $this->parseErrors( $obj->getPossibleErrors() ); + $retval['errors'] = $this->parseErrors( $obj->getFinalPossibleErrors() ); $result->setIndexedTagName( $retval['errors'], 'error' ); return $retval; @@ -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..a369994b 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 @@ -40,11 +44,22 @@ class ApiParse extends ApiBase { $params = $this->extractRequestParams(); $text = $params['text']; $title = $params['title']; + if ( $title === null ) { + $titleProvided = false; + // A title is needed for parsing, so arbitrarily choose one + $title = 'API'; + } else { + $titleProvided = true; + } + $page = $params['page']; $pageid = $params['pageid']; $oldid = $params['oldid']; - if ( !is_null( $page ) && ( !is_null( $text ) || $title != 'API' ) ) { + $model = $params['contentmodel']; + $format = $params['contentformat']; + + if ( !is_null( $page ) && ( !is_null( $text ) || $titleProvided ) ) { $this->dieUsage( 'The page parameter cannot be used together with the text and title parameters', 'params' ); } @@ -61,7 +76,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 @@ -87,23 +102,22 @@ class ApiParse extends ApiBase { $titleObj = $rev->getTitle(); $wgTitle = $titleObj; $pageObj = WikiPage::factory( $titleObj ); - $popts = $pageObj->makeParserOptions( $this->getContext() ); - $popts->enableLimitReport( !$params['disablepp'] ); + $popts = $this->makeParserOptions( $pageObj, $params ); // 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,56 +150,84 @@ 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'] ) ) { $oldid = $pageObj->getLatest(); } - $popts = $pageObj->makeParserOptions( $this->getContext() ); - $popts->enableLimitReport( !$params['disablepp'] ); + $popts = $this->makeParserOptions( $pageObj, $params ); // 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'] ); + $popts = $this->makeParserOptions( $pageObj, $params ); + + if ( is_null( $text ) ) { + if ( $titleProvided && ( $prop || $params['generatexml'] ) ) { + $this->setWarning( + "'title' used without 'text', and parsed page properties were requested " . + "(did you mean to use 'page' instead of 'title'?)" + ); + } + // Prevent warning from ContentHandler::makeContent() + $text = ''; + } + + // If we are parsing text, do not use the content model of the default + // API title, but default to wikitext to keep BC. + if ( !$titleProvided && is_null( $model ) ) { + $model = CONTENT_MODEL_WIKITEXT; + $this->setWarning( "No 'title' or 'contentmodel' was given, assuming $model." ); + } + + 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 ); + ApiResult::setContent( $result_array['text'], $this->pstContent->serialize( $format ) ); if ( isset( $prop['wikitext'] ) ) { $result_array['wikitext'] = array(); - $result->setContent( $result_array['wikitext'], $this->text ); + ApiResult::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(); @@ -202,21 +244,35 @@ class ApiParse extends ApiBase { if ( isset( $prop['text'] ) ) { $result_array['text'] = array(); - $result->setContent( $result_array['text'], $p_result->getText() ); + ApiResult::setContent( $result_array['text'], $p_result->getText() ); } if ( !is_null( $params['summary'] ) ) { $result_array['parsedsummary'] = array(); - $result->setContent( $result_array['parsedsummary'], Linker::formatComment( $params['summary'], $titleObj ) ); + ApiResult::setContent( $result_array['parsedsummary'], Linker::formatComment( $params['summary'], $titleObj ) ); + } + + if ( isset( $prop['langlinks'] ) || isset( $prop['languageshtml'] ) ) { + $langlinks = $p_result->getLanguageLinks(); + + if ( $params['effectivelanglinks'] ) { + // Link flags are ignored for now, but may in the future be + // included in the result. + $linkFlags = array(); + wfRunHooks( 'LanguageLinks', array( $titleObj, &$langlinks, &$linkFlags ) ); + } + } else { + $langlinks = false; } if ( isset( $prop['langlinks'] ) ) { - $result_array['langlinks'] = $this->formatLangLinks( $p_result->getLanguageLinks() ); + $result_array['langlinks'] = $this->formatLangLinks( $langlinks ); } if ( isset( $prop['languageshtml'] ) ) { - $languagesHtml = $this->languagesHtml( $p_result->getLanguageLinks() ); + $languagesHtml = $this->languagesHtml( $langlinks ); + $result_array['languageshtml'] = array(); - $result->setContent( $result_array['languageshtml'], $languagesHtml ); + ApiResult::setContent( $result_array['languageshtml'], $languagesHtml ); } if ( isset( $prop['categories'] ) ) { $result_array['categories'] = $this->formatCategoryLinks( $p_result->getCategories() ); @@ -224,7 +280,7 @@ class ApiParse extends ApiBase { if ( isset( $prop['categorieshtml'] ) ) { $categoriesHtml = $this->categoriesHtml( $p_result->getCategories() ); $result_array['categorieshtml'] = array(); - $result->setContent( $result_array['categorieshtml'], $categoriesHtml ); + ApiResult::setContent( $result_array['categorieshtml'], $categoriesHtml ); } if ( isset( $prop['links'] ) ) { $result_array['links'] = $this->formatLinks( $p_result->getLinks() ); @@ -265,7 +321,7 @@ class ApiParse extends ApiBase { if ( isset( $prop['headhtml'] ) ) { $result_array['headhtml'] = array(); - $result->setContent( $result_array['headhtml'], $context->getOutput()->headElement( $context->getSkin() ) ); + ApiResult::setContent( $result_array['headhtml'], $context->getOutput()->headElement( $context->getSkin() ) ); } } @@ -275,10 +331,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 ) ) { + ApiResult::setContent( $result_array['wikitext'], $this->content->serialize( $format ) ); + if ( !is_null( $this->pstContent ) ) { $result_array['psttext'] = array(); - $result->setContent( $result_array['psttext'], $this->pstText ); + ApiResult::setContent( $result_array['psttext'], $this->pstContent->serialize( $format ) ); } } if ( isset( $prop['properties'] ) ) { @@ -286,15 +342,19 @@ 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 { $xml = $dom->__toString(); } $result_array['parsetree'] = array(); - $result->setContent( $result_array['parsetree'], $xml ); + ApiResult::setContent( $result_array['parsetree'], $xml ); } $result_mapping = array( @@ -319,21 +379,42 @@ class ApiParse extends ApiBase { } /** + * Constructs a ParserOptions object + * + * @param WikiPage $pageObj + * @param array $params + * + * @return ParserOptions + */ + protected function makeParserOptions( WikiPage $pageObj, array $params ) { + wfProfileIn( __METHOD__ ); + + $popts = $pageObj->makeParserOptions( $this->getContext() ); + $popts->enableLimitReport( !$params['disablepp'] ); + $popts->setIsPreview( $params['preview'] || $params['sectionpreview'] ); + $popts->setIsSectionPreview( $params['sectionpreview'] ); + + wfProfileOut( __METHOD__ ); + return $popts; + } + + /** * @param $page WikiPage * @param $popts ParserOptions * @param $pageId Int * @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 +423,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 ) { @@ -369,7 +453,7 @@ class ApiParse extends ApiBase { if ( $title ) { $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); } - $this->getResult()->setContent( $entry, $bits[1] ); + ApiResult::setContent( $entry, $bits[1] ); $result[] = $entry; } return $result; @@ -380,7 +464,7 @@ class ApiParse extends ApiBase { foreach ( $links as $link => $sortkey ) { $entry = array(); $entry['sortkey'] = $sortkey; - $this->getResult()->setContent( $entry, $link ); + ApiResult::setContent( $entry, $link ); $result[] = $entry; } return $result; @@ -415,14 +499,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; @@ -434,7 +518,7 @@ class ApiParse extends ApiBase { foreach ( $nslinks as $title => $id ) { $entry = array(); $entry['ns'] = $ns; - $this->getResult()->setContent( $entry, Title::makeTitle( $ns, $title )->getFullText() ); + ApiResult::setContent( $entry, Title::makeTitle( $ns, $title )->getFullText() ); if ( $id != 0 ) { $entry['exists'] = ''; } @@ -456,7 +540,7 @@ class ApiParse extends ApiBase { $entry['url'] = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); } - $this->getResult()->setContent( $entry, $title->getFullText() ); + ApiResult::setContent( $entry, $title->getFullText() ); $result[] = $entry; } } @@ -468,7 +552,7 @@ class ApiParse extends ApiBase { foreach ( $headItems as $tag => $content ) { $entry = array(); $entry['tag'] = $tag; - $this->getResult()->setContent( $entry, $content ); + ApiResult::setContent( $entry, $content ); $result[] = $entry; } return $result; @@ -479,7 +563,7 @@ class ApiParse extends ApiBase { foreach ( $properties as $name => $value ) { $entry = array(); $entry['name'] = $name; - $this->getResult()->setContent( $entry, $value ); + ApiResult::setContent( $entry, $value ); $result[] = $entry; } return $result; @@ -490,7 +574,7 @@ class ApiParse extends ApiBase { foreach ( $css as $file => $link ) { $entry = array(); $entry['file'] = $file; - $this->getResult()->setContent( $entry, $link ); + ApiResult::setContent( $entry, $link ); $result[] = $entry; } return $result; @@ -506,9 +590,7 @@ class ApiParse extends ApiBase { public function getAllowedParams() { return array( - 'title' => array( - ApiBase::PARAM_DFLT => 'API', - ), + 'title' => null, 'text' => null, 'summary' => null, 'page' => null, @@ -544,20 +626,31 @@ class ApiParse extends ApiBase { ), 'pst' => false, 'onlypst' => false, + 'effectivelanglinks' => false, 'uselang' => null, 'section' => null, 'disablepp' => false, 'generatexml' => false, + 'preview' => false, + 'sectionpreview' => false, + 'contentformat' => array( + ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(), + ), + 'contentmodel' => array( + ApiBase::PARAM_TYPE => ContentHandler::getContentModels(), + ) ); } public function getParamDescription() { $p = $this->getModulePrefix(); + $wikitext = CONTENT_MODEL_WIKITEXT; return array( - 'text' => 'Wikitext to parse', + 'text' => "Text to parse. Use {$p}title or {$p}contentmodel to control the content model", 'summary' => 'Summary to parse', 'redirects' => "If the {$p}page or the {$p}pageid parameter is set to a redirect, resolve it", - 'title' => 'Title of page the text belongs to', + 'title' => "Title of page the text belongs to. " . + "If omitted, \"API\" is used as the title with content model $wikitext", 'page' => "Parse the content of this page. Cannot be used together with {$p}text and {$p}title", 'pageid' => "Parse the content of this page. Overrides {$p}page", 'oldid' => "Parse the content of this revision. Overrides {$p}page and {$p}pageid", @@ -581,52 +674,74 @@ class ApiParse extends ApiBase { ' wikitext - Gives the original wikitext that was parsed', ' properties - Gives various properties defined in the parsed wikitext', ), + 'effectivelanglinks' => array( + 'Includes language links supplied by extensions', + '(for use with prop=langlinks|languageshtml)', + ), 'pst' => array( 'Do a pre-save transform on the input before parsing it', - 'Ignored if page, pageid or oldid is used' + "Only valid when used with {$p}text", ), 'onlypst' => array( 'Do a pre-save transform (PST) on the input, but don\'t parse it', - 'Returns the same wikitext, after a PST has been applied. Ignored if page, pageid or oldid is used' + 'Returns the same wikitext, after a PST has been applied.', + "Only valid when used with {$p}text", ), '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 contentmodel=$wikitext)", + 'preview' => 'Parse in preview mode', + 'sectionpreview' => 'Parse in section preview mode (enables preview mode too)', + 'contentformat' => array( + 'Content serialization format used for the input text', + "Only valid when used with {$p}text", + ), + 'contentmodel' => array( + "Content model of the input text. Default is the model of the " . + "specified ${p}title, or $wikitext if ${p}title is not specified", + "Only valid when used with {$p}text", + ), ); } public function getDescription() { + $p = $this->getModulePrefix(); return array( - 'Parses wikitext and returns parser output', + 'Parses content and returns parser output', 'See the various prop-Modules of action=query to get information from the current version of a page', + 'There are several ways to specify the text to parse:', + "1) Specify a page or revision, using {$p}page, {$p}pageid, or {$p}oldid.", + "2) Specify content explicitly, using {$p}text, {$p}title, and {$p}contentmodel.", + "3) Specify only a summary to parse. {$p}prop should be given an empty value.", ); } public function getPossibleErrors() { return array_merge( parent::getPossibleErrors(), array( array( 'code' => 'params', 'info' => 'The page parameter cannot be used together with the text and title parameters' ), - array( 'code' => 'params', 'info' => 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?' ), array( 'code' => 'missingrev', 'info' => 'There is no revision ID oldid' ), array( 'code' => 'permissiondenied', 'info' => 'You don\'t have permission to view deleted revisions' ), array( 'code' => 'missingtitle', 'info' => 'The page you specified doesn\'t exist' ), array( 'code' => 'nosuchsection', 'info' => 'There is no section sectionnumber in page' ), array( 'nosuchpageid' ), array( 'invalidtitle', 'title' ), + array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ), + array( 'code' => 'notwikitext', 'info' => 'The requested operation is only supported on wikitext content.' ), + array( 'code' => 'pagecannotexist', 'info' => "Namespace doesn't allow actual pages" ), ) ); } public function getExamples() { return array( - 'api.php?action=parse&text={{Project:Sandbox}}' + 'api.php?action=parse&page=Project:Sandbox' => 'Parse a page', + 'api.php?action=parse&text={{Project:Sandbox}}' => 'Parse wikitext', + 'api.php?action=parse&text={{PAGENAME}}&title=Test' => 'Parse wikitext, specifying the page title', + 'api.php?action=parse&summary=Some+[[link]]&prop=' => 'Parse a summary', ); } 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..bd2fde2b 100644 --- a/includes/api/ApiPatrol.php +++ b/includes/api/ApiPatrol.php @@ -30,20 +30,32 @@ */ class ApiPatrol extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - /** * Patrols the article or provides the reason the patrol failed. */ public function execute() { $params = $this->extractRequestParams(); - - $rc = RecentChange::newFromID( $params['rcid'] ); - if ( !$rc instanceof RecentChange ) { - $this->dieUsageMsg( array( 'nosuchrcid', $params['rcid'] ) ); + $this->requireOnlyOneParameter( $params, 'rcid', 'revid' ); + + if ( isset( $params['rcid'] ) ) { + $rc = RecentChange::newFromID( $params['rcid'] ); + if ( !$rc ) { + $this->dieUsageMsg( array( 'nosuchrcid', $params['rcid'] ) ); + } + } else { + $rev = Revision::newFromId( $params['revid'] ); + if ( !$rev ) { + $this->dieUsageMsg( array( 'nosuchrevid', $params['revid'] ) ); + } + $rc = $rev->getRecentChange(); + if ( !$rc ) { + $this->dieUsage( + 'The revision ' . $params['revid'] . " can't be patrolled as it's too old", + 'notpatrollable' + ); + } } + $retval = $rc->doMarkPatrolled( $this->getUser() ); if ( $retval ) { @@ -70,8 +82,10 @@ class ApiPatrol extends ApiBase { ApiBase::PARAM_REQUIRED => true ), 'rcid' => array( - ApiBase::PARAM_TYPE => 'integer', - ApiBase::PARAM_REQUIRED => true + ApiBase::PARAM_TYPE => 'integer' + ), + 'revid' => array( + ApiBase::PARAM_TYPE => 'integer' ), ); } @@ -80,6 +94,7 @@ class ApiPatrol extends ApiBase { return array( 'token' => 'Patrol token obtained from list=recentchanges', 'rcid' => 'Recentchanges ID to patrol', + 'revid' => 'Revision ID to patrol', ); } @@ -98,8 +113,16 @@ class ApiPatrol extends ApiBase { } public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'nosuchrcid', 'rcid' ), + return array_merge( + parent::getPossibleErrors(), + parent::getRequireOnlyOneParameterErrorMessages( array( 'rcid', 'revid' ) ), + array( + array( 'nosuchrcid', 'rcid' ), + array( 'nosuchrevid', 'revid' ), + array( + 'code' => 'notpatrollable', + 'info' => "The revision can't be patrolled as it's too old" + ) ) ); } @@ -113,15 +136,12 @@ class ApiPatrol extends ApiBase { public function getExamples() { return array( - 'api.php?action=patrol&token=123abc&rcid=230672766' + 'api.php?action=patrol&token=123abc&rcid=230672766', + 'api.php?action=patrol&token=123abc&revid=230672766' ); } 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..7830c8b4 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(); @@ -107,8 +103,7 @@ class ApiProtect extends ApiBase { $status = $pageObj->doUpdateRestrictions( $protections, $expiryarray, $cascade, $params['reason'], $this->getUser() ); if ( !$status->isOK() ) { - $errors = $status->getErrorsArray(); - $this->dieUsageMsg( $errors[0] ); + $this->dieStatus( $status ); } $res = array( 'title' => $titleObj->getPrefixedText(), @@ -178,7 +173,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 +229,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..0812ba51 100644 --- a/includes/api/ApiPurge.php +++ b/includes/api/ApiPurge.php @@ -31,69 +31,71 @@ */ 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 ); + $forceRecursiveLinkUpdate = $params['forcerecursivelinkupdate']; + $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 || $forceRecursiveLinkUpdate ) { + if ( !$this->getUser()->pingLimiter( 'linkpurge' ) ) { + 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, $forceRecursiveLinkUpdate, $p_result ); DataUpdate::runUpdates( $updates ); $r['linkupdate'] = ''; @@ -114,24 +116,59 @@ 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( + 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, + 'forcerecursivelinkupdate' => 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()->getFinalParamDescription() + + array( + 'forcelinkupdate' => 'Update the links tables', + 'forcerecursivelinkupdate' => 'Update the links table, and update ' . + 'the links tables for any page that uses this page as a template', + ); } public function getResultProperties() { @@ -155,9 +192,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 +211,9 @@ class ApiPurge extends ApiBase { } public function getPossibleErrors() { - $psModule = new ApiPageSet( $this ); return array_merge( parent::getPossibleErrors(), - $psModule->getPossibleErrors() + $this->getPageSet()->getFinalPossibleErrors() ); } @@ -185,8 +226,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..e03837fc 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,17 @@ class ApiQuery extends ApiBase { 'templates' => 'ApiQueryLinks', ); - private $mQueryListModules = array( + /** + * List of Api Query list modules + * @var array + */ + private static $QueryListModules = array( 'allcategories' => 'ApiQueryAllCategories', + 'allfileusages' => 'ApiQueryAllLinks', 'allimages' => 'ApiQueryAllImages', 'alllinks' => 'ApiQueryAllLinks', 'allpages' => 'ApiQueryAllPages', + 'alltransclusions' => 'ApiQueryAllLinks', 'allusers' => 'ApiQueryAllUsers', 'backlinks' => 'ApiQueryBacklinks', 'blocks' => 'ApiQueryBlocks', @@ -80,6 +81,8 @@ class ApiQuery extends ApiBase { 'iwbacklinks' => 'ApiQueryIWBacklinks', 'langbacklinks' => 'ApiQueryLangBacklinks', 'logevents' => 'ApiQueryLogEvents', + 'pageswithprop' => 'ApiQueryPagesWithProp', + 'pagepropnames' => 'ApiQueryPagePropNames', 'protectedtitles' => 'ApiQueryProtectedTitles', 'querypage' => 'ApiQueryQueryPage', 'random' => 'ApiQueryRandom', @@ -92,16 +95,27 @@ 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', + 'filerepoinfo' => 'ApiQueryFileRepoInfo', ); - 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 +124,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 +152,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 +176,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 +236,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 +279,135 @@ class ApiQuery extends ApiBase { // Set the cache mode $this->getMain()->setCacheMode( $cacheMode ); + + if ( $completeModules === null ) { + return; // Legacy continue, we are done + } + + // Reformat query-continue result section + $result = $this->getResult(); + $qc = $result->getData(); + if ( isset( $qc['query-continue'] ) ) { + $qc = $qc['query-continue']; + $result->unsetValue( null, 'query-continue' ); + } elseif ( $this->mGeneratorContinue !== null ) { + $qc = array(); + } else { + // no more "continue"s, we are done! + return; + } + + // we are done with all the modules that do not have result in query-continue + $completeModules = array_merge( $completeModules, array_diff_key( $modules, $qc ) ); + if ( $pagesetParams !== null ) { + // The pageset is still in use, check if all props have finished + $incompleteProps = array_intersect_key( $propModules, $qc ); + if ( count( $incompleteProps ) > 0 ) { + // Properties are not done, continue with the same pageset state - copy current parameters + $main = $this->getMain(); + $contValues = array(); + foreach ( $pagesetParams as $param ) { + // The param name is already prefix-encoded + $contValues[$param] = $main->getVal( $param ); + } + } elseif ( $this->mGeneratorContinue !== null ) { + // Move to the next set of pages produced by pageset, properties need to be restarted + $contValues = $this->mGeneratorContinue; + $pagesetParams = array_keys( $contValues ); + $completeModules = array_diff_key( $completeModules, $propModules ); + } else { + // Done with the pageset, finish up with the the lists and meta modules + $pagesetParams = null; + } + } + + $continue = '||' . implode( '|', array_keys( $completeModules ) ); + if ( $pagesetParams !== null ) { + // list of all pageset parameters to use in the next request + $continue = implode( '|', $pagesetParams ) . $continue; + } else { + // we are done with the pageset + $contValues = array(); + $continue = '-' . $continue; + } + $contValues['continue'] = $continue; + foreach ( $qc as $qcModule ) { + foreach ( $qcModule as $qcKey => $qcValue ) { + $contValues[$qcKey] = $qcValue; + } + } + $this->getResult()->addValue( null, 'continue', $contValues ); + } + + /** + * Parse 'continue' parameter into the list of complete modules and a list of generator parameters + * @param array|null $pagesetParams returns list of generator params or null if pageset is done + * @param array|null $completeModules returns list of finished modules (as keys), or null if legacy + */ + private function initContinue( &$pagesetParams, &$completeModules ) { + $pagesetParams = array(); + $continue = $this->mParams['continue']; + if ( $continue !== null ) { + $this->mUseLegacyContinue = false; + if ( $continue !== '' ) { + // Format: ' pagesetParam1 | pagesetParam2 || module1 | module2 | module3 | ... + // If pageset is done, use '-' + $continue = explode( '||', $continue ); + $this->dieContinueUsageIf( count( $continue ) !== 2 ); + if ( $continue[0] === '-' ) { + $pagesetParams = null; // No need to execute pageset + } elseif ( $continue[0] !== '' ) { + // list of pageset params that might need to be repeated + $pagesetParams = explode( '|', $continue[0] ); + } + $continue = $continue[1]; + } + if ( $continue !== '' ) { + $completeModules = array_flip( explode( '|', $continue ) ); + } else { + $completeModules = array(); + } + } else { + $this->mUseLegacyContinue = true; + $completeModules = null; + } + } + + /** + * Validate sub-modules, filter out completed ones, and do requestExtraData() + * @param array $allModules An dict of name=>instance of all modules requested by the client + * @param array|null $completeModules list of finished modules, or null if legacy continue + * @param bool $usePageset True if pageset will be executed + * @return array of modules to be processed during this execution + */ + private function initModules( $allModules, $completeModules, $usePageset ) { + $modules = $allModules; + $tmp = $completeModules; + $wasPosted = $this->getRequest()->wasPosted(); + + /** @var $module ApiQueryBase */ + foreach ( $allModules as $moduleName => $module ) { + if ( !$wasPosted && $module->mustBePosted() ) { + $this->dieUsageMsgOrDebug( array( 'mustbeposted', $moduleName ) ); + } + if ( $completeModules !== null && array_key_exists( $moduleName, $completeModules ) ) { + // If this module is done, mark all its params as used + $module->extractRequestParams(); + // Make sure this module is not used during execution + unset( $modules[$moduleName] ); + unset( $tmp[$moduleName] ); + } elseif ( $completeModules === null || $usePageset ) { + // Query modules may optimize data requests through the $this->getPageSet() + // object by adding extra fields from the page table. + // This function will gather all the extra request fields from the modules. + $module->requestExtraData( $this->mPageSet ); + } else { + // Error - this prop module must have finished before generator is done + $this->dieContinueUsageIf( $this->mModuleMgr->getModuleGroup( $moduleName ) === 'prop' ); + } + } + $this->dieContinueUsageIf( $completeModules !== null && count( $tmp ) !== 0 ); + return $modules; } /** @@ -320,32 +433,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 +465,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,12 +508,13 @@ class ApiQuery extends ApiBase { ); } // Report special pages + /** @var $title Title */ foreach ( $pageSet->getSpecialTitles() as $fakeId => $title ) { $vals = array(); ApiQueryBase::addTitleInfo( $vals, $title ); $vals['special'] = ''; if ( $title->isSpecialPage() && - !SpecialPageFactory::exists( $title->getDbKey() ) ) { + !SpecialPageFactory::exists( $title->getDBkey() ) ) { $vals['missing'] = ''; } elseif ( $title->getNamespace() == NS_MEDIA && !wfFindFile( $title ) ) { @@ -489,7 +532,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 +543,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 +602,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 +615,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 +646,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 +693,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()->getFinalParamDescription() + 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,31 +722,25 @@ 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()->getFinalPossibleErrors() + ); } 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=', ); } public function getHelpUrls() { return array( + 'https://www.mediawiki.org/wiki/API:Query', 'https://www.mediawiki.org/wiki/API:Meta', 'https://www.mediawiki.org/wiki/API:Properties', '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..3f5c6ee7 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" ); @@ -79,9 +76,8 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { if ( $dir == 'newer' ) { $this->addWhereRange( 'cat_pages', 'newer', $min, $max ); } else { - $this->addWhereRange( 'cat_pages', 'older', $max, $min); + $this->addWhereRange( 'cat_pages', 'older', $max, $min ); } - if ( isset( $params['prefix'] ) ) { $this->addWhere( 'cat_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); @@ -125,7 +121,7 @@ class ApiQueryAllCategories extends ApiQueryGeneratorBase { $pages[] = $titleObj; } else { $item = array(); - $result->setContent( $item, $titleObj->getText() ); + ApiResult::setContent( $item, $titleObj->getText() ); if ( isset( $prop['size'] ) ) { $item['size'] = intval( $row->cat_pages ); $item['pages'] = $row->cat_pages - $row->cat_subcats - $row->cat_files; @@ -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..ccc7a3a2 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' ); } @@ -188,7 +189,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { if ( !is_null( $params['mime'] ) ) { global $wgMiserMode; - if ( $wgMiserMode ) { + if ( $wgMiserMode ) { $this->dieUsage( 'MIME search disabled in Miser Mode', 'mimesearchdisabled' ); } @@ -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__ ); @@ -256,7 +260,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase { } public function getAllowedParams() { - return array ( + return array( 'sort' => array( ApiBase::PARAM_DFLT => 'name', ApiBase::PARAM_TYPE => array( @@ -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..47d1bcef 100644 --- a/includes/api/ApiQueryAllLinks.php +++ b/includes/api/ApiQueryAllLinks.php @@ -32,7 +32,51 @@ 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->fieldTitle = 'title'; + $this->dfltNamespace = NS_MAIN; + $this->hasNamespace = true; + $this->indexTag = 'l'; + $this->description = 'Enumerate all links that point to a given namespace'; + $this->descriptionWhat = 'link'; + $this->descriptionTargets = 'linked titles'; + $this->descriptionLinking = 'linking'; + break; + case 'alltransclusions': + $prefix = 'at'; + $this->table = 'templatelinks'; + $this->tablePrefix = 'tl_'; + $this->fieldTitle = 'title'; + $this->dfltNamespace = NS_TEMPLATE; + $this->hasNamespace = true; + $this->indexTag = 't'; + $this->description = 'List all transclusions (pages embedded using {{x}}), including non-existing'; + $this->descriptionWhat = 'transclusion'; + $this->descriptionTargets = 'transcluded titles'; + $this->descriptionLinking = 'transcluding'; + break; + case 'allfileusages': + $prefix = 'af'; + $this->table = 'imagelinks'; + $this->tablePrefix = 'il_'; + $this->fieldTitle = 'to'; + $this->dfltNamespace = NS_FILE; + $this->hasNamespace = false; + $this->indexTag = 'f'; + $this->description = 'List all file usages, including non-existing'; + $this->descriptionWhat = 'file'; + $this->descriptionTargets = 'file titles'; + $this->descriptionLinking = 'using'; + break; + default: + ApiBase::dieDebug( __METHOD__, 'Unknown module name' ); + } + + parent::__construct( $query, $moduleName, $prefix ); } public function execute() { @@ -55,75 +99,81 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $db = $this->getDB(); $params = $this->extractRequestParams(); + $pfx = $this->tablePrefix; + $fieldTitle = $this->fieldTitle; $prop = array_flip( $params['prop'] ); $fld_ids = isset( $prop['ids'] ); $fld_title = isset( $prop['title'] ); + if ( $this->hasNamespace ) { + $namespace = $params['namespace']; + } else { + $namespace = $this->dfltNamespace; + } 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->descriptionWhat}s mode", + 'params' ); } $this->addOption( 'DISTINCT' ); } - $this->addTables( 'pagelinks' ); - $this->addWhereFld( 'pl_namespace', $params['namespace'] ); - - if ( !is_null( $params['from'] ) && !is_null( $params['continue'] ) ) { - $this->dieUsage( 'alcontinue and alfrom cannot be used together', 'params' ); + $this->addTables( $this->table ); + if ( $this->hasNamespace ) { + $this->addWhereFld( $pfx . 'namespace', $namespace ); } - 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}{$fieldTitle} $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}{$fieldTitle} $op $continueTitle OR " . + "({$pfx}{$fieldTitle} = $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 . $fieldTitle, 'newer', $from, $to ); if ( isset( $params['prefix'] ) ) { - $this->addWhere( 'pl_title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); + $this->addWhere( $pfx . $fieldTitle . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) ); } - $this->addFields( 'pl_title' ); - $this->addFieldsIf( 'pl_from', !$params['unique'] ); + $this->addFields( array( 'pl_title' => $pfx . $fieldTitle ) ); + $this->addFieldsIf( array( 'pl_from' => $pfx . 'from' ), !$params['unique'] ); - $this->addOption( 'USE INDEX', 'pl_namespace' ); + if ( $this->hasNamespace ) { + $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 . $fieldTitle . $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 +182,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; } @@ -143,7 +193,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { $vals['fromid'] = intval( $row->pl_from ); } if ( $fld_title ) { - $title = Title::makeTitle( $params['namespace'], $row->pl_title ); + $title = Title::makeTitle( $namespace, $row->pl_title ); ApiQueryBase::addTitleInfo( $vals, $title ); } $fit = $result->addValue( array( 'query', $this->getModuleName() ), null, $vals ); @@ -151,24 +201,28 @@ 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( $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 ); } } public function getAllowedParams() { - return array( + $allowedParams = array( 'continue' => null, 'from' => null, 'to' => null, @@ -183,7 +237,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { ) ), 'namespace' => array( - ApiBase::PARAM_DFLT => 0, + ApiBase::PARAM_DFLT => $this->dfltNamespace, ApiBase::PARAM_TYPE => 'namespace' ), 'limit' => array( @@ -201,25 +255,39 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase { ) ), ); + if ( !$this->hasNamespace ) { + unset( $allowedParams['namespace'] ); + } + return $allowedParams; } public function getParamDescription() { $p = $this->getModulePrefix(); - 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", + $what = $this->descriptionWhat; + $targets = $this->descriptionTargets; + $linking = $this->descriptionLinking; + $paramDescription = array( + 'from' => "The title of the $what to start enumerating from", + 'to' => "The title of the $what to stop enumerating at", + 'prefix' => "Search for all $targets that begin with this value", + 'unique' => array( + "Only show distinct $targets. Cannot be used with {$p}prop=ids.", + '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 $what", ), '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', ); + if ( !$this->hasNamespace ) { + unset( $paramDescription['namespace'] ); + } + return $paramDescription; } public function getResultProperties() { @@ -235,30 +303,36 @@ 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(); + $what = $this->descriptionWhat; 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 {$what}s mode" ), ) ); } public function getExamples() { + $p = $this->getModulePrefix(); + $name = $this->getModuleName(); + $what = $this->descriptionWhat; + $targets = $this->descriptionTargets; return array( - 'api.php?action=query&list=alllinks&alunique=&alfrom=B', + "api.php?action=query&list={$name}&{$p}from=B&{$p}prop=ids|title" + => "List $targets with page ids they are from, including missing ones. Start at B", + "api.php?action=query&list={$name}&{$p}unique=&{$p}from=B" + => "List unique $targets", + "api.php?action=query&generator={$name}&g{$p}unique=&g{$p}from=B" + => "Gets all $targets, marking the missing ones", + "api.php?action=query&generator={$name}&g{$p}from=B" + => "Gets pages containing the {$what}s", ); } public function getHelpUrls() { - return 'https://www.mediawiki.org/wiki/API:Alllinks'; - } - - public function getVersion() { - return __CLASS__ . ': $Id$'; + $name = ucfirst( $this->getModuleName() ); + return "https://www.mediawiki.org/wiki/API:{$name}"; } } diff --git a/includes/api/ApiQueryAllMessages.php b/includes/api/ApiQueryAllMessages.php index f5e1146b..d47c7b76 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 { @@ -86,7 +87,7 @@ class ApiQueryAllMessages extends ApiQueryBase { foreach ( $messages_target as $message ) { // === 0: must be at beginning of string (position 0) if ( strpos( $message, $params['prefix'] ) === 0 ) { - if( !$skip ) { + if ( !$skip ) { $skip = true; } $messages_filtered[] = $message; @@ -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..d95980c2 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' ); } @@ -177,7 +174,7 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { $res = $this->select( __METHOD__ ); //Get gender information - if( MWNamespace::hasGenderDistinction( $params['namespace'] ) ) { + if ( MWNamespace::hasGenderDistinction( $params['namespace'] ) ) { $users = array(); foreach ( $res as $row ) { $users[] = $row->page_title; @@ -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( @@ -307,7 +304,10 @@ class ApiQueryAllPages extends ApiQueryGeneratorBase { 'prtype' => 'Limit to protected pages only', 'prlevel' => "The protection level (must be used with {$p}prtype= parameter)", 'prfiltercascade' => "Filter protections based on cascadingness (ignored when {$p}prtype isn't set)", - 'filterlanglinks' => 'Filter based on whether a page has langlinks', + 'filterlanglinks' => array( + 'Filter based on whether a page has langlinks', + 'Note that this may not consider langlinks added by extensions.', + ), 'limit' => 'How many total pages to return.', 'prexpiry' => array( 'Which protection expiry to filter the page on', @@ -336,7 +336,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 +350,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 +358,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..1948a51a 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 ) { + 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..2d1089a7 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(); @@ -227,10 +229,10 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { $orderBy = array(); $sort = ( $this->params['dir'] == 'descending' ? ' DESC' : '' ); // Don't order by namespace/title if it's constant in the WHERE clause - if( $this->hasNS && count( array_unique( $allRedirNs ) ) != 1 ) { + if ( $this->hasNS && count( array_unique( $allRedirNs ) ) != 1 ) { $orderBy[] = $this->bl_ns . $sort; } - if( count( array_unique( $allRedirDBkey ) ) != 1 ) { + if ( count( array_unique( $allRedirDBkey ) ) != 1 ) { $orderBy[] = $this->bl_title . $sort; } $orderBy[] = $this->bl_from . $sort; @@ -246,13 +248,16 @@ 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(); if ( $this->params['limit'] == 'max' ) { $this->params['limit'] = $this->getMain()->canApiHighLimits() ? $botMax : $userMax; $result->setParsedLimit( $this->getModuleName(), $this->params['limit'] ); + } else { + $this->params['limit'] = intval( $this->params['limit'] ); + $this->validateLimit( 'limit', $this->params['limit'], 1, $userMax, $botMax ); } $this->processContinue(); @@ -292,9 +297,9 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { // We've reached the one extra which shows that there are additional pages to be had. Stop here... // We need to keep the parent page of this redir in if ( $this->hasNS ) { - $parentID = $this->pageMap[$row-> { $this->bl_ns } ][$row-> { $this->bl_title } ]; + $parentID = $this->pageMap[$row->{$this->bl_ns}][$row->{$this->bl_title}]; } else { - $parentID = $this->pageMap[NS_FILE][$row-> { $this->bl_title } ]; + $parentID = $this->pageMap[NS_FILE][$row->{$this->bl_title}]; } $this->continueStr = $this->getContinueRedirStr( $parentID, $row->page_id ); break; @@ -375,8 +380,8 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase { if ( $row->page_is_redirect ) { $a['redirect'] = ''; } - $ns = $this->hasNS ? $row-> { $this->bl_ns } : NS_FILE; - $parentID = $this->pageMap[$ns][$row-> { $this->bl_title } ]; + $ns = $this->hasNS ? $row->{$this->bl_ns} : NS_FILE; + $parentID = $this->pageMap[$ns][$row->{$this->bl_title}]; // Put all the results in an array first $this->resultArr[$parentID]['redirlinks'][] = $a; $this->getResult()->setIndexedTagName( $this->resultArr[$parentID]['redirlinks'], $this->bl_code ); @@ -406,20 +411,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 +454,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 +534,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 +560,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..8668e04b 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 ) { @@ -433,7 +432,7 @@ abstract class ApiQueryBase extends ApiBase { if ( trim( $key ) == '' ) { return ''; } - $t = Title::newFromDbKey( $key ); + $t = Title::newFromDBkey( $key ); // This really shouldn't happen but we gotta check anyway if ( !$t ) { $this->dieUsageMsg( array( 'invalidtitle', $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 ) { @@ -479,7 +478,7 @@ abstract class ApiQueryBase extends ApiBase { * @param $protocol String * @return null|string */ - public function prepareUrlQuerySearchString( $query = null, $protocol = null) { + public function prepareUrlQuerySearchString( $query = null, $protocol = null ) { $db = $this->getDb(); if ( !is_null( $query ) || $query != '' ) { if ( is_null( $protocol ) ) { @@ -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..e3c27f5e 100644 --- a/includes/api/ApiQueryBlocks.php +++ b/includes/api/ApiQueryBlocks.php @@ -63,7 +63,7 @@ class ApiQueryBlocks extends ApiQueryBase { $this->addTables( 'ipblocks' ); $this->addFields( 'ipb_auto' ); - $this->addFieldsIf ( 'ipb_id', $fld_id ); + $this->addFieldsIf( 'ipb_id', $fld_id ); $this->addFieldsIf( array( 'ipb_address', 'ipb_user' ), $fld_user || $fld_userid ); $this->addFieldsIf( 'ipb_by_text', $fld_by ); $this->addFieldsIf( 'ipb_by', $fld_byid ); @@ -91,17 +91,30 @@ class ApiQueryBlocks extends ApiQueryBase { $this->addWhereFld( 'ipb_auto', 0 ); } if ( isset( $params['ip'] ) ) { - list( $ip, $range ) = IP::parseCIDR( $params['ip'] ); - if ( $ip && $range ) { - // We got a CIDR range - if ( $range < 16 ) - $this->dieUsage( 'CIDR ranges broader than /16 are not accepted', 'cidrtoobroad' ); - $lower = wfBaseConvert( $ip, 10, 16, 8, false ); - $upper = wfBaseConvert( $ip + pow( 2, 32 - $range ) - 1, 10, 16, 8, false ); + global $wgBlockCIDRLimit; + if ( IP::isIPv4( $params['ip'] ) ) { + $type = 'IPv4'; + $cidrLimit = $wgBlockCIDRLimit['IPv4']; + $prefixLen = 0; + } elseif ( IP::isIPv6( $params['ip'] ) ) { + $type = 'IPv6'; + $cidrLimit = $wgBlockCIDRLimit['IPv6']; + $prefixLen = 3; // IP::toHex output is prefixed with "v6-" } else { - $lower = $upper = IP::toHex( $params['ip'] ); + $this->dieUsage( 'IP parameter is not valid', 'param_ip' ); + } + + # Check range validity, if it's a CIDR + list( $ip, $range ) = IP::parseCIDR( $params['ip'] ); + if ( $ip !== false && $range !== false && $range < $cidrLimit ) { + $this->dieUsage( "$type CIDR ranges broader than /$cidrLimit are not accepted", 'cidrtoobroad' ); } - $prefix = substr( $lower, 0, 4 ); + + # Let IP::parseRange handle calculating $upper, instead of duplicating the logic here. + list( $lower, $upper ) = IP::parseRange( $params['ip'] ); + + # Extract the common prefix to any rangeblock affecting this IP/CIDR + $prefix = substr( $lower, 0, $prefixLen + floor( $cidrLimit / 4 ) ); # Fairly hard to make a malicious SQL statement out of hex characters, # but it is good practice to add quotes @@ -120,10 +133,10 @@ class ApiQueryBlocks extends ApiQueryBase { $show = array_flip( $params['show'] ); /* Check for conflicting parameters. */ - if ( ( isset ( $show['account'] ) && isset ( $show['!account'] ) ) - || ( isset ( $show['ip'] ) && isset ( $show['!ip'] ) ) - || ( isset ( $show['range'] ) && isset ( $show['!range'] ) ) - || ( isset ( $show['temp'] ) && isset ( $show['!temp'] ) ) + if ( ( isset( $show['account'] ) && isset( $show['!account'] ) ) + || ( isset( $show['ip'] ) && isset( $show['!ip'] ) ) + || ( isset( $show['range'] ) && isset( $show['!range'] ) ) + || ( isset( $show['temp'] ) && isset( $show['!temp'] ) ) ) { $this->dieUsageMsg( 'show' ); } @@ -132,10 +145,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 +195,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 @@ -294,6 +307,7 @@ class ApiQueryBlocks extends ApiQueryBase { } public function getParamDescription() { + global $wgBlockCIDRLimit; $p = $this->getModulePrefix(); return array( 'start' => 'The timestamp to start enumerating from', @@ -301,8 +315,12 @@ 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.', - 'Cannot be used together with bkusers. CIDR ranges broader than /16 are not accepted' ), + 'ip' => array( + 'Get all blocks applying to this IP or CIDR range, including range blocks.', + "Cannot be used together with bkusers. CIDR ranges broader than " . + "IPv4/{$wgBlockCIDRLimit['IPv4']} or IPv6/{$wgBlockCIDRLimit['IPv6']} " . + "are not accepted" + ), 'limit' => 'The maximum amount of blocks to list', 'prop' => array( 'Which properties to get', @@ -383,10 +401,19 @@ class ApiQueryBlocks extends ApiQueryBase { } public function getPossibleErrors() { + global $wgBlockCIDRLimit; return array_merge( parent::getPossibleErrors(), $this->getRequireOnlyOneParameterErrorMessages( array( 'users', 'ip' ) ), array( - array( 'code' => 'cidrtoobroad', 'info' => 'CIDR ranges broader than /16 are not accepted' ), + array( + 'code' => 'cidrtoobroad', + 'info' => "IPv4 CIDR ranges broader than /{$wgBlockCIDRLimit['IPv4']} are not accepted" + ), + array( + 'code' => 'cidrtoobroad', + 'info' => "IPv6 CIDR ranges broader than /{$wgBlockCIDRLimit['IPv6']} are not accepted" + ), + array( 'code' => 'param_ip', 'info' => 'IP parameter is not valid' ), array( 'code' => 'param_user', 'info' => 'User parameter may not be empty' ), array( 'code' => 'param_user', 'info' => 'User name user is not valid' ), array( 'show' ), @@ -404,8 +431,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..5d714f57 100644 --- a/includes/api/ApiQueryCategories.php +++ b/includes/api/ApiQueryCategories.php @@ -49,11 +49,10 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { /** * @param $resultPageSet ApiPageSet - * @return */ private function run( $resultPageSet = null ) { if ( $this->getPageSet()->getGoodTitleCount() == 0 ) { - return; // nothing to do + return; // nothing to do } $params = $this->extractRequestParams(); @@ -85,10 +84,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] ); @@ -177,7 +173,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { break; } - $titles[] = Title :: makeTitle( NS_CATEGORY, $row->cl_to ); + $titles[] = Title::makeTitle( NS_CATEGORY, $row->cl_to ); } $resultPageSet->populateFromTitles( $titles ); } @@ -187,7 +183,7 @@ class ApiQueryCategories extends ApiQueryGeneratorBase { return array( 'prop' => array( ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => array ( + ApiBase::PARAM_TYPE => array( 'sortkey', 'timestamp', 'hidden', @@ -276,8 +272,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..704d108a 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 @@ -221,7 +217,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { if ( $fld_sortkeyprefix ) { $vals['sortkeyprefix'] = $row->cl_sortkey_prefix; } - if ( $fld_type ) { + if ( $fld_type ) { $vals['type'] = $row->cl_type; } if ( $fld_timestamp ) { @@ -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' ); } } @@ -262,7 +258,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { 'prop' => array( ApiBase::PARAM_DFLT => 'ids|title', ApiBase::PARAM_ISMULTI => true, - ApiBase::PARAM_TYPE => array ( + ApiBase::PARAM_TYPE => array( 'ids', 'title', 'sortkey', @@ -271,7 +267,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase { 'timestamp', ) ), - 'namespace' => array ( + 'namespace' => array( ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => 'namespace', ), @@ -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..82733133 100644 --- a/includes/api/ApiQueryDeletedrevs.php +++ b/includes/api/ApiQueryDeletedrevs.php @@ -50,13 +50,18 @@ class ApiQueryDeletedrevs extends ApiQueryBase { $fld_user = isset( $prop['user'] ); $fld_userid = isset( $prop['userid'] ); $fld_comment = isset( $prop['comment'] ); - $fld_parsedcomment = isset ( $prop['parsedcomment'] ); + $fld_parsedcomment = isset( $prop['parsedcomment'] ); $fld_minor = isset( $prop['minor'] ); $fld_len = isset( $prop['len'] ); $fld_sha1 = isset( $prop['sha1'] ); $fld_content = isset( $prop['content'] ); $fld_token = isset( $prop['token'] ); + // If we're in JSON callback mode, no tokens can be obtained + if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) ) { + $fld_token = false; + } + $result = $this->getResult(); $pageSet = $this->getPageSet(); $titles = $pageSet->getTitles(); @@ -74,15 +79,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 ) { + 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 +121,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 +165,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 +311,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 +366,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 +401,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 +425,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..0311fa7f 100644 --- a/includes/api/ApiQueryDuplicateFiles.php +++ b/includes/api/ApiQueryDuplicateFiles.php @@ -48,8 +48,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { } /** - * @param $resultPageSet ApiPageSet - * @return + * @param ApiPageSet $resultPageSet */ private function run( $resultPageSet = null ) { $params = $this->extractRequestParams(); @@ -59,17 +58,14 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { } $images = $namespaces[NS_FILE]; - if( $params['dir'] == 'descending' ) { + if ( $params['dir'] == 'descending' ) { $images = array_reverse( $images ); } $skipUntilThisDup = false; if ( isset( $params['continue'] ) ) { $cont = explode( '|', $params['continue'] ); - if ( count( $cont ) != 2 ) { - $this->dieUsage( 'Invalid continue param. You should pass the ' . - 'original value returned by the previous query', '_badcontinue' ); - } + $this->dieContinueUsageIf( count( $cont ) != 2 ); $fromImage = $cont[0]; $skipUntilThisDup = $cont[1]; // Filter out any images before $fromImage @@ -83,7 +79,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { } $filesToFind = array_keys( $images ); - if( $params['localonly'] ) { + if ( $params['localonly'] ) { $files = RepoGroup::singleton()->getLocalRepo()->findFiles( $filesToFind ); } else { $files = RepoGroup::singleton()->findFiles( $filesToFind ); @@ -95,33 +91,35 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { $sha1s = array(); foreach ( $files as $file ) { + /** @var $file File */ $sha1s[$file->getName()] = $file->getSha1(); } // find all files with the hashes, result format is: array( hash => array( dup1, dup2 ), hash1 => ... ) $filesToFindBySha1s = array_unique( array_values( $sha1s ) ); - if( $params['localonly'] ) { + if ( $params['localonly'] ) { $filesBySha1s = RepoGroup::singleton()->getLocalRepo()->findBySha1s( $filesToFindBySha1s ); } else { $filesBySha1s = RepoGroup::singleton()->findBySha1s( $filesToFindBySha1s ); } // iterate over $images to handle continue param correct - foreach( $images as $image => $pageId ) { - if( !isset( $sha1s[$image] ) ) { + foreach ( $images as $image => $pageId ) { + if ( !isset( $sha1s[$image] ) ) { continue; //file does not exist } $sha1 = $sha1s[$image]; $dupFiles = $filesBySha1s[$sha1]; - if( $params['dir'] == 'descending' ) { + if ( $params['dir'] == 'descending' ) { $dupFiles = array_reverse( $dupFiles ); } + /** @var $dupFile File */ foreach ( $dupFiles as $dupFile ) { $dupName = $dupFile->getName(); - if( $image == $dupName && $dupFile->isLocal() ) { + if ( $image == $dupName && $dupFile->isLocal() ) { continue; //ignore the local file itself } - if( $skipUntilThisDup !== false && $dupName < $skipUntilThisDup ) { + if ( $skipUntilThisDup !== false && $dupName < $skipUntilThisDup ) { continue; //skip to pos after the image from continue param } $skipUntilThisDup = false; @@ -133,14 +131,14 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { break; } if ( !is_null( $resultPageSet ) ) { - $titles[] = $file->getTitle(); + $titles[] = $dupFile->getTitle(); } else { $r = array( 'name' => $dupName, 'user' => $dupFile->getUser( 'text' ), 'timestamp' => wfTimestamp( TS_ISO_8601, $dupFile->getTimestamp() ) ); - if( !$dupFile->isLocal() ) { + if ( !$dupFile->isLocal() ) { $r['shared'] = ''; } $fit = $this->addPageSubItem( $pageId, $r ); @@ -150,7 +148,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase { } } } - if( !$fit ) { + if ( !$fit ) { break; } } @@ -204,12 +202,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 +212,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..456e87ba 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, ); } @@ -213,12 +218,13 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase { ), 'offset' => 'Used for paging. Use the value returned for "continue"', 'protocol' => array( - "Protocol of the url. If empty and {$p}query set, the protocol is http.", + "Protocol of the URL. If empty and {$p}query set, the protocol is http.", "Leave both this and {$p}query empty to list all external links" ), 'query' => 'Search string without protocol. See [[Special:LinkSearch]]. Leave empty to list all external links', '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..583ef697 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, ); } @@ -126,10 +131,11 @@ class ApiQueryExternalLinks extends ApiQueryBase { 'limit' => 'How many links to return', 'offset' => 'When more results are available, use this to continue', 'protocol' => array( - "Protocol of the url. If empty and {$p}query set, the protocol is http.", + "Protocol of the URL. If empty and {$p}query set, the protocol is http.", "Leave both this and {$p}query empty to list all external links" ), 'query' => 'Search string without protocol. Useful for checking whether a certain page contains a certain external url', + '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/ApiQueryFileRepoInfo.php b/includes/api/ApiQueryFileRepoInfo.php new file mode 100644 index 00000000..3a353533 --- /dev/null +++ b/includes/api/ApiQueryFileRepoInfo.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © 2013 Mark Holmquist <mtraceur@member.fsf.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.22 + */ + +/** + * A query action to return meta information about the foreign file repos + * configured on the wiki. + * + * @ingroup API + */ +class ApiQueryFileRepoInfo extends ApiQueryBase { + + public function __construct( $query, $moduleName ) { + parent::__construct( $query, $moduleName, 'fri' ); + } + + protected function getInitialisedRepoGroup() { + $repoGroup = RepoGroup::singleton(); + + if ( !$repoGroup->reposInitialised ) { + $repoGroup->initialiseRepos(); + } + + return $repoGroup; + } + + public function execute() { + $params = $this->extractRequestParams(); + $props = array_flip( $params['prop'] ); + + $repos = array(); + + $repoGroup = $this->getInitialisedRepoGroup(); + + $repoGroup->forEachForeignRepo( function ( $repo ) use ( &$repos, $props ) { + $repos[] = array_intersect_key( $repo->getInfo(), $props ); + } ); + + $repos[] = array_intersect_key( $repoGroup->localRepo->getInfo(), $props ); + + $result = $this->getResult(); + $result->setIndexedTagName( $repos, 'repo' ); + $result->addValue( array( 'query' ), 'repos', $repos ); + } + + public function getCacheMode( $params ) { + return 'public'; + } + + public function getAllowedParams() { + $props = $this->getProps(); + + return array( + 'prop' => array( + ApiBase::PARAM_DFLT => join( '|', $props ), + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => $props, + ), + ); + } + + public function getProps() { + $props = array(); + $repoGroup = $this->getInitialisedRepoGroup(); + + $repoGroup->forEachForeignRepo( function ( $repo ) use ( &$props ) { + $props = array_merge( $props, array_keys( $repo->getInfo() ) ); + } ); + + return array_values( array_unique( array_merge( $props, array_keys( $repoGroup->localRepo->getInfo() ) ) ) ); + } + + public function getParamDescription() { + $p = $this->getModulePrefix(); + return array( + 'prop' => array( + 'Which repository properties to get (there may be more available on some wikis):', + ' apiurl - URL to the repository API - helpful for getting image info from the host.', + ' name - The key of the repository - used in e.g. $wgForeignFileRepos and imageinfo return values.', + ' displayname - The human-readable name of the repository wiki.', + ' rooturl - Root URL for image paths.', + ' local - Whether that repository is the local one or not.', + ), + ); + } + + public function getDescription() { + return 'Return meta information about image repositories configured on the wiki.'; + } + + public function getExamples() { + return array( + 'api.php?action=query&meta=filerepoinfo&friprop=apiurl|name|displayname', + ); + } +} diff --git a/includes/api/ApiQueryFilearchive.php b/includes/api/ApiQueryFilearchive.php index a5486ef4..f53cd386 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 ); @@ -226,7 +218,7 @@ class ApiQueryFilearchive extends ApiQueryBase { } public function getAllowedParams() { - return array ( + return array( 'from' => null, 'continue' => null, 'to' => null, @@ -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', @@ -289,7 +281,7 @@ class ApiQueryFilearchive extends ApiQueryBase { ' parseddescription - Parse the description on the version', ' mime - Adds MIME of the image', ' mediatype - Adds the media type of the image', - ' metadata - Lists EXIF metadata for the version of the image', + ' metadata - Lists Exif metadata for the version of the image', ' bitdepth - Adds the bit depth of the version', ' archivename - Adds the file name of the archive version for non-latest versions' ), @@ -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' ), ) ); } @@ -383,7 +374,7 @@ class ApiQueryFilearchive extends ApiQueryBase { ); } - public function getVersion() { - return __CLASS__ . ': $Id$'; + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Filearchive'; } } diff --git a/includes/api/ApiQueryIWBacklinks.php b/includes/api/ApiQueryIWBacklinks.php index c5012f08..ebae3e76 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' ), ) ); } @@ -244,7 +240,7 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase { ); } - public function getVersion() { - return __CLASS__ . ': $Id$'; + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Iwbacklinks'; } } diff --git a/includes/api/ApiQueryIWLinks.php b/includes/api/ApiQueryIWLinks.php index 30c7f5a8..be539311 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] ); @@ -84,8 +81,8 @@ class ApiQueryIWLinks extends ApiQueryBase { $this->addOption( 'ORDER BY', 'iwl_from' . $sort ); } else { $this->addOption( 'ORDER BY', array( - 'iwl_title' . $sort, - 'iwl_from' . $sort + 'iwl_from' . $sort, + 'iwl_title' . $sort )); } } else { @@ -93,9 +90,10 @@ class ApiQueryIWLinks extends ApiQueryBase { if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) { $this->addOption( 'ORDER BY', 'iwl_prefix' . $sort ); } else { - $this->addOption( 'ORDER BY', array ( + $this->addOption( 'ORDER BY', array( 'iwl_from' . $sort, - 'iwl_prefix' . $sort + 'iwl_prefix' . $sort, + 'iwl_title' . $sort )); } } @@ -187,7 +185,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' ), ) ); } @@ -197,7 +194,7 @@ class ApiQueryIWLinks extends ApiQueryBase { ); } - public function getVersion() { - return __CLASS__ . ': $Id$'; + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Iwlinks'; } } diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php index d822eed5..0ea28684 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 @@ -74,19 +72,44 @@ class ApiQueryImageInfo extends ApiQueryBase { $result = $this->getResult(); //search only inside the local repo - if( $params['localonly'] ) { + if ( $params['localonly'] ) { $images = RepoGroup::singleton()->getLocalRepo()->findFiles( $titles ); } else { $images = RepoGroup::singleton()->findFiles( $titles ); } - foreach ( $images as $img ) { - // Skip redirects - if ( $img->getOriginalTitle()->isRedirect() ) { - continue; + foreach ( $titles as $title ) { + $pageId = $pageIds[NS_FILE][$title]; + $start = $title === $fromTitle ? $fromTimestamp : $params['start']; + + if ( !isset( $images[$title] ) ) { + if ( isset( $prop['uploadwarning'] ) ) { + // Uploadwarning needs info about non-existing files + $images[$title] = wfLocalFile( $title ); + } else { + $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 +123,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 +164,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... @@ -150,9 +175,12 @@ class ApiQueryImageInfo extends ApiQueryBase { } break; } - $fit = $this->addPageSubItem( $pageId, - self::getInfo( $oldie, $prop, $result, - $finalThumbParams, $params['metadataversion'] ) ); + $fit = self::getTransformCount() < self::TRANSFORM_LIMIT && + $this->addPageSubItem( $pageId, + self::getInfo( $oldie, $prop, $result, + $finalThumbParams, $params['metadataversion'] + ) + ); if ( !$fit ) { if ( count( $pageIds[NS_FILE] ) == 1 ) { $this->setContinueEnumParameter( 'start', @@ -167,45 +195,32 @@ 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 ) { $p = $this->getModulePrefix(); - // Height and width. - if ( $params['urlheight'] != -1 && $params['urlwidth'] == -1 ) { - $this->dieUsage( "{$p}urlheight cannot be used without {$p}urlwidth", "{$p}urlwidth" ); - } - if ( $params['urlwidth'] != -1 ) { $scale = array(); $scale['width'] = $params['urlwidth']; $scale['height'] = $params['urlheight']; + } elseif ( $params['urlheight'] != -1 ) { + // Height is specified but width isn't + // Don't set $scale['width']; this signals mergeThumbParams() to fill it with the image's width + $scale = array(); + $scale['height'] = $params['urlheight']; } else { $scale = null; if ( $params['urlparam'] ) { $this->dieUsage( "{$p}urlparam requires {$p}urlwidth", "urlparam_no_width" ); } - return $scale; } return $scale; @@ -216,11 +231,25 @@ 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 ) { + protected function mergeThumbParams( $image, $thumbParams, $otherParams ) { + global $wgThumbLimits; + + if ( !isset( $thumbParams['width'] ) && isset( $thumbParams['height'] ) ) { + // We want to limit only by height in this situation, so pass the + // image's full width as the limiting width. But some file types + // don't have a width of their own, so pick something arbitrary so + // thumbnailing the default icon works. + if ( $image->getWidth() <= 0 ) { + $thumbParams['width'] = max( $wgThumbLimits ); + } else { + $thumbParams['width'] = $image->getWidth(); + } + } + if ( !$otherParams ) { return $thumbParams; } @@ -246,8 +275,8 @@ class ApiQueryImageInfo extends ApiQueryBase { if ( isset( $paramList['width'] ) ) { if ( intval( $paramList['width'] ) != intval( $thumbParams['width'] ) ) { - $this->dieUsage( "{$p}urlparam had width of {$paramList['width']} but " - . "{$p}urlwidth was {$thumbParams['width']}", "urlparam_urlwidth_mismatch" ); + $this->setWarning( "Ignoring width value set in {$p}urlparam ({$paramList['width']}) " + . "in favor of width value derived from {$p}urlwidth/{$p}urlheight ({$thumbParams['width']})" ); } } @@ -264,10 +293,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' ) { @@ -334,6 +363,7 @@ class ApiQueryImageInfo extends ApiQueryBase { $mediatype = isset( $prop['mediatype'] ); $archive = isset( $prop['archivename'] ); $bitdepth = isset( $prop['bitdepth'] ); + $uploadwarning = isset( $prop['uploadwarning'] ); if ( ( $url || $sha1 || $meta || $mime || $mediatype || $archive || $bitdepth ) && $file->isDeleted( File::DELETED_FILE ) ) { @@ -346,6 +376,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 +391,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 +408,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; @@ -400,10 +433,25 @@ class ApiQueryImageInfo extends ApiQueryBase { $vals['bitdepth'] = $file->getBitDepth(); } + if ( $uploadwarning ) { + $vals['html'] = SpecialUpload::getExistsWarning( UploadBase::getExistsWarning( $file ) ); + } + return $vals; } /** + * 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 +480,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()->getDBkey() . '|' . $start; } public function getAllowedParams() { @@ -494,6 +545,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 = '' ) { @@ -511,9 +563,10 @@ class ApiQueryImageInfo extends ApiQueryBase { 'thumbmime' => ' thumbmime - Adds MIME type of the image thumbnail' . ' (requires url and param ' . $modulePrefix . 'urlwidth)', 'mediatype' => ' mediatype - Adds the media type of the image', - 'metadata' => ' metadata - Lists EXIF metadata for the version of the image', + 'metadata' => ' metadata - Lists Exif metadata for the version of the image', 'archivename' => ' archivename - Adds the file name of the archive version for non-latest versions', 'bitdepth' => ' bitdepth - Adds the bit depth of the version', + 'uploadwarning' => ' uploadwarning - Used by the Special:Upload page to get information about an existing file. Not intended for use outside MediaWiki core', ); } @@ -521,7 +574,7 @@ class ApiQueryImageInfo extends ApiQueryBase { * Returns the descriptions for the properties provided by getPropertyNames() * * @param array $filter List of properties to filter out - * + * @param string $modulePrefix * @return array */ public static function getPropertyDescriptions( $filter = array(), $modulePrefix = '' ) { @@ -540,11 +593,12 @@ 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' ), - 'urlheight' => "Similar to {$p}urlwidth. Cannot be used without {$p}urlwidth", + 'For performance reasons if this option is used, ' . + 'no more than ' . self::TRANSFORM_LIMIT . ' scaled images will be returned.' ), + 'urlheight' => "Similar to {$p}urlwidth.", 'urlparam' => array( "A handler specific parameter string. For example, pdf's ", "might use 'page15-100px'. {$p}urlwidth must be used and be consistent with {$p}urlparam" ), - 'limit' => 'How many image revisions to return', + 'limit' => 'How many image revisions to return per image', 'start' => 'Timestamp to start listing from', 'end' => 'Timestamp to stop listing at', 'metadataversion' => array( "Version of metadata to use. if 'latest' is specified, use latest version.", @@ -578,6 +632,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 +696,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,8 +742,6 @@ 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 " . - "match the one in {$p}urlwidth" ), ) ); } @@ -687,8 +755,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..017684ed 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; @@ -54,11 +56,11 @@ class ApiQueryInfo extends ApiQueryBase { * @return void */ public function requestExtraData( $pageSet ) { - global $wgDisableCounters; + global $wgDisableCounters, $wgContentHandlerUseDB; $pageSet->requestField( 'page_restrictions' ); // when resolving redirects, no page will have this field - if( !$pageSet->isResolvingRedirects() ) { + if ( !$pageSet->isResolvingRedirects() ) { $pageSet->requestField( 'page_is_redirect' ); } $pageSet->requestField( 'page_is_new' ); @@ -68,6 +70,9 @@ class ApiQueryInfo extends ApiQueryBase { $pageSet->requestField( 'page_touched' ); $pageSet->requestField( 'page_latest' ); $pageSet->requestField( 'page_len' ); + if ( $wgContentHandlerUseDB ) { + $pageSet->requestField( 'page_content_model' ); + } } /** @@ -96,7 +101,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 +123,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 +137,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 +151,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 +165,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 +179,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 +198,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 +212,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 +226,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 +240,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 +253,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 +274,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 +311,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 +324,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 +342,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 */ @@ -343,13 +351,17 @@ class ApiQueryInfo extends ApiQueryBase { $titleExists = $pageid > 0; //$title->exists() needs pageid, which is not set for all title objects $ns = $title->getNamespace(); $dbkey = $title->getDBkey(); + + $pageInfo['contentmodel'] = $title->getContentModel(); + $pageInfo['pagelanguage'] = $title->getPageLanguage()->getCode(); + if ( $titleExists ) { global $wgDisableCounters; $pageInfo['touched'] = wfTimestamp( TS_ISO_8601, $this->pageTouched[$pageid] ); $pageInfo['lastrevid'] = intval( $this->pageLatest[$pageid] ); $pageInfo['counter'] = $wgDisableCounters - ? "" + ? '' : intval( $this->pageCounter[$pageid] ); $pageInfo['length'] = intval( $this->pageLength[$pageid] ); @@ -387,6 +399,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 +414,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 +426,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 +470,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, @@ -462,7 +483,7 @@ class ApiQueryInfo extends ApiQueryBase { $this->protections[$title->getNamespace()][$title->getDBkey()][] = $a; } // Also check old restrictions - foreach( $this->titles as $pageId => $title ) { + foreach ( $this->titles as $pageId => $title ) { if ( $this->pageRestrictions[$pageId] ) { $namespace = $title->getNamespace(); $dbKey = $title->getDBkey(); @@ -585,6 +606,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 ) { @@ -647,7 +669,9 @@ class ApiQueryInfo extends ApiQueryBase { private function getWatchedInfo() { $user = $this->getUser(); - if ( $user->isAnon() || count( $this->everything ) == 0 ) { + if ( $user->isAnon() || count( $this->everything ) == 0 + || !$user->isAllowed( 'viewmywatchlist' ) + ) { return; } @@ -678,6 +702,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 +773,7 @@ class ApiQueryInfo extends ApiQueryBase { 'protection', 'talkid', 'watched', # private + 'watchers', # private 'notificationtimestamp', # private 'subjectid', 'url', @@ -734,6 +799,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', @@ -762,11 +828,18 @@ class ApiQueryInfo extends ApiQueryBase { 'starttimestamp' => array( ApiBase::PROP_TYPE => 'timestamp', ApiBase::PROP_NULLABLE => true - ) + ), + 'contentmodel' => 'string', ), 'watched' => array( 'watched' => 'boolean' ), + 'watchers' => array( + 'watchers' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) + ), 'notificationtimestamp' => array( 'notificationtimestamp' => array( ApiBase::PROP_TYPE => 'timestamp', @@ -809,12 +882,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 +892,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..5bd451b6 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' ? '<' : '>'; @@ -198,7 +195,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { 'prop' => array( 'Which properties to get', ' lllang - Adds the language code of the language link', - ' lltitle - Adds the title of the language ink', + ' lltitle - Adds the title of the language link', ), 'limit' => 'How many total pages to return', 'dir' => 'The direction in which to list', @@ -226,14 +223,14 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { return array( 'Find all pages that link to the given language link.', 'Can be used to find all links with a language code, or', 'all links to a title (with a given language).', - 'Using neither parameter is effectively "All Language Links"', + 'Using neither parameter is effectively "All Language Links".', + 'Note that this may not consider language links added by extensions.', ); } 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' ), ) ); } @@ -244,7 +241,7 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase { ); } - public function getVersion() { - return __CLASS__ . ': $Id$'; + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Langbacklinks'; } } diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php index 3109a090..aa796e31 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] ); @@ -70,18 +67,19 @@ class ApiQueryLangLinks extends ApiQueryBase { ); } - $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); - if ( isset( $params['lang'] ) ) { + //FIXME: (follow-up) To allow extensions to add to the language links, we need + // to load them all, add the extra links, then apply paging. + // Should not be terrible, it's not going to be more than a few hundred links. + + // Note that, since (ll_from, ll_lang) is a unique key, we don't need + // to sort by ll_title to ensure deterministic ordering. + $sort = ( $params['dir'] == 'descending' ? ' DESC' : '' ); + if ( isset( $params['lang'] ) ) { $this->addWhereFld( 'll_lang', $params['lang'] ); if ( isset( $params['title'] ) ) { $this->addWhereFld( 'll_title', $params['title'] ); - $this->addOption( 'ORDER BY', 'll_from' . $sort ); - } else { - $this->addOption( 'ORDER BY', array( - 'll_title' . $sort, - 'll_from' . $sort - )); } + $this->addOption( 'ORDER BY', 'll_from' . $sort ); } else { // Don't order by ll_from if it's constant in the WHERE clause if ( count( $this->getPageSet()->getGoodTitles() ) == 1 ) { @@ -179,7 +177,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 +189,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..26774ef4 100644 --- a/includes/api/ApiQueryLogEvents.php +++ b/includes/api/ApiQueryLogEvents.php @@ -49,16 +49,16 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->fld_ids = isset( $prop['ids'] ); $this->fld_title = isset( $prop['title'] ); $this->fld_type = isset( $prop['type'] ); - $this->fld_action = isset ( $prop['action'] ); + $this->fld_action = isset( $prop['action'] ); $this->fld_user = isset( $prop['user'] ); $this->fld_userid = isset( $prop['userid'] ); $this->fld_timestamp = isset( $prop['timestamp'] ); $this->fld_comment = isset( $prop['comment'] ); - $this->fld_parsedcomment = isset ( $prop['parsedcomment'] ); + $this->fld_parsedcomment = isset( $prop['parsedcomment'] ); $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 ); } @@ -67,10 +67,10 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->addTables( array( 'logging', 'user', 'page' ) ); $this->addOption( 'STRAIGHT_JOIN' ); $this->addJoinConds( array( - 'user' => array( 'JOIN', + 'user' => array( 'LEFT 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 @@ -82,8 +82,8 @@ class ApiQueryLogEvents extends ApiQueryBase { ) ); $this->addFieldsIf( array( 'log_id', 'page_id' ), $this->fld_ids ); - $this->addFieldsIf( array( 'log_user', 'user_name' ), $this->fld_user ); - $this->addFieldsIf( 'user_id', $this->fld_userid ); + $this->addFieldsIf( array( 'log_user', 'log_user_text', 'user_name' ), $this->fld_user ); + $this->addFieldsIf( 'log_user', $this->fld_userid ); $this->addFieldsIf( array( 'log_namespace', 'log_title' ), $this->fld_title || $this->fld_parsedcomment ); $this->addFieldsIf( 'log_comment', $this->fld_comment || $this->fld_parsedcomment ); $this->addFieldsIf( 'log_params', $this->fld_details ); @@ -98,8 +98,7 @@ class ApiQueryLogEvents extends ApiQueryBase { $this->addTables( 'change_tag' ); $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'log_id=ct_log_id' ) ) ) ); $this->addWhereFld( 'ct_tag', $params['tag'] ); - global $wgOldChangeTagsIndex; - $index['change_tag'] = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id'; + $index['change_tag'] = 'change_tag_tag_id'; } if ( !is_null( $params['action'] ) ) { @@ -151,7 +150,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 +200,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 +208,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 +240,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; @@ -260,11 +264,26 @@ class ApiQueryLogEvents extends ApiQueryBase { $vals[$type] = $vals2; $params = null; break; + case 'upload': + if ( isset( $params['img_timestamp'] ) ) { + $params['img_timestamp'] = wfTimestamp( TS_ISO_8601, $params['img_timestamp'] ); + } + 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; } @@ -275,18 +294,22 @@ class ApiQueryLogEvents extends ApiQueryBase { if ( $this->fld_ids ) { $vals['logid'] = intval( $row->log_id ); - $vals['pageid'] = intval( $row->page_id ); } if ( $this->fld_title || $this->fld_parsedcomment ) { $title = Title::makeTitle( $row->log_namespace, $row->log_title ); } - if ( $this->fld_title ) { + if ( $this->fld_title || $this->fld_ids ) { if ( LogEventsList::isDeleted( $row, LogPage::DELETED_ACTION ) ) { $vals['actionhidden'] = ''; } else { - ApiQueryBase::addTitleInfo( $vals, $title ); + if ( $this->fld_title ) { + ApiQueryBase::addTitleInfo( $vals, $title ); + } + if ( $this->fld_ids ) { + $vals['pageid'] = intval( $row->page_id ); + } } } @@ -316,10 +339,10 @@ class ApiQueryLogEvents extends ApiQueryBase { $vals['userhidden'] = ''; } else { if ( $this->fld_user ) { - $vals['user'] = $row->user_name; + $vals['user'] = $row->user_name === null ? $row->log_user_text : $row->user_name; } if ( $this->fld_userid ) { - $vals['userid'] = $row->user_id; + $vals['userid'] = $row->log_user; } if ( !$row->log_user ) { @@ -362,8 +385,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 +459,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 +553,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..a23ff06b --- /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..6f2f02e4 --- /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..222ad074 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; } @@ -157,7 +157,7 @@ class ApiQueryProtectedTitles extends ApiQueryGeneratorBase { ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => array_diff( $wgRestrictionLevels, array( '' ) ) ), - 'limit' => array ( + 'limit' => array( ApiBase::PARAM_DFLT => 10, ApiBase::PARAM_TYPE => 'limit', ApiBase::PARAM_MIN => 1, @@ -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..79fe0498 100644 --- a/includes/api/ApiQueryQueryPage.php +++ b/includes/api/ApiQueryQueryPage.php @@ -32,27 +32,18 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { private $qpMap; - /** - * Some query pages are useless because they're available elsewhere in the API - */ - private $uselessQueryPages = array( - 'MIMEsearch', // aiprop=mime - 'LinkSearch', // list=exturlusage - 'FileDuplicateSearch', // prop=duplicatefiles - ); - public function __construct( $query, $moduleName ) { parent::__construct( $query, $moduleName, 'qp' ); // We need to do this to make sure $wgQueryPages is set up // This SUCKS global $IP; - require_once( "$IP/includes/QueryPage.php" ); + require_once "$IP/includes/QueryPage.php"; // Build mapping from special page names to QueryPage classes - global $wgQueryPages; + global $wgQueryPages, $wgAPIUselessQueryPages; $this->qpMap = array(); foreach ( $wgQueryPages as $page ) { - if( !in_array( $page[1], $this->uselessQueryPages ) ) { + if ( !in_array( $page[1], $wgAPIUselessQueryPages ) ) { $this->qpMap[$page[1]] = $page[0]; } } @@ -75,6 +66,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 +133,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 +204,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { public function getPossibleErrors() { return array_merge( parent::getPossibleErrors(), array( - array( 'specialpage-cantexecute' ) + array( 'specialpage-cantexecute' ) ) ); } @@ -221,7 +214,7 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase { ); } - public function getVersion() { - return __CLASS__ . ': $Id$'; + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Querypage'; } } diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php index ddf5841b..2754bdae 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' ); } @@ -184,7 +186,7 @@ class ApiQueryRandom extends ApiQueryGeneratorBase { return 'api.php?action=query&list=random&rnnamespace=0&rnlimit=2'; } - public function getVersion() { - return __CLASS__ . ': $Id: ApiQueryRandom.php overlordq$'; + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Random'; } } diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php index 7ae4f371..6b10bdc6 100644 --- a/includes/api/ApiQueryRecentChanges.php +++ b/includes/api/ApiQueryRecentChanges.php @@ -39,7 +39,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { private $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false, $fld_flags = false, $fld_timestamp = false, $fld_title = false, $fld_ids = false, $fld_sizes = false, $fld_redirect = false, $fld_patrolled = false, $fld_loginfo = false, - $fld_tags = false, $token = array(); + $fld_tags = false, $fld_sha1 = false, $token = array(); private $tokenFunctions; @@ -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'] ); @@ -121,6 +121,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $this->fld_patrolled = isset( $prop['patrolled'] ); $this->fld_loginfo = isset( $prop['loginfo'] ); $this->fld_tags = isset( $prop['tags'] ); + $this->fld_sha1 = isset( $prop['sha1'] ); } public function execute() { @@ -149,6 +150,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 +240,6 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { 'rc_title', 'rc_cur_id', 'rc_type', - 'rc_moved_to_ns', - 'rc_moved_to_title', 'rc_deleted' ) ); @@ -231,12 +255,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 ); @@ -249,6 +274,12 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { $this->addFields( 'ts_tags' ); } + if ( $this->fld_sha1 ) { + $this->addTables( 'revision' ); + $this->addJoinConds( array( 'revision' => array( 'LEFT JOIN', array( 'rc_this_oldid=rev_id' ) ) ) ); + $this->addFields( array( 'rev_sha1', 'rev_deleted' ) ); + } + if ( $params['toponly'] || $showRedirects ) { $this->addTables( 'page' ); $this->addJoinConds( array( 'page' => array( 'LEFT JOIN', array( 'rc_namespace=page_namespace', 'rc_title=page_title' ) ) ) ); @@ -262,9 +293,8 @@ 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'] ); - global $wgOldChangeTagsIndex; - $index['change_tag'] = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id'; + $this->addWhereFld( 'ct_tag', $params['tag'] ); + $index['change_tag'] = 'change_tag_tag_id'; } $this->token = $params['token']; @@ -283,7 +313,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 +327,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 +346,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 +373,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 +386,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. */ @@ -457,6 +481,19 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } } + if ( $this->fld_sha1 && $row->rev_sha1 !== null ) { + // The RevDel check should currently never pass due to the + // rc_deleted = 0 condition in the WHERE clause, but in case that + // ever changes we check it here too. + if ( $row->rev_deleted & Revision::DELETED_TEXT ) { + $vals['sha1hidden'] = ''; + } elseif ( $row->rev_sha1 !== '' ) { + $vals['sha1'] = wfBaseConvert( $row->rev_sha1, 36, 16, 40 ); + } else { + $vals['sha1'] = ''; + } + } + if ( !is_null( $this->token ) ) { $tokenFunctions = $this->getTokenFunctions(); foreach ( $this->token as $t ) { @@ -481,13 +518,15 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { } return $retval; } - switch( $type ) { + switch ( $type ) { case 'edit': return RC_EDIT; case 'new': return RC_NEW; case 'log': return RC_LOG; + case 'external': + return RC_EXTERNAL; } } @@ -551,7 +590,8 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { 'redirect', 'patrolled', 'loginfo', - 'tags' + 'tags', + 'sha1', ) ), 'token' => array( @@ -584,11 +624,13 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => array( 'edit', + 'external', 'new', 'log' ) ), 'toponly' => false, + 'continue' => null, ); } @@ -616,6 +658,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ' patrolled - Tags edits that have been patrolled', ' loginfo - Adds log information (logid, logtype, etc) to log entries', ' tags - Lists tags for the entry', + ' sha1 - Adds the content checksum for entries associated with a revision', ), 'token' => 'Which tokens to obtain for each change', 'show' => array( @@ -626,6 +669,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', ); } @@ -712,7 +756,17 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase { ApiBase::PROP_TYPE => 'string', ApiBase::PROP_NULLABLE => true ) - ) + ), + 'sha1' => array( + 'sha1' => array( + ApiBase::PROP_TYPE => 'string', + ApiBase::PROP_NULLABLE => true + ), + 'sha1hidden' => array( + ApiBase::PROP_TYPE => 'boolean', + ApiBase::PROP_NULLABLE => true + ), + ), ); self::addTokenProperties( $props, $this->getTokenFunctions() ); @@ -741,8 +795,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..415288ef 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(); @@ -147,23 +146,28 @@ class ApiQueryRevisions extends ApiQueryBase { $prop = array_flip( $params['prop'] ); // Optional fields - $this->fld_ids = isset ( $prop['ids'] ); + $this->fld_ids = isset( $prop['ids'] ); // $this->addFieldsIf('rev_text_id', $this->fld_ids); // should this be exposed? - $this->fld_flags = isset ( $prop['flags'] ); - $this->fld_timestamp = isset ( $prop['timestamp'] ); - $this->fld_comment = isset ( $prop['comment'] ); - $this->fld_parsedcomment = isset ( $prop['parsedcomment'] ); - $this->fld_size = isset ( $prop['size'] ); - $this->fld_sha1 = isset ( $prop['sha1'] ); + $this->fld_flags = isset( $prop['flags'] ); + $this->fld_timestamp = isset( $prop['timestamp'] ); + $this->fld_comment = isset( $prop['comment'] ); + $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->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,16 @@ 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'] ); - global $wgOldChangeTagsIndex; - $index['change_tag'] = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id'; + $this->addWhereFld( 'ct_tag', $params['tag'] ); + $index['change_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 +260,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 +337,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 +435,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 +487,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 +641,7 @@ class ApiQueryRevisions extends ApiQueryBase { 'userid', 'size', 'sha1', + 'contentmodel', 'comment', 'parsedcomment', 'content', @@ -616,6 +691,10 @@ class ApiQueryRevisions extends ApiQueryBase { 'continue' => null, 'diffto' => null, 'difftotext' => null, + 'contentformat' => array( + ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(), + ApiBase::PARAM_DFLT => null + ), ); } @@ -631,6 +710,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 +724,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 +736,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 +791,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 +818,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 +853,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..36b55979 100644 --- a/includes/api/ApiQuerySearch.php +++ b/includes/api/ApiQuerySearch.php @@ -31,6 +31,14 @@ */ class ApiQuerySearch extends ApiQueryGeneratorBase { + /** + * When $wgSearchType is null, $wgSearchAlternatives[0] is null. Null isn't + * a valid option for an array for PARAM_TYPE, so we'll use a fake name + * that can't possibly be a class name and describes what the null behavior + * does + */ + const BACKEND_NULL_PARAM = 'database-backed'; + public function __construct( $query, $moduleName ) { parent::__construct( $query, $moduleName, 'sr' ); } @@ -59,7 +67,8 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { $prop = array_flip( $params['prop'] ); // Create search engine instance and set options - $search = SearchEngine::create(); + $search = isset( $params['backend'] ) && $params['backend'] != self::BACKEND_NULL_PARAM ? + SearchEngine::create( $params['backend'] ) : SearchEngine::create(); $search->setLimitOffset( $limit + 1, $params['offset'] ); $search->setNamespaces( $params['namespace'] ); $search->showRedirects = $params['redirects']; @@ -93,6 +102,8 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } if ( is_null( $matches ) ) { $this->dieUsage( "{$what} search is disabled", "search-{$what}-disabled" ); + } elseif ( $matches instanceof Status && !$matches->isGood() ) { + $this->dieUsage( $matches->getWikiText(), 'search-error' ); } $apiResult = $this->getResult(); @@ -168,7 +179,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } } if ( isset( $prop['hasrelated'] ) && $result->hasRelated() ) { - $vals['hasrelated'] = ""; + $vals['hasrelated'] = ''; } // Add item to results and see whether it fits @@ -199,13 +210,15 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { } public function getAllowedParams() { - return array( + global $wgSearchType; + + $params = array( 'search' => array( ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true ), 'namespace' => array( - ApiBase::PARAM_DFLT => 0, + ApiBase::PARAM_DFLT => NS_MAIN, ApiBase::PARAM_TYPE => 'namespace', ApiBase::PARAM_ISMULTI => true, ), @@ -252,10 +265,23 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { ApiBase::PARAM_MAX2 => ApiBase::LIMIT_SML2 ) ); + + $alternatives = SearchEngine::getSearchTypes(); + if ( count( $alternatives ) > 1 ) { + if ( $alternatives[0] === null ) { + $alternatives[0] = self::BACKEND_NULL_PARAM; + } + $params['backend'] = array( + ApiBase::PARAM_DFLT => $wgSearchType, + ApiBase::PARAM_TYPE => $alternatives, + ); + } + + return $params; } public function getParamDescription() { - return array( + $descriptions = array( 'search' => 'Search for all page titles (or content) that has this value', 'namespace' => 'The namespace(s) to enumerate', 'what' => 'Search inside the text or titles', @@ -278,6 +304,12 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { 'offset' => 'Use this value to continue paging (return by query)', 'limit' => 'How many total pages to return' ); + + if ( count( SearchEngine::getSearchTypes() ) > 1 ) { + $descriptions['backend'] = 'Which search backend to use, if not the default'; + } + + return $descriptions; } public function getResultProperties() { @@ -345,6 +377,7 @@ class ApiQuerySearch extends ApiQueryGeneratorBase { return array_merge( parent::getPossibleErrors(), array( array( 'code' => 'search-text-disabled', 'info' => 'text search is disabled' ), array( 'code' => 'search-title-disabled', 'info' => 'title search is disabled' ), + array( 'code' => 'search-error', 'info' => 'search error has occurred' ), ) ); } @@ -359,8 +392,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..a7767062 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,19 +114,57 @@ class ApiQuerySiteinfo extends ApiQueryBase { } protected function appendGeneralInfo( $property ) { - global $wgContLang; + global $wgContLang, + $wgDisableLangConversion, + $wgDisableTitleConversion; $data = array(); $mainPage = Title::newMainPage(); $data['mainpage'] = $mainPage->getPrefixedText(); - $data['base'] = wfExpandUrl( $mainPage->getFullUrl(), PROTO_CURRENT ); + $data['base'] = wfExpandUrl( $mainPage->getFullURL(), PROTO_CURRENT ); $data['sitename'] = $GLOBALS['wgSitename']; + $data['logo'] = $GLOBALS['wgLogo']; $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(); + $allowFrom = array( '' ); + $allowException = true; + if ( !$GLOBALS['wgAllowExternalImages'] ) { + if ( $GLOBALS['wgEnableImageWhitelist'] ) { + $data['imagewhitelistenabled'] = ''; + } + $allowFrom = $GLOBALS['wgAllowExternalImagesFrom']; + $allowException = !empty( $allowFrom ); + } + if ( $allowException ) { + $data['externalimages'] = (array)$allowFrom; + $this->getResult()->setIndexedTagName( $data['externalimages'], 'prefix' ); + } + + 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; @@ -144,15 +185,15 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['lang'] = $GLOBALS['wgLanguageCode']; $fallbacks = array(); - foreach( $wgContLang->getFallbackLanguages() as $code ) { + foreach ( $wgContLang->getFallbackLanguages() as $code ) { $fallbacks[] = array( 'code' => $code ); } $data['fallback'] = $fallbacks; $this->getResult()->setIndexedTagName( $data['fallback'], 'lang' ); - if( $wgContLang->hasVariants() ) { + if ( $wgContLang->hasVariants() ) { $variants = array(); - foreach( $wgContLang->getVariants() as $code ) { + foreach ( $wgContLang->getVariants() as $code ) { $variants[] = array( 'code' => $code ); } $data['variants'] = $variants; @@ -227,6 +268,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' ); @@ -250,6 +296,8 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data[] = $item; } + sort( $data ); + $this->getResult()->setIndexedTagName( $data, 'ns' ); return $this->getResult()->addValue( 'query', $property, $data ); } @@ -314,10 +362,10 @@ class ApiQuerySiteinfo extends ApiQueryBase { $val['language'] = $langNames[$prefix]; } $val['url'] = wfExpandUrl( $row['iw_url'], PROTO_CURRENT ); - if( isset( $row['iw_wikiid'] ) ) { + if ( isset( $row['iw_wikiid'] ) ) { $val['wikiid'] = $row['iw_wikiid']; } - if( isset( $row['iw_api'] ) ) { + if ( isset( $row['iw_api'] ) ) { $val['api'] = $row['iw_api']; } @@ -345,7 +393,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { ); } } else { - list( $host, $lag, $index ) = $lb->getMaxLag(); + list( , $lag, $index ) = $lb->getMaxLag(); $data[] = array( 'host' => $wgShowHostnames ? $lb->getServerName( $index ) @@ -373,11 +421,15 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data['activeusers'] = intval( SiteStats::activeUsers() ); $data['admins'] = intval( SiteStats::numberingroup( 'sysop' ) ); $data['jobs'] = intval( SiteStats::jobs() ); + + wfRunHooks( 'APIQuerySiteInfoStatisticsInfo', array( &$data ) ); + return $this->getResult()->addValue( 'query', $property, $data ); } protected function appendUserGroups( $property, $numberInGroup ) { - global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups, $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; + global $wgGroupPermissions, $wgAddGroups, $wgRemoveGroups; + global $wgGroupsAddToSelf, $wgGroupsRemoveFromSelf; $data = array(); $result = $this->getResult(); @@ -425,7 +477,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { global $wgFileExtensions; $data = array(); - foreach ( $wgFileExtensions as $ext ) { + foreach ( array_unique( $wgFileExtensions ) as $ext ) { $data[] = array( 'ext' => $ext ); } $this->getResult()->setIndexedTagName( $data, 'fe' ); @@ -457,7 +509,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']; @@ -489,7 +541,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { $data = array( 'url' => $url ? $url : '', - 'text' => $text ? $text : '' + 'text' => $text ? $text : '' ); return $this->getResult()->addValue( 'query', $property, $data ); @@ -513,9 +565,17 @@ class ApiQuerySiteinfo extends ApiQueryBase { public function appendSkins( $property ) { $data = array(); + $usable = Skin::getUsableSkins(); + $default = Skin::normalizeKey( 'default' ); foreach ( Skin::getSkinNames() as $name => $displayName ) { $skin = array( 'code' => $name ); ApiResult::setContent( $skin, $displayName ); + if ( !isset( $usable[$name] ) ) { + $skin['unusable'] = ''; + } + if ( $name === $default ) { + $skin['default'] = ''; + } $data[] = $skin; } $this->getResult()->setIndexedTagName( $data, 'skin' ); @@ -525,7 +585,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 +604,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 +622,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 +664,7 @@ class ApiQuerySiteinfo extends ApiQueryBase { 'functionhooks', 'showhooks', 'variables', + 'protocols', ) ), 'filteriw' => array( @@ -621,20 +690,23 @@ class ApiQuerySiteinfo extends ApiQueryBase { ' specialpagealiases - List of special page aliases', ' magicwords - List of magic words and their aliases', ' statistics - Returns site statistics', - " interwikimap - Returns interwiki map (optionally filtered, (optionally localised by using {$p}inlanguagecode))", + " interwikimap - Returns interwiki map " . + "(optionally filtered, (optionally localised by using {$p}inlanguagecode))", ' dbrepllag - Returns database server with the highest replication lag', ' usergroups - Returns user groups and the associated permissions', ' extensions - Returns extensions installed on the wiki', ' fileextensions - Returns list of file extensions allowed to be uploaded', ' rightsinfo - Returns wiki rights (license) information if available', - " languages - Returns a list of languages MediaWiki supports (optionally localised by using {$p}inlanguagecode)", + " languages - Returns a list of languages MediaWiki supports" . + "(optionally localised by using {$p}inlanguagecode)", ' skins - Returns a list of all enabled skins', ' extensiontags - Returns a list of parser extension tags', ' functionhooks - Returns a list of parser function hooks', ' showhooks - Returns a list of all subscribed hooks (contents of $wgHooks)', ' 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', + 'filteriw' => 'Return only local or only nonlocal entries of the interwiki map', 'showalldb' => 'List all database servers, not just the one lagging the most', 'numberingroup' => 'Lists the number of users in user groups', 'inlanguagecode' => 'Language code for localised language names (best effort, use CLDR extension)', @@ -646,9 +718,10 @@ class ApiQuerySiteinfo extends ApiQueryBase { } public function getPossibleErrors() { - return array_merge( parent::getPossibleErrors(), array( - array( 'code' => 'includeAllDenied', 'info' => 'Cannot view all servers info unless $wgShowHostnames is true' ), - ) ); + return array_merge( parent::getPossibleErrors(), array( array( + 'code' => 'includeAllDenied', + 'info' => 'Cannot view all servers info unless $wgShowHostnames is true' + ), ) ); } public function getExamples() { @@ -662,8 +735,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..732df9a4 100644 --- a/includes/api/ApiQueryTags.php +++ b/includes/api/ApiQueryTags.php @@ -133,8 +133,7 @@ class ApiQueryTags extends ApiQueryBase { public function getAllowedParams() { return array( - 'continue' => array( - ), + 'continue' => null, 'limit' => array( ApiBase::PARAM_DFLT => 10, ApiBase::PARAM_TYPE => 'limit', @@ -162,7 +161,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', ), @@ -196,7 +195,7 @@ class ApiQueryTags extends ApiQueryBase { ); } - public function getVersion() { - return __CLASS__ . ': $Id$'; + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Tags'; } } diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php index f30b1325..9a9be7b2 100644 --- a/includes/api/ApiQueryUserContributions.php +++ b/includes/api/ApiQueryUserContributions.php @@ -48,7 +48,7 @@ class ApiQueryContributions extends ApiQueryBase { $this->fld_ids = isset( $prop['ids'] ); $this->fld_title = isset( $prop['title'] ); $this->fld_comment = isset( $prop['comment'] ); - $this->fld_parsedcomment = isset ( $prop['parsedcomment'] ); + $this->fld_parsedcomment = isset( $prop['parsedcomment'] ); $this->fld_size = isset( $prop['size'] ); $this->fld_sizediff = isset( $prop['sizediff'] ); $this->fld_flags = isset( $prop['flags'] ); @@ -83,10 +83,10 @@ class ApiQueryContributions extends ApiQueryBase { // Do the actual query. $res = $this->select( __METHOD__ ); - if( $this->fld_sizediff ) { + if ( $this->fld_sizediff ) { $revIds = array(); foreach ( $res as $row ) { - if( $row->rev_parent_id ) { + if ( $row->rev_parent_id ) { $revIds[] = $row->rev_parent_id; } } @@ -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' ); } @@ -258,7 +255,7 @@ class ApiQueryContributions extends ApiQueryBase { $this->addFieldsIf( 'rev_comment', $this->fld_comment || $this->fld_parsedcomment ); $this->addFieldsIf( 'rev_len', $this->fld_size || $this->fld_sizediff ); $this->addFieldsIf( 'rev_minor_edit', $this->fld_flags ); - $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff ); + $this->addFieldsIf( 'rev_parent_id', $this->fld_flags || $this->fld_sizediff || $this->fld_ids ); $this->addFieldsIf( 'rc_patrolled', $this->fld_patrolled ); if ( $this->fld_tags ) { @@ -271,8 +268,7 @@ class ApiQueryContributions extends ApiQueryBase { $this->addTables( 'change_tag' ); $this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'rev_id=ct_rev_id' ) ) ) ); $this->addWhereFld( 'ct_tag', $this->params['tag'] ); - global $wgOldChangeTagsIndex; - $index['change_tag'] = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id'; + $index['change_tag'] = 'change_tag_tag_id'; } if ( $this->params['toponly'] ) { @@ -300,6 +296,10 @@ class ApiQueryContributions extends ApiQueryBase { $vals['pageid'] = intval( $row->rev_page ); $vals['revid'] = intval( $row->rev_id ); // $vals['textid'] = intval( $row->rev_text_id ); // todo: Should this field be exposed? + + if ( !is_null( $row->rev_parent_id ) ) { + $vals['parentid'] = intval( $row->rev_parent_id ); + } } $title = Title::makeTitle( $row->page_namespace, $row->page_title ); @@ -445,7 +445,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( @@ -477,7 +477,11 @@ class ApiQueryContributions extends ApiQueryBase { ), 'ids' => array( 'pageid' => 'integer', - 'revid' => 'integer' + 'revid' => 'integer', + 'parentid' => array( + ApiBase::PROP_TYPE => 'integer', + ApiBase::PROP_NULLABLE => true + ) ), 'title' => array( 'ns' => 'namespace', @@ -546,8 +550,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..3c85ea69 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'] ) ) { @@ -104,12 +104,15 @@ class ApiQueryUserInfo extends ApiQueryBase { } if ( isset( $this->prop['preferencestoken'] ) && - is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) + is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) && + $user->isAllowed( 'editmyoptions' ) ) { $vals['preferencestoken'] = $user->getEditToken( '', $this->getMain()->getRequest() ); } if ( isset( $this->prop['editcount'] ) ) { + // use intval to prevent null if a non-logged-in user calls + // api.php?format=jsonfm&action=query&meta=userinfo&uiprop=editcount $vals['editcount'] = intval( $user->getEditCount() ); } @@ -121,11 +124,13 @@ class ApiQueryUserInfo extends ApiQueryBase { $vals['realname'] = $user->getRealName(); } - if ( isset( $this->prop['email'] ) ) { - $vals['email'] = $user->getEmail(); - $auth = $user->getEmailAuthenticationTimestamp(); - if ( !is_null( $auth ) ) { - $vals['emailauthenticated'] = wfTimestamp( TS_ISO_8601, $auth ); + if ( $user->isAllowed( 'viewmyprivateinfo' ) ) { + if ( isset( $this->prop['email'] ) ) { + $vals['email'] = $user->getEmail(); + $auth = $user->getEmailAuthenticationTimestamp(); + if ( !is_null( $auth ) ) { + $vals['emailauthenticated'] = wfTimestamp( TS_ISO_8601, $auth ); + } } } @@ -167,8 +172,9 @@ class ApiQueryUserInfo extends ApiQueryBase { if ( $user->isNewbie() ) { $categories[] = 'ip'; $categories[] = 'subnet'; - if ( !$user->isAnon() ) + if ( !$user->isAnon() ) { $categories[] = 'newbie'; + } } $categories = array_merge( $categories, $user->getGroups() ); @@ -303,8 +309,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..dccfee67 100644 --- a/includes/api/ApiQueryUsers.php +++ b/includes/api/ApiQueryUsers.php @@ -110,26 +110,46 @@ 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(); $data[$name]['name'] = $name; if ( isset( $this->prop['editcount'] ) ) { - $data[$name]['editcount'] = intval( $user->getEditCount() ); + $data[$name]['editcount'] = $user->getEditCount(); } if ( isset( $this->prop['registration'] ) ) { @@ -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'] = ''; @@ -198,11 +204,13 @@ class ApiQueryUsers extends ApiQueryBase { } } + $context = $this->getContext(); // Second pass: add result data to $retval foreach ( $goodNames as $u ) { if ( !isset( $data[$u] ) ) { $data[$u] = array( 'name' => $u ); $urPage = new UserrightsPage; + $urPage->setContext( $context ); $iwUser = $urPage->fetchUser( $u ); if ( $iwUser instanceof UserRightsProxy ) { @@ -244,16 +252,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 +312,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 +398,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..22843f50 100644 --- a/includes/api/ApiQueryWatchlist.php +++ b/includes/api/ApiQueryWatchlist.php @@ -68,7 +68,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->fld_user = isset( $prop['user'] ); $this->fld_userid = isset( $prop['userid'] ); $this->fld_comment = isset( $prop['comment'] ); - $this->fld_parsedcomment = isset ( $prop['parsedcomment'] ); + $this->fld_parsedcomment = isset( $prop['parsedcomment'] ); $this->fld_timestamp = isset( $prop['timestamp'] ); $this->fld_sizes = isset( $prop['sizes'] ); $this->fld_patrol = isset( $prop['patrol'] ); @@ -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', @@ -135,7 +135,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { if ( !$params['allrev'] ) { $this->addTables( 'page' ); - $this->addJoinConds( array( 'page' => array( 'LEFT JOIN','rc_cur_id=page_id' ) ) ); + $this->addJoinConds( array( 'page' => array( 'LEFT JOIN', 'rc_cur_id=page_id' ) ) ); $this->addWhere( 'rc_this_oldid=page_latest OR rc_type=' . RC_LOG ); } @@ -143,12 +143,11 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $show = array_flip( $params['show'] ); /* Check for conflicting parameters. */ - if ( ( isset ( $show['minor'] ) && isset ( $show['!minor'] ) ) - || ( isset ( $show['bot'] ) && isset ( $show['!bot'] ) ) - || ( isset ( $show['anon'] ) && isset ( $show['!anon'] ) ) - || ( isset ( $show['patrolled'] ) && isset ( $show['!patrolled'] ) ) - ) - { + if ( ( isset( $show['minor'] ) && isset( $show['!minor'] ) ) + || ( isset( $show['bot'] ) && isset( $show['!bot'] ) ) + || ( isset( $show['anon'] ) && isset( $show['!anon'] ) ) + || ( isset( $show['patrolled'] ) && isset( $show['!patrolled'] ) ) + ) { $this->dieUsageMsg( 'show' ); } @@ -171,6 +170,10 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { $this->addWhereIf( 'rc_patrolled != 0', isset( $show['patrolled'] ) ); } + if ( !is_null( $params['type'] ) ) { + $this->addWhereFld( 'rc_type', $this->parseRCType( $params['type'] ) ); + } + if ( !is_null( $params['user'] ) && !is_null( $params['excludeuser'] ) ) { $this->dieUsage( 'user and excludeuser cannot be used together', 'user-excludeuser' ); } @@ -226,6 +229,32 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { private function extractRowInfo( $row ) { $vals = array(); + $type = intval( $row->rc_type ); + + /* Determine what kind of change this was. */ + switch ( $type ) { + case RC_EDIT: + $vals['type'] = 'edit'; + break; + case RC_NEW: + $vals['type'] = 'new'; + break; + case RC_MOVE: + $vals['type'] = 'move'; + break; + case RC_LOG: + $vals['type'] = 'log'; + break; + case RC_EXTERNAL: + $vals['type'] = 'external'; + break; + case RC_MOVE_OVER_REDIRECT: + $vals['type'] = 'move over redirect'; + break; + default: + $vals['type'] = $type; + } + if ( $this->fld_ids ) { $vals['pageid'] = intval( $row->rc_cur_id ); $vals['revid'] = intval( $row->rc_this_oldid ); @@ -240,14 +269,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'] = ''; } @@ -310,6 +341,27 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { return $vals; } + /* Copied from ApiQueryRecentChanges. */ + private function parseRCType( $type ) { + if ( is_array( $type ) ) { + $retval = array(); + foreach ( $type as $t ) { + $retval[] = $this->parseRCType( $t ); + } + return $retval; + } + switch ( $type ) { + case 'edit': + return RC_EDIT; + case 'new': + return RC_NEW; + case 'log': + return RC_LOG; + case 'external': + return RC_EXTERNAL; + } + } + public function getAllowedParams() { return array( 'allrev' => false, @@ -319,7 +371,7 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'end' => array( ApiBase::PARAM_TYPE => 'timestamp' ), - 'namespace' => array ( + 'namespace' => array( ApiBase::PARAM_ISMULTI => true, ApiBase::PARAM_TYPE => 'namespace' ), @@ -374,6 +426,15 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { '!patrolled', ) ), + 'type' => array( + ApiBase::PARAM_ISMULTI => true, + ApiBase::PARAM_TYPE => array( + 'edit', + 'external', + 'new', + 'log', + ) + ), 'owner' => array( ApiBase::PARAM_TYPE => 'user' ), @@ -413,6 +474,13 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { 'Show only items that meet this criteria.', "For example, to see only minor edits done by logged-in users, set {$p}show=minor|!anon" ), + 'type' => array( + 'Which types of changes to show', + ' edit - Regular page edits', + ' external - External changes', + ' new - Page creations', + ' log - Log entries', + ), 'owner' => 'The name of the user whose watchlist you\'d like to access', 'token' => 'Give a security token (settable in preferences) to allow access to another user\'s watchlist' ); @@ -421,6 +489,17 @@ class ApiQueryWatchlist extends ApiQueryGeneratorBase { public function getResultProperties() { global $wgLogTypes; return array( + '' => array( + 'type' => array( + ApiBase::PROP_TYPE => array( + 'edit', + 'new', + 'move', + 'log', + 'move over redirect' + ) + ) + ), 'ids' => array( 'pageid' => 'integer', 'revid' => 'integer', @@ -511,15 +590,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..ea4e724a 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( @@ -225,7 +223,7 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase { ); } - public function getVersion() { - return __CLASS__ . ': $Id$'; + public function getHelpUrls() { + return 'https://www.mediawiki.org/wiki/API:Watchlistraw'; } } 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..d219c91c 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(); @@ -44,7 +40,7 @@ class ApiRsd extends ApiBase { $service = array( 'apis' => $this->formatRsdApiList() ); ApiResult::setContent( $service, 'MediaWiki', 'engineName' ); ApiResult::setContent( $service, 'https://www.mediawiki.org/', 'engineLink' ); - ApiResult::setContent( $service, Title::newMainPage()->getCanonicalUrl(), 'homePageLink' ); + ApiResult::setContent( $service, Title::newMainPage()->getCanonicalURL(), 'homePageLink' ); $result->setIndexedTagName( $service['apis'], 'api' ); @@ -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..53a68fde 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(); @@ -41,15 +39,19 @@ class ApiSetNotificationTimestamp extends ApiBase { if ( $user->isAnon() ) { $this->dieUsage( 'Anonymous users cannot use watchlist change notifications', 'notloggedin' ); } + if ( !$user->isAllowed( 'editmywatchlist' ) ) { + $this->dieUsage( 'You don\'t have permission to edit your watchlist', 'permissiondenied' ); + } $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 +98,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 +137,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 +164,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 +191,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 +207,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()->getFinalParamDescription() + 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 +264,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->getFinalPossibleErrors(), + $this->getRequireMaxOneParameterErrorMessages( + array( 'timestamp', 'torevid', 'newerthanrevid' ) ), + $this->getRequireOnlyOneParameterErrorMessages( + array_merge( array( 'entirewatchlist' ), array_keys( $ps->getFinalParams() ) ) ), array( array( 'code' => 'notloggedin', 'info' => 'Anonymous users cannot use watchlist change notifications' ), array( 'code' => 'multpages', 'info' => 'torevid may only be used with a single page' ), @@ -269,17 +288,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..d220a5e6 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,20 +45,24 @@ class ApiTokens extends ApiBase { } $this->getResult()->addValue( null, $this->getModuleName(), $res ); - wfProfileOut( __METHOD__ ); } private function getTokenTypes() { + // If we're in JSON callback mode, no tokens can be obtained + if ( !is_null( $this->getMain()->getRequest()->getVal( 'callback' ) ) ) { + return array(); + } + static $types = null; if ( $types ) { 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 +81,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 +106,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..6a739a2f 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. */ @@ -43,12 +39,6 @@ class ApiUnblock extends ApiBase { $user = $this->getUser(); $params = $this->extractRequestParams(); - if ( $params['gettoken'] ) { - $res['unblocktoken'] = $user->getEditToken(); - $this->getResult()->addValue( null, $this->getModuleName(), $res ); - return; - } - if ( is_null( $params['id'] ) && is_null( $params['user'] ) ) { $this->dieUsageMsg( 'unblock-notarget' ); } @@ -100,10 +90,6 @@ class ApiUnblock extends ApiBase { ), 'user' => null, 'token' => null, - 'gettoken' => array( - ApiBase::PARAM_DFLT => false, - ApiBase::PARAM_DEPRECATED => true, - ), 'reason' => '', ); } @@ -114,7 +100,6 @@ class ApiUnblock extends ApiBase { 'id' => "ID of the block you want to unblock (obtained through list=blocks). Cannot be used together with {$p}user", 'user' => "Username, IP address or IP range you want to unblock. Cannot be used together with {$p}id", 'token' => "An unblock token previously obtained through prop=info", - 'gettoken' => 'If set, an unblock token will be returned, and no other action will be taken', 'reason' => 'Reason for unblock', ); } @@ -122,10 +107,6 @@ class ApiUnblock extends ApiBase { public function getResultProperties() { return array( '' => array( - 'unblocktoken' => array( - ApiBase::PROP_TYPE => 'string', - ApiBase::PROP_NULLABLE => true - ), 'id' => array( ApiBase::PROP_TYPE => 'integer', ApiBase::PROP_NULLABLE => true @@ -178,8 +159,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..467eccf8 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,28 +49,33 @@ 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' ); // Copy the session key to the file key, for backward compatibility. - if( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) { + if ( !$this->mParams['filekey'] && $this->mParams['sessionkey'] ) { $this->mParams['filekey'] = $this->mParams['sessionkey']; } // Select an upload module - if ( !$this->selectUploadModule() ) { - // This is not a true upload, but a status request or similar - return; - } - if ( !isset( $this->mUpload ) ) { - $this->dieUsage( 'No upload module set', 'nomodule' ); + try { + if ( !$this->selectUploadModule() ) { + return; // not a true upload, but a status request or similar + } elseif ( !isset( $this->mUpload ) ) { + $this->dieUsage( 'No upload module set', 'nomodule' ); + } + } catch ( UploadStashException $e ) { // XXX: don't spam exception log + $this->dieUsage( get_class( $e ) . ": " . $e->getMessage(), 'stasherror' ); } // 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,28 +85,38 @@ class ApiUpload extends ApiBase { // Check if the uploaded file is sane if ( $this->mParams['chunk'] ) { - $maxSize = $this->mUpload->getMaxUploadSize( ); - if( $this->mParams['filesize'] > $maxSize ) { + $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(); - if ( $result['result'] === 'Success' ) { - $result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() ); + // Get the result based on the current upload context: + try { + $result = $this->getContextResult(); + if ( $result['result'] === 'Success' ) { + $result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() ); + } + } catch ( UploadStashException $e ) { // XXX: don't spam exception log + $this->dieUsage( get_class( $e ) . ": " . $e->getMessage(), 'stasherror' ); } $this->getResult()->addValue( null, $this->getModuleName(), $result ); @@ -111,14 +124,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,13 +145,14 @@ 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 ){ - $result = array (); + 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 try { @@ -152,12 +167,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 +187,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 +203,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( $filekey ); + if ( $progress && $progress['result'] === 'Poll' ) { + $this->dieUsage( "Chunk assembly already in progress.", 'stashfailed' ); + } + UploadBase::setSessionStatus( + $filekey, + array( 'result' => 'Poll', + 'stage' => 'queued', 'status' => Status::newGood() ) + ); + $ok = JobQueueGroup::singleton()->push( new AssembleUploadChunksJob( + Title::makeTitle( NS_FILE, $filekey ), + array( + 'filename' => $this->mParams['filename'], + 'filekey' => $filekey, + 'session' => $this->getContext()->exportSession() + ) + ) ); + if ( $ok ) { + $result['result'] = 'Poll'; + } else { + UploadBase::setSessionStatus( $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 +284,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 +294,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 +323,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 +358,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 @@ -313,7 +368,7 @@ class ApiUpload extends ApiBase { if ( $this->mParams['chunk'] ) { // 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 +389,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( @@ -352,6 +410,10 @@ class ApiUpload extends ApiBase { $this->dieUsageMsg( 'copyuploadbaddomain' ); } + if ( !UploadFromUrl::isAllowedUrl( $this->mParams['url'] ) ) { + $this->dieUsageMsg( 'copyuploadbadurl' ); + } + $async = false; if ( $this->mParams['asyncdownload'] ) { $this->checkAsyncDownloadEnabled(); @@ -396,16 +458,23 @@ 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; } - // TODO: Move them to ApiBase's message map - switch( $verification['status'] ) { + $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'] ) { // Recoverable errors case UploadBase::MIN_LENGTH_PARTNAME: $this->dieRecoverableError( 'filename-tooshort', 'filename' ); @@ -435,7 +504,7 @@ class ApiUpload extends ApiBase { case UploadBase::FILETYPE_BADTYPE: $extradata = array( 'filetype' => $verification['finalExt'], - 'allowed' => $wgFileExtensions + 'allowed' => array_values( array_unique( $wgFileExtensions ) ) ); $this->getResult()->setIndexedTagName( $extradata['allowed'], 'ext' ); @@ -460,12 +529,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 @@ -497,18 +565,18 @@ class ApiUpload extends ApiBase { if ( isset( $warnings['exists'] ) ) { $warning = $warnings['exists']; unset( $warnings['exists'] ); - $warnings[$warning['warning']] = $warning['file']->getName(); + $localFile = isset( $warning['normalizedFile'] ) ? $warning['normalizedFile'] : $warning['file']; + $warnings[$warning['warning']] = $localFile->getName(); } } 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 +585,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 +595,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() ); + + 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 ); + $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 +660,7 @@ 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' ); } } @@ -601,7 +698,9 @@ class ApiUpload extends ApiBase { ), ), 'ignorewarnings' => false, - 'file' => null, + 'file' => array( + ApiBase::PARAM_TYPE => 'upload', + ), 'url' => null, 'filekey' => null, 'sessionkey' => array( @@ -612,11 +711,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 +744,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 +797,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 +817,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' => 'stasherror', 'info' => 'An upload stash error occurred' ), array( 'fileexists-forbidden' ), array( 'fileexists-shared-forbidden' ), ) @@ -740,8 +847,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..7d308285 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() { @@ -42,6 +38,7 @@ class ApiUserrights extends ApiBase { $user = $this->getUrUser(); $form = new UserrightsPage; + $form->setContext( $this->getContext() ); $r['user'] = $user->getName(); $r['userid'] = $user->getId(); list( $r['added'], $r['removed'] ) = @@ -66,10 +63,10 @@ class ApiUserrights extends ApiBase { $params = $this->extractRequestParams(); $form = new UserrightsPage; + $form->setContext( $this->getContext() ); $status = $form->fetchUser( $params['user'] ); if ( !$status->isOK() ) { - $errors = $status->getErrorsArray(); - $this->dieUsageMsg( $errors[0] ); + $this->dieStatus( $status ); } else { $user = $status->value; } @@ -87,7 +84,7 @@ class ApiUserrights extends ApiBase { } public function getAllowedParams() { - return array ( + return array( 'user' => array( ApiBase::PARAM_TYPE => 'string', ApiBase::PARAM_REQUIRED => true @@ -141,8 +138,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..c7d636a1 100644 --- a/includes/api/ApiWatch.php +++ b/includes/api/ApiWatch.php @@ -31,36 +31,48 @@ */ class ApiWatch extends ApiBase { - public function __construct( $main, $action ) { - parent::__construct( $main, $action ); - } - public function execute() { $user = $this->getUser(); if ( !$user->isLoggedIn() ) { $this->dieUsage( 'You must be logged-in to have a watchlist', 'notloggedin' ); } + if ( !$user->isAllowed( 'editmywatchlist' ) ) { + $this->dieUsage( 'You don\'t have permission to edit your watchlist', 'permissiondenied' ); + } $params = $this->extractRequestParams(); $title = Title::newFromText( $params['title'] ); - if ( !$title || $title->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(); - $success = UnwatchAction::doUnwatch( $title, $user ); + $status = UnwatchAction::doUnwatch( $title, $user ); } else { $res['watched'] = ''; $res['message'] = $this->msg( 'addedwatchtext', $title->getPrefixedText() )->title( $title )->parseAsBlock(); - $success = WatchAction::doWatch( $title, $user ); + $status = WatchAction::doWatch( $title, $user ); } - if ( !$success ) { - $this->dieUsageMsg( 'hookaborted' ); + + if ( !is_null( $oldLang ) ) { + $this->getContext()->setLanguage( $oldLang ); // Reset language to $oldLang + } + + if ( !$status->isOK() ) { + $this->dieStatus( $status ); } $this->getResult()->addValue( null, $this->getModuleName(), $res ); } @@ -88,6 +100,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 +112,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 +150,4 @@ class ApiWatch extends ApiBase { public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/API:Watch'; } - - public function getVersion() { - return __CLASS__ . ': $Id$'; - } } |