From 4ac9fa081a7c045f6a9f1cfc529d82423f485b2e Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Sun, 8 Dec 2013 09:55:49 +0100 Subject: Update to MediaWiki 1.22.0 --- includes/api/ApiBase.php | 119 ++++++++++++++------ includes/api/ApiBlock.php | 2 +- includes/api/ApiComparePages.php | 8 +- includes/api/ApiCreateAccount.php | 52 ++++----- includes/api/ApiDelete.php | 3 +- includes/api/ApiEditPage.php | 59 ++++++++-- includes/api/ApiExpandTemplates.php | 4 +- includes/api/ApiFeedContributions.php | 15 ++- includes/api/ApiFeedWatchlist.php | 90 +++++++++++---- includes/api/ApiFormatBase.php | 8 +- includes/api/ApiFormatJson.php | 24 ++-- includes/api/ApiFormatWddx.php | 75 +++++++------ includes/api/ApiFormatXml.php | 137 ++++++++++------------- includes/api/ApiImageRotate.php | 32 +++--- includes/api/ApiImport.php | 6 +- includes/api/ApiMain.php | 36 +++--- includes/api/ApiMove.php | 4 +- includes/api/ApiOpenSearch.php | 21 +++- includes/api/ApiOptions.php | 8 +- includes/api/ApiPageSet.php | 18 ++- includes/api/ApiParamInfo.php | 6 +- includes/api/ApiParse.php | 157 ++++++++++++++++++++------- includes/api/ApiPatrol.php | 46 ++++++-- includes/api/ApiProtect.php | 3 +- includes/api/ApiPurge.php | 29 +++-- includes/api/ApiQuery.php | 10 +- includes/api/ApiQueryAllCategories.php | 4 +- includes/api/ApiQueryAllImages.php | 4 +- includes/api/ApiQueryAllLinks.php | 116 +++++++++++++------- includes/api/ApiQueryAllMessages.php | 2 +- includes/api/ApiQueryAllPages.php | 7 +- includes/api/ApiQueryAllUsers.php | 4 +- includes/api/ApiQueryBacklinks.php | 15 ++- includes/api/ApiQueryBase.php | 4 +- includes/api/ApiQueryBlocks.php | 61 ++++++++--- includes/api/ApiQueryCategories.php | 5 +- includes/api/ApiQueryCategoryMembers.php | 6 +- includes/api/ApiQueryDeletedrevs.php | 6 +- includes/api/ApiQueryDuplicateFiles.php | 23 ++-- includes/api/ApiQueryExtLinksUsage.php | 6 +- includes/api/ApiQueryExternalLinks.php | 8 +- includes/api/ApiQueryFileRepoInfo.php | 115 ++++++++++++++++++++ includes/api/ApiQueryFilearchive.php | 8 +- includes/api/ApiQueryIWBacklinks.php | 4 + includes/api/ApiQueryIWLinks.php | 13 ++- includes/api/ApiQueryImageInfo.php | 80 +++++++++----- includes/api/ApiQueryInfo.php | 20 +++- includes/api/ApiQueryLangBacklinks.php | 9 +- includes/api/ApiQueryLangLinks.php | 17 +-- includes/api/ApiQueryLogEvents.php | 24 ++-- includes/api/ApiQueryORM.php | 4 +- includes/api/ApiQueryPagesWithProp.php | 2 +- includes/api/ApiQueryProtectedTitles.php | 2 +- includes/api/ApiQueryQueryPage.php | 19 ++-- includes/api/ApiQueryRandom.php | 4 + includes/api/ApiQueryRecentChanges.php | 43 +++++++- includes/api/ApiQueryRevisions.php | 29 +++-- includes/api/ApiQuerySearch.php | 39 ++++++- includes/api/ApiQuerySiteinfo.php | 62 ++++++++--- includes/api/ApiQueryTags.php | 7 +- includes/api/ApiQueryUserContributions.php | 21 ++-- includes/api/ApiQueryUserInfo.php | 20 ++-- includes/api/ApiQueryUsers.php | 6 +- includes/api/ApiQueryWatchlist.php | 95 ++++++++++++++-- includes/api/ApiQueryWatchlistRaw.php | 4 + includes/api/ApiRsd.php | 2 +- includes/api/ApiSetNotificationTimestamp.php | 7 +- includes/api/ApiUpload.php | 84 +++++++------- includes/api/ApiUserrights.php | 7 +- includes/api/ApiWatch.php | 11 +- 70 files changed, 1349 insertions(+), 652 deletions(-) create mode 100644 includes/api/ApiQueryFileRepoInfo.php (limited to 'includes/api') diff --git a/includes/api/ApiBase.php b/includes/api/ApiBase.php index 9351a8d8..ce6ecda6 100644 --- a/includes/api/ApiBase.php +++ b/includes/api/ApiBase.php @@ -304,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 { @@ -445,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)"; } @@ -689,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' ); } } @@ -725,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' ); } } @@ -822,9 +821,9 @@ abstract class ApiBase extends ContextSource { * 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': @@ -866,12 +865,7 @@ 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() ); } /** @@ -967,9 +961,9 @@ abstract class ApiBase extends ContextSource { } 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 ) ) { @@ -1081,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; } @@ -1165,7 +1159,7 @@ abstract class ApiBase extends ContextSource { /** * Validate and normalize of parameters of type 'user' * @param string $value Parameter value - * @param string $encParamName Parameter value + * @param string $encParamName Parameter name * @return string Validated and normalized parameter */ private function validateUser( $value, $encParamName ) { @@ -1222,6 +1216,44 @@ abstract class ApiBase extends ContextSource { throw new UsageException( $description, $this->encodeParamName( $errorCode ), $httpRespCode, $extradata ); } + /** + * 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. */ @@ -1372,6 +1404,7 @@ abstract class ApiBase extends ContextSource { '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' ), @@ -1397,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 ); @@ -1412,10 +1445,10 @@ abstract class ApiBase extends ContextSource { */ public function dieUsageMsgOrDebug( $error ) { global $wgDebugAPI; - if( $wgDebugAPI !== true ) { + if ( $wgDebugAPI !== true ) { $this->dieUsageMsg( $error ); } else { - if( is_string( $error ) ) { + if ( is_string( $error ) ) { $error = array( $error ); } $parsed = $this->parseMsg( $error ); @@ -1448,7 +1481,7 @@ abstract class ApiBase extends ContextSource { // Check whether the error array was nested // array( array( , ), array( , ) ) - if( is_array( $key ) ) { + if ( is_array( $key ) ) { $error = $key; $key = array_shift( $error ); } @@ -1470,7 +1503,7 @@ abstract class ApiBase extends ContextSource { * @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" ); } /** @@ -1535,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' ); @@ -1546,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; @@ -1560,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() { @@ -1574,10 +1614,9 @@ abstract class ApiBase extends ContextSource { } if ( array_key_exists( 'continue', $params ) ) { $ret[] = array( - array( - 'code' => 'badcontinue', - 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' - ) ); + 'code' => 'badcontinue', + 'info' => 'Invalid continue param. You should pass the original value returned by the previous query' + ); } } @@ -1595,13 +1634,31 @@ 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' ); } return $ret; } + /** + * 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 array $errors List of errors. Items can be in the for array( key, param1, param2, ... ) or array( 'code' => ..., 'info' => ... ) diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index 6f3d1e4f..975153ac 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -98,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 ); diff --git a/includes/api/ApiComparePages.php b/includes/api/ApiComparePages.php index 79ffcb0a..1e35c349 100644 --- a/includes/api/ApiComparePages.php +++ b/includes/api/ApiComparePages.php @@ -81,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 || $title->isExternal() ) { + 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(); diff --git a/includes/api/ApiCreateAccount.php b/includes/api/ApiCreateAccount.php index 69748c93..0e752c56 100644 --- a/includes/api/ApiCreateAccount.php +++ b/includes/api/ApiCreateAccount.php @@ -47,17 +47,19 @@ class ApiCreateAccount extends ApiBase { $params = $this->extractRequestParams(); - $result = array(); - // Init session if necessary if ( session_id() == '' ) { wfSetupSession(); } - if( $params['mailpassword'] && !$params['email'] ) { + 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(), @@ -82,22 +84,20 @@ class ApiCreateAccount extends ApiBase { $status = $loginForm->addNewaccountInternal(); $result = array(); - if( $status->isGood() ) { + if ( $status->isGood() ) { // Success! + global $wgEmailAuthentication; $user = $status->getValue(); - // If we showed up language selection links, and one was in use, be - // smart (and sensible) and save that language as the user's preference - global $wgLoginLanguageSelector, $wgEmailAuthentication; - if( $wgLoginLanguageSelector && $params['language'] ) { + if ( $params['language'] ) { $user->setOption( 'language', $params['language'] ); } - if( $params['mailpassword'] ) { + 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() ) ) { + } elseif ( $wgEmailAuthentication && Sanitizer::validateEmail( $user->getEmail() ) ) { // Send out an email authentication message if needed $status->merge( $user->sendConfirmationMail() ); } @@ -124,33 +124,23 @@ class ApiCreateAccount extends ApiBase { $apiResult = $this->getResult(); - if( $status->hasMessage( 'sessionfailure' ) || $status->hasMessage( 'nocookiesfornew' ) ) { + 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() ) { + } elseif ( !$status->isOK() ) { // There was an error. Die now. - // Cannot use dieUsageMsg() directly because extensions - // might return custom error messages. - $errors = $status->getErrorsArray(); - if( $errors[0] instanceof Message ) { - $code = 'aborted'; - $desc = $errors[0]; - } else { - $code = array_shift( $errors[0] ); - $desc = wfMessage( $code, $errors[0] ); - } - $this->dieUsage( $desc, $code ); - } elseif( !$status->isGood() ) { + $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 ) { + if ( $warnings ) { + foreach ( $warnings as &$warning ) { $apiResult->setIndexedTagName( $warning['params'], 'param' ); } $apiResult->setIndexedTagName( $warnings, 'warning' ); @@ -263,8 +253,8 @@ class ApiCreateAccount extends ApiBase { $errors = parent::getPossibleErrors(); // All local errors are from LoginForm, which means they're actually message keys. - foreach( $localErrors as $error ) { - $errors[] = array( 'code' => $error, 'info' => wfMessage( $error )->parse() ); + foreach ( $localErrors as $error ) { + $errors[] = array( 'code' => $error, 'info' => wfMessage( $error )->inLanguage( 'en' )->useDatabase( false )->parse() ); } $errors[] = array( @@ -279,12 +269,16 @@ class ApiCreateAccount extends ApiBase { '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 )->parse() + 'info' => wfMessage( 'passwordtooshort', $wgMinimalPasswordLength )->inLanguage( 'en' )->useDatabase( false )->parse() ); return $errors; } diff --git a/includes/api/ApiDelete.php b/includes/api/ApiDelete.php index d1f0806e..aea10482 100644 --- a/includes/api/ApiDelete.php +++ b/includes/api/ApiDelete.php @@ -61,8 +61,7 @@ class ApiDelete extends ApiBase { $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 diff --git a/includes/api/ApiEditPage.php b/includes/api/ApiEditPage.php index 4916145b..bd61895b 100644 --- a/includes/api/ApiEditPage.php +++ b/includes/api/ApiEditPage.php @@ -100,7 +100,7 @@ class ApiEditPage extends ApiBase { $name = $titleObj->getPrefixedDBkey(); $model = $contentHandler->getModelID(); - $this->dieUsage( "The requested format $contentFormat is not supported for content model ". + $this->dieUsage( "The requested format $contentFormat is not supported for content model " . " $model used by $name", 'badformat' ); } @@ -146,7 +146,7 @@ class ApiEditPage extends ApiBase { } } - // @todo: Add support for appending/prepending to the Content interface + // @todo Add support for appending/prepending to the Content interface if ( !( $content instanceof TextContent ) ) { $mode = $contentHandler->getModelID(); @@ -159,12 +159,17 @@ class ApiEditPage extends ApiBase { $this->dieUsage( "Sections are not supported for this content model: $modelName.", 'sectionsnotsupported' ); } - // Process the content for section edits - $section = intval( $params['section'] ); - $content = $content->getSection( $section ); + 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' ); + if ( !$content ) { + $this->dieUsage( "There is no section {$section}.", 'nosuchsection' ); + } } } @@ -262,7 +267,7 @@ class ApiEditPage extends ApiBase { $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime } - if ( $params['minor'] || ( !$params['notminor'] && $user->getOption( 'minordefault' ) ) ) { + if ( $params['minor'] || ( !$params['notminor'] && $user->getOption( 'minordefault' ) ) ) { $requestArray['wpMinoredit'] = ''; } @@ -275,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'] = ''; @@ -293,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 ); @@ -316,11 +329,37 @@ class ApiEditPage extends ApiBase { $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 ); @@ -342,7 +381,7 @@ 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' ); diff --git a/includes/api/ApiExpandTemplates.php b/includes/api/ApiExpandTemplates.php index f5898fb3..d5c789c3 100644 --- a/includes/api/ApiExpandTemplates.php +++ b/includes/api/ApiExpandTemplates.php @@ -65,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 ); } diff --git a/includes/api/ApiFeedContributions.php b/includes/api/ApiFeedContributions.php index 015a9922..05691093 100644 --- a/includes/api/ApiFeedContributions.php +++ b/includes/api/ApiFeedContributions.php @@ -43,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' ); } @@ -82,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 ); } @@ -93,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 ); @@ -106,9 +106,8 @@ class ApiFeedContributions extends ApiBase { $this->feedItemAuthor( $revision ), $comments ); - } else { - return null; } + return null; } /** @@ -124,7 +123,7 @@ class ApiFeedContributions extends ApiBase { * @return string */ protected function feedItemDesc( $revision ) { - if( $revision ) { + if ( $revision ) { $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text(); $content = $revision->getContent(); @@ -149,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 diff --git a/includes/api/ApiFeedWatchlist.php b/includes/api/ApiFeedWatchlist.php index 6c793b36..fbb70fbc 100644 --- a/includes/api/ApiFeedWatchlist.php +++ b/includes/api/ApiFeedWatchlist.php @@ -33,6 +33,10 @@ */ class ApiFeedWatchlist extends ApiBase { + private $watchlistModule = null; + private $linkToDiffs = false; + private $linkToSections = false; + /** * This module uses a custom feed wrapper printer. * @@ -42,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. @@ -54,11 +56,11 @@ 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' ); } @@ -74,7 +76,7 @@ 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 ( $params['wlowner'] !== null ) { @@ -86,6 +88,12 @@ class ApiFeedWatchlist extends ApiBase { 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'] ) { @@ -93,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'] = ''; @@ -160,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']; @@ -168,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 @@ -182,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'], ); } diff --git a/includes/api/ApiFormatBase.php b/includes/api/ApiFormatBase.php index d8aa1634..b89fb3a7 100644 --- a/includes/api/ApiFormatBase.php +++ b/includes/api/ApiFormatBase.php @@ -85,7 +85,7 @@ abstract class ApiFormatBase extends ApiBase { * * @param bool $b Whether or not ampersands should be escaped. */ - public function setUnescapeAmps ( $b ) { + public function setUnescapeAmps( $b ) { $this->mUnescapeAmps = $b; } @@ -170,12 +170,12 @@ abstract class ApiFormatBase extends ApiBase { ?>
-You are looking at the HTML representation of the mFormat ); ?> format.
+You are looking at the HTML representation of the mFormat; ?> format.
HTML is good for debugging, but is unsuitable for application use.
Specify the format parameter to change the output format.
-To see the non HTML representation of the mFormat ); ?> format, set format=mFormat ) ); ?>.
+To see the non HTML representation of the mFormat; ?> format, set format=mFormat ); ?>.
See the complete documentation, or -API help for more information. +API help for more information.
 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.',
 		);
 	}
 
