[TASK] Ensure HTTP RequestHandlers always return a PSR-7 Repsonse
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Http / RequestHandler.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Install\Http;
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 use Psr\Http\Message\ResponseInterface;
19 use Psr\Http\Message\ServerRequestInterface;
20 use TYPO3\CMS\Core\Core\Bootstrap;
21 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
22 use TYPO3\CMS\Core\FormProtection\InstallToolFormProtection;
23 use TYPO3\CMS\Core\Http\HtmlResponse;
24 use TYPO3\CMS\Core\Http\JsonResponse;
25 use TYPO3\CMS\Core\Http\RequestHandlerInterface;
26 use TYPO3\CMS\Core\Messaging\FlashMessage;
27 use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
28 use TYPO3\CMS\Install\Authentication\AuthenticationService;
29 use TYPO3\CMS\Install\Controller\AbstractController;
30 use TYPO3\CMS\Install\Controller\EnvironmentController;
31 use TYPO3\CMS\Install\Controller\LayoutController;
32 use TYPO3\CMS\Install\Controller\LoginController;
33 use TYPO3\CMS\Install\Controller\MaintenanceController;
34 use TYPO3\CMS\Install\Controller\SettingsController;
35 use TYPO3\CMS\Install\Controller\UpgradeController;
36 use TYPO3\CMS\Install\Service\EnableFileService;
37 use TYPO3\CMS\Install\Service\SessionService;
38 use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
39
40 /**
41 * Default request handler for all requests inside the TYPO3 Install Tool, which does a simple hardcoded
42 * dispatching to a controller based on the get/post variable.
43 */
44 class RequestHandler implements RequestHandlerInterface
45 {
46 /**
47 * Instance of the current TYPO3 bootstrap
48 * @var Bootstrap
49 */
50 protected $bootstrap;
51
52 /**
53 * @var array List of valid controllers
54 */
55 protected $controllers = [
56 'layout' => LayoutController::class,
57 'login' => LoginController::class,
58 'maintenance' => MaintenanceController::class,
59 'settings' => SettingsController::class,
60 'upgrade' => UpgradeController::class,
61 'environment' => EnvironmentController::class,
62 ];
63
64 /**
65 * Constructor handing over the bootstrap
66 *
67 * @param Bootstrap $bootstrap
68 */
69 public function __construct(Bootstrap $bootstrap)
70 {
71 $this->bootstrap = $bootstrap;
72 }
73
74 /**
75 * Handles an install tool request for normal operations
76 *
77 * @param ServerRequestInterface $request
78 * @return ResponseInterface
79 */
80 public function handleRequest(ServerRequestInterface $request): ResponseInterface
81 {
82 $controllerName = $request->getQueryParams()['install']['controller'] ?? 'layout';
83 $actionName = $request->getParsedBody()['install']['action'] ?? $request->getQueryParams()['install']['action'] ?? 'init';
84 $action = $actionName . 'Action';
85
86 $session = $this->initializeSession();
87 if ($actionName === 'init') {
88 $controller = new LayoutController();
89 $response = $controller->initAction($request);
90 } elseif ($actionName === 'checkEnableInstallToolFile') {
91 $response = new JsonResponse([
92 'success' => $this->checkEnableInstallToolFile(),
93 ]);
94 } elseif ($actionName === 'showEnableInstallToolFile') {
95 $controller = new LoginController();
96 $response = $controller->showEnableInstallToolFileAction($request);
97 } elseif ($actionName === 'checkLogin') {
98 if (!$this->checkEnableInstallToolFile() && !$session->isAuthorizedBackendUserSession()) {
99 throw new \RuntimeException('Not authorized', 1505563556);
100 }
101 if ($session->isExpired() || !$session->isAuthorized()) {
102 // Session expired, log out user, start new session
103 $session->resetSession();
104 $session->startSession();
105 $response = new JsonResponse([
106 'success' => false,
107 ]);
108 } else {
109 $session->refreshSession();
110 $response = new JsonResponse([
111 'success' => true,
112 ]);
113 }
114 } elseif ($actionName === 'showLogin') {
115 if (!$this->checkEnableInstallToolFile()) {
116 throw new \RuntimeException('Not authorized', 1505564888);
117 }
118 $controller = new LoginController();
119 $response = $controller->showLoginAction($request);
120 } elseif ($actionName === 'login') {
121 if (!$this->checkEnableInstallToolFile()) {
122 throw new \RuntimeException('Not authorized', 1505567462);
123 }
124 $this->checkSessionToken($request, $session);
125 $this->checkSessionLifetime($session);
126 $password = $request->getParsedBody()['install']['password'] ?? null;
127 $authService = new AuthenticationService($session);
128 if ($authService->loginWithPassword($password)) {
129 $response = new JsonResponse([
130 'success' => true,
131 ]);
132 } else {
133 if (is_null($password) || empty($password)) {
134 $messageQueue = (new FlashMessageQueue('install'))->enqueue(
135 new FlashMessage('Please enter the install tool password', '', FlashMessage::ERROR)
136 );
137 } else {
138 $saltFactory = SaltFactory::getSaltingInstance(null, 'BE');
139 $hashedPassword = $saltFactory->getHashedPassword($password);
140 $messageQueue = (new FlashMessageQueue('install'))->enqueue(
141 new FlashMessage(
142 'Given password does not match the install tool login password. Calculated hash: ' . $hashedPassword,
143 '',
144 FlashMessage::ERROR
145 )
146 );
147 }
148 $response = new JsonResponse([
149 'success' => false,
150 'status' => $messageQueue,
151 ]);
152 }
153 } elseif ($actionName === 'logout') {
154 if (EnableFileService::installToolEnableFileExists() && !EnableFileService::isInstallToolEnableFilePermanent()) {
155 EnableFileService::removeInstallToolEnableFile();
156 }
157 $formProtection = FormProtectionFactory::get(
158 InstallToolFormProtection::class
159 );
160 $formProtection->clean();
161 $session->destroySession();
162 $response = new JsonResponse([
163 'success' => true,
164 ]);
165 } else {
166 if (
167 !$this->checkSessionToken($request, $session)
168 || !$this->checkSessionLifetime($session)
169 || !$session->isAuthorized()
170 ) {
171 return new HtmlResponse('', 401, ['WWW-Authenticate' => 'FormBased']);
172 }
173 $session->refreshSession();
174 if (!array_key_exists($controllerName, $this->controllers)) {
175 throw new \RuntimeException(
176 'Unknown controller ' . $controllerName,
177 1505215756
178 );
179 }
180 /** @var AbstractController $controller */
181 $controller = new $this->controllers[$controllerName];
182 if (!method_exists($controller, $action)) {
183 throw new \RuntimeException(
184 'Unknown action method ' . $action . ' in controller ' . $controllerName,
185 1505216027
186 );
187 }
188 $response = $controller->$action($request);
189 }
190
191 return $response;
192 }
193
194 /**
195 * This request handler can handle any request when not in CLI mode.
196 * Warning: Order of these methods is security relevant and interferes with different access
197 * conditions (new/existing installation). See the single method comments for details.
198 *
199 * @param ServerRequestInterface $request
200 * @return bool Returns always TRUE
201 */
202 public function canHandleRequest(ServerRequestInterface $request): bool
203 {
204 $basicIntegrity = $this->bootstrap->checkIfEssentialConfigurationExists()
205 && !empty($GLOBALS['TYPO3_CONF_VARS']['BE']['installToolPassword'])
206 && !EnableFileService::isFirstInstallAllowed();
207 if (!$basicIntegrity) {
208 return false;
209 }
210 return true;
211 }
212
213 /**
214 * Returns the priority - how eager the handler is to actually handle the request.
215 *
216 * @return int The priority of the request handler.
217 */
218 public function getPriority(): int
219 {
220 return 50;
221 }
222
223 /**
224 * Checks if ENABLE_INSTALL_TOOL exists.
225 *
226 * @return bool
227 */
228 protected function checkEnableInstallToolFile()
229 {
230 return EnableFileService::checkInstallToolEnableFile();
231 }
232
233 /**
234 * Initialize session object.
235 * Subclass will throw exception if session can not be created or if
236 * preconditions like a valid encryption key are not set.
237 *
238 * @return SessionService
239 */
240 protected function initializeSession()
241 {
242 $session = new SessionService();
243 if (!$session->hasSession()) {
244 $session->startSession();
245 }
246 return $session;
247 }
248
249 /**
250 * Use form protection API to find out if protected POST forms are ok.
251 *
252 * @param SessionService $session
253 * @return bool
254 * @throws \RuntimeException
255 */
256 protected function checkSessionToken(ServerRequestInterface $request, SessionService $session): bool
257 {
258 $postValues = $request->getParsedBody()['install'];
259 // no post data is there, so no token check necessary
260 if (empty($postValues)) {
261 return true;
262 }
263 $tokenOk = false;
264 // A token must be given as soon as there is POST data
265 if (isset($postValues['token'])) {
266 $formProtection = FormProtectionFactory::get(
267 InstallToolFormProtection::class
268 );
269 $action = (string)$postValues['action'];
270 if ($action === '') {
271 throw new \RuntimeException(
272 'No POST action given for token check',
273 1369326593
274 );
275 }
276 $tokenOk = $formProtection->validateToken($postValues['token'], 'installTool', $action);
277 }
278 if (!$tokenOk) {
279 $session->resetSession();
280 $session->startSession();
281 }
282 return $tokenOk;
283 }
284
285 /**
286 * Check if session expired.
287 * If the session has expired, the login form is displayed.
288 *
289 * @param SessionService $session
290 * @return bool True if session lifetime is OK
291 */
292 protected function checkSessionLifetime(SessionService $session): bool
293 {
294 $isExpired = $session->isExpired();
295 if ($isExpired) {
296 // Session expired, log out user, start new session
297 $session->resetSession();
298 $session->startSession();
299 }
300 return !$isExpired;
301 }
302 }