Follow-ups to r56684: add a member function for extensions to add header text cleanly...
[lhc/web/wiklou.git] / includes / specials / SpecialUserlogin.php
1 <?php
2 /**
3 * SpecialPage for logging users into the wiki
4 * @ingroup SpecialPage
5 */
6
7 class SpecialUserLogin extends SpecialPage {
8
9 var $mUsername, $mPassword, $mReturnTo, $mCookieCheck, $mPosted;
10 var $mCreateaccount, $mCreateaccountMail, $mMailmypassword;
11 var $mRemember, $mDomain, $mLanguage;
12 var $mSkipCookieCheck, $mReturnToQuery;
13
14 public $mDomains = array();
15
16 public $mFormHeader = ''; # Can be filled by hooks etc
17 public $mFormFields = array(
18 'Name' => array(
19 'type' => 'text',
20 'label-message' => 'yourname',
21 'id' => 'wpName1',
22 'tabindex' => '1',
23 'size' => '20',
24 'required' => '1',
25 ),
26 'Password' => array(
27 'type' => 'password',
28 'label-message' => 'yourpassword',
29 'size' => '20',
30 'id' => 'wpPassword1',
31 ),
32 'Domain' => array(
33 'type' => 'select',
34 'id' => 'wpDomain',
35 'label-message' => 'yourdomainname',
36 'options' => null,
37 'default' => null,
38 ),
39 'Remember' => array(
40 'type' => 'check',
41 'label-message' => 'remembermypassword',
42 'id' => 'wpRemember',
43 )
44 );
45
46 protected $mLogin; # Login object
47
48 public function __construct(){
49 parent::__construct( 'Userlogin' );
50 }
51
52 function execute( $par ) {
53 global $wgRequest;
54
55 # Redirect out for account creation, for B/C
56 $type = ( $par == 'signup' ) ? $par : $wgRequest->getText( 'type' );
57 if( $type == 'signup' ){
58 $sp = new SpecialCreateAccount();
59 $sp->execute( $par );
60 return;
61 }
62
63 # Because we're transitioning from logged-out, who might not
64 # have a session, to logged-in, who always do, we need to make
65 # sure that we *always* have a session...
66 if( session_id() == '' ) {
67 wfSetupSession();
68 }
69
70 $this->loadQuery();
71 $this->mLogin = new Login();
72
73 if ( $wgRequest->getCheck( 'wpCookieCheck' ) ) {
74 $this->onCookieRedirectCheck();
75 return;
76 } else if( $wgRequest->wasPosted() ) {
77 if ( $this->mMailmypassword ) {
78 return $this->showMailPage();
79 } else {
80 return $this->processLogin();
81 }
82 } else {
83 $this->mainLoginForm( '' );
84 }
85 }
86
87 /**
88 * Load member variables from the HTTP request data
89 * @param $par String the fragment passed to execute()
90 */
91 protected function loadQuery(){
92 global $wgRequest, $wgAuth, $wgHiddenPrefs, $wgEnableEmail, $wgRedirectOnLogin;
93
94 $this->mUsername = $wgRequest->getText( 'wpName' );
95 $this->mPassword = $wgRequest->getText( 'wpPassword' );
96 $this->mDomain = $wgRequest->getText( 'wpDomain' );
97 $this->mLanguage = $wgRequest->getText( 'uselang' );
98
99 $this->mReturnTo = $wgRequest->getVal( 'returnto' );
100 $this->mReturnToQuery = $wgRequest->getVal( 'returntoquery' );
101
102 $this->mMailmypassword = $wgRequest->getCheck( 'wpMailmypassword' )
103 && $wgEnableEmail;
104 $this->mRemember = $wgRequest->getCheck( 'wpRemember' );
105 $this->mSkipCookieCheck = $wgRequest->getCheck( 'wpSkipCookieCheck' );
106
107 if( !$wgAuth->validDomain( $this->mDomain ) ) {
108 $this->mDomain = 'invaliddomain';
109 }
110 $wgAuth->setDomain( $this->mDomain );
111
112 if ( $wgRedirectOnLogin ) {
113 $this->mReturnTo = $wgRedirectOnLogin;
114 $this->mReturnToQuery = '';
115 }
116 # When switching accounts, it sucks to get automatically logged out
117 $returnToTitle = Title::newFromText( $this->mReturnTo );
118 if( is_object( $returnToTitle ) && $returnToTitle->isSpecial( 'Userlogout' ) ) {
119 $this->mReturnTo = '';
120 $this->mReturnToQuery = '';
121 }
122 }
123
124 /**
125 * Show the main login form
126 * @param $msg String a message key for a warning/error message
127 * that may have been generated on a previous iteration
128 */
129 protected function mainLoginForm( $msg, $msgtype = 'error' ) {
130 global $wgUser, $wgOut, $wgEnableEmail;
131 global $wgCookiePrefix, $wgLoginLanguageSelector;
132 global $wgAuth, $wgCookieExpiration;
133
134 # Preload the name field with something if we can
135 if ( '' == $this->mUsername ) {
136 if ( $wgUser->isLoggedIn() ) {
137 $this->mUsername = $wgUser->getName();
138 } elseif( isset( $_COOKIE[$wgCookiePrefix.'UserName'] ) ) {
139 $this->mUsername = $_COOKIE[$wgCookiePrefix.'UserName'];
140 }
141 }
142 if( $this->mUsername ){
143 $this->mFormFields['Name']['default'] = $this->mUsername;
144 $this->mFormFields['Password']['autofocus'] = '1';
145 } else {
146 $this->mFormFields['Name']['autofocus'] = '1';
147 }
148
149 # Parse the error message if we got one
150 if( $msg ){
151 if( $msgtype == 'error' ){
152 $msg = wfMsgExt( 'loginerror', 'parseinline' ) . ' ' . $msg;
153 }
154 $msg = Html::rawElement(
155 'div',
156 array( 'class' => $msgtype . 'box' ),
157 $msg
158 );
159 } else {
160 $msg = '';
161 }
162
163 # Make sure the returnTo strings don't get lost if the
164 # user changes language, etc
165 $linkq = array();
166 if ( !empty( $this->mReturnTo ) ) {
167 $linkq['returnto'] = wfUrlencode( $this->mReturnTo );
168 if ( !empty( $this->mReturnToQuery ) )
169 $linkq['returntoquery'] = wfUrlencode( $this->mReturnToQuery );
170 }
171
172 # Pass any language selection on to the mode switch link
173 if( $wgLoginLanguageSelector && $this->mLanguage )
174 $linkq['uselang'] = $this->mLanguage;
175
176 $skin = $wgUser->getSkin();
177 $link = $skin->link(
178 SpecialPage::getTitleFor( 'CreateAccount' ),
179 wfMsgHtml( 'nologinlink' ),
180 array(),
181 $linkq );
182
183 # Don't show a "create account" link if the user can't
184 $link = $wgUser->isAllowed( 'createaccount' ) && !$wgUser->isLoggedIn()
185 ? wfMsgWikiHtml( 'nologin', $link )
186 : '';
187
188 # Prepare language selection links as needed
189 $langSelector = $wgLoginLanguageSelector
190 ? Html::rawElement(
191 'div',
192 array( 'id' => 'languagelinks' ),
193 self::makeLanguageSelector( $this->getTitle(), $this->mReturnTo ) )
194 : '';
195
196 # Add a 'mail reset' button if available
197 $buttons = '';
198 if( $wgEnableEmail && $wgAuth->allowPasswordChange() ){
199 $buttons = Html::element(
200 'input',
201 array(
202 'type' => 'submit',
203 'name' => 'wpMailmypassword',
204 'value' => wfMsg( 'mailmypassword' ),
205 'id' => 'wpMailmypassword',
206 )
207 );
208 }
209
210 # Give authentication and captcha plugins a chance to
211 # modify the form, by hook or by using $wgAuth
212 $wgAuth->modifyUITemplate( $this, 'login' );
213 wfRunHooks( 'UserLoginForm', array( &$this ) );
214
215 # The most likely use of the hook is to enable domains;
216 # check that now, and add fields if necessary
217 if( $this->mDomains ){
218 $this->mFormFields['Domain']['options'] = $this->mDomains;
219 $this->mFormFields['Domain']['default'] = $this->mDomain;
220 } else {
221 unset( $this->mFormFields['Domain'] );
222 }
223
224 # Or to tweak the 'remember my password' checkbox
225 if( !($wgCookieExpiration > 0) ){
226 # Remove it altogether
227 unset( $this->mFormFields['Remember'] );
228 } elseif( $wgUser->getOption( 'rememberpassword' ) || $this->mRemember ){
229 # Or check it by default
230 # FIXME: this doesn't always work?
231 $this->mFormFields['Remember']['checked'] = '1';
232 }
233
234 $form = new HTMLForm( $this->mFormFields, '' );
235 $form->setTitle( $this->getTitle() );
236 $form->setSubmitText( wfMsg( 'login' ) );
237 $form->setSubmitId( 'wpLoginAttempt' );
238 $form->suppressReset();
239 $form->loadData();
240
241 $formContents = ''
242 . Html::rawElement( 'p', array( 'id' => 'userloginlink' ),
243 $link )
244 . Html::rawElement( 'div', array( 'id' => 'userloginprompt' ),
245 wfMsgExt( 'loginprompt', array( 'parseinline' ) ) )
246 . $this->mFormHeader
247 . $langSelector
248 . $form->getBody()
249 . $form->getButtons()
250 . $buttons
251 . Xml::hidden( 'returnto', $this->mReturnTo )
252 . Xml::hidden( 'returntoquery', $this->mReturnToQuery )
253 ;
254
255 $wgOut->setPageTitle( wfMsg( 'login' ) );
256 $wgOut->setRobotPolicy( 'noindex,nofollow' );
257 $wgOut->setArticleRelated( false );
258 $wgOut->disallowUserJs(); # Stop malicious userscripts sniffing passwords
259
260 $wgOut->addHTML(
261 Html::rawElement(
262 'div',
263 array( 'id' => 'loginstart' ),
264 wfMsgExt( 'loginstart', array( 'parseinline' ) )
265 ) .
266 $msg .
267 Html::rawElement(
268 'div',
269 array( 'id' => 'userloginForm' ),
270 $form->wrapForm( $formContents )
271 ) .
272 Html::rawElement(
273 'div',
274 array( 'id' => 'loginend' ),
275 wfMsgExt( 'loginend', array( 'parseinline' ) )
276 )
277 );
278 }
279
280 /**
281 * Check if a session cookie is present.
282 *
283 * This will not pick up a cookie set during _this_ request, but is meant
284 * to ensure that the client is returning the cookie which was set on a
285 * previous pass through the system.
286 *
287 * @private
288 */
289 protected function hasSessionCookie() {
290 global $wgDisableCookieCheck, $wgRequest;
291 return $wgDisableCookieCheck || $wgRequest->checkSessionCookie();
292 }
293
294 /**
295 * Do a redirect back to the same page, so we can check any
296 * new session cookies.
297 */
298 protected function cookieRedirectCheck() {
299 global $wgOut;
300
301 $query = array( 'wpCookieCheck' => '1');
302 if ( $this->mReturnTo ) $query['returnto'] = $this->mReturnTo;
303 $check = $this->getTitle()->getFullURL( $query );
304
305 return $wgOut->redirect( $check );
306 }
307
308 /**
309 * Check the cookies and show errors if they're not enabled.
310 * @param $type String action being performed
311 */
312 protected function onCookieRedirectCheck() {
313 if ( $this->hasSessionCookie() ) {
314 return self::successfulLogin(
315 'loginsuccess',
316 $this->mReturnTo,
317 $this->mReturnToQuery
318 );
319 } else {
320 return $this->mainLoginForm( wfMsgExt( 'nocookieslogin', array( 'parseinline' ) ) );
321 }
322 }
323
324 /**
325 * Produce a bar of links which allow the user to select another language
326 * during login/registration but retain "returnto"
327 * @param $title Title to use in the link
328 * @param $returnTo query string to append
329 * @return String HTML for bar
330 */
331 public static function makeLanguageSelector( $title, $returnTo=false ) {
332 global $wgLang;
333
334 $msg = wfMsgForContent( 'loginlanguagelinks' );
335 if( $msg != '' && !wfEmptyMsg( 'loginlanguagelinks', $msg ) ) {
336 $langs = explode( "\n", $msg );
337 $links = array();
338 foreach( $langs as $lang ) {
339 $lang = trim( $lang, '* ' );
340 $parts = explode( '|', $lang );
341 if (count($parts) >= 2) {
342 $links[] = SpecialUserLogin::makeLanguageSelectorLink(
343 $parts[0], $parts[1], $title, $returnTo );
344 }
345 }
346 return count( $links ) > 0 ? wfMsgHtml( 'loginlanguagelabel', $wgLang->pipeList( $links ) ) : '';
347 } else {
348 return '';
349 }
350 }
351
352 /**
353 * Create a language selector link for a particular language
354 * Links back to this page preserving type and returnto
355 * @param $text Link text
356 * @param $lang Language code
357 * @param $title Title to link to
358 * @param $returnTo String returnto query
359 */
360 public static function makeLanguageSelectorLink( $text, $lang, $title, $returnTo=false ) {
361 global $wgUser;
362 $attr = array( 'uselang' => $lang );
363 if( $returnTo )
364 $attr['returnto'] = $returnTo;
365 $skin = $wgUser->getSkin();
366 return $skin->linkKnown(
367 $title,
368 htmlspecialchars( $text ),
369 array(),
370 $attr
371 );
372 }
373
374 /**
375 * Display a "login successful" page.
376 * @param $message String message key of main message to display
377 * @param $html String HTML to optionally add
378 * @param $returnto Title to returnto
379 * @param $returntoQuery String query string for returnto link
380 */
381 public static function displaySuccessfulLogin( $message, $html='', $returnTo=false, $returnToQuery=false ) {
382 global $wgOut, $wgUser;
383
384 $wgOut->setPageTitle( wfMsg( 'loginsuccesstitle' ) );
385 $wgOut->setRobotPolicy( 'noindex,nofollow' );
386 $wgOut->setArticleRelated( false );
387 $wgOut->addWikiMsg( $message, $wgUser->getName() );
388 $wgOut->addHTML( $html );
389
390 if ( $returnTo ) {
391 $wgOut->returnToMain( null, $returnTo, $returnToQuery );
392 } else {
393 $wgOut->returnToMain( null );
394 }
395 }
396
397 /**
398 * Display any messages generated by hooks, or HTTP redirect to
399 * $this->mReturnTo (or Main Page if that's undefined). Formerly we had a
400 * nice message here, but that's not as useful as just being sent to
401 * wherever you logged in from. It should be clear that the action was
402 * successful, given the lack of error messages plus the appearance of your
403 * name in the upper right.
404 *
405 * Remember that this function can be accessed from a variety of
406 * places, such as Special:ResetPass, or Special:CreateAccount.
407 * @param $message String message key of a message to display if
408 * we don't redirect
409 * @param $returnTo String title of page to redirect to
410 * @param $returnToQuery String query string to add to the redirect.
411 * @param $html String empty string to go straight
412 * to the redirect, or valid HTML to add underneath the text.
413 */
414 public static function successfulLogin( $message, $returnTo='', $returnToQuery='', $html='' ) {
415 global $wgUser, $wgOut;
416
417 if( $html === '' ) {
418 $titleObj = Title::newFromText( $returnTo );
419 if ( !$titleObj instanceof Title ) {
420 $titleObj = Title::newMainPage();
421 }
422 $wgOut->redirect( $titleObj->getFullURL( $returnToQuery ) );
423 } else {
424 SpecialUserLogin::displaySuccessfulLogin( $message, $html, $returnTo, $returnToQuery );
425 }
426 }
427
428
429 protected function processLogin(){
430 global $wgUser, $wgAuth;
431 $result = $this->mLogin->attemptLogin();
432 switch ( $result ) {
433 case Login::SUCCESS:
434 if( $this->hasSessionCookie() || $this->mSkipCookieCheck ) {
435 # Replace the language object to provide user interface in
436 # correct language immediately on this first page load.
437 global $wgLang, $wgRequest;
438 $code = $wgRequest->getVal( 'uselang', $wgUser->getOption( 'language' ) );
439 $wgLang = Language::factory( $code );
440 return self::successfulLogin(
441 'loginsuccess',
442 $this->mReturnTo,
443 $this->mReturnToQuery,
444 $this->mLogin->mLoginResult );
445 } else {
446 # Do a redirect check to ensure that the cookies are
447 # being retained by the user's browser.
448 return $this->cookieRedirectCheck();
449 }
450 break;
451
452 case Login::NO_NAME:
453 case Login::ILLEGAL:
454 case Login::WRONG_PLUGIN_PASS:
455 case Login::WRONG_PASS:
456 case Login::EMPTY_PASS:
457 case Login::THROTTLED:
458 $this->mainLoginForm( wfMsgExt( $this->mLogin->mLoginResult, 'parseinline' ) );
459 break;
460
461 case Login::NOT_EXISTS:
462 if( $wgUser->isAllowed( 'createaccount' ) ){
463 $this->mainLoginForm( wfMsgExt( 'nosuchuser', 'parseinline', htmlspecialchars( $this->mName ) ) );
464 } else {
465 $this->mainLoginForm( wfMsgExt( 'nosuchusershort', 'parseinline', htmlspecialchars( $this->mName ) ) );
466 }
467 break;
468
469 case Login::RESET_PASS:
470 # 'Shell out' to Special:ResetPass to get the user to
471 # set a new permanent password from a temporary one.
472 $reset = new SpecialResetpass();
473 $reset->mHeaderMsg = 'resetpass_announce';
474 $reset->mHeaderMsgType = 'success';
475 $reset->execute( null );
476 break;
477
478 case Login::CREATE_BLOCKED:
479 $this->userBlockedMessage();
480 break;
481
482 case Login::ABORTED:
483 $msg = $this->mLogin->mLoginResult ? $this->mLogin->mLoginResult : $this->mLogin->mCreateResult;
484 $this->mainLoginForm( wfMsgExt( $msg, 'parseinline' ) );
485 break;
486
487 default:
488 throw new MWException( "Unhandled case value: $result" );
489 }
490 }
491
492 /**
493 * Attempt to send the user a password-reset mail, and display
494 * the results (good, bad or ugly).
495 */
496 protected function showMailPage(){
497 global $wgOut;
498 $result = $this->mLogin->mailPassword();
499
500 switch( $result ){
501 case Login::READ_ONLY :
502 $wgOut->readOnlyPage();
503 return;
504 case Login::MAIL_PASSCHANGE_FORBIDDEN:
505 $this->mainLoginForm( wfMsgExt( 'resetpass_forbidden', 'parseinline' ) );
506 return;
507 case Login::MAIL_BLOCKED:
508 $this->mainLoginForm( wfMsgExt( 'blocked-mailpassword', 'parseinline' ) );
509 return;
510 case Login::MAIL_PING_THROTTLED:
511 $wgOut->rateLimited();
512 return;
513 case Login::MAIL_PASS_THROTTLED:
514 global $wgPasswordReminderResendTime;
515 # Round the time in hours to 3 d.p., in case someone
516 # is specifying minutes or seconds.
517 $this->mainLoginForm( wfMsgExt(
518 'throttled-mailpassword',
519 array( 'parsemag' ),
520 round( $wgPasswordReminderResendTime, 3 )
521 ) );
522 return;
523 case Login::NO_NAME:
524 $this->mainLoginForm( wfMsgExt( 'noname', 'parseinline' ) );
525 return;
526 case Login::NOT_EXISTS:
527 $this->mainLoginForm( wfMsgWikiHtml( 'nosuchuser', htmlspecialchars( $this->mLogin->mUser->getName() ) ) );
528 return;
529 case Login::MAIL_EMPTY_EMAIL:
530 $this->mainLoginForm( wfMsgExt( 'noemail', 'parseinline', $this->mLogin->mUser->getName() ) );
531 return;
532 case Login::MAIL_BAD_IP:
533 $this->mainLoginForm( wfMsgExt( 'badipaddress', 'parseinline' ) );
534 return;
535 case Login::MAIL_ERROR:
536 $this->mainLoginForm( wfMsgExt( 'mailerror', 'parseinline', $this->mLogin->mMailResult->getMessage() ) );
537 return;
538 case Login::SUCCESS:
539 $this->mainLoginForm( wfMsgExt( 'passwordsent', 'parseinline', $this->mLogin->mUser->getName() ), 'success' );
540 return;
541 }
542 }
543
544 /**
545 * Add text to the header. Only write to $mFormHeader directly
546 * if you're determined to overwrite anything that other
547 * extensions might have added.
548 * @param $text String HTML
549 */
550 public function addFormHeader( $text ){
551 $this->mFormHeader .= $text;
552 }
553
554 /**
555 * Since the UserLoginForm hook was changed to pass a SpecialPage
556 * instead of a QuickTemplate derivative, old extensions might
557 * easily try calling this method expecing it to exist. Tempting
558 * though it is to let them have the fatal error, let's at least
559 * fail gracefully...
560 * @deprecated
561 */
562 public function set(){
563 wfDeprecated( __METHOD__ );
564 }
565 }