Add multifactor\Setup class for stronger typing
authorTim Düsterhus <duesterhus@woltlab.com>
Wed, 11 Nov 2020 14:59:19 +0000 (15:59 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Mon, 16 Nov 2020 16:29:06 +0000 (17:29 +0100)
com.woltlab.wcf/templates/multifactorAuthentication.tpl
wcfsetup/install/files/lib/data/user/User.class.php
wcfsetup/install/files/lib/form/MultifactorAuthenticationForm.class.php
wcfsetup/install/files/lib/form/MultifactorManageForm.class.php
wcfsetup/install/files/lib/page/AccountSecurityPage.class.php
wcfsetup/install/files/lib/system/request/LinkHandler.class.php
wcfsetup/install/files/lib/system/user/multifactor/BackupMultifactorMethod.class.php
wcfsetup/install/files/lib/system/user/multifactor/IMultifactorMethod.class.php
wcfsetup/install/files/lib/system/user/multifactor/Setup.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/user/multifactor/TotpMultifactorMethod.class.php

index 2570189b5f95653717c59c2a06a72a3173678f0d..fbcefda83b395995e40bfc13ec3474b09bef9220 100644 (file)
@@ -5,9 +5,9 @@
                <div class="boxContent">
                        <nav>
                                <ol class="boxMenu">
-                                       {foreach from=$methods key='_setupId' item='method'}
-                                               <li{if $setupId == $_setupId} class="active"{/if}>
-                                                       <a class="boxMenuLink" href="{link controller='MultifactorAuthentication' id=$_setupId}{/link}"><span class="boxMenuLinkTitle">{lang}wcf.user.security.multifactor.{$method->objectType}{/lang}</span></a>
+                                       {foreach from=$setups item='_setup'}
+                                               <li{if $setup->getId() == $_setup->getId()} class="active"{/if}>
+                                                       <a class="boxMenuLink" href="{link controller='MultifactorAuthentication' object=$_setup}{/link}"><span class="boxMenuLinkTitle">{lang}wcf.user.security.multifactor.{$_setup->getObjectType()->objectType}{/lang}</span></a>
                                                </li>
                                        {/foreach}
                                </ol>
index 5fab3dfc3336acad5e644cb996ed249a10b03908..8bc0e834c6743cb81f8b43ac966fa22c07b053c3 100644 (file)
@@ -5,8 +5,6 @@ use wcf\data\language\Language;
 use wcf\data\user\group\UserGroup;
 use wcf\data\DatabaseObject;
 use wcf\data\IUserContent;
-use wcf\data\object\type\ObjectType;
-use wcf\data\object\type\ObjectTypeCache;
 use wcf\data\user\option\UserOption;
 use wcf\system\cache\builder\UserOptionCacheBuilder;
 use wcf\system\language\LanguageFactory;
@@ -692,27 +690,6 @@ final class User extends DatabaseObject implements IPopoverObject, IRouteControl
                return REGISTER_ACTIVATION_METHOD & self::REGISTER_ACTIVATION_USER;
        }
        
-       /**
-        * Returns the multi-factor methods the user set up.
-        * 
-        * @return      ObjectType[]
-        * @since       5.4
-        */
-       public function getEnabledMultifactorMethods(): array {
-               $sql = "SELECT  *
-                       FROM    wcf".WCF_N."_user_multifactor
-                       WHERE   userID = ?";
-               $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute([$this->userID]);
-               
-               $methods = [];
-               while ($row = $statement->fetchArray()) {
-                       $methods[$row['setupID']] = ObjectTypeCache::getInstance()->getObjectType($row['objectTypeID']);
-               }
-               
-               return $methods;
-       }
-       
        /**
         * @inheritDoc
         */
index 3febf050c6935d9959127ce98087e92084b0a911..5f1ea18ad818f94ffff266b5f865b4a7ead37748 100644 (file)
@@ -7,6 +7,7 @@ use wcf\system\exception\IllegalLinkException;
 use wcf\system\exception\PermissionDeniedException;
 use wcf\system\request\LinkHandler;
 use wcf\system\user\multifactor\IMultifactorMethod;
