<page identifier="com.woltlab.wcf.MultifactorManage">
<pageType>system</pageType>
<controller>wcf\form\MultifactorManageForm</controller>
- <name language="de">Mehrfaktor-Authentifizierung einrichten</name>
<name language="en">Manage Multi-Factor Authentication</name>
+ <name language="de">Mehrfaktor-Authentifizierung einrichten</name>
<hasFixedParent>1</hasFixedParent>
<parent>com.woltlab.wcf.AccountSecurity</parent>
<excludeFromLandingPage>1</excludeFromLandingPage>
<availableDuringOfflineMode>1</availableDuringOfflineMode>
<requireObjectID>1</requireObjectID>
</page>
+ <page identifier="com.woltlab.wcf.MultifactorDisable">
+ <pageType>system</pageType>
+ <controller>wcf\form\MultifactorDisableForm</controller>
+ <name language="en">Disable Multi-Factor Authentication</name>
+ <name language="de">Mehrfaktor-Authentifizierung deaktivieren</name>
+ <hasFixedParent>1</hasFixedParent>
+ <parent>com.woltlab.wcf.AccountSecurity</parent>
+ <excludeFromLandingPage>1</excludeFromLandingPage>
+ <availableDuringOfflineMode>1</availableDuringOfflineMode>
+ <requireObjectID>1</requireObjectID>
+ <content language="en">
+ <title>Disable Multi-Factor Authentication</title>
+ </content>
+ <content language="de">
+ <title>Mehrfaktor-Authentifizierung deaktivieren</title>
+ </content>
+ </page>
</import>
<delete>
<page identifier="com.woltlab.wcf.Mail"/>
--- /dev/null
+{lang}wcf.user.security.multifactor.disable.explanation{/lang}
</div>
<div class="accountSecurityButtons">
- <a class="small button" href="{link controller='MultifactorManage' id=$method->objectTypeID}{/link}">
- {lang}wcf.user.security.multifactor.{if $enabledMultifactorMethods[$method->objectTypeID]|isset}manage{else}setup{/if}{/lang}
- </a>
+ {if $enabledMultifactorMethods[$method->objectTypeID]|isset}
+ {if $method->objectType !== 'com.woltlab.wcf.multifactor.backup'}
+ <a class="small button" href="{link controller='MultifactorDisable' object=$enabledMultifactorMethods[$method->objectTypeID]}{/link}">
+ {lang}wcf.user.security.multifactor.disable{/lang}
+ </a>
+ {/if}
+
+ <a class="small button buttonPrimary" href="{link controller='MultifactorManage' object=$method}{/link}">
+ {lang}wcf.user.security.multifactor.manage{/lang}
+ </a>
+ {else}
+ <a class="small button buttonPrimary" href="{link controller='MultifactorManage' object=$method}{/link}">
+ {lang}wcf.user.security.multifactor.setup{/lang}
+ </a>
+ {/if}
</div>
</div>
</li>
--- /dev/null
+{include file='userMenuSidebar'}
+
+{include file='header' __disableAds=true __sidebarLeftHasMenu=true}
+
+{@$form->getHtml()}
+
+{include file='footer' __disableAds=true}
{include file='header' __disableAds=true __sidebarLeftHasMenu=true}
-
{if $backupForm}
{if $form->showsSuccessMessage()}
<p class="success">
--- /dev/null
+<?php
+namespace wcf\form;
+use wcf\data\object\type\ObjectType;
+use wcf\data\user\UserEditor;
+use wcf\page\AccountSecurityPage;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\form\builder\field\BooleanFormField;
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+use wcf\system\form\builder\field\validation\FormFieldValidator;
+use wcf\system\form\builder\TemplateFormNode;
+use wcf\system\menu\user\UserMenu;
+use wcf\system\request\LinkHandler;
+use wcf\system\user\multifactor\Setup;
+use wcf\system\WCF;
+use wcf\util\HeaderUtil;
+
+/**
+ * Represents the multi-factor disable form.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2020 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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();
+ }
+}
$this->processor->createManagementForm($this->form, $this->setup, $this->returnData);
}
+ /**
+ * @inheritDoc
+ */
public function save() {
AbstractForm::save();
*/
protected function setFormAction() {
$this->form->action(LinkHandler::getInstance()->getControllerLink(static::class, [
- 'id' => $this->method->objectTypeID,
+ 'object' => $this->method,
]));
}
*/
private $row;
+ private $isDeleted = false;
+
private function __construct(array $row) {
$this->row = $row;
}
* Returns the setup ID.
*/
public function getId(): int {
+ if ($this->isDeleted) {
+ throw new \BadMethodCallException('The Setup is deleted.');
+ }
+
return $this->row['setupID'];
}
* @see Setup::getId()
*/
public function getObjectID(): int {
+ if ($this->isDeleted) {
+ throw new \BadMethodCallException('The Setup is deleted.');
+ }
+
return $this->getId();
}
* 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']);
}
* Returns the user.
*/
public function getUser(): User {
+ if ($this->isDeleted) {
+ throw new \BadMethodCallException('The Setup is deleted.');
+ }
+
return UserRuntimeCache::getInstance()->getObject($this->row['userID']);
}
* 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 = ?
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.
*/
<item name="wcf.user.security.multifactor.com.woltlab.wcf.multifactor.email.description"><![CDATA[<p class="small">{if LANGUAGE_USE_INFORMAL_VARIANT}Du erhältst bei jedem Login einen Einmalcode an deine E-Mail-Adresse.{else}Sie erhalten bei jedem Login einen Einmalcode an Ihre E-Mail-Adresse.{/if}</p>]]></item>
<item name="wcf.user.security.multifactor.description"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Schütze dein Benutzerkonto{else}Schützen Sie Ihr Benutzerkonto{/if}, indem bei jedem Login eine zusätzliche Authentifizierung mit Hilfe eines zweiten Faktors erforderlich ist.]]></item>
<item name="wcf.user.security.activeSessions.description"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Du bist{else}Sie sind{/if} derzeit in den aufgelisteten Webbrowsern eingeloggt. {if LANGUAGE_USE_INFORMAL_VARIANT}Beende Sitzungen, die du nicht mehr benötigst oder nicht erkennst.{else}Beenden Sie Sitzungen, die Sie nicht mehr benötigen oder nicht erkennen.{/if} {if LANGUAGE_USE_INFORMAL_VARIANT}Du kannst dein Kennwort{else}Sie können Ihr Kennwort{/if} in der <a href="{link controller='AccountManagement'}{/link}">Account-Verwaltung</a> ändern.]]></item>
+ <item name="wcf.user.security.multifactor.disable"><![CDATA[Deaktivieren]]></item>
+ <item name="wcf.user.security.multifactor.disable.explanation"><![CDATA[<p>Mit dem Absenden dieses Formulars {if LANGUAGE_USE_INFORMAL_VARIANT}deaktivierst du{else}deaktivieren Sie{/if} das Verfahren <strong>{lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang}</strong> zur Nutzung mit der Mehrfaktor-Authentifizierung. {if LANGUAGE_USE_INFORMAL_VARIANT}Du wirst{else}Sie werden{/if} dieses Verfahren anschließend nicht mehr nutzen können, um {if LANGUAGE_USE_INFORMAL_VARIANT}dich{else}sich{/if} zu authentifizieren.</p>
+{if !$remaining|empty}
+<p>Nach Deaktivierung von <strong>{lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang}</strong> {plural value=$remaining|count 1='steht folgendes' other='stehen folgende'} Verfahren weiterhin zur Mehrfaktor-Authentifizierung zur Verfügung.</p>
+<ul class="nativeList">
+{foreach from=$remaining item='method'}
+<li><a href="{link controller='MultifactorManage' object=$method->getObjectType()}{/link}">{lang}wcf.user.security.multifactor.{$method->getObjectType()->objectType}{/lang}</a></li>
+{/foreach}
+</ul>
+{else}
+<p>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.</p>
+{/if}]]></item>
+ <item name="wcf.user.security.multifactor.disable.confirm"><![CDATA[{if $remaining|empty}Mehrfaktor-Authentifizierung vollständig deaktivieren{else}<strong>{lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang}</strong> deaktivieren{/if}]]></item>
+ <item name="wcf.user.security.multifactor.disable.confirm.required"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Bitte bestätige, dass du die Hinweise gelesen hast und mit der Deaktivierung fortfahren möchtest.{else}Bitte bestätigen Sie, dass Sie die Hinweise gelesen haben und mit der Deaktivierung fortfahren möchten.{/if}]]></item>
+ <item name="wcf.user.security.multifactor.disable.success"><![CDATA[Das Verfahren <strong>{lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang}</strong> wurde erfolgreich deaktiviert.]]></item>
+ <item name="wcf.user.security.multifactor.disable.success.full"><![CDATA[Die Mehrfaktor-Authentifizierung wurde erfolgreich deaktiviert.]]></item>
</category>
<category name="wcf.user.trophy">
<item name="wcf.user.trophy.trophyPoints"><![CDATA[Trophäen]]></item>
<item name="wcf.user.security.multifactor.com.woltlab.wcf.multifactor.email.description"><![CDATA[<p class="small">You will receive a one time code via email after logging in.</p>]]></item>
<item name="wcf.user.security.multifactor.description"><![CDATA[Protect your account by requiring authentication with a second factor for every login.]]></item>
<item name="wcf.user.security.activeSessions.description"><![CDATA[You are currently logged into your account in the listed web browsers. Revoke sessions you no longer need or that you do not recognize. You can change your password within the <a href="{link controller='AccountManagement'}{/link}">Account Management Form</a>.]]></item>
+ <item name="wcf.user.security.multifactor.disable"><![CDATA[Disable]]></item>
+ <item name="wcf.user.security.multifactor.disable.explanation"><![CDATA[<p>By submitting this form the <strong>{lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang}</strong> method for multi-factor authentication will be disabled. You will no longer be able to use this method to authenticate yourself.</p>
+{if !$remaining|empty}
+<p>After disabling <strong>{lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang}</strong> the following methods will still be available for multi-factor authentication.</p>
+<ul class="nativeList">
+{foreach from=$remaining item='method'}
+<li><a href="{link controller='MultifactorManage' object=$method->getObjectType()}{/link}">{lang}wcf.user.security.multifactor.{$method->getObjectType()->objectType}{/lang}</a></li>
+{/foreach}
+</ul>
+{else}
+<p>Disabling <strong>{lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang}</strong> will fully disable the use of multi-factor authentication for your account as it is the only active method.</p>
+{/if}]]></item>
+ <item name="wcf.user.security.multifactor.disable.confirm"><![CDATA[{if $remaining|empty}Completely Disable Multi-Factor Authentication{else}Disable <strong>{lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang}</strong>{/if}]]></item>
+ <item name="wcf.user.security.multifactor.disable.confirm.required"><![CDATA[Please confirm that you read the explanation and that you would like to proceed.]]></item>
+ <item name="wcf.user.security.multifactor.disable.success"><![CDATA[The <strong>{lang}wcf.user.security.multifactor.{$setup->getObjectType()->objectType}{/lang}</strong> method has successfully been disabled.]]></item>
+ <item name="wcf.user.security.multifactor.disable.success.full"><![CDATA[The multi-factor authentication has successfully been disabled.]]></item>
</category>
<category name="wcf.user.trophy">
<item name="wcf.user.trophy.trophyPoints"><![CDATA[Trophies]]></item>