{capture assign='contentTitle'}{lang}wcf.user.security.multifactor.authentication{/lang}{/capture}
{capture assign='sidebarLeft'}
-<section class="box">
+ <section class="box">
<h2 class="boxTitle">{lang}wcf.user.security.multifactor.methods{/lang}</h2>
<div class="boxContent">
"__labelFormField",
"__mediaSetCategoryDialog",
"__messageQuoteManager",
+ "__multifactorBackupCodeField",
+ "__multifactorEmailCodeField",
+ "__multifactorTotpCodeField",
"__multilineTextFormField",
"__multiPageCondition",
"__multipleSelectionFormField",
--- /dev/null
+<input type="text" {*
+ *}id="{@$field->getPrefixedId()}" {*
+ *}name="{@$field->getPrefixedId()}" {*
+ *}value="{if !$field->isI18n() || !$field->hasI18nValues() || $availableLanguages|count === 1}{$field->getValue()}{/if}" {*
+ *}class="multifactorBackupCode" {*
+ *}autocomplete="off" {*
+ *}pattern="[0-9\s]*" {*
+ *}inputmode="numeric"{*
+ *}{if $field->getChunks() && $field->getChunkLength()} size="{$field->getChunks() - 1 + $field->getChunks() * $field->getChunkLength()}"{/if}{*
+ *}{if $field->isAutofocused()} autofocus{/if}{*
+ *}{if $field->isRequired()} required{/if}{*
+ *}{if $field->isImmutable()} disabled{/if}{*
+ *}{if $field->getMinimumLength() !== null} minlength="{$field->getMinimumLength()}"{/if}{*
+ *}{if $field->getMaximumLength() !== null} maxlength="{$field->getMaximumLength()}"{/if}{*
+ *}{if $field->getPlaceholder() !== null} placeholder="{$field->getPlaceholder()}"{/if}{*
+ *}{if $field->getDocument()->isAjax()} data-dialog-submit-on-enter="true"{/if}{*
+*}>
--- /dev/null
+<input type="text" {*
+ *}id="{@$field->getPrefixedId()}" {*
+ *}name="{@$field->getPrefixedId()}" {*
+ *}value="{if !$field->isI18n() || !$field->hasI18nValues() || $availableLanguages|count === 1}{$field->getValue()}{/if}" {*
+ *}class="multifactorEmailCode" {*
+ *}autocomplete="off" {*
+ *}{if $field->getMaximumLength() !== null}size="{$field->getMaximumLength()}" {/if}{*
+ *}pattern="[0-9]*" {*
+ *}inputmode="numeric"{*
+ *}{if $field->isAutofocused()} autofocus{/if}{*
+ *}{if $field->isRequired()} required{/if}{*
+ *}{if $field->isImmutable()} disabled{/if}{*
+ *}{if $field->getMinimumLength() !== null} minlength="{$field->getMinimumLength()}"{/if}{*
+ *}{if $field->getMaximumLength() !== null} maxlength="{$field->getMaximumLength()}"{/if}{*
+ *}{if $field->getPlaceholder() !== null} placeholder="{$field->getPlaceholder()}"{/if}{*
+ *}{if $field->getDocument()->isAjax()} data-dialog-submit-on-enter="true"{/if}{*
+*}>
--- /dev/null
+<input type="text" {*
+ *}id="{@$field->getPrefixedId()}" {*
+ *}name="{@$field->getPrefixedId()}" {*
+ *}value="{if !$field->isI18n() || !$field->hasI18nValues() || $availableLanguages|count === 1}{$field->getValue()}{/if}" {*
+ *}class="multifactorTotpCode" {*
+ *}autocomplete="off" {*
+ *}{if $field->getMaximumLength() !== null}size="{$field->getMaximumLength()}" {/if}{*
+ *}pattern="[0-9]*" {*
+ *}inputmode="numeric"{*
+ *}{if $field->isAutofocused()} autofocus{/if}{*
+ *}{if $field->isRequired()} required{/if}{*
+ *}{if $field->isImmutable()} disabled{/if}{*
+ *}{if $field->getMinimumLength() !== null} minlength="{$field->getMinimumLength()}"{/if}{*
+ *}{if $field->getMaximumLength() !== null} maxlength="{$field->getMaximumLength()}"{/if}{*
+ *}{if $field->getPlaceholder() !== null} placeholder="{$field->getPlaceholder()}"{/if}{*
+ *}{if $field->getDocument()->isAjax()} data-dialog-submit-on-enter="true"{/if}{*
+*}>
--- /dev/null
+{capture assign='pageTitle'}{lang}wcf.user.security.multifactor.authentication{/lang}{/capture}
+{capture assign='contentTitle'}{lang}wcf.user.security.multifactor.authentication{/lang}{/capture}
+
+{include file='header' __isLogin=true}
+
+<section class="box">
+ <h2 class="boxTitle">{lang}wcf.user.security.multifactor.methods{/lang}</h2>
+
+ <div class="boxContent">
+ <nav>
+ <ol class="boxMenu">
+ {foreach from=$setups item='_setup'}
+ <li{if $setup->getId() == $_setup->getId()} class="active"{/if}>
+ <a class="boxMenuLink" href="{link controller='MultifactorAuthentication' object=$_setup url=$redirectUrl}{/link}"><span class="boxMenuLinkTitle">{lang}wcf.user.security.multifactor.{$_setup->getObjectType()->objectType}{/lang}</span></a>
+ </li>
+ {/foreach}
+ </ol>
+ </nav>
+ </div>
+</section>
+
+<div class="section box48">
+ {@$userProfile->getAvatar()->getImageTag(48)}
+
+ <div>
+ <div class="containerHeadline">
+ <h3>
+ {lang}wcf.user.security.multifactor.authentication.user.headline{/lang}
+ </h3>
+ </div>
+ <div class="containerContent">
+ {lang}wcf.user.security.multifactor.authentication.user.content{/lang}
+ </div>
+
+ <form action="{link controller='MultifactorAuthenticationAbort'}{/link}" method="post">
+ <button type="submit">{lang}wcf.user.security.multifactor.authentication.logout{/lang}</button>
+ {csrfToken}
+ </form>
+ </div>
+</div>
+
+{@$form->getHtml()}
+
+{include file='footer'}
*/
protected function performRedirect(bool $needsMultifactor = false) {
if ($needsMultifactor) {
- $this->url = LinkHandler::getInstance()->getControllerLink(MultifactorAuthenticationForm::class, [
+ $this->url = LinkHandler::getInstance()->getLink('MultifactorAuthentication', [
'url' => $this->url,
]);
}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+
+use wcf\form\AbstractForm;
+use wcf\system\exception\PermissionDeniedException;
+use wcf\system\request\LinkHandler;
+use wcf\system\WCF;
+use wcf\util\HeaderUtil;
+
+/**
+ * Aborts the multi-factor authentication process.
+ *
+ * @author Tim Duesterhus
+ * @copyright 2001-2020 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\Acp\Form
+ * @since 5.4
+ */
+class MultifactorAuthenticationAbortForm extends AbstractForm {
+ const AVAILABLE_DURING_OFFLINE_MODE = true;
+
+ /**
+ * @inheritDoc
+ */
+ public $useTemplate = false;
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (WCF::getUser()->userID) {
+ throw new PermissionDeniedException();
+ }
+
+ $user = WCF::getSession()->getPendingUserChange();
+ if (!$user) {
+ $this->performRedirect();
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save() {
+ parent::save();
+
+ WCF::getSession()->clearPendingUserChange();
+
+ $this->saved();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function saved() {
+ parent::saved();
+
+ $this->performRedirect();
+ }
+
+ /**
+ * Returns to the landing page otherwise.
+ */
+ protected function performRedirect() {
+ HeaderUtil::redirect(
+ LinkHandler::getInstance()->getControllerLink(LoginForm::class)
+ );
+ exit;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function show() {
+ parent::show();
+
+ // It is not expected to reach this place, because the form should
+ // never be accessed via a direct link.
+ // If we reach it nonetheless we simply redirect back to the authentication
+ // form which contains the proper button to perform the submission.
+ HeaderUtil::redirect(LinkHandler::getInstance()->getControllerLink(MultifactorAuthenticationForm::class));
+ exit;
+ }
+}
--- /dev/null
+<?php
+namespace wcf\acp\form;
+use wcf\data\object\type\ObjectType;
+use wcf\data\user\User;
+use wcf\form\AbstractForm;
+use wcf\form\AbstractFormBuilderForm;
+use wcf\system\application\ApplicationHandler;
+use wcf\system\cache\runtime\UserProfileRuntimeCache;
+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;
+use wcf\util\HeaderUtil;
+
+/**
+ * Represents the multi-factor authentication 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\Acp\Form
+ * @since 5.4
+ */
+class MultifactorAuthenticationForm extends AbstractFormBuilderForm {
+ const AVAILABLE_DURING_OFFLINE_MODE = true;
+
+ /**
+ * @inheritDoc
+ */
+ public $formAction = 'authenticate';
+
+ /**
+ * @var User
+ */
+ private $user;
+
+ /**
+ * @var Setup[]
+ */
+ private $setups;
+
+ /**
+ * @var ObjectType
+ */
+ private $method;
+
+ /**
+ * @var IMultifactorMethod
+ */
+ private $processor;
+
+ /**
+ * @var Setup
+ */
+ private $setup;
+
+ /**
+ * @var string
+ */
+ public $redirectUrl;
+
+ /**
+ * @inheritDoc
+ */
+ public function readParameters() {
+ parent::readParameters();
+
+ if (!empty($_GET['url']) && ApplicationHandler::getInstance()->isInternalURL($_GET['url'])) {
+ $this->redirectUrl = $_GET['url'];
+ }
+
+ if (WCF::getUser()->userID) {
+ $this->performRedirect();
+ }
+
+ $this->user = WCF::getSession()->getPendingUserChange();
+ if (!$this->user) {
+ throw new PermissionDeniedException();
+ }
+
+ $this->setups = Setup::getAllForUser($this->user);
+
+ if (empty($this->setups)) {
+ throw new \LogicException('Unreachable');
+ }
+
+ \uasort($this->setups, function (Setup $a, Setup $b) {
+ return $b->getObjectType()->priority <=> $a->getObjectType()->priority;
+ });
+
+ $setupId = \array_keys($this->setups)[0];
+ if (isset($_GET['id'])) {
+ $setupId = intval($_GET['id']);
+ }
+
+ if (!isset($this->setups[$setupId])) {
+ throw new IllegalLinkException();
+ }
+
+ $this->setup = $this->setups[$setupId];
+ $this->method = $this->setup->getObjectType();
+ \assert($this->method->getDefinition()->definitionName === 'com.woltlab.wcf.multifactor');
+
+ $this->processor = $this->method->getProcessor();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function createForm() {
+ parent::createForm();
+
+ $this->processor->createAuthenticationForm($this->form, $this->setup);
+ }
+
+ public function save() {
+ AbstractForm::save();
+
+ WCF::getDB()->beginTransaction();
+
+ $setup = $this->setup->lock();
+
+ $this->returnData = $this->processor->processAuthenticationForm($this->form, $setup);
+
+ WCF::getDB()->commitTransaction();
+
+ WCF::getSession()->applyPendingUserChange($this->user);
+
+ $this->saved();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function saved() {
+ AbstractForm::saved();
+
+ $this->performRedirect();
+ }
+
+ /**
+ * Returns to the redirectUrl if given and to the landing page otherwise.
+ */
+ protected function performRedirect() {
+ if ($this->redirectUrl) {
+ HeaderUtil::redirect($this->redirectUrl);
+ }
+ else {
+ HeaderUtil::redirect(LinkHandler::getInstance()->getLink());
+ }
+ exit;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function setFormAction() {
+ $this->form->action(LinkHandler::getInstance()->getControllerLink(static::class, [
+ 'object' => $this->setup,
+ 'url' => $this->redirectUrl,
+ ]));
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function assignVariables() {
+ parent::assignVariables();
+
+ WCF::getTPL()->assign([
+ 'setups' => $this->setups,
+ 'user' => $this->user,
+ 'userProfile' => UserProfileRuntimeCache::getInstance()->getObject($this->user->userID),
+ 'setup' => $this->setup,
+ 'redirectUrl' => $this->redirectUrl,
+ ]);
+ }
+}
exit;
}
}
- else if (empty($pathInfo) || !preg_match('~^/?(login|logout)/~i', $pathInfo)) {
+ else if (empty($pathInfo) || !preg_match('~^/?(login|logout|multifactor-authentication|multifactor-authentication-abort)/~i', $pathInfo)) {
if (WCF::getUser()->userID == 0) {
// work-around for AJAX-requests within ACP
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {