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 $this->mLoginResult
= 'noname';
180 return self
::NO_NAME
;
183 global $wgPasswordAttemptThrottle;
185 if ( is_array( $wgPasswordAttemptThrottle ) ) {
186 $throttleKey = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName
) );
187 $count = $wgPasswordAttemptThrottle['count'];
188 $period = $wgPasswordAttemptThrottle['seconds'];
191 $throttleCount = $wgMemc->get( $throttleKey );
192 if ( !$throttleCount ) {
193 $wgMemc->add( $throttleKey, 1, $period ); # Start counter
194 } else if ( $throttleCount < $count ) {
195 $wgMemc->incr($throttleKey);
196 } else if ( $throttleCount >= $count ) {
197 $this->mLoginResult
= 'login-throttled';
198 return self
::THROTTLED
;
202 # Unstub $wgUser now, and check to see if we're logging in as the same
203 # name. As well as the obvious, unstubbing $wgUser (say by calling
204 # getName()) calls the UserLoadFromSession hook, which potentially
205 # creates the user in the database. Until we load $wgUser, checking
206 # for user existence using User::newFromName($name)->getId() below
207 # will effectively be using stale data.
208 if ( $wgUser->getName() === $this->mName
) {
209 wfDebug( __METHOD__
.": already logged in as {$this->mName}\n" );
210 return self
::SUCCESS
;
213 $this->mExtUser
= ExternalUser
::newFromName( $this->mName
);
215 # If the given username produces a valid ExternalUser, which is
216 # linked to an existing local user, use that, regardless of
217 # whether the username matches up.
218 if( $this->mExtUser
){
219 $user = $this->mExtUser
->getLocalUser();
220 if( $user instanceof User
){
221 $this->mUser
= $user;
225 # TODO: Allow some magic here for invalid external names, e.g., let the
226 # user choose a different wiki name.
227 if( is_null( $this->mUser
) ||
!User
::isUsableName( $this->mUser
->getName() ) ) {
228 return self
::ILLEGAL
;
231 # If the user doesn't exist in the local database, our only chance
232 # is for an external auth plugin to autocreate the local user first.
233 if ( $this->mUser
->getID() == 0 ) {
234 if ( $this->canAutoCreate() == self
::SUCCESS
) {
235 $isAutoCreated = true;
236 wfDebug( __METHOD__
.": creating account\n" );
237 $result = $this->initUser( true );
238 if( $result !== self
::SUCCESS
){
242 return $this->canAutoCreate();
245 $isAutoCreated = false;
246 $this->mUser
->load();
249 # Give general extensions, such as a captcha, a chance to abort logins
250 if( !wfRunHooks( 'AbortLogin', array( $this->mUser
, $this->mPassword
, &$this->mLoginResult
) ) ) {
251 return self
::ABORTED
;
254 if( !$this->mUser
->checkPassword( $this->mPassword
) ) {
255 if( $this->mUser
->checkTemporaryPassword( $this->mPassword
) ) {
256 # The e-mailed temporary password should not be used for actual
257 # logins; that's a very sloppy habit, and insecure if an
258 # attacker has a few seconds to click "search" on someone's
261 # Allow it to be used only to reset the password a single time
262 # to a new value, which won't be in the user's e-mail archives
264 # For backwards compatibility, we'll still recognize it at the
265 # login form to minimize surprises for people who have been
266 # logging in with a temporary password for some time.
268 # As a side-effect, we can authenticate the user's e-mail ad-
269 # dress if it's not already done, since the temporary password
270 # was sent via e-mail.
271 if( !$this->mUser
->isEmailConfirmed() ) {
272 $this->mUser
->confirmEmail();
273 $this->mUser
->saveSettings();
276 # At this point we just return an appropriate code/ indicating
277 # that the UI should show a password reset form; bot interfaces
278 # etc will probably just fail cleanly here.
279 $retval = self
::RESET_PASS
;
281 if( $this->mPassword
=== '' ){
282 $retval = self
::EMPTY_PASS
;
283 $this->mLoginResult
= 'wrongpasswordempty';
285 $retval = self
::WRONG_PASS
;
286 $this->mLoginResult
= 'wrongpassword';
290 $wgAuth->updateUser( $this->mUser
);
291 $wgUser = $this->mUser
;
293 # Reset throttle after a successful login
294 if( $throttleCount ) {
295 $wgMemc->delete( $throttleKey );
298 if( $isAutoCreated ) {
299 # Must be run after $wgUser is set, for correct new user log
300 wfRunHooks( 'AuthPluginAutoCreate', array( $wgUser ) );
303 $retval = self
::SUCCESS
;
305 wfRunHooks( 'LoginAuthenticateAudit', array( $this->mUser
, $this->mPassword
, $retval ) );
310 * Actually add a user to the database.
311 * Give it a User object that has been initialised with a name.
313 * @param $autocreate Bool is this is an autocreation from an external
314 * authentication database?
315 * @param $byEmail Bool is this request going to be handled by sending
316 * the password by email?
317 * @return Bool whether creation was successful (should only fail for
320 protected function initUser( $autocreate=false, $byEmail=false ) {
321 global $wgAuth, $wgUser;
324 'name' => User
::getCanonicalName( $this->mName
),
325 'password' => $byEmail ?
null : User
::crypt( $this->mPassword
),
326 'email' => $this->mEmail
,
328 'rememberpassword' => $this->mRemember ?
1 : 0,
332 $this->mUser
= User
::createNew( $this->mName
, $fields );
334 if( $this->mUser
=== null ){
338 # Let old AuthPlugins play with the user
339 $wgAuth->initUser( $this->mUser
, $autocreate );
341 # Or new ExternalUser plugins
342 if( $this->mExtUser
) {
343 $this->mExtUser
->link( $this->mUser
->getId() );
344 $email = $this->mExtUser
->getPref( 'emailaddress' );
345 if( $email && !$this->mEmail
) {
346 $this->mUser
->setEmail( $email );
350 # Update user count and newuser logs
351 $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
352 $ssUpdate->doUpdate();
354 $this->mUser
->addNewUserLogEntryAutoCreate();
355 elseif( $wgUser->isAnon() )
356 # Avoid spamming IP addresses all over the newuser log
357 $this->mUser
->addNewUserLogEntry( $this->mUser
, $byEmail );
359 $this->mUser
->addNewUserLogEntry( $wgUser, $byEmail );
362 wfRunHooks( 'AddNewAccount', array( $this->mUser
) );
368 * Entry point to create a new local account from user-supplied
369 * data loaded from the WebRequest. We handle initialising the
370 * email here because it's needed for some backend things; frontend
371 * interfaces calling this should handle recording things like
373 * @param $byEmail Bool whether to email the user their new password
374 * @return Status code; Login::SUCCESS == the user was successfully created
376 public function attemptCreation( $byEmail=false ) {
377 global $wgUser, $wgOut;
378 global $wgEnableSorbs, $wgProxyWhitelist;
379 global $wgMemc, $wgAccountCreationThrottle;
381 global $wgEmailAuthentication, $wgEmailConfirmToEdit;
384 return self
::READ_ONLY
;
386 # If the user passes an invalid domain, something is fishy
387 if( !$wgAuth->validDomain( $this->mDomain
) ) {
388 $this->mCreateResult
= 'wrongpassword';
389 return self
::CREATE_BADDOMAIN
;
392 # If we are not allowing users to login locally, we should be checking
393 # to see if the user is actually able to authenticate to the authenti-
394 # cation server before they create an account (otherwise, they can
395 # create a local account and login as any domain user). We only need
396 # to check this for domains that aren't local.
397 if( !in_array( $this->mDomain
, array( 'local', '' ) )
398 && !$wgAuth->canCreateAccounts()
399 && ( !$wgAuth->userExists( $this->mUsername
)
400 ||
!$wgAuth->authenticate( $this->mUsername
, $this->mPassword
)
403 $this->mCreateResult
= 'wrongpassword';
404 return self
::WRONG_PLUGIN_PASS
;
408 if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) &&
409 $wgUser->inSorbsBlacklist( $ip ) )
411 $this->mCreateResult
= 'sorbs_create_account_reason';
412 return self
::CREATE_SORBS
;
415 # Now create a dummy user ($user) and check if it is valid
416 $name = trim( $this->mName
);
417 $user = User
::newFromName( $name, 'creatable' );
418 if ( is_null( $user ) ) {
419 $this->mCreateResult
= 'noname';
420 return self
::CREATE_BADNAME
;
423 if ( $this->mUser
->idForName() != 0 ) {
424 $this->mCreateResult
= 'userexists';
425 return self
::CREATE_EXISTS
;
428 # Check that the password is acceptable, if we're actually
431 $valid = $this->mUser
->isValidPassword( $this->mPassword
);
432 if ( $valid !== true ) {
433 $this->mCreateResult
= $valid;
434 return self
::CREATE_BADPASS
;
438 # if you need a confirmed email address to edit, then obviously you
439 # need an email address. Equally if we're going to send the password to it.
440 if ( $wgEmailConfirmToEdit && empty( $this->mEmail
) ||
$byEmail ) {
441 $this->mCreateResult
= 'noemailcreate';
442 return self
::CREATE_NEEDEMAIL
;
445 if( !empty( $this->mEmail
) && !User
::isValidEmailAddr( $this->mEmail
) ) {
446 $this->mCreateResult
= 'invalidemailaddress';
447 return self
::CREATE_BADEMAIL
;
450 # Set some additional data so the AbortNewAccount hook can be used for
451 # more than just username validation
452 $this->mUser
->setEmail( $this->mEmail
);
453 $this->mUser
->setRealName( $this->mRealName
);
455 if( !wfRunHooks( 'AbortNewAccount', array( $this->mUser
, &$this->mCreateResult
) ) ) {
456 # Hook point to add extra creation throttles and blocks
457 wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" );
458 return self
::ABORTED
;
461 if ( $wgAccountCreationThrottle && $wgUser->isPingLimitable() ) {
462 $key = wfMemcKey( 'acctcreate', 'ip', $ip );
463 $value = $wgMemc->get( $key );
465 $wgMemc->set( $key, 0, 86400 );
467 if ( $value >= $wgAccountCreationThrottle ) {
468 return self
::THROTTLED
;
470 $wgMemc->incr( $key );
473 # Since we're creating a new local user, give the external
474 # database a chance to synchronise.
475 if( !$wgAuth->addUser( $this->mUser
, $this->mPassword
, $this->mEmail
, $this->mRealName
) ) {
476 $this->mCreateResult
= 'externaldberror';
477 return self
::ABORTED
;
480 $result = $this->initUser( false, $byEmail );
481 if( $result === null )
482 # It's unlikely we'd get here without some exception
483 # being thrown, but it's probably possible...
487 # Send out an email message if needed
489 $this->mailPassword( 'createaccount-title', 'createaccount-text' );
490 if( WikiError
::isError( $this->mMailResult
) ){
491 # FIXME: If the password email hasn't gone out,
492 # then the account is inaccessible :(
493 return self
::MAIL_ERROR
;
495 return self
::SUCCESS
;
498 if( $wgEmailAuthentication && User
::isValidEmailAddr( $this->mUser
->getEmail() ) )
500 $this->mMailResult
= $this->mUser
->sendConfirmationMail();
501 return WikiError
::isError( $this->mMailResult
)
510 * Email the user a new password, if appropriate to do so.
511 * @param $text String message key
512 * @param $title String message key
513 * @return Status code
515 public function mailPassword( $text='passwordremindertext', $title='passwordremindertitle' ) {
516 global $wgUser, $wgOut, $wgAuth, $wgServer, $wgScript, $wgNewPasswordExpiry;
519 return self
::READ_ONLY
;
521 # If we let the email go out, it will take users to a form where
522 # they are forced to change their password, so don't let us go
523 # there if we don't want passwords changed.
524 if( !$wgAuth->allowPasswordChange() )
525 return self
::MAIL_PASSCHANGE_FORBIDDEN
;
527 # Check against blocked IPs
528 # FIXME: -- should we not?
529 if( $wgUser->isBlocked() )
530 return self
::MAIL_BLOCKED
;
533 if( !wfRunHooks( 'UserLoginMailPassword', array( $this->mName
, &$this->mMailResult
) ) )
534 return self
::ABORTED
;
536 # Check against the rate limiter
537 if( $wgUser->pingLimiter( 'mailpassword' ) )
538 return self
::MAIL_PING_THROTTLED
;
540 # Check for a valid name
541 if ($this->mName
=== '' )
542 return self
::NO_NAME
;
543 $this->mUser
= User
::newFromName( $this->mName
);
544 if( is_null( $this->mUser
) )
545 return self
::NO_NAME
;
547 # And that the resulting user actually exists
548 if ( $this->mUser
->getId() === 0 )
549 return self
::NOT_EXISTS
;
551 # Check against password throttle
552 if ( $this->mUser
->isPasswordReminderThrottled() )
553 return self
::MAIL_PASS_THROTTLED
;
555 # User doesn't have email address set
556 if ( $this->mUser
->getEmail() === '' )
557 return self
::MAIL_EMPTY_EMAIL
;
559 # Don't send to people who are acting fishily by hiding their IP
562 return self
::MAIL_BAD_IP
;
564 # Let hooks do things with the data
565 wfRunHooks( 'User::mailPasswordInternal', array(&$wgUser, &$ip, &$this->mUser
) );
567 $newpass = $this->mUser
->randomPassword();
568 $this->mUser
->setNewpassword( $newpass, true );
569 $this->mUser
->saveSettings();
571 $message = wfMsgExt( $text, array( 'parsemag' ), $ip, $this->mUser
->getName(), $newpass,
572 $wgServer . $wgScript, round( $wgNewPasswordExpiry / 86400 ) );
573 $this->mMailResult
= $this->mUser
->sendMail( wfMsg( $title ), $message );
575 if( WikiError
::isError( $this->mMailResult
) ) {
576 return self
::MAIL_ERROR
;
578 return self
::SUCCESS
;
584 * For backwards compatibility, mainly with the state constants, which
585 * could be referred to in old extensions with the old class name.
588 class LoginForm
extends Login
{}