Added account creation API.
authorTyler Romeo <tylerromeo@gmail.com>
Wed, 8 Aug 2012 13:58:47 +0000 (09:58 -0400)
committerMatmaRex <matma.rex@gmail.com>
Fri, 11 Jan 2013 00:00:49 +0000 (01:00 +0100)
Created new API action "createaccount" that
allows access to account creation. Takes
username, password, email, realname, token,
and optionally mailpassword and reason.

Errors are given in an errors array. Note
there is no way to natively handle CAPTCHAs
as there is no uniform implementation of
presenting CAPTCHA links to user. Right now
the best an extension can do is return an
error in AbortNewAccount, which will then
be displayed as an error in the Api result.

Change-Id: Ibdb1e50d434fb857683e1e4ff5a4a5a91c6b7c3a

RELEASE-NOTES-1.21
includes/AutoLoader.php
includes/api/ApiCreateAccount.php [new file with mode: 0644]
includes/api/ApiMain.php
tests/phpunit/includes/api/ApiAccountCreationTest.php [new file with mode: 0644]

index ddd8f97..7ea3e87 100644 (file)
@@ -76,6 +76,7 @@ production.
 * (bug 39397) Hide rollback link if a user is the only contributor of the page.
 * $wgPageInfoTransclusionLimit limits the list size of transcluded articles
   on the info action. Default is 50.
+* Added action=createaccount to allow user account creation.
 
 === Bug fixes in 1.21 ===
 * (bug 40353) SpecialDoubleRedirect should support interwiki redirects.
