6d199d5a89ca3ec4844f3ff27ddb55e58a0dfce3
[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 return self::NO_NAME;
180 }
181
182 global $wgPasswordAttemptThrottle;
183 $throttleCount = 0;
184 if ( is_array( $wgPasswordAttemptThrottle ) ) {
185 $throttleKey = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) );
186 $count = $wgPasswordAttemptThrottle['count'];
187 $period = $wgPasswordAttemptThrottle['seconds'];
188
189 global $wgMemc;
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;
197 }
198 }
199
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;
209 }
210
211 $this->mExtUser = ExternalUser::newFromName( $this->mName );
212
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;
217 }
218
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 );
226 } else {
227 return $this->canAutoCreate();
228 }
229 } else {
230 $isAutoCreated = false;
231 $this->mUser->load();
232 }
233
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 ) ) ) {
237 return $abort;
238 }
239
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
245 # open mail reader.
246 #
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
249 #
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.
253 #
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();
260 }
261
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;
266 } else {
267 $retval = ( $this->mPassword === '' ) ? self::EMPTY_PASS : self::WRONG_PASS;
268 }
269 } else {
270 $wgAuth->updateUser( $this->mUser );
271 $wgUser = $this->mUser;
272
273 # Reset throttle after a successful login
274 if( $throttleCount ) {
275 $wgMemc->delete( $throttleKey );
276 }
277
278 if( $isAutoCreated ) {
279 # Must be run after $wgUser is set, for correct new user log
280 wfRunHooks( 'AuthPluginAutoCreate', array( $wgUser ) );
281 }
282
283 $retval = self::SUCCESS;
284 }
285 wfRunHooks( 'LoginAuthenticateAudit', array( $this->mUser, $this->mPassword, $retval ) );
286 return $retval;
287 }
288
289 /**
290 * Actually add a user to the database.
291 * Give it a User object that has been initialised with a name.
292 *
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
298 * Db errors etc).
299 */
300 protected function initUser( $autocreate=false, $byEmail=false ) {
301 global $wgAuth, $wgUser;
302
303 $fields = array(
304 'name' => User::getCanonicalName( $this->mName ),
305 'password' => $byEmail ? null : User::crypt( $this->mPassword ),
306 'email' => $this->mEmail,
307 'options' => array(
308 'rememberpassword' => $this->mRemember ? 1 : 0,
309 ),
310 );
311
312 $this->mUser = User::createNew( $this->mName, $fields );
313
314 if( $this->mUser === null ){
315 return null;
316 }
317
318 # Let old AuthPlugins play with the user
319 $wgAuth->initUser( $this->mUser, $autocreate );
320
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 );
327 }
328 }
329
330 # Update user count and newuser logs
331 $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
332 $ssUpdate->doUpdate();
333 if( $autocreate )
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 );
338 else
339 $this->mUser->addNewUserLogEntry( $wgUser, $byEmail );
340
341 # Run hooks
342 wfRunHooks( 'AddNewAccount', array( $this->mUser ) );
343
344 return true;
345 }
346
347 /**
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
352 * preference options
353 * @param $byEmail Bool whether to email the user their new password
354 * @return Status code; Login::SUCCESS == the user was successfully created
355 */
356 public function attemptCreation( $byEmail=false ) {
357 global $wgUser, $wgOut;
358 global $wgEnableSorbs, $wgProxyWhitelist;
359 global $wgMemc, $wgAccountCreationThrottle;
360 global $wgAuth;
361 global $wgEmailAuthentication, $wgEmailConfirmToEdit;
362
363 if( wfReadOnly() )
364 return self::READ_ONLY;
365
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;
370 }
371
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 )
381 ) )
382 {
383 $this->mCreateResult = 'wrongpassword';
384 return self::WRONG_PLUGIN_PASS;
385 }
386
387 $ip = wfGetIP();
388 if ( $wgEnableSorbs && !in_array( $ip, $wgProxyWhitelist ) &&
389 $wgUser->inSorbsBlacklist( $ip ) )
390 {
391 $this->mCreateResult = 'sorbs_create_account_reason';
392 return self::CREATE_SORBS;
393 }
394
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;
401 }
402
403 if ( $this->mUser->idForName() != 0 ) {
404 $this->mCreateResult = 'userexists';
405 return self::CREATE_EXISTS;
406 }
407
408 # Check that the password is acceptable, if we're actually
409 # going to use it
410 if( !$byEmail ){
411 $valid = $this->mUser->isValidPassword( $this->mPassword );
412 if ( $valid !== true ) {
413 $this->mCreateResult = $valid;
414 return self::CREATE_BADPASS;
415 }
416 }
417
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;
423 }
424
425 if( !empty( $this->mEmail ) && !User::isValidEmailAddr( $this->mEmail ) ) {
426 $this->mCreateResult = 'invalidemailaddress';
427 return self::CREATE_BADEMAIL;
428 }
429
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 );
434
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;
439 }
440
441 if ( $wgAccountCreationThrottle && $wgUser->isPingLimitable() ) {
442 $key = wfMemcKey( 'acctcreate', 'ip', $ip );
443 $value = $wgMemc->get( $key );
444 if ( !$value ) {
445 $wgMemc->set( $key, 0, 86400 );
446 }
447 if ( $value >= $wgAccountCreationThrottle ) {
448 return self::THROTTLED;
449 }
450 $wgMemc->incr( $key );
451 }
452
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;
458 }
459
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...
464 return self::FAILED;
465
466
467 # Send out an email message if needed
468 if( $byEmail ){
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;
474 } else {
475 return self::SUCCESS;
476 }
477 } else {
478 if( $wgEmailAuthentication && User::isValidEmailAddr( $this->mUser->getEmail() ) )
479 {
480 $this->mMailResult = $this->mUser->sendConfirmationMail();
481 return WikiError::isError( $this->mMailResult )
482 ? self::MAIL_ERROR
483 : self::SUCCESS;
484 }
485 }
486 return true;
487 }
488
489 /**
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
494 */
495 public function mailPassword( $text='passwordremindertext', $title='passwordremindertitle' ) {
496 global $wgUser, $wgOut, $wgAuth, $wgServer, $wgScript, $wgNewPasswordExpiry;
497
498 if( wfReadOnly() )
499 return self::READ_ONLY;
500
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;
506
507 # Check against blocked IPs
508 # FIXME: -- should we not?
509 if( $wgUser->isBlocked() )
510 return self::MAIL_BLOCKED;
511
512 # Check for hooks
513 if( !wfRunHooks( 'UserLoginMailPassword', array( $this->mName, &$this->mMailResult ) ) )
514 return self::ABORTED;
515
516 # Check against the rate limiter
517 if( $wgUser->pingLimiter( 'mailpassword' ) )
518 return self::MAIL_PING_THROTTLED;
519
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;
526
527 # And that the resulting user actually exists
528 if ( $this->mUser->getId() === 0 )
529 return self::NOT_EXISTS;
530
531 # Check against password throttle
532 if ( $this->mUser->isPasswordReminderThrottled() )
533 return self::MAIL_PASS_THROTTLED;
534
535 # User doesn't have email address set
536 if ( $this->mUser->getEmail() === '' )
537 return self::MAIL_EMPTY_EMAIL;
538
539 # Don't send to people who are acting fishily by hiding their IP
540 $ip = wfGetIP();
541 if( !$ip )
542 return self::MAIL_BAD_IP;
543
544 # Let hooks do things with the data
545 wfRunHooks( 'User::mailPasswordInternal', array(&$wgUser, &$ip, &$this->mUser) );
546
547 $newpass = $this->mUser->randomPassword();
548 $this->mUser->setNewpassword( $newpass, true );
549 $this->mUser->saveSettings();
550
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 );
554
555 if( WikiError::isError( $this->mMailResult ) ) {
556 return self::MAIL_ERROR;
557 } else {
558 return self::SUCCESS;
559 }
560 }
561 }
562
563 /**
564 * For backwards compatibility, mainly with the state constants, which
565 * could be referred to in old extensions with the old class name.
566 * @deprecated
567 */
568 class LoginForm extends Login {}