+use wcf\system\user\multifactor\Setup;
 use wcf\system\WCF;
 
 /**
@@ -32,9 +33,9 @@ class MultifactorAuthenticationForm extends AbstractFormBuilderForm {
        private $user;
        
        /**
-        * @var ObjectType[]
+        * @var Setup[]
         */
-       private $methods;
+       private $setups;
        
        /**
         * @var ObjectType
@@ -47,9 +48,9 @@ class MultifactorAuthenticationForm extends AbstractFormBuilderForm {
        private $processor;
        
        /**
-        * @var int
+        * @var Setup
         */
-       private $setupId;
+       private $setup;
        
        /**
         * @inheritDoc
@@ -66,26 +67,27 @@ class MultifactorAuthenticationForm extends AbstractFormBuilderForm {
                        throw new PermissionDeniedException();
                }
                
-               $this->methods = $this->user->getEnabledMultifactorMethods();
+               $this->setups = Setup::getAllForUser($this->user);
                
-               if (empty($this->methods)) {
+               if (empty($this->setups)) {
                        throw new \LogicException('Unreachable');
                }
                
-               \uasort($this->methods, function (ObjectType $a, ObjectType $b) {
-                       return $b->priority <=> $a->priority;
+               \uasort($this->setups, function (Setup $a, Setup $b) {
+                       return $b->getObjectType()->priority <=> $a->getObjectType()->priority;
                });
                
-               $this->setupId = \array_keys($this->methods)[0];
+               $setupId = \array_keys($this->setups)[0];
                if (isset($_GET['id'])) {
-                       $this->setupId = $_GET['id'];
+                       $setupId = $_GET['id'];
                }
                
-               if (!isset($this->methods[$this->setupId])) {
+               if (!isset($this->setups[$setupId])) {
                        throw new IllegalLinkException();
                }
                
-               $this->method = $this->methods[$this->setupId];
+               $this->setup = $this->setups[$setupId];
+               $this->method = $this->setup->getObjectType();
                \assert($this->method->getDefinition()->definitionName === 'com.woltlab.wcf.multifactor');
                
                $this->processor = $this->method->getProcessor();
@@ -97,7 +99,7 @@ class MultifactorAuthenticationForm extends AbstractFormBuilderForm {
        protected function createForm() {
                parent::createForm();
                
-               $this->processor->createAuthenticationForm($this->form, $this->setupId);
+               $this->processor->createAuthenticationForm($this->form, $this->setup);
        }
        
        public function save() {
@@ -105,7 +107,9 @@ class MultifactorAuthenticationForm extends AbstractFormBuilderForm {
                
                WCF::getDB()->beginTransaction();
                
-               $this->returnData = $this->processor->processAuthenticationForm($this->form, $this->setupId);
+               $setup = $this->setup->lock();
+               
+               $this->returnData = $this->processor->processAuthenticationForm($this->form, $setup);
                
                WCF::getDB()->commitTransaction();
                
@@ -132,7 +136,7 @@ class MultifactorAuthenticationForm extends AbstractFormBuilderForm {
         */
        protected function setFormAction() {
                $this->form->action(LinkHandler::getInstance()->getControllerLink(static::class, [
-                       'id' => $this->setupId,
+                       'object' => $this->setup,
                ]));
        }
        
@@ -143,10 +147,9 @@ class MultifactorAuthenticationForm extends AbstractFormBuilderForm {
                parent::assignVariables();
                
                WCF::getTPL()->assign([
-                       'method' => $this->method,
-                       'methods' => $this->methods,
+                       'setups' => $this->setups,
                        'user' => $this->user,
-                       'setupId' => $this->setupId,
+                       'setup' => $this->setup,
                ]);
        }
 }
