Passed
Push — master ( 428a99...6f7ef8 )
by
unknown
19:55
created

MfaConfigurationController::handleRequest()   C

Complexity

Conditions 17
Paths 39

Size

Total Lines 50
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 32
c 1
b 0
f 0
dl 0
loc 50
rs 5.2166
cc 17
nc 39
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Backend\Controller;
19
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
use Psr\Http\Message\UriInterface;
23
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
24
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderManifestInterface;
25
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager;
26
use TYPO3\CMS\Core\Authentication\Mfa\MfaViewType;
27
use TYPO3\CMS\Core\Http\HtmlResponse;
28
use TYPO3\CMS\Core\Http\RedirectResponse;
29
use TYPO3\CMS\Core\Imaging\Icon;
30
use TYPO3\CMS\Core\Messaging\FlashMessage;
31
use TYPO3\CMS\Core\Messaging\FlashMessageService;
32
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
33
use TYPO3\CMS\Core\Utility\GeneralUtility;
34
use TYPO3\CMS\Extbase\Mvc\View\ViewInterface;
35
use TYPO3\CMS\Fluid\View\StandaloneView;
36
37
/**
38
 * Controller to configure MFA providers in the backend
39
 *
40
 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
41
 */
42
class MfaConfigurationController extends AbstractMfaController
43
{
44
    protected array $allowedActions = ['overview', 'setup', 'activate', 'deactivate', 'unlock', 'edit', 'save'];
45
    private array $providerActionsWhenInactive = ['setup', 'activate'];
46
    private array $providerActionsWhenActive = ['deactivate', 'unlock', 'edit', 'save'];
47
48
    /**
49
     * Main entry point, checking prerequisite, initializing and setting
50
     * up the view and finally dispatching to the requested action.
51
     */
52
    public function handleRequest(ServerRequestInterface $request): ResponseInterface
53
    {
54
        $action = (string)($request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'overview');
55
56
        if (!$this->isActionAllowed($action)) {
57
            return new HtmlResponse('Action not allowed', 400);
58
        }
59
60
        $mfaProvider = null;
61
        $identifier = (string)($request->getQueryParams()['identifier'] ?? $request->getParsedBody()['identifier'] ?? '');
62
        // Check if given identifier is valid
63
        if ($this->isValidIdentifier($identifier)) {
64
            $mfaProvider = $this->mfaProviderRegistry->getProvider($identifier);
65
        }
66
        // All actions expect "overview" require a provider to deal with.
67
        // If non is found at this point, initiate a redirect to the overview.
68
        if ($mfaProvider === null && $action !== 'overview') {
69
            $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerNotFound'), '', FlashMessage::ERROR);
70
            return new RedirectResponse($this->getActionUri('overview'));
71
        }
72
        // If a valid provider is given, check if the requested action can be performed on this provider
73
        if ($mfaProvider !== null) {
74
            $isProviderActive = $mfaProvider->isActive(
75
                MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser())
76
            );
77
            // Some actions require the provider to be inactive
78
            if ($isProviderActive && in_array($action, $this->providerActionsWhenInactive, true)) {
79
                $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerActive'), '', FlashMessage::ERROR);
80
                return new RedirectResponse($this->getActionUri('overview'));
81
            }
82
            // Some actions require the provider to be active
83
            if (!$isProviderActive && in_array($action, $this->providerActionsWhenActive, true)) {
84
                $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerNotActive'), '', FlashMessage::ERROR);
85
                return new RedirectResponse($this->getActionUri('overview'));
86
            }
87
        }
88
89
        switch ($action) {
90
            case 'overview':
91
                return $this->overviewAction($request, $this->initializeView($action));
92
            case 'setup':
93
            case 'edit':
94
                return $this->{$action . 'Action'}($request, $mfaProvider, $this->initializeView($action));
95
            case 'activate':
96
            case 'deactivate':
97
            case 'unlock':
98
            case 'save':
99
                return $this->{$action . 'Action'}($request, $mfaProvider);
100
            default:
101
                return new HtmlResponse('Action not allowed', 400);
102
        }
103
    }
104
105
    /**
106
     * Setup the overview with all available MFA providers
107
     */
108
    public function overviewAction(ServerRequestInterface $request, ViewInterface $view): ResponseInterface
