API changes for AuthManager
authorBrad Jorsch <bjorsch@wikimedia.org>
Mon, 11 Jan 2016 18:20:22 +0000 (13:20 -0500)
committerGergő Tisza <gtisza@wikimedia.org>
Mon, 16 May 2016 15:12:52 +0000 (15:12 +0000)
Changes here are:
* action=login is deprecated for use other than bot passwords
* list=users will indicate if a missing user name is creatable.
* Added action=query&meta=authmanagerinfo
* Added action=clientlogin is to be used to log into the main account
* action=createaccount is changed in a non-BC manner
* Added action=linkaccount
* Added action=unlinkaccount
* Added action=changeauthenticationdata
* Added action=removeauthenticationdata
* Added action=resetpassword

Bug: T110276
Bug: T110747
Bug: T110751
Bug: T32788
Bug: T67857
Bug: T28597
Bug: T76103
Change-Id: I244fa9b1e0623247d6d9fa30990411c6df94a496

23 files changed:
autoload.php
includes/Setup.php
includes/api/ApiAMCreateAccount.php [new file with mode: 0644]
includes/api/ApiAuthManagerHelper.php [new file with mode: 0644]
includes/api/ApiChangeAuthenticationData.php [new file with mode: 0644]
includes/api/ApiClientLogin.php [new file with mode: 0644]
includes/api/ApiCreateAccount.php
includes/api/ApiFormatJson.php
includes/api/ApiLinkAccount.php [new file with mode: 0644]
includes/api/ApiLogin.php
includes/api/ApiMain.php
includes/api/ApiPageSet.php
includes/api/ApiQuery.php
includes/api/ApiQueryAuthManagerInfo.php [new file with mode: 0644]
includes/api/ApiQueryUsers.php
includes/api/ApiRemoveAuthenticationData.php [new file with mode: 0644]
includes/api/ApiResetPassword.php [new file with mode: 0644]
includes/api/i18n/en.json
includes/api/i18n/qqq.json
includes/password/PasswordPolicyChecks.php
tests/phpunit/includes/api/ApiCreateAccountTest.php [deleted file]
tests/phpunit/includes/api/ApiLoginTest.php
tests/phpunit/includes/api/ApiTestCase.php

