public function execute() { if (!$this->hasAnyRoutes()) { $this->dieUsage('No password reset routes are available.', 'moduledisabled'); } $params = $this->extractRequestParams() + ['user' => null, 'email' => null]; $this->requireOnlyOneParameter($params, 'user', 'email'); $passwordReset = new PasswordReset($this->getConfig(), AuthManager::singleton()); $status = $passwordReset->isAllowed($this->getUser(), $params['capture']); if (!$status->isOK()) { $this->dieStatus(Status::wrap($status)); } $status = $passwordReset->execute($this->getUser(), $params['user'], $params['email'], $params['capture']); if (!$status->isOK()) { $status->value = null; $this->dieStatus(Status::wrap($status)); } $result = $this->getResult(); $result->addValue(['resetpassword'], 'status', 'success'); if ($params['capture']) { $passwords = $status->getValue() ?: []; ApiResult::setArrayType($passwords, 'kvp', 'user'); ApiResult::setIndexedTagName($passwords, 'p'); $result->addValue(['resetpassword'], 'passwords', $passwords); } }
/** * Hide the password reset page if resets are disabled. * @return bool */ public function isListed() { if ($this->passwordReset->isAllowed($this->getUser())->isGood()) { return parent::isListed(); } return false; }
/** * @dataProvider provideIsAllowed */ public function testIsAllowed($passwordResetRoutes, $enableEmail, $allowsAuthenticationDataChange, $canEditPrivate, $canSeePassword, $userIsBlocked, $isAllowed, $isAllowedToDisplayPassword) { $config = new HashConfig(['PasswordResetRoutes' => $passwordResetRoutes, 'EnableEmail' => $enableEmail]); $authManager = $this->getMockBuilder(AuthManager::class)->disableOriginalConstructor()->getMock(); $authManager->expects($this->any())->method('allowsAuthenticationDataChange')->willReturn($allowsAuthenticationDataChange ? Status::newGood() : Status::newFatal('foo')); $user = $this->getMock(User::class); $user->expects($this->any())->method('getName')->willReturn('Foo'); $user->expects($this->any())->method('isBlocked')->willReturn($userIsBlocked); $user->expects($this->any())->method('isAllowed')->will($this->returnCallback(function ($perm) use($canEditPrivate, $canSeePassword) { if ($perm === 'editmyprivateinfo') { return $canEditPrivate; } elseif ($perm === 'passwordreset') { return $canSeePassword; } else { $this->fail('Unexpected permission check'); } })); $passwordReset = new PasswordReset($config, $authManager); $this->assertSame($isAllowed, $passwordReset->isAllowed($user)->isGood()); $this->assertSame($isAllowedToDisplayPassword, $passwordReset->isAllowed($user, true)->isGood()); }
/** * Generates a form from the given request. * @param AuthenticationRequest[] $requests * @param string $action AuthManager action name * @param string|Message $msg * @param string $msgType * @return HTMLForm */ protected function getAuthForm(array $requests, $action, $msg = '', $msgType = 'error') { global $wgSecureLogin, $wgLoginLanguageSelector; // FIXME merge this with parent if (isset($this->authForm)) { return $this->authForm; } $usingHTTPS = $this->getRequest()->getProtocol() === 'https'; // get basic form description from the auth logic $fieldInfo = AuthenticationRequest::mergeFieldInfo($requests); $fakeTemplate = $this->getFakeTemplate($msg, $msgType); $this->fakeTemplate = $fakeTemplate; // FIXME there should be a saner way to pass this to the hook // this will call onAuthChangeFormFields() $formDescriptor = static::fieldInfoToFormDescriptor($requests, $fieldInfo, $this->authAction); $this->postProcessFormDescriptor($formDescriptor); $context = $this->getContext(); if ($context->getRequest() !== $this->getRequest()) { // We have overridden the request, need to make sure the form uses that too. $context = new DerivativeContext($this->getContext()); $context->setRequest($this->getRequest()); } $form = HTMLForm::factory('vform', $formDescriptor, $context); $form->addHiddenField('authAction', $this->authAction); if ($wgLoginLanguageSelector) { $form->addHiddenField('uselang', $this->mLanguage); } $form->addHiddenField('force', $this->securityLevel); $form->addHiddenField($this->getTokenName(), $this->getToken()->toString()); if ($wgSecureLogin) { // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved if (!$this->isSignup()) { $form->addHiddenField('wpForceHttps', (int) $this->mStickHTTPS); $form->addHiddenField('wpFromhttp', $usingHTTPS); } } // set properties of the form itself $form->setAction($this->getPageTitle()->getLocalURL($this->getReturnToQueryStringFragment())); $form->setName('userlogin' . ($this->isSignup() ? '2' : '')); if ($this->isSignup()) { $form->setId('userlogin2'); } // add pre/post text // header used by ConfirmEdit, CondfirmAccount, Persona, WikimediaIncubator, SemanticSignup // should be above the error message but HTMLForm doesn't support that $form->addHeaderText($fakeTemplate->html('header')); // FIXME the old form used this for error/warning messages which does not play well with // HTMLForm (maybe it could with a subclass?); for now only display it for signups // (where the JS username validation needs it) and alway empty if ($this->isSignup()) { // used by the mediawiki.special.userlogin.signup.js module $statusAreaAttribs = ['id' => 'mw-createacct-status-area']; // $statusAreaAttribs += $msg ? [ 'class' => "{$msgType}box" ] : [ 'style' => 'display: none;' ]; $form->addHeaderText(Html::element('div', $statusAreaAttribs)); } // header used by MobileFrontend $form->addHeaderText($fakeTemplate->html('formheader')); // blank signup footer for site customization if ($this->isSignup() && $this->showExtraInformation()) { // Use signupend-https for HTTPS requests if it's not blank, signupend otherwise $signupendMsg = $this->msg('signupend'); $signupendHttpsMsg = $this->msg('signupend-https'); if (!$signupendMsg->isDisabled()) { $signupendText = $usingHTTPS && !$signupendHttpsMsg->isBlank() ? $signupendHttpsMsg->parse() : $signupendMsg->parse(); $form->addPostText(Html::rawElement('div', ['id' => 'signupend'], $signupendText)); } } // warning header for non-standard workflows (e.g. security reauthentication) if (!$this->isSignup() && $this->getUser()->isLoggedIn()) { $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin'; $form->addHeaderText(Html::rawElement('div', ['class' => 'warningbox'], $this->msg($reauthMessage)->params($this->getUser()->getName())->parse())); } if (!$this->isSignup() && $this->showExtraInformation()) { $passwordReset = new PasswordReset($this->getConfig(), AuthManager::singleton()); if ($passwordReset->isAllowed($this->getUser())) { $form->addFooterText(Html::rawElement('div', ['class' => 'mw-ui-vform-field mw-form-related-link-container'], Linker::link(SpecialPage::getTitleFor('PasswordReset'), $this->msg('userlogin-resetpassword-link')->escaped()))); } // Don't show a "create account" link if the user can't. if ($this->showCreateAccountLink()) { // link to the other action $linkTitle = $this->getTitleFor($this->isSignup() ? 'Userlogin' : 'CreateAccount'); $linkq = $this->getReturnToQueryStringFragment(); // Pass any language selection on to the mode switch link if ($wgLoginLanguageSelector && $this->mLanguage) { $linkq .= '&uselang=' . $this->mLanguage; } $createOrLoginHref = $linkTitle->getLocalURL($linkq); if ($this->getUser()->isLoggedIn()) { $createOrLoginHtml = Html::rawElement('div', ['class' => 'mw-ui-vform-field'], Html::element('a', ['id' => 'mw-createaccount-join', 'href' => $createOrLoginHref, 'tabindex' => 100], $this->msg('userlogin-createanother')->escaped())); } else { $createOrLoginHtml = Html::rawElement('div', ['id' => 'mw-createaccount-cta', 'class' => 'mw-ui-vform-field'], $this->msg('userlogin-noaccount')->escaped() . Html::element('a', ['id' => 'mw-createaccount-join', 'href' => $createOrLoginHref, 'class' => 'mw-ui-button', 'tabindex' => 100], $this->msg('userlogin-joinproject')->escaped())); } $form->addFooterText($createOrLoginHtml); } } $form->suppressDefaultSubmit(); $this->authForm = $form; return $form; }
/** * Create a HTMLForm descriptor for the core login fields. * @param FakeAuthTemplate $template B/C data (not used but needed by getBCFieldDefinitions) * @return array */ protected function getFieldDefinitions($template) { global $wgEmailConfirmToEdit, $wgLoginLanguageSelector; $isLoggedIn = $this->getUser()->isLoggedIn(); $continuePart = $this->isContinued() ? 'continue-' : ''; $anotherPart = $isLoggedIn ? 'another-' : ''; $expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration(); $expirationDays = ceil($expiration / (3600 * 24)); $secureLoginLink = ''; if ($this->mSecureLoginUrl) { $secureLoginLink = Html::element('a', ['href' => $this->mSecureLoginUrl, 'class' => 'mw-ui-flush-right mw-secure'], $this->msg('userlogin-signwithsecure')->text()); } $usernameHelpLink = ''; if (!$this->msg('createacct-helpusername')->isDisabled()) { $usernameHelpLink = Html::rawElement('span', ['class' => 'mw-ui-flush-right'], $this->msg('createacct-helpusername')->parse()); } if ($this->isSignup()) { $fieldDefinitions = ['statusarea' => ['type' => 'info', 'raw' => true, 'default' => Html::element('div', ['id' => 'mw-createacct-status-area']), 'weight' => -105], 'username' => ['label-raw' => $this->msg('userlogin-yourname')->escaped() . $usernameHelpLink, 'id' => 'wpName2', 'placeholder-message' => $isLoggedIn ? 'createacct-another-username-ph' : 'userlogin-yourname-ph'], 'mailpassword' => ['type' => 'check', 'label-message' => 'createaccountmail', 'name' => 'wpCreateaccountMail', 'id' => 'wpCreateaccountMail'], 'password' => ['id' => 'wpPassword2', 'placeholder-message' => 'createacct-yourpassword-ph', 'hide-if' => ['===', 'wpCreateaccountMail', '1']], 'domain' => [], 'retype' => ['baseField' => 'password', 'type' => 'password', 'label-message' => 'createacct-yourpasswordagain', 'id' => 'wpRetype', 'cssclass' => 'loginPassword', 'size' => 20, 'validation-callback' => function ($value, $alldata) { if (empty($alldata['mailpassword']) && !empty($alldata['password'])) { if (!$value) { return $this->msg('htmlform-required'); } elseif ($value !== $alldata['password']) { return $this->msg('badretype'); } } return true; }, 'hide-if' => ['===', 'wpCreateaccountMail', '1'], 'placeholder-message' => 'createacct-yourpasswordagain-ph'], 'email' => ['type' => 'email', 'label-message' => $wgEmailConfirmToEdit ? 'createacct-emailrequired' : 'createacct-emailoptional', 'id' => 'wpEmail', 'cssclass' => 'loginText', 'size' => '20', 'required' => $wgEmailConfirmToEdit, 'validation-callback' => function ($value, $alldata) { global $wgEmailConfirmToEdit; // AuthManager will check most of these, but that will make the auth // session fail and this won't, so nicer to do it this way if (!$value && $wgEmailConfirmToEdit) { // no point in allowing registration without email when email is // required to edit return $this->msg('noemailtitle'); } elseif (!$value && !empty($alldata['mailpassword'])) { // cannot send password via email when there is no email address return $this->msg('noemailcreate'); } elseif ($value && !Sanitizer::validateEmail($value)) { return $this->msg('invalidemailaddress'); } return true; }, 'placeholder-message' => 'createacct-' . $anotherPart . 'email-ph'], 'realname' => ['type' => 'text', 'help-message' => $isLoggedIn ? 'createacct-another-realname-tip' : 'prefs-help-realname', 'label-message' => 'createacct-realname', 'cssclass' => 'loginText', 'size' => 20, 'id' => 'wpRealName'], 'reason' => ['type' => 'text', 'label-message' => 'createacct-reason', 'cssclass' => 'loginText', 'id' => 'wpReason', 'size' => '20', 'placeholder-message' => 'createacct-reason-ph'], 'extrainput' => [], 'createaccount' => ['type' => 'submit', 'default' => $this->msg('createacct-' . $anotherPart . $continuePart . 'submit')->text(), 'name' => 'wpCreateaccount', 'id' => 'wpCreateaccount', 'weight' => 100]]; } else { $fieldDefinitions = ['username' => ['label-raw' => $this->msg('userlogin-yourname')->escaped() . $secureLoginLink, 'id' => 'wpName1', 'placeholder-message' => 'userlogin-yourname-ph'], 'password' => ['id' => 'wpPassword1', 'placeholder-message' => 'userlogin-yourpassword-ph'], 'domain' => [], 'extrainput' => [], 'rememberMe' => ['type' => 'check', 'name' => 'wpRemember', 'label-message' => $this->msg('userlogin-remembermypassword')->numParams($expirationDays), 'id' => 'wpRemember'], 'loginattempt' => ['type' => 'submit', 'default' => $this->msg('pt-login-' . $continuePart . 'button')->text(), 'id' => 'wpLoginAttempt', 'weight' => 100], 'linkcontainer' => ['type' => 'info', 'cssclass' => 'mw-form-related-link-container mw-userlogin-help', 'raw' => true, 'default' => Html::element('a', ['href' => Skin::makeInternalOrExternalUrl(wfMessage('helplogin-url')->inContentLanguage()->text())], $this->msg('userlogin-helplink2')->text()), 'weight' => 200], 'skipReset' => ['weight' => 110, 'flags' => []]]; } $fieldDefinitions['username'] += ['type' => 'text', 'name' => 'wpName', 'cssclass' => 'loginText', 'size' => 20]; $fieldDefinitions['password'] += ['type' => 'password', 'name' => 'wpPassword', 'cssclass' => 'loginPassword', 'size' => 20]; if ($template->get('header') || $template->get('formheader')) { // B/C for old extensions that haven't been converted to AuthManager (or have been // but somebody is using the old version) and still use templates via the // UserCreateForm/UserLoginForm hook. // 'header' used by ConfirmEdit, CondfirmAccount, Persona, WikimediaIncubator, SemanticSignup // 'formheader' used by MobileFrontend $fieldDefinitions['header'] = ['type' => 'info', 'raw' => true, 'default' => $template->get('header') ?: $template->get('formheader'), 'weight' => -110]; } if ($this->mEntryError) { $fieldDefinitions['entryError'] = ['type' => 'info', 'default' => Html::rawElement('div', ['class' => $this->mEntryErrorType . 'box'], $this->mEntryError), 'raw' => true, 'rawrow' => true, 'weight' => -100]; } if (!$this->showExtraInformation()) { unset($fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend']); } if ($this->isSignup() && $this->showExtraInformation()) { // blank signup footer for site customization // uses signupend-https for HTTPS requests if it's not blank, signupend otherwise $signupendMsg = $this->msg('signupend'); $signupendHttpsMsg = $this->msg('signupend-https'); if (!$signupendMsg->isDisabled()) { $usingHTTPS = $this->getRequest()->getProtocol() === 'https'; $signupendText = $usingHTTPS && !$signupendHttpsMsg->isBlank() ? $signupendHttpsMsg->parse() : $signupendMsg->parse(); $fieldDefinitions['signupend'] = ['type' => 'info', 'raw' => true, 'default' => Html::rawElement('div', ['id' => 'signupend'], $signupendText), 'weight' => 225]; } } if (!$this->isSignup() && $this->showExtraInformation()) { $passwordReset = new PasswordReset($this->getConfig(), AuthManager::singleton()); if ($passwordReset->isAllowed($this->getUser())->isGood()) { $fieldDefinitions['passwordReset'] = ['type' => 'info', 'raw' => true, 'cssclass' => 'mw-form-related-link-container', 'default' => Linker::link(SpecialPage::getTitleFor('PasswordReset'), $this->msg('userlogin-resetpassword-link')->escaped()), 'weight' => 230]; } // Don't show a "create account" link if the user can't. if ($this->showCreateAccountLink()) { // link to the other action $linkTitle = $this->getTitleFor($this->isSignup() ? 'Userlogin' : 'CreateAccount'); $linkq = $this->getReturnToQueryStringFragment(); // Pass any language selection on to the mode switch link if ($wgLoginLanguageSelector && $this->mLanguage) { $linkq .= '&uselang=' . $this->mLanguage; } $loggedIn = $this->getUser()->isLoggedIn(); $fieldDefinitions['createOrLogin'] = ['type' => 'info', 'raw' => true, 'linkQuery' => $linkq, 'default' => function ($params) use($loggedIn, $linkTitle) { return Html::rawElement('div', ['id' => 'mw-createaccount' . (!$loggedIn ? '-cta' : ''), 'class' => $loggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field'], ($loggedIn ? '' : $this->msg('userlogin-noaccount')->escaped()) . Html::element('a', ['id' => 'mw-createaccount-join' . ($loggedIn ? '-loggedin' : ''), 'href' => $linkTitle->getLocalURL($params['linkQuery']), 'class' => $loggedIn ? '' : 'mw-ui-button', 'tabindex' => 100], $this->msg($loggedIn ? 'userlogin-createanother' : 'userlogin-joinproject')->escaped())); }, 'weight' => 235]; } } $fieldDefinitions = $this->getBCFieldDefinitions($fieldDefinitions, $template); $fieldDefinitions = array_filter($fieldDefinitions); return $fieldDefinitions; }