diff --git a/includes/api/ApiFormatWddx.php b/includes/api/ApiFormatWddx.php
index 62b69bb6..5685d937 100644
--- a/includes/api/ApiFormatWddx.php
+++ b/includes/api/ApiFormatWddx.php
@@ -46,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( "$nl" );
 			$this->printText( "$nl" );
@@ -64,44 +64,43 @@ 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 ()
-				// or a regular 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$nl" );
-				} else {
-					// Associative array ()
-					$this->printText( "$indstr$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$nl" );
-					}
-					$this->printText( "$indstr$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 ()
+			// or a regular 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 );
 				}
-				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$nl" );
+			} else {
+				// Associative array ()
+				$this->printText( "$indstr$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$nl" );
+				}
+				$this->printText( "$indstr$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 ) );
 		}
 	}
 
diff --git a/includes/api/ApiFormatXml.php b/includes/api/ApiFormatXml.php
index b4e8e330..4ec149c0 100644
--- a/includes/api/ApiFormatXml.php
+++ b/includes/api/ApiFormatXml.php
@@ -32,7 +32,6 @@ 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;
 
@@ -50,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'];
 
@@ -71,8 +69,7 @@ class ApiFormatXml extends ApiFormatBase {
 		$this->printText(
 			self::recXmlPrint( $this->mRootElemName,
 				$data,
-				$this->getIsHtml() ? - 2 : null,
-				$this->mDoubleQuote
+				$this->getIsHtml() ? - 2 : null
 			)
 		);
 	}
@@ -117,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;
@@ -131,84 +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:
-				// 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;
+
+				$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 );
+			}
 		}
 		return $retval;
 	}
