MediaWiki
REL1_21
|
00001 <?php 00029 class LoginForm extends SpecialPage { 00030 00031 const SUCCESS = 0; 00032 const NO_NAME = 1; 00033 const ILLEGAL = 2; 00034 const WRONG_PLUGIN_PASS = 3; 00035 const NOT_EXISTS = 4; 00036 const WRONG_PASS = 5; 00037 const EMPTY_PASS = 6; 00038 const RESET_PASS = 7; 00039 const ABORTED = 8; 00040 const CREATE_BLOCKED = 9; 00041 const THROTTLED = 10; 00042 const USER_BLOCKED = 11; 00043 const NEED_TOKEN = 12; 00044 const WRONG_TOKEN = 13; 00045 00046 var $mUsername, $mPassword, $mRetype, $mReturnTo, $mCookieCheck, $mPosted; 00047 var $mAction, $mCreateaccount, $mCreateaccountMail; 00048 var $mLoginattempt, $mRemember, $mEmail, $mDomain, $mLanguage; 00049 var $mSkipCookieCheck, $mReturnToQuery, $mToken, $mStickHTTPS; 00050 var $mType, $mReason, $mRealName; 00051 var $mAbortLoginErrorMsg = 'login-abort-generic'; 00052 private $mLoaded = false; 00053 00057 private $mExtUser = null; 00058 00062 private $mOverrideRequest = null; 00063 00067 public function __construct( $request = null ) { 00068 parent::__construct( 'Userlogin' ); 00069 00070 $this->mOverrideRequest = $request; 00071 } 00072 00076 function load() { 00077 global $wgAuth, $wgHiddenPrefs, $wgEnableEmail; 00078 00079 if ( $this->mLoaded ) { 00080 return; 00081 } 00082 $this->mLoaded = true; 00083 00084 if ( $this->mOverrideRequest === null ) { 00085 $request = $this->getRequest(); 00086 } else { 00087 $request = $this->mOverrideRequest; 00088 } 00089 00090 $this->mType = $request->getText( 'type' ); 00091 $this->mUsername = $request->getText( 'wpName' ); 00092 $this->mPassword = $request->getText( 'wpPassword' ); 00093 $this->mRetype = $request->getText( 'wpRetype' ); 00094 $this->mDomain = $request->getText( 'wpDomain' ); 00095 $this->mReason = $request->getText( 'wpReason' ); 00096 $this->mCookieCheck = $request->getVal( 'wpCookieCheck' ); 00097 $this->mPosted = $request->wasPosted(); 00098 $this->mCreateaccountMail = $request->getCheck( 'wpCreateaccountMail' ) 00099 && $wgEnableEmail; 00100 $this->mCreateaccount = $request->getCheck( 'wpCreateaccount' ) && !$this->mCreateaccountMail; 00101 $this->mLoginattempt = $request->getCheck( 'wpLoginattempt' ); 00102 $this->mAction = $request->getVal( 'action' ); 00103 $this->mRemember = $request->getCheck( 'wpRemember' ); 00104 $this->mStickHTTPS = $request->getCheck( 'wpStickHTTPS' ); 00105 $this->mLanguage = $request->getText( 'uselang' ); 00106 $this->mSkipCookieCheck = $request->getCheck( 'wpSkipCookieCheck' ); 00107 $this->mToken = ( $this->mType == 'signup' ) ? $request->getVal( 'wpCreateaccountToken' ) : $request->getVal( 'wpLoginToken' ); 00108 $this->mReturnTo = $request->getVal( 'returnto', '' ); 00109 $this->mReturnToQuery = $request->getVal( 'returntoquery', '' ); 00110 00111 if( $wgEnableEmail ) { 00112 $this->mEmail = $request->getText( 'wpEmail' ); 00113 } else { 00114 $this->mEmail = ''; 00115 } 00116 if( !in_array( 'realname', $wgHiddenPrefs ) ) { 00117 $this->mRealName = $request->getText( 'wpRealName' ); 00118 } else { 00119 $this->mRealName = ''; 00120 } 00121 00122 if( !$wgAuth->validDomain( $this->mDomain ) ) { 00123 $this->mDomain = $wgAuth->getDomain(); 00124 } 00125 $wgAuth->setDomain( $this->mDomain ); 00126 00127 # 1. When switching accounts, it sucks to get automatically logged out 00128 # 2. Do not return to PasswordReset after a successful password change 00129 # but goto Wiki start page (Main_Page) instead ( bug 33997 ) 00130 $returnToTitle = Title::newFromText( $this->mReturnTo ); 00131 if( is_object( $returnToTitle ) && ( 00132 $returnToTitle->isSpecial( 'Userlogout' ) 00133 || $returnToTitle->isSpecial( 'PasswordReset' ) ) ) { 00134 $this->mReturnTo = ''; 00135 $this->mReturnToQuery = ''; 00136 } 00137 } 00138 00139 function getDescription() { 00140 return $this->msg( $this->getUser()->isAllowed( 'createaccount' ) ? 00141 'userlogin' : 'userloginnocreate' )->text(); 00142 } 00143 00144 public function execute( $par ) { 00145 if ( session_id() == '' ) { 00146 wfSetupSession(); 00147 } 00148 00149 $this->load(); 00150 $this->setHeaders(); 00151 00152 global $wgSecureLogin; 00153 if ( 00154 $this->mType !== 'signup' && 00155 $wgSecureLogin && 00156 WebRequest::detectProtocol() !== 'https' 00157 ) { 00158 $title = $this->getFullTitle(); 00159 $query = array( 00160 'returnto' => $this->mReturnTo, 00161 'returntoquery' => $this->mReturnToQuery, 00162 'wpStickHTTPS' => $this->mStickHTTPS 00163 ); 00164 $url = $title->getFullURL( $query, false, PROTO_HTTPS ); 00165 $this->getOutput()->redirect( $url ); 00166 return; 00167 } 00168 00169 if ( $par == 'signup' ) { # Check for [[Special:Userlogin/signup]] 00170 $this->mType = 'signup'; 00171 } 00172 00173 if ( !is_null( $this->mCookieCheck ) ) { 00174 $this->onCookieRedirectCheck( $this->mCookieCheck ); 00175 return; 00176 } elseif( $this->mPosted ) { 00177 if( $this->mCreateaccount ) { 00178 $this->addNewAccount(); 00179 return; 00180 } elseif ( $this->mCreateaccountMail ) { 00181 $this->addNewAccountMailPassword(); 00182 return; 00183 } elseif ( ( 'submitlogin' == $this->mAction ) || $this->mLoginattempt ) { 00184 $this->processLogin(); 00185 return; 00186 } 00187 } 00188 $this->mainLoginForm( '' ); 00189 } 00190 00194 function addNewAccountMailPassword() { 00195 if ( $this->mEmail == '' ) { 00196 $this->mainLoginForm( $this->msg( 'noemailcreate' )->escaped() ); 00197 return; 00198 } 00199 00200 $status = $this->addNewaccountInternal(); 00201 if( !$status->isGood() ) { 00202 $error = $this->getOutput()->parse( $status->getWikiText() ); 00203 $this->mainLoginForm( $error ); 00204 return; 00205 } 00206 00207 $u = $status->getValue(); 00208 00209 // Wipe the initial password and mail a temporary one 00210 $u->setPassword( null ); 00211 $u->saveSettings(); 00212 $result = $this->mailPasswordInternal( $u, false, 'createaccount-title', 'createaccount-text' ); 00213 00214 wfRunHooks( 'AddNewAccount', array( $u, true ) ); 00215 $u->addNewUserLogEntry( 'byemail', $this->mReason ); 00216 00217 $out = $this->getOutput(); 00218 $out->setPageTitle( $this->msg( 'accmailtitle' ) ); 00219 00220 if( !$result->isGood() ) { 00221 $this->mainLoginForm( $this->msg( 'mailerror', $result->getWikiText() )->text() ); 00222 } else { 00223 $out->addWikiMsg( 'accmailtext', $u->getName(), $u->getEmail() ); 00224 $this->executeReturnTo( 'success' ); 00225 } 00226 } 00227 00232 function addNewAccount() { 00233 global $wgContLang, $wgUser, $wgEmailAuthentication, $wgLoginLanguageSelector; 00234 00235 # Create the account and abort if there's a problem doing so 00236 $status = $this->addNewAccountInternal(); 00237 if( !$status->isGood() ) { 00238 $error = $this->getOutput()->parse( $status->getWikiText() ); 00239 $this->mainLoginForm( $error ); 00240 return false; 00241 } 00242 00243 $u = $status->getValue(); 00244 00245 # Only save preferences if the user is not creating an account for someone else. 00246 if ( $this->getUser()->isAnon() ) { 00247 # If we showed up language selection links, and one was in use, be 00248 # smart (and sensible) and save that language as the user's preference 00249 if( $wgLoginLanguageSelector && $this->mLanguage ) { 00250 $u->setOption( 'language', $this->mLanguage ); 00251 } else { 00252 00253 # Otherwise the user's language preference defaults to $wgContLang, 00254 # but it may be better to set it to their preferred $wgContLang variant, 00255 # based on browser preferences or URL parameters. 00256 $u->setOption( 'language', $wgContLang->getPreferredVariant() ); 00257 } 00258 if ( $wgContLang->hasVariants() ) { 00259 $u->setOption( 'variant', $wgContLang->getPreferredVariant() ); 00260 } 00261 } 00262 00263 $out = $this->getOutput(); 00264 00265 # Send out an email authentication message if needed 00266 if( $wgEmailAuthentication && Sanitizer::validateEmail( $u->getEmail() ) ) { 00267 $status = $u->sendConfirmationMail(); 00268 if( $status->isGood() ) { 00269 $out->addWikiMsg( 'confirmemail_oncreate' ); 00270 } else { 00271 $out->addWikiText( $status->getWikiText( 'confirmemail_sendfailed' ) ); 00272 } 00273 } 00274 00275 # Save settings (including confirmation token) 00276 $u->saveSettings(); 00277 00278 # If not logged in, assume the new account as the current one and set 00279 # session cookies then show a "welcome" message or a "need cookies" 00280 # message as needed 00281 if( $this->getUser()->isAnon() ) { 00282 $u->setCookies(); 00283 $wgUser = $u; 00284 // This should set it for OutputPage and the Skin 00285 // which is needed or the personal links will be 00286 // wrong. 00287 $this->getContext()->setUser( $u ); 00288 wfRunHooks( 'AddNewAccount', array( $u, false ) ); 00289 $u->addNewUserLogEntry( 'create' ); 00290 if( $this->hasSessionCookie() ) { 00291 $this->successfulCreation(); 00292 } else { 00293 $this->cookieRedirectCheck( 'new' ); 00294 } 00295 } else { 00296 # Confirm that the account was created 00297 $out->setPageTitle( $this->msg( 'accountcreated' ) ); 00298 $out->addWikiMsg( 'accountcreatedtext', $u->getName() ); 00299 $out->addReturnTo( $this->getTitle() ); 00300 wfRunHooks( 'AddNewAccount', array( $u, false ) ); 00301 $u->addNewUserLogEntry( 'create2', $this->mReason ); 00302 } 00303 return true; 00304 } 00305 00312 public function addNewAccountInternal() { 00313 global $wgAuth, $wgMemc, $wgAccountCreationThrottle, 00314 $wgMinimalPasswordLength, $wgEmailConfirmToEdit; 00315 00316 // If the user passes an invalid domain, something is fishy 00317 if( !$wgAuth->validDomain( $this->mDomain ) ) { 00318 return Status::newFatal( 'wrongpassword' ); 00319 } 00320 00321 // If we are not allowing users to login locally, we should be checking 00322 // to see if the user is actually able to authenticate to the authenti- 00323 // cation server before they create an account (otherwise, they can 00324 // create a local account and login as any domain user). We only need 00325 // to check this for domains that aren't local. 00326 if( 'local' != $this->mDomain && $this->mDomain != '' ) { 00327 if( 00328 !$wgAuth->canCreateAccounts() && 00329 ( 00330 !$wgAuth->userExists( $this->mUsername ) || 00331 !$wgAuth->authenticate( $this->mUsername, $this->mPassword ) 00332 ) 00333 ) { 00334 return Status::newFatal( 'wrongpassword' ); 00335 } 00336 } 00337 00338 if ( wfReadOnly() ) { 00339 throw new ReadOnlyError; 00340 } 00341 00342 # Request forgery checks. 00343 if ( !self::getCreateaccountToken() ) { 00344 self::setCreateaccountToken(); 00345 return Status::newFatal( 'nocookiesfornew' ); 00346 } 00347 00348 # The user didn't pass a createaccount token 00349 if ( !$this->mToken ) { 00350 return Status::newFatal( 'sessionfailure' ); 00351 } 00352 00353 # Validate the createaccount token 00354 if ( $this->mToken !== self::getCreateaccountToken() ) { 00355 return Status::newFatal( 'sessionfailure' ); 00356 } 00357 00358 # Check permissions 00359 $currentUser = $this->getUser(); 00360 $creationBlock = $currentUser->isBlockedFromCreateAccount(); 00361 if ( !$currentUser->isAllowed( 'createaccount' ) ) { 00362 throw new PermissionsError( 'createaccount' ); 00363 } elseif ( $creationBlock instanceof Block ) { 00364 // Throws an ErrorPageError. 00365 $this->userBlockedMessage( $creationBlock ); 00366 // This should never be reached. 00367 return false; 00368 } 00369 00370 # Include checks that will include GlobalBlocking (Bug 38333) 00371 $permErrors = $this->getTitle()->getUserPermissionsErrors( 'createaccount', $currentUser, true ); 00372 if ( count( $permErrors ) ) { 00373 throw new PermissionsError( 'createaccount', $permErrors ); 00374 } 00375 00376 $ip = $this->getRequest()->getIP(); 00377 if ( $currentUser->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) { 00378 return Status::newFatal( 'sorbs_create_account_reason' ); 00379 } 00380 00381 # Now create a dummy user ($u) and check if it is valid 00382 $name = trim( $this->mUsername ); 00383 $u = User::newFromName( $name, 'creatable' ); 00384 if ( !is_object( $u ) ) { 00385 return Status::newFatal( 'noname' ); 00386 } elseif ( 0 != $u->idForName() ) { 00387 return Status::newFatal( 'userexists' ); 00388 } 00389 00390 if ( $this->mCreateaccountMail ) { 00391 # do not force a password for account creation by email 00392 # set invalid password, it will be replaced later by a random generated password 00393 $this->mPassword = null; 00394 } else { 00395 if ( $this->mPassword !== $this->mRetype ) { 00396 return Status::newFatal( 'badretype' ); 00397 } 00398 00399 # check for minimal password length 00400 $valid = $u->getPasswordValidity( $this->mPassword ); 00401 if ( $valid !== true ) { 00402 if ( !is_array( $valid ) ) { 00403 $valid = array( $valid, $wgMinimalPasswordLength ); 00404 } 00405 return call_user_func_array( 'Status::newFatal', $valid ); 00406 } 00407 } 00408 00409 # if you need a confirmed email address to edit, then obviously you 00410 # need an email address. 00411 if ( $wgEmailConfirmToEdit && strval( $this->mEmail ) === '' ) { 00412 return Status::newFatal( 'noemailtitle' ); 00413 } 00414 00415 if ( strval( $this->mEmail ) !== '' && !Sanitizer::validateEmail( $this->mEmail ) ) { 00416 return Status::newFatal( 'invalidemailaddress' ); 00417 } 00418 00419 # Set some additional data so the AbortNewAccount hook can be used for 00420 # more than just username validation 00421 $u->setEmail( $this->mEmail ); 00422 $u->setRealName( $this->mRealName ); 00423 00424 $abortError = ''; 00425 if( !wfRunHooks( 'AbortNewAccount', array( $u, &$abortError ) ) ) { 00426 // Hook point to add extra creation throttles and blocks 00427 wfDebug( "LoginForm::addNewAccountInternal: a hook blocked creation\n" ); 00428 return Status::newFatal( new RawMessage( $abortError ) ); 00429 } 00430 00431 // Hook point to check for exempt from account creation throttle 00432 if ( !wfRunHooks( 'ExemptFromAccountCreationThrottle', array( $ip ) ) ) { 00433 wfDebug( "LoginForm::exemptFromAccountCreationThrottle: a hook allowed account creation w/o throttle\n" ); 00434 } else { 00435 if ( ( $wgAccountCreationThrottle && $currentUser->isPingLimitable() ) ) { 00436 $key = wfMemcKey( 'acctcreate', 'ip', $ip ); 00437 $value = $wgMemc->get( $key ); 00438 if ( !$value ) { 00439 $wgMemc->set( $key, 0, 86400 ); 00440 } 00441 if ( $value >= $wgAccountCreationThrottle ) { 00442 return Status::newFatal( 'acct_creation_throttle_hit', $wgAccountCreationThrottle ); 00443 } 00444 $wgMemc->incr( $key ); 00445 } 00446 } 00447 00448 if( !$wgAuth->addUser( $u, $this->mPassword, $this->mEmail, $this->mRealName ) ) { 00449 return Status::newFatal( 'externaldberror' ); 00450 } 00451 00452 self::clearCreateaccountToken(); 00453 return $this->initUser( $u, false ); 00454 } 00455 00465 function initUser( $u, $autocreate ) { 00466 global $wgAuth; 00467 00468 $status = $u->addToDatabase(); 00469 if ( !$status->isOK() ) { 00470 return $status; 00471 } 00472 00473 if ( $wgAuth->allowPasswordChange() ) { 00474 $u->setPassword( $this->mPassword ); 00475 } 00476 00477 $u->setEmail( $this->mEmail ); 00478 $u->setRealName( $this->mRealName ); 00479 $u->setToken(); 00480 00481 $wgAuth->initUser( $u, $autocreate ); 00482 00483 if ( $this->mExtUser ) { 00484 $this->mExtUser->linkToLocal( $u->getId() ); 00485 $email = $this->mExtUser->getPref( 'emailaddress' ); 00486 if ( $email && !$this->mEmail ) { 00487 $u->setEmail( $email ); 00488 } 00489 } 00490 00491 $u->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 ); 00492 $u->saveSettings(); 00493 00494 # Update user count 00495 DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 0, 0, 0, 1 ) ); 00496 00497 return Status::newGood( $u ); 00498 } 00499 00508 public function authenticateUserData() { 00509 global $wgUser, $wgAuth; 00510 00511 $this->load(); 00512 00513 if ( $this->mUsername == '' ) { 00514 return self::NO_NAME; 00515 } 00516 00517 // We require a login token to prevent login CSRF 00518 // Handle part of this before incrementing the throttle so 00519 // token-less login attempts don't count towards the throttle 00520 // but wrong-token attempts do. 00521 00522 // If the user doesn't have a login token yet, set one. 00523 if ( !self::getLoginToken() ) { 00524 self::setLoginToken(); 00525 return self::NEED_TOKEN; 00526 } 00527 // If the user didn't pass a login token, tell them we need one 00528 if ( !$this->mToken ) { 00529 return self::NEED_TOKEN; 00530 } 00531 00532 $throttleCount = self::incLoginThrottle( $this->mUsername ); 00533 if ( $throttleCount === true ) { 00534 return self::THROTTLED; 00535 } 00536 00537 // Validate the login token 00538 if ( $this->mToken !== self::getLoginToken() ) { 00539 return self::WRONG_TOKEN; 00540 } 00541 00542 // Load the current user now, and check to see if we're logging in as 00543 // the same name. This is necessary because loading the current user 00544 // (say by calling getName()) calls the UserLoadFromSession hook, which 00545 // potentially creates the user in the database. Until we load $wgUser, 00546 // checking for user existence using User::newFromName($name)->getId() below 00547 // will effectively be using stale data. 00548 if ( $this->getUser()->getName() === $this->mUsername ) { 00549 wfDebug( __METHOD__ . ": already logged in as {$this->mUsername}\n" ); 00550 return self::SUCCESS; 00551 } 00552 00553 $this->mExtUser = ExternalUser::newFromName( $this->mUsername ); 00554 00555 # TODO: Allow some magic here for invalid external names, e.g., let the 00556 # user choose a different wiki name. 00557 $u = User::newFromName( $this->mUsername ); 00558 if( !( $u instanceof User ) || !User::isUsableName( $u->getName() ) ) { 00559 return self::ILLEGAL; 00560 } 00561 00562 $isAutoCreated = false; 00563 if ( $u->getID() == 0 ) { 00564 $status = $this->attemptAutoCreate( $u ); 00565 if ( $status !== self::SUCCESS ) { 00566 return $status; 00567 } else { 00568 $isAutoCreated = true; 00569 } 00570 } else { 00571 global $wgExternalAuthType, $wgAutocreatePolicy; 00572 if ( $wgExternalAuthType && $wgAutocreatePolicy != 'never' 00573 && is_object( $this->mExtUser ) 00574 && $this->mExtUser->authenticate( $this->mPassword ) 00575 ) { 00576 # The external user and local user have the same name and 00577 # password, so we assume they're the same. 00578 $this->mExtUser->linkToLocal( $u->getID() ); 00579 } 00580 00581 $u->load(); 00582 } 00583 00584 // Give general extensions, such as a captcha, a chance to abort logins 00585 $abort = self::ABORTED; 00586 if( !wfRunHooks( 'AbortLogin', array( $u, $this->mPassword, &$abort, &$this->mAbortLoginErrorMsg ) ) ) { 00587 return $abort; 00588 } 00589 00590 global $wgBlockDisablesLogin; 00591 if ( !$u->checkPassword( $this->mPassword ) ) { 00592 if( $u->checkTemporaryPassword( $this->mPassword ) ) { 00593 // The e-mailed temporary password should not be used for actu- 00594 // al logins; that's a very sloppy habit, and insecure if an 00595 // attacker has a few seconds to click "search" on someone's o- 00596 // pen mail reader. 00597 // 00598 // Allow it to be used only to reset the password a single time 00599 // to a new value, which won't be in the user's e-mail ar- 00600 // chives. 00601 // 00602 // For backwards compatibility, we'll still recognize it at the 00603 // login form to minimize surprises for people who have been 00604 // logging in with a temporary password for some time. 00605 // 00606 // As a side-effect, we can authenticate the user's e-mail ad- 00607 // dress if it's not already done, since the temporary password 00608 // was sent via e-mail. 00609 if( !$u->isEmailConfirmed() ) { 00610 $u->confirmEmail(); 00611 $u->saveSettings(); 00612 } 00613 00614 // At this point we just return an appropriate code/ indicating 00615 // that the UI should show a password reset form; bot inter- 00616 // faces etc will probably just fail cleanly here. 00617 $retval = self::RESET_PASS; 00618 } else { 00619 $retval = ( $this->mPassword == '' ) ? self::EMPTY_PASS : self::WRONG_PASS; 00620 } 00621 } elseif ( $wgBlockDisablesLogin && $u->isBlocked() ) { 00622 // If we've enabled it, make it so that a blocked user cannot login 00623 $retval = self::USER_BLOCKED; 00624 } else { 00625 $wgAuth->updateUser( $u ); 00626 $wgUser = $u; 00627 // This should set it for OutputPage and the Skin 00628 // which is needed or the personal links will be 00629 // wrong. 00630 $this->getContext()->setUser( $u ); 00631 00632 // Please reset throttle for successful logins, thanks! 00633 if ( $throttleCount ) { 00634 self::clearLoginThrottle( $this->mUsername ); 00635 } 00636 00637 if ( $isAutoCreated ) { 00638 // Must be run after $wgUser is set, for correct new user log 00639 wfRunHooks( 'AuthPluginAutoCreate', array( $u ) ); 00640 } 00641 00642 $retval = self::SUCCESS; 00643 } 00644 wfRunHooks( 'LoginAuthenticateAudit', array( $u, $this->mPassword, $retval ) ); 00645 return $retval; 00646 } 00647 00654 public static function incLoginThrottle( $username ) { 00655 global $wgPasswordAttemptThrottle, $wgMemc, $wgRequest; 00656 $username = trim( $username ); // sanity 00657 00658 $throttleCount = 0; 00659 if ( is_array( $wgPasswordAttemptThrottle ) ) { 00660 $throttleKey = wfMemcKey( 'password-throttle', $wgRequest->getIP(), md5( $username ) ); 00661 $count = $wgPasswordAttemptThrottle['count']; 00662 $period = $wgPasswordAttemptThrottle['seconds']; 00663 00664 $throttleCount = $wgMemc->get( $throttleKey ); 00665 if ( !$throttleCount ) { 00666 $wgMemc->add( $throttleKey, 1, $period ); // start counter 00667 } elseif ( $throttleCount < $count ) { 00668 $wgMemc->incr( $throttleKey ); 00669 } elseif ( $throttleCount >= $count ) { 00670 return true; 00671 } 00672 } 00673 00674 return $throttleCount; 00675 } 00676 00682 public static function clearLoginThrottle( $username ) { 00683 global $wgMemc, $wgRequest; 00684 $username = trim( $username ); // sanity 00685 00686 $throttleKey = wfMemcKey( 'password-throttle', $wgRequest->getIP(), md5( $username ) ); 00687 $wgMemc->delete( $throttleKey ); 00688 } 00689 00698 function attemptAutoCreate( $user ) { 00699 global $wgAuth, $wgAutocreatePolicy; 00700 00701 if ( $this->getUser()->isBlockedFromCreateAccount() ) { 00702 wfDebug( __METHOD__ . ": user is blocked from account creation\n" ); 00703 return self::CREATE_BLOCKED; 00704 } 00705 00711 if ( $this->mExtUser ) { 00712 # mExtUser is neither null nor false, so use the new ExternalAuth 00713 # system. 00714 if ( $wgAutocreatePolicy == 'never' ) { 00715 return self::NOT_EXISTS; 00716 } 00717 if ( !$this->mExtUser->authenticate( $this->mPassword ) ) { 00718 return self::WRONG_PLUGIN_PASS; 00719 } 00720 } else { 00721 # Old AuthPlugin. 00722 if ( !$wgAuth->autoCreate() ) { 00723 return self::NOT_EXISTS; 00724 } 00725 if ( !$wgAuth->userExists( $user->getName() ) ) { 00726 wfDebug( __METHOD__ . ": user does not exist\n" ); 00727 return self::NOT_EXISTS; 00728 } 00729 if ( !$wgAuth->authenticate( $user->getName(), $this->mPassword ) ) { 00730 wfDebug( __METHOD__ . ": \$wgAuth->authenticate() returned false, aborting\n" ); 00731 return self::WRONG_PLUGIN_PASS; 00732 } 00733 } 00734 00735 $abortError = ''; 00736 if( !wfRunHooks( 'AbortAutoAccount', array( $user, &$abortError ) ) ) { 00737 // Hook point to add extra creation throttles and blocks 00738 wfDebug( "LoginForm::attemptAutoCreate: a hook blocked creation: $abortError\n" ); 00739 $this->mAbortLoginErrorMsg = $abortError; 00740 return self::ABORTED; 00741 } 00742 00743 wfDebug( __METHOD__ . ": creating account\n" ); 00744 $status = $this->initUser( $user, true ); 00745 00746 if ( !$status->isOK() ) { 00747 $errors = $status->getErrorsByType( 'error' ); 00748 $this->mAbortLoginErrorMsg = $errors[0]['message']; 00749 return self::ABORTED; 00750 } 00751 00752 return self::SUCCESS; 00753 } 00754 00755 function processLogin() { 00756 global $wgMemc, $wgLang, $wgSecureLogin; 00757 00758 switch ( $this->authenticateUserData() ) { 00759 case self::SUCCESS: 00760 # We've verified now, update the real record 00761 $user = $this->getUser(); 00762 if( (bool)$this->mRemember != $user->getBoolOption( 'rememberpassword' ) ) { 00763 $user->setOption( 'rememberpassword', $this->mRemember ? 1 : 0 ); 00764 $user->saveSettings(); 00765 } else { 00766 $user->invalidateCache(); 00767 } 00768 00769 if( $wgSecureLogin && !$this->mStickHTTPS ) { 00770 $user->setCookies( null, false ); 00771 } else { 00772 $user->setCookies(); 00773 } 00774 self::clearLoginToken(); 00775 00776 // Reset the throttle 00777 $request = $this->getRequest(); 00778 $key = wfMemcKey( 'password-throttle', $request->getIP(), md5( $this->mUsername ) ); 00779 $wgMemc->delete( $key ); 00780 00781 if( $this->hasSessionCookie() || $this->mSkipCookieCheck ) { 00782 /* Replace the language object to provide user interface in 00783 * correct language immediately on this first page load. 00784 */ 00785 $code = $request->getVal( 'uselang', $user->getOption( 'language' ) ); 00786 $userLang = Language::factory( $code ); 00787 $wgLang = $userLang; 00788 $this->getContext()->setLanguage( $userLang ); 00789 // Reset SessionID on Successful login (bug 40995) 00790 $this->renewSessionId(); 00791 $this->successfulLogin(); 00792 } else { 00793 $this->cookieRedirectCheck( 'login' ); 00794 } 00795 break; 00796 00797 case self::NEED_TOKEN: 00798 $this->mainLoginForm( $this->msg( 'nocookiesforlogin' )->parse() ); 00799 break; 00800 case self::WRONG_TOKEN: 00801 $this->mainLoginForm( $this->msg( 'sessionfailure' )->text() ); 00802 break; 00803 case self::NO_NAME: 00804 case self::ILLEGAL: 00805 $this->mainLoginForm( $this->msg( 'noname' )->text() ); 00806 break; 00807 case self::WRONG_PLUGIN_PASS: 00808 $this->mainLoginForm( $this->msg( 'wrongpassword' )->text() ); 00809 break; 00810 case self::NOT_EXISTS: 00811 if( $this->getUser()->isAllowed( 'createaccount' ) ) { 00812 $this->mainLoginForm( $this->msg( 'nosuchuser', 00813 wfEscapeWikiText( $this->mUsername ) )->parse() ); 00814 } else { 00815 $this->mainLoginForm( $this->msg( 'nosuchusershort', 00816 wfEscapeWikiText( $this->mUsername ) )->text() ); 00817 } 00818 break; 00819 case self::WRONG_PASS: 00820 $this->mainLoginForm( $this->msg( 'wrongpassword' )->text() ); 00821 break; 00822 case self::EMPTY_PASS: 00823 $this->mainLoginForm( $this->msg( 'wrongpasswordempty' )->text() ); 00824 break; 00825 case self::RESET_PASS: 00826 $this->resetLoginForm( $this->msg( 'resetpass_announce' )->text() ); 00827 break; 00828 case self::CREATE_BLOCKED: 00829 $this->userBlockedMessage( $this->getUser()->mBlock ); 00830 break; 00831 case self::THROTTLED: 00832 $this->mainLoginForm( $this->msg( 'login-throttled' )->text() ); 00833 break; 00834 case self::USER_BLOCKED: 00835 $this->mainLoginForm( $this->msg( 'login-userblocked', 00836 $this->mUsername )->escaped() ); 00837 break; 00838 case self::ABORTED: 00839 $this->mainLoginForm( $this->msg( $this->mAbortLoginErrorMsg )->text() ); 00840 break; 00841 default: 00842 throw new MWException( 'Unhandled case value' ); 00843 } 00844 } 00845 00849 function resetLoginForm( $error ) { 00850 $this->getOutput()->addHTML( Xml::element( 'p', array( 'class' => 'error' ), $error ) ); 00851 $reset = new SpecialChangePassword(); 00852 $reset->setContext( $this->getContext() ); 00853 $reset->execute( null ); 00854 } 00855 00863 function mailPasswordInternal( $u, $throttle = true, $emailTitle = 'passwordremindertitle', $emailText = 'passwordremindertext' ) { 00864 global $wgCanonicalServer, $wgScript, $wgNewPasswordExpiry; 00865 00866 if ( $u->getEmail() == '' ) { 00867 return Status::newFatal( 'noemail', $u->getName() ); 00868 } 00869 $ip = $this->getRequest()->getIP(); 00870 if( !$ip ) { 00871 return Status::newFatal( 'badipaddress' ); 00872 } 00873 00874 $currentUser = $this->getUser(); 00875 wfRunHooks( 'User::mailPasswordInternal', array( &$currentUser, &$ip, &$u ) ); 00876 00877 $np = $u->randomPassword(); 00878 $u->setNewpassword( $np, $throttle ); 00879 $u->saveSettings(); 00880 $userLanguage = $u->getOption( 'language' ); 00881 $m = $this->msg( $emailText, $ip, $u->getName(), $np, '<' . $wgCanonicalServer . $wgScript . '>', 00882 round( $wgNewPasswordExpiry / 86400 ) )->inLanguage( $userLanguage )->text(); 00883 $result = $u->sendMail( $this->msg( $emailTitle )->inLanguage( $userLanguage )->text(), $m ); 00884 00885 return $result; 00886 } 00887 00898 function successfulLogin() { 00899 # Run any hooks; display injected HTML if any, else redirect 00900 $currentUser = $this->getUser(); 00901 $injected_html = ''; 00902 wfRunHooks( 'UserLoginComplete', array( &$currentUser, &$injected_html ) ); 00903 00904 if( $injected_html !== '' ) { 00905 $this->displaySuccessfulAction( $this->msg( 'loginsuccesstitle' ), 00906 'loginsuccess', $injected_html ); 00907 } else { 00908 $this->executeReturnTo( 'successredirect' ); 00909 } 00910 } 00911 00918 function successfulCreation() { 00919 # Run any hooks; display injected HTML 00920 $currentUser = $this->getUser(); 00921 $injected_html = ''; 00922 $welcome_creation_msg = 'welcomecreation-msg'; 00923 00924 wfRunHooks( 'UserLoginComplete', array( &$currentUser, &$injected_html ) ); 00925 00931 wfRunHooks( 'BeforeWelcomeCreation', array( &$welcome_creation_msg, &$injected_html ) ); 00932 00933 $this->displaySuccessfulAction( $this->msg( 'welcomeuser', $this->getUser()->getName() ), 00934 $welcome_creation_msg, $injected_html ); 00935 } 00936 00944 private function displaySuccessfulAction( $title, $msgname, $injected_html ) { 00945 $out = $this->getOutput(); 00946 $out->setPageTitle( $title ); 00947 if ( $msgname ) { 00948 $out->addWikiMsg( $msgname, wfEscapeWikiText( $this->getUser()->getName() ) ); 00949 } 00950 00951 $out->addHTML( $injected_html ); 00952 00953 $this->executeReturnTo( 'success' ); 00954 } 00955 00964 function userBlockedMessage( Block $block ) { 00965 # Let's be nice about this, it's likely that this feature will be used 00966 # for blocking large numbers of innocent people, e.g. range blocks on 00967 # schools. Don't blame it on the user. There's a small chance that it 00968 # really is the user's fault, i.e. the username is blocked and they 00969 # haven't bothered to log out before trying to create an account to 00970 # evade it, but we'll leave that to their guilty conscience to figure 00971 # out. 00972 throw new ErrorPageError( 00973 'cantcreateaccounttitle', 00974 'cantcreateaccount-text', 00975 array( 00976 $block->getTarget(), 00977 $block->mReason ? $block->mReason : $this->msg( 'blockednoreason' )->text(), 00978 $block->getByName() 00979 ) 00980 ); 00981 } 00982 00991 private function executeReturnTo( $type ) { 00992 global $wgRedirectOnLogin, $wgSecureLogin; 00993 00994 if ( $type != 'error' && $wgRedirectOnLogin !== null ) { 00995 $returnTo = $wgRedirectOnLogin; 00996 $returnToQuery = array(); 00997 } else { 00998 $returnTo = $this->mReturnTo; 00999 $returnToQuery = wfCgiToArray( $this->mReturnToQuery ); 01000 } 01001 01002 $returnToTitle = Title::newFromText( $returnTo ); 01003 if ( !$returnToTitle ) { 01004 $returnToTitle = Title::newMainPage(); 01005 } 01006 01007 if ( $wgSecureLogin && !$this->mStickHTTPS ) { 01008 $options = array( 'http' ); 01009 $proto = PROTO_HTTP; 01010 } elseif( $wgSecureLogin ) { 01011 $options = array( 'https' ); 01012 $proto = PROTO_HTTPS; 01013 } else { 01014 $options = array(); 01015 $proto = PROTO_RELATIVE; 01016 } 01017 01018 if ( $type == 'successredirect' ) { 01019 $redirectUrl = $returnToTitle->getFullURL( $returnToQuery, false, $proto ); 01020 $this->getOutput()->redirect( $redirectUrl ); 01021 } else { 01022 $this->getOutput()->addReturnTo( $returnToTitle, $returnToQuery, null, $options ); 01023 } 01024 } 01025 01029 function mainLoginForm( $msg, $msgtype = 'error' ) { 01030 global $wgEnableEmail, $wgEnableUserEmail; 01031 global $wgHiddenPrefs, $wgLoginLanguageSelector; 01032 global $wgAuth, $wgEmailConfirmToEdit, $wgCookieExpiration; 01033 global $wgSecureLogin, $wgSecureLoginDefaultHTTPS, $wgPasswordResetRoutes; 01034 01035 $titleObj = $this->getTitle(); 01036 $user = $this->getUser(); 01037 01038 if ( $this->mType == 'signup' ) { 01039 // Block signup here if in readonly. Keeps user from 01040 // going through the process (filling out data, etc) 01041 // and being informed later. 01042 $permErrors = $titleObj->getUserPermissionsErrors( 'createaccount', $user, true ); 01043 if ( count( $permErrors ) ) { 01044 throw new PermissionsError( 'createaccount', $permErrors ); 01045 } elseif ( $user->isBlockedFromCreateAccount() ) { 01046 $this->userBlockedMessage( $user->isBlockedFromCreateAccount() ); 01047 return; 01048 } elseif ( wfReadOnly() ) { 01049 throw new ReadOnlyError; 01050 } 01051 } 01052 01053 // Pre-fill username (if not creating an account, bug 44775). 01054 if ( $this->mUsername == '' && $this->mType != 'signup' ) { 01055 if ( $user->isLoggedIn() ) { 01056 $this->mUsername = $user->getName(); 01057 } else { 01058 $this->mUsername = $this->getRequest()->getCookie( 'UserName' ); 01059 } 01060 } 01061 01062 if ( $this->mType == 'signup' ) { 01063 $template = new UsercreateTemplate(); 01064 $q = 'action=submitlogin&type=signup'; 01065 $linkq = 'type=login'; 01066 $linkmsg = 'gotaccount'; 01067 $this->getOutput()->addModules( 'mediawiki.special.userlogin.signup' ); 01068 } else { 01069 $template = new UserloginTemplate(); 01070 $q = 'action=submitlogin&type=login'; 01071 $linkq = 'type=signup'; 01072 $linkmsg = 'nologin'; 01073 } 01074 01075 if ( $this->mReturnTo !== '' ) { 01076 $returnto = '&returnto=' . wfUrlencode( $this->mReturnTo ); 01077 if ( $this->mReturnToQuery !== '' ) { 01078 $returnto .= '&returntoquery=' . 01079 wfUrlencode( $this->mReturnToQuery ); 01080 } 01081 $q .= $returnto; 01082 $linkq .= $returnto; 01083 } 01084 01085 # Don't show a "create account" link if the user can't 01086 if( $this->showCreateOrLoginLink( $user ) ) { 01087 # Pass any language selection on to the mode switch link 01088 if( $wgLoginLanguageSelector && $this->mLanguage ) { 01089 $linkq .= '&uselang=' . $this->mLanguage; 01090 } 01091 $link = Html::element( 'a', array( 'href' => $titleObj->getLocalURL( $linkq ) ), 01092 $this->msg( $linkmsg . 'link' )->text() ); # Calling either 'gotaccountlink' or 'nologinlink' 01093 01094 $template->set( 'link', $this->msg( $linkmsg )->rawParams( $link )->parse() ); 01095 } else { 01096 $template->set( 'link', '' ); 01097 } 01098 01099 // Decide if we default stickHTTPS on 01100 if ( $wgSecureLoginDefaultHTTPS && $this->mAction != 'submitlogin' && !$this->mLoginattempt ) { 01101 $this->mStickHTTPS = true; 01102 } 01103 01104 $resetLink = $this->mType == 'signup' 01105 ? null 01106 : is_array( $wgPasswordResetRoutes ) && in_array( true, array_values( $wgPasswordResetRoutes ) ); 01107 01108 $template->set( 'header', '' ); 01109 $template->set( 'name', $this->mUsername ); 01110 $template->set( 'password', $this->mPassword ); 01111 $template->set( 'retype', $this->mRetype ); 01112 $template->set( 'createemailset', $this->mCreateaccountMail ); 01113 $template->set( 'email', $this->mEmail ); 01114 $template->set( 'realname', $this->mRealName ); 01115 $template->set( 'domain', $this->mDomain ); 01116 $template->set( 'reason', $this->mReason ); 01117 01118 $template->set( 'action', $titleObj->getLocalURL( $q ) ); 01119 $template->set( 'message', $msg ); 01120 $template->set( 'messagetype', $msgtype ); 01121 $template->set( 'createemail', $wgEnableEmail && $user->isLoggedIn() ); 01122 $template->set( 'userealname', !in_array( 'realname', $wgHiddenPrefs ) ); 01123 $template->set( 'useemail', $wgEnableEmail ); 01124 $template->set( 'emailrequired', $wgEmailConfirmToEdit ); 01125 $template->set( 'emailothers', $wgEnableUserEmail ); 01126 $template->set( 'canreset', $wgAuth->allowPasswordChange() ); 01127 $template->set( 'resetlink', $resetLink ); 01128 $template->set( 'canremember', ( $wgCookieExpiration > 0 ) ); 01129 $template->set( 'usereason', $user->isLoggedIn() ); 01130 $template->set( 'remember', $user->getOption( 'rememberpassword' ) || $this->mRemember ); 01131 $template->set( 'cansecurelogin', ( $wgSecureLogin === true ) ); 01132 $template->set( 'stickHTTPS', $this->mStickHTTPS ); 01133 01134 if ( $this->mType == 'signup' ) { 01135 if ( !self::getCreateaccountToken() ) { 01136 self::setCreateaccountToken(); 01137 } 01138 $template->set( 'token', self::getCreateaccountToken() ); 01139 } else { 01140 if ( !self::getLoginToken() ) { 01141 self::setLoginToken(); 01142 } 01143 $template->set( 'token', self::getLoginToken() ); 01144 } 01145 01146 # Prepare language selection links as needed 01147 if( $wgLoginLanguageSelector ) { 01148 $template->set( 'languages', $this->makeLanguageSelector() ); 01149 if( $this->mLanguage ) { 01150 $template->set( 'uselang', $this->mLanguage ); 01151 } 01152 } 01153 01154 // Use loginend-https for HTTPS requests if it's not blank, loginend otherwise 01155 // Ditto for signupend 01156 $usingHTTPS = WebRequest::detectProtocol() == 'https'; 01157 $loginendHTTPS = $this->msg( 'loginend-https' ); 01158 $signupendHTTPS = $this->msg( 'signupend-https' ); 01159 if ( $usingHTTPS && !$loginendHTTPS->isBlank() ) { 01160 $template->set( 'loginend', $loginendHTTPS->parse() ); 01161 } else { 01162 $template->set( 'loginend', $this->msg( 'loginend' )->parse() ); 01163 } 01164 if ( $usingHTTPS && !$signupendHTTPS->isBlank() ) { 01165 $template->set( 'signupend', $signupendHTTPS->parse() ); 01166 } else { 01167 $template->set( 'signupend', $this->msg( 'signupend' )->parse() ); 01168 } 01169 01170 // Give authentication and captcha plugins a chance to modify the form 01171 $wgAuth->modifyUITemplate( $template, $this->mType ); 01172 if ( $this->mType == 'signup' ) { 01173 wfRunHooks( 'UserCreateForm', array( &$template ) ); 01174 } else { 01175 wfRunHooks( 'UserLoginForm', array( &$template ) ); 01176 } 01177 01178 $out = $this->getOutput(); 01179 $out->disallowUserJs(); // just in case... 01180 $out->addTemplate( $template ); 01181 } 01182 01190 function showCreateOrLoginLink( &$user ) { 01191 if( $this->mType == 'signup' ) { 01192 return true; 01193 } elseif( $user->isAllowed( 'createaccount' ) ) { 01194 return true; 01195 } else { 01196 return false; 01197 } 01198 } 01199 01210 function hasSessionCookie() { 01211 global $wgDisableCookieCheck; 01212 return $wgDisableCookieCheck ? true : $this->getRequest()->checkSessionCookie(); 01213 } 01214 01219 public static function getLoginToken() { 01220 global $wgRequest; 01221 return $wgRequest->getSessionData( 'wsLoginToken' ); 01222 } 01223 01227 public static function setLoginToken() { 01228 global $wgRequest; 01229 // Generate a token directly instead of using $user->editToken() 01230 // because the latter reuses $_SESSION['wsEditToken'] 01231 $wgRequest->setSessionData( 'wsLoginToken', MWCryptRand::generateHex( 32 ) ); 01232 } 01233 01237 public static function clearLoginToken() { 01238 global $wgRequest; 01239 $wgRequest->setSessionData( 'wsLoginToken', null ); 01240 } 01241 01246 public static function getCreateaccountToken() { 01247 global $wgRequest; 01248 return $wgRequest->getSessionData( 'wsCreateaccountToken' ); 01249 } 01250 01254 public static function setCreateaccountToken() { 01255 global $wgRequest; 01256 $wgRequest->setSessionData( 'wsCreateaccountToken', MWCryptRand::generateHex( 32 ) ); 01257 } 01258 01262 public static function clearCreateaccountToken() { 01263 global $wgRequest; 01264 $wgRequest->setSessionData( 'wsCreateaccountToken', null ); 01265 } 01266 01270 private function renewSessionId() { 01271 global $wgSecureLogin, $wgCookieSecure; 01272 if( $wgSecureLogin && !$this->mStickHTTPS ) { 01273 $wgCookieSecure = false; 01274 } 01275 01276 // If either we don't trust PHP's entropy, or if we need 01277 // to change cookie settings when logging in because of 01278 // wpStickHTTPS, then change the session ID manually. 01279 $cookieParams = session_get_cookie_params(); 01280 if ( wfCheckEntropy() && $wgCookieSecure == $cookieParams['secure'] ) { 01281 session_regenerate_id( false ); 01282 } else { 01283 $tmp = $_SESSION; 01284 session_destroy(); 01285 wfSetupSession( MWCryptRand::generateHex( 32 ) ); 01286 $_SESSION = $tmp; 01287 } 01288 } 01289 01293 function cookieRedirectCheck( $type ) { 01294 $titleObj = SpecialPage::getTitleFor( 'Userlogin' ); 01295 $query = array( 'wpCookieCheck' => $type ); 01296 if ( $this->mReturnTo !== '' ) { 01297 $query['returnto'] = $this->mReturnTo; 01298 $query['returntoquery'] = $this->mReturnToQuery; 01299 } 01300 $check = $titleObj->getFullURL( $query ); 01301 01302 $this->getOutput()->redirect( $check ); 01303 } 01304 01308 function onCookieRedirectCheck( $type ) { 01309 if ( !$this->hasSessionCookie() ) { 01310 if ( $type == 'new' ) { 01311 $this->mainLoginForm( $this->msg( 'nocookiesnew' )->parse() ); 01312 } elseif ( $type == 'login' ) { 01313 $this->mainLoginForm( $this->msg( 'nocookieslogin' )->parse() ); 01314 } else { 01315 # shouldn't happen 01316 $this->mainLoginForm( $this->msg( 'error' )->text() ); 01317 } 01318 } else { 01319 $this->successfulLogin(); 01320 } 01321 } 01322 01329 function makeLanguageSelector() { 01330 $msg = $this->msg( 'loginlanguagelinks' )->inContentLanguage(); 01331 if( !$msg->isBlank() ) { 01332 $langs = explode( "\n", $msg->text() ); 01333 $links = array(); 01334 foreach( $langs as $lang ) { 01335 $lang = trim( $lang, '* ' ); 01336 $parts = explode( '|', $lang ); 01337 if ( count( $parts ) >= 2 ) { 01338 $links[] = $this->makeLanguageSelectorLink( $parts[0], trim( $parts[1] ) ); 01339 } 01340 } 01341 return count( $links ) > 0 ? $this->msg( 'loginlanguagelabel' )->rawParams( 01342 $this->getLanguage()->pipeList( $links ) )->escaped() : ''; 01343 } else { 01344 return ''; 01345 } 01346 } 01347 01356 function makeLanguageSelectorLink( $text, $lang ) { 01357 if( $this->getLanguage()->getCode() == $lang ) { 01358 // no link for currently used language 01359 return htmlspecialchars( $text ); 01360 } 01361 $query = array( 'uselang' => $lang ); 01362 if( $this->mType == 'signup' ) { 01363 $query['type'] = 'signup'; 01364 } 01365 if( $this->mReturnTo !== '' ) { 01366 $query['returnto'] = $this->mReturnTo; 01367 $query['returntoquery'] = $this->mReturnToQuery; 01368 } 01369 01370 $attr = array(); 01371 $targetLanguage = Language::factory( $lang ); 01372 $attr['lang'] = $attr['hreflang'] = $targetLanguage->getHtmlCode(); 01373 01374 return Linker::linkKnown( 01375 $this->getTitle(), 01376 htmlspecialchars( $text ), 01377 $attr, 01378 $query 01379 ); 01380 } 01381 01382 protected function getGroupName() { 01383 return 'login'; 01384 } 01385 }