index 70417e9..6cfffad 100644 (file)
@@ -17,10 +17,14 @@ $wgAutoloadLocalClasses = [
        'AlterSharedConstraints' => __DIR__ . '/maintenance/oracle/alterSharedConstraints.php',
        'AncientPagesPage' => __DIR__ . '/includes/specials/SpecialAncientpages.php',
        'AnsiTermColorer' => __DIR__ . '/maintenance/term/MWTerm.php',
+       'ApiAMCreateAccount' => __DIR__ . '/includes/api/ApiAMCreateAccount.php',
+       'ApiAuthManagerHelper' => __DIR__ . '/includes/api/ApiAuthManagerHelper.php',
        'ApiBase' => __DIR__ . '/includes/api/ApiBase.php',
        'ApiBlock' => __DIR__ . '/includes/api/ApiBlock.php',
+       'ApiChangeAuthenticationData' => __DIR__ . '/includes/api/ApiChangeAuthenticationData.php',
        'ApiCheckToken' => __DIR__ . '/includes/api/ApiCheckToken.php',
        'ApiClearHasMsg' => __DIR__ . '/includes/api/ApiClearHasMsg.php',
+       'ApiClientLogin' => __DIR__ . '/includes/api/ApiClientLogin.php',
        'ApiComparePages' => __DIR__ . '/includes/api/ApiComparePages.php',
        'ApiContinuationManager' => __DIR__ . '/includes/api/ApiContinuationManager.php',
        'ApiCreateAccount' => __DIR__ . '/includes/api/ApiCreateAccount.php',
@@ -48,6 +52,7 @@ $wgAutoloadLocalClasses = [
        'ApiImageRotate' => __DIR__ . '/includes/api/ApiImageRotate.php',
        'ApiImport' => __DIR__ . '/includes/api/ApiImport.php',
        'ApiImportReporter' => __DIR__ . '/includes/api/ApiImport.php',
+       'ApiLinkAccount' => __DIR__ . '/includes/api/ApiLinkAccount.php',
        'ApiLogin' => __DIR__ . '/includes/api/ApiLogin.php',
        'ApiLogout' => __DIR__ . '/includes/api/ApiLogout.php',
        'ApiMain' => __DIR__ . '/includes/api/ApiMain.php',
@@ -75,6 +80,7 @@ $wgAutoloadLocalClasses = [
        'ApiQueryAllPages' => __DIR__ . '/includes/api/ApiQueryAllPages.php',
        'ApiQueryAllRevisions' => __DIR__ . '/includes/api/ApiQueryAllRevisions.php',
        'ApiQueryAllUsers' => __DIR__ . '/includes/api/ApiQueryAllUsers.php',
+       'ApiQueryAuthManagerInfo' => __DIR__ . '/includes/api/ApiQueryAuthManagerInfo.php',
        'ApiQueryBacklinks' => __DIR__ . '/includes/api/ApiQueryBacklinks.php',
        'ApiQueryBacklinksprop' => __DIR__ . '/includes/api/ApiQueryBacklinksprop.php',
        'ApiQueryBase' => __DIR__ . '/includes/api/ApiQueryBase.php',
@@ -123,6 +129,8 @@ $wgAutoloadLocalClasses = [
        'ApiQueryWatchlist' => __DIR__ . '/includes/api/ApiQueryWatchlist.php',
        'ApiQueryWatchlistRaw' => __DIR__ . '/includes/api/ApiQueryWatchlistRaw.php',
        'ApiRawMessage' => __DIR__ . '/includes/api/ApiMessage.php',
+       'ApiRemoveAuthenticationData' => __DIR__ . '/includes/api/ApiRemoveAuthenticationData.php',
+       'ApiResetPassword' => __DIR__ . '/includes/api/ApiResetPassword.php',
        'ApiResult' => __DIR__ . '/includes/api/ApiResult.php',
        'ApiRevisionDelete' => __DIR__ . '/includes/api/ApiRevisionDelete.php',
        'ApiRollback' => __DIR__ . '/includes/api/ApiRollback.php',
index e76ec2c..e57b96a 100644 (file)
@@ -450,6 +450,22 @@ if ( $wgProfileOnly ) {
        $wgDebugLogFile = '';
 }
 
+// Disable AuthManager API modules if $wgDisableAuthManager
+if ( $wgDisableAuthManager ) {
+       $wgAPIModules += [
+               'clientlogin' => 'ApiDisabled',
+               'createaccount' => 'ApiCreateAccount', // Use the non-AuthManager version
+               'linkaccount' => 'ApiDisabled',
+               'unlinkaccount' => 'ApiDisabled',
+               'changeauthenticationdata' => 'ApiDisabled',
+               'removeauthenticationdata' => 'ApiDisabled',
+               'resetpassword' => 'ApiDisabled',
+       ];
+       $wgAPIMetaModules += [
+               'authmanagerinfo' => 'ApiQueryDisabled',
+       ];
+}
+
 // Backwards compatibility with old password limits
 if ( $wgMinimalPasswordLength !== false ) {
        $wgPasswordPolicy['policies']['default']['MinimalPasswordLength'] = $wgMinimalPasswordLength;
diff --git a/includes/api/ApiAMCreateAccount.php b/includes/api/ApiAMCreateAccount.php
new file mode 100644 (file)
index 0000000..806b8d2
--- /dev/null
@@ -0,0 +1,132 @@
+<?php
+/**
+ * Copyright © 2016 Brad Jorsch <bjorsch@wikimedia.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+
+/**
+ * Create an account with AuthManager
+ *
+ * @ingroup API
+ */
+class ApiAMCreateAccount extends ApiBase {
+
+       public function __construct( ApiMain $main, $action ) {
+               parent::__construct( $main, $action, 'create' );
+       }
+
+       public function getFinalDescription() {
+               // A bit of a hack to append 'api-help-authmanager-general-usage'
+               $msgs = parent::getFinalDescription();
+               $msgs[] = ApiBase::makeMessage( 'api-help-authmanager-general-usage', $this->getContext(), [
+                       $this->getModulePrefix(),
+                       $this->getModuleName(),
+                       $this->getModulePath(),
+                       AuthManager::ACTION_CREATE,
+                       self::needsToken(),
+               ] );
+               return $msgs;
+       }
+
+       public function execute() {
+               $params = $this->extractRequestParams();
+
+               $this->requireAtLeastOneParameter( $params, 'continue', 'returnurl' );
+
+               if ( $params['returnurl'] !== null ) {
+                       $bits = wfParseUrl( $params['returnurl'] );
+                       if ( !$bits || $bits['scheme'] === '' ) {
+                               $encParamName = $this->encodeParamName( 'returnurl' );
+                               $this->dieUsage(
+                                       "Invalid value '{$params['returnurl']}' for url parameter $encParamName",
+                                       "badurl_{$encParamName}"
+                               );
+                       }
+               }
+
+               $helper = new ApiAuthManagerHelper( $this );
+               $manager = AuthManager::singleton();
+
+               // Make sure it's possible to log in
+               if ( !$manager->canCreateAccounts() ) {
+                       $this->getResult()->addValue( null, 'createaccount', $helper->formatAuthenticationResponse(
+                               AuthenticationResponse::newFail(
+                                       $this->msg( 'userlogin-cannot-' . AuthManager::ACTION_CREATE )
+                               )
+                       ) );
+                       return;
+               }
+
+               // Perform the create step
+               if ( $params['continue'] ) {
+                       $reqs = $helper->loadAuthenticationRequests( AuthManager::ACTION_CREATE_CONTINUE );
+                       $res = $manager->continueAccountCreation( $reqs );
+               } else {
+                       $reqs = $helper->loadAuthenticationRequests( AuthManager::ACTION_CREATE );
+                       if ( $params['preservestate'] ) {
+                               $req = $helper->getPreservedRequest();
+                               if ( $req ) {
+                                       $reqs[] = $req;
+                               }
+                       }
+                       $res = $manager->beginAccountCreation( $this->getUser(), $reqs, $params['returnurl'] );
+               }
+
+               $this->getResult()->addValue( null, 'createaccount',
+                       $helper->formatAuthenticationResponse( $res ) );
+       }
+
+       public function isReadMode() {
+               return false;
+       }
+
+       public function isWriteMode() {
+               return true;
+       }
+
+       public function needsToken() {
+               return 'createaccount';
+       }
+
+       public function getAllowedParams() {
+               return ApiAuthManagerHelper::getStandardParams( AuthManager::ACTION_CREATE,
+                       'requests', 'messageformat', 'mergerequestfields', 'preservestate', 'returnurl', 'continue'
+               );
+       }
+
+       public function dynamicParameterDocumentation() {
+               return [ 'api-help-authmanagerhelper-additional-params', AuthManager::ACTION_CREATE ];
+       }
+
+       protected function getExamplesMessages() {
+               return [
+                       'action=createaccount&username=Example&password=ExamplePassword&retype=ExamplePassword'
+                               . '&createreturnurl=http://example.org/&createtoken=123ABC'
+                               => 'apihelp-createaccount-example-create',
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Account_creation';
+       }
+}
diff --git a/includes/api/ApiAuthManagerHelper.php b/includes/api/ApiAuthManagerHelper.php
new file mode 100644 (file)
index 0000000..2997405
--- /dev/null
@@ -0,0 +1,365 @@
+<?php
+/**
+ * Copyright © 2016 Brad Jorsch <bjorsch@wikimedia.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.27
+ */
+
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\CreateFromLoginAuthenticationRequest;
+
+/**
+ * Helper class for AuthManager-using API modules. Intended for use via
+ * composition.
+ *
+ * @ingroup API
+ */
+class ApiAuthManagerHelper {
+
+       /** @var ApiBase API module, for context and parameters */
+       private $module;
+
+       /** @var string Message output format */
+       private $messageFormat;
+
+       /**
+        * @param ApiBase $module API module, for context and parameters
+        */
+       public function __construct( ApiBase $module ) {
+               $this->module = $module;
+
+               $params = $module->extractRequestParams();
+               $this->messageFormat = isset( $params['messageformat'] ) ? $params['messageformat'] : 'wikitext';
+       }
+
+       /**
+        * Static version of the constructor, for chaining
+        * @param ApiBase $module API module, for context and parameters
+        * @return ApiAuthManagerHelper
+        */
+       public static function newForModule( ApiBase $module ) {
+               return new self( $module );
+       }
+
+       /**
+        * Format a message for output
+        * @param array &$res Result array
+        * @param string $key Result key
+        * @param Message $message
+        */
+       private function formatMessage( array &$res, $key, Message $message ) {
+               switch ( $this->messageFormat ) {
+                       case 'none':
+                               break;
+
+                       case 'wikitext':
+                               $res[$key] = $message->setContext( $this->module )->text();
+                               break;
+
+                       case 'html':
+                               $res[$key] = $message->setContext( $this->module )->parseAsBlock();
+                               $res[$key] = Parser::stripOuterParagraph( $res[$key] );
+                               break;
+
+                       case 'raw':
+                               $res[$key] = [
+                                       'key' => $message->getKey(),
+                                       'params' => $message->getParams(),
+                               ];
+                               break;
+               }
+       }
+
+       /**
+        * Call $manager->securitySensitiveOperationStatus()
+        * @param string $operation Operation being checked.
+        * @throws UsageException
+        */
+       public function securitySensitiveOperation( $operation ) {
+               $status = AuthManager::singleton()->securitySensitiveOperationStatus( $operation );
+               switch ( $status ) {
+                       case AuthManager::SEC_OK:
+                               return;
+
+                       case AuthManager::SEC_REAUTH:
+                               $this->module->dieUsage(
+                                       'You have not authenticated recently in this session, please reauthenticate.', 'reauthenticate'
+                               );
+
+                       case AuthManager::SEC_FAIL:
+                               $this->module->dieUsage(
+                                       'This action is not available as your identify cannot be verified.', 'cannotreauthenticate'
+                               );
+
+                       default:
+                               throw new UnexpectedValueException( "Unknown status \"$status\"" );
+               }
+       }
+
+       /**
+        * Filter out authentication requests by class name
+        * @param AuthenticationRequest[] $reqs Requests to filter
+        * @param string[] $blacklist Class names to remove
+        * @return AuthenticationRequest[]
+        */
+       public static function blacklistAuthenticationRequests( array $reqs, array $blacklist ) {
+               if ( $blacklist ) {
+                       $blacklist = array_flip( $blacklist );
+                       $reqs = array_filter( $reqs, function ( $req ) use ( $blacklist ) {
+                               return !isset( $blacklist[get_class( $req )] );
+                       } );
+               }
+               return $reqs;
+       }
+
+       /**
+        * Fetch and load the AuthenticationRequests for an action
+        * @param string $action One of the AuthManager::ACTION_* constants
+        * @return AuthenticationRequest[]
+        */
+       public function loadAuthenticationRequests( $action ) {
+               $params = $this->module->extractRequestParams();
+
+               $manager = AuthManager::singleton();
+               $reqs = $manager->getAuthenticationRequests( $action, $this->module->getUser() );
+
+               // Filter requests, if requested to do so
+               $wantedRequests = null;
+               if ( isset( $params['requests'] ) ) {
+                       $wantedRequests = array_flip( $params['requests'] );
+               } elseif ( isset( $params['request'] ) ) {
+                       $wantedRequests = [ $params['request'] => true ];
+               }
+               if ( $wantedRequests !== null ) {
+                       $reqs = array_filter( $reqs, function ( $req ) use ( $wantedRequests ) {
+                               return isset( $wantedRequests[$req->getUniqueId()] );
+                       } );
+               }
+
+               // Collect the fields for all the requests
+               $fields = [];
+               foreach ( $reqs as $req ) {
+                       $fields += (array)$req->getFieldInfo();
+               }
+
+               // Extract the request data for the fields and mark those request
+               // parameters as used
+               $data = array_intersect_key( $this->module->getRequest()->getValues(), $fields );
+               $this->module->getMain()->markParamsUsed( array_keys( $data ) );
+
+               return AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
+       }
+
+       /**
+        * Format an AuthenticationResponse for return
+        * @param AuthenticationResponse $res
+        * @return array
+        */
+       public function formatAuthenticationResponse( AuthenticationResponse $res ) {
+               $params = $this->module->extractRequestParams();
+
+               $ret = [
+                       'status' => $res->status,
+               ];
+
+               if ( $res->status === AuthenticationResponse::PASS && $res->username !== null ) {
+                       $ret['username'] = $res->username;
+               }
+
+               if ( $res->status === AuthenticationResponse::REDIRECT ) {
+                       $ret['redirecttarget'] = $res->redirectTarget;
+                       if ( $res->redirectApiData !== null ) {
+                               $ret['redirectdata'] = $res->redirectApiData;
+                       }
+               }
+
+               if ( $res->status === AuthenticationResponse::REDIRECT ||
+                       $res->status === AuthenticationResponse::UI ||
+                       $res->status === AuthenticationResponse::RESTART
+               ) {
+                       $ret += $this->formatRequests( $res->neededRequests );
+               }
+
+               if ( $res->status === AuthenticationResponse::FAIL ||
+                       $res->status === AuthenticationResponse::UI ||
+                       $res->status === AuthenticationResponse::RESTART
+               ) {
+                       $this->formatMessage( $ret, 'message', $res->message );
+               }
+
+               if ( $res->status === AuthenticationResponse::FAIL ||
+                       $res->status === AuthenticationResponse::RESTART
+               ) {
+                       $this->module->getRequest()->getSession()->set(
+                               'ApiAuthManagerHelper::createRequest',
+                               $res->createRequest
+                       );
+                       $ret['canpreservestate'] = $res->createRequest !== null;
+               } else {
+                       $this->module->getRequest()->getSession()->remove( 'ApiAuthManagerHelper::createRequest' );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Fetch the preserved CreateFromLoginAuthenticationRequest, if any
+        * @return CreateFromLoginAuthenticationRequest|null
+        */
+       public function getPreservedRequest() {
+               $ret = $this->module->getRequest()->getSession()->get( 'ApiAuthManagerHelper::createRequest' );
+               return $ret instanceof CreateFromLoginAuthenticationRequest ? $ret : null;
+       }
+
+       /**
+        * Format an array of AuthenticationRequests for return
+        * @param AuthenticationRequest[] $reqs
+        * @return array Will have a 'requests' key, and also 'fields' if $module's
+        *  params include 'mergerequestfields'.
+        */
+       public function formatRequests( array $reqs ) {
+               $params = $this->module->extractRequestParams();
+               $mergeFields = !empty( $params['mergerequestfields'] );
+
+               $ret = [ 'requests' => [] ];
+               foreach ( $reqs as $req ) {
+                       $describe = $req->describeCredentials();
+                       $reqInfo = [
+                               'id' => $req->getUniqueId(),
+                               'metadata' => $req->getMetadata(),
+                       ];
+                       switch ( $req->required ) {
+                               case AuthenticationRequest::OPTIONAL:
+                                       $reqInfo['required'] = 'optional';
+                                       break;
+                               case AuthenticationRequest::REQUIRED:
+                                       $reqInfo['required'] = 'required';
+                                       break;
+                               case AuthenticationRequest::PRIMARY_REQUIRED:
+                                       $reqInfo['required'] = 'primary-required';
+                                       break;
+                       }
+                       $this->formatMessage( $reqInfo, 'provider', $describe['provider'] );
+                       $this->formatMessage( $reqInfo, 'account', $describe['account'] );
+                       if ( !$mergeFields ) {
+                               $reqInfo['fields'] = $this->formatFields( (array)$req->getFieldInfo() );
+                       }
+                       $ret['requests'][] = $reqInfo;
+               }
+
+               if ( $mergeFields ) {
+                       $fields = AuthenticationRequest::mergeFieldInfo( $reqs );
+                       $ret['fields'] = $this->formatFields( $fields );
+               }
+
+               return $ret;
+       }
+
+       /**
+        * Clean up a field array for output
+        * @param ApiBase $module For context and parameters 'mergerequestfields'
+        *  and 'messageformat'
+        * @param array $fields
+        * @return array
+        */
+       private function formatFields( array $fields ) {
+               static $copy = [
+                       'type' => true,
+                       'image' => true,
+                       'value' => true,
+               ];
+
+               $module = $this->module;
+               $retFields = [];
+
+               foreach ( $fields as $name => $field ) {
+                       $ret = array_intersect_key( $field, $copy );
+
+                       if ( isset( $field['options'] ) ) {
+                               $ret['options'] = array_map( function ( $msg ) use ( $module ) {
+                                       return $msg->setContext( $module )->plain();
+                               }, $field['options'] );
+                               ApiResult::setArrayType( $ret['options'], 'assoc' );
+                       }
+                       $this->formatMessage( $ret, 'label', $field['label'] );
+                       $this->formatMessage( $ret, 'help', $field['help'] );
+                       $ret['optional'] = !empty( $field['optional'] );
+
+                       $retFields[$name] = $ret;
+               }
+
+               ApiResult::setArrayType( $retFields, 'assoc' );
+
+               return $retFields;
+       }
+
+       /**
+        * Fetch the standard parameters this helper recognizes
+        * @param string $action AuthManager action
+        * @param string $param... Parameters to use
+        * @return array
+        */
+       public static function getStandardParams( $action, $param /* ... */ ) {
+               $params = [
+                       'requests' => [
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_ISMULTI => true,
+                               ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-requests', $action ],
+                       ],
+                       'request' => [
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_REQUIRED => true,
+                               ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-request', $action ],
+                       ],
+                       'messageformat' => [
+                               ApiBase::PARAM_DFLT => 'wikitext',
+                               ApiBase::PARAM_TYPE => [ 'html', 'wikitext', 'raw', 'none' ],
+                               ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-messageformat',
+                       ],
+                       'mergerequestfields' => [
+                               ApiBase::PARAM_DFLT => false,
+                               ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-mergerequestfields',
+                       ],
+                       'preservestate' => [
+                               ApiBase::PARAM_DFLT => false,
+                               ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-preservestate',
+                       ],
+                       'returnurl' => [
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-returnurl',
+                       ],
+                       'continue' => [
+                               ApiBase::PARAM_DFLT => false,
+                               ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-continue',
+                       ],
+               ];
+
+               $ret = [];
+               $wantedParams = func_get_args();
+               array_shift( $wantedParams );
+               foreach ( $wantedParams as $name ) {
+                       if ( isset( $params[$name] ) ) {
+                               $ret[$name] = $params[$name];
+                       }
+               }
+               return $ret;
+       }
+}
diff --git a/includes/api/ApiChangeAuthenticationData.php b/includes/api/ApiChangeAuthenticationData.php
new file mode 100644 (file)
index 0000000..54547ef
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Copyright © 2016 Brad Jorsch <bjorsch@wikimedia.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * Change authentication data with AuthManager
+ *
+ * @ingroup API
+ */
+class ApiChangeAuthenticationData extends ApiBase {
+
+       public function __construct( ApiMain $main, $action ) {
+               parent::__construct( $main, $action, 'changeauth' );
+       }
+
+       public function execute() {
+               if ( !$this->getUser()->isLoggedIn() ) {
+                       $this->dieUsage( 'Must be logged in to change authentication data', 'notloggedin' );
+               }
+
+               $helper = new ApiAuthManagerHelper( $this );
+               $manager = AuthManager::singleton();
+
+               // Check security-sensitive operation status
+               $helper->securitySensitiveOperation( 'ChangeCredentials' );
+
+               // Fetch the request
+               $reqs = ApiAuthManagerHelper::blacklistAuthenticationRequests(
+                       $helper->loadAuthenticationRequests( AuthManager::ACTION_CHANGE ),
+                       $this->getConfig()->get( 'ChangeCredentialsBlacklist' )
+               );
+               if ( count( $reqs ) !== 1 ) {
+                       $this->dieUsage( 'Failed to create change request', 'badrequest' );
+               }
+               $req = reset( $reqs );
+
+               // Make the change
+               $status = $manager->allowsAuthenticationDataChange( $req, true );
+               if ( !$status->isGood() ) {
+                       $this->dieStatus( $status );
+               }
+               $manager->changeAuthenticationData( $req );
+
+               $this->getResult()->addValue( null, 'changeauthenticationdata', [ 'status' => 'success' ] );
+       }
+
+       public function isWriteMode() {
+               return true;
+       }
+
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       public function getAllowedParams() {
+               return ApiAuthManagerHelper::getStandardParams( AuthManager::ACTION_CHANGE,
+                       'request'
+               );
+       }
+
+       public function dynamicParameterDocumentation() {
+               return [ 'api-help-authmanagerhelper-additional-params', AuthManager::ACTION_CHANGE ];
+       }
+
+       protected function getExamplesMessages() {
+               return [
+                       'action=changeauthenticationdata' .
+                               '&changeauthrequest=MediaWiki%5CAuth%5CPasswordAuthenticationRequest' .
+                               '&password=ExamplePassword&retype=ExamplePassword&changeauthtoken=123ABC'
+                               => 'apihelp-changeauthenticationdata-example-password',
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Manage_authentication_data';
+       }
+}
diff --git a/includes/api/ApiClientLogin.php b/includes/api/ApiClientLogin.php
new file mode 100644 (file)
index 0000000..711234a
--- /dev/null
@@ -0,0 +1,128 @@
+<?php
+/**
+ * Copyright © 2016 Brad Jorsch <bjorsch@wikimedia.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+
+/**
+ * Log in to the wiki with AuthManager
+ *
+ * @ingroup API
+ */
+class ApiClientLogin extends ApiBase {
+
+       public function __construct( ApiMain $main, $action ) {
+               parent::__construct( $main, $action, 'login' );
+       }
+
+       public function getFinalDescription() {
+               // A bit of a hack to append 'api-help-authmanager-general-usage'
+               $msgs = parent::getFinalDescription();
+               $msgs[] = ApiBase::makeMessage( 'api-help-authmanager-general-usage', $this->getContext(), [
+                       $this->getModulePrefix(),
+                       $this->getModuleName(),
+                       $this->getModulePath(),
+                       AuthManager::ACTION_LOGIN,
+                       self::needsToken(),
+               ] );
+               return $msgs;
+       }
+
+       public function execute() {
+               $params = $this->extractRequestParams();
+
+               $this->requireAtLeastOneParameter( $params, 'continue', 'returnurl' );
+
+               if ( $params['returnurl'] !== null ) {
+                       $bits = wfParseUrl( $params['returnurl'] );
+                       if ( !$bits || $bits['scheme'] === '' ) {
+                               $encParamName = $this->encodeParamName( 'returnurl' );
+                               $this->dieUsage(
+                                       "Invalid value '{$params['returnurl']}' for url parameter $encParamName",
+                                       "badurl_{$encParamName}"
+                               );
+                       }
+               }
+
+               $helper = new ApiAuthManagerHelper( $this );
+               $manager = AuthManager::singleton();
+
+               // Make sure it's possible to log in
+               if ( !$manager->canAuthenticateNow() ) {
+                       $this->getResult()->addValue( null, 'clientlogin', $helper->formatAuthenticationResponse(
+                               AuthenticationResponse::newFail( $this->msg( 'userlogin-cannot-' . AuthManager::ACTION_LOGIN ) )
+                       ) );
+                       return;
+               }
+
+               // Perform the login step
+               if ( $params['continue'] ) {
+                       $reqs = $helper->loadAuthenticationRequests( AuthManager::ACTION_LOGIN_CONTINUE );
+                       $res = $manager->continueAuthentication( $reqs );
+               } else {
+                       $reqs = $helper->loadAuthenticationRequests( AuthManager::ACTION_LOGIN );
+                       if ( $params['preservestate'] ) {
+                               $req = $helper->getPreservedRequest();
+                               if ( $req ) {
+                                       $reqs[] = $req;
+                               }
+                       }
+                       $res = $manager->beginAuthentication( $reqs, $params['returnurl'] );
+               }
+
+               $this->getResult()->addValue( null, 'clientlogin',
+                       $helper->formatAuthenticationResponse( $res ) );
+       }
+
+       public function isReadMode() {
+               return false;
+       }
+
+       public function needsToken() {
+               return 'login';
+       }
+
+       public function getAllowedParams() {
+               return ApiAuthManagerHelper::getStandardParams( AuthManager::ACTION_LOGIN,
+                       'requests', 'messageformat', 'mergerequestfields', 'preservestate', 'returnurl', 'continue'
+               );
+       }
+
+       public function dynamicParameterDocumentation() {
+               return [ 'api-help-authmanagerhelper-additional-params', AuthManager::ACTION_LOGIN ];
+       }
+
+       protected function getExamplesMessages() {
+               return [
+                       'action=clientlogin&username=Example&password=ExamplePassword&'
+                               . 'loginreturnurl=http://example.org/&logintoken=123ABC'
+                               => 'apihelp-clientlogin-example-login',
+                       'action=clientlogin&logincontinue=1&OATHToken=987654&logintoken=123ABC'
+                               => 'apihelp-clientlogin-example-login2',
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Login';
+       }
+}
index 5552a85..6a48610 100644 (file)
@@ -27,6 +27,7 @@ use MediaWiki\Logger\LoggerFactory;
  * Unit to authenticate account registration attempts to the current wiki.
  *
  * @ingroup API
+ * @deprecated since 1.27, only used when $wgDisableAuthManager is true
  */
 class ApiCreateAccount extends ApiBase {
        public function execute() {
index 36cbbd9..41de925 100644 (file)
@@ -42,7 +42,7 @@ class ApiFormatJson extends ApiFormatBase {
                        # outside the control of the end user.
                        # (and do it here because ApiMain::reportUnusedParams() gets called
                        # before our ::execute())
-                       $this->getMain()->getCheck( '_' );
+                       $this->getMain()->markParamsUsed( '_' );
                }
        }
 
diff --git a/includes/api/ApiLinkAccount.php b/includes/api/ApiLinkAccount.php
new file mode 100644 (file)
index 0000000..14347d8
--- /dev/null
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Copyright © 2016 Brad Jorsch <bjorsch@wikimedia.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
+
+/**
+ * Link an account with AuthManager
+ *
+ * @ingroup API
+ */
+class ApiLinkAccount extends ApiBase {
+
+       public function __construct( ApiMain $main, $action ) {
+               parent::__construct( $main, $action, 'link' );
+       }
+
+       public function getFinalDescription() {
+               // A bit of a hack to append 'api-help-authmanager-general-usage'
+               $msgs = parent::getFinalDescription();
+               $msgs[] = ApiBase::makeMessage( 'api-help-authmanager-general-usage', $this->getContext(), [
+                       $this->getModulePrefix(),
+                       $this->getModuleName(),
+                       $this->getModulePath(),
+                       AuthManager::ACTION_LINK,
+                       self::needsToken(),
+               ] );
+               return $msgs;
+       }
+
+       public function execute() {
+               if ( !$this->getUser()->isLoggedIn() ) {
+                       $this->dieUsage( 'Must be logged in to link accounts', 'notloggedin' );
+               }
+
+               $params = $this->extractRequestParams();
+
+               $this->requireAtLeastOneParameter( $params, 'continue', 'returnurl' );
+
+               if ( $params['returnurl'] !== null ) {
+                       $bits = wfParseUrl( $params['returnurl'] );
+                       if ( !$bits || $bits['scheme'] === '' ) {
+                               $encParamName = $this->encodeParamName( 'returnurl' );
+                               $this->dieUsage(
+                                       "Invalid value '{$params['returnurl']}' for url parameter $encParamName",
+                                       "badurl_{$encParamName}"
+                               );
+                       }
+               }
+
+               $helper = new ApiAuthManagerHelper( $this );
+               $manager = AuthManager::singleton();
+
+               // Check security-sensitive operation status
+               $helper->securitySensitiveOperation( 'LinkAccounts' );
+
+               // Make sure it's possible to link accounts
+               if ( !$manager->canLinkAccounts() ) {
+                       $this->getResult()->addValue( null, 'linkaccount', $helper->formatAuthenticationResponse(
+                               AuthenticationResponse::newFail( $this->msg( 'userlogin-cannot-' . AuthManager::ACTION_LINK ) )
+                       ) );
+                       return;
+               }
+
+               // Perform the link step
+               if ( $params['continue'] ) {
+                       $reqs = $helper->loadAuthenticationRequests( AuthManager::ACTION_LINK_CONTINUE );
+                       $res = $manager->continueAccountLink( $reqs );
+               } else {
+                       $reqs = $helper->loadAuthenticationRequests( AuthManager::ACTION_LINK );
+                       $res = $manager->beginAccountLink( $this->getUser(), $reqs, $params['returnurl'] );
+               }
+
+               $this->getResult()->addValue( null, 'linkaccount',
+                       $helper->formatAuthenticationResponse( $res ) );
+       }
+
+       public function isReadMode() {
+               return false;
+       }
+
+       public function isWriteMode() {
+               return true;
+       }
+
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       public function getAllowedParams() {
+               return ApiAuthManagerHelper::getStandardParams( AuthManager::ACTION_LINK,
+                       'requests', 'messageformat', 'mergerequestfields', 'returnurl', 'continue'
+               );
+       }
+
+       public function dynamicParameterDocumentation() {
+               return [ 'api-help-authmanagerhelper-additional-params', AuthManager::ACTION_LINK ];
+       }
+
+       protected function getExamplesMessages() {
+               return [
+                       'action=linkaccount&provider=Example&linkreturnurl=http://example.org/&linktoken=123ABC'
+                               => 'apihelp-linkaccount-example-link',
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Linkaccount';
+       }
+}
index 3891415..3572229 100644 (file)
@@ -25,6 +25,9 @@
  * @file
  */
 
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AuthenticationRequest;
+use MediaWiki\Auth\AuthenticationResponse;
 use MediaWiki\Logger\LoggerFactory;
 
 /**
@@ -38,6 +41,16 @@ class ApiLogin extends ApiBase {
                parent::__construct( $main, $action, 'lg' );
        }
 
+       protected function getDescriptionMessage() {
+               if ( $this->getConfig()->get( 'DisableAuthManager' ) ) {
+                       return 'apihelp-login-description-nonauthmanager';
+               } elseif ( $this->getConfig()->get( 'EnableBotPasswords' ) ) {
+                       return 'apihelp-login-description';
+               } else {
+                       return 'apihelp-login-description-nobotpasswords';
+               }
+       }
+
        /**
         * Executes the log-in attempt using the parameters passed. If
         * the log-in succeeds, it attaches a cookie to the session
@@ -83,11 +96,11 @@ class ApiLogin extends ApiBase {
                $loginType = 'N/A';
 
                // Check login token
-               $token = LoginForm::getLoginToken();
+               $token = $session->getToken( '', 'login' );
                if ( $token->wasNew() || !$params['token'] ) {
-                       $authRes = LoginForm::NEED_TOKEN;
+                       $authRes = 'NeedToken';
                } elseif ( !$token->match( $params['token'] ) ) {
-                       $authRes = LoginForm::WRONG_TOKEN;
+                       $authRes = 'WrongToken';
                }
 
                // Try bot passwords
@@ -99,48 +112,104 @@ class ApiLogin extends ApiBase {
                        );
                        if ( $status->isOK() ) {
                                $session = $status->getValue();
-                               $authRes = LoginForm::SUCCESS;
+                               $authRes = 'Success';
                                $loginType = 'BotPassword';
                        } else {
+                               $authRes = 'Failed';
+                               $message = $status->getMessage();
                                LoggerFactory::getInstance( 'authmanager' )->info(
                                        'BotPassword login failed: ' . $status->getWikiText( false, false, 'en' )
                                );
                        }
                }
 
-               // Normal login
                if ( $authRes === false ) {
-                       $context->setRequest( new DerivativeRequest(
-                               $this->getContext()->getRequest(),
-                               [
-                                       'wpName' => $params['name'],
-                                       'wpPassword' => $params['password'],
-                                       'wpDomain' => $params['domain'],
-                                       'wpLoginToken' => $params['token'],
-                                       'wpRemember' => ''
-                               ]
-                       ) );
-                       $loginForm = new LoginForm();
-                       $loginForm->setContext( $context );
-                       $authRes = $loginForm->authenticateUserData();
-                       $loginType = 'LoginForm';
+                       if ( $this->getConfig()->get( 'DisableAuthManager' ) ) {
+                               // Non-AuthManager login
+                               $context->setRequest( new DerivativeRequest(
+                                       $this->getContext()->getRequest(),
+                                       [
+                                               'wpName' => $params['name'],
+                                               'wpPassword' => $params['password'],
+                                               'wpDomain' => $params['domain'],
+                                               'wpLoginToken' => $params['token'],
+                                               'wpRemember' => ''
+                                       ]
+                               ) );
+                               $loginForm = new LoginForm();
+                               $loginForm->setContext( $context );
+                               $authRes = $loginForm->authenticateUserData();
+                               $loginType = 'LoginForm';
+
+                               switch ( $authRes ) {
+                                       case LoginForm::SUCCESS:
+                                               $authRes = 'Success';
+                                               break;
+                                       case LoginForm::NEED_TOKEN:
+                                               $authRes = 'NeedToken';
+                                               break;
+                               }
+                       } else {
+                               // Simplified AuthManager login, for backwards compatibility
+                               $manager = AuthManager::singleton();
+                               $reqs = AuthenticationRequest::loadRequestsFromSubmission(
+                                       $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN, $this->getUser() ),
+                                       [
+                                               'username' => $params['name'],
+                                               'password' => $params['password'],
+                                               'domain' => $params['domain'],
+                                               'rememberMe' => true,
+                                       ]
+                               );
+                               $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' );
+                               switch ( $res->status ) {
+                                       case AuthenticationResponse::PASS:
+                                               if ( $this->getConfig()->get( 'EnableBotPasswords' ) ) {
+                                                       $warn = 'Main-account login via action=login is deprecated and may stop working ' .
+                                                               'without warning.';
+                                                       $warn .= ' To continue login with action=login, see [[Special:BotPasswords]].';
+                                                       $warn .= ' To safely continue using main-account login, see action=clientlogin.';
+                                               } else {
+                                                       $warn = 'Login via action=login is deprecated and may stop working without warning.';
+                                                       $warn .= ' To safely log in, see action=clientlogin.';
+                                               }
+                                               $this->setWarning( $warn );
+                                               $authRes = 'Success';
+                                               $loginType = 'AuthManager';
+                                               break;
+
+                                       case AuthenticationResponse::FAIL:
+                                               // Hope it's not a PreAuthenticationProvider that failed...
+                                               $authRes = 'Failed';
+                                               $message = $res->message;
+                                               \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
+                                                       ->info( __METHOD__ . ': Authentication failed: ' . $message->plain() );
+                                               break;
+
+                                       default:
+                                               $authRes = 'Aborted';
+                                               break;
+                               }
+                       }
                }
 
+               $result['result'] = $authRes;
                switch ( $authRes ) {
-                       case LoginForm::SUCCESS:
-                               $user = $context->getUser();
-                               $this->getContext()->setUser( $user );
-                               $user->setCookies( $this->getRequest(), null, true );
+                       case 'Success':
+                               if ( $this->getConfig()->get( 'DisableAuthManager' ) ) {
+                                       $user = $context->getUser();
+                                       $this->getContext()->setUser( $user );
+                                       $user->setCookies( $this->getRequest(), null, true );
+                               } else {
+                                       $user = $session->getUser();
+                               }
 
                                ApiQueryInfo::resetTokenCache();
 
-                               // Run hooks.
-                               // @todo FIXME: Split back and frontend from this hook.
-                               // @todo FIXME: This hook should be placed in the backend
+                               // Deprecated hook
                                $injected_html = '';
                                Hooks::run( 'UserLoginComplete', [ &$user, &$injected_html ] );
 
-                               $result['result'] = 'Success';
                                $result['lguserid'] = intval( $user->getId() );
                                $result['lgusername'] = $user->getName();
 
@@ -148,15 +217,14 @@ class ApiLogin extends ApiBase {
                                // point (1.28 at the earliest, and see T121527). They were ok
                                // when the core cookie-based login was the only thing, but
                                // CentralAuth broke that a while back and
-                               // SessionManager/AuthManager are *really* going to break it.
+                               // SessionManager/AuthManager *really* break it.
                                $result['lgtoken'] = $user->getToken();
                                $result['cookieprefix'] = $this->getConfig()->get( 'CookiePrefix' );
                                $result['sessionid'] = $session->getId();
                                break;
 
-                       case LoginForm::NEED_TOKEN:
-                               $result['result'] = 'NeedToken';
-                               $result['token'] = LoginForm::getLoginToken()->toString();
+                       case 'NeedToken':
+                               $result['token'] = $token->toString();
                                $this->setWarning( 'Fetching a token via action=login is deprecated. ' .
                                   'Use action=query&meta=tokens&type=login instead.' );
                                $this->logFeatureUsage( 'action=login&!lgtoken' );
@@ -166,6 +234,25 @@ class ApiLogin extends ApiBase {
                                $result['sessionid'] = $session->getId();
                                break;
 
+                       case 'WrongToken':
+                               break;
+
+                       case 'Failed':
+                               $result['reason'] = $message->useDatabase( 'false' )->inLanguage( 'en' )->text();
+                               break;
+
+                       case 'Aborted':
+                               $result['reason'] = 'Authentication requires user interaction, ' .
+                                  'which is not supported by action=login.';
+                               if ( $this->getConfig()->get( 'EnableBotPasswords' ) ) {
+                                       $result['reason'] .= ' To be able to login with action=login, see [[Special:BotPasswords]].';
+                                       $result['reason'] .= ' To continue using main-account login, see action=clientlogin.';
+                               } else {
+                                       $result['reason'] .= ' To log in, see action=clientlogin.';
+                               }
+                               break;
+
+                       // Results from LoginForm for when $wgDisableAuthManager is true
                        case LoginForm::WRONG_TOKEN:
                                $result['result'] = 'WrongToken';
                                break;
@@ -230,14 +317,22 @@ class ApiLogin extends ApiBase {
 
                $this->getResult()->addValue( null, 'login', $result );
 
+               if ( $loginType === 'LoginForm' && isset( LoginForm::$statusCodes[$authRes] ) ) {
+                       $authRes = LoginForm::$statusCodes[$authRes];
+               }
                LoggerFactory::getInstance( 'authmanager' )->info( 'Login attempt', [
                        'event' => 'login',
-                       'successful' => $authRes === LoginForm::SUCCESS,
+                       'successful' => $authRes === 'Success',
                        'loginType' => $loginType,
-                       'status' => LoginForm::$statusCodes[$authRes],
+                       'status' => $authRes,
                ] );
        }
 
+       public function isDeprecated() {
+               return !$this->getConfig()->get( 'DisableAuthManager' ) &&
+                       !$this->getConfig()->get( 'EnableBotPasswords' );
+       }
+
        public function mustBePosted() {
                return true;
        }
index 15cea44..b944385 100644 (file)
@@ -49,8 +49,14 @@ class ApiMain extends ApiBase {
         */
        private static $Modules = [
                'login' => 'ApiLogin',
+               'clientlogin' => 'ApiClientLogin',
                'logout' => 'ApiLogout',
-               'createaccount' => 'ApiCreateAccount',
+               'createaccount' => 'ApiAMCreateAccount',
+               'linkaccount' => 'ApiLinkAccount',
+               'unlinkaccount' => 'ApiRemoveAuthenticationData',
+               'changeauthenticationdata' => 'ApiChangeAuthenticationData',
+               'removeauthenticationdata' => 'ApiRemoveAuthenticationData',
+               'resetpassword' => 'ApiResetPassword',
                'query' => 'ApiQuery',
                'expandtemplates' => 'ApiExpandTemplates',
                'parse' => 'ApiParse',
@@ -1438,6 +1444,14 @@ class ApiMain extends ApiBase {
                return array_keys( $this->mParamsUsed );
        }
 
+       /**
+        * Mark parameters as used
+        * @param string|string[] $params
+        */
+       public function markParamsUsed( $params ) {
+               $this->mParamsUsed += array_fill_keys( (array)$params, true );
+       }
+
        /**
         * Get a request value, and register the fact that it was used, for logging.
         * @param string $name
index f278989..af4e536 100644 (file)
@@ -178,7 +178,7 @@ class ApiPageSet extends ApiBase {
                                // Prevent warnings from being reported on these parameters
                                $main = $this->getMain();
                                foreach ( $generator->extractRequestParams() as $paramName => $param ) {
-                                       $main->getVal( $generator->encodeParamName( $paramName ) );
+                                       $main->markParamsUsed( $generator->encodeParamName( $paramName ) );
                                }
                        }
 
index 733ea2c..3ca4c08 100644 (file)
@@ -112,6 +112,7 @@ class ApiQuery extends ApiBase {
         */
        private static $QueryMetaModules = [
                'allmessages' => 'ApiQueryAllMessages',
+               'authmanagerinfo' => 'ApiQueryAuthManagerInfo',
                'siteinfo' => 'ApiQuerySiteinfo',
                'userinfo' => 'ApiQueryUserInfo',
                'filerepoinfo' => 'ApiQueryFileRepoInfo',
diff --git a/includes/api/ApiQueryAuthManagerInfo.php b/includes/api/ApiQueryAuthManagerInfo.php
new file mode 100644 (file)
index 0000000..b591f9c
--- /dev/null
@@ -0,0 +1,116 @@
+<?php
+/**
+ * Copyright © 2016 Brad Jorsch <bjorsch@wikimedia.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @since 1.27
+ */
+
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * A query action to return meta information about AuthManager state.
+ *
+ * @ingroup API
+ */
+class ApiQueryAuthManagerInfo extends ApiQueryBase {
+
+       public function __construct( ApiQuery $query, $moduleName ) {
+               parent::__construct( $query, $moduleName, 'ami' );
+       }
+
+       public function execute() {
+               $params = $this->extractRequestParams();
+               $helper = new ApiAuthManagerHelper( $this );
+
+               $manager = AuthManager::singleton();
+               $ret = [
+                       'canauthenticatenow' => $manager->canAuthenticateNow(),
+                       'cancreateaccounts' => $manager->canCreateAccounts(),
+                       'canlinkaccounts' => $manager->canLinkAccounts(),
+                       'haspreservedstate' => $helper->getPreservedRequest() !== null,
+               ];
+
+               if ( $params['securitysensitiveoperation'] !== null ) {
+                       $ret['securitysensitiveoperationstatus'] = $manager->securitySensitiveOperationStatus(
+                               $params['securitysensitiveoperation']
+                       );
+               }
+
+               if ( $params['requestsfor'] ) {
+                       $reqs = $manager->getAuthenticationRequests( $params['requestsfor'], $this->getUser() );
+
+                       // Filter out blacklisted requests, depending on the action
+                       switch ( $params['requestsfor'] ) {
+                               case AuthManager::ACTION_CHANGE:
+                                       $reqs = ApiAuthManagerHelper::blacklistAuthenticationRequests(
+                                               $reqs, $this->getConfig()->get( 'ChangeCredentialsBlacklist' )
+                                       );
+                                       break;
+                               case AuthManager::ACTION_REMOVE:
+                                       $reqs = ApiAuthManagerHelper::blacklistAuthenticationRequests(
+                                               $reqs, $this->getConfig()->get( 'RemoveCredentialsBlacklist' )
+                                       );
+                                       break;
+                       }
+
+                       $ret += $helper->formatRequests( $reqs );
+               }
+
+               $this->getResult()->addValue( [ 'query' ], $this->getModuleName(), $ret );
+       }
+
+       public function getCacheMode( $params ) {
+               return 'public';
+       }
+
+       public function getAllowedParams() {
+               return [
+                       'securitysensitiveoperation' => null,
+                       'requestsfor' => [
+                               ApiBase::PARAM_TYPE => [
+                                       AuthManager::ACTION_LOGIN,
+                                       AuthManager::ACTION_LOGIN_CONTINUE,
+                                       AuthManager::ACTION_CREATE,
+                                       AuthManager::ACTION_CREATE_CONTINUE,
+                                       AuthManager::ACTION_LINK,
+                                       AuthManager::ACTION_LINK_CONTINUE,
+                                       AuthManager::ACTION_CHANGE,
+                                       AuthManager::ACTION_REMOVE,
+                                       AuthManager::ACTION_UNLINK,
+                               ],
+                       ],
+               ] + ApiAuthManagerHelper::getStandardParams( '', 'mergerequestfields' );
+       }
+
+       protected function getExamplesMessages() {
+               return [
+                       'action=query&meta=authmanagerinfo&amirequestsfor=' . urlencode( AuthManager::ACTION_LOGIN )
+                               => 'apihelp-query+filerepoinfo-example-login',
+                       'action=query&meta=authmanagerinfo&amirequestsfor=' . urlencode( AuthManager::ACTION_LOGIN ) .
+                               '&amimergerequestfields=1'
+                               => 'apihelp-query+filerepoinfo-example-login-merged',
+                       'action=query&meta=authmanagerinfo&amisecuritysensitiveoperation=foo'
+                               => 'apihelp-query+filerepoinfo-example-securitysensitiveoperation',
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Authmanagerinfo';
+       }
+}
index ee6ec76..68ec38d 100644 (file)
@@ -49,6 +49,7 @@ class ApiQueryUsers extends ApiQueryBase {
                'emailable',
                'gender',
                'centralids',
+               'cancreate',
        ];
 
        public function __construct( ApiQuery $query, $moduleName ) {
@@ -260,6 +261,10 @@ class ApiQueryUsers extends ApiQueryBase {
                                        }
                                } else {
                                        $data[$u]['missing'] = true;
+                                       if ( isset( $this->prop['cancreate'] ) && !$this->getConfig()->get( 'DisableAuthManager' ) ) {
+                                               $data[$u]['cancreate'] = MediaWiki\Auth\AuthManager::singleton()->canCreateAccount( $u )
+                                                       ->isGood();
+                                       }
                                }
                        } else {
                                if ( isset( $this->prop['groups'] ) && isset( $data[$u]['groups'] ) ) {
@@ -299,7 +304,7 @@ class ApiQueryUsers extends ApiQueryBase {
        }
 
        public function getAllowedParams() {
-               return [
+               $ret = [
                        'prop' => [
                                ApiBase::PARAM_ISMULTI => true,
                                ApiBase::PARAM_TYPE => [
@@ -312,6 +317,8 @@ class ApiQueryUsers extends ApiQueryBase {
                                        'emailable',
                                        'gender',
                                        'centralids',
+                                       // When adding a prop, consider whether it should be added
+                                       // to self::$publicProps
                                ],
                                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
                        ],
@@ -326,6 +333,10 @@ class ApiQueryUsers extends ApiQueryBase {
                                ApiBase::PARAM_ISMULTI => true
                        ],
                ];
+               if ( !$this->getConfig()->get( 'DisableAuthManager' ) ) {
+                       $ret['prop'][ApiBase::PARAM_TYPE][] = 'cancreate';
+               }
+               return $ret;
        }
 
        protected function getExamplesMessages() {
diff --git a/includes/api/ApiRemoveAuthenticationData.php b/includes/api/ApiRemoveAuthenticationData.php
new file mode 100644 (file)
index 0000000..30e40fb
--- /dev/null
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Copyright © 2016 Brad Jorsch <bjorsch@wikimedia.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * Remove authentication data from AuthManager
+ *
+ * @ingroup API
+ */
+class ApiRemoveAuthenticationData extends ApiBase {
+
+       private $authAction;
+       private $operation;
+
+       public function __construct( ApiMain $main, $action ) {
+               parent::__construct( $main, $action );
+
+               $this->authAction = $action === 'unlinkaccount'
+                       ? AuthManager::ACTION_UNLINK
+                       : AuthManager::ACTION_REMOVE;
+               $this->operation = $action === 'unlinkaccount'
+                       ? 'UnlinkAccount'
+                       : 'RemoveCredentials';
+       }
+
+       public function execute() {
+               if ( !$this->getUser()->isLoggedIn() ) {
+                       $this->dieUsage( 'Must be logged in to remove authentication data', 'notloggedin' );
+               }
+
+               $params = $this->extractRequestParams();
+               $manager = AuthManager::singleton();
+
+               // Check security-sensitive operation status
+               ApiAuthManagerHelper::newForModule( $this )->securitySensitiveOperation( $this->operation );
+
+               // Fetch the request. No need to load from the request, so don't use
+               // ApiAuthManagerHelper's method.
+               $blacklist = $this->authAction === AuthManager::ACTION_REMOVE
+                       ? array_flip( $this->getConfig()->get( 'RemoveCredentialsBlacklist' ) )
+                       : [];
+               $reqs = array_filter(
+                       $manager->getAuthenticationRequests( $this->authAction, $this->getUser() ),
+                       function ( $req ) use ( $params, $blacklist ) {
+                               return $req->getUniqueId() === $params['request'] &&
+                                       !isset( $blacklist[get_class( $req )] );
+                       }
+               );
+               if ( count( $reqs ) !== 1 ) {
+                       $this->dieUsage( 'Failed to create change request', 'badrequest' );
+               }
+               $req = reset( $reqs );
+
+               // Perform the removal
+               $status = $manager->allowsAuthenticationDataChange( $req, true );
+               if ( !$status->isGood() ) {
+                       $this->dieStatus( $status );
+               }
+               $manager->changeAuthenticationData( $req );
+
+               $this->getResult()->addValue( null, $this->getModuleName(), [ 'status' => 'success' ] );
+       }
+
+       public function isWriteMode() {
+               return true;
+       }
+
+       public function needsToken() {
+               return 'csrf';
+       }
+
+       public function getAllowedParams() {
+               return ApiAuthManagerHelper::getStandardParams( $this->authAction,
+                       'request'
+               );
+       }
+
+       protected function getExamplesMessages() {
+               $path = $this->getModulePath();
+               $action = $this->getModuleName();
+               return [
+                       "action={$action}&request=FooAuthenticationRequest&token=123ABC"
+                               => "apihelp-{$path}-example-simple",
+               ];
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Manage_authentication_data';
+       }
+}
diff --git a/includes/api/ApiResetPassword.php b/includes/api/ApiResetPassword.php
new file mode 100644 (file)
index 0000000..042ad69
--- /dev/null
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Copyright © 2016 Brad Jorsch <bjorsch@wikimedia.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use MediaWiki\Auth\AuthManager;
+
+/**
+ * Reset password, with AuthManager
+ *
+ * @ingroup API
+ */
+class ApiResetPassword extends ApiBase {
+
+       private $hasAnyRoutes = null;
+
+       /**
+        * Determine whether any reset routes are available.
+        * @return bool
+        */
+       private function hasAnyRoutes() {
+               if ( $this->hasAnyRoutes === null ) {
+                       $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
+                       $this->hasAnyRoutes = !empty( $resetRoutes['username'] ) || !empty( $resetRoutes['email'] );
+               }
+               return $this->hasAnyRoutes;
+       }
+
+       protected function getDescriptionMessage() {
+               if ( !$this->hasAnyRoutes() ) {
+                       return 'apihelp-resetpassword-description-noroutes';
+               }
+               return parent::getDescriptionMessage();
+       }
+
+       public function execute() {
+               if ( !$this->hasAnyRoutes() ) {
+                       $this->dieUsage( 'No password reset routes are available.', 'moduledisabled' );
+               }
+
+               $params = $this->extractRequestParams() + [
+                       // Make sure the keys exist even if getAllowedParams didn't define them
+                       'user' => null,
+                       'email' => null,
+               ];
+
+               $this->requireOnlyOneParameter( $params, 'user', 'email' );
+
+               $passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
+
+               $status = $passwordReset->isAllowed( $this->getUser(), $params['capture'] );
+               if ( !$status->isOK() ) {
+                       $this->dieStatus( Status::wrap( $status ) );
+               }
+
+               $status = $passwordReset->execute(
+                       $this->getUser(), $params['user'], $params['email'], $params['capture']
+               );
+               if ( !$status->isOK() ) {
+                       $status->value = null;
+                       $this->dieStatus( Status::wrap( $status ) );
+               }
+
+               $result = $this->getResult();
+               $result->addValue( [ 'resetpassword' ], 'status', 'success' );
+               if ( $params['capture'] ) {
+                       $passwords = $status->getValue() ?: [];
+                       ApiResult::setArrayType( $passwords, 'kvp', 'user' );
+                       ApiResult::setIndexedTagName( $passwords, 'p' );
+                       $result->addValue( [ 'resetpassword' ], 'passwords', $passwords );
+               }
+       }
+
+       public function isWriteMode() {
+               return $this->hasAnyRoutes();
+       }
+
+       public function needsToken() {
+               if ( !$this->hasAnyRoutes() ) {
+                       return false;
+               }
+               return 'csrf';
+       }
+
+       public function getAllowedParams() {
+               if ( !$this->hasAnyRoutes() ) {
+                       return [];
+               }
+
+               $ret = [
+                       'user' => [
+                               ApiBase::PARAM_TYPE => 'user',
+                       ],
+                       'email' => [
+                               ApiBase::PARAM_TYPE => 'string',
+                       ],
+                       'capture' => false,
+               ];
+
+               $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
+               if ( empty( $resetRoutes['username'] ) ) {
+                       unset( $ret['user'] );
+               }
+               if ( empty( $resetRoutes['email'] ) ) {
+                       unset( $ret['email'] );
+               }
+
+               return $ret;
+       }
+
+       protected function getExamplesMessages() {
+               $ret = [];
+               $resetRoutes = $this->getConfig()->get( 'PasswordResetRoutes' );
+
+               if ( !empty( $resetRoutes['username'] ) ) {
+                       $ret['action=resetpassword&user=Example&token=123ABC'] = 'apihelp-resetpassword-example-user';
+               }
+               if ( !empty( $resetRoutes['email'] ) ) {
+                       $ret['action=resetpassword&user=user@example.com&token=123ABC'] =
+                               'apihelp-resetpassword-example-email';
+               }
+
+               return $ret;
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Manage_authentication_data';
+       }
+}
index 2b23da0..43eed78 100644 (file)
@@ -34,6 +34,9 @@
        "apihelp-block-example-ip-simple": "Block IP address <kbd>192.0.2.5</kbd> for three days with reason <kbd>First strike</kbd>.",
        "apihelp-block-example-user-complex": "Block user <kbd>Vandal</kbd> indefinitely with reason <kbd>Vandalism</kbd>, and prevent new account creation and email sending.",
 
+       "apihelp-changeauthenticationdata-description": "Change authentication data for the current user.",
+       "apihelp-changeauthenticationdata-example-password": "Attempt to change the current user's password to <kbd>ExamplePassword</kbd>.",
+
        "apihelp-checktoken-description": "Check the validity of a token from <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd>.",
        "apihelp-checktoken-param-type": "Type of token being tested.",
        "apihelp-checktoken-param-token": "Token to test.",
        "apihelp-clearhasmsg-description": "Clears the <code>hasmsg</code> flag for the current user.",
        "apihelp-clearhasmsg-example-1": "Clear the <code>hasmsg</code> flag for the current user.",
 
+       "apihelp-clientlogin-description": "Log in to the wiki using the interactive flow.",
+       "apihelp-clientlogin-example-login": "Start the process of logging in to the wiki as user <kbd>Example</kbd> with password <kbd>ExamplePassword</kbd>.",
+       "apihelp-clientlogin-example-login2": "Continue logging in after a UI response for two-factor auth, supplying an <var>OATHToken</var> of <kbd>987654</kbd>.",
+
        "apihelp-compare-description": "Get the difference between 2 pages.\n\nA revision number, a page title, or a page ID for both \"from\" and \"to\" must be passed.",
        "apihelp-compare-param-fromtitle": "First title to compare.",
        "apihelp-compare-param-fromid": "First page ID to compare.",
@@ -53,6 +60,7 @@
        "apihelp-compare-example-1": "Create a diff between revision 1 and 2.",
 
        "apihelp-createaccount-description": "Create a new user account.",
+       "apihelp-createaccount-example-create": "Start the process of creating user <kbd>Example</kbd> with password <kbd>ExamplePassword</kbd>.",
        "apihelp-createaccount-param-name": "Username.",
        "apihelp-createaccount-param-password": "Password (ignored if <var>$1mailpassword</var> is set).",
        "apihelp-createaccount-param-domain": "Domain for external authentication (optional).",
        "apihelp-import-param-rootpage": "Import as subpage of this page. Cannot be used together with <var>$1namespace</var>.",
        "apihelp-import-example-import": "Import [[meta:Help:ParserFunctions]] to namespace 100 with full history.",
 
-       "apihelp-login-description": "Log in and get authentication cookies.\n\nIn the event of a successful log-in, the needed cookies will be included in the HTTP response headers. In the event of a failed log-in, further attempts may be throttled to limit automated password guessing attacks.",
+       "apihelp-linkaccount-description": "Link an account from a third-party provider to the current user.",
+       "apihelp-linkaccount-example-link": "Start the process of linking to an account from <kbd>Example</kbd>.",
+
+       "apihelp-login-description": "Log in and get authentication cookies.\n\nThis action should only be used in combination with [[Special:BotPasswords]]; use for main-account login is deprecated and may fail without warning. To safely log in to the main account, use <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
+       "apihelp-login-description-nobotpasswords": "Log in and get authentication cookies.\n\nThis action is deprecated and may fail without warning. To safely log in, use <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
+       "apihelp-login-description-nonauthmanager": "Log in and get authentication cookies.\n\nIn the event of a successful log-in, the needed cookies will be included in the HTTP response headers. In the event of a failed log-in, further attempts may be throttled to limit automated password guessing attacks.",
        "apihelp-login-param-name": "User name.",
        "apihelp-login-param-password": "Password.",
        "apihelp-login-param-domain": "Domain (optional).",
        "apihelp-query+allusers-param-attachedwiki": "With <kbd>$1prop=centralids</kbd>, also indicate whether the user is attached with the wiki identified by this ID.",
        "apihelp-query+allusers-example-Y": "List users starting at <kbd>Y</kbd>.",
 
+       "apihelp-query+authmanagerinfo-description": "Retrieve information about the current authentication status.",
+       "apihelp-query+authmanagerinfo-param-securitysensitiveoperation": "Test whether the user's current authentication status is sufficient for the specified security-sensitive operation.",
+       "apihelp-query+authmanagerinfo-param-requestsfor": "Fetch information about the authentication requests needed for the specified authentication action.",
+       "apihelp-query+filerepoinfo-example-login": "Fetch the requests that may be used when beginning a login.",
+       "apihelp-query+filerepoinfo-example-login-merged": "Fetch the requests that may be used when beginning a login, with form fields merged.",
+       "apihelp-query+filerepoinfo-example-securitysensitiveoperation": "Test whether authentication is sufficient for action <kbd>foo</kbd>.",
+
        "apihelp-query+backlinks-description": "Find all pages that link to the given page.",
        "apihelp-query+backlinks-param-title": "Title to search. Cannot be used together with <var>$1pageid</var>.",
        "apihelp-query+backlinks-param-pageid": "Page ID to search. Cannot be used together with <var>$1title</var>.",
        "apihelp-query+users-paramvalue-prop-emailable": "Tags if the user can and wants to receive email through [[Special:Emailuser]].",
        "apihelp-query+users-paramvalue-prop-gender": "Tags the gender of the user. Returns \"male\", \"female\", or \"unknown\".",
        "apihelp-query+users-paramvalue-prop-centralids": "Adds the central IDs and attachment status for the user.",
+       "apihelp-query+users-paramvalue-prop-cancreate": "Indicates whether an account for valid but unregistered usernames can be created.",
        "apihelp-query+users-param-attachedwiki": "With <kbd>$1prop=centralids</kbd>, indicate whether the user is attached with the wiki identified by this ID.",
        "apihelp-query+users-param-users": "A list of users to obtain information for.",
        "apihelp-query+users-param-token": "Use <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]]</kbd> instead.",
        "apihelp-query+watchlistraw-example-simple": "List pages on the current user's watchlist.",
        "apihelp-query+watchlistraw-example-generator": "Fetch page info for pages on the current user's watchlist.",
 
+       "apihelp-removeauthenticationdata-description": "Remove authentication data for the current user.",
+       "apihelp-removeauthenticationdata-example-simple": "Attempt to remove the current user's data for <kbd>FooAuthenticationRequest</kbd>.",
+
+       "apihelp-resetpassword-description": "Send a password reset email to a user.",
+       "apihelp-resetpassword-description-noroutes": "No password reset routes are available.\n\nEnable routes in <var>[[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var> to use this module.",
+       "apihelp-resetpassword-param-user": "User being reset.",
+       "apihelp-resetpassword-param-email": "Email address of the user being reset.",
+       "apihelp-resetpassword-param-capture": "Return the temporary passwords that were sent. Requires the <code>passwordreset</code> user right.",
+       "apihelp-resetpassword-example-user": "Send a password reset email to user <kbd>Example</kbd>.",
+       "apihelp-resetpassword-example-email": "Send a password reset email for all users with email address <kbd>user@example.com</kbd>.",
+
        "apihelp-revisiondelete-description": "Delete and undelete revisions.",
        "apihelp-revisiondelete-param-type": "Type of revision deletion being performed.",
        "apihelp-revisiondelete-param-target": "Page title for the revision deletion, if required for the type.",
        "apihelp-undelete-example-page": "Undelete page <kbd>Main Page</kbd>.",
        "apihelp-undelete-example-revisions": "Undelete two revisions of page <kbd>Main Page</kbd>.",
 
+       "apihelp-unlinkaccount-description": "Remove a linked third-party account from the current user.",
+       "apihelp-unlinkaccount-example-simple": "Attempt to remove the current user's link for the provider associated with <kbd>FooAuthenticationRequest</kbd>.",
+
        "apihelp-upload-description": "Upload a file, or get the status of pending uploads.\n\nSeveral methods are available:\n* Upload file contents directly, using the <var>$1file</var> parameter.\n* Upload the file in pieces, using the <var>$1filesize</var>, <var>$1chunk</var>, and <var>$1offset</var> parameters.\n* Have the MediaWiki server fetch a file from a URL, using the <var>$1url</var> parameter.\n* Complete an earlier upload that failed due to warnings, using the <var>$1filekey</var> parameter.\nNote that the HTTP POST must be done as a file upload (i.e. using <code>multipart/form-data</code>) when sending the <var>$1file</var>.",
        "apihelp-upload-param-filename": "Target filename.",
        "apihelp-upload-param-comment": "Upload comment. Also used as the initial page text for new files if <var>$1text</var> is not specified.",
        "api-help-right-apihighlimits": "Use higher limits in API queries (slow queries: $1; fast queries: $2). The limits for slow queries also apply to multivalue parameters.",
        "api-help-open-in-apisandbox": "<small>[open in sandbox]</small>",
 
+       "api-help-authmanager-general-usage": "The general procedure to use this module is:\n# Fetch the fields available from <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> with <kbd>amirequestsfor=$4</kbd>, and a <kbd>$5</kbd> token from <kbd>[[Special:ApiHelp/query+tokens|action=query&meta=tokens]].\n# Present the fields to the user, and obtain their submission.\n# Post to this module, supplying <var>$1returnurl</var> and any relevant fields.\n# Check the <samp>status</samp> in the response.\n#* If you received <samp>PASS</samp> or <samp>FAIL</samp>, you're done. The operation either succeeded or it didn't.\n#* If you received <samp>UI</samp>, present the new fields to the user and obtain their submission. Then post to this module with <var>$1continue</var> and the relevant fields set, and repeat step 4.\n#* If you received <samp>REDIRECT</samp>, direct the user to the <samp>redirecttarget</samp> and wait for the return to <var>$1returnurl</var>. Then post to this module with <var>$1continue</var> and any fields passed to the return URL, and repeat step 4.\n#* If you received <samp>RESTART</samp>, that means the authentication worked but we don't have an linked user account. You might treat this as UI or as FAIL.",
+       "api-help-authmanagerhelper-requests": "Only use these authentication requests, by the <samp>id</samp> returned from <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> with <kbd>amirequestsfor=$1</kbd> or from a previous response from this module.",
+       "api-help-authmanagerhelper-request": "Use this authentication request, by the <samp>id</samp> returned from <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> with <kbd>amirequestsfor=$1</kbd>.",
+       "api-help-authmanagerhelper-messageformat": "Format to use for returning messages.",
+       "api-help-authmanagerhelper-mergerequestfields": "Merge field information for all authentication requests into one array.",
+       "api-help-authmanagerhelper-preservestate": "Preserve state from a previous failed login attempt, if possible.",
+       "api-help-authmanagerhelper-returnurl": "Return URL for third-party authentication flows, must be absolute. Either this or <var>$1continue</var> is required.\n\nUpon receiving a <samp>REDIRECT</samp> response, you will typically open a browser or web view to the specified <samp>redirecttarget</samp> URL for a third-party authentication flow. When that completes, the third party will send the browser or web view to this URL. You should extract any query or POST parameters from the URL and pass them as a <var>$1continue</var> request to this API module.",
+       "api-help-authmanagerhelper-continue": "This request is a continuation after an earlier <samp>UI</samp> or <samp>REDIRECT</samp> response. Either this or <var>$1returnurl</var> is required.",
+       "api-help-authmanagerhelper-additional-params": "This module accepts additional parameters depending on the available authentication requests. Use <kbd>[[Special:ApiHelp/query+authmanagerinfo|action=query&meta=authmanagerinfo]]</kbd> with <kbd>amirequestsfor=$1</kbd> (or a previous response from this module, if applicable) to determine the requests available and the fields that they use.",
+
        "api-credits-header": "Credits",
        "api-credits": "API developers:\n* Yuri Astrakhan (creator, lead developer Sep 2006–Sep 2007)\n* Roan Kattouw (lead developer Sep 2007–2009)\n* Victor Vasiliev\n* Bryan Tong Minh\n* Sam Reed\n* Brad Jorsch (lead developer 2013–present)\n\nPlease send your comments, suggestions and questions to mediawiki-api@lists.wikimedia.org\nor file a bug report at https://phabricator.wikimedia.org/."
 }
index 4ded4aa..37bb4b0 100644 (file)
@@ -41,6 +41,8 @@
        "apihelp-block-param-watchuser": "{{doc-apihelp-param|block|watchuser}}",
        "apihelp-block-example-ip-simple": "{{doc-apihelp-example|block}}",
        "apihelp-block-example-user-complex": "{{doc-apihelp-example|block}}",
+       "apihelp-changeauthenticationdata-description": "{{doc-apihelp-description|changeauthenticationdata}}",
+       "apihelp-changeauthenticationdata-example-password": "{{doc-apihelp-example|changeauthenticationdata}}",
        "apihelp-checktoken-description": "{{doc-apihelp-description|checktoken}}",
        "apihelp-checktoken-param-type": "{{doc-apihelp-param|checktoken|type}}",
        "apihelp-checktoken-param-token": "{{doc-apihelp-param|checktoken|token}}",
@@ -48,6 +50,9 @@
        "apihelp-checktoken-example-simple": "{{doc-apihelp-example|checktoken}}",
        "apihelp-clearhasmsg-description": "{{doc-apihelp-description|clearhasmsg}}",
        "apihelp-clearhasmsg-example-1": "{{doc-apihelp-example|clearhasmsg}}",
+       "apihelp-clientlogin-description": "{{doc-apihelp-description|clientlogin}}",
+       "apihelp-clientlogin-example-login": "{{doc-apihelp-example|clientlogin}}",
+       "apihelp-clientlogin-example-login2": "{{doc-apihelp-example|clientlogin}}",
        "apihelp-compare-description": "{{doc-apihelp-description|compare}}",
        "apihelp-compare-param-fromtitle": "{{doc-apihelp-param|compare|fromtitle}}",
        "apihelp-compare-param-fromid": "{{doc-apihelp-param|compare|fromid}}",
@@ -57,6 +62,7 @@
        "apihelp-compare-param-torev": "{{doc-apihelp-param|compare|torev}}",
        "apihelp-compare-example-1": "{{doc-apihelp-example|compare}}",
        "apihelp-createaccount-description": "{{doc-apihelp-description|createaccount}}",
+       "apihelp-createaccount-example-create": "{{doc-apihelp-example|createaccount}}",
        "apihelp-createaccount-param-name": "{{doc-apihelp-param|createaccount|name}}\n{{Identical|Username}}",
        "apihelp-createaccount-param-password": "{{doc-apihelp-param|createaccount|password}}",
        "apihelp-createaccount-param-domain": "{{doc-apihelp-param|createaccount|domain}}",
        "apihelp-import-param-namespace": "{{doc-apihelp-param|import|namespace}}",
        "apihelp-import-param-rootpage": "{{doc-apihelp-param|import|rootpage}}",
        "apihelp-import-example-import": "{{doc-apihelp-example|import}}",
-       "apihelp-login-description": "{{doc-apihelp-description|login}}",
+       "apihelp-linkaccount-description": "{{doc-apihelp-description|linkaccount}}",
+       "apihelp-linkaccount-example-link": "{{doc-apihelp-example|linkaccount}}",
+       "apihelp-login-description": "{{doc-apihelp-description|login|info=This message is used when <code>$wgEnableBotPasswords</code> is true.|seealso=* {{msg-mw|apihelp-login-description-nobotpasswords}}}}",
+       "apihelp-login-description-nobotpasswords": "{{doc-apihelp-description|login|info=This message is used when <code>$wgEnableBotPasswords</code> is false.|seealso=* {{msg-mw|apihelp-login-description}}}}",
+       "apihelp-login-description-nonauthmanager": "{{doc-apihelp-description|login|info=This message is used when <code>$wgDisableAuthManager</code> is true.|seealso=* {{msg-mw|apihelp-login-description}}}}",
        "apihelp-login-param-name": "{{doc-apihelp-param|login|name}}\n{{Identical|Username}}",
        "apihelp-login-param-password": "{{doc-apihelp-param|login|password}}\n{{Identical|Password}}",
        "apihelp-login-param-domain": "{{doc-apihelp-param|login|domain}}",
        "apihelp-query+allusers-param-activeusers": "{{doc-apihelp-param|query+allusers|activeusers|params=* $1 - Value of [[mw:Manual:$wgActiveUserDays]]|paramstart=2}}",
        "apihelp-query+allusers-param-attachedwiki": "{{doc-apihelp-param|query+allusers|attachedwiki}}",
        "apihelp-query+allusers-example-Y": "{{doc-apihelp-example|query+allusers}}",
+       "apihelp-query+authmanagerinfo-description": "{{doc-apihelp-description|query+authmanagerinfo}}",
+       "apihelp-query+authmanagerinfo-param-requestsfor": "{{doc-apihelp-param|query+authmanagerinfo|requestsfor}}",
+       "apihelp-query+authmanagerinfo-param-securitysensitiveoperation": "{{doc-apihelp-param|query+authmanagerinfo|securitysensitiveoperation}}",
+       "apihelp-query+filerepoinfo-example-login": "{{doc-apihelp-example|query+filerepoinfo}}",
+       "apihelp-query+filerepoinfo-example-login-merged": "{{doc-apihelp-example|query+filerepoinfo}}",
+       "apihelp-query+filerepoinfo-example-securitysensitiveoperation": "{{doc-apihelp-example|query+filerepoinfo}}",
        "apihelp-query+backlinks-description": "{{doc-apihelp-description|query+backlinks}}",
        "apihelp-query+backlinks-param-title": "{{doc-apihelp-param|query+backlinks|title}}",
        "apihelp-query+backlinks-param-pageid": "{{doc-apihelp-param|query+backlinks|pageid}}",
        "apihelp-query+users-paramvalue-prop-emailable": "{{doc-apihelp-paramvalue|query+users|prop|emailable}}",
        "apihelp-query+users-paramvalue-prop-gender": "{{doc-apihelp-paramvalue|query+users|prop|gender}}",
        "apihelp-query+users-paramvalue-prop-centralids": "{{doc-apihelp-paramvalue|query+users|prop|centralids}}",
+       "apihelp-query+users-paramvalue-prop-cancreate": "{{doc-apihelp-paramvalue|query+users|prop|cancreate}}",
        "apihelp-query+users-param-attachedwiki": "{{doc-apihelp-param|query+users|attachedwiki}}",
        "apihelp-query+users-param-users": "{{doc-apihelp-param|query+users|users}}",
        "apihelp-query+users-param-token": "{{doc-apihelp-param|query+users|token}}",
        "apihelp-query+watchlistraw-param-totitle": "{{doc-apihelp-param|query+watchlistraw|totitle}}",
        "apihelp-query+watchlistraw-example-simple": "{{doc-apihelp-example|query+watchlistraw}}",
        "apihelp-query+watchlistraw-example-generator": "{{doc-apihelp-example|query+watchlistraw}}",
+       "apihelp-removeauthenticationdata-description": "{{doc-apihelp-description|removeauthenticationdata}}",
+       "apihelp-removeauthenticationdata-example-simple": "{{doc-apihelp-example|removeauthenticationdata}}",
+       "apihelp-resetpassword-description": "{{doc-apihelp-description|resetpassword|seealso=* {{msg-mw|apihelp-resetpassword-description-noroutes}}}}",
+       "apihelp-resetpassword-description-noroutes": "{{doc-apihelp-description|resetpassword|info=This message is used when no known routes are enabled in <var>[[mw:Manual:$wgPasswordResetRoutes|$wgPasswordResetRoutes]]</var>.|seealso={{msg-mw|apihelp-resetpassword-description}}}}",
+       "apihelp-resetpassword-param-user": "{{doc-apihelp-param|resetpassword|user}}",
+       "apihelp-resetpassword-param-email": "{{doc-apihelp-param|resetpassword|email}}",
+       "apihelp-resetpassword-param-capture": "{{doc-apihelp-param|resetpassword|capture}}",
+       "apihelp-resetpassword-example-email": "{{doc-apihelp-example|resetpassword}}",
+       "apihelp-resetpassword-example-user": "{{doc-apihelp-example|resetpassword}}",
        "apihelp-revisiondelete-description": "{{doc-apihelp-description|revisiondelete}}",
        "apihelp-revisiondelete-param-type": "{{doc-apihelp-param|revisiondelete|type}}",
        "apihelp-revisiondelete-param-target": "{{doc-apihelp-param|revisiondelete|target}}",
        "apihelp-undelete-param-watchlist": "{{doc-apihelp-param|undelete|watchlist}}",
        "apihelp-undelete-example-page": "{{doc-apihelp-example|undelete}}",
        "apihelp-undelete-example-revisions": "{{doc-apihelp-example|undelete}}",
+       "apihelp-unlinkaccount-description": "{{doc-apihelp-description|unlinkaccount}}",
+       "apihelp-unlinkaccount-example-simple": "{{doc-apihelp-example|unlinkaccount}}",
        "apihelp-upload-description": "{{doc-apihelp-description|upload}}",
        "apihelp-upload-param-filename": "{{doc-apihelp-param|upload|filename}}",
        "apihelp-upload-param-comment": "{{doc-apihelp-param|upload|comment}}",
        "api-help-permissions-granted-to": "Used to introduce the list of groups each permission is assigned to.\n\nParameters:\n* $1 - Number of groups\n* $2 - List of group names, comma-separated",
        "api-help-right-apihighlimits": "{{technical}}{{doc-right|apihighlimits|prefix=api-help}}\nThis message is used instead of {{msg-mw|right-apihighlimits}} in the API help to display the actual limits.\n\nParameters:\n* $1 - Limit for slow queries\n* $2 - Limit for fast queries",
        "api-help-open-in-apisandbox": "Text for the link to open an API example in [[Special:ApiSandbox]].",
+       "api-help-authmanager-general-usage": "Text giving a brief overview of how to use an AuthManager-using API module. Parameters:\n* $1 - Module parameter prefix, e.g. \"login\"\n* $2 - Module name, e.g. \"clientlogin\"\n* $3 - Module path, e.g. \"clientlogin\"\n* $4 - AuthManager action to use with this module.\n* $5 - Token type needed by the module.",
+       "api-help-authmanagerhelper-additional-params": "Message to display for AuthManager modules that take additional parameters to populate AuthenticationRequests. Parameters:\n* $1 - AuthManager action used by this module\n* $2 - Module parameter prefix, e.g. \"login\"\n* $3 - Module name, e.g. \"clientlogin\"\n* $4 - Module path, e.g. \"clientlogin\"",
+       "api-help-authmanagerhelper-requests": "{{doc-apihelp-param|description=the \"requests\" parameter for AuthManager-using API modules|params=* $1 - AuthManager action used by this module|paramstart=2|noseealso=1}}",
+       "api-help-authmanagerhelper-request": "{{doc-apihelp-param|description=the \"request\" parameter for AuthManager-using API modules|params=* $1 - AuthManager action used by this module|paramstart=2|noseealso=1}}",
+       "api-help-authmanagerhelper-messageformat": "{{doc-apihelp-param|description=the \"messageformat\" parameter for AuthManager-using API modules|noseealso=1}}",
+       "api-help-authmanagerhelper-mergerequestfields": "{{doc-apihelp-param|description=the \"mergerequestfields\" parameter for AuthManager-using API modules|noseealso=1}}",
+       "api-help-authmanagerhelper-preservestate": "{{doc-apihelp-param|description=the \"preservestate\" parameter for AuthManager-using API modules|noseealso=1}}",
+       "api-help-authmanagerhelper-returnurl": "{{doc-apihelp-param|description=the \"returnurl\" parameter for AuthManager-using API modules|noseealso=1}}",
+       "api-help-authmanagerhelper-continue": "{{doc-apihelp-param|description=the \"continue\" parameter for AuthManager-using API modules|noseealso=1}}",
        "api-credits-header": "Header for the API credits section in the API help output\n{{Identical|Credit}}",
        "api-credits": "API credits text, displayed in the API help output"
 }
index d7aee5b..b3776bd 100644 (file)
@@ -105,11 +105,15 @@ class PasswordPolicyChecks {
 
                $status = Status::newGood();
                $username = $user->getName();
-               if ( $policyVal
-                       && isset( $blockedLogins[$username] )
-                       && $password == $blockedLogins[$username]
-               ) {
-                       $status->error( 'password-login-forbidden' );
+               if ( $policyVal ) {
+                       if ( isset( $blockedLogins[$username] ) && $password == $blockedLogins[$username] ) {
+                               $status->error( 'password-login-forbidden' );
+                       }
+
+                       // Example from ApiChangeAuthenticationRequest
+                       if ( $password === 'ExamplePassword' ) {
+                               $status->error( 'password-login-forbidden' );
+                       }
                }
                return $status;
        }
diff --git a/tests/phpunit/includes/api/ApiCreateAccountTest.php b/tests/phpunit/includes/api/ApiCreateAccountTest.php
deleted file mode 100644 (file)
index 9a83e61..0000000
+++ /dev/null
@@ -1,160 +0,0 @@
-<?php
-
-/**
- * @group Database
- * @group API
- * @group medium
- *
- * @covers ApiCreateAccount
- */
-class ApiCreateAccountTest extends ApiTestCase {
-       protected function setUp() {
-               parent::setUp();
-               $this->setMwGlobals( [ 'wgEnableEmail' => true ] );
-       }
-
-       /**
-        * Test the account creation API with a valid request. Also
-        * make sure the new account can log in and is valid.
-        *
-        * This test does multiple API requests so it might end up being
-        * a bit slow. Raise the default timeout.
-        * @group medium
-        */
-       public function testValid() {
-               global $wgServer;
-
-               if ( !isset( $wgServer ) ) {
-                       $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
-               }
-
-               $password = PasswordFactory::generateRandomPasswordString();
-
-               $ret = $this->doApiRequest( [
-                       'action' => 'createaccount',
-                       'name' => 'Apitestnew',
-                       'password' => $password,
-                       'email' => 'test@domain.test',
-                       'realname' => 'Test Name'
-               ] );
-
-               $result = $ret[0];
-               $this->assertNotInternalType( 'bool', $result );
-               $this->assertNotInternalType( 'null', $result['createaccount'] );
-
-               // Should first ask for token.
-               $a = $result['createaccount'];
-               $this->assertEquals( 'NeedToken', $a['result'] );
-               $token = $a['token'];
-
-               // Finally create the account
-               $ret = $this->doApiRequest(
-                       [
-                               'action' => 'createaccount',
-                               'name' => 'Apitestnew',
-                               'password' => $password,
-                               'token' => $token,
-                               'email' => 'test@domain.test',
-                               'realname' => 'Test Name'
-                       ],
-                       $ret[2]
-               );
-
-               $result = $ret[0];
-               $this->assertNotInternalType( 'bool', $result );
-               $this->assertEquals( 'Success', $result['createaccount']['result'] );
-
-               // Try logging in with the new user.
-               $ret = $this->doApiRequest( [
-                       'action' => 'login',
-                       'lgname' => 'Apitestnew',
-                       'lgpassword' => $password,
-               ] );
-
-               $result = $ret[0];
-               $this->assertNotInternalType( 'bool', $result );
-               $this->assertNotInternalType( 'null', $result['login'] );
-
-               $a = $result['login']['result'];
-               $this->assertEquals( 'NeedToken', $a );
-               $token = $result['login']['token'];
-
-               $ret = $this->doApiRequest(
-                       [
-                               'action' => 'login',
-                               'lgtoken' => $token,
-                               'lgname' => 'Apitestnew',
-                               'lgpassword' => $password,
-                       ],
-                       $ret[2]
-               );
-
-               $result = $ret[0];
-
-               $this->assertNotInternalType( 'bool', $result );
-               $a = $result['login']['result'];
-
-               $this->assertEquals( 'Success', $a );
-
-               // log out to destroy the session
-               $ret = $this->doApiRequest(
-                       [
-                               'action' => 'logout',
-                       ],
-                       $ret[2]
-               );
-               $this->assertEquals( [], $ret[0] );
-       }
-
-       /**
-        * Make sure requests with no names are invalid.
-        * @expectedException UsageException
-        */
-       public function testNoName() {
-               $this->doApiRequest( [
-                       'action' => 'createaccount',
-                       'token' => LoginForm::getCreateaccountToken()->toString(),
-                       'password' => 'password',
-               ] );
-       }
-
-       /**
-        * Make sure requests with no password are invalid.
-        * @expectedException UsageException
-        */
-       public function testNoPassword() {
-               $this->doApiRequest( [
-                       'action' => 'createaccount',
-                       'name' => 'testName',
-                       'token' => LoginForm::getCreateaccountToken()->toString(),
-               ] );
-       }
-
-       /**
-        * Make sure requests with existing users are invalid.
-        * @expectedException UsageException
-        */
-       public function testExistingUser() {
-               $this->doApiRequest( [
-                       'action' => 'createaccount',
-                       'name' => 'Apitestsysop',
-                       'token' => LoginForm::getCreateaccountToken()->toString(),
-                       'password' => 'password',
-                       'email' => 'test@domain.test',
-               ] );
-       }
-
-       /**
-        * Make sure requests with invalid emails are invalid.
-        * @expectedException UsageException
-        */
-       public function testInvalidEmail() {
-               $this->doApiRequest( [
-                       'action' => 'createaccount',
-                       'name' => 'Test User',
-                       'token' => LoginForm::getCreateaccountToken()->toString(),
-                       'password' => 'password',
-                       'email' => 'invalid',
-               ] );
-       }
-}
index bcd884e..e9afd45 100644 (file)
@@ -13,6 +13,8 @@ class ApiLoginTest extends ApiTestCase {
         * Test result of attempted login with an empty username
         */
        public function testApiLoginNoName() {
+               global $wgDisableAuthManager;
+
                $session = [
                        'wsTokenSecrets' => [ 'login' => 'foobar' ],
                ];
@@ -20,11 +22,11 @@ class ApiLoginTest extends ApiTestCase {
                        'lgname' => '', 'lgpassword' => self::$users['sysop']->password,
                        'lgtoken' => (string)( new MediaWiki\Session\Token( 'foobar', '' ) )
                ], $session );
-               $this->assertEquals( 'NoName', $data[0]['login']['result'] );
+               $this->assertEquals( $wgDisableAuthManager ? 'NoName' : 'Failed', $data[0]['login']['result'] );
        }
 
        public function testApiLoginBadPass() {
-               global $wgServer;
+               global $wgServer, $wgDisableAuthManager;
 
                $user = self::$users['sysop'];
                $user->getUser()->logout();
@@ -61,7 +63,7 @@ class ApiLoginTest extends ApiTestCase {
                $this->assertNotInternalType( "bool", $result );
                $a = $result["login"]["result"];
 
-               $this->assertEquals( "WrongPass", $a );
+               $this->assertEquals( $wgDisableAuthManager ? 'WrongPass' : 'Failed', $a );
        }
 
        public function testApiLoginGoodPass() {
index 31e98d0..ff5640a 100644 (file)
@@ -184,6 +184,13 @@ abstract class ApiTestCase extends MediaWikiLangTestCase {
                        $data[2]
                );
 
+               if ( $data[0]['login']['result'] === 'Success' ) {
+                       // DWIM
+                       global $wgUser;
+                       $wgUser = self::$users[$user]->getUser();
+                       RequestContext::getMain()->setUser( $wgUser );
+               }
+
                return $data;
        }