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}
-
-
-
-
- *
- {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}
-
-
-
-
- *
- {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;
+ }
+}