+<?php
+
+/**
+ * Encapsulates the backend activities of logging a user into the wiki.
+ */
+class Login {
+
+ const SUCCESS = 0;
+ const NO_NAME = 1;
+ const ILLEGAL = 2;
+ const WRONG_PLUGIN_PASS = 3;
+ const NOT_EXISTS = 4;
+ const WRONG_PASS = 5;
+ const EMPTY_PASS = 6;
+ const RESET_PASS = 7;
+ const ABORTED = 8;
+ const THROTTLED = 10;
+ const FAILED = 11;
+ const READ_ONLY = 12;
+
+ const MAIL_PASSCHANGE_FORBIDDEN = 21;
+ const MAIL_BLOCKED = 22;
+ const MAIL_PING_THROTTLED = 23;
+ const MAIL_PASS_THROTTLED = 24;
+ const MAIL_EMPTY_EMAIL = 25;
+ const MAIL_BAD_IP = 26;
+ const MAIL_ERROR = 27;
+
+ const CREATE_BLOCKED = 40;
+ const CREATE_EXISTS = 41;
+ const CREATE_SORBS = 42;
+ const CREATE_BADDOMAIN = 43;
+ const CREATE_BADNAME = 44;
+ const CREATE_BADPASS = 45;
+ const CREATE_NEEDEMAIL = 46;
+ const CREATE_BADEMAIL = 47;
+
+ protected $mName;
+ protected $mPassword;
+ public $mRemember; # 0 or 1
+ public $mEmail;
+ public $mDomain;
+ public $mRealname;
+
+ private $mExtUser = null;
+
+ public $mUser;
+
+ public $mLoginResult = '';
+ public $mMailResult = '';
+ public $mCreateResult = '';
+
+ /**
+ * Constructor
+ * @param WebRequest $request A WebRequest object passed by reference.
+ * uses $wgRequest if not given.
+ */
+ public function __construct( &$request=null ) {
+ global $wgRequest, $wgAuth, $wgHiddenPrefs, $wgEnableEmail, $wgRedirectOnLogin;
+ if( !$request ) $request = &$wgRequest;
+
+ $this->mName = $request->getText( 'wpName' );
+ $this->mPassword = $request->getText( 'wpPassword' );
+ $this->mDomain = $request->getText( 'wpDomain' );
+ $this->mRemember = $request->getCheck( 'wpRemember' ) ? 1 : 0;
+
+ if( $wgEnableEmail ) {
+ $this->mEmail = $request->getText( 'wpEmail' );
+ } else {
+ $this->mEmail = '';
+ }
+ if( !in_array( 'realname', $wgHiddenPrefs ) ) {
+ $this->mRealName = $request->getText( 'wpRealName' );
+ } else {
+ $this->mRealName = '';
+ }
+
+ if( !$wgAuth->validDomain( $this->mDomain ) ) {
+ $this->mDomain = 'invaliddomain';
+ }
+ $wgAuth->setDomain( $this->mDomain );
+
+ # Load the user, if they exist in the local database.
+ $this->mUser = User::newFromName( trim( $this->mName ), 'usable' );
+ }
+
+ /**
+ * Having initialised the Login object with (at least) the wpName
+ * and wpPassword pair, attempt to authenticate the user and log
+ * them into the wiki. Authentication may come from the local
+ * user database, or from an AuthPlugin- or ExternalUser-based
+ * foreign database; in the latter case, a local user record may
+ * or may not be created and initialised.
+ * @return a Login class constant representing the status.
+ */
+ public function attemptLogin(){
+ global $wgUser;
+
+ $code = $this->authenticateUserData();
+ if( $code != self::SUCCESS ){
+ return $code;
+ }
+
+ # Log the user in and remember them if they asked for that.
+ if( (bool)$this->mRemember != (bool)$wgUser->getOption( 'rememberpassword' ) ) {
+ $wgUser->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
+ $wgUser->saveSettings();
+ } else {
+ $wgUser->invalidateCache();
+ }
+ $wgUser->setCookies();
+
+ # Reset the password throttle
+ $key = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) );
+ global $wgMemc;
+ $wgMemc->delete( $key );
+
+ wfRunHooks( 'UserLoginComplete', array( &$wgUser, &$this->mLoginResult ) );
+
+ return self::SUCCESS;
+ }
+
+ /**
+ * Check whether there is an external authentication mechanism from
+ * which we can automatically authenticate the user and create a
+ * local account for them.
+ * @return integer Status code. Login::SUCCESS == clear to proceed
+ * with user creation.
+ */
+ protected function canAutoCreate() {
+ global $wgAuth, $wgUser, $wgAutocreatePolicy;
+
+ if( $wgUser->isBlockedFromCreateAccount() ) {
+ wfDebug( __METHOD__.": user is blocked from account creation\n" );
+ return self::CREATE_BLOCKED;
+ }
+
+ # If the external authentication plugin allows it, automatically
+ # create a new account for users that are externally defined but
+ # have not yet logged in.
+ if( $this->mExtUser ) {
+ # mExtUser is neither null nor false, so use the new
+ # ExternalAuth system.
+ if( $wgAutocreatePolicy == 'never' ) {
+ return self::NOT_EXISTS;
+ }
+ if( !$this->mExtUser->authenticate( $this->mPassword ) ) {
+ return self::WRONG_PLUGIN_PASS;
+ }
+ } else {
+ # Old AuthPlugin.
+ if( !$wgAuth->autoCreate() ) {
+ return self::NOT_EXISTS;
+ }
+ if( !$wgAuth->userExists( $this->mUser->getName() ) ) {
+ wfDebug( __METHOD__.": user does not exist\n" );
+ return self::NOT_EXISTS;
+ }
+ if( !$wgAuth->authenticate( $this->mUser->getName(), $this->mPassword ) ) {
+ wfDebug( __METHOD__.": \$wgAuth->authenticate() returned false, aborting\n" );
+ return self::WRONG_PLUGIN_PASS;
+ }
+ }
+
+ return self::SUCCESS;
+ }
+
+ /**
+ * Internally authenticate the login request.
+ *
+ * This may create a local account as a side effect if the
+ * authentication plugin allows transparent local account
+ * creation.
+ */
+ protected function authenticateUserData() {
+ global $wgUser, $wgAuth;
+
+ if ( '' == $this->mName ) {
+ return self::NO_NAME;
+ }
+
+ global $wgPasswordAttemptThrottle;
+ $throttleCount = 0;
+ if ( is_array( $wgPasswordAttemptThrottle ) ) {
+ $throttleKey = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) );
+ $count = $wgPasswordAttemptThrottle['count'];
+ $period = $wgPasswordAttemptThrottle['seconds'];
+
+ global $wgMemc;
+ $throttleCount = $wgMemc->get( $throttleKey );
+ if ( !$throttleCount ) {
+ $wgMemc->add( $throttleKey, 1, $period ); # Start counter
+ } else if ( $throttleCount < $count ) {
+ $wgMemc->incr($throttleKey);
+ } else if ( $throttleCount >= $count ) {
+ return self::THROTTLED;
+ }
+ }
+
+ # Unstub $wgUser now, and check to see if we're logging in as the same
+ # name. As well as the obvious, unstubbing $wgUser (say by calling
+ # getName()) calls the UserLoadFromSession hook, which potentially
+ # creates the user in the database. Until we load $wgUser, checking
+ # for user existence using User::newFromName($name)->getId() below
+ # will effectively be using stale data.
+ if ( $wgUser->getName() === $this->mName ) {
+ wfDebug( __METHOD__.": already logged in as {$this->mName}\n" );
+ return self::SUCCESS;
+ }
+
+ $this->mExtUser = ExternalUser::newFromName( $this->mName );
+
+ # TODO: Allow some magic here for invalid external names, e.g., let the
+ # user choose a different wiki name.
+ if( is_null( $this->mUser ) || !User::isUsableName( $this->mUser->getName() ) ) {
+ return self::ILLEGAL;
+ }
+
+ # If the user doesn't exist in the local database, our only chance
+ # is for an external auth plugin to autocreate the local user.
+ if ( $this->mUser->getID() == 0 ) {
+ if ( $this->canAutoCreate() == self::SUCCESS ) {
+ $isAutoCreated = true;
+ wfDebug( __METHOD__.": creating account\n" );
+ $this->initUser( true );
+ } else {
+ return $this->canAutoCreate();
+ }
+ } else {
+ $isAutoCreated = false;
+ $this->mUser->load();
+ }
+
+ # Give general extensions, such as a captcha, a chance to abort logins
+ $abort = self::ABORTED;
+ if( !wfRunHooks( 'AbortLogin', array( $this->mUser, $this->mPassword, &$abort ) ) ) {
+ return $abort;
+ }
+
+ if( !$this->mUser->checkPassword( $this->mPassword ) ) {
+ if( $this->mUser->checkTemporaryPassword( $this->mPassword ) ) {
+ # The e-mailed temporary password should not be used for actual
+ # logins; that's a very sloppy habit, and insecure if an
+ # attacker has a few seconds to click "search" on someone's
+ # open mail reader.
+ #
+ # Allow it to be used only to reset the password a single time
+ # to a new value, which won't be in the user's e-mail archives
+ #
+ # For backwards compatibility, we'll still recognize it at the
+ # login form to minimize surprises for people who have been
+ # logging in with a temporary password for some time.
+ #
+ # As a side-effect, we can authenticate the user's e-mail ad-
+ # dress if it's not already done, since the temporary password
+ # was sent via e-mail.
+ if( !$this->mUser->isEmailConfirmed() ) {
+ $this->mUser->confirmEmail();
+ $this->mUser->saveSettings();
+ }
+
+ # At this point we just return an appropriate code/ indicating
+ # that the UI should show a password reset form; bot interfaces
+ # etc will probably just fail cleanly here.
+ $retval = self::RESET_PASS;
+ } else {
+ $retval = ( $this->mPassword === '' ) ? self::EMPTY_PASS : self::WRONG_PASS;
+ }
+ } else {
+ $wgAuth->updateUser( $this->mUser );
+ $wgUser = $this->mUser;
+
+ # Reset throttle after a successful login
+ if( $throttleCount ) {
+ $wgMemc->delete( $throttleKey );
+ }
+
+ if( $isAutoCreated ) {
+ # Must be run after $wgUser is set, for correct new user log
+ wfRunHooks( 'AuthPluginAutoCreate', array( $wgUser ) );
+ }
+
+ $retval = self::SUCCESS;
+ }
+ wfRunHooks( 'LoginAuthenticateAudit', array( $this->mUser, $this->mPassword, $retval ) );
+ return $retval;
+ }
+
+ /**
+ * Actually add a user to the database.
+ * Give it a User object that has been initialised with a name.
+ *
+ * @param $autocreate Bool is this is an autocreation from an external
+ * authentication database?
+ * @param $byEmail Bool is this request going to be handled by sending
+ * the password by email?
+ * @return Bool whether creation was successful (should only fail for
+ * Db errors etc).
+ */
+ protected function initUser( $autocreate=false, $byEmail=false ) {
+ global $wgAuth;
+
+ $fields = array(
+ 'name' => $this->mName,
+ 'password' => $byEmail ? null : $this->mPassword,
+ 'email' => $this->mEmail,
+ 'options' => array(
+ 'rememberpassword' => $this->mRemember ? 1 : 0,
+ ),
+ );
+
+ $this->mUser = User::createNew( $this->mName, $fields );
+
+ if( $this->mUser === null ){
+ return null;
+ }
+
+ # Let old AuthPlugins play with the user
+ $wgAuth->initUser( $this->mUser, $autocreate );
+
+ # Or new ExternalUser plugins
+ if( $this->mExtUser ) {
+ $this->mExtUser->link( $this->mUser->getId() );
+ $email = $this->mExtUser->getPref( 'emailaddress' );
+ if( $email && !$this->mEmail ) {
+ $this->mUser->setEmail( $email );
+ }
+ }
+
+ # Update user count and newuser logs
+ $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
+ $ssUpdate->doUpdate();
+ if( $autocreate )
+ $this->mUser->addNewUserLogEntryAutoCreate();
+ else
+ $this->mUser->addNewUserLogEntry( $byEmail );
+
+ # Run hooks
+ wfRunHooks( 'AddNewAccount', array( $this->mUser ) );
+
+ return true;
+ }
+
+ /**
+ * Entry point to create a new local account from user-supplied
+ * data loaded from the WebRequest. We handle initialising the
+ * email here because it's needed for some backend things; frontend
+ * interfaces calling this should handle recording things like
+ * preference options
+ * @param $byEmail Bool whether to email the user their new password
+ * @return Status code; Login::SUCCESS == the user was successfully created
+ */
+ public function attemptCreation( $byEmail=false ) {
+ global $wgUser, $wgOut;
+ global $wgEnableSorbs, $wgProxyWhitelist;
+ global $wgMemc, $wgAccountCreationThrottle;
+ global $wgAuth;
+ global $wgEmailAuthentication, $wgEmailConfirmToEdit;
+
+ if( wfReadOnly() )
+ return self::READ_ONLY;
+
+ # If the user passes an invalid domain, something is fishy
+ if( !$wgAuth->validDomain( $this->mDomain ) ) {
+ $this->mCreateResult = 'wrongpassword';
+ return self::CREATE_BADDOMAIN;
+ }
+
+ # If we are not allowing users to login locally, we should be checking
+ # to see if the user is actually able to authenticate to the authenti-
+ # cation server before they create an account (otherwise, they can
+ # create a local account and login as any domain user). We only need
+ # to check this for domains that aren't local.
+ if( !in_array( $this->mDomain, array( 'local', '' ) )
+ && !$wgAuth->canCreateAccounts()
+ && ( !$wgAuth->userExists( $this->mUsername )
+ || !$wgAuth->authenticate( $this->mUsername, $this->mPassword )
+ ) )
+ {
+ $this->mCreateResult = 'wrongpassword';
+ return self::WRONG_PLUGIN_PASS;
+ }
+
+ $ip = wfGetIP();
+ if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) &&
+ $wgUser->inSorbsBlacklist( $ip ) )
+ {
+ $this->mCreateResult = 'sorbs_create_account_reason';
+ return self::CREATE_SORBS;
+ }
+
+ # Now create a dummy user ($user) and check if it is valid
+ $name = trim( $this->mName );
+ $user = User::newFromName( $name, 'creatable' );
+ if ( is_null( $user ) ) {
+ $this->mCreateResult = 'noname';
+ return self::CREATE_BADNAME;
+ }
+
+ if ( $this->mUser->idForName() != 0 ) {
+ $this->mCreateResult = 'userexists';
+ return self::CREATE_EXISTS;
+ }
+
+ # Check that the password is acceptable, if we're actually
+ # going to use it
+ if( !$byEmail ){
+ $valid = $this->mUser->isValidPassword( $this->mPassword );
+ if ( $valid !== true ) {
+ $this->mCreateResult = $valid;
+ return self::CREATE_BADPASS;
+ }
+ }
+
+ # if you need a confirmed email address to edit, then obviously you
+ # need an email address. Equally if we're going to send the password to it.
+ if ( $wgEmailConfirmToEdit && empty( $this->mEmail ) || $byEmail ) {
+ $this->mCreateResult = 'noemailcreate';
+ return self::CREATE_NEEDEMAIL;
+ }
+
+ if( !empty( $this->mEmail ) && !User::isValidEmailAddr( $this->mEmail ) ) {
+ $this->mCreateResult = 'invalidemailaddress';
+ return self::CREATE_BADEMAIL;
+ }
+
+ # Set some additional data so the AbortNewAccount hook can be used for
+ # more than just username validation
+ $this->mUser->setEmail( $this->mEmail );
+ $this->mUser->setRealName( $this->mRealName );
+
+ if( !wfRunHooks( 'AbortNewAccount', array( $this->mUser, &$this->mCreateResult ) ) ) {
+ # Hook point to add extra creation throttles and blocks
+ wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" );
+ return self::ABORTED;
+ }
+
+ if ( $wgAccountCreationThrottle && $wgUser->isPingLimitable() ) {
+ $key = wfMemcKey( 'acctcreate', 'ip', $ip );
+ $value = $wgMemc->get( $key );
+ if ( !$value ) {
+ $wgMemc->set( $key, 0, 86400 );
+ }
+ if ( $value >= $wgAccountCreationThrottle ) {
+ return self::THROTTLED;
+ }
+ $wgMemc->incr( $key );
+ }
+
+ # Since we're creating a new local user, give the external
+ # database a chance to synchronise.
+ if( !$wgAuth->addUser( $this->mUser, $this->mPassword, $this->mEmail, $this->mRealName ) ) {
+ $this->mCreateResult = 'externaldberror';
+ return self::ABORTED;
+ }
+
+ $result = $this->initUser( false, $byEmail );
+ if( $result === null )
+ # It's unlikely we'd get here without some exception
+ # being thrown, but it's probably possible...
+ return self::FAILED;
+
+
+ # Send out an email message if needed
+ if( $byEmail ){
+ $this->mailPassword( 'createaccount-title', 'createaccount-text' );
+ if( WikiError::isError( $this->mMailResult ) ){
+ # FIXME: If the password email hasn't gone out,
+ # then the account is inaccessible :(
+ return self::MAIL_ERROR;
+ } else {
+ return self::SUCCESS;
+ }
+ } else {
+ if( $wgEmailAuthentication && User::isValidEmailAddr( $this->mUser->getEmail() ) )
+ {
+ $this->mMailResult = $this->mUser->sendConfirmationMail();
+ return WikiError::isError( $this->mMailResult )
+ ? self::MAIL_ERROR
+ : self::SUCCESS;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Email the user a new password, if appropriate to do so.
+ * @param $text String message key
+ * @param $title String message key
+ * @return Status code
+ */
+ public function mailPassword( $text='passwordremindertext', $title='passwordremindertitle' ) {
+ global $wgUser, $wgOut, $wgAuth, $wgServer, $wgScript, $wgNewPasswordExpiry;
+
+ if( wfReadOnly() )
+ return self::READ_ONLY;
+
+ # If we let the email go out, it will take users to a form where
+ # they are forced to change their password, so don't let us go
+ # there if we don't want passwords changed.
+ if( !$wgAuth->allowPasswordChange() )
+ return self::MAIL_PASSCHANGE_FORBIDDEN;
+
+ # Check against blocked IPs
+ # FIXME: -- should we not?
+ if( $wgUser->isBlocked() )
+ return self::MAIL_BLOCKED;
+
+ # Check for hooks
+ if( !wfRunHooks( 'UserLoginMailPassword', array( $this->mName, &$this->mMailResult ) ) )
+ return self::ABORTED;
+
+ # Check against the rate limiter
+ if( $wgUser->pingLimiter( 'mailpassword' ) )
+ return self::MAIL_PING_THROTTLED;
+
+ # Check for a valid name
+ if ($this->mName === '' )
+ return self::NO_NAME;
+ $this->mUser = User::newFromName( $this->mName );
+ if( is_null( $this->mUser ) )
+ return self::NO_NAME;
+
+ # And that the resulting user actually exists
+ if ( $this->mUser->getId() === 0 )
+ return self::NOT_EXISTS;
+
+ # Check against password throttle
+ if ( $this->mUser->isPasswordReminderThrottled() )
+ return self::MAIL_PASS_THROTTLED;
+
+ # User doesn't have email address set
+ if ( $this->mUser->getEmail() === '' )
+ return self::MAIL_EMPTY_EMAIL;
+
+ # Don't send to people who are acting fishily by hiding their IP
+ $ip = wfGetIP();
+ if( !$ip )
+ return self::MAIL_BAD_IP;
+
+ # Let hooks do things with the data
+ wfRunHooks( 'User::mailPasswordInternal', array(&$wgUser, &$ip, &$this->mUser) );
+
+ $newpass = $this->mUser->randomPassword();
+ $this->mUser->setNewpassword( $newpass, true );
+ $this->mUser->saveSettings();
+
+ $message = wfMsgExt( $text, array( 'parsemag' ), $ip, $this->mUser->getName(), $newpass,
+ $wgServer . $wgScript, round( $wgNewPasswordExpiry / 86400 ) );
+ $this->mMailResult = $this->mUser->sendMail( wfMsg( $title ), $message );
+
+ if( WikiError::isError( $this->mMailResult ) ) {
+ return self::MAIL_ERROR;
+ } else {
+ return self::SUCCESS;
+ }
+ }
+}
+
+/**
+ * For backwards compatibility, mainly with the state constants, which
+ * could be referred to in old extensions with the old class name.
+ * @deprecated
+ */
+class LoginForm extends Login {}
\ No newline at end of file