Completed
Push — master ( a276ba...2a702e )
by
unknown
18:45
created

Maintenance::checkEnableInstallToolFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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\Install\Middleware;
19
20
use Psr\Container\ContainerInterface;
21
use Psr\Http\Message\ResponseInterface;
22
use Psr\Http\Message\ServerRequestInterface;
23
use Psr\Http\Server\MiddlewareInterface;
24
use Psr\Http\Server\RequestHandlerInterface;
25
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
26
use TYPO3\CMS\Core\Configuration\Features;
27
use TYPO3\CMS\Core\Core\Environment;
28
use TYPO3\CMS\Core\Crypto\PasswordHashing\PasswordHashFactory;
29
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
30
use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
31
use TYPO3\CMS\Core\Http\HtmlResponse;
32
use TYPO3\CMS\Core\Http\JsonResponse;
33
use TYPO3\CMS\Core\Http\Security\ReferrerEnforcer;
34
use TYPO3\CMS\Core\Messaging\FlashMessage;
35
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
36
use TYPO3\CMS\Core\Package\FailsafePackageManager;
37
use TYPO3\CMS\Core\Package\PackageInterface;
38
use TYPO3\CMS\Install\Authentication\AuthenticationService;
39
use TYPO3\CMS\Install\Controller\AbstractController;
40
use TYPO3\CMS\Install\Controller\EnvironmentController;
41
use TYPO3\CMS\Install\Controller\IconController;
42
use TYPO3\CMS\Install\Controller\LayoutController;
43
use TYPO3\CMS\Install\Controller\LoginController;
44
use TYPO3\CMS\Install\Controller\MaintenanceController;
45
use TYPO3\CMS\Install\Controller\SettingsController;
46
use TYPO3\CMS\Install\Controller\UpgradeController;
47
use TYPO3\CMS\Install\Service\EnableFileService;
48
use TYPO3\CMS\Install\Service\SessionService;
49
50
/**
51
 * Default middleware for all requests inside the TYPO3 Install Tool, which does a simple hardcoded
52
 * dispatching to a controller based on the get/post variable.
53
 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
54
 */