index 2967cfab09e22413fc748a3e5f01cdcdd2c1b1bc..c3b706c2a8ba9c7851529aad7984c8fc0ec7fe76 100644 (file)
@@ -7,6 +7,7 @@ use wcf\system\exception\IllegalLinkException;
 use wcf\system\menu\user\UserMenu;
 use wcf\system\request\LinkHandler;
 use wcf\system\user\multifactor\IMultifactorMethod;
+use wcf\system\user\multifactor\Setup;
 use wcf\system\WCF;
 
 /**
@@ -40,9 +41,9 @@ class MultifactorManageForm extends AbstractFormBuilderForm {
        private $processor;
        
        /**
-        * @var int
+        * @var ?Setup
         */
-       private $setupId;
+       private $setup;
        
        /**
         * @var mixed
@@ -70,17 +71,7 @@ class MultifactorManageForm extends AbstractFormBuilderForm {
                
                $this->method = $objectType;
                $this->processor = $this->method->getProcessor();
-               
-               $sql = "SELECT  setupID
-                       FROM    wcf".WCF_N."_user_multifactor
-                       WHERE   userID = ?
-                               AND objectTypeID = ?";
-               $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute([
-                       WCF::getUser()->userID,
-                       $this->method->objectTypeID,
-               ]);
-               $this->setupId = $statement->fetchSingleColumn();
+               $this->setup = Setup::find($this->method, WCF::getUser());
        }
        
        /**
@@ -89,7 +80,7 @@ class MultifactorManageForm extends AbstractFormBuilderForm {
        protected function createForm() {
                parent::createForm();
                
-               $this->processor->createManagementForm($this->form, $this->setupId, $this->returnData);
+               $this->processor->createManagementForm($this->form, $this->setup, $this->returnData);
        }
        
        public function save() {
@@ -97,62 +88,27 @@ class MultifactorManageForm extends AbstractFormBuilderForm {
 
                WCF::getDB()->beginTransaction();
                
-               /** @var int|null $setupId */
-               $setupId = null;
-               if ($this->setupId) {
-                       $setupId = $this->lockSetup($this->setupId);
+               /** @var Setup|null $setup */
+               $setup = null;
+               if ($this->setup) {
+                       $setup = $this->setup->lock();
                }
                else {
-                       $setupId = $this->allocateSetUpId($this->method->objectTypeID);
+                       $setup = Setup::allocateSetUpId($this->method, WCF::getUser());
                }
                
-               if (!$setupId) {
+               if (!$setup) {
                        throw new \RuntimeException("Multifactor setup disappeared");
                }
                
-               $this->returnData = $this->processor->processManagementForm($this->form, $setupId);
+               $this->returnData = $this->processor->processManagementForm($this->form, $setup);
                
-               $this->setupId = $setupId;
+               $this->setup = $setup;
                WCF::getDB()->commitTransaction();
                
                $this->saved();
        }
        
-       /**
-        * Locks the set up, preventing any concurrent changes.
-        */
-       protected function lockSetup(int $setupId): int {
-               $sql = "SELECT  setupId
-                       FROM    wcf".WCF_N."_user_multifactor
-                       WHERE   setupId = ?
-                       FOR UPDATE";
-               $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute([
-                       $setupId,
-               ]);
-               
-               $dbSetupId = \intval($statement->fetchSingleColumn());
-               assert($setupId === $dbSetupId);
-               
-               return $dbSetupId;
-       }
-       
-       /**
-        * Allocates a fresh setup ID for the given objectTypeID.
-        */
-       protected function allocateSetUpId(int $objectTypeID): int {
-               $sql = "INSERT INTO     wcf".WCF_N."_user_multifactor
-                                       (userID, objectTypeID)
-                       VALUES          (?, ?)";
-               $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute([
-                       WCF::getUser()->userID,
-                       $objectTypeID,
-               ]);
-               
-               return \intval(WCF::getDB()->getInsertID("wcf".WCF_N."_user_multifactor", 'setupID'));
-       }
-       
        /**
         * @inheritDoc
         */
index 95f6b05565517d0995e10da41a1145cc69c51194..66cb59f04b5f97743bcd2e7fcc2ab3992cd471a4 100644 (file)
@@ -5,6 +5,7 @@ use wcf\data\object\type\ObjectTypeCache;
 use wcf\system\menu\user\UserMenu;
 use wcf\system\session\Session;
 use wcf\system\session\SessionHandler;
+use wcf\system\user\multifactor\Setup;
 use wcf\system\WCF;
 
 /**
@@ -33,7 +34,7 @@ class AccountSecurityPage extends AbstractPage {
        private $multifactorMethods;
        
        /**
-        * @var int[]
+        * @var Setup[]
         */
        private $enabledMultifactorMethods;
        
@@ -55,9 +56,10 @@ class AccountSecurityPage extends AbstractPage {
                        return $b->priority <=> $a->priority;
                });
                
