Follow-up to r56684 - set message keys in Login.php, and readd the ExternalUser check...
[lhc/web/wiklou.git] / includes / Login.php
1 <?php
2
3 /**
4 * Encapsulates the backend activities of logging a user into the wiki.
5 */
6 class Login {
7
8 const SUCCESS = 0;
9 const NO_NAME = 1;
10 const ILLEGAL = 2;
11 const WRONG_PLUGIN_PASS = 3;
12 const NOT_EXISTS = 4;
13 const WRONG_PASS = 5;
14 const EMPTY_PASS = 6;
15 const RESET_PASS = 7;
16 const ABORTED = 8;
17 const THROTTLED = 10;
18 const FAILED = 11;
19 const READ_ONLY = 12;
20
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;
28
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;
37
38 protected $mName;
39 protected $mPassword;
40 public $mRemember; # 0 or 1
41 public $mEmail;
42 public $mDomain;
43 public $mRealname;
44
45 private $mExtUser = null;
46
47 public $mUser;
48
49 public $mLoginResult = '';
50 public $mMailResult = '';
51 public $mCreateResult = '';
52
53 /**
54 * Constructor
55 * @param WebRequest $request A WebRequest object passed by reference.
56 * uses $wgRequest if not given.
57 */
58 public function __construct( &$request=null ) {
59 global $wgRequest, $wgAuth, $wgHiddenPrefs, $wgEnableEmail, $wgRedirectOnLogin;
60 if( !$request ) $request = &$wgRequest;
61
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;
66
67 if( $wgEnableEmail ) {
68 $this->mEmail = $request->getText( 'wpEmail' );
69 } else {
70 $this->mEmail = '';
71 }
72 if( !in_array( 'realname', $wgHiddenPrefs ) ) {
73 $this->mRealName = $request->getText( 'wpRealName' );
74 } else {
75 $this->mRealName = '';
76 }
77
78 if( !$wgAuth->validDomain( $this->mDomain ) ) {
79 $this->mDomain = 'invaliddomain';
80 }
81 $wgAuth->setDomain( $this->mDomain );
82
83 # Load the user, if they exist in the local database.
84 $this->mUser = User::newFromName( trim( $this->mName ), 'usable' );
85 }
86
87 /**
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.
95 */
96 public function attemptLogin(){
97 global $wgUser;
98
99 $code = $this->authenticateUserData();
100 if( $code != self::SUCCESS ){
101 return $code;
102 }
103
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();
108 } else {
109 $wgUser->invalidateCache();
110 }
111 $wgUser->setCookies();
112
113 # Reset the password throttle
114 $key = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) );
115 global $wgMemc;
116 $wgMemc->delete( $key );
117
118 wfRunHooks( 'UserLoginComplete', array( &$wgUser, &$this->mLoginResult ) );
119
120 return self::SUCCESS;
121 }
122
123 /**
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.
129 */
130 protected function canAutoCreate() {
131 global $wgAuth, $wgUser, $wgAutocreatePolicy;
132
133 if( $wgUser->isBlockedFromCreateAccount() ) {
134 wfDebug( __METHOD__.": user is blocked from account creation\n" );
135 return self::CREATE_BLOCKED;
136 }
137
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;
146 }
147 if( !$this->mExtUser->authenticate( $this->mPassword ) ) {
148 return self::WRONG_PLUGIN_PASS;
149 }
150 } else {
151 # Old AuthPlugin.
152 if( !$wgAuth->autoCreate() ) {
153 return self::NOT_EXISTS;
154 }
155 if( !$wgAuth->userExists( $this->mUser->getName() ) ) {
156 wfDebug( __METHOD__.": user does not exist\n" );
157 return self::NOT_EXISTS;
158 }
159 if( !$wgAuth->authenticate( $this->mUser->getName(), $this->mPassword ) ) {
160 wfDebug( __METHOD__.": \$wgAuth->authenticate() returned false, aborting\n" );
161 return self::WRONG_PLUGIN_PASS;
162 }
163 }
164
165 return self::SUCCESS;
166 }
167
168 /**
169 * Internally authenticate the login request.
170 *
171 * This may create a local account as a side effect if the
172 * authentication plugin allows transparent local account
173 * creation.
174 */
175 protected function authenticateUserData() {
176 global $wgUser, $wgAuth;
177
178 if ( '' == $this->mName ) {
179 $this->mLoginResult = 'noname';
180 return self::NO_NAME;
181 }
182
183 global $wgPasswordAttemptThrottle;
184 $throttleCount = 0;
185 if ( is_array( $wgPasswordAttemptThrottle ) ) {
186 $throttleKey = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) );
187 $count = $wgPasswordAttemptThrottle['count'];
188 $period = $wgPasswordAttemptThrottle['seconds'];
189
190 global $wgMemc;
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;
199 }
200 }
201
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;
211 }
212
213 $this->mExtUser = ExternalUser::newFromName( $this->mName );
214
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;
222 }
223 }
224
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;
229 }
230
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 ){
239 return $result;
240 };
241 } else {
242 return $this->canAutoCreate();
243 }
244 } else {
245 $isAutoCreated = false;
246 $this->mUser->load();
247 }
248
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;
252 }
253
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
259 # open mail reader.
260 #
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
263 #
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.
267 #
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();
274 }
275
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;
280 } else {
281 if( $this->mPassword === '' ){
282 $retval = self::EMPTY_PASS;
283 $this->mLoginResult = 'wrongpasswordempty';
284 } else {
285 $retval = self::WRONG_PASS;
286 $this->mLoginResult = 'wrongpassword';
287 }
288 }
289 } else {
290 $wgAuth->updateUser( $this->mUser );
291 $wgUser = $this->mUser;
292
293 # Reset throttle after a successful login
294 if( $throttleCount ) {
295 $wgMemc->delete( $throttleKey );
296 }
297
298 if( $isAutoCreated ) {
299 # Must be run after $wgUser is set, for correct new user log
300 wfRunHooks( 'AuthPluginAutoCreate', array( $wgUser ) );
301 }
302
303 $retval = self::SUCCESS;
304 }
305 wfRunHooks( 'LoginAuthenticateAudit', array( $this->mUser, $this->mPassword, $retval ) );
306 return $retval;
307 }
308
309 /**
310 * Actually add a user to the database.
311 * Give it a User object that has been initialised with a name.
312 *
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
318 * Db errors etc).
319 */
320 protected function initUser( $autocreate=false, $byEmail=false ) {
321 global $wgAuth, $wgUser;
322
323 $fields = array(
324 'name' => User::getCanonicalName( $this->mName ),
325 'password' => $byEmail ? null : User::crypt( $this->mPassword ),
326 'email' => $this->mEmail,
327 'options' => array(
328 'rememberpassword' => $this->mRemember ? 1 : 0,
329 ),
330 );
331
332 $this->mUser = User::createNew( $this->mName, $fields );
333
334 if( $this->mUser === null ){
335 return null;
336 }
337
338 # Let old AuthPlugins play with the user
339 $wgAuth->initUser( $this->mUser, $autocreate );
340
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 );
347 }
348 }
349
350 # Update user count and newuser logs
351 $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
352 $ssUpdate->doUpdate();
353 if( $autocreate )
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 );
358 else
359 $this->mUser->addNewUserLogEntry( $wgUser, $byEmail );
360
361 # Run hooks
362 wfRunHooks( 'AddNewAccount', array( $this->mUser ) );
363
364 return true;
365 }
366
367 /**
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
372 * preference options
373 * @param $byEmail Bool whether to email the user their new password
374 * @return Status code; Login::SUCCESS == the user was successfully created
375 */
376 public function attemptCreation( $byEmail=false ) {
377 global $wgUser, $wgOut;
378 global $wgEnableSorbs, $wgProxyWhitelist;
379 global $wgMemc, $wgAccountCreationThrottle;
380 global $wgAuth;
381 global $wgEmailAuthentication, $wgEmailConfirmToEdit;
382
383 if( wfReadOnly() )
384 return self::READ_ONLY;
385
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;
390 }
391
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 )
401 ) )
402 {
403 $this->mCreateResult = 'wrongpassword';
404 return self::WRONG_PLUGIN_PASS;
405 }
406
407 $ip = wfGetIP();
408 if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) &&
409 $wgUser->inSorbsBlacklist( $ip ) )
410 {
411 $this->mCreateResult = 'sorbs_create_account_reason';
412 return self::CREATE_SORBS;
413 }
414
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;
421 }
422
423 if ( $this->mUser->idForName() != 0 ) {
424 $this->mCreateResult = 'userexists';
425 return self::CREATE_EXISTS;
426 }
427
428 # Check that the password is acceptable, if we're actually
429 # going to use it
430 if( !$byEmail ){
431 $valid = $this->mUser->isValidPassword( $this->mPassword );
432 if ( $valid !== true ) {
433 $this->mCreateResult = $valid;
434 return self::CREATE_BADPASS;
435 }
436 }
437
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;
443 }
444
445 if( !empty( $this->mEmail ) && !User::isValidEmailAddr( $this->mEmail ) ) {
446 $this->mCreateResult = 'invalidemailaddress';
447 return self::CREATE_BADEMAIL;
448 }
449
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 );
454
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;
459 }
460
461 if ( $wgAccountCreationThrottle && $wgUser->isPingLimitable() ) {
462 $key = wfMemcKey( 'acctcreate', 'ip', $ip );
463 $value = $wgMemc->get( $key );
464 if ( !$value ) {
465 $wgMemc->set( $key, 0, 86400 );
466 }
467 if ( $value >= $wgAccountCreationThrottle ) {
468 return self::THROTTLED;
469 }
470 $wgMemc->incr( $key );
471 }
472
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;
478 }
479
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...
484 return self::FAILED;
485
486
487 # Send out an email message if needed
488 if( $byEmail ){
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;
494 } else {
495 return self::SUCCESS;
496 }
497 } else {
498 if( $wgEmailAuthentication && User::isValidEmailAddr( $this->mUser->getEmail() ) )
499 {
500 $this->mMailResult = $this->mUser->sendConfirmationMail();
501 return WikiError::isError( $this->mMailResult )
502 ? self::MAIL_ERROR
503 : self::SUCCESS;
504 }
505 }
506 return true;
507 }
508
509 /**
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
514 */
515 public function mailPassword( $text='passwordremindertext', $title='passwordremindertitle' ) {
516 global $wgUser, $wgOut, $wgAuth, $wgServer, $wgScript, $wgNewPasswordExpiry;
517
518 if( wfReadOnly() )
519 return self::READ_ONLY;
520
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;
526
527 # Check against blocked IPs
528 # FIXME: -- should we not?
529 if( $wgUser->isBlocked() )
530 return self::MAIL_BLOCKED;
531
532 # Check for hooks
533 if( !wfRunHooks( 'UserLoginMailPassword', array( $this->mName, &$this->mMailResult ) ) )
534 return self::ABORTED;
535
536 # Check against the rate limiter
537 if( $wgUser->pingLimiter( 'mailpassword' ) )
538 return self::MAIL_PING_THROTTLED;
539
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;
546
547 # And that the resulting user actually exists
548 if ( $this->mUser->getId() === 0 )
549 return self::NOT_EXISTS;
550
551 # Check against password throttle
552 if ( $this->mUser->isPasswordReminderThrottled() )
553 return self::MAIL_PASS_THROTTLED;
554
555 # User doesn't have email address set
556 if ( $this->mUser->getEmail() === '' )
557 return self::MAIL_EMPTY_EMAIL;
558
559 # Don't send to people who are acting fishily by hiding their IP
560 $ip = wfGetIP();
561 if( !$ip )
562 return self::MAIL_BAD_IP;
563
564 # Let hooks do things with the data
565 wfRunHooks( 'User::mailPasswordInternal', array(&$wgUser, &$ip, &$this->mUser) );
566
567 $newpass = $this->mUser->randomPassword();
568 $this->mUser->setNewpassword( $newpass, true );
569 $this->mUser->saveSettings();
570
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 );
574
575 if( WikiError::isError( $this->mMailResult ) ) {
576 return self::MAIL_ERROR;
577 } else {
578 return self::SUCCESS;
579 }
580 }
581 }
582
583 /**
584 * For backwards compatibility, mainly with the state constants, which
585 * could be referred to in old extensions with the old class name.
586 * @deprecated
587 */
588 class LoginForm extends Login {}