Migrate login form to a form builder form
authorMarcel Werk <burntime@woltlab.com>
Sat, 7 Dec 2024 14:28:45 +0000 (15:28 +0100)
committerMarcel Werk <burntime@woltlab.com>
Sat, 7 Dec 2024 14:28:45 +0000 (15:28 +0100)
com.woltlab.wcf/templates/login.tpl
wcfsetup/install/files/acp/templates/login.tpl
wcfsetup/install/files/lib/acp/form/LoginForm.class.php
wcfsetup/install/files/lib/form/LoginForm.class.php
wcfsetup/install/files/style/ui/authFlow.scss

index 0aeba5f161bcbea7b8941b82eb6353c51df00ddc..3957784e1cbd83ee1ce177eb557214949065e8bf 100644 (file)
@@ -8,67 +8,6 @@
        <woltlab-core-notice type="info">{lang}wcf.user.login.forceLogin{/lang}</woltlab-core-notice>
 {/if}
 
-{if !$errorField|empty && $errorField == 'cookie'}
-       <woltlab-core-notice type="error">{lang}wcf.user.login.error.cookieRequired{/lang}</woltlab-core-notice>
-{else}
-       {include file='shared_formError'}
-{/if}
-
-<form id="loginForm" method="post" action="{$loginController}">
-       <dl{if $errorField == 'username'} class="formError"{/if}>
-               <dt>
-                       <label for="username">{lang}wcf.user.usernameOrEmail{/lang}</label> <span class="formFieldRequired">*</span>
-               </dt>
-               <dd>
-                       <input type="text" id="username" name="username" value="{$username}" required autofocus class="long" autocomplete="username">
-                       {if $errorField == 'username'}
-                               <small class="innerError">
-                                       {if $errorType == 'empty'}
-                                               {lang}wcf.global.form.error.empty{/lang}
-                                       {else}
-                                               {lang}wcf.user.username.error.{@$errorType}{/lang}
-                                       {/if}
-                               </small>
-                       {/if}
-               </dd>
-       </dl>
-       
-       <dl{if $errorField == 'password'} class="formError"{/if}>
-               <dt>
-                       <label for="password">{lang}wcf.user.password{/lang}</label> <span class="formFieldRequired">*</span>
-               </dt>
-               <dd>
-                       <input type="password" id="password" name="password" value="{$password}" required class="long" autocomplete="current-password">
-                       {if $errorField == 'password'}
-                               <small class="innerError">
-                                       {if $errorType == 'empty'}
-                                               {lang}wcf.global.form.error.empty{/lang}
-                                       {else}
-                                               {lang}wcf.user.password.error.{@$errorType}{/lang}
-                                       {/if}
-                               </small>
-                       {/if}
-                       {if $__userAuthConfig->canChangePassword}
-                               <small><a href="{link controller='LostPassword'}{/link}">{lang}wcf.user.lostPassword{/lang}</a></small>
-                       {/if}
-               </dd>
-       </dl>
-       
-       {event name='fields'}
-
-       {include file='shared_captcha' supportsAsyncCaptcha=true}
-
-       <div class="formSubmit">
-               <input type="submit" value="{lang}wcf.user.button.login{/lang}" accesskey="s">
-               {csrfToken}
-       </div>
-
-       {include file='thirdPartySsoButtons'}
-</form>
-
-<p class="formFieldRequiredNotice">
-       <span class="formFieldRequired">*</span>
-       {lang}wcf.global.form.required{/lang}
-</p>
+{unsafe:$form->getHtml()}
 
 {include file='authFlowFooter'}
index 5ce11fdf8c8ed8a024c87dc7d371d7919a8290d8..c116232b96a188c5101a4e7576b82c1b58bcfa26 100644 (file)
@@ -6,60 +6,6 @@
        </div>
 </header>
 
-{if !$errorField|empty && $errorField == 'cookie'}
-       <woltlab-core-notice type="error">{lang}wcf.user.login.error.cookieRequired{/lang}</woltlab-core-notice>
-{else}
-       {include file='shared_formError'}
-{/if}
-
-<form id="loginForm" method="post" action="{$loginController}">
-       <dl{if $errorField == 'username'} class="formError"{/if}>
-               <dt>
-                       <label for="username">{lang}wcf.user.usernameOrEmail{/lang}</label> <span class="formFieldRequired">*</span>
-               </dt>
-               <dd>
-                       <input type="text" id="username" name="username" value="{$username}" required autofocus class="long" autocomplete="username">
-                       {if $errorField == 'username'}
-                               <small class="innerError">
-                                       {if $errorType == 'empty'}
-                                               {lang}wcf.global.form.error.empty{/lang}
-                                       {else}
-                                               {lang}wcf.user.username.error.{@$errorType}{/lang}
-                                       {/if}
-                               </small>
-                       {/if}
-               </dd>
-       </dl>
-       
-       <dl{if $errorField == 'password'} class="formError"{/if}>
-               <dt>
-                       <label for="password">{lang}wcf.user.password{/lang}</label> <span class="formFieldRequired">*</span>
-               </dt>
-               <dd>
-                       <input type="password" id="password" name="password" value="{$password}" required class="long" autocomplete="current-password">
-                       {if $errorField == 'password'}
-                               <small class="innerError">
-                                       {if $errorType == 'empty'}
-                                               {lang}wcf.global.form.error.empty{/lang}
-                                       {else}
-                                               {lang}wcf.user.password.error.{@$errorType}{/lang}
-                                       {/if}
-                               </small>
-                       {/if}
-               </dd>
-       </dl>
-
-       {include file='shared_captcha' supportsAsyncCaptcha=true}
-
-       <div class="formSubmit">
-               <input type="submit" value="{lang}wcf.user.button.login{/lang}" accesskey="s">
-               {csrfToken}
-       </div>
-</form>
-
-<p class="formFieldRequiredNotice">
-       <span class="formFieldRequired">*</span>
-       {lang}wcf.global.form.required{/lang}
-</p>
+{unsafe:$form->getHtml()}
 
 {include file='footer'}