55
class Maintenance implements MiddlewareInterface
56
{
57
    /**
58
     * @var FailsafePackageManager
59
     */
60
    protected $packageManager;
61
62
    /**
63
     * @var ConfigurationManager
64
     */
65
    protected $configurationManager;
66
67
    /**
68
     * @var PasswordHashFactory
69
     */
70
    protected $passwordHashFactory;
71
72
    /**
73
     * @var ContainerInterface
74
     */
75
    private $container;
76
77
    /**
78
     * @var array List of valid controllers
79
     */
80
    protected $controllers = [
81
        'icon' => IconController::class,
82
        'layout' => LayoutController::class,
83
        'login' => LoginController::class,
84
        'maintenance' => MaintenanceController::class,
85
        'settings' => SettingsController::class,
86
        'upgrade' => UpgradeController::class,
87
        'environment' => EnvironmentController::class,
88
    ];
89
90
    public function __construct(
91
        FailsafePackageManager $packageManager,
92
        ConfigurationManager $configurationManager,
93
        PasswordHashFactory $passwordHashFactory,
94
        ContainerInterface $container
95
    ) {
96
        $this->packageManager = $packageManager;
97
        $this->configurationManager = $configurationManager;
98
        $this->passwordHashFactory = $passwordHashFactory;
99
        $this->container = $container;
100
    }
101
102
    /**
103
     * Handles an Install Tool request for normal operations
104
     *
105
     * @param ServerRequestInterface $request
106
     * @param RequestHandlerInterface $handler
107
     * @return ResponseInterface
108
     */
109
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
110
    {
111
        if (!$this->canHandleRequest($request)) {
112
            return $handler->handle($request);
113
        }
114
115
        $controllerName = $request->getQueryParams()['install']['controller'] ?? 'layout';
116
        $actionName = $request->getParsedBody()['install']['action'] ?? $request->getQueryParams()['install']['action'] ?? 'init';
117
118
        if ($actionName === 'showEnableInstallToolFile' && EnableFileService::isInstallToolEnableFilePermanent()) {
119
            $actionName = 'showLogin';
120
        }
121
122
        $action = $actionName . 'Action';
123
124
        // not session related actions
125
        if ($actionName === 'init') {
126
            $controller = $this->container->get(LayoutController::class);
127
            return $controller->initAction($request);
128
        }
129
        if ($actionName === 'checkEnableInstallToolFile') {
130
            return new JsonResponse([
131
                'success' => $this->checkEnableInstallToolFile(),
132
            ]);
133
        }
134
        if ($actionName === 'showEnableInstallToolFile') {
135
            $controller = $this->container->get(LoginController::class);
136
            return $controller->showEnableInstallToolFileAction($request);
137
        }
138
        if ($actionName === 'showLogin') {
139
            if (!$this->checkEnableInstallToolFile()) {
140
                throw new \RuntimeException('Not authorized', 1505564888);
141
            }
142
            $controller = $this->container->get(LoginController::class);
143
            return $controller->showLoginAction($request);
144
        }
145
146
        // session related actions
147
        $session = new SessionService();
148
        if ($actionName === 'preAccessCheck') {
149
            $response = new JsonResponse([
150
                'installToolLocked' => !$this->checkEnableInstallToolFile(),
151
                'isAuthorized' => $session->isAuthorized()
152
            ]);
153
        } elseif ($actionName === 'checkLogin') {
154
            if (!$this->checkEnableInstallToolFile() && !$session->isAuthorizedBackendUserSession()) {
155
                throw new \RuntimeException('Not authorized', 1505563556);
156
            }
157
            if ($session->isAuthorized()) {
158
                $session->refreshSession();
159
                $response = new JsonResponse([
160
                    'success' => true,
161
                ]);
162
            } else {
163
                // Session expired, log out user, start new session
164
                $session->resetSession();
165
                $session->startSession();
166
                $response = new JsonResponse([
167
                    'success' => false,
168
                ]);
169
            }
170
        } elseif ($actionName === 'login') {
171
            $session->initializeSession();
172
            if (!$this->checkEnableInstallToolFile()) {
173
                throw new \RuntimeException('Not authorized', 1505567462);
174
            }
175
            $this->checkSessionToken($request, $session);
176
            $this->checkSessionLifetime($session);
177
            $password = $request->getParsedBody()['install']['password'] ?? null;
178
            $authService = new AuthenticationService($session);
179
            if ($authService->loginWithPassword($password, $request)) {
180
                $response = new JsonResponse([
181
                    'success' => true,
182
                ]);
183
            } else {
184
                if ($password === null || empty($password)) {
185
                    $messageQueue = (new FlashMessageQueue('install'))->enqueue(
186
                        new FlashMessage('Please enter the install tool password', '', FlashMessage::ERROR)
187
                    );
188
                } else {
189
                    $hashInstance = $this->passwordHashFactory->getDefaultHashInstance('BE');
190
                    $hashedPassword = $hashInstance->getHashedPassword($password);
191
                    $messageQueue = (new FlashMessageQueue('install'))->enqueue(
192
                        new FlashMessage(
193
                            'Given password does not match the install tool login password. Calculated hash: ' . $hashedPassword,
194
                            '',
195
                            FlashMessage::ERROR
196
                        )
197
                    );
198
                }
199
                $response = new JsonResponse([
200
                    'success' => false,
201
                    'status' => $messageQueue,
202
                ]);
203
            }
204
        } elseif ($actionName === 'logout') {
205
            if (EnableFileService::installToolEnableFileExists() && !EnableFileService::isInstallToolEnableFilePermanent()) {
206
                EnableFileService::removeInstallToolEnableFile();
207
            }
208
            $formProtection = FormProtectionFactory::get(
209
                InstallToolFormProtection::class
210
            );
211
            $formProtection->clean();
212
            $session->destroySession();
213
            $response = new JsonResponse([
214
                'success' => true,
215
            ]);
216
        } else {
217
            $enforceReferrerResponse = $this->enforceReferrer($request);
218
            if ($enforceReferrerResponse instanceof ResponseInterface) {
0 ignored issues
show
introduced by
$enforceReferrerResponse is always a sub-type of Psr\Http\Message\ResponseInterface.
Loading history...
219
                return $enforceReferrerResponse;
220
            }
221
            $session->initializeSession();
222
            if (
223
                !$this->checkSessionToken($request, $session)
224
                || !$this->checkSessionLifetime($session)
225
                || !$session->isAuthorized()
226
            ) {
227
                return new HtmlResponse('', 403);
228
            }
229
            $session->refreshSession();
230
            if (!array_key_exists($controllerName, $this->controllers)) {
231
                throw new \RuntimeException(
232
                    'Unknown controller ' . $controllerName,
233
                    1505215756
234
                );
235
            }
236
            $this->recreatePackageStatesFileIfMissing();
237
            $className = $this->controllers[$controllerName];
238
            /** @var AbstractController $controller */
239
            $controller = $this->container->get($className);
240
            if (!method_exists($controller, $action)) {
241
                throw new \RuntimeException(
242
                    'Unknown action method ' . $action . ' in controller ' . $controllerName,
243
                    1505216027
244
                );
245
            }
246
            $response = $controller->$action($request);
247
        }
248
249
        return $response;
250
    }
251
252
    /**
253
     * This request handler can handle any request when not in CLI mode.
254
     * Warning: Order of these methods is security relevant and interferes with different access
255
     * conditions (new/existing installation). See the single method comments for details.
256
     *
257
     * @param ServerRequestInterface $request
258
     * @return bool Returns always TRUE
259
     */
260
    protected function canHandleRequest(ServerRequestInterface $request): bool
261
    {
262
        $basicIntegrity = $this->checkIfEssentialConfigurationExists()
263
            && !empty($GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'])
264
            && !EnableFileService::isFirstInstallAllowed();
265
        if (!$basicIntegrity) {
266
            return false;
267
        }
268
        return true;
269
    }
270
271
    /**
272
     * Checks if ENABLE_INSTALL_TOOL exists.
273
     *
274
     * @return bool
275
     */
276
    protected function checkEnableInstallToolFile()
277
    {
278
        return EnableFileService::checkInstallToolEnableFile();
279
    }
280
281
    /**
282
     * Use form protection API to find out if protected POST forms are ok.
283
     *
284
     * @param ServerRequestInterface $request
285
     * @param SessionService $session
286
     * @return bool
287
     */
288
    protected function checkSessionToken(ServerRequestInterface $request, SessionService $session): bool
289
    {
290
        $postValues = $request->getParsedBody()['install'];
291
        // no post data is there, so no token check necessary
292
        if (empty($postValues)) {
293
            return true;
294
        }
295
        $tokenOk = false;
296
        // A token must be given as soon as there is POST data
297
        if (isset($postValues['token'])) {
298
            $formProtection = FormProtectionFactory::get(
299
                InstallToolFormProtection::class
300
            );
301
            $action = (string)$postValues['action'];
302
            if ($action === '') {
303
                throw new \RuntimeException(
304
                    'No POST action given for token check',
305
                    1369326593
306
                );
307
            }
308
            $tokenOk = $formProtection->validateToken($postValues['token'], 'installTool', $action);
309
        }
310
        if (!$tokenOk) {
311
            $session->resetSession();
312
            $session->startSession();
313
        }
314
        return $tokenOk;
315
    }
316
317
    /**
318
     * Check if session expired.
319
     * If the session has expired, the login form is displayed.
320
     *
321
     * @param SessionService $session
322
     * @return bool True if session lifetime is OK
323
     */
324
    protected function checkSessionLifetime(SessionService $session): bool
325
    {
326
        $isExpired = $session->isExpired();
327
        if ($isExpired) {
328
            // Session expired, log out user, start new session
329
            $session->resetSession();
330
            $session->startSession();
331
        }
332
        return !$isExpired;
333
    }
334
335
    /**
336
     * Check if LocalConfiguration.php and PackageStates.php exist
337
     *
338
     * @return bool TRUE when the essential configuration is available, otherwise FALSE
339
     */
340
    protected function checkIfEssentialConfigurationExists(): bool
341
    {
342
        return file_exists($this->configurationManager->getLocalConfigurationFileLocation());
343
    }
344
345
    /**
346
     * Create PackageStates.php if missing and LocalConfiguration exists.
347
     *
348
     * It is fired if PackageStates.php is deleted on a running instance,
349
     * all packages marked as "part of minimal system" are activated in this case.
350
     */
351
    protected function recreatePackageStatesFileIfMissing(): void
352
    {
353
        if (!file_exists(Environment::getLegacyConfigPath() . '/PackageStates.php')) {
354
            $packages = $this->packageManager->getAvailablePackages();
355
            foreach ($packages as $package) {
356
                if ($package instanceof PackageInterface && $package->isPartOfMinimalUsableSystem()) {
357
                    $this->packageManager->activatePackage($package->getPackageKey());
358
                }
359
            }
360
            $this->packageManager->forceSortAndSavePackageStates();
361
        }
362
    }
363
364
    /**
365
     * Evaluates HTTP `Referer` header (which is denied by client to be a custom
366
     * value) - attempts to ensure the value is given using a HTML client refresh.
367
     * see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer
368
     *
369
     * @param ServerRequestInterface $request
370
     * @return ResponseInterface|null
371
     */
372
    protected function enforceReferrer(ServerRequestInterface $request): ?ResponseInterface
373
    {
374
        if (!(new Features())->isFeatureEnabled('security.backend.enforceReferrer')) {
375
            return null;
376
        }
377
        return (new ReferrerEnforcer($request))->handle([
378
            'flags' => ['refresh-always'],
379
            'subject' => 'Install Tool',
380
        ]);
381
    }
382
}
383