[FEATURE] Add support for PSR-15 HTTP middlewares
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Http / RequestHandler.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Frontend\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 Psr\Http\Server\RequestHandlerInterface as PsrRequestHandlerInterface;
21 use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
22 use TYPO3\CMS\Core\Core\Bootstrap;
23 use TYPO3\CMS\Core\FrontendEditing\FrontendEditingController;
24 use TYPO3\CMS\Core\Http\NullResponse;
25 use TYPO3\CMS\Core\Http\RequestHandlerInterface;
26 use TYPO3\CMS\Core\Log\LogManager;
27 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
28 use TYPO3\CMS\Core\Utility\GeneralUtility;
29 use TYPO3\CMS\Core\Utility\MathUtility;
30 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
31 use TYPO3\CMS\Frontend\Page\PageGenerator;
32 use TYPO3\CMS\Frontend\Utility\CompressionUtility;
33 use TYPO3\CMS\Frontend\View\AdminPanelView;
34
35 /**
36 * This is the main entry point of the TypoScript driven standard front-end
37 *
38 * Basically put, this is the script which all requests for TYPO3 delivered pages goes to in the
39 * frontend (the website). The script instantiates a $TSFE object, includes libraries and does a little logic here
40 * and there in order to instantiate the right classes to create the webpage.
41 * Previously, this was called index_ts.php and also included the logic for the lightweight "eID" concept,
42 * which is now handled in a separate request handler (EidRequestHandler).
43 */
44 class RequestHandler implements RequestHandlerInterface, PsrRequestHandlerInterface
45 {
46 /**
47 * Instance of the current TYPO3 bootstrap
48 * @var Bootstrap
49 */
50 protected $bootstrap;
51
52 /**
53 * Instance of the timetracker
54 * @var TimeTracker
55 */
56 protected $timeTracker;
57
58 /**
59 * Instance of the TSFE object
60 * @var TypoScriptFrontendController
61 */
62 protected $controller;
63
64 /**
65 * The request handed over
66 * @var ServerRequestInterface
67 */
68 protected $request;
69
70 /**
71 * Constructor handing over the bootstrap and the original request
72 *
73 * @param Bootstrap $bootstrap
74 */
75 public function __construct(Bootstrap $bootstrap)
76 {
77 $this->bootstrap = $bootstrap;
78 }
79
80 /**
81 * Handles a frontend request
82 *
83 * @param ServerRequestInterface $request
84 * @return ResponseInterface
85 */
86 public function handleRequest(ServerRequestInterface $request): ResponseInterface
87 {
88 return $this->handle($request);
89 }
90
91 /**
92 * Handles a frontend request, after finishing running middlewares
93 *
94 * @param ServerRequestInterface $request
95 * @return ResponseInterface|null
96 */
97 public function handle(ServerRequestInterface $request): ResponseInterface
98 {
99 $response = null;
100 $this->request = $request;
101 $this->initializeTimeTracker();
102
103 // Hook to preprocess the current request:
104 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/index_ts.php']['preprocessRequest'] ?? [] as $hookFunction) {
105 $hookParameters = [];
106 GeneralUtility::callUserFunction($hookFunction, $hookParameters, $hookParameters);
107 }
108
109 $this->initializeController();
110
111 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_force']
112 && !GeneralUtility::cmpIP(
113 GeneralUtility::getIndpEnv('REMOTE_ADDR'),
114 $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']
115 )
116 ) {
117 $this->controller->pageUnavailableAndExit('This page is temporarily unavailable.');
118 }
119
120 $this->controller->connectToDB();
121
122 // Output compression
123 // Remove any output produced until now
124 $this->bootstrap->endOutputBufferingAndCleanPreviousOutput();
125 $this->initializeOutputCompression();
126
127 // Initializing the Frontend User
128 $this->timeTracker->push('Front End user initialized', '');
129 $this->controller->initFEuser();
130 $this->timeTracker->pull();
131
132 // Initializing a possible logged-in Backend User
133 /** @var $GLOBALS['BE_USER'] \TYPO3\CMS\Backend\FrontendBackendUserAuthentication */
134 $GLOBALS['BE_USER'] = $this->controller->initializeBackendUser();
135
136 // Process the ID, type and other parameters.
137 // After this point we have an array, $page in TSFE, which is the page-record
138 // of the current page, $id.
139 $this->timeTracker->push('Process ID', '');
140 // Initialize admin panel since simulation settings are required here:
141 if ($this->controller->isBackendUserLoggedIn()) {
142 $GLOBALS['BE_USER']->initializeAdminPanel();
143 $this->bootstrap
144 ->initializeBackendRouter()
145 ->loadExtTables();
146 }
147 $this->controller->checkAlternativeIdMethods();
148 $this->controller->clear_preview();
149 $this->controller->determineId();
150
151 // Now, if there is a backend user logged in and he has NO access to this page,
152 // then re-evaluate the id shown! _GP('ADMCMD_noBeUser') is placed here because
153 // \TYPO3\CMS\Version\Hook\PreviewHook might need to know if a backend user is logged in.
154 if (
155 $this->controller->isBackendUserLoggedIn()
156 && (!$GLOBALS['BE_USER']->extPageReadAccess($this->controller->page) || GeneralUtility::_GP('ADMCMD_noBeUser'))
157 ) {
158 // Remove user
159 unset($GLOBALS['BE_USER']);
160 $this->controller->beUserLogin = false;
161 // Re-evaluate the page-id.
162 $this->controller->checkAlternativeIdMethods();
163 $this->controller->clear_preview();
164 $this->controller->determineId();
165 }
166
167 $this->controller->makeCacheHash();
168 $this->timeTracker->pull();
169
170 // Admin Panel & Frontend editing
171 if ($this->controller->isBackendUserLoggedIn()) {
172 $GLOBALS['BE_USER']->initializeFrontendEdit();
173 if ($GLOBALS['BE_USER']->adminPanel instanceof AdminPanelView) {
174 $this->bootstrap->initializeLanguageObject();
175 }
176 if ($GLOBALS['BE_USER']->frontendEdit instanceof FrontendEditingController) {
177 $GLOBALS['BE_USER']->frontendEdit->initConfigOptions();
178 }
179 }
180
181 // Starts the template
182 $this->timeTracker->push('Start Template', '');
183 $this->controller->initTemplate();
184 $this->timeTracker->pull();
185 // Get from cache
186 $this->timeTracker->push('Get Page from cache', '');
187 $this->controller->getFromCache();
188 $this->timeTracker->pull();
189 // Get config if not already gotten
190 // After this, we should have a valid config-array ready
191 $this->controller->getConfigArray();
192 // Setting language and locale
193 $this->timeTracker->push('Setting language and locale', '');
194 $this->controller->settingLanguage();
195 $this->controller->settingLocale();
196 $this->timeTracker->pull();
197
198 // Convert POST data to utf-8 for internal processing if metaCharset is different
199 $this->controller->convPOSTCharset();
200
201 $this->controller->initializeRedirectUrlHandlers();
202
203 $this->controller->handleDataSubmission();
204
205 // Check for shortcut page and redirect
206 $this->controller->checkPageForShortcutRedirect();
207 $this->controller->checkPageForMountpointRedirect();
208
209 // Generate page
210 $this->controller->setUrlIdToken();
211 $this->timeTracker->push('Page generation', '');
212 if ($this->controller->isGeneratePage()) {
213 $this->controller->generatePage_preProcessing();
214 $this->controller->preparePageContentGeneration();
215 // Content generation
216 if (!$this->controller->isINTincScript()) {
217 PageGenerator::renderContent();
218 $this->controller->setAbsRefPrefix();
219 }
220 $this->controller->generatePage_postProcessing();
221 } elseif ($this->controller->isINTincScript()) {
222 $this->controller->preparePageContentGeneration();
223 }
224 $this->controller->releaseLocks();
225 $this->timeTracker->pull();
226
227 // Render non-cached parts
228 if ($this->controller->isINTincScript()) {
229 $this->timeTracker->push('Non-cached objects', '');
230 $this->controller->INTincScript();
231 $this->timeTracker->pull();
232 }
233
234 // Output content
235 $sendTSFEContent = false;
236 if ($this->controller->isOutputting()) {
237 $this->timeTracker->push('Print Content', '');
238 $this->controller->processOutput();
239 $sendTSFEContent = true;
240 $this->timeTracker->pull();
241 }
242 // Store session data for fe_users
243 $this->controller->storeSessionData();
244
245 // Create a Response object when sending content
246 if ($sendTSFEContent) {
247 $response = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\Response::class);
248 }
249
250 // Statistics
251 $GLOBALS['TYPO3_MISC']['microtime_end'] = microtime(true);
252 if ($sendTSFEContent) {
253 if (isset($this->controller->config['config']['debug'])) {
254 $includeParseTime = (bool)$this->controller->config['config']['debug'];
255 } else {
256 $includeParseTime = !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']);
257 }
258 if ($includeParseTime) {
259 $response = $response->withHeader('X-TYPO3-Parsetime', $this->timeTracker->getParseTime() . 'ms');
260 }
261 }
262 $this->controller->redirectToExternalUrl();
263 // Preview info
264 $this->controller->previewInfo();
265 // Hook for end-of-frontend
266 $this->controller->hook_eofe();
267 // Finish timetracking
268 $this->timeTracker->pull();
269
270 // Admin panel
271 if ($this->controller->isBackendUserLoggedIn() && $GLOBALS['BE_USER'] instanceof FrontendBackendUserAuthentication) {
272 if ($GLOBALS['BE_USER']->isAdminPanelVisible()) {
273 $this->controller->content = str_ireplace('</body>', $GLOBALS['BE_USER']->displayAdminPanel() . '</body>', $this->controller->content);
274 }
275 }
276
277 if ($sendTSFEContent) {
278 // Send content-length header.
279 // Notice that all HTML content outside the length of the content-length header will be cut off!
280 // Therefore content of unknown length from included PHP-scripts and if admin users are logged
281 // in (admin panel might show...) or if debug mode is turned on, we disable it!
282 if (
283 (!isset($this->controller->config['config']['enableContentLengthHeader']) || $this->controller->config['config']['enableContentLengthHeader'])
284 && !$this->controller->beUserLogin && !$GLOBALS['TYPO3_CONF_VARS']['FE']['debug']
285 && !$this->controller->config['config']['debug'] && !$this->controller->doWorkspacePreview()
286 ) {
287 header('Content-Length: ' . strlen($this->controller->content));
288 }
289 $response->getBody()->write($this->controller->content);
290 }
291 GeneralUtility::makeInstance(LogManager::class)
292 ->getLogger(get_class())->debug('END of FRONTEND session', ['_FLUSH' => true]);
293
294 return $response ?: new NullResponse();
295 }
296
297 /**
298 * This request handler can handle any frontend request.
299 *
300 * @param ServerRequestInterface $request
301 * @return bool If the request is not an eID request, TRUE otherwise FALSE
302 */
303 public function canHandleRequest(ServerRequestInterface $request): bool
304 {
305 return $request->getQueryParams()['eID'] || $request->getParsedBody()['eID'] ? false : true;
306 }
307
308 /**
309 * Returns the priority - how eager the handler is to actually handle the
310 * request.
311 *
312 * @return int The priority of the request handler.
313 */
314 public function getPriority(): int
315 {
316 return 50;
317 }
318
319 /**
320 * Initializes output compression when enabled, could be split up and put into Bootstrap
321 * at a later point
322 */
323 protected function initializeOutputCompression()
324 {
325 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] && extension_loaded('zlib')) {
326 if (MathUtility::canBeInterpretedAsInteger($GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'])) {
327 @ini_set('zlib.output_compression_level', $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel']);
328 }
329 ob_start([GeneralUtility::makeInstance(CompressionUtility::class), 'compressionOutputHandler']);
330 }
331 }
332
333 /**
334 * Timetracking started depending if a Backend User is logged in
335 */
336 protected function initializeTimeTracker()
337 {
338 $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']) ?: 'be_typo_user';
339
340 /** @var TimeTracker timeTracker */
341 $this->timeTracker = GeneralUtility::makeInstance(TimeTracker::class, ($this->request->getCookieParams()[$configuredCookieName] ? true : false));
342 $this->timeTracker->start();
343 }
344
345 /**
346 * Creates an instance of TSFE and sets it as a global variable
347 */
348 protected function initializeController()
349 {
350 $this->controller = GeneralUtility::makeInstance(
351 TypoScriptFrontendController::class,
352 null,
353 GeneralUtility::_GP('id'),
354 GeneralUtility::_GP('type'),
355 GeneralUtility::_GP('no_cache'),
356 GeneralUtility::_GP('cHash'),
357 null,
358 GeneralUtility::_GP('MP')
359 );
360 // setting the global variable for the controller
361 // We have to define this as reference here, because there is code around
362 // which exchanges the TSFE object in the global variable. The reference ensures
363 // that the $controller member always works on the same object as the global variable.
364 // This is a dirty workaround and bypasses the protected access modifier of the controller member.
365 $GLOBALS['TSFE'] = &$this->controller;
366 }
367 }