index 68104fd..5eb497d 100644 (file)
@@ -334,6 +334,7 @@ $wgAutoloadLocalClasses = array(
        'ApiBase' => 'includes/api/ApiBase.php',
        'ApiBlock' => 'includes/api/ApiBlock.php',
        'ApiComparePages' => 'includes/api/ApiComparePages.php',
+       'ApiCreateAccount' => 'includes/api/ApiCreateAccount.php',
        'ApiDelete' => 'includes/api/ApiDelete.php',
        'ApiDisabled' => 'includes/api/ApiDisabled.php',
        'ApiEditPage' => 'includes/api/ApiEditPage.php',
diff --git a/includes/api/ApiCreateAccount.php b/includes/api/ApiCreateAccount.php
new file mode 100644 (file)
index 0000000..628f48e
--- /dev/null
@@ -0,0 +1,265 @@
+<?php
+/**
+ * Created on August 7, 2012
+ *
+ * Copyright © 2012 Tyler Romeo <tylerromeo@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Unit to authenticate account registration attempts to the current wiki.
+ *
+ * @ingroup API
+ */
+class ApiCreateAccount extends ApiBase {
+       public function execute() {
+               $params = $this->extractRequestParams();
+
+               $result = array();
+
+               // Init session if necessary
+               if ( session_id() == '' ) {
+                       wfSetupSession();
+               }
+
+               if( $params['mailpassword'] && !$params['email'] ) {
+                       $this->dieUsageMsg( 'noemail' );
+               }
+
+               $context = new DerivativeContext( $this->getContext() );
+               $context->setRequest( new DerivativeRequest(
+                       $this->getContext()->getRequest(),
+                       array(
+                               'type' => 'signup',
+                               'uselang' => $params['language'],
+                               'wpName' => $params['name'],
+                               'wpPassword' => $params['password'],
+                               'wpRetype' => $params['password'],
+                               'wpDomain' => $params['domain'],
+                               'wpEmail' => $params['email'],
+                               'wpRealName' => $params['realname'],
+                               'wpCreateaccountToken' => $params['token'],
+                               'wpCreateaccount' => $params['mailpassword'] ? null : '1',
+                               'wpCreateaccountMail' => $params['mailpassword'] ? '1' : null
+                       )
+               ) );
+
+               $loginForm = new LoginForm();
+               $loginForm->setContext( $context );
+               $loginForm->load();
+
+               $status = $loginForm->addNewaccountInternal();
+               $result = array();
+               if( $status->isGood() ) {
+                       // Success!
+                       $user = $status->getValue();
+
+                       // If we showed up language selection links, and one was in use, be
+                       // smart (and sensible) and save that language as the user's preference
+                       global $wgLoginLanguageSelector, $wgEmailAuthentication;
+                       if( $wgLoginLanguageSelector && $params['language'] ) {
+                               $user->setOption( 'language', $params['language'] );
+                       }
+
+                       if( $params['mailpassword'] ) {
+                               // If mailpassword was set, disable the password and send an email.
+                               $user->setPassword( null );
+                               $status->merge( $loginForm->mailPasswordInternal( $user, false, 'createaccount-title', 'createaccount-text' ) );
+                       } elseif( $wgEmailAuthentication && Sanitizer::validateEmail( $user->getEmail() ) ) {
+                               // Send out an email authentication message if needed
+                               $status->merge( $user->sendConfirmationMail() );
+                       }
+
+                       // Save settings (including confirmation token)
+                       $user->saveSettings();
+
+                       wfRunHooks( 'AddNewAccount', array( $user, false ) );
+                       $user->addNewUserLogEntry( $this->getUser()->isAnon(), $params['reason'] );
+
+                       // Add username, id, and token to result.
+                       $result['username'] = $user->getName();
+                       $result['userid'] = $user->getId();
+                       $result['token'] = $user->getToken();
+               }
+
+               $apiResult = $this->getResult();
+
+               if( $status->hasMessage( 'sessionfailure' ) ) {
+                       // Token was incorrect, so add it to result, but don't throw an exception.
+                       $result['token'] = LoginForm::getCreateaccountToken();
+                       $result['result'] = 'needtoken';
+               } elseif( !$status->isOK() ) {
+                       // There was an error. Die now.
+                       // Cannot use dieUsageMsg() directly because extensions
+                       // might return custom error messages.
+                       $errors = $status->getErrorsArray();
+                       if( $errors[0] instanceof Message ) {
+                               $code = 'aborted';
+                               $desc = $errors[0];
+                       } else {
+                               $code = array_shift( $errors[0] );
+                               $desc = wfMessage( $code, $errors[0] );
+                       }
+                       $this->dieUsage( $desc, $code );
+               } elseif( !$status->isGood() ) {
+                       // Status is not good, but OK. This means warnings.
+                       $result['result'] = 'warning';
+
+                       // Add any warnings to the result
+                       $warnings = $status->getErrorsByType( 'warning' );
+                       if( $warnings ) {
+                               foreach( $warnings as &$warning ) {
+                                       $apiResult->setIndexedTagName( $warning['params'], 'param' );
+                               }
+                               $apiResult->setIndexedTagName( $warnings, 'warning' );
+                               $result['warnings'] = $warnings;
+                       }
+               } else {
+                       // Everything was fine.
+                       $result['result'] = 'success';
+               }
+
+               $apiResult->addValue( null, 'createaccount', $result );
+       }
+
+       public function getDescription() {
+               return 'Create a new user account.';
+       }
+
+       public function mustBePosted() {
+               return true;
+       }
+
+       public function isReadMode() {
+               return false;
+       }
+
+       public function isWriteMode() {
+               return true;
+       }
+
+       public function getAllowedParams() {
+               global $wgEmailConfirmToEdit;
+               return array(
+                       'name' => array(
+                               ApiBase::PARAM_TYPE => 'user',
+                               ApiBase::PARAM_REQUIRED => true
+                       ),
+                       'password' => null,
+                       'domain' => null,
+                       'token' => null,
+                       'email' => array(
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_REQUIRED => $wgEmailConfirmToEdit
+                       ),
+                       'realname' => null,
+                       'mailpassword' => array(
+                               ApiBase::PARAM_TYPE => 'boolean',
+                               ApiBase::PARAM_DFLT => false
+                       ),
+                       'reason' => null,
+                       'language' => null
+               );
+       }
+
+       public function getParamDescription() {
+               $p = $this->getModulePrefix();
+               return array(
+                       'name' => 'User Name',
+                       'password' => "Password (ignored if {$p}mailpassword is set)",
+                       'domain' => 'Domain (optional)',
+                       'token' => 'Account creation token obtained in first request',
+                       'email' => 'Email address of user',
+                       'realname' => 'Real Name of user',
+                       'mailpassword' => 'Whether to generate and mail a random password to the user',
+                       'reason' => "Optional reason for creating the account (used when {$p}mailpassword is set)",
+                       'language' => 'Language code to set for the user.'
+               );
+       }
+
+       public function getResultProperties() {
+               return array(
+                       'createaccount' => array(
+                               'result' => array(
+                                       ApiBase::PROP_TYPE => array(
+                                               'success',
+                                               'warning',
+                                               'needtoken'
+                                       )
+                               ),
+                               'username' => array(
+                                       ApiBase::PROP_TYPE => 'string',
+                                       ApiBase::PROP_NULLABLE => true
+                               ),
+                               'userid' => array(
+                                       ApiBase::PROP_TYPE => 'int',
+                                       ApiBase::PROP_NULLABLE => true
+                               ),
+                               'token' => array(
+                                       ApiBase::PROP_TYPE => 'string',
+                                       ApiBase::PROP_NULLABLE => true
+                               ),
+                       )
+               );
+       }
+
+       public function getPossibleErrors() {
+               $localErrors = array(
+                       'wrongpassword',
+                       'sessionfailure',
+                       'sorbs_create_account_reason',
+                       'noname',
+                       'userexists',
+                       'password-name-match',
+                       'password-login-forbidden',
+                       'noemailtitle',
+                       'invalidemailaddress',
+                       'externaldberror'
+               );
+
+               $errors = parent::getPossibleErrors();
+               // All local errors are from LoginForm, which means they're actually message keys.
+               foreach( $localErrors as $error ) {
+                       $errors[] = array( 'code' => $error, 'info' => wfMessage( $error )->parse() );
+               }
+
+               // 'passwordtooshort' has parameters. :(
+               global $wgMinimalPasswordLength;
+               $errors[] = array(
+                       'code' => 'passwordtooshort',
+                       'info' => wfMessage( 'passwordtooshort', $wgMinimalPasswordLength )->parse()
+               );
+               return $errors;
+       }
+
+       public function getExamples() {
+               return array(
+                       'api.php?action=createaccount&name=testuser&password=test123',
+                       'api.php?action=createaccount&name=testmailuser&mailpassword=true&reason=MyReason',
+               );
+       }
+
+       public function getHelpUrls() {
+               return 'https://www.mediawiki.org/wiki/API:Account creation';
+       }
+
+       public function getVersion() {
+               return __CLASS__ . ': $Id$';
+       }
+}
index 1e8c84e..135e0bd 100644 (file)
@@ -51,6 +51,7 @@ class ApiMain extends ApiBase {
        private static $Modules = array(
                'login' => 'ApiLogin',
                'logout' => 'ApiLogout',
+               'createaccount' => 'ApiCreateAccount',
                'query' => 'ApiQuery',
                'expandtemplates' => 'ApiExpandTemplates',
                'parse' => 'ApiParse',
diff --git a/tests/phpunit/includes/api/ApiAccountCreationTest.php b/tests/phpunit/includes/api/ApiAccountCreationTest.php
new file mode 100644 (file)
index 0000000..933db0d
--- /dev/null
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * @group Database
+ * @group API
+ */
+class ApiCreateAccountTest extends ApiTestCase {
+       function setUp() {
+               parent::setUp();
+               LoginForm::setCreateaccountToken();
+       }
+
+       /**
+        * Test the account creation API with a valid request. Also
+        * make sure the new account can log in and is valid.
+        */
+       function testValid() {
+               global $wgServer;
+
+               if ( !isset( $wgServer ) ) {
+                       $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
+               }
+
+               $password = User::randomPassword();
+
+               $ret = $this->doApiRequest( array(
+                       'action' => 'createaccount',
+                       'name' => 'Apitestnew',
+                       'password' => $password,
+                       'email' => 'test@example.com',
+                       '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( array(
+                       '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( array(
+                       '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( array(
+                       '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 );
+       }
+
+       /**
+        * Make sure requests with no names are invalid.
+        * @expectedException UsageException
+        */
+       function testNoName() {
+               $ret = $this->doApiRequest( array(
+                       'action' => 'createaccount',
+                       'token' => LoginForm::getCreateaccountToken(),
+                       'password' => 'password',
+               ) );
+       }
+
+       /**
+        * Make sure requests with no password are invalid.
+        * @expectedException UsageException
+        */
+       function testNoPassword() {
+               $ret = $this->doApiRequest( array(
+                       'action' => 'createaccount',
+                       'name' => 'testName',
+                       'token' => LoginForm::getCreateaccountToken(),
+               ) );
+       }
+
+       /**
+        * Make sure requests with existing users are invalid.
+        * @expectedException UsageException
+        */
+       function testExistingUser() {
+               $this->doApiRequest( array(
+                       'action' => 'createaccount',
+                       'name' => 'Apitestsysop',
+                       'token' => LoginForm::getCreateaccountToken(),
+                       'password' => 'password',
+                       'email' => 'test@domain.test',
+               ) );
+       }
+
+       /**
+        * Make sure requests with invalid emails are invalid.
+        * @expectedException UsageException
+        */
+       function testInvalidEmail() {
+               $this->doApiRequest( array(
+                       'action' => 'createaccount',
+                       'name' => 'Test User',
+                       'token' => LoginForm::getCreateaccountToken(),
+                       'password' => 'password',
+                       'email' => 'sjlfsjklj',
+               ) );
+       }
+}