109
    {
110
        $this->addOverviewButtons($request);
111
        $view->assignMultiple([
112
            'providers' => $this->allowedProviders,
113
            'defaultProvider' => $this->getDefaultProviderIdentifier(),
114
            'recommendedProvider' => $this->getRecommendedProviderIdentifier(),
115
            'setupRequired' => $this->mfaRequired && !$this->mfaProviderRegistry->hasActiveProviders($this->getBackendUser())
116
        ]);
117
        $this->moduleTemplate->setContent($view->render());
118
        return new HtmlResponse($this->moduleTemplate->renderContent());
119
    }
120
121
    /**
122
     * Render form to setup a provider by using provider specific content
123
     */
124
    public function setupAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider, ViewInterface $view): ResponseInterface
125
    {
126
        $this->addFormButtons();
127
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
128
        $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::SETUP);
129
        $view->assignMultiple([
130
            'provider' => $mfaProvider,
131
            'providerContent' => $providerResponse->getBody()
132
        ]);
133
        $this->moduleTemplate->setContent($view->render());
134
        return new HtmlResponse($this->moduleTemplate->renderContent());
135
    }
136
137
    /**
138
     * Handle activate request, receiving from the setup view
139
     * by forwarding the request to the appropriate provider.
140
     * Furthermore, add the provider as default provider in case
141
     * it is the recommended provider for this user, or no default
142
     * provider is yet defined the newly activated provider is allowed
143
     * to be a default provider and there are no other providers which
144
     * would suite as default provider.
145
     */
146
    public function activateAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface
147
    {
148
        $backendUser = $this->getBackendUser();
149
        $isRecommendedProvider = $this->getRecommendedProviderIdentifier() === $mfaProvider->getIdentifier();
150
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $backendUser);
151
        $languageService = $this->getLanguageService();
152
        if (!$mfaProvider->activate($request, $propertyManager)) {
153
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:activate.failure'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::ERROR);
154
            return new RedirectResponse($this->getActionUri('setup', ['identifier' => $mfaProvider->getIdentifier()]));
155
        }
156
        if ($isRecommendedProvider
157
            || (
158
                $this->getDefaultProviderIdentifier() === ''
159
                && $mfaProvider->isDefaultProviderAllowed()
160
                && !$this->hasSuitableDefaultProviders([$mfaProvider->getIdentifier()])
161
            )
162
        ) {
163
            $this->setDefaultProvider($mfaProvider);
164
        }
165
        // If this is the first activated provider, the user has logged in without being required
166
        // to pass the MFA challenge. Therefore no session entry exists. To prevent the challenge
167
        // from showing up after the activation we need to set the session data here.
168
        if (!(bool)($backendUser->getSessionData('mfa') ?? false)) {
169
            $backendUser->setSessionData('mfa', true);
170
        }
171
        $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:activate.success'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::OK);
172
        return new RedirectResponse($this->getActionUri('overview'));
173
    }
174
175
    /**
176
     * Handle deactivate request by forwarding the request to the
177
     * appropriate provider. Also remove the provider as default
178
     * provider from user UC, if set.
179
     */
180
    public function deactivateAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface
181
    {
182
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
183
        $languageService = $this->getLanguageService();
184
        if (!$mfaProvider->deactivate($request, $propertyManager)) {
185
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:deactivate.failure'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::ERROR);
186
        } else {
187
            if ($this->isDefaultProvider($mfaProvider)) {
188
                $this->removeDefaultProvider();
189
            }
190
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:deactivate.success'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::OK);
191
        }
192
        return new RedirectResponse($this->getActionUri('overview'));
193
    }
194
195
    /**
196
     * Handle unlock request by forwarding the request to the appropriate provider
197
     */
198
    public function unlockAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface
199
    {
200
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
201
        $languageService = $this->getLanguageService();
202
        if (!$mfaProvider->unlock($request, $propertyManager)) {
203
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:unlock.failure'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::ERROR);
204
        } else {
205
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:unlock.success'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::OK);
206
        }
207
        return new RedirectResponse($this->getActionUri('overview'));
208
    }
209
210
    /**
211
     * Render form to edit a provider by using provider specific content
212
     */
213
    public function editAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider, ViewInterface $view): ResponseInterface
