<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>
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;
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
*/
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;
/**
private $user;
/**
- * @var ObjectType[]
+ * @var Setup[]
*/
- private $methods;
+ private $setups;
/**
* @var ObjectType
private $processor;
/**
- * @var int
+ * @var Setup
*/
- private $setupId;
+ private $setup;
/**
* @inheritDoc
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();
protected function createForm() {
parent::createForm();
- $this->processor->createAuthenticationForm($this->form, $this->setupId);
+ $this->processor->createAuthenticationForm($this->form, $this->setup);
}
public function save() {
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();
*/
protected function setFormAction() {
$this->form->action(LinkHandler::getInstance()->getControllerLink(static::class, [
- 'id' => $this->setupId,
+ 'object' => $this->setup,
]));
}
parent::assignVariables();
WCF::getTPL()->assign([
- 'method' => $this->method,
- 'methods' => $this->methods,
+ 'setups' => $this->setups,
'user' => $this->user,
- 'setupId' => $this->setupId,
+ 'setup' => $this->setup,
]);
}
}
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;
/**
private $processor;
/**
- * @var int
+ * @var ?Setup
*/
- private $setupId;
+ private $setup;
/**
* @var mixed
$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());
}
/**
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() {
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
*/
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;
/**
private $multifactorMethods;
/**
- * @var int[]
+ * @var Setup[]
*/
private $enabledMultifactorMethods;
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;
+ }
}
/**
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;
$parameters['id'] = $parameters['object']->getObjectID();
$parameters['title'] = $parameters['object']->getTitle();
}
+ else if ($parameters['object'] instanceof IIDObject) {
+ $parameters['id'] = $parameters['object']->getObjectID();
+ }
}
unset($parameters['object']);
/**
* 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',
/**
* @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);
/**
* @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++) {
$algorithName = PasswordAlgorithmManager::getInstance()->getNameFromAlgorithm($this->algorithm);
foreach ($codes as $identifier => $code) {
$statement->execute([
- $setupId,
+ $setup->getId(),
$identifier,
$algorithName.':'.$this->algorithm->hash($code),
\TIME_NOW,
/**
* @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([
->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(
/**
* @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 *
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);
$statement = WCF::getDB()->prepareStatement($sql);
$statement->execute([
\TIME_NOW,
- $setupId,
+ $setup->getId(),
$usedCode['identifier'],
]);
*
* 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.
*
* @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.
*
* @throws \RuntimeException
*/
- public function processAuthenticationForm(IFormDocument $form, int $setupId): void;
+ public function processAuthenticationForm(IFormDocument $form, Setup $setup): void;
}
--- /dev/null
+<?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;
+ }
+}
/**
* 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',
/**
* @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')
// 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()) {
/**
* @inheritDoc
*/
- public function processManagementForm(IFormDocument $form, int $setupId): void {
+ public function processManagementForm(IFormDocument $form, Setup $setup): void {
$formData = $form->getData();
\assert(
AND deviceID = ?";
$statement = WCF::getDB()->prepareStatement($sql);
$statement->execute([
- $setupId,
+ $setup->getId(),
$formData['delete'],
]);
WHERE setupID = ?";
$statement = WCF::getDB()->prepareStatement($sql);
$statement->execute([
- $setupId,
+ $setup->getId(),
]);
if (!$statement->fetchSingleColumn()) {
VALUES (?, ?, ?, ?, ?, ?)";
$statement = WCF::getDB()->prepareStatement($sql);
$statement->execute([
- $setupId,
+ $setup->getId(),
Hex::encode(\random_bytes(16)),
$formData['data']['deviceName'] ?: $defaultName,
$formData['data']['secret'],
/**
* @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) {
->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(
/**
* @inheritDoc
*/
- public function processAuthenticationForm(IFormDocument $form, int $setupId): void {
+ public function processAuthenticationForm(IFormDocument $form, Setup $setup): void {
$formData = $form->getData();
$sql = "SELECT *
FOR UPDATE";
$statement = WCF::getDB()->prepareStatement($sql);
$statement->execute([
- $setupId,
+ $setup->getId(),
$formData['data']['deviceID'],
]);
$device = $statement->fetchArray();
$statement->execute([
\TIME_NOW,
$formData['data']['code']['minCounter'],
- $setupId,
+ $setup->getId(),
$formData['data']['deviceID'],
$formData['data']['code']['minCounter'],
]);