[TASK] Use PSR-7 Response objects for external URL redirects
[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 middleware (EidHandler).
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 // Fetch the initialized time tracker object
102 $this->timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
103 $this->initializeController();
104
105 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_force']
106 && !GeneralUtility::cmpIP(
107 GeneralUtility::getIndpEnv('REMOTE_ADDR'),
108 $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']
109 )
110 ) {
111 $this->controller->pageUnavailableAndExit('This page is temporarily unavailable.');
112 }
113
114 $this->controller->connectToDB();
115
116 // Output compression
117 // Remove any output produced until now
118 $this->bootstrap->endOutputBufferingAndCleanPreviousOutput();
119 $this->initializeOutputCompression();
120
121 // Initializing the Frontend User
122 $this->timeTracker->push('Front End user initialized', '');
123 $this->controller->initFEuser();
124 $this->timeTracker->pull();
125
126 // Initializing a possible logged-in Backend User
127 /** @var $GLOBALS['BE_USER'] \TYPO3\CMS\Backend\FrontendBackendUserAuthentication */
128 $GLOBALS['BE_USER'] = $this->controller->initializeBackendUser();
129
130 // Process the ID, type and other parameters.
131 // After this point we have an array, $page in TSFE, which is the page-record
132 // of the current page, $id.
133 $this->timeTracker->push('Process ID', '');
134 // Initialize admin panel since simulation settings are required here:
135 if ($this->controller->isBackendUserLoggedIn()) {
136 $GLOBALS['BE_USER']->initializeAdminPanel();
137 $this->bootstrap
138 ->initializeBackendRouter()
139 ->loadExtTables();
140 }
141 $this->controller->checkAlternativeIdMethods();
142 $this->controller->clear_preview();
143 $this->controller->determineId();
144
145 // Now, if there is a backend user logged in and he has NO access to this page,
146 // then re-evaluate the id shown! _GP('ADMCMD_noBeUser') is placed here because
147 // \TYPO3\CMS\Version\Hook\PreviewHook might need to know if a backend user is logged in.
148 if (
149 $this->controller->isBackendUserLoggedIn()
150 && (!$GLOBALS['BE_USER']->extPageReadAccess($this->controller->page) || GeneralUtility::_GP('ADMCMD_noBeUser'))
151 ) {
152 // Remove user
153 unset($GLOBALS['BE_USER']);
154 $this->controller->beUserLogin = false;
155 // Re-evaluate the page-id.
156 $this->controller->checkAlternativeIdMethods();
157 $this->controller->clear_preview();
158 $this->controller->determineId();
159 }
160
161 $this->controller->makeCacheHash();
162 $this->timeTracker->pull();
163
164 // Admin Panel & Frontend editing
165 if ($this->controller->isBackendUserLoggedIn()) {
166 $GLOBALS['BE_USER']->initializeFrontendEdit();
167 if ($GLOBALS['BE_USER']->adminPanel instanceof AdminPanelView) {
168 $this->bootstrap->initializeLanguageObject();
169 }
170 if ($GLOBALS['BE_USER']->frontendEdit instanceof FrontendEditingController) {
171 $GLOBALS['BE_USER']->frontendEdit->initConfigOptions();
172 }
173 }
174
175 // Starts the template
176 $this->timeTracker->push('Start Template', '');
177 $this->controller->initTemplate();
178 $this->timeTracker->pull();
179 // Get from cache
180 $this->timeTracker->push('Get Page from cache', '');
181 $this->controller->getFromCache();
182 $this->timeTracker->pull();
183 // Get config if not already gotten
184 // After this, we should have a valid config-array ready
185 $this->controller->getConfigArray();
186 // Setting language and locale
187 $this->timeTracker->push('Setting language and locale', '');
188 $this->controller->settingLanguage();
189 $this->controller->settingLocale();
190 $this->timeTracker->pull();
191
192 // Convert POST data to utf-8 for internal processing if metaCharset is different
193 $this->controller->convPOSTCharset();
194
195 $this->controller->initializeRedirectUrlHandlers();
196
197 $this->controller->handleDataSubmission();
198
199 // Check for shortcut page and redirect
200 $this->controller->checkPageForShortcutRedirect();
201 $this->controller->checkPageForMountpointRedirect();
202
203 // Generate page
204 $this->controller->setUrlIdToken();
205 $this->timeTracker->push('Page generation', '');
206 if ($this->controller->isGeneratePage()) {
207 $this->controller->generatePage_preProcessing();
208 $this->controller->preparePageContentGeneration();
209 // Content generation
210 if (!$this->controller->isINTincScript()) {
211 PageGenerator::renderContent();
212 $this->controller->setAbsRefPrefix();
213 }
214 $this->controller->generatePage_postProcessing();
215 } elseif ($this->controller->isINTincScript()) {
216 $this->controller->preparePageContentGeneration();
217 }
218 $this->controller->releaseLocks();
219 $this->timeTracker->pull();
220
221 // Render non-cached parts
222 if ($this->controller->isINTincScript()) {
223 $this->timeTracker->push('Non-cached objects', '');
224 $this->controller->INTincScript();
225 $this->timeTracker->pull();
226 }
227
228 // Output content
229 $sendTSFEContent = false;
230 if ($this->controller->isOutputting()) {
231 $this->timeTracker->push('Print Content', '');
232 $this->controller->processOutput();
233 $sendTSFEContent = true;
234 $this->timeTracker->pull();
235 }
236 // Store session data for fe_users
237 $this->controller->storeSessionData();
238
239 // Create a Response object when sending content
240 if ($sendTSFEContent) {
241 $response = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Http\Response::class);
242 }
243
244 // Statistics
245 $GLOBALS['TYPO3_MISC']['microtime_end'] = microtime(true);
246 if ($sendTSFEContent) {
247 if (isset($this->controller->config['config']['debug'])) {
248 $includeParseTime = (bool)$this->controller->config['config']['debug'];
249 } else {
250 $includeParseTime = !empty($GLOBALS['TYPO3_CONF_VARS']['FE']['debug']);
251 }
252 if ($includeParseTime) {
253 $response = $response->withHeader('X-TYPO3-Parsetime', $this->timeTracker->getParseTime() . 'ms');
254 }
255 }
256 $redirectResponse = $this->controller->redirectToExternalUrl();
257 if ($redirectResponse instanceof ResponseInterface) {
258 return $redirectResponse;
259 }
260
261 // Preview info
262 $this->controller->previewInfo();
263 // Hook for end-of-frontend
264 $this->controller->hook_eofe();
265 // Finish timetracking
266 $this->timeTracker->pull();
267
268 // Admin panel
269 if ($this->controller->isBackendUserLoggedIn() && $GLOBALS['BE_USER'] instanceof FrontendBackendUserAuthentication) {
270 if ($GLOBALS['BE_USER']->isAdminPanelVisible()) {
271 $this->controller->content = str_ireplace('</body>', $GLOBALS['BE_USER']->displayAdminPanel() . '</body>', $this->controller->content);
272 }
273 }
274
275 if ($sendTSFEContent) {
276 // Send content-length header.
277 // Notice that all HTML content outside the length of the content-length header will be cut off!
278 // Therefore content of unknown length from included PHP-scripts and if admin users are logged
279 // in (admin panel might show...) or if debug mode is turned on, we disable it!
280 if (
281 (!isset($this->controller->config['config']['enableContentLengthHeader']) || $this->controller->config['config']['enableContentLengthHeader'])
282 && !$this->controller->isBackendUserLoggedIn() && !$GLOBALS['TYPO3_CONF_VARS']['FE']['debug']
283 && !$this->controller->config['config']['debug'] && !$this->controller->doWorkspacePreview()
284 ) {
285 header('Content-Length: ' . strlen($this->controller->content));
286 }
287 $response->getBody()->write($this->controller->content);
288 }
289 GeneralUtility::makeInstance(LogManager::class)
290 ->getLogger(get_class())->debug('END of FRONTEND session', ['_FLUSH' => true]);
291
292 return $response ?: new NullResponse();
293 }
294
295 /**
296 * This request handler can handle any frontend request.
297 *
298 * @param ServerRequestInterface $request
299 * @return bool If the request is not an eID request, TRUE otherwise FALSE
300 */
301 public function canHandleRequest(ServerRequestInterface $request): bool
302 {
303 return true;
304 }
305
306 /**
307 * Returns the priority - how eager the handler is to actually handle the
308 * request.
309 *
310 * @return int The priority of the request handler.
311 */
312 public function getPriority(): int
313 {
314 return 50;
315 }
316
317 /**
318 * Initializes output compression when enabled, could be split up and put into Bootstrap
319 * at a later point
320 */
321 protected function initializeOutputCompression()
322 {
323 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'] && extension_loaded('zlib')) {
324 if (MathUtility::canBeInterpretedAsInteger($GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel'])) {
325 @ini_set('zlib.output_compression_level', $GLOBALS['TYPO3_CONF_VARS']['FE']['compressionLevel']);
326 }
327 ob_start([GeneralUtility::makeInstance(CompressionUtility::class), 'compressionOutputHandler']);
328 }
329 }
330
331 /**
332 * Creates an instance of TSFE and sets it as a global variable
333 */
334 protected function initializeController()
335 {
336 $this->controller = GeneralUtility::makeInstance(
337 TypoScriptFrontendController::class,
338 null,
339 GeneralUtility::_GP('id'),
340 GeneralUtility::_GP('type'),
341 GeneralUtility::_GP('no_cache'),
342 GeneralUtility::_GP('cHash'),
343 null,
344 GeneralUtility::_GP('MP')
345 );
346 // setting the global variable for the controller
347 // We have to define this as reference here, because there is code around
348 // which exchanges the TSFE object in the global variable. The reference ensures
349 // that the $controller member always works on the same object as the global variable.
350 // This is a dirty workaround and bypasses the protected access modifier of the controller member.
351 $GLOBALS['TSFE'] = &$this->controller;
352 }
353 }