@@ -232,7 +215,6 @@ class ApiFormatXml extends ApiFormatBase {
 
 	public function getAllowedParams() {
 		return array(
-			'xmldoublequote' => false,
 			'xslt' => null,
 			'includexmlnamespace' => false,
 		);
@@ -240,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  as stylesheet. This should be a wiki page '
 				. 'in the MediaWiki namespace whose page name ends with ".xsl"',
 			'includexmlnamespace' => 'If specified, adds an XML namespace'
diff --git a/includes/api/ApiImageRotate.php b/includes/api/ApiImageRotate.php
index b2d75825..7a60e831 100644
--- a/includes/api/ApiImageRotate.php
+++ b/includes/api/ApiImageRotate.php
@@ -22,7 +22,6 @@
  */
 
 class ApiImageRotate extends ApiBase {
-
 	private $mPageSet = null;
 
 	public function __construct( $main, $action ) {
@@ -38,31 +37,28 @@ class ApiImageRotate extends ApiBase {
 	 */
 	private static function addValues( array &$result, $values, $flag = null, $name = null ) {
 		foreach ( $values as $val ) {
-			if( $val instanceof Title ) {
+			if ( $val instanceof Title ) {
 				$v = array();
 				ApiQueryBase::addTitleInfo( $v, $val );
-			} elseif( $name !== null ) {
+			} elseif ( $name !== null ) {
 				$v = array( $name => $val );
 			} else {
 				$v = $val;
 			}
-			if( $flag !== null ) {
+			if ( $flag !== null ) {
 				$v[$flag] = '';
 			}
 			$result[] = $v;
 		}
 	}
 
-
 	public function execute() {
 		$params = $this->extractRequestParams();
-		$rotation = $params[ 'rotation' ];
-		$user = $this->getUser();
+		$rotation = $params['rotation'];
 
 		$pageSet = $this->getPageSet();
 		$pageSet->execute();
 
-		$result = array();
 		$result = array();
 
 		self::addValues( $result, $pageSet->getInvalidTitles(), 'invalid', 'title' );
@@ -111,15 +107,17 @@ class ApiImageRotate extends ApiBase {
 				continue;
 			}
 			$ext = strtolower( pathinfo( "$srcPath", PATHINFO_EXTENSION ) );
-			$tmpFile = TempFSFile::factory( 'rotate_', $ext);
+			$tmpFile = TempFSFile::factory( 'rotate_', $ext );
 			$dstPath = $tmpFile->getPath();
 			$err = $handler->rotate( $file, array(
 				"srcPath" => $srcPath,
 				"dstPath" => $dstPath,
-				"rotation"=> $rotation
+				"rotation" => $rotation
 			) );
 			if ( !$err ) {
-				$comment = wfMessage( 'rotate-comment' )->numParams( $rotation )->text();
+				$comment = wfMessage(
+					'rotate-comment'
+				)->numParams( $rotation )->inContentLanguage()->text();
 				$status = $file->upload( $dstPath,
 					$comment, $comment, 0, false, false, $this->getUser() );
 				if ( $status->isGood() ) {
@@ -152,13 +150,14 @@ class ApiImageRotate extends ApiBase {
 
 	/**
 	 * Checks that the user has permissions to perform rotations.
-	 * @param $user User The user to check.
+	 * @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 )
+			$title->getUserPermissionsErrors( 'edit', $user ),
+			$title->getUserPermissionsErrors( 'upload', $user )
 		);
 
 		if ( $permissionErrors ) {
@@ -179,7 +178,6 @@ class ApiImageRotate extends ApiBase {
 	}
 
 	public function getAllowedParams( $flags = 0 ) {
-		$pageSet = $this->getPageSet();
 		$result = array(
 			'rotation' => array(
 				ApiBase::PARAM_TYPE => array( '90', '180', '270' ),
@@ -198,7 +196,7 @@ class ApiImageRotate extends ApiBase {
 
 	public function getParamDescription() {
 		$pageSet = $this->getPageSet();
-		return $pageSet->getParamDescription() + array(
+		return $pageSet->getFinalParamDescription() + array(
 			'rotation' => 'Degrees to rotate image clockwise',
 			'token' => 'Edit token. You can get one of these through action=tokens',
 		);
@@ -220,7 +218,7 @@ class ApiImageRotate extends ApiBase {
 		$pageSet = $this->getPageSet();
 		return array_merge(
 			parent::getPossibleErrors(),
-			$pageSet->getPossibleErrors()
+			$pageSet->getFinalPossibleErrors()
 		);
 	}
 
diff --git a/includes/api/ApiImport.php b/includes/api/ApiImport.php
index 1f0a5fab..f48a822e 100644
--- a/includes/api/ApiImport.php
+++ b/includes/api/ApiImport.php
@@ -57,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 );
@@ -66,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(
diff --git a/includes/api/ApiMain.php b/includes/api/ApiMain.php
index 7b2fd914..c11f16cb 100644
--- a/includes/api/ApiMain.php
+++ b/includes/api/ApiMain.php
@@ -83,7 +83,7 @@ class ApiMain extends ApiBase {
 		'import' => 'ApiImport',
 		'userrights' => 'ApiUserrights',
 		'options' => 'ApiOptions',
-		'imagerotate' =>'ApiImageRotate',
+		'imagerotate' => 'ApiImageRotate',
 	);
 
 	/**
@@ -274,7 +274,7 @@ class ApiMain extends ApiBase {
 			return;
 		}
 
-		if ( !User::groupHasPermission( '*', 'read' ) ) {
+		if ( !User::isEveryoneAllowed( 'read' ) ) {
 			// Private wiki, only private headers
 			if ( $mode !== 'private' ) {
 				wfDebug( __METHOD__ . ": ignoring request for $mode cache mode, private wiki\n" );
@@ -383,13 +383,8 @@ class ApiMain extends ApiBase {
 			wfRunHooks( 'ApiMain::onException', array( $this, $e ) );
 
 			// Log it
-			if ( $e instanceof MWException && !( $e instanceof UsageException ) ) {
-				global $wgLogExceptionBacktrace;
-				if ( $wgLogExceptionBacktrace ) {
-					wfDebugLog( 'exception', $e->getLogMessage() . "\n" . $e->getTraceAsString() . "\n" );
-				} else {
-					wfDebugLog( 'exception', $e->getLogMessage() );
-				}
+			if ( !( $e instanceof UsageException ) ) {
+				MWExceptionHandler::logException( $e );
 			}
 
 			// Handle any kind of exception by outputting properly formatted error message.
@@ -418,7 +413,7 @@ class ApiMain extends ApiBase {
 		}
 
 		// Log the request whether or not there was an error
-		$this->logRequest( microtime( true ) - $t);
+		$this->logRequest( microtime( true ) - $t );
 
 		// Send cache headers after any code which might generate an error, to
 		// avoid sending public cache headers for errors.
@@ -609,7 +604,7 @@ 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 ( !$this->mModuleMgr->isDefined( $value, 'format' ) ) {
@@ -763,7 +758,7 @@ class ApiMain extends ApiBase {
 	 */
 	protected function checkExecutePermissions( $module ) {
 		$user = $this->getUser();
-		if ( $module->isReadMode() && !User::groupHasPermission( '*', 'read' ) &&
+		if ( $module->isReadMode() && !User::isEveryoneAllowed( 'read' ) &&
 			!$user->isAllowed( 'read' ) )
 		{
 			$this->dieUsageMsg( 'readrequired' );
@@ -782,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 );
 		}
 	}
@@ -857,7 +852,7 @@ class ApiMain extends ApiBase {
 			' ' . $request->getMethod() .
 			' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
 			' ' . $request->getIP() .
-			' T=' . $milliseconds .'ms';
+			' T=' . $milliseconds . 'ms';
 		foreach ( $this->getParamsUsed() as $name ) {
 			$value = $request->getVal( $name );
 			if ( $value === null ) {
@@ -944,7 +939,7 @@ class ApiMain extends ApiBase {
 			$unusedParams = array_diff( $allParams, $paramsUsed );
 		}
 
-		if( count( $unusedParams ) ) {
+		if ( count( $unusedParams ) ) {
 			$s = count( $unusedParams ) > 1 ? 's' : '';
 			$this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
 		}
@@ -957,7 +952,7 @@ class ApiMain extends ApiBase {
 	 */
 	protected function printResult( $isError ) {
 		global $wgDebugAPI;
-		if( $wgDebugAPI !== false ) {
+		if ( $wgDebugAPI !== false ) {
 			$this->setWarning( 'SECURITY WARNING: $wgDebugAPI is enabled' );
 		}
 
@@ -1002,7 +997,7 @@ class ApiMain extends ApiBase {
 				ApiBase::PARAM_DFLT => 'help',
 				ApiBase::PARAM_TYPE => $this->mModuleMgr->getNames( 'action' )
 			),
-			'maxlag'  => array(
+			'maxlag' => array(
 				ApiBase::PARAM_TYPE => 'integer'
 			),
 			'smaxage' => array(
@@ -1014,7 +1009,7 @@ class ApiMain extends ApiBase {
 				ApiBase::PARAM_DFLT => 0
 			),
 			'requestid' => null,
-			'servedby'  => false,
+			'servedby' => false,
 			'origin' => null,
 		);
 	}
@@ -1042,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.',
@@ -1114,7 +1110,7 @@ class ApiMain extends ApiBase {
 		return array(
 			'API developers:',
 			'    Roan Kattouw ".@gmail.com" (lead developer Sep 2007-2009)',
-			'    Victor Vasiliev - vasilvv at gee mail dot com',
+			'    Victor Vasiliev - vasilvv @ gmail . com',
 			'    Bryan Tong Minh - bryan . tongminh @ gmail . com',
 			'    Sam Reed - sam @ reedyboy . net',
 			'    Yuri Astrakhan "@gmail.com" (creator, lead developer Sep 2006-Sep 2007, 2012-present)',
@@ -1143,7 +1139,7 @@ class ApiMain extends ApiBase {
 		$this->setHelp();
 		// Get help text from cache if present
 		$key = wfMemcKey( 'apihelp', $this->getModuleName(),
-			SpecialVersion::getVersion( 'nodb' ) );
+			str_replace( ' ', '_', SpecialVersion::getVersion( 'nodb' ) ) );
 		if ( $wgAPICacheHelpTimeout > 0 ) {
 			$cached = $wgMemc->get( $key );
 			if ( $cached ) {
diff --git a/includes/api/ApiMove.php b/includes/api/ApiMove.php
index 3e846e3b..c18036cf 100644
--- a/includes/api/ApiMove.php
+++ b/includes/api/ApiMove.php
@@ -88,7 +88,7 @@ class ApiMove extends ApiBase {
 			$r['redirectcreated'] = '';
 		}
 
-		if( $toTitleExists ) {
+		if ( $toTitleExists ) {
 			$r['moveoverredirect'] = '';
 		}
 
@@ -99,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 {
diff --git a/includes/api/ApiOpenSearch.php b/includes/api/ApiOpenSearch.php
index caf361ac..315ace37 100644
--- a/includes/api/ApiOpenSearch.php
+++ b/includes/api/ApiOpenSearch.php
@@ -1,7 +1,5 @@
 @gmail.com"
@@ -29,8 +27,20 @@
  */
 class ApiOpenSearch extends ApiBase {
 
+	/**
+	 * 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() {
@@ -94,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' ),
+			)
 		);
 	}
 
@@ -103,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',
 		);
 	}
 
diff --git a/includes/api/ApiOptions.php b/includes/api/ApiOptions.php
index 8c996a26..7256066d 100644
--- a/includes/api/ApiOptions.php
+++ b/includes/api/ApiOptions.php
@@ -42,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;
 
@@ -50,7 +54,7 @@ class ApiOptions extends ApiBase {
 		}
 
 		if ( $params['reset'] ) {
-			$user->resetOptions( $params['resetkinds'] );
+			$user->resetOptions( $params['resetkinds'], $this->getContext() );
 			$changed = true;
 		}
 
@@ -83,7 +87,7 @@ class ApiOptions extends ApiBase {
 				case 'registered-checkmatrix':
 					// A key for a multiselect or checkmatrix option.
 					$validation = true;
-					$value = $value !== null ? (bool) $value : null;
+					$value = $value !== null ? (bool)$value : null;
 					break;
 				case 'userjs':
 					// Allow non-default preferences prefixed with 'userjs-', to be set by user scripts
diff --git a/includes/api/ApiPageSet.php b/includes/api/ApiPageSet.php
index 074efe4b..b05cb2b6 100644
--- a/includes/api/ApiPageSet.php
+++ b/includes/api/ApiPageSet.php
@@ -69,6 +69,9 @@ class ApiPageSet extends ApiBase {
 	private $mFakePageId = -1;
 	private $mCacheMode = 'public';
 	private $mRequestedPageFields = array();
+	/**
+	 * @var int
+	 */
 	private $mDefaultNamespace = NS_MAIN;
 
 	/**
@@ -149,7 +152,6 @@ class ApiPageSet extends ApiBase {
 			if ( !$isDryRun ) {
 				$generator->executeGenerator( $this );
 				wfRunHooks( 'APIQueryGeneratorAfterExecute', array( &$generator, &$this ) );
-				$this->resolvePendingRedirects();
 			} else {
 				// Prevent warnings from being reported on these parameters
 				$main = $this->getMain();
@@ -160,6 +162,10 @@ class ApiPageSet extends ApiBase {
 			$generator->profileOut();
 			$this->profileIn();
 
+			if ( !$isDryRun ) {
+				$this->resolvePendingRedirects();
+			}
+
 			if ( !$isQuery ) {
 				// If this pageset is not part of the query, we called profileIn() above
 				$dbSource->profileOut();
@@ -185,7 +191,7 @@ class ApiPageSet extends ApiBase {
 
 			if ( !$isDryRun ) {
 				// Populate page information with the original user input
-				switch( $dataSource ) {
+				switch ( $dataSource ) {
 					case 'titles':
 						$this->initFromTitles( $this->mParams['titles'] );
 						break;
@@ -404,7 +410,7 @@ class ApiPageSet extends ApiBase {
 	 * @return array of raw_prefixed_title (string) => prefixed_title (string)
 	 * @since 1.21
 	 */
-	public function getNormalizedTitlesAsResult( $result = null  ) {
+	public function getNormalizedTitlesAsResult( $result = null ) {
 		$values = array();
 		foreach ( $this->getNormalizedTitles() as $rawTitleStr => $titleStr ) {
 			$values[] = array(
@@ -595,13 +601,13 @@ class ApiPageSet extends ApiBase {
 		}
 
 		foreach ( $this->mRequestedPageFields as $fieldName => &$fieldValues ) {
-			$fieldValues[$pageId] = $row-> $fieldName;
+			$fieldValues[$pageId] = $row->$fieldName;
 		}
 	}
 
 	/**
 	 * Do not use, does nothing, will be removed
-	 * @deprecated 1.21
+	 * @deprecated since 1.21
 	 */
 	public function finishPageSetGeneration() {
 		wfDeprecated( __METHOD__, '1.21' );
@@ -859,7 +865,7 @@ class ApiPageSet extends ApiBase {
 			$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;
diff --git a/includes/api/ApiParamInfo.php b/includes/api/ApiParamInfo.php
index 27f8cefd..3e1a7531 100644
--- a/includes/api/ApiParamInfo.php
+++ b/includes/api/ApiParamInfo.php
@@ -149,7 +149,7 @@ class ApiParamInfo extends ApiBase {
 				$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;
@@ -157,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;
 			}
@@ -300,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;
diff --git a/includes/api/ApiParse.php b/includes/api/ApiParse.php
index 09b7a882..a369994b 100644
--- a/includes/api/ApiParse.php
+++ b/includes/api/ApiParse.php
@@ -44,6 +44,14 @@ 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'];
@@ -51,7 +59,7 @@ class ApiParse extends ApiBase {
 		$model = $params['contentmodel'];
 		$format = $params['contentformat'];
 
-		if ( !is_null( $page ) && ( !is_null( $text ) || $title != 'API' ) ) {
+		if ( !is_null( $page ) && ( !is_null( $text ) || $titleProvided ) ) {
 			$this->dieUsage( 'The page parameter cannot be used together with the text and title parameters', 'params' );
 		}
 
@@ -94,8 +102,7 @@ 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 ( $rev->isCurrent() ) {
@@ -152,8 +159,7 @@ class ApiParse extends ApiBase {
 					$oldid = $pageObj->getLatest();
 				}
 
-				$popts = $pageObj->makeParserOptions( $this->getContext() );
-				$popts->enableLimitReport( !$params['disablepp'] );
+				$popts = $this->makeParserOptions( $pageObj, $params );
 
 				// Potentially cached
 				$p_result = $this->getParsedContent( $pageObj, $popts, $pageid,
@@ -170,11 +176,24 @@ class ApiParse extends ApiBase {
 			$wgTitle = $titleObj;
 			$pageObj = WikiPage::factory( $titleObj );
 
-			$popts = $pageObj->makeParserOptions( $this->getContext() );
-			$popts->enableLimitReport( !$params['disablepp'] );
+			$popts = $this->makeParserOptions( $pageObj, $params );
 
 			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' );
+				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 {
@@ -194,10 +213,10 @@ class ApiParse extends ApiBase {
 				// Build a result and bail out
 				$result_array = array();
 				$result_array['text'] = array();
-				$result->setContent( $result_array['text'], $this->pstContent->serialize( $format ) );
+				ApiResult::setContent( $result_array['text'], $this->pstContent->serialize( $format ) );
 				if ( isset( $prop['wikitext'] ) ) {
 					$result_array['wikitext'] = array();
-					$result->setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
+					ApiResult::setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
 				}
 				$result->addValue( null, $this->getModuleName(), $result_array );
 				return;
@@ -225,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() );
@@ -247,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() );
@@ -288,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() ) );
 			}
 		}
 
@@ -298,10 +331,10 @@ class ApiParse extends ApiBase {
 
 		if ( isset( $prop['wikitext'] ) ) {
 			$result_array['wikitext'] = array();
-			$result->setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
+			ApiResult::setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
 			if ( !is_null( $this->pstContent ) ) {
 				$result_array['psttext'] = array();
-				$result->setContent( $result_array['psttext'], $this->pstContent->serialize( $format ) );
+				ApiResult::setContent( $result_array['psttext'], $this->pstContent->serialize( $format ) );
 			}
 		}
 		if ( isset( $prop['properties'] ) ) {
@@ -321,7 +354,7 @@ class ApiParse extends ApiBase {
 				$xml = $dom->__toString();
 			}
 			$result_array['parsetree'] = array();
-			$result->setContent( $result_array['parsetree'], $xml );
+			ApiResult::setContent( $result_array['parsetree'], $xml );
 		}
 
 		$result_mapping = array(
@@ -345,6 +378,26 @@ 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
@@ -400,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;
@@ -411,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;
@@ -465,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'] = '';
 				}
@@ -487,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;
 			}
 		}
@@ -499,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;
@@ -510,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;
@@ -521,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;
@@ -537,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,
@@ -575,10 +626,13 @@ 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(),
 			),
@@ -590,11 +644,13 @@ class ApiParse extends ApiBase {
 
 	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",
@@ -618,34 +674,52 @@ 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 (requires prop=wikitext)',
-			'contentformat' => 'Content serialization format used for the input text',
-			'contentmodel' => 'Content model of the new content',
+			'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' ),
@@ -660,7 +734,10 @@ class ApiParse extends ApiBase {
 
 	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',
 		);
 	}
 
diff --git a/includes/api/ApiPatrol.php b/includes/api/ApiPatrol.php
index 4d4fbba9..bd2fde2b 100644
--- a/includes/api/ApiPatrol.php
+++ b/includes/api/ApiPatrol.php
@@ -35,11 +35,27 @@ class ApiPatrol extends ApiBase {
 	 */
 	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 ) {
@@ -66,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'
 			),
 		);
 	}
@@ -76,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',
 		);
 	}
 
@@ -94,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"
+				)
 		) );
 	}
 
@@ -109,7 +136,8 @@ 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'
 		);
 	}
 
diff --git a/includes/api/ApiProtect.php b/includes/api/ApiProtect.php
index 503c6920..7830c8b4 100644
--- a/includes/api/ApiProtect.php
+++ b/includes/api/ApiProtect.php
@@ -103,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(),
diff --git a/includes/api/ApiPurge.php b/includes/api/ApiPurge.php
index 134f4a0d..0812ba51 100644
--- a/includes/api/ApiPurge.php
+++ b/includes/api/ApiPurge.php
@@ -42,15 +42,15 @@ class ApiPurge extends ApiBase {
 	 */
 	private static function addValues( array &$result, $values, $flag = null, $name = null ) {
 		foreach ( $values as $val ) {
-			if( $val instanceof Title ) {
+			if ( $val instanceof Title ) {
 				$v = array();
 				ApiQueryBase::addTitleInfo( $v, $val );
-			} elseif( $name !== null ) {
+			} elseif ( $name !== null ) {
 				$v = array( $name => $val );
 			} else {
 				$v = $val;
 			}
-			if( $flag !== null ) {
+			if ( $flag !== null ) {
 				$v[$flag] = '';
 			}
 			$result[] = $v;
@@ -64,6 +64,7 @@ class ApiPurge extends ApiBase {
 		$params = $this->extractRequestParams();
 
 		$forceLinkUpdate = $params['forcelinkupdate'];
+		$forceRecursiveLinkUpdate = $params['forcerecursivelinkupdate'];
 		$pageSet = $this->getPageSet();
 		$pageSet->execute();
 
@@ -82,8 +83,8 @@ class ApiPurge extends ApiBase {
 			$page->doPurge(); // Directly purge and skip the UI part of purge().
 			$r['purged'] = '';
 
-			if ( $forceLinkUpdate ) {
-				if ( !$this->getUser()->pingLimiter() ) {
+			if ( $forceLinkUpdate || $forceRecursiveLinkUpdate ) {
+				if ( !$this->getUser()->pingLimiter( 'linkpurge' ) ) {
 					global $wgEnableParserCache;
 
 					$popts = $page->makeParserOptions( 'canonical' );
@@ -93,7 +94,8 @@ class ApiPurge extends ApiBase {
 					$p_result = $content->getParserOutput( $title, $page->getLatest(), $popts, $wgEnableParserCache );
 
 					# Update the links tables
-					$updates = $content->getSecondaryDataUpdates( $title, null, true, $p_result );
+					$updates = $content->getSecondaryDataUpdates(
+						$title, null, $forceRecursiveLinkUpdate, $p_result );
 					DataUpdate::runUpdates( $updates );
 
 					$r['linkupdate'] = '';
@@ -150,7 +152,10 @@ class ApiPurge extends ApiBase {
 	}
 
 	public function getAllowedParams( $flags = 0 ) {
-		$result = array( 'forcelinkupdate' => false );
+		$result = array(
+			'forcelinkupdate' => false,
+			'forcerecursivelinkupdate' => false
+		);
 		if ( $flags ) {
 			$result += $this->getPageSet()->getFinalParams( $flags );
 		}
@@ -158,8 +163,12 @@ class ApiPurge extends ApiBase {
 	}
 
 	public function getParamDescription() {
-		return $this->getPageSet()->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() {
@@ -204,7 +213,7 @@ class ApiPurge extends ApiBase {
 	public function getPossibleErrors() {
 		return array_merge(
 			parent::getPossibleErrors(),
-			$this->getPageSet()->getPossibleErrors()
+			$this->getPageSet()->getFinalPossibleErrors()
 		);
 	}
 
diff --git a/includes/api/ApiQuery.php b/includes/api/ApiQuery.php
index f69ad234..e03837fc 100644
--- a/includes/api/ApiQuery.php
+++ b/includes/api/ApiQuery.php
@@ -64,6 +64,7 @@ class ApiQuery extends ApiBase {
 	 */
 	private static $QueryListModules = array(
 		'allcategories' => 'ApiQueryAllCategories',
+		'allfileusages' => 'ApiQueryAllLinks',
 		'allimages' => 'ApiQueryAllImages',
 		'alllinks' => 'ApiQueryAllLinks',
 		'allpages' => 'ApiQueryAllPages',
@@ -102,6 +103,7 @@ class ApiQuery extends ApiBase {
 		'allmessages' => 'ApiQueryAllMessages',
 		'siteinfo' => 'ApiQuerySiteinfo',
 		'userinfo' => 'ApiQueryUserInfo',
+		'filerepoinfo' => 'ApiQueryFileRepoInfo',
 	);
 
 	/**
@@ -382,7 +384,6 @@ class ApiQuery extends ApiBase {
 		$modules = $allModules;
 		$tmp = $completeModules;
 		$wasPosted = $this->getRequest()->wasPosted();
-		$main = $this->getMain();
 
 		/** @var $module ApiQueryBase */
 		foreach ( $allModules as $moduleName => $module ) {
@@ -513,7 +514,7 @@ class ApiQuery extends ApiBase {
 			ApiQueryBase::addTitleInfo( $vals, $title );
 			$vals['special'] = '';
 			if ( $title->isSpecialPage() &&
-					!SpecialPageFactory::exists( $title->getDbKey() ) ) {
+					!SpecialPageFactory::exists( $title->getDBkey() ) ) {
 				$vals['missing'] = '';
 			} elseif ( $title->getNamespace() == NS_MEDIA &&
 					!wfFindFile( $title ) ) {
@@ -697,7 +698,7 @@ class ApiQuery extends ApiBase {
 	}
 
 	public function getParamDescription() {
-		return $this->getPageSet()->getParamDescription() + 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',
@@ -723,7 +724,7 @@ class ApiQuery extends ApiBase {
 	public function getPossibleErrors() {
 		return array_merge(
 			parent::getPossibleErrors(),
-			$this->getPageSet()->getPossibleErrors()
+			$this->getPageSet()->getFinalPossibleErrors()
 		);
 	}
 
@@ -736,6 +737,7 @@ class ApiQuery extends ApiBase {
 
 	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',
diff --git a/includes/api/ApiQueryAllCategories.php b/includes/api/ApiQueryAllCategories.php
index 496a0eb8..3f5c6ee7 100644
--- a/includes/api/ApiQueryAllCategories.php
+++ b/includes/api/ApiQueryAllCategories.php
@@ -76,7 +76,7 @@ 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'] ) ) {
@@ -121,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;
diff --git a/includes/api/ApiQueryAllImages.php b/includes/api/ApiQueryAllImages.php
index e24b162c..ccc7a3a2 100644
--- a/includes/api/ApiQueryAllImages.php
+++ b/includes/api/ApiQueryAllImages.php
@@ -189,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' );
 			}
 
@@ -260,7 +260,7 @@ class ApiQueryAllImages extends ApiQueryGeneratorBase {
 	}
 
 	public function getAllowedParams() {
-		return array (
+		return array(
 			'sort' => array(
 				ApiBase::PARAM_DFLT => 'name',
 				ApiBase::PARAM_TYPE => array(
diff --git a/includes/api/ApiQueryAllLinks.php b/includes/api/ApiQueryAllLinks.php
index e355f8b0..47d1bcef 100644
--- a/includes/api/ApiQueryAllLinks.php
+++ b/includes/api/ApiQueryAllLinks.php
@@ -37,24 +37,41 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
 				$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->descriptionLink = 'link';
-				$this->descriptionLinked = 'linked';
+				$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->descriptionLink = 'transclusion';
-				$this->descriptionLinked = 'transcluded';
+				$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' );
 		}
@@ -83,21 +100,29 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
 		$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 ( $fld_ids ) {
 				$this->dieUsage(
-					"{$this->getModuleName()} cannot return corresponding page ids in unique {$this->descriptionLink}s mode",
+					"{$this->getModuleName()} cannot return corresponding page ids in unique {$this->descriptionWhat}s mode",
 					'params' );
 			}
 			$this->addOption( 'DISTINCT' );
 		}
 
 		$this->addTables( $this->table );
-		$this->addWhereFld( $pfx . 'namespace', $params['namespace'] );
+		if ( $this->hasNamespace ) {
+			$this->addWhereFld( $pfx . 'namespace', $namespace );
+		}
 
 		$continue = !is_null( $params['continue'] );
 		if ( $continue ) {
@@ -106,14 +131,14 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
 			if ( $params['unique'] ) {
 				$this->dieContinueUsageIf( count( $continueArr ) != 1 );
 				$continueTitle = $db->addQuotes( $continueArr[0] );
-				$this->addWhere( "{$pfx}title $op= $continueTitle" );
+				$this->addWhere( "{$pfx}{$fieldTitle} $op= $continueTitle" );
 			} else {
 				$this->dieContinueUsageIf( count( $continueArr ) != 2 );
 				$continueTitle = $db->addQuotes( $continueArr[0] );
 				$continueFrom = intval( $continueArr[1] );
 				$this->addWhere(
-					"{$pfx}title $op $continueTitle OR " .
-					"({$pfx}title = $continueTitle AND " .
+					"{$pfx}{$fieldTitle} $op $continueTitle OR " .
+					"({$pfx}{$fieldTitle} = $continueTitle AND " .
 					"{$pfx}from $op= $continueFrom)"
 				);
 			}
@@ -122,22 +147,24 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
 		// 'continue' always overrides 'from'
 		$from = ( $continue || is_null( $params['from'] ) ? null : $this->titlePartToKey( $params['from'] ) );
 		$to = ( is_null( $params['to'] ) ? null : $this->titlePartToKey( $params['to'] ) );
-		$this->addWhereRange( $pfx . 'title', 'newer', $from, $to );
+		$this->addWhereRange( $pfx . $fieldTitle, 'newer', $from, $to );
 
 		if ( isset( $params['prefix'] ) ) {
-			$this->addWhere( $pfx . 'title' . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
+			$this->addWhere( $pfx . $fieldTitle . $db->buildLike( $this->titlePartToKey( $params['prefix'] ), $db->anyString() ) );
 		}
 
-		$this->addFields( array( 'pl_title' => $pfx . 'title' ) );
+		$this->addFields( array( 'pl_title' => $pfx . $fieldTitle ) );
 		$this->addFieldsIf( array( 'pl_from' => $pfx . 'from' ), !$params['unique'] );
 
-		$this->addOption( 'USE INDEX', $pfx . '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[] = $pfx . 'title' . $sort;
+		$orderBy[] = $pfx . $fieldTitle . $sort;
 		if ( !$params['unique'] ) {
 			$orderBy[] = $pfx . 'from' . $sort;
 		}
@@ -166,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 );
@@ -179,7 +206,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
 					break;
 				}
 			} elseif ( $params['unique'] ) {
-				$titles[] = Title::makeTitle( $params['namespace'], $row->pl_title );
+				$titles[] = Title::makeTitle( $namespace, $row->pl_title );
 			} else {
 				$pageids[] = $row->pl_from;
 			}
@@ -195,7 +222,7 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
 	}
 
 	public function getAllowedParams() {
-		return array(
+		$allowedParams = array(
 			'continue' => null,
 			'from' => null,
 			'to' => null,
@@ -228,30 +255,39 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
 				)
 			),
 		);
+		if ( !$this->hasNamespace ) {
+			unset( $allowedParams['namespace'] );
+		}
+		return $allowedParams;
 	}
 
 	public function getParamDescription() {
 		$p = $this->getModulePrefix();
-		$link = $this->descriptionLink;
+		$what = $this->descriptionWhat;
+		$targets = $this->descriptionTargets;
 		$linking = $this->descriptionLinking;
-		return array(
-			'from' => "The title of the $link to start enumerating from",
-			'to' => "The title of the $link to stop enumerating at",
-			'prefix' => "Search for all $link titles that begin with this value",
+		$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 $link titles. Cannot be used with {$p}prop=ids.",
-					'When used as a generator, yields target pages instead of source pages.',
+				"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 the pageid of the $linking page (Cannot be used with {$p}unique)",
-				" title  - Adds the title of the $link",
+				" title  - Adds the title of the $what",
 			),
 			'namespace' => 'The namespace to enumerate',
-			'limit' => "How many total items 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() {
@@ -272,29 +308,31 @@ class ApiQueryAllLinks extends ApiQueryGeneratorBase {
 
 	public function getPossibleErrors() {
 		$m = $this->getModuleName();
-		$link = $this->descriptionLink;
+		$what = $this->descriptionWhat;
 		return array_merge( parent::getPossibleErrors(), array(
-			array( 'code' => 'params', 'info' => "{$m} cannot return corresponding page ids in unique {$link}s mode" ),
+			array( 'code' => 'params', 'info' => "{$m} cannot return corresponding page ids in unique {$what}s mode" ),
 		) );
 	}
 
 	public function getExamples() {
 		$p = $this->getModulePrefix();
-		$link = $this->descriptionLink;
-		$linked = $this->descriptionLinked;
+		$name = $this->getModuleName();
+		$what = $this->descriptionWhat;
+		$targets = $this->descriptionTargets;
 		return array(
-			"api.php?action=query&list=all{$link}s&{$p}from=B&{$p}prop=ids|title"
-					=> "List $linked titles with page ids they are from, including missing ones. Start at B",
-			"api.php?action=query&list=all{$link}s&{$p}unique=&{$p}from=B"
-					=> "List unique $linked titles",
-			"api.php?action=query&generator=all{$link}s&g{$p}unique=&g{$p}from=B"
-					=> "Gets all $link targets, marking the missing ones",
-			"api.php?action=query&generator=all{$link}s&g{$p}from=B"
-					=> "Gets pages containing the {$link}s",
+			"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:All{$this->descriptionLink}s";
+		$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 c9811b0d..d47c7b76 100644
--- a/includes/api/ApiQueryAllMessages.php
+++ b/includes/api/ApiQueryAllMessages.php
@@ -87,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;
diff --git a/includes/api/ApiQueryAllPages.php b/includes/api/ApiQueryAllPages.php
index d718b967..d95980c2 100644
--- a/includes/api/ApiQueryAllPages.php
+++ b/includes/api/ApiQueryAllPages.php
@@ -174,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;
@@ -304,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',
diff --git a/includes/api/ApiQueryAllUsers.php b/includes/api/ApiQueryAllUsers.php
index 7283aa00..1948a51a 100644
--- a/includes/api/ApiQueryAllUsers.php
+++ b/includes/api/ApiQueryAllUsers.php
@@ -83,12 +83,12 @@ class ApiQueryAllUsers extends ApiQueryBase {
 
 		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 ) ) {
+			if ( !count( $groups ) ) {
 				$this->getResult()->setIndexedTagName_internal( array( 'query', $this->getModuleName() ), '' );
 				return;
 			}
diff --git a/includes/api/ApiQueryBacklinks.php b/includes/api/ApiQueryBacklinks.php
index 3ef6b840..2d1089a7 100644
--- a/includes/api/ApiQueryBacklinks.php
+++ b/includes/api/ApiQueryBacklinks.php
@@ -229,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;
@@ -255,6 +255,9 @@ class ApiQueryBacklinks extends ApiQueryGeneratorBase {
 		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();
@@ -294,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;
@@ -377,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 );
diff --git a/includes/api/ApiQueryBase.php b/includes/api/ApiQueryBase.php
index 7819ead4..8668e04b 100644
--- a/includes/api/ApiQueryBase.php
+++ b/includes/api/ApiQueryBase.php
@@ -432,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 ) );
@@ -478,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 ) ) {
diff --git a/includes/api/ApiQueryBlocks.php b/includes/api/ApiQueryBlocks.php
index d9be9f28..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' );
 			}
-			$prefix = substr( $lower, 0, 4 );
+
+			# 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' );
+			}
+
+			# 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' );
 			}
@@ -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' ),
diff --git a/includes/api/ApiQueryCategories.php b/includes/api/ApiQueryCategories.php
index 69a64415..5d714f57 100644
--- a/includes/api/ApiQueryCategories.php
+++ b/includes/api/ApiQueryCategories.php
@@ -49,7 +49,6 @@ class ApiQueryCategories extends ApiQueryGeneratorBase {
 
 	/**
 	 * @param $resultPageSet ApiPageSet
-	 * @return
 	 */
 	private function run( $resultPageSet = null ) {
 		if ( $this->getPageSet()->getGoodTitleCount() == 0 ) {
@@ -174,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 );
 		}
@@ -184,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',
diff --git a/includes/api/ApiQueryCategoryMembers.php b/includes/api/ApiQueryCategoryMembers.php
index 9dbd8593..704d108a 100644
--- a/includes/api/ApiQueryCategoryMembers.php
+++ b/includes/api/ApiQueryCategoryMembers.php
@@ -217,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 ) {
@@ -258,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',
@@ -267,7 +267,7 @@ class ApiQueryCategoryMembers extends ApiQueryGeneratorBase {
 					'timestamp',
 				)
 			),
-			'namespace' => array (
+			'namespace' => array(
 				ApiBase::PARAM_ISMULTI => true,
 				ApiBase::PARAM_TYPE => 'namespace',
 			),
diff --git a/includes/api/ApiQueryDeletedrevs.php b/includes/api/ApiQueryDeletedrevs.php
index 890e4ecf..82733133 100644
--- a/includes/api/ApiQueryDeletedrevs.php
+++ b/includes/api/ApiQueryDeletedrevs.php
@@ -50,7 +50,7 @@ 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'] );
@@ -79,13 +79,13 @@ 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', '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' );
 				}
 			}
 		} 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' );
 				}
diff --git a/includes/api/ApiQueryDuplicateFiles.php b/includes/api/ApiQueryDuplicateFiles.php
index 18dcba85..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,7 +58,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
 		}
 		$images = $namespaces[NS_FILE];
 
-		if( $params['dir'] == 'descending' ) {
+		if ( $params['dir'] == 'descending' ) {
 			$images = array_reverse( $images );
 		}
 
@@ -80,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 );
@@ -98,29 +97,29 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
 
 		// 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;
@@ -139,7 +138,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
 						'user' => $dupFile->getUser( 'text' ),
 						'timestamp' => wfTimestamp( TS_ISO_8601, $dupFile->getTimestamp() )
 					);
-					if( !$dupFile->isLocal() ) {
+					if ( !$dupFile->isLocal() ) {
 						$r['shared'] = '';
 					}
 					$fit = $this->addPageSubItem( $pageId, $r );
@@ -149,7 +148,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
 					}
 				}
 			}
-			if( !$fit ) {
+			if ( !$fit ) {
 				break;
 			}
 		}
diff --git a/includes/api/ApiQueryExtLinksUsage.php b/includes/api/ApiQueryExtLinksUsage.php
index eb9cdf9e..456e87ba 100644
--- a/includes/api/ApiQueryExtLinksUsage.php
+++ b/includes/api/ApiQueryExtLinksUsage.php
@@ -123,7 +123,7 @@ class ApiQueryExtLinksUsage extends ApiQueryGeneratorBase {
 				if ( $fld_url ) {
 					$to = $row->el_to;
 					// expand protocol-relative urls
-					if( $params['expandurl'] ) {
+					if ( $params['expandurl'] ) {
 						$to = wfExpandUrl( $to, PROTO_CANONICAL );
 					}
 					$vals['url'] = $to;
@@ -218,13 +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.',
-			'expandurl' => 'Expand protocol-relative urls with the canonical protocol',
+			'expandurl' => 'Expand protocol-relative URLs with the canonical protocol',
 		);
 
 		if ( $wgMiserMode ) {
diff --git a/includes/api/ApiQueryExternalLinks.php b/includes/api/ApiQueryExternalLinks.php
index 761b49ea..583ef697 100644
--- a/includes/api/ApiQueryExternalLinks.php
+++ b/includes/api/ApiQueryExternalLinks.php
@@ -88,7 +88,7 @@ class ApiQueryExternalLinks extends ApiQueryBase {
 			$entry = array();
 			$to = $row->el_to;
 			// expand protocol-relative urls
-			if( $params['expandurl'] ) {
+			if ( $params['expandurl'] ) {
 				$to = wfExpandUrl( $to, PROTO_CANONICAL );
 			}
 			ApiResult::setContent( $entry, $to );
@@ -131,11 +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',
+			'expandurl' => 'Expand protocol-relative URLs with the canonical protocol',
 		);
 	}
 
@@ -148,7 +148,7 @@ class ApiQueryExternalLinks extends ApiQueryBase {
 	}
 
 	public function getDescription() {
-		return 'Returns all external urls (not interwikis) from the given page(s)';
+		return 'Returns all external URLs (not interwikis) from the given page(s)';
 	}
 
 	public function getPossibleErrors() {
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 @@
+
+ *
+ * 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 021074a9..f53cd386 100644
--- a/includes/api/ApiQueryFilearchive.php
+++ b/includes/api/ApiQueryFilearchive.php
@@ -218,7 +218,7 @@ class ApiQueryFilearchive extends ApiQueryBase {
 	}
 
 	public function getAllowedParams() {
-		return array (
+		return array(
 			'from' => null,
 			'continue' => null,
 			'to' => null,
@@ -281,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'
 			),
@@ -373,4 +373,8 @@ class ApiQueryFilearchive extends ApiQueryBase {
 			),
 		);
 	}
+
+	public function getHelpUrls() {
+		return 'https://www.mediawiki.org/wiki/API:Filearchive';
+	}
 }
diff --git a/includes/api/ApiQueryIWBacklinks.php b/includes/api/ApiQueryIWBacklinks.php
index b47d31f2..ebae3e76 100644
--- a/includes/api/ApiQueryIWBacklinks.php
+++ b/includes/api/ApiQueryIWBacklinks.php
@@ -239,4 +239,8 @@ class ApiQueryIWBacklinks extends ApiQueryGeneratorBase {
 			'api.php?action=query&generator=iwbacklinks&giwbltitle=Test&giwblprefix=wikibooks&prop=info'
 		);
 	}
+
+	public function getHelpUrls() {
+		return 'https://www.mediawiki.org/wiki/API:Iwbacklinks';
+	}
 }
diff --git a/includes/api/ApiQueryIWLinks.php b/includes/api/ApiQueryIWLinks.php
index fc77b4e6..be539311 100644
--- a/includes/api/ApiQueryIWLinks.php
+++ b/includes/api/ApiQueryIWLinks.php
@@ -81,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 {
@@ -90,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
 				));
 			}
 		}
@@ -192,4 +193,8 @@ class ApiQueryIWLinks extends ApiQueryBase {
 			'api.php?action=query&prop=iwlinks&titles=Main%20Page' => 'Get interwiki links from the [[Main Page]]',
 		);
 	}
+
+	public function getHelpUrls() {
+		return 'https://www.mediawiki.org/wiki/API:Iwlinks';
+	}
 }
diff --git a/includes/api/ApiQueryImageInfo.php b/includes/api/ApiQueryImageInfo.php
index 95c2745a..0ea28684 100644
--- a/includes/api/ApiQueryImageInfo.php
+++ b/includes/api/ApiQueryImageInfo.php
@@ -72,7 +72,7 @@ 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 );
@@ -82,12 +82,17 @@ class ApiQueryImageInfo extends ApiQueryBase {
 				$start = $title === $fromTitle ? $fromTimestamp : $params['start'];
 
 				if ( !isset( $images[$title] ) ) {
-					$result->addValue(
-						array( 'query', 'pages', intval( $pageId ) ),
-						'imagerepository', ''
-					);
-					// The above can't fail because it doesn't increase the result size
-					continue;
+					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;
+					}
 				}
 
 				/** @var $img File */
@@ -170,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',
@@ -199,21 +207,20 @@ class ApiQueryImageInfo extends ApiQueryBase {
 	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;
@@ -228,7 +235,21 @@ class ApiQueryImageInfo extends ApiQueryBase {
 	 * @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;
 		}
@@ -254,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']})" );
 			}
 		}
 
@@ -342,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 ) ) {
@@ -411,6 +433,10 @@ class ApiQueryImageInfo extends ApiQueryBase {
 			$vals['bitdepth'] = $file->getBitDepth();
 		}
 
+		if ( $uploadwarning ) {
+			$vals['html'] = SpecialUpload::getExistsWarning( UploadBase::getExistsWarning( $file ) );
+		}
+
 		return $vals;
 	}
 
@@ -461,7 +487,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
 		if ( $start === null ) {
 			$start = $img->getTimestamp();
 		}
-		return $img->getOriginalTitle()->getText() . '|' . $start;
+		return $img->getOriginalTitle()->getDBkey() . '|' . $start;
 	}
 
 	public function getAllowedParams() {
@@ -537,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',
 		);
 	}
 
@@ -547,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 = '' ) {
@@ -566,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.",
@@ -714,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 doesn't " .
-				"match the one in {$p}urlwidth" ),
 		) );
 	}
 
diff --git a/includes/api/ApiQueryInfo.php b/includes/api/ApiQueryInfo.php
index 37cd9159..017684ed 100644
--- a/includes/api/ApiQueryInfo.php
+++ b/includes/api/ApiQueryInfo.php
@@ -56,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' );
@@ -70,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' );
+		}
 	}
 
 	/**
@@ -348,6 +351,10 @@ 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;
 
@@ -476,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();
@@ -662,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;
 		}
 
@@ -819,7 +828,8 @@ class ApiQueryInfo extends ApiQueryBase {
 				'starttimestamp' => array(
 					ApiBase::PROP_TYPE => 'timestamp',
 					ApiBase::PROP_NULLABLE => true
-				)
+				),
+				'contentmodel' => 'string',
 			),
 			'watched' => array(
 				'watched' => 'boolean'
diff --git a/includes/api/ApiQueryLangBacklinks.php b/includes/api/ApiQueryLangBacklinks.php
index 7a4880a4..5bd451b6 100644
--- a/includes/api/ApiQueryLangBacklinks.php
+++ b/includes/api/ApiQueryLangBacklinks.php
@@ -195,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',
@@ -223,7 +223,8 @@ 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.',
 		);
 	}
 
@@ -239,4 +240,8 @@ class ApiQueryLangBacklinks extends ApiQueryGeneratorBase {
 			'api.php?action=query&generator=langbacklinks&glbltitle=Test&glbllang=fr&prop=info'
 		);
 	}
+
+	public function getHelpUrls() {
+		return 'https://www.mediawiki.org/wiki/API:Langbacklinks';
+	}
 }
diff --git a/includes/api/ApiQueryLangLinks.php b/includes/api/ApiQueryLangLinks.php
index ac65d2d2..aa796e31 100644
--- a/includes/api/ApiQueryLangLinks.php
+++ b/includes/api/ApiQueryLangLinks.php
@@ -67,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 ) {
diff --git a/includes/api/ApiQueryLogEvents.php b/includes/api/ApiQueryLogEvents.php
index 73dcea49..1a2719ed 100644
--- a/includes/api/ApiQueryLogEvents.php
+++ b/includes/api/ApiQueryLogEvents.php
@@ -49,12 +49,12 @@ 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'] );
 
@@ -67,7 +67,7 @@ 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',
@@ -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'] ) ) {
@@ -241,7 +240,7 @@ class ApiQueryLogEvents extends ApiQueryBase {
 				break;
 			case 'rights':
 				$vals2 = array();
-				if( $legacy ) {
+				if ( $legacy ) {
 					list( $vals2['old'], $vals2['new'] ) = $params;
 				} else {
 					$vals2['new'] = implode( ', ', $params['5::newgroups'] );
@@ -265,6 +264,11 @@ 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 ) ) {
 			$logParams = array();
@@ -331,10 +335,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 ) {
diff --git a/includes/api/ApiQueryORM.php b/includes/api/ApiQueryORM.php
index 41d8f11c..a23ff06b 100644
--- a/includes/api/ApiQueryORM.php
+++ b/includes/api/ApiQueryORM.php
@@ -228,7 +228,7 @@ abstract class ApiQueryORM extends ApiQueryBase {
 	 * @return array
 	 */
 	public function getAllowedParams() {
-		$params = array (
+		$params = array(
 			'props' => array(
 				ApiBase::PARAM_TYPE => $this->getTable()->getFieldNames(),
 				ApiBase::PARAM_ISMULTI => true,
@@ -252,7 +252,7 @@ abstract class ApiQueryORM extends ApiQueryBase {
 	 * @return array
 	 */
 	public function getParamDescription() {
-		$descriptions = array (
+		$descriptions = array(
 			'props' => 'Fields to query',
 			'continue' => 'Offset number from where to continue the query',
 			'limit' => 'Max amount of rows to return',
diff --git a/includes/api/ApiQueryPagesWithProp.php b/includes/api/ApiQueryPagesWithProp.php
index 0132fc3e..6f2f02e4 100644
--- a/includes/api/ApiQueryPagesWithProp.php
+++ b/includes/api/ApiQueryPagesWithProp.php
@@ -133,7 +133,7 @@ class ApiQueryPagesWithProp extends ApiQueryGeneratorBase {
 			'prop' => array(
 				ApiBase::PARAM_DFLT => 'ids|title',
 				ApiBase::PARAM_ISMULTI => true,
-				ApiBase::PARAM_TYPE => array (
+				ApiBase::PARAM_TYPE => array(
 					'ids',
 					'title',
 					'value',
diff --git a/includes/api/ApiQueryProtectedTitles.php b/includes/api/ApiQueryProtectedTitles.php
index 4aa00007..222ad074 100644
--- a/includes/api/ApiQueryProtectedTitles.php
+++ b/includes/api/ApiQueryProtectedTitles.php
@@ -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,
diff --git a/includes/api/ApiQueryQueryPage.php b/includes/api/ApiQueryQueryPage.php
index b03bdfb8..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];
 			}
 		}
@@ -222,4 +213,8 @@ class ApiQueryQueryPage extends ApiQueryGeneratorBase {
 			'api.php?action=query&list=querypage&qppage=Ancientpages'
 		);
 	}
+
+	public function getHelpUrls() {
+		return 'https://www.mediawiki.org/wiki/API:Querypage';
+	}
 }
diff --git a/includes/api/ApiQueryRandom.php b/includes/api/ApiQueryRandom.php
index ae3bb893..2754bdae 100644
--- a/includes/api/ApiQueryRandom.php
+++ b/includes/api/ApiQueryRandom.php
@@ -185,4 +185,8 @@ class ApiQueryRandom extends ApiQueryGeneratorBase {
 	public function getExamples() {
 		return 'api.php?action=query&list=random&rnnamespace=0&rnlimit=2';
 	}
+
+	public function getHelpUrls() {
+		return 'https://www.mediawiki.org/wiki/API:Random';
+	}
 }
diff --git a/includes/api/ApiQueryRecentChanges.php b/includes/api/ApiQueryRecentChanges.php
index 8aceab22..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;
 
@@ -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() {
@@ -273,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' ) ) ) );
@@ -287,8 +294,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
 			$this->addTables( 'change_tag' );
 			$this->addJoinConds( array( 'change_tag' => array( 'INNER JOIN', array( 'rc_id=ct_rc_id' ) ) ) );
 			$this->addWhereFld( 'ct_tag', $params['tag'] );
-			global $wgOldChangeTagsIndex;
-			$index['change_tag'] = $wgOldChangeTagsIndex ? 'ct_tag' : 'change_tag_tag_id';
+			$index['change_tag'] = 'change_tag_tag_id';
 		}
 
 		$this->token = $params['token'];
@@ -475,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 ) {
@@ -499,7 +518,7 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
 			}
 			return $retval;
 		}
-		switch( $type ) {
+		switch ( $type ) {
 			case 'edit':
 				return RC_EDIT;
 			case 'new':
@@ -571,7 +590,8 @@ class ApiQueryRecentChanges extends ApiQueryGeneratorBase {
 					'redirect',
 					'patrolled',
 					'loginfo',
-					'tags'
+					'tags',
+					'sha1',
 				)
 			),
 			'token' => array(
@@ -638,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(
@@ -735,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() );
diff --git a/includes/api/ApiQueryRevisions.php b/includes/api/ApiQueryRevisions.php
index 192fe873..415288ef 100644
--- a/includes/api/ApiQueryRevisions.php
+++ b/includes/api/ApiQueryRevisions.php
@@ -146,17 +146,17 @@ 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_contentmodel = isset ( $prop['contentmodel'] );
+		$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'] ) ) {
@@ -189,8 +189,7 @@ class ApiQueryRevisions extends ApiQueryBase {
 			$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';
+			$index['change_tag'] = 'change_tag_tag_id';
 		}
 
 		if ( isset( $prop['content'] ) || !is_null( $this->difftotext ) ) {
@@ -520,7 +519,7 @@ class ApiQueryRevisions extends ApiQueryBase {
 				} else {
 					$this->setWarning( "Conversion to XML is supported for wikitext only, " .
 										$title->getPrefixedDBkey() .
-										" uses content model " . $content->getModel() . ")" );
+										" uses content model " . $content->getModel() );
 				}
 			}
 
@@ -533,7 +532,7 @@ class ApiQueryRevisions extends ApiQueryBase {
 				} else {
 					$this->setWarning( "Template expansion is supported for wikitext only, " .
 						$title->getPrefixedDBkey() .
-						" uses content model " . $content->getModel() . ")" );
+						" uses content model " . $content->getModel() );
 
 					$text = false;
 				}
@@ -550,7 +549,7 @@ class ApiQueryRevisions extends ApiQueryBase {
 				if ( !$content->isSupportedFormat( $format ) ) {
 					$name = $title->getPrefixedDBkey();
 
-					$this->dieUsage( "The requested format {$this->contentFormat} is not supported ".
+					$this->dieUsage( "The requested format {$this->contentFormat} is not supported " .
 									"for content model $model used by $name", 'badformat' );
 				}
 
@@ -593,7 +592,7 @@ class ApiQueryRevisions extends ApiQueryBase {
 
 						$name = $title->getPrefixedDBkey();
 
-						$this->dieUsage( "The requested format {$this->contentFormat} is not supported for ".
+						$this->dieUsage( "The requested format {$this->contentFormat} is not supported for " .
 											"content model $model used by $name", 'badformat' );
 					}
 
diff --git a/includes/api/ApiQuerySearch.php b/includes/api/ApiQuerySearch.php
index 86183391..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();
@@ -199,7 +210,9 @@ class ApiQuerySearch extends ApiQueryGeneratorBase {
 	}
 
 	public function getAllowedParams() {
-		return array(
+		global $wgSearchType;
+
+		$params = array(
 			'search' => array(
 				ApiBase::PARAM_TYPE => 'string',
 				ApiBase::PARAM_REQUIRED => 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' ),
 		) );
 	}
 
diff --git a/includes/api/ApiQuerySiteinfo.php b/includes/api/ApiQuerySiteinfo.php
index 810e1d6b..a7767062 100644
--- a/includes/api/ApiQuerySiteinfo.php
+++ b/includes/api/ApiQuerySiteinfo.php
@@ -121,14 +121,29 @@ class ApiQuerySiteinfo extends ApiQueryBase {
 		$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;
 		$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'] = '';
 		}
@@ -170,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;
@@ -281,6 +296,8 @@ class ApiQuerySiteinfo extends ApiQueryBase {
 			$data[] = $item;
 		}
 
+		sort( $data );
+
 		$this->getResult()->setIndexedTagName( $data, 'ns' );
 		return $this->getResult()->addValue( 'query', $property, $data );
 	}
@@ -345,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'];
 			}
 
@@ -404,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();
@@ -456,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' );
@@ -520,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 );
@@ -544,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' );
@@ -661,13 +690,15 @@ 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',
@@ -675,7 +706,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
 				' 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)',
@@ -687,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() {
diff --git a/includes/api/ApiQueryTags.php b/includes/api/ApiQueryTags.php
index e0637ff7..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',
@@ -195,4 +194,8 @@ class ApiQueryTags extends ApiQueryBase {
 			'api.php?action=query&list=tags&tgprop=displayname|description|hitcount'
 		);
 	}
+
+	public function getHelpUrls() {
+		return 'https://www.mediawiki.org/wiki/API:Tags';
+	}
 }
diff --git a/includes/api/ApiQueryUserContributions.php b/includes/api/ApiQueryUserContributions.php
index 597c412d..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;
 				}
 			}
@@ -255,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 ) {
@@ -268,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'] ) {
@@ -297,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 );
@@ -474,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',
diff --git a/includes/api/ApiQueryUserInfo.php b/includes/api/ApiQueryUserInfo.php
index 1a491eca..3c85ea69 100644
--- a/includes/api/ApiQueryUserInfo.php
+++ b/includes/api/ApiQueryUserInfo.php
@@ -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() );
 
diff --git a/includes/api/ApiQueryUsers.php b/includes/api/ApiQueryUsers.php
index 72ab7866..dccfee67 100644
--- a/includes/api/ApiQueryUsers.php
+++ b/includes/api/ApiQueryUsers.php
@@ -127,7 +127,7 @@ class ApiQueryUsers extends ApiQueryBase {
 				$this->addFields( array( 'user_name', 'ug_group' ) );
 				$userGroupsRes = $this->select( __METHOD__ );
 
-				foreach( $userGroupsRes as $row ) {
+				foreach ( $userGroupsRes as $row ) {
 					$userGroups[$row->user_name][] = $row->ug_group;
 				}
 			}
@@ -149,7 +149,7 @@ class ApiQueryUsers extends ApiQueryBase {
 				$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'] ) ) {
@@ -204,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 ) {
diff --git a/includes/api/ApiQueryWatchlist.php b/includes/api/ApiQueryWatchlist.php
index 90b12c14..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'] );
@@ -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 );
@@ -312,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,
@@ -321,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'
 			),
@@ -376,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'
 			),
@@ -415,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'
 		);
@@ -423,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',
diff --git a/includes/api/ApiQueryWatchlistRaw.php b/includes/api/ApiQueryWatchlistRaw.php
index 2cb4d9eb..ea4e724a 100644
--- a/includes/api/ApiQueryWatchlistRaw.php
+++ b/includes/api/ApiQueryWatchlistRaw.php
@@ -222,4 +222,8 @@ class ApiQueryWatchlistRaw extends ApiQueryGeneratorBase {
 			'api.php?action=query&generator=watchlistraw&gwrshow=changed&prop=revisions',
 		);
 	}
+
+	public function getHelpUrls() {
+		return 'https://www.mediawiki.org/wiki/API:Watchlistraw';
+	}
 }
diff --git a/includes/api/ApiRsd.php b/includes/api/ApiRsd.php
index c4a1328c..d219c91c 100644
--- a/includes/api/ApiRsd.php
+++ b/includes/api/ApiRsd.php
@@ -40,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' );
 
diff --git a/includes/api/ApiSetNotificationTimestamp.php b/includes/api/ApiSetNotificationTimestamp.php
index 58d5d9ab..53a68fde 100644
--- a/includes/api/ApiSetNotificationTimestamp.php
+++ b/includes/api/ApiSetNotificationTimestamp.php
@@ -39,6 +39,9 @@ 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' );
@@ -212,7 +215,7 @@ class ApiSetNotificationTimestamp extends ApiBase {
 	}
 
 	public function getParamDescription() {
-		return $this->getPageSet()->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)',
@@ -270,7 +273,7 @@ class ApiSetNotificationTimestamp extends ApiBase {
 		$ps = $this->getPageSet();
 		return array_merge(
 			parent::getPossibleErrors(),
-			$ps->getPossibleErrors(),
+			$ps->getFinalPossibleErrors(),
 			$this->getRequireMaxOneParameterErrorMessages(
 				array( 'timestamp', 'torevid', 'newerthanrevid' ) ),
 			$this->getRequireOnlyOneParameterErrorMessages(
diff --git a/includes/api/ApiUpload.php b/includes/api/ApiUpload.php
index 7d67aa6e..467eccf8 100644
--- a/includes/api/ApiUpload.php
+++ b/includes/api/ApiUpload.php
@@ -56,15 +56,19 @@ class ApiUpload extends ApiBase {
 		$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() ) {
-			return; // not a true upload, but a status request or similar
-		} elseif ( !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
@@ -82,7 +86,7 @@ class ApiUpload extends ApiBase {
 		// Check if the uploaded file is sane
 		if ( $this->mParams['chunk'] ) {
 			$maxSize = $this->mUpload->getMaxUploadSize();
-			if( $this->mParams['filesize'] > $maxSize ) {
+			if ( $this->mParams['filesize'] > $maxSize ) {
 				$this->dieUsage( 'The file you submitted was too large', 'file-too-large' );
 			}
 			if ( !$this->mUpload->getTitle() ) {
@@ -106,9 +110,13 @@ class ApiUpload extends ApiBase {
 		}
 
 		// Get the result based on the current upload context:
-		$result = $this->getContextResult();
-		if ( $result['result'] === 'Success' ) {
-			$result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() );
+		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 );
@@ -144,7 +152,7 @@ class ApiUpload extends ApiBase {
 	 * @return array
 	 */
 	private function getStashResult( $warnings ) {
-		$result = array ();
+		$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 {
@@ -216,27 +224,27 @@ class ApiUpload extends ApiBase {
 		// Check we added the last chunk:
 		if ( $this->mParams['offset'] + $chunkSize == $this->mParams['filesize'] ) {
 			if ( $this->mParams['async'] ) {
-				$progress = UploadBase::getSessionStatus( $this->mParams['filekey'] );
+				$progress = UploadBase::getSessionStatus( $filekey );
 				if ( $progress && $progress['result'] === 'Poll' ) {
 					$this->dieUsage( "Chunk assembly already in progress.", 'stashfailed' );
 				}
 				UploadBase::setSessionStatus(
-					$this->mParams['filekey'],
+					$filekey,
 					array( 'result' => 'Poll',
 						'stage' => 'queued', 'status' => Status::newGood() )
 				);
 				$ok = JobQueueGroup::singleton()->push( new AssembleUploadChunksJob(
-					Title::makeTitle( NS_FILE, $this->mParams['filekey'] ),
+					Title::makeTitle( NS_FILE, $filekey ),
 					array(
-						'filename'  => $this->mParams['filename'],
-						'filekey'   => $this->mParams['filekey'],
-						'session'   => $this->getContext()->exportSession()
+						'filename' => $this->mParams['filename'],
+						'filekey' => $filekey,
+						'session' => $this->getContext()->exportSession()
 					)
 				) );
 				if ( $ok ) {
 					$result['result'] = 'Poll';
 				} else {
-					UploadBase::setSessionStatus( $this->mParams['filekey'], false );
+					UploadBase::setSessionStatus( $filekey, false );
 					$this->dieUsage(
 						"Failed to start AssembleUploadChunks.php", 'stashfailed' );
 				}
@@ -358,11 +366,9 @@ class ApiUpload extends ApiBase {
 		}
 
 		if ( $this->mParams['chunk'] ) {
-			$this->checkChunkedEnabled();
-
 			// Chunk upload
 			$this->mUpload = new UploadFromChunks();
-			if( isset( $this->mParams['filekey'] ) ) {
+			if ( isset( $this->mParams['filekey'] ) ) {
 				// handle new chunk
 				$this->mUpload->continueChunks(
 					$this->mParams['filename'],
@@ -404,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();
@@ -452,9 +462,9 @@ class ApiUpload extends ApiBase {
 		$verification = $this->mUpload->verifyUpload();
 		if ( $verification['status'] === UploadBase::OK ) {
 			return;
-		} else {
-			return $this->checkVerification( $verification );
 		}
+
+		$this->checkVerification( $verification );
 	}
 
 	/**
@@ -463,8 +473,8 @@ class ApiUpload extends ApiBase {
 	protected function checkVerification( array $verification ) {
 		global $wgFileExtensions;
 
-		// TODO: Move them to ApiBase's message map
-		switch( $verification['status'] ) {
+		// @todo Move them to ApiBase's message map
+		switch ( $verification['status'] ) {
 			// Recoverable errors
 			case UploadBase::MIN_LENGTH_PARTNAME:
 				$this->dieRecoverableError( 'filename-tooshort', 'filename' );
@@ -494,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' );
 
@@ -555,7 +565,8 @@ 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;
@@ -596,12 +607,12 @@ class ApiUpload extends ApiBase {
 			$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()
+					'filename' => $this->mParams['filename'],
+					'filekey' => $this->mParams['filekey'],
+					'comment' => $this->mParams['comment'],
+					'text' => $this->mParams['text'],
+					'watch' => $watch,
+					'session' => $this->getContext()->exportSession()
 				)
 			) );
 			if ( $ok ) {
@@ -653,13 +664,6 @@ class ApiUpload extends ApiBase {
 		}
 	}
 
-	protected function checkChunkedEnabled() {
-		global $wgAllowChunkedUploads;
-		if ( !$wgAllowChunkedUploads ) {
-			$this->dieUsage( 'Chunked uploads disabled', 'chunkeduploaddisabled' );
-		}
-	}
-
 	public function mustBePosted() {
 		return true;
 	}
@@ -816,7 +820,7 @@ class ApiUpload extends ApiBase {
 				array( 'code' => 'publishfailed', 'info' => 'Publishing of stashed file failed' ),
 				array( 'code' => 'internal-error', 'info' => 'An internal error occurred' ),
 				array( 'code' => 'asynccopyuploaddisabled', 'info' => 'Asynchronous copy uploads disabled' ),
-				array( 'code' => 'chunkeduploaddisabled', 'info' => 'Chunked uploads disabled' ),
+				array( 'code' => 'stasherror', 'info' => 'An upload stash error occurred' ),
 				array( 'fileexists-forbidden' ),
 				array( 'fileexists-shared-forbidden' ),
 			)
diff --git a/includes/api/ApiUserrights.php b/includes/api/ApiUserrights.php
index b9b1eeda..7d308285 100644
--- a/includes/api/ApiUserrights.php
+++ b/includes/api/ApiUserrights.php
@@ -38,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'] ) =
@@ -62,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;
 		}
@@ -83,7 +84,7 @@ class ApiUserrights extends ApiBase {
 	}
 
 	public function getAllowedParams() {
-		return array (
+		return array(
 			'user' => array(
 				ApiBase::PARAM_TYPE => 'string',
 				ApiBase::PARAM_REQUIRED => true
diff --git a/includes/api/ApiWatch.php b/includes/api/ApiWatch.php
index 3e51299f..c7d636a1 100644
--- a/includes/api/ApiWatch.php
+++ b/includes/api/ApiWatch.php
@@ -36,6 +36,9 @@ class ApiWatch extends ApiBase {
 		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'] );
@@ -57,19 +60,19 @@ class ApiWatch extends ApiBase {
 		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 ( !is_null( $oldLang ) ) {
 			$this->getContext()->setLanguage( $oldLang ); // Reset language to $oldLang
 		}
 
-		if ( !$success ) {
-			$this->dieUsageMsg( 'hookaborted' );
+		if ( !$status->isOK() ) {
+			$this->dieStatus( $status );
 		}
 		$this->getResult()->addValue( null, $this->getModuleName(), $res );
 	}
-- 
cgit v1.2.2