214
    {
215
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
216
        if ($mfaProvider->isLocked($propertyManager)) {
217
            // Do not show edit view for locked providers
218
            $this->addFlashMessage($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:providerIsLocked'), '', FlashMessage::ERROR);
219
            return new RedirectResponse($this->getActionUri('overview'));
220
        }
221
        $this->addFormButtons();
222
        $providerResponse = $mfaProvider->handleRequest($request, $propertyManager, MfaViewType::EDIT);
223
        $view->assignMultiple([
224
            'provider' => $mfaProvider,
225
            'providerContent' => $providerResponse->getBody(),
226
            'isDefaultProvider' => $this->isDefaultProvider($mfaProvider)
227
        ]);
228
        $this->moduleTemplate->setContent($view->render());
229
        return new HtmlResponse($this->moduleTemplate->renderContent());
230
    }
231
232
    /**
233
     * Handle save request, receiving from the edit view by
234
     * forwarding the request to the appropriate provider.
235
     */
236
    public function saveAction(ServerRequestInterface $request, MfaProviderManifestInterface $mfaProvider): ResponseInterface
237
    {
238
        $propertyManager = MfaProviderPropertyManager::create($mfaProvider, $this->getBackendUser());
239
        $languageService = $this->getLanguageService();
240
        if (!$mfaProvider->update($request, $propertyManager)) {
241
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:save.failure'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::ERROR);
242
        } else {
243
            if ((bool)($request->getParsedBody()['defaultProvider'] ?? false)) {
244
                $this->setDefaultProvider($mfaProvider);
245
            } elseif ($this->isDefaultProvider($mfaProvider)) {
246
                $this->removeDefaultProvider();
247
            }
248
            $this->addFlashMessage(sprintf($languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_mfa.xlf:save.success'), $languageService->sL($mfaProvider->getTitle())), '', FlashMessage::OK);
249
        }
250
        if (!$mfaProvider->isActive($propertyManager)) {
251
            return new RedirectResponse($this->getActionUri('overview'));
252
        }
253
        return new RedirectResponse($this->getActionUri('edit', ['identifier' => $mfaProvider->getIdentifier()]));
254
    }
255
256
    /**
257
     * Initialize the standalone view and set the template name
258
     */
259
    protected function initializeView(string $templateName): ViewInterface
260
    {
261
        $view = GeneralUtility::makeInstance(StandaloneView::class);
262
        $view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/Mfa']);
263
        $view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials']);
264
        $view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
265
        $view->setTemplate($templateName);
266
        return $view;
267
    }
268
269
    /**
270
     * Build a uri for the current controller based on the
271
     * given action, respecting additional parameters.
272
     */
273
    protected function getActionUri(string $action, array $additionalParameters = []): UriInterface
274
    {
275
        if (!$this->isActionAllowed($action)) {
276
            $action = 'overview';
277
        }
278
        return $this->uriBuilder->buildUriFromRoute('mfa', array_merge(['action' => $action], $additionalParameters));
279
    }
280
281
    /**
282
     * Check if there are more suitable default providers for the current user
283
     */
284
    protected function hasSuitableDefaultProviders(array $excludedProviders = []): bool
285
    {
286
        foreach ($this->allowedProviders as $identifier => $provider) {
287
            if (!in_array($identifier, $excludedProviders, true)
288
                && $provider->isDefaultProviderAllowed()
289
                && $provider->isActive(MfaProviderPropertyManager::create($provider, $this->getBackendUser()))
290
            ) {
291
                return true;
292
            }
293
        }
294
        return false;
295
    }
296
297
    /**
298
     * Get the default provider
299
     */
300
    protected function getDefaultProviderIdentifier(): string
301
    {
302
        $defaultProviderIdentifier = (string)($this->getBackendUser()->uc['mfa']['defaultProvider'] ?? '');
303
        // The default provider value is only valid, if the corresponding provider exist and is allowed
304
        if ($this->isValidIdentifier($defaultProviderIdentifier)) {
305
            $defaultProvider = $this->mfaProviderRegistry->getProvider($defaultProviderIdentifier);
306
            $propertyManager = MfaProviderPropertyManager::create($defaultProvider, $this->getBackendUser());
307
            // Also check if the provider is activated for the user
308
            if ($defaultProvider->isActive($propertyManager)) {
309
                return $defaultProviderIdentifier;
310
            }
311
        }
312
313
        // If the stored provider is not valid, clean up the UC
314
        $this->removeDefaultProvider();
315
        return '';
316
    }
317
318
    /**
319
     * Get the recommended provider
320
     */
321
    protected function getRecommendedProviderIdentifier(): string
322
    {
323
        $recommendedProviderIdentifier = (string)($this->mfaTsConfig['recommendedProvider'] ?? '');
324
        // Check if valid and allowed to be default provider, which is obviously a prerequisite
325
        if (!$this->isValidIdentifier($recommendedProviderIdentifier)
326
            || !$this->mfaProviderRegistry->getProvider($recommendedProviderIdentifier)->isDefaultProviderAllowed()
327
        ) {
328
            // If the provider, defined in user TSconfig is not valid or is not set, check the globally defined
329
            $recommendedProviderIdentifier = (string)($GLOBALS['TYPO3_CONF_VARS']['BE']['recommendedMfaProvider'] ?? '');
330
            if (!$this->isValidIdentifier($recommendedProviderIdentifier)
331
                || !$this->mfaProviderRegistry->getProvider($recommendedProviderIdentifier)->isDefaultProviderAllowed()
332
            ) {
333
                // If also not valid or not set, return
334
                return '';
335
            }
336
        }
337
338
        $provider = $this->mfaProviderRegistry->getProvider($recommendedProviderIdentifier);
339
        $propertyManager = MfaProviderPropertyManager::create($provider, $this->getBackendUser());
340
        // If the defined recommended provider is valid, check if it is not yet activated
341
        return !$provider->isActive($propertyManager) ? $recommendedProviderIdentifier : '';
342
    }
343
344
    protected function isDefaultProvider(MfaProviderManifestInterface $mfaProvider): bool
345
    {
346
        return $this->getDefaultProviderIdentifier() === $mfaProvider->getIdentifier();
347
    }
348
349
    protected function setDefaultProvider(MfaProviderManifestInterface $mfaProvider): void
350
    {
351
        $this->getBackendUser()->uc['mfa']['defaultProvider'] = $mfaProvider->getIdentifier();
352
        $this->getBackendUser()->writeUC();
353
    }
354
355
    protected function removeDefaultProvider(): void
356
    {
357
        $this->getBackendUser()->uc['mfa']['defaultProvider'] = '';
358
        $this->getBackendUser()->writeUC();
359
    }
360
361
    protected function addFlashMessage(string $message, string $title = '', int $severity = FlashMessage::INFO): void
362
    {
363
        $flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $title, $severity, true);
364
        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
365
        $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
366
        $defaultFlashMessageQueue->enqueue($flashMessage);
367
    }
368
369
    protected function addOverviewButtons(ServerRequestInterface $request): void
370
    {
371
        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
372
373
        if (($returnUrl = $this->getReturnUrl($request)) !== '') {
374
            $button = $buttonBar
375
                ->makeLinkButton()
376
                ->setHref($returnUrl)
377
                ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL))
378
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
379
                ->setShowLabelText(true);
380
            $buttonBar->addButton($button);
381
        }
382
383
        $reloadButton = $buttonBar
384
            ->makeLinkButton()
385
            ->setHref($request->getAttribute('normalizedParams')->getRequestUri())
386
            ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
387
            ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-refresh', Icon::SIZE_SMALL));
388
        $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
389
    }