-               $this->enabledMultifactorMethods = array_flip(array_map(function (ObjectType $o) {
-                       return $o->objectTypeID;
-               }, WCF::getUser()->getEnabledMultifactorMethods()));
+               $setups = Setup::getAllForUser(WCF::getUser());
+               foreach ($setups as $setup) {
+                       $this->enabledMultifactorMethods[$setup->getObjectType()->objectTypeID] = $setup;
+               }
        }
        
        /**
index 4b242662516c8689e5face609cf9270235781f34..990a4a6a147bd144b5744eecac8a0a74015431e6 100644 (file)
@@ -2,6 +2,7 @@
 namespace wcf\system\request;
 use wcf\data\page\PageCache;
 use wcf\data\DatabaseObjectDecorator;
+use wcf\data\IIDObject;
 use wcf\system\application\ApplicationHandler;
 use wcf\system\language\LanguageFactory;
 use wcf\system\Regex;
@@ -193,6 +194,9 @@ class LinkHandler extends SingletonFactory {
                                $parameters['id'] = $parameters['object']->getObjectID();
                                $parameters['title'] = $parameters['object']->getTitle();
                        }
+                       else if ($parameters['object'] instanceof IIDObject) {
+                               $parameters['id'] = $parameters['object']->getObjectID();
+                       }
                }
                unset($parameters['object']);
                
index cdb217e85fc7abeda816e072bee4342743ac6453..57de3f104676d8e9830320c606947bf301fc6aa0 100644 (file)
@@ -40,12 +40,12 @@ class BackupMultifactorMethod implements IMultifactorMethod {
        /**
         * Returns the number of remaining codes.
         */
