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