4 * Encapsulates the backend activities of logging a user into the wiki.
11 const WRONG_PLUGIN_PASS
= 3;
21 const MAIL_PASSCHANGE_FORBIDDEN
= 21;
22 const MAIL_BLOCKED
= 22;
23 const MAIL_PING_THROTTLED
= 23;
24 const MAIL_PASS_THROTTLED
= 24;
25 const MAIL_EMPTY_EMAIL
= 25;
26 const MAIL_BAD_IP
= 26;
27 const MAIL_ERROR
= 27;
29 const CREATE_BLOCKED
= 40;
30 const CREATE_EXISTS
= 41;
31 const CREATE_SORBS
= 42;
32 const CREATE_BADDOMAIN
= 43;
33 const CREATE_BADNAME
= 44;
34 const CREATE_BADPASS
= 45;
35 const CREATE_NEEDEMAIL
= 46;
36 const CREATE_BADEMAIL
= 47;
40 public $mRemember; # 0 or 1
45 private $mExtUser = null;
49 public $mLoginResult = '';
50 public $mMailResult = '';
51 public $mCreateResult = '';
55 * @param WebRequest $request A WebRequest object passed by reference.
56 * uses $wgRequest if not given.
58 public function __construct( &$request=null ) {
59 global $wgRequest, $wgAuth, $wgHiddenPrefs, $wgEnableEmail, $wgRedirectOnLogin;
60 if( !$request ) $request = &$wgRequest;
62 $this->mName
= $request->getText( 'wpName' );
63 $this->mPassword
= $request->getText( 'wpPassword' );
64 $this->mDomain
= $request->getText( 'wpDomain' );
65 $this->mRemember
= $request->getCheck( 'wpRemember' ) ?
1 : 0;
67 if( $wgEnableEmail ) {
68 $this->mEmail
= $request->getText( 'wpEmail' );
72 if( !in_array( 'realname', $wgHiddenPrefs ) ) {
73 $this->mRealName
= $request->getText( 'wpRealName' );
75 $this->mRealName
= '';
78 if( !$wgAuth->validDomain( $this->mDomain
) ) {
79 $this->mDomain
= 'invaliddomain';
81 $wgAuth->setDomain( $this->mDomain
);
83 # Load the user, if they exist in the local database.
84 $this->mUser
= User
::newFromName( trim( $this->mName
), 'usable' );
88 * Having initialised the Login object with (at least) the wpName
89 * and wpPassword pair, attempt to authenticate the user and log
90 * them into the wiki. Authentication may come from the local
91 * user database, or from an AuthPlugin- or ExternalUser-based
92 * foreign database; in the latter case, a local user record may
93 * or may not be created and initialised.
94 * @return a Login class constant representing the status.
96 public function attemptLogin(){
99 $code = $this->authenticateUserData();
100 if( $code != self
::SUCCESS
){
104 # Log the user in and remember them if they asked for that.
105 if( (bool)$this->mRemember
!= (bool)$wgUser->getOption( 'rememberpassword' ) ) {
106 $wgUser->setOption( 'rememberpassword', $this->mRemember ?
1 : 0 );
107 $wgUser->saveSettings();
109 $wgUser->invalidateCache();
111 $wgUser->setCookies();
113 # Reset the password throttle
114 $key = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName
) );
116 $wgMemc->delete( $key );
118 wfRunHooks( 'UserLoginComplete', array( &$wgUser, &$this->mLoginResult
) );
120 return self
::SUCCESS
;
124 * Check whether there is an external authentication mechanism from
125 * which we can automatically authenticate the user and create a
126 * local account for them.
127 * @return integer Status code. Login::SUCCESS == clear to proceed
128 * with user creation.
130 protected function canAutoCreate() {
131 global $wgAuth, $wgUser, $wgAutocreatePolicy;
133 if( $wgUser->isBlockedFromCreateAccount() ) {
134 wfDebug( __METHOD__
.": user is blocked from account creation\n" );
135 return self
::CREATE_BLOCKED
;
138 # If the external authentication plugin allows it, automatically
139 # create a new account for users that are externally defined but
140 # have not yet logged in.
141 if( $this->mExtUser
) {
142 # mExtUser is neither null nor false, so use the new
143 # ExternalAuth system.
144 if( $wgAutocreatePolicy == 'never' ) {
145 return self
::NOT_EXISTS
;
147 if( !$this->mExtUser
->authenticate( $this->mPassword
) ) {
148 return self
::WRONG_PLUGIN_PASS
;
152 if( !$wgAuth->autoCreate() ) {
153 return self
::NOT_EXISTS
;
155 if( !$wgAuth->userExists( $this->mUser
->getName() ) ) {
156 wfDebug( __METHOD__
.": user does not exist\n" );
157 return self
::NOT_EXISTS
;
159 if( !$wgAuth->authenticate( $this->mUser
->getName(), $this->mPassword
) ) {
160 wfDebug( __METHOD__
.": \$wgAuth->authenticate() returned false, aborting\n" );
161 return self
::WRONG_PLUGIN_PASS
;
165 return self
::SUCCESS
;
169 * Internally authenticate the login request.
171 * This may create a local account as a side effect if the
172 * authentication plugin allows transparent local account
175 protected function authenticateUserData() {
176 global $wgUser, $wgAuth;
178 if ( '' == $this->mName
) {
179 return self
::NO_NAME
;
182 global $wgPasswordAttemptThrottle;
184 if ( is_array( $wgPasswordAttemptThrottle ) ) {
185 $throttleKey = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName
) );
186 $count = $wgPasswordAttemptThrottle['count'];
187 $period = $wgPasswordAttemptThrottle['seconds'];
190 $throttleCount = $wgMemc->get( $throttleKey );
191 if ( !$throttleCount ) {
192 $wgMemc->add( $throttleKey, 1, $period ); # Start counter
193 } else if ( $throttleCount < $count ) {
194 $wgMemc->incr($throttleKey);
195 } else if ( $throttleCount >= $count ) {
196 return self
::THROTTLED
;
200 # Unstub $wgUser now, and check to see if we're logging in as the same
201 # name. As well as the obvious, unstubbing $wgUser (say by calling
202 # getName()) calls the UserLoadFromSession hook, which potentially
203 # creates the user in the database. Until we load $wgUser, checking
204 # for user existence using User::newFromName($name)->getId() below
205 # will effectively be using stale data.
206 if ( $wgUser->getName() === $this->mName
) {
207 wfDebug( __METHOD__
.": already logged in as {$this->mName}\n" );
208 return self
::SUCCESS
;
211 $this->mExtUser
= ExternalUser
::newFromName( $this->mName
);
213 # TODO: Allow some magic here for invalid external names, e.g., let the
214 # user choose a different wiki name.
215 if( is_null( $this->mUser
) ||
!User
::isUsableName( $this->mUser
->getName() ) ) {
216 return self
::ILLEGAL
;
219 # If the user doesn't exist in the local database, our only chance
220 # is for an external auth plugin to autocreate the local user.
221 if ( $this->mUser
->getID() == 0 ) {
222 if ( $this->canAutoCreate() == self
::SUCCESS
) {
223 $isAutoCreated = true;
224 wfDebug( __METHOD__
.": creating account\n" );
225 $this->initUser( true );
227 return $this->canAutoCreate();
230 $isAutoCreated = false;
231 $this->mUser
->load();
234 # Give general extensions, such as a captcha, a chance to abort logins
235 $abort = self
::ABORTED
;
236 if( !wfRunHooks( 'AbortLogin', array( $this->mUser
, $this->mPassword
, &$abort ) ) ) {
240 if( !$this->mUser
->checkPassword( $this->mPassword
) ) {
241 if( $this->mUser
->checkTemporaryPassword( $this->mPassword
) ) {
242 # The e-mailed temporary password should not be used for actual
243 # logins; that's a very sloppy habit, and insecure if an
244 # attacker has a few seconds to click "search" on someone's
247 # Allow it to be used only to reset the password a single time
248 # to a new value, which won't be in the user's e-mail archives
250 # For backwards compatibility, we'll still recognize it at the
251 # login form to minimize surprises for people who have been
252 # logging in with a temporary password for some time.
254 # As a side-effect, we can authenticate the user's e-mail ad-
255 # dress if it's not already done, since the temporary password
256 # was sent via e-mail.
257 if( !$this->mUser
->isEmailConfirmed() ) {
258 $this->mUser
->confirmEmail();
259 $this->mUser
->saveSettings();
262 # At this point we just return an appropriate code/ indicating
263 # that the UI should show a password reset form; bot interfaces
264 # etc will probably just fail cleanly here.
265 $retval = self
::RESET_PASS
;
267 $retval = ( $this->mPassword
=== '' ) ? self
::EMPTY_PASS
: self
::WRONG_PASS
;
270 $wgAuth->updateUser( $this->mUser
);
271 $wgUser = $this->mUser
;
273 # Reset throttle after a successful login
274 if( $throttleCount ) {
275 $wgMemc->delete( $throttleKey );
278 if( $isAutoCreated ) {
279 # Must be run after $wgUser is set, for correct new user log
280 wfRunHooks( 'AuthPluginAutoCreate', array( $wgUser ) );
283 $retval = self
::SUCCESS
;
285 wfRunHooks( 'LoginAuthenticateAudit', array( $this->mUser
, $this->mPassword
, $retval ) );
290 * Actually add a user to the database.
291 * Give it a User object that has been initialised with a name.
293 * @param $autocreate Bool is this is an autocreation from an external
294 * authentication database?
295 * @param $byEmail Bool is this request going to be handled by sending
296 * the password by email?
297 * @return Bool whether creation was successful (should only fail for
300 protected function initUser( $autocreate=false, $byEmail=false ) {
301 global $wgAuth, $wgUser;
304 'name' => User
::getCanonicalName( $this->mName
),
305 'password' => $byEmail ?
null : User
::crypt( $this->mPassword
),
306 'email' => $this->mEmail
,
308 'rememberpassword' => $this->mRemember ?
1 : 0,
312 $this->mUser
= User
::createNew( $this->mName
, $fields );
314 if( $this->mUser
=== null ){
318 # Let old AuthPlugins play with the user
319 $wgAuth->initUser( $this->mUser
, $autocreate );
321 # Or new ExternalUser plugins
322 if( $this->mExtUser
) {
323 $this->mExtUser
->link( $this->mUser
->getId() );
324 $email = $this->mExtUser
->getPref( 'emailaddress' );
325 if( $email && !$this->mEmail
) {
326 $this->mUser
->setEmail( $email );
330 # Update user count and newuser logs
331 $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
332 $ssUpdate->doUpdate();
334 $this->mUser
->addNewUserLogEntryAutoCreate();
335 elseif( $wgUser->isAnon() )
336 # Avoid spamming IP addresses all over the newuser log
337 $this->mUser
->addNewUserLogEntry( $this->mUser
, $byEmail );
339 $this->mUser
->addNewUserLogEntry( $wgUser, $byEmail );
342 wfRunHooks( 'AddNewAccount', array( $this->mUser
) );
348 * Entry point to create a new local account from user-supplied
349 * data loaded from the WebRequest. We handle initialising the
350 * email here because it's needed for some backend things; frontend
351 * interfaces calling this should handle recording things like
353 * @param $byEmail Bool whether to email the user their new password
354 * @return Status code; Login::SUCCESS == the user was successfully created
356 public function attemptCreation( $byEmail=false ) {
357 global $wgUser, $wgOut;
358 global $wgEnableSorbs, $wgProxyWhitelist;
359 global $wgMemc, $wgAccountCreationThrottle;
361 global $wgEmailAuthentication, $wgEmailConfirmToEdit;
364 return self
::READ_ONLY
;
366 # If the user passes an invalid domain, something is fishy
367 if( !$wgAuth->validDomain( $this->mDomain
) ) {
368 $this->mCreateResult
= 'wrongpassword';
369 return self
::CREATE_BADDOMAIN
;
372 # If we are not allowing users to login locally, we should be checking
373 # to see if the user is actually able to authenticate to the authenti-
374 # cation server before they create an account (otherwise, they can
375 # create a local account and login as any domain user). We only need
376 # to check this for domains that aren't local.
377 if( !in_array( $this->mDomain
, array( 'local', '' ) )
378 && !$wgAuth->canCreateAccounts()
379 && ( !$wgAuth->userExists( $this->mUsername
)
380 ||
!$wgAuth->authenticate( $this->mUsername
, $this->mPassword
)
383 $this->mCreateResult
= 'wrongpassword';
384 return self
::WRONG_PLUGIN_PASS
;
388 if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) &&
389 $wgUser->inSorbsBlacklist( $ip ) )
391 $this->mCreateResult
= 'sorbs_create_account_reason';
392 return self
::CREATE_SORBS
;
395 # Now create a dummy user ($user) and check if it is valid
396 $name = trim( $this->mName
);
397 $user = User
::newFromName( $name, 'creatable' );
398 if ( is_null( $user ) ) {
399 $this->mCreateResult
= 'noname';
400 return self
::CREATE_BADNAME
;
403 if ( $this->mUser
->idForName() != 0 ) {
404 $this->mCreateResult
= 'userexists';
405 return self
::CREATE_EXISTS
;
408 # Check that the password is acceptable, if we're actually
411 $valid = $this->mUser
->isValidPassword( $this->mPassword
);
412 if ( $valid !== true ) {
413 $this->mCreateResult
= $valid;
414 return self
::CREATE_BADPASS
;
418 # if you need a confirmed email address to edit, then obviously you
419 # need an email address. Equally if we're going to send the password to it.
420 if ( $wgEmailConfirmToEdit && empty( $this->mEmail
) ||
$byEmail ) {
421 $this->mCreateResult
= 'noemailcreate';
422 return self
::CREATE_NEEDEMAIL
;
425 if( !empty( $this->mEmail
) && !User
::isValidEmailAddr( $this->mEmail
) ) {
426 $this->mCreateResult
= 'invalidemailaddress';
427 return self
::CREATE_BADEMAIL
;
430 # Set some additional data so the AbortNewAccount hook can be used for
431 # more than just username validation
432 $this->mUser
->setEmail( $this->mEmail
);
433 $this->mUser
->setRealName( $this->mRealName
);
435 if( !wfRunHooks( 'AbortNewAccount', array( $this->mUser
, &$this->mCreateResult
) ) ) {
436 # Hook point to add extra creation throttles and blocks
437 wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" );
438 return self
::ABORTED
;
441 if ( $wgAccountCreationThrottle && $wgUser->isPingLimitable() ) {
442 $key = wfMemcKey( 'acctcreate', 'ip', $ip );
443 $value = $wgMemc->get( $key );
445 $wgMemc->set( $key, 0, 86400 );
447 if ( $value >= $wgAccountCreationThrottle ) {
448 return self
::THROTTLED
;
450 $wgMemc->incr( $key );
453 # Since we're creating a new local user, give the external
454 # database a chance to synchronise.
455 if( !$wgAuth->addUser( $this->mUser
, $this->mPassword
, $this->mEmail
, $this->mRealName
) ) {
456 $this->mCreateResult
= 'externaldberror';
457 return self
::ABORTED
;
460 $result = $this->initUser( false, $byEmail );
461 if( $result === null )
462 # It's unlikely we'd get here without some exception
463 # being thrown, but it's probably possible...
467 # Send out an email message if needed
469 $this->mailPassword( 'createaccount-title', 'createaccount-text' );
470 if( WikiError
::isError( $this->mMailResult
) ){
471 # FIXME: If the password email hasn't gone out,
472 # then the account is inaccessible :(
473 return self
::MAIL_ERROR
;
475 return self
::SUCCESS
;
478 if( $wgEmailAuthentication && User
::isValidEmailAddr( $this->mUser
->getEmail() ) )
480 $this->mMailResult
= $this->mUser
->sendConfirmationMail();
481 return WikiError
::isError( $this->mMailResult
)
490 * Email the user a new password, if appropriate to do so.
491 * @param $text String message key
492 * @param $title String message key
493 * @return Status code
495 public function mailPassword( $text='passwordremindertext', $title='passwordremindertitle' ) {
496 global $wgUser, $wgOut, $wgAuth, $wgServer, $wgScript, $wgNewPasswordExpiry;
499 return self
::READ_ONLY
;
501 # If we let the email go out, it will take users to a form where
502 # they are forced to change their password, so don't let us go
503 # there if we don't want passwords changed.
504 if( !$wgAuth->allowPasswordChange() )
505 return self
::MAIL_PASSCHANGE_FORBIDDEN
;
507 # Check against blocked IPs
508 # FIXME: -- should we not?
509 if( $wgUser->isBlocked() )
510 return self
::MAIL_BLOCKED
;
513 if( !wfRunHooks( 'UserLoginMailPassword', array( $this->mName
, &$this->mMailResult
) ) )
514 return self
::ABORTED
;
516 # Check against the rate limiter
517 if( $wgUser->pingLimiter( 'mailpassword' ) )
518 return self
::MAIL_PING_THROTTLED
;
520 # Check for a valid name
521 if ($this->mName
=== '' )
522 return self
::NO_NAME
;
523 $this->mUser
= User
::newFromName( $this->mName
);
524 if( is_null( $this->mUser
) )
525 return self
::NO_NAME
;
527 # And that the resulting user actually exists
528 if ( $this->mUser
->getId() === 0 )
529 return self
::NOT_EXISTS
;
531 # Check against password throttle
532 if ( $this->mUser
->isPasswordReminderThrottled() )
533 return self
::MAIL_PASS_THROTTLED
;
535 # User doesn't have email address set
536 if ( $this->mUser
->getEmail() === '' )
537 return self
::MAIL_EMPTY_EMAIL
;
539 # Don't send to people who are acting fishily by hiding their IP
542 return self
::MAIL_BAD_IP
;
544 # Let hooks do things with the data
545 wfRunHooks( 'User::mailPasswordInternal', array(&$wgUser, &$ip, &$this->mUser
) );
547 $newpass = $this->mUser
->randomPassword();
548 $this->mUser
->setNewpassword( $newpass, true );
549 $this->mUser
->saveSettings();
551 $message = wfMsgExt( $text, array( 'parsemag' ), $ip, $this->mUser
->getName(), $newpass,
552 $wgServer . $wgScript, round( $wgNewPasswordExpiry / 86400 ) );
553 $this->mMailResult
= $this->mUser
->sendMail( wfMsg( $title ), $message );
555 if( WikiError
::isError( $this->mMailResult
) ) {
556 return self
::MAIL_ERROR
;
558 return self
::SUCCESS
;
564 * For backwards compatibility, mainly with the state constants, which
565 * could be referred to in old extensions with the old class name.
568 class LoginForm
extends Login
{}