diff --git a/com.woltlab.wcf/templates/login.tpl b/com.woltlab.wcf/templates/login.tpl index 0aeba5f161..3957784e1c 100644 --- a/com.woltlab.wcf/templates/login.tpl +++ b/com.woltlab.wcf/templates/login.tpl @@ -8,67 +8,6 @@ {lang}wcf.user.login.forceLogin{/lang} {/if} -{if !$errorField|empty && $errorField == 'cookie'} - {lang}wcf.user.login.error.cookieRequired{/lang} -{else} - {include file='shared_formError'} -{/if} - -
- -
- * -
-
- - {if $errorField == 'username'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.user.username.error.{@$errorType}{/lang} - {/if} - - {/if} -
- - - -
- * -
-
- - {if $errorField == 'password'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.user.password.error.{@$errorType}{/lang} - {/if} - - {/if} - {if $__userAuthConfig->canChangePassword} - {lang}wcf.user.lostPassword{/lang} - {/if} -
- - - {event name='fields'} - - {include file='shared_captcha' supportsAsyncCaptcha=true} - -
- - {csrfToken} -
- - {include file='thirdPartySsoButtons'} - - -

- * - {lang}wcf.global.form.required{/lang} -

+{unsafe:$form->getHtml()} {include file='authFlowFooter'} diff --git a/wcfsetup/install/files/acp/templates/login.tpl b/wcfsetup/install/files/acp/templates/login.tpl index 5ce11fdf8c..c116232b96 100644 --- a/wcfsetup/install/files/acp/templates/login.tpl +++ b/wcfsetup/install/files/acp/templates/login.tpl @@ -6,60 +6,6 @@ -{if !$errorField|empty && $errorField == 'cookie'} - {lang}wcf.user.login.error.cookieRequired{/lang} -{else} - {include file='shared_formError'} -{/if} - -
- -
- * -
-
- - {if $errorField == 'username'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.user.username.error.{@$errorType}{/lang} - {/if} - - {/if} -
- - - -
- * -
-
- - {if $errorField == 'password'} - - {if $errorType == 'empty'} - {lang}wcf.global.form.error.empty{/lang} - {else} - {lang}wcf.user.password.error.{@$errorType}{/lang} - {/if} - - {/if} -
- - - {include file='shared_captcha' supportsAsyncCaptcha=true} - -
- - {csrfToken} -
- - -

- * - {lang}wcf.global.form.required{/lang} -

+{unsafe:$form->getHtml()} {include file='footer'} diff --git a/wcfsetup/install/files/lib/acp/form/LoginForm.class.php b/wcfsetup/install/files/lib/acp/form/LoginForm.class.php index 7cc5bb72a8..8e2e23f880 100755 --- a/wcfsetup/install/files/lib/acp/form/LoginForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/LoginForm.class.php @@ -7,12 +7,19 @@ use wcf\data\user\User; use wcf\data\user\UserProfile; use wcf\event\user\authentication\UserLoggedIn; -use wcf\form\AbstractCaptchaForm; +use wcf\form\AbstractForm; +use wcf\form\AbstractFormBuilderForm; use wcf\system\event\EventHandler; use wcf\system\exception\NamedUserException; use wcf\system\exception\UserInputException; +use wcf\system\form\builder\field\CaptchaFormField; +use wcf\system\form\builder\field\PasswordFormField; +use wcf\system\form\builder\field\TextFormField; +use wcf\system\form\builder\field\validation\FormFieldValidationError; +use wcf\system\form\builder\field\validation\FormFieldValidator; use wcf\system\request\LinkHandler; use wcf\system\request\RequestHandler; +use wcf\system\user\authentication\DefaultUserAuthentication; use wcf\system\user\authentication\EmailUserAuthentication; use wcf\system\user\authentication\LoginRedirect; use wcf\system\user\authentication\UserAuthenticationFactory; @@ -28,31 +35,48 @@ * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License */ -class LoginForm extends AbstractCaptchaForm +class LoginForm extends AbstractFormBuilderForm { - /** - * given login username - * @var string - */ - public $username = ''; + protected bool $useCaptcha = false; + protected ?User $user = null; - /** - * given login password - * @var string - */ - public $password = ''; + #[\Override] + protected function createForm() + { + parent::createForm(); + + $this->form->appendChildren([ + TextFormField::create('username') + ->label('wcf.user.usernameOrEmail') + ->required() + ->autoFocus() + ->maximumLength(255), + PasswordFormField::create('password') + ->label('wcf.user.password') + ->required() + ->passwordStrengthMeter(false) + ->removeFieldClass('medium') + ->addFieldClass('long') + ->autocomplete("current-password") + ->addValidator(new FormFieldValidator( + 'passwordValidator', + $this->validatePassword(...) + )), + CaptchaFormField::create() + ->available($this->useCaptcha) + ->objectType(CAPTCHA_TYPE) + ]); + } - /** - * user object - * @var User - */ - public $user; + #[\Override] + public function finalizeForm() + { + parent::finalizeForm(); - /** - * @inheritDoc - */ - public $useCaptcha = false; + $this->renameSubmitButton(); + } + #[\Override] public function __run() { WCF::getTPL()->assign([ @@ -62,9 +86,7 @@ public function __run() return parent::__run(); } - /** - * @inheritDoc - */ + #[\Override] public function readParameters() { parent::readParameters(); @@ -107,120 +129,127 @@ public function readParameters() } } - /** - * @inheritDoc - */ - public function readFormParameters() + protected function validatePassword(PasswordFormField $passwordFormField): void { - parent::readFormParameters(); - - if (isset($_POST['username'])) { - $this->username = StringUtil::trim($_POST['username']); - } - if (isset($_POST['password'])) { - $this->password = $_POST['password']; - } - } + $usernameFormField = $this->form->getNodeById('username'); + \assert($usernameFormField instanceof TextFormField); + $validationError = null; - /** - * Validates the user access data. - */ - protected function validateUser() - { try { $this->user = UserAuthenticationFactory::getInstance() ->getUserAuthentication() - ->loginManually($this->username, $this->password); + ->loginManually($usernameFormField->getValue(), $passwordFormField->getValue()); } catch (UserInputException $e) { - if ($e->getField() == 'username') { + $validationError = $e; + + if ($e->getField() === 'username') { try { - $this->user = EmailUserAuthentication::getInstance() - ->loginManually($this->username, $this->password); - } catch (UserInputException $e2) { - if ($e2->getField() == 'username') { - throw $e; + $user = $this->tryAuthenticationByEmail($usernameFormField->getValue(), $passwordFormField->getValue()); + if ($user !== null) { + $this->user = $user; + $validationError = null; + } + } catch (UserInputException $emailException) { + // The attempt to use the email address as login username is + // only implicit, therefore we only use the inner exception + // if the error is about an incorrect password. + if ($emailException->getField() !== 'username') { + $validationError = $emailException; } - throw $e2; } - } else { - throw $e; } } - } - /** - * @inheritDoc - */ - public function submit() - { - parent::submit(); + if ($validationError !== null) { + if ($validationError->getField() == 'username') { + $usernameFormField->addValidationError( + new FormFieldValidationError( + $validationError->getType(), + 'wcf.user.username.error.' . $validationError->getType(), + [ + 'username' => $usernameFormField->getValue(), + ] + ) + ); + } else if ($validationError->getField() == 'password') { + $passwordFormField->addValidationError( + new FormFieldValidationError( + $validationError->getType(), + 'wcf.user.password.error.' . $validationError->getType() + ) + ); + } else { + throw new \LogicException('unreachable'); + } - // save authentication failure - if (ENABLE_USER_AUTHENTICATION_FAILURE) { - if ($this->errorField == 'username' || $this->errorField == 'password') { - $user = User::getUserByUsername($this->username); - if (!$user->userID) { - $user = User::getUserByEmail($this->username); - } + $this->saveAuthenticationFailure($validationError->getField(), $usernameFormField->getValue()); + } - $action = new UserAuthenticationFailureAction([], 'create', [ - 'data' => [ - 'environment' => RequestHandler::getInstance()->isACPRequest() ? 'admin' : 'user', - 'userID' => $user->userID ?: null, - 'username' => \mb_substr($this->username, 0, 100), - 'time' => TIME_NOW, - 'ipAddress' => UserUtil::getIpAddress(), - 'userAgent' => UserUtil::getUserAgent(), - 'validationError' => 'invalid' . \ucfirst($this->errorField), - ], - ]); - $action->executeAction(); - - if ($this->captchaObjectType) { - $this->captchaObjectType->getProcessor()->reset(); - } + if (RequestHandler::getInstance()->isACPRequest() && $this->user !== null) { + $userProfile = new UserProfile($this->user); + if (!$userProfile->getPermission('admin.general.canUseAcp')) { + $usernameFormField->addValidationError( + new FormFieldValidationError( + 'acpNotAuthorized', + 'wcf.user.username.error.acpNotAuthorized', + [ + 'username' => $usernameFormField->getValue(), + ] + ) + ); } } - } - - /** - * @inheritDoc - */ - public function validate() - { - parent::validate(); if (!WCF::getSession()->hasValidCookie()) { - throw new UserInputException('cookie'); + $this->form->invalid(); + $this->form->errorMessage('wcf.user.login.error.cookieRequired'); } + } - // error handling - if (empty($this->username)) { - throw new UserInputException('username'); + protected function tryAuthenticationByEmail( + string $username, + #[\SensitiveParameter] string $password + ): ?User { + $defaultAuthentication = UserAuthenticationFactory::getInstance()->getUserAuthentication(); + if (\get_class($defaultAuthentication) !== DefaultUserAuthentication::class) { + // The email fallback is only supported for the built-in + // authentication method. + return null; } - if (empty($this->password)) { - throw new UserInputException('password'); - } + return EmailUserAuthentication::getInstance()->loginManually($username, $password); + } - $this->validateUser(); + protected function saveAuthenticationFailure(string $errorField, string $username): void + { + if (!ENABLE_USER_AUTHENTICATION_FAILURE) { + return; + } - if (RequestHandler::getInstance()->isACPRequest() && $this->user !== null) { - $userProfile = new UserProfile($this->user); - if (!$userProfile->getPermission('admin.general.canUseAcp')) { - throw new UserInputException('username', 'acpNotAuthorized'); - } + $user = User::getUserByUsername($username); + if (!$user->userID) { + $user = User::getUserByEmail($username); } + + $action = new UserAuthenticationFailureAction([], 'create', [ + 'data' => [ + 'environment' => RequestHandler::getInstance()->isACPRequest() ? 'admin' : 'user', + 'userID' => $user->userID ?: null, + 'username' => \mb_substr($username, 0, 100), + 'time' => TIME_NOW, + 'ipAddress' => UserUtil::getIpAddress(), + 'userAgent' => UserUtil::getUserAgent(), + 'validationError' => 'invalid' . \ucfirst($errorField), + ], + ]); + $action->executeAction(); } - /** - * @inheritDoc - */ + #[\Override] public function save() { - parent::save(); + AbstractForm::save(); - // change user $needsMultifactor = WCF::getSession()->changeUserAfterMultifactorAuthentication($this->user); if (!$needsMultifactor) { WCF::getSession()->registerReauthentication(); @@ -237,7 +266,7 @@ public function save() /** * Performs the redirect after successful authentication. */ - protected function performRedirect(bool $needsMultifactor = false) + protected function performRedirect(bool $needsMultifactor = false): void { if ($needsMultifactor) { $url = LinkHandler::getInstance()->getLink('MultifactorAuthentication'); @@ -250,17 +279,8 @@ protected function performRedirect(bool $needsMultifactor = false) exit; } - /** - * @inheritDoc - */ - public function assignVariables() + private function renameSubmitButton(): void { - parent::assignVariables(); - - WCF::getTPL()->assign([ - 'username' => $this->username, - 'password' => $this->password, - 'loginController' => LinkHandler::getInstance()->getControllerLink(static::class), - ]); + $this->form->getButton('submitButton')->label('wcf.user.button.login'); } } diff --git a/wcfsetup/install/files/lib/form/LoginForm.class.php b/wcfsetup/install/files/lib/form/LoginForm.class.php index ecdf794159..6e3c6520a3 100644 --- a/wcfsetup/install/files/lib/form/LoginForm.class.php +++ b/wcfsetup/install/files/lib/form/LoginForm.class.php @@ -4,6 +4,7 @@ use wcf\event\user\authentication\UserLoggedIn; use wcf\system\event\EventHandler; +use wcf\system\form\builder\TemplateFormNode; use wcf\system\WCF; /** @@ -17,14 +18,23 @@ class LoginForm extends \wcf\acp\form\LoginForm { const AVAILABLE_DURING_OFFLINE_MODE = true; + #[\Override] + protected function createForm() + { + parent::createForm(); + + $this->form->appendChild( + TemplateFormNode::create('thirdPartySsoButtons') + ->templateName('thirdPartySsoButtons') + ); + } + #[\Override] public function save() { AbstractForm::save(); - // change user $needsMultifactor = WCF::getSession()->changeUserAfterMultifactorAuthentication($this->user); - if (!$needsMultifactor) { EventHandler::getInstance()->fire( new UserLoggedIn($this->user) diff --git a/wcfsetup/install/files/style/ui/authFlow.scss b/wcfsetup/install/files/style/ui/authFlow.scss index 9a6d2bc3b3..75afd49960 100644 --- a/wcfsetup/install/files/style/ui/authFlow.scss +++ b/wcfsetup/install/files/style/ui/authFlow.scss @@ -123,3 +123,13 @@ } } } + +/* This code is necessary to move the third-party buttons in the login form below the submit button. */ +form#login { + display: flex; + flex-direction: column; + + .authOtherOptionButtons { + order: 1; + } +}