Replace spinner.gif with a new version (ajax-loader.gif resized to 20x20 with convert)
[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 CREATE_BLOCKED = 9;
18 const THROTTLED = 10;
19
20 const MAIL_READ_ONLY = 11;
21 const MAIL_PASSCHANGE_FORBIDDEN = 12;
22 const MAIL_BLOCKED = 13;
23 const MAIL_PING_THROTTLED = 14;
24 const MAIL_PASS_THROTTLED = 15;
25 const MAIL_EMPTY_EMAIL = 16;
26 const MAIL_BAD_IP = 17;
27 const MAIL_ERROR = 18;
28
29 var $mName, $mPassword, $mPosted;
30 var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage;
31
32 private $mExtUser = null;
33
34 public $mUser;
35 public $mMailResult;
36
37 /**
38 * Constructor
39 * @param WebRequest $request A WebRequest object passed by reference.
40 * uses $wgRequest if not given.
41 */
42 public function __construct( &$request=null ) {
43 global $wgRequest, $wgAuth, $wgHiddenPrefs, $wgEnableEmail, $wgRedirectOnLogin;
44 if( !$request ) $request = &$wgRequest;
45
46 $this->mName = $request->getText( 'wpName' );
47 $this->mPassword = $request->getText( 'wpPassword' );
48 $this->mDomain = $request->getText( 'wpDomain' );
49 $this->mPosted = $request->wasPosted();
50 $this->mRemember = $request->getCheck( 'wpRemember' );
51
52 if( $wgEnableEmail ) {
53 $this->mEmail = $request->getText( 'wpEmail' );
54 } else {
55 $this->mEmail = '';
56 }
57 if( !in_array( 'realname', $wgHiddenPrefs ) ) {
58 $this->mRealName = $request->getText( 'wpRealName' );
59 } else {
60 $this->mRealName = '';
61 }
62
63 if( !$wgAuth->validDomain( $this->mDomain ) ) {
64 $this->mDomain = 'invaliddomain';
65 }
66 $wgAuth->setDomain( $this->mDomain );
67
68 # Attempt to generate the User
69 $this->mUser = User::newFromName( $this->mName );
70 }
71
72 /**
73 * Actually add a user to the database.
74 * Give it a User object that has been initialised with a name.
75 *
76 * @param $u User object.
77 * @param $autocreate boolean -- true if this is an autocreation via auth plugin
78 * @return User object.
79 */
80 public function initUser( $autocreate ) {
81 global $wgAuth;
82
83 $this->mUser->addToDatabase();
84
85 if ( $wgAuth->allowPasswordChange() ) {
86 $this->mUser->setPassword( $this->mPassword );
87 }
88
89 $this->mUser->setEmail( $this->mEmail );
90 $this->mUser->setRealName( $this->mRealName );
91 $this->mUser->setToken();
92
93 $wgAuth->initUser( $this->mUser, $autocreate );
94
95 if( $this->mExtUser ) {
96 $this->mExtUser->link( $this->mUser->getId() );
97 $email = $this->mExtUser->getPref( 'emailaddress' );
98 if( $email && !$this->mEmail ) {
99 $this->mUser->setEmail( $email );
100 }
101 }
102
103 $this->mUser->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
104 $this->mUser->saveSettings();
105
106 # Update user count
107 $ssUpdate = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
108 $ssUpdate->doUpdate();
109
110 return $this->mUser;
111 }
112
113 public function attemptLogin(){
114 global $wgUser;
115 $code = $this->authenticateUserData();
116 if( !$code == self::SUCCESS ){
117 return $code;
118 }
119 if( (bool)$this->mRemember != (bool)$wgUser->getOption( 'rememberpassword' ) ) {
120 $wgUser->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 );
121 $wgUser->saveSettings();
122 } else {
123 $wgUser->invalidateCache();
124 }
125 $wgUser->setCookies();
126
127 # Reset the throttle
128 $key = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) );
129 global $wgMemc;
130 $wgMemc->delete( $key );
131
132 $injected_html = '';
133 wfRunHooks('UserLoginComplete', array(&$wgUser, &$injected_html));
134
135 return self::SUCCESS;
136 }
137
138 /**
139 * Internally authenticate the login request.
140 *
141 * This may create a local account as a side effect if the
142 * authentication plugin allows transparent local account
143 * creation.
144 */
145 public function authenticateUserData() {
146 global $wgUser, $wgAuth;
147 if ( '' == $this->mName ) {
148 return self::NO_NAME;
149 }
150
151 global $wgPasswordAttemptThrottle;
152
153 $throttleCount = 0;
154 if ( is_array( $wgPasswordAttemptThrottle ) ) {
155 $throttleKey = wfMemcKey( 'password-throttle', wfGetIP(), md5( $this->mName ) );
156 $count = $wgPasswordAttemptThrottle['count'];
157 $period = $wgPasswordAttemptThrottle['seconds'];
158
159 global $wgMemc;
160 $throttleCount = $wgMemc->get( $throttleKey );
161 if ( !$throttleCount ) {
162 $wgMemc->add( $throttleKey, 1, $period ); // start counter
163 } else if ( $throttleCount < $count ) {
164 $wgMemc->incr($throttleKey);
165 } else if ( $throttleCount >= $count ) {
166 return self::THROTTLED;
167 }
168 }
169
170 # Load $wgUser now, and check to see if we're logging in as the same
171 # name. This is necessary because loading $wgUser (say by calling
172 # getName()) calls the UserLoadFromSession hook, which potentially
173 # creates the user in the database. Until we load $wgUser, checking
174 # for user existence using User::newFromName($name)->getId() below
175 # will effectively be using stale data.
176 if ( $wgUser->getName() === $this->mName ) {
177 wfDebug( __METHOD__.": already logged in as {$this->mName}\n" );
178 return self::SUCCESS;
179 }
180
181 $this->mExtUser = ExternalUser::newFromName( $this->mName );
182
183 # TODO: Allow some magic here for invalid external names, e.g., let the
184 # user choose a different wiki name.
185 if( is_null( $this->mUser ) || !User::isUsableName( $this->mUser->getName() ) ) {
186 return self::ILLEGAL;
187 }
188
189 $isAutoCreated = false;
190 if ( 0 == $this->mUser->getID() ) {
191 $status = $this->attemptAutoCreate( $this->mUser );
192 if ( $status !== self::SUCCESS ) {
193 return $status;
194 } else {
195 $isAutoCreated = true;
196 }
197 } else {
198 $this->mUser->load();
199 }
200
201 # Give general extensions, such as a captcha, a chance to abort logins
202 $abort = self::ABORTED;
203 if( !wfRunHooks( 'AbortLogin', array( $this->mUser, $this->mPassword, &$abort ) ) ) {
204 return $abort;
205 }
206
207 if( !$this->mUser->checkPassword( $this->mPassword ) ) {
208 if( $this->mUser->checkTemporaryPassword( $this->mPassword ) ) {
209 # The e-mailed temporary password should not be used for actual
210 # logins; that's a very sloppy habit, and insecure if an
211 # attacker has a few seconds to click "search" on someone's
212 # open mail reader.
213 #
214 # Allow it to be used only to reset the password a single time
215 # to a new value, which won't be in the user's e-mail archives
216 #
217 # For backwards compatibility, we'll still recognize it at the
218 # login form to minimize surprises for people who have been
219 # logging in with a temporary password for some time.
220 #
221 # As a side-effect, we can authenticate the user's e-mail ad-
222 # dress if it's not already done, since the temporary password
223 # was sent via e-mail.
224 if( !$this->mUser->isEmailConfirmed() ) {
225 $this->mUser->confirmEmail();
226 $this->mUser->saveSettings();
227 }
228
229 # At this point we just return an appropriate code/ indicating
230 # that the UI should show a password reset form; bot interfaces
231 # etc will probably just fail cleanly here.
232 $retval = self::RESET_PASS;
233 } else {
234 $retval = '' == $this->mPassword ? self::EMPTY_PASS : self::WRONG_PASS;
235 }
236 } else {
237 $wgAuth->updateUser( $this->mUser );
238 $wgUser = $this->mUser;
239
240 # Reset throttle after a successful login
241 if( $throttleCount ) {
242 $wgMemc->delete( $throttleKey );
243 }
244
245 if( $isAutoCreated ) {
246 # Must be run after $wgUser is set, for correct new user log
247 wfRunHooks( 'AuthPluginAutoCreate', array( $wgUser ) );
248 }
249
250 $retval = self::SUCCESS;
251 }
252 wfRunHooks( 'LoginAuthenticateAudit', array( $this->mUser, $this->mPassword, $retval ) );
253 return $retval;
254 }
255
256 /**
257 * Attempt to automatically create a user on login. Only succeeds if there
258 * is an external authentication method which allows it.
259 * @return integer Status code
260 */
261 public function attemptAutoCreate( $user ) {
262 global $wgAuth, $wgUser, $wgAutocreatePolicy;
263
264 if( $wgUser->isBlockedFromCreateAccount() ) {
265 wfDebug( __METHOD__.": user is blocked from account creation\n" );
266 return self::CREATE_BLOCKED;
267 }
268
269 # If the external authentication plugin allows it, automatically cre-
270 # ate a new account for users that are externally defined but have not
271 # yet logged in.
272 if( $this->mExtUser ) {
273 # mExtUser is neither null nor false, so use the new ExternalAuth
274 # system.
275 if( $wgAutocreatePolicy == 'never' ) {
276 return self::NOT_EXISTS;
277 }
278 if( !$this->mExtUser->authenticate( $this->mPassword ) ) {
279 return self::WRONG_PLUGIN_PASS;
280 }
281 } else {
282 # Old AuthPlugin.
283 if( !$wgAuth->autoCreate() ) {
284 return self::NOT_EXISTS;
285 }
286 if( !$wgAuth->userExists( $user->getName() ) ) {
287 wfDebug( __METHOD__.": user does not exist\n" );
288 return self::NOT_EXISTS;
289 }
290 if( !$wgAuth->authenticate( $user->getName(), $this->mPassword ) ) {
291 wfDebug( __METHOD__.": \$wgAuth->authenticate() returned false, aborting\n" );
292 return self::WRONG_PLUGIN_PASS;
293 }
294 }
295
296 wfDebug( __METHOD__.": creating account\n" );
297 $this->initUser( true );
298 return self::SUCCESS;
299 }
300
301 /**
302 * Email the user a new password, if appropriate to do so.
303 * @param $text String message key
304 * @param $title String message key
305 * @return Status code
306 */
307 public function mailPassword( $text='passwordremindertext', $title='passwordremindertitle' ) {
308 global $wgUser, $wgOut, $wgAuth, $wgServer, $wgScript, $wgNewPasswordExpiry;
309
310 if( wfReadOnly() )
311 return self::MAIL_READ_ONLY;
312
313 if( !$wgAuth->allowPasswordChange() )
314 return self::MAIL_PASSCHANGE_FORBIDDEN;
315
316 # Check against blocked IPs
317 # FIXME: -- should we not?
318 if( $wgUser->isBlocked() )
319 return self::MAIL_BLOCKED;
320
321 # Check for hooks
322 $error = null;
323 if ( ! wfRunHooks( 'UserLoginMailPassword', array( $this->mName, &$error ) ) )
324 return $error;
325
326 # Check against the rate limiter
327 if( $wgUser->pingLimiter( 'mailpassword' ) )
328 return self::MAIL_PING_THROTTLED;
329
330 # Check for a valid name
331 if ( '' == $this->mName )
332 return self::NO_NAME;
333 $this->mUser = User::newFromName( $this->mName );
334 if( is_null( $this->mUser ) )
335 return self::NO_NAME;
336
337 # And that the resulting user actually exists
338 if ( 0 == $this->mUser->getId() )
339 return self::NOT_EXISTS;
340
341 # Check against password throttle
342 if ( $this->mUser->isPasswordReminderThrottled() )
343 return self::MAIL_PASS_THROTTLED;
344
345 # User doesn't have email address set
346 if ( '' == $this->mUser->getEmail() )
347 return self::MAIL_EMPTY_EMAIL;
348
349 # Don't send to people who are acting fishily by hiding their IP
350 $ip = wfGetIP();
351 if( !$ip )
352 return self::MAIL_BAD_IP;
353
354 # Let hooks do things with the data
355 wfRunHooks( 'User::mailPasswordInternal', array(&$wgUser, &$ip, &$this->mUser) );
356
357 $newpass = $this->mUser->randomPassword();
358 $this->mUser->setNewpassword( $newpass, true );
359 $this->mUser->saveSettings();
360
361 $message = wfMsgExt( $text, array( 'parsemag' ), $ip, $this->mUser->getName(), $newpass,
362 $wgServer . $wgScript, round( $wgNewPasswordExpiry / 86400 ) );
363 $this->mMailResult = $this->mUser->sendMail( wfMsg( $title ), $message );
364
365 if( WikiError::isError( $this->mMailResult ) ) {
366 return self::MAIL_ERROR;
367 } else {
368 return self::SUCCESS;
369 }
370 }
371 }
372
373 /**
374 * For backwards compatibility, mainly with the state constants, which
375 * could be referred to in old extensions with the old class name.
376 * @deprecated
377 */
378 class LoginForm extends Login {}