From: Tim Düsterhus
Verantwort
Verantwort
diff --git a/wcfsetup/install/files/lib/form/MultifactorDisableForm.class.php b/wcfsetup/install/files/lib/form/MultifactorDisableForm.class.php
new file mode 100644
index 0000000000..8f4a6c4f25
--- /dev/null
+++ b/wcfsetup/install/files/lib/form/MultifactorDisableForm.class.php
@@ -0,0 +1,231 @@
+
+ * @package WoltLabSuite\Core\Form
+ * @since 5.4
+ */
+class MultifactorDisableForm extends AbstractFormBuilderForm {
+ /**
+ * @inheritDoc
+ */
+ public $loginRequired = true;
+
+ /**
+ * @var ObjectType
+ */
+ private $method;
+
+ /**
+ * @var Setup
+ */
+ private $setup;
+
+ /**
+ * @var Setup[]
+ */
+ private $setups;
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (!isset($_GET['id'])) {
+ throw new IllegalLinkException();
+ }
+
+ $this->setups = Setup::getAllForUser(WCF::getUser());
+
+ if (empty($this->setups)) {
+ throw new IllegalLinkException();
+ }
+
+ if (!isset($this->setups[$_GET['id']])) {
+ throw new IllegalLinkException();
+ }
+
+ $this->setup = $this->setups[$_GET['id']];
+ $this->method = $this->setup->getObjectType();
+ \assert($this->method->getDefinition()->definitionName === 'com.woltlab.wcf.multifactor');
+
+ // Backup codes may not be disabled.
+ if ($this->method->objectType === 'com.woltlab.wcf.multifactor.backup') {
+ throw new PermissionDeniedException();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function createForm() {
+ parent::createForm();
+
+ $this->form->appendChildren([
+ TemplateFormNode::create('explanation')
+ ->templateName('__multifactorDisableExplanation')
+ ->variables([
+ 'remaining' => $this->setupsWithoutDisableRequest(
+ $this->setupsWithoutBackupCodes($this->setups)
+ ),
+ 'setup' => $this->setup,
+ ]),
+ BooleanFormField::create('confirm')
+ ->label('wcf.user.security.multifactor.disable.confirm', [
+ 'remaining' => $this->setupsWithoutDisableRequest(
+ $this->setupsWithoutBackupCodes($this->setups)
+ ),
+ 'setup' => $this->setup,
+ ])
+ ->addValidator(new FormFieldValidator('confirm', function(BooleanFormField $formField) {
+ if (!$formField->getValue()) {
+ $formField->addValidationError(
+ new FormFieldValidationError(
+ 'required',
+ 'wcf.user.security.multifactor.disable.confirm.required'
+ )
+ );
+ }
+ })),
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ AbstractForm::save();
+
+ WCF::getDB()->beginTransaction();
+
+ $this->form->successMessage('wcf.user.security.multifactor.disable.success', [
+ 'setup' => $this->setup,
+ ]);
+ $this->setup->delete();
+
+ $setups = Setup::getAllForUser(WCF::getUser());
+ $remaining = $this->setupsWithoutBackupCodes($setups);
+
+ if (empty($remaining)) {
+ foreach ($setups as $setup) {
+ $setup->delete();
+ }
+ $this->disableMultifactorAuth();
+ $this->form->successMessage('wcf.user.security.multifactor.disable.success.full');
+ }
+
+ WCF::getDB()->commitTransaction();
+
+ $this->saved();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function saved() {
+ AbstractForm::saved();
+
+ HeaderUtil::delayedRedirect(
+ LinkHandler::getInstance()
+ ->getControllerLink(AccountSecurityPage::class),
+ $this->form->getSuccessMessage()
+ );
+ exit;
+ }
+
+ /**
+ * Returns the active setups without the backup codes.
+ *
+ * @param Setup[] $setups
+ * @return Setup[]
+ */
+ protected function setupsWithoutBackupCodes(array $setups): array {
+ return array_filter($setups, function (Setup $setup) {
+ return $setup->getObjectType()->objectType !== 'com.woltlab.wcf.multifactor.backup';
+ });
+ }
+
+ /**
+ * Returns the active setups without the setup that is going to be disabled.
+ *
+ * @param Setup[] $setups
+ * @return Setup[]
+ */
+ protected function setupsWithoutDisableRequest(array $setups): array {
+ return array_filter($setups, function (Setup $setup) {
+ return $setup->getId() !== $this->setup->getId();
+ });
+ }
+
+ /**
+ * Disables multifactor authentication for the user.
+ */
+ protected function disableMultifactorAuth(): void {
+ // This method intentionally does not use UserAction to prevent
+ // events from firing.
+ //
+ // This method is being run from within a transaction to ensure
+ // a consistent database state in case any part of the MFA setup
+ // fails. Event listeners could run complex logic, including
+ // queries that modify the database state, possibly leading to
+ // a very large transaction and much more surface area for
+ // unexpected failures.
+ //
+ // Use the saved@MultifactorDisableForm event if you need to run
+ // logic in response to a user disabling MFA.
+ $editor = new UserEditor(WCF::getUser());
+ $editor->update([
+ 'multifactorActive' => 0,
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function setFormAction() {
+ $this->form->action(LinkHandler::getInstance()->getControllerLink(static::class, [
+ 'object' => $this->setup,
+ ]));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'method' => $this->method,
+ 'setups' => $this->setups,
+ ]);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function show() {
+ UserMenu::getInstance()->setActiveMenuItem('wcf.user.menu.profile.security');
+
+ parent::show();
+ }
+}
diff --git a/wcfsetup/install/files/lib/form/MultifactorManageForm.class.php b/wcfsetup/install/files/lib/form/MultifactorManageForm.class.php
index a9592aa91c..1dad21c363 100644
--- a/wcfsetup/install/files/lib/form/MultifactorManageForm.class.php
+++ b/wcfsetup/install/files/lib/form/MultifactorManageForm.class.php
@@ -98,6 +98,9 @@ class MultifactorManageForm extends AbstractFormBuilderForm {
$this->processor->createManagementForm($this->form, $this->setup, $this->returnData);
}
+ /**
+ * @inheritDoc
+ */
public function save() {
AbstractForm::save();
@@ -221,7 +224,7 @@ class MultifactorManageForm extends AbstractFormBuilderForm {
*/
protected function setFormAction() {
$this->form->action(LinkHandler::getInstance()->getControllerLink(static::class, [
- 'id' => $this->method->objectTypeID,
+ 'object' => $this->method,
]));
}
diff --git a/wcfsetup/install/files/lib/system/user/multifactor/Setup.class.php b/wcfsetup/install/files/lib/system/user/multifactor/Setup.class.php
index 7548bfbe47..1fd920740c 100644
--- a/wcfsetup/install/files/lib/system/user/multifactor/Setup.class.php
+++ b/wcfsetup/install/files/lib/system/user/multifactor/Setup.class.php
@@ -22,6 +22,8 @@ final class Setup implements IIDObject {
*/
private $row;
+ private $isDeleted = false;
+
private function __construct(array $row) {
$this->row = $row;
}
@@ -30,6 +32,10 @@ final class Setup implements IIDObject {
* Returns the setup ID.
*/
public function getId(): int {
+ if ($this->isDeleted) {
+ throw new \BadMethodCallException('The Setup is deleted.');
+ }
+
return $this->row['setupID'];
}
@@ -37,6 +43,10 @@ final class Setup implements IIDObject {
* @see Setup::getId()
*/
public function getObjectID(): int {
+ if ($this->isDeleted) {
+ throw new \BadMethodCallException('The Setup is deleted.');
+ }
+
return $this->getId();
}
@@ -44,6 +54,10 @@ final class Setup implements IIDObject {
* Returns the object type.
*/
public function getObjectType(): ObjectType {
+ if ($this->isDeleted) {
+ throw new \BadMethodCallException('The Setup is deleted.');
+ }
+
return ObjectTypeCache::getInstance()->getObjectType($this->row['objectTypeID']);
}
@@ -51,6 +65,10 @@ final class Setup implements IIDObject {
* Returns the user.
*/
public function getUser(): User {
+ if ($this->isDeleted) {
+ throw new \BadMethodCallException('The Setup is deleted.');
+ }
+
return UserRuntimeCache::getInstance()->getObject($this->row['userID']);
}
@@ -58,6 +76,10 @@ final class Setup implements IIDObject {
* Locks the database record for this setup, preventing concurrent changes, and returns itself.
*/
public function lock(): self {
+ if ($this->isDeleted) {
+ throw new \BadMethodCallException('The Setup is deleted.');
+ }
+
$sql = "SELECT setupId
FROM wcf".WCF_N."_user_multifactor
WHERE setupId = ?
@@ -73,6 +95,19 @@ final class Setup implements IIDObject {
return $this;
}
+ /**
+ * Deletes the setup.
+ */
+ public function delete(): void {
+ $sql = "DELETE FROM wcf".WCF_N."_user_multifactor
+ WHERE setupId = ?";
+ $statement = WCF::getDB()->prepareStatement($sql);
+ $statement->execute([
+ $this->getId(),
+ ]);
+ $this->isDeleted = true;
+ }
+
/**
* Returns an existing setup for the given objectType and user or null if none was found.
*/
diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml
index fe46550090..994acdc89d 100644
--- a/wcfsetup/install/lang/de.xml
+++ b/wcfsetup/install/lang/de.xml
@@ -4904,6 +4904,22 @@ Die E-Mail-Adresse des neuen Benutzers lautet: {@$user->email}
Nach Deaktivierung von {lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang} {plural value=$remaining|count 1='steht folgendes' other='stehen folgende'} Verfahren weiterhin zur Mehrfaktor-Authentifizierung zur Verfügung.
+Die Deaktivierung des Verfahrens wird die Mehrfaktor-Authentifizierung für {if LANGUAGE_USE_INFORMAL_VARIANT}dein{else}Ihr{/if} Benutzerkonto vollständig deaktivieren, da es das einzige aktive Verfahren ist.
+{/if}]]> +After disabling {lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang} the following methods will still be available for multi-factor authentication.
+Disabling {lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang} will fully disable the use of multi-factor authentication for your account as it is the only active method.
+{/if}]]>