-       public function getStatusText(int $setupId): string {
+       public function getStatusText(Setup $setup): string {
                $sql = "SELECT  COUNT(*) - COUNT(useTime) AS count, MAX(useTime) AS lastUsed
                        FROM    wcf".WCF_N."_user_multifactor_backup
                        WHERE   setupID = ?";
                $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute([$setupId]);
+               $statement->execute([$setup->getId()]);
                
                return WCF::getLanguage()->getDynamicVariable(
                        'wcf.user.security.multifactor.backup.status',
@@ -57,16 +57,16 @@ class BackupMultifactorMethod implements IMultifactorMethod {
        /**
         * @inheritDoc
         */
-       public function createManagementForm(IFormDocument $form, ?int $setupId, $returnData = null): void {
+       public function createManagementForm(IFormDocument $form, ?Setup $setup, $returnData = null): void {
                $form->addDefaultButton(false);
                $form->successMessage('wcf.user.security.multifactor.backup.success');
                
-               if ($setupId) {
+               if ($setup) {
                        $sql = "SELECT  *
                                FROM    wcf".WCF_N."_user_multifactor_backup
                                WHERE   setupID = ?";
                        $statement = WCF::getDB()->prepareStatement($sql);
-                       $statement->execute([$setupId]);
+                       $statement->execute([$setup->getId()]);
                        
                        $codes = $statement->fetchAll(\PDO::FETCH_ASSOC);
                        
@@ -134,14 +134,14 @@ class BackupMultifactorMethod implements IMultifactorMethod {
        /**
         * @inheritDoc
         */
-       public function processManagementForm(IFormDocument $form, int $setupId): array {
+       public function processManagementForm(IFormDocument $form, Setup $setup): array {
                $formData = $form->getData();
                \assert($formData['action'] === 'generateCodes' || $formData['action'] === 'regenerateCodes');
                
                $sql = "DELETE FROM     wcf".WCF_N."_user_multifactor_backup
                        WHERE           setupID = ?";
                $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute([$setupId]);
+               $statement->execute([$setup->getId()]);
                
                $codes = [];
                for ($i = 0; $i < 10; $i++) {
@@ -168,7 +168,7 @@ class BackupMultifactorMethod implements IMultifactorMethod {
                $algorithName = PasswordAlgorithmManager::getInstance()->getNameFromAlgorithm($this->algorithm);
                foreach ($codes as $identifier => $code) {
                        $statement->execute([
-                               $setupId,
+                               $setup->getId(),
                                $identifier,
                                $algorithName.':'.$this->algorithm->hash($code),
                                \TIME_NOW,
@@ -202,12 +202,12 @@ class BackupMultifactorMethod implements IMultifactorMethod {
        /**
         * @inheritDoc
         */
-       public function createAuthenticationForm(IFormDocument $form, int $setupId): void {
+       public function createAuthenticationForm(IFormDocument $form, Setup $setup): void {
                $sql = "SELECT  *
                        FROM    wcf".WCF_N."_user_multifactor_backup
                        WHERE   setupID = ?";
                $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute([$setupId]);
+               $statement->execute([$setup->getId()]);
                $codes = $statement->fetchAll(\PDO::FETCH_ASSOC);
                
                $form->appendChildren([
@@ -215,9 +215,9 @@ class BackupMultifactorMethod implements IMultifactorMethod {
                                ->label('wcf.user.security.multifactor.backup.code')
                                ->autoFocus()
                                ->required()
-                               ->addValidator(new FormFieldValidator('code', function (TextFormField $field) use ($codes, $setupId) {
-                                       FloodControl::getInstance()->registerUserContent('com.woltlab.wcf.multifactor.backup', $setupId);
-                                       $attempts = FloodControl::getInstance()->countUserContent('com.woltlab.wcf.multifactor.backup', $setupId, new \DateInterval('PT1H'));
+                               ->addValidator(new FormFieldValidator('code', function (TextFormField $field) use ($codes, $setup) {
+                                       FloodControl::getInstance()->registerUserContent('com.woltlab.wcf.multifactor.backup', $setup->getId());
+                                       $attempts = FloodControl::getInstance()->countUserContent('com.woltlab.wcf.multifactor.backup', $setup->getId(), new \DateInterval('PT1H'));
                                        if ($attempts['count'] > self::USER_ATTEMPTS_PER_HOUR) {
                                                $field->value('');
                                                $field->addValidationError(new FormFieldValidationError(
@@ -241,7 +241,7 @@ class BackupMultifactorMethod implements IMultifactorMethod {
        /**
         * @inheritDoc
         */
-       public function processAuthenticationForm(IFormDocument $form, int $setupId): void {
+       public function processAuthenticationForm(IFormDocument $form, Setup $setup): void {
                $userCode = \preg_replace('/\s+/', '', $form->getData()['data']['code']);
                
                $sql = "SELECT  *
@@ -249,7 +249,7 @@ class BackupMultifactorMethod implements IMultifactorMethod {
                        WHERE   setupID = ?
                        FOR UPDATE";
                $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute([$setupId]);
+               $statement->execute([$setup->getId()]);
                $codes = $statement->fetchAll(\PDO::FETCH_ASSOC);
                
                $usedCode = $this->findValidCode($userCode, $codes);
@@ -266,7 +266,7 @@ class BackupMultifactorMethod implements IMultifactorMethod {
                $statement = WCF::getDB()->prepareStatement($sql);
                $statement->execute([
                        \TIME_NOW,
-                       $setupId,
+                       $setup->getId(),
                        $usedCode['identifier'],
                ]);
                
index ba52493e6afdb8b2436a2fabd0afadb84e665340..5b9b16989f708482fcd4784c944b357c5bc31089 100644 (file)
@@ -17,12 +17,12 @@ interface IMultifactorMethod {
         * 
         * An example text could be: "5 backup codes remaining".
         */
-       public function getStatusText(int $setupId): string;
+       public function getStatusText(Setup $setup): string;
        
        /**
         * Populates the form to set-up and manage this method.
         */
-       public function createManagementForm(IFormDocument $form, ?int $setupId, $returnData = null): void;
+       public function createManagementForm(IFormDocument $form, ?Setup $setup, $returnData = null): void;
        
        /**
         * Updates the database information based on the data received in the management form.
@@ -38,12 +38,12 @@ interface IMultifactorMethod {
         * 
         * @return      mixed   Opaque data that will be passed as `$returnData` in createManagementForm().
         */
-       public function processManagementForm(IFormDocument $form, int $setupId);
+       public function processManagementForm(IFormDocument $form, Setup $setup);
        
        /**
         * Populates the form to authenticate a user with this method.
         */
-       public function createAuthenticationForm(IFormDocument $form, int $setupId): void;
+       public function createAuthenticationForm(IFormDocument $form, Setup $setup): void;
        
        /**
         * Updates the database information based on the data received in the authentication form.
@@ -60,5 +60,5 @@ interface IMultifactorMethod {
         * 
         * @throws \RuntimeException
         */
-       public function processAuthenticationForm(IFormDocument $form, int $setupId): void;
+       public function processAuthenticationForm(IFormDocument $form, Setup $setup): void;
 }
diff --git a/wcfsetup/install/files/lib/system/user/multifactor/Setup.class.php b/wcfsetup/install/files/lib/system/user/multifactor/Setup.class.php
new file mode 100644 (file)
index 0000000..edc54a6
--- /dev/null
@@ -0,0 +1,128 @@
+<?php
+namespace wcf\system\user\multifactor;
+use wcf\data\IIDObject;
+use wcf\data\object\type\ObjectType;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\data\user\User;
+use wcf\system\WCF;
+
+/**
+ * Represents a multifactor setup.
+ *
+ * @author     Tim Duesterhus
+ * @copyright  2001-2020 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\System\User\Multifactor
+ * @since      5.4
+ */
+final class Setup implements IIDObject {
+       /**
+        * @var array
+        */
+       private $row;
+       
+       private function __construct(array $row) {
+               $this->row = $row;
+       }
+       
+       /**
+        * Returns the setup ID.
+        */
+       public function getId(): int {
+               return $this->row['setupID'];
+       }
+       
+       /**
+        * @see Setup::getId()
+        */
+       public function getObjectID(): int {
+               return $this->getId();
+       }
+       
+       /**
+        * Returns the object type.
+        */
+       public function getObjectType(): ObjectType {
+               return ObjectTypeCache::getInstance()->getObjectType($this->row['objectTypeID']);
+       }
+       
+       /**
+        * Locks the database record for this setup, preventing concurrent changes, and returns itself.
+        */
+       public function lock(): self {
+               $sql = "SELECT  setupId
+                       FROM    wcf".WCF_N."_user_multifactor
+                       WHERE   setupId = ?
+                       FOR UPDATE";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute([
+                       $this->getId(),
+               ]);
+               
+               $setupId = \intval($statement->fetchSingleColumn());
+               \assert($setupId === $this->getId());
+               
+               return $this;
+       }
+       
+       /**
+        * Returns an existing setup for the given objectType and user or null if none was found.
+        */
+       public static function find(ObjectType $objectType, User $user): ?self {
+               $sql = "SELECT  *
+                       FROM    wcf".WCF_N."_user_multifactor
+                       WHERE   userID = ?
+                               AND objectTypeID = ?
+                       FOR UPDATE";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute([
+                       $user->userID,
+                       $objectType->objectTypeID,
+               ]);
+               $row = $statement->fetchSingleRow();
+               
+               if ($row) {
+                       return new self($row);
+               }
+               
+               return null;
+       }
+       
+       /**
+        * Returns all setups for a single user.
+        */
+       public static function getAllForUser(User $user): array {
+               $sql = "SELECT  *
+                       FROM    wcf".WCF_N."_user_multifactor
+                       WHERE   userID = ?
+                       FOR UPDATE";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute([$user->userID]);
+               
+               $setups = [];
+               while ($row = $statement->fetchArray()) {
+                       $setups[$row['setupID']] = new self($row);
+               }
+               
+               return $setups;
+       }
+       
+       /**
+        * Allocates a fresh setup for the given objectType and user.
+        */
+       public static function allocateSetUpId(ObjectType $objectType, User $user): Setup {
+               $sql = "INSERT INTO     wcf".WCF_N."_user_multifactor
+                                       (userID, objectTypeID)
+                       VALUES          (?, ?)";
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute([
+                       $user->userID,
+                       $objectType->objectTypeID,
+               ]);
+               
+               $setup = self::find($objectType, $user);
+               \assert($setup);
+               
+               return $setup;
+       }
+}
index d081f22020f56d24a9b0098deceba709bba51294..f7f69cbd18a467aaa16b190cd011ef053cad3598 100644 (file)
@@ -34,12 +34,12 @@ class TotpMultifactorMethod implements IMultifactorMethod {
        /**
         * Returns the number of devices the user set up.
         */
-       public function getStatusText(int $setupId): string {
+       public function getStatusText(Setup $setup): string {
                $sql = "SELECT  COUNT(*) AS count, MAX(useTime) AS lastUsed
                        FROM    wcf".WCF_N."_user_multifactor_totp
                        WHERE   setupID = ?";
                $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute([$setupId]);
+               $statement->execute([$setup->getId()]);
                
                return WCF::getLanguage()->getDynamicVariable(
                        'wcf.user.security.multifactor.totp.status',
@@ -50,7 +50,7 @@ class TotpMultifactorMethod implements IMultifactorMethod {
        /**
         * @inheritDoc
         */
-       public function createManagementForm(IFormDocument $form, ?int $setupId, $returnData = null): void {
+       public function createManagementForm(IFormDocument $form, ?Setup $setup, $returnData = null): void {
                $form->addDefaultButton(false);
                $newDeviceContainer = NewDeviceContainer::create()
                        ->label('wcf.user.security.multifactor.totp.newDevice')
@@ -88,12 +88,12 @@ class TotpMultifactorMethod implements IMultifactorMethod {
                // Note: The order of the two parts of the form is important. Pressing submit within an input
                // will implicitly press the first submit button. If this container comes first the submit
                // button will be a delete button.
-               if ($setupId) {
+               if ($setup) {
                        $sql = "SELECT  deviceID, deviceName, createTime, useTime
                                FROM    wcf".WCF_N."_user_multifactor_totp
                                WHERE   setupID = ?";
                        $statement = WCF::getDB()->prepareStatement($sql);
-                       $statement->execute([$setupId]);
+                       $statement->execute([$setup->getId()]);
                        $devicesContainer = FormContainer::create('devices')
                                ->label('wcf.user.security.multifactor.totp.devices');
                        while ($row = $statement->fetchArray()) {
@@ -117,7 +117,7 @@ class TotpMultifactorMethod implements IMultifactorMethod {
        /**
         * @inheritDoc
         */
-       public function processManagementForm(IFormDocument $form, int $setupId): void {
+       public function processManagementForm(IFormDocument $form, Setup $setup): void {
                $formData = $form->getData();
                
                \assert(
@@ -131,7 +131,7 @@ class TotpMultifactorMethod implements IMultifactorMethod {
                                        AND     deviceID = ?";
                        $statement = WCF::getDB()->prepareStatement($sql);
                        $statement->execute([
-                               $setupId,
+                               $setup->getId(),
                                $formData['delete'],
                        ]);
                        
@@ -140,7 +140,7 @@ class TotpMultifactorMethod implements IMultifactorMethod {
                                WHERE   setupID = ?";
                        $statement = WCF::getDB()->prepareStatement($sql);
                        $statement->execute([
-                               $setupId,
+                               $setup->getId(),
                        ]);
                        
                        if (!$statement->fetchSingleColumn()) {
@@ -154,7 +154,7 @@ class TotpMultifactorMethod implements IMultifactorMethod {
                                VALUES          (?, ?, ?, ?, ?, ?)";
                        $statement = WCF::getDB()->prepareStatement($sql);
                        $statement->execute([
-                               $setupId,
+                               $setup->getId(),
                                Hex::encode(\random_bytes(16)),
                                $formData['data']['deviceName'] ?: $defaultName,
                                $formData['data']['secret'],
@@ -167,13 +167,13 @@ class TotpMultifactorMethod implements IMultifactorMethod {
        /**
         * @inheritDoc
         */
-       public function createAuthenticationForm(IFormDocument $form, int $setupId): void {
+       public function createAuthenticationForm(IFormDocument $form, Setup $setup): void {
                $sql = "SELECT          *
                        FROM            wcf".WCF_N."_user_multifactor_totp
                        WHERE           setupID = ?
                        ORDER BY        deviceName";
                $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute([$setupId]);
+               $statement->execute([$setup->getId()]);
                $devices = $statement->fetchAll(\PDO::FETCH_ASSOC);
                
                if (count($devices) > 1) {
@@ -208,9 +208,9 @@ class TotpMultifactorMethod implements IMultifactorMethod {
                                ->label('wcf.user.security.multifactor.totp.code')
                                ->autoFocus()
                                ->required()
-                               ->addValidator(new FormFieldValidator('code', function (CodeFormField $field) use ($devices, $setupId) {
-                                       FloodControl::getInstance()->registerUserContent('com.woltlab.wcf.multifactor.backup', $setupId);
-                                       $attempts = FloodControl::getInstance()->countUserContent('com.woltlab.wcf.multifactor.backup', $setupId, new \DateInterval('PT10M'));
+                               ->addValidator(new FormFieldValidator('code', function (CodeFormField $field) use ($devices, $setup) {
+                                       FloodControl::getInstance()->registerUserContent('com.woltlab.wcf.multifactor.backup', $setup->getId());
+                                       $attempts = FloodControl::getInstance()->countUserContent('com.woltlab.wcf.multifactor.backup', $setup->getId(), new \DateInterval('PT10M'));
                                        if ($attempts['count'] > self::USER_ATTEMPTS_PER_TEN_MINUTES) {
                                                $field->value('');
                                                $field->addValidationError(new FormFieldValidationError(
@@ -248,7 +248,7 @@ class TotpMultifactorMethod implements IMultifactorMethod {
        /**
         * @inheritDoc
         */
-       public function processAuthenticationForm(IFormDocument $form, int $setupId): void {
+       public function processAuthenticationForm(IFormDocument $form, Setup $setup): void {
                $formData = $form->getData();
                
                $sql = "SELECT  *
@@ -258,7 +258,7 @@ class TotpMultifactorMethod implements IMultifactorMethod {
                        FOR UPDATE";
                $statement = WCF::getDB()->prepareStatement($sql);
                $statement->execute([
-                       $setupId,
+                       $setup->getId(),
                        $formData['data']['deviceID'],
                ]);
                $device = $statement->fetchArray();
@@ -277,7 +277,7 @@ class TotpMultifactorMethod implements IMultifactorMethod {
                $statement->execute([
                        \TIME_NOW,
                        $formData['data']['code']['minCounter'],
-                       $setupId,
+                       $setup->getId(),
                        $formData['data']['deviceID'],
                        $formData['data']['code']['minCounter'],
                ]);