index 7cc5bb72a8556a66703eeadaf2e745cb3920b71e..cfc495aa16a580166f1cb8cc9f30717bdce1d29a 100755 (executable)
@@ -7,12 +7,19 @@ use wcf\data\user\authentication\failure\UserAuthenticationFailureAction;
 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 @@ use wcf\util\UserUtil;
  * @copyright   2001-2019 WoltLab GmbH
  * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  */
-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 @@ class LoginForm extends AbstractCaptchaForm
         return parent::__run();
     }
 
-    /**
-     * @inheritDoc
-     */
+    #[\Override]
     public function readParameters()
     {
         parent::readParameters();
@@ -107,120 +129,112 @@ class LoginForm extends AbstractCaptchaForm
         }
     }
 
-    /**
-     * @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);
+        $handleException = 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') {
+            if (
+                \get_class(UserAuthenticationFactory::getInstance()->getUserAuthentication()) === DefaultUserAuthentication::class
+                && $e->getField() == 'username'
+            ) {
                 try {
                     $this->user = EmailUserAuthentication::getInstance()
-                        ->loginManually($this->username, $this->password);
+                        ->loginManually($usernameFormField->getValue(), $passwordFormField->getValue());
                 } catch (UserInputException $e2) {
                     if ($e2->getField() == 'username') {
-                        throw $e;
+                        $handleException = $e;
+                    } else {
+                        $handleException = $e2;
                     }
-                    throw $e2;
                 }
             } else {
-                throw $e;
+                $handleException = $e;
             }
         }
-    }
 
-    /**
-     * @inheritDoc
-     */
-    public function submit()
-    {
-        parent::submit();
-
-        // 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);
-                }
+        if ($handleException !== null) {
+            if ($handleException->getField() == 'username') {
+                $usernameFormField->addValidationError(
+                    new FormFieldValidationError(
+                        $handleException->getType(),
+                        'wcf.user.username.error.' . $handleException->getType(),
+                        [
+                            'username' => $usernameFormField->getValue(),
+                        ]
+                    )
+                );
+            } else if ($handleException->getField() == 'password') {
+                $passwordFormField->addValidationError(
+                    new FormFieldValidationError(
+                        $handleException->getType(),
+                        'wcf.user.password.error.' . $handleException->getType()
+                    )
+                );
+            } else {
+                throw new \LogicException('unreachable');
+            }
 
-                $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();
+            $this->saveAuthenticationFailure($handleException->getField(), $usernameFormField->getValue());
+        }
 
-                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 saveAuthenticationFailure(string $errorField, string $username): void
+    {
+        if (!ENABLE_USER_AUTHENTICATION_FAILURE) {
+            return;
         }
 
-        if (empty($this->password)) {
-            throw new UserInputException('password');
+        $user = User::getUserByUsername($username);
+        if (!$user->userID) {
+            $user = User::getUserByEmail($username);
         }
 
-        $this->validateUser();
-
-        if (RequestHandler::getInstance()->isACPRequest() && $this->user !== null) {
-            $userProfile = new UserProfile($this->user);
-            if (!$userProfile->getPermission('admin.general.canUseAcp')) {
-                throw new UserInputException('username', 'acpNotAuthorized');
-            }
-        }
+        $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 +251,7 @@ class LoginForm extends AbstractCaptchaForm
     /**
      * 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 +264,8 @@ class LoginForm extends AbstractCaptchaForm
         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');
     }
 }
index ecdf7941592ddb1e63cff88df5c67b622eea43c8..6e3c6520a3b356a46be4b1e1164939e9f76df2cf 100644 (file)
@@ -4,6 +4,7 @@ namespace wcf\form;
 
 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)
index 9a6d2bc3b3f7d0bf93d113d841fa9f31459e5e47..75afd4996011e6cadc89d4d8741663677b145f04 100644 (file)
                }
        }
 }
+
+/* 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;
+       }
+}