390
391
    protected function addFormButtons(): void
392
    {
393
        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
394
        $lang = $this->getLanguageService();
395
396
        $closeButton = $buttonBar
397
            ->makeLinkButton()
398
            ->setHref($this->uriBuilder->buildUriFromRoute('mfa', ['action' => 'overview']))
399
            ->setClasses('t3js-editform-close')
400
            ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
401
            ->setShowLabelText(true)
402
            ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL));
403
        $buttonBar->addButton($closeButton);
404
405
        $saveButton = $buttonBar
406
            ->makeInputButton()
407
            ->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'))
408
            ->setName('save')
409
            ->setValue('1')
410
            ->setShowLabelText(true)
411
            ->setForm('mfaConfigurationController')
412
            ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL));
413
        $buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
414
    }
415
416
    protected function getReturnUrl(ServerRequestInterface $request): string
417
    {
418
        $returnUrl = GeneralUtility::sanitizeLocalUrl(
419
            $request->getQueryParams()['returnUrl'] ?? $request->getParsedBody()['returnUrl'] ?? ''
420
        );
421
422
        if ($returnUrl === '' && ExtensionManagementUtility::isLoaded('setup')) {
423
            $returnUrl = (string)$this->uriBuilder->buildUriFromRoute('user_setup');
424
        }
425
426
        return $returnUrl;
427
    }
428
}
429