[BUGFIX] Restore getUrl support for list of headers
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Controller / ErrorController.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Frontend\Controller;
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 TYPO3\CMS\Core\Controller\ErrorPageController;
20 use TYPO3\CMS\Core\Error\Http\PageNotFoundException;
21 use TYPO3\CMS\Core\Error\Http\ServiceUnavailableException;
22 use TYPO3\CMS\Core\Http\HtmlResponse;
23 use TYPO3\CMS\Core\Http\RedirectResponse;
24 use TYPO3\CMS\Core\Utility\GeneralUtility;
25
26 /**
27 * Handles "Page Not Found" or "Page Unavailable" requests,
28 * returns a response object.
29 */
30 class ErrorController
31 {
32 /**
33 * Used for creating a 500 response ("Page unavailable"), usually due some misconfiguration
34 * but if configured, a RedirectResponse could be returned as well.
35 *
36 * @param string $message
37 * @param array $reasons
38 * @return ResponseInterface
39 * @throws ServiceUnavailableException
40 */
41 public function unavailableAction(string $message, array $reasons = []): ResponseInterface
42 {
43 if (!$this->isPageUnavailableHandlerConfigured()) {
44 throw new ServiceUnavailableException($message, 1518472181);
45 }
46 return $this->handlePageError(
47 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling'],
48 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling_statheader'],
49 $message,
50 $reasons
51 );
52 }
53
54 /**
55 * Used for creating a 404 response ("Page Not Found"),
56 * but if configured, a RedirectResponse could be returned as well.
57 *
58 * @param string $message
59 * @param array $reasons
60 * @return ResponseInterface
61 * @throws PageNotFoundException
62 */
63 public function pageNotFoundAction(string $message, array $reasons = []): ResponseInterface
64 {
65 if (!$GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling']) {
66 throw new PageNotFoundException($message, 1518472189);
67 }
68 return $this->handlePageError(
69 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'],
70 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling_statheader'],
71 $message,
72 $reasons
73 );
74 }
75
76 /**
77 * Used for creating a 403 response ("Access denied"),
78 * but if configured, a RedirectResponse could be returned as well.
79 *
80 * @param string $message
81 * @param array $reasons
82 * @return ResponseInterface
83 */
84 public function accessDeniedAction(string $message, array $reasons = []): ResponseInterface
85 {
86 return $this->handlePageError(
87 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling'],
88 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageNotFound_handling_accessdeniedheader'],
89 $message,
90 $reasons
91 );
92 }
93
94 /**
95 * Checks whether the pageUnavailableHandler should be used. To be used, pageUnavailable_handling must be set
96 * and devIPMask must not match the current visitor's IP address.
97 *
98 * @return bool TRUE/FALSE whether the pageUnavailable_handler should be used.
99 */
100 protected function isPageUnavailableHandlerConfigured(): bool
101 {
102 return
103 $GLOBALS['TYPO3_CONF_VARS']['FE']['pageUnavailable_handling']
104 && !GeneralUtility::cmpIP(
105 GeneralUtility::getIndpEnv('REMOTE_ADDR'),
106 $GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask']
107 )
108 ;
109 }
110
111 /**
112 * Generic error page handler.
113 *
114 * @param mixed $errorHandler See docs of ['FE']['pageNotFound_handling'] and ['FE']['pageUnavailable_handling'] for all possible values
115 * @param string $header If set, this is passed directly to the PHP function, header()
116 * @param string $reason If set, error messages will also mention this as the reason for the page-not-found.
117 * @param array $pageAccessFailureReasons
118 * @return ResponseInterface
119 * @throws \RuntimeException
120 */
121 protected function handlePageError($errorHandler, string $header = '', string $reason = '', array $pageAccessFailureReasons = []): ResponseInterface
122 {
123 $response = null;
124 $content = '';
125 // Simply boolean; Just shows TYPO3 error page with reason:
126 if (gettype($errorHandler) === 'boolean' || strtolower($errorHandler) === 'true' || (string)$errorHandler === '1') {
127 $content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
128 'Page Not Found',
129 'The page did not exist or was inaccessible.' . ($reason ? ' Reason: ' . $reason : '')
130 );
131 } elseif (GeneralUtility::isFirstPartOfStr($errorHandler, 'USER_FUNCTION:')) {
132 $funcRef = trim(substr($errorHandler, 14));
133 $params = [
134 'currentUrl' => GeneralUtility::getIndpEnv('REQUEST_URI'),
135 'reasonText' => $reason,
136 'pageAccessFailureReasons' => $pageAccessFailureReasons
137 ];
138 try {
139 $content = GeneralUtility::callUserFunction($funcRef, $params, $this);
140 } catch (\Exception $e) {
141 throw new \RuntimeException('Error: 404 page by USER_FUNCTION "' . $funcRef . '" failed.', 1518472235, $e);
142 }
143 } elseif (GeneralUtility::isFirstPartOfStr($errorHandler, 'READFILE:')) {
144 $readFile = GeneralUtility::getFileAbsFileName(trim(substr($errorHandler, 9)));
145 if (@is_file($readFile)) {
146 $content = str_replace(
147 [
148 '###CURRENT_URL###',
149 '###REASON###'
150 ],
151 [
152 GeneralUtility::getIndpEnv('REQUEST_URI'),
153 htmlspecialchars($reason)
154 ],
155 file_get_contents($readFile)
156 );
157 } else {
158 throw new \RuntimeException('Configuration Error: 404 page "' . $readFile . '" could not be found.', 1518472245);
159 }
160 } elseif (GeneralUtility::isFirstPartOfStr($errorHandler, 'REDIRECT:')) {
161 $response = new RedirectResponse(substr($errorHandler, 9));
162 } elseif ($errorHandler !== '') {
163 // Check if URL is relative
164 $urlParts = parse_url($errorHandler);
165 // parse_url could return an array without the key "host", the empty check works better than strict check
166 if (empty($urlParts['host'])) {
167 $urlParts['host'] = GeneralUtility::getIndpEnv('HTTP_HOST');
168 if ($errorHandler[0] === '/') {
169 $errorHandler = GeneralUtility::getIndpEnv('TYPO3_REQUEST_HOST') . $errorHandler;
170 } else {
171 $errorHandler = GeneralUtility::getIndpEnv('TYPO3_REQUEST_DIR') . $errorHandler;
172 }
173 $checkBaseTag = false;
174 } else {
175 $checkBaseTag = true;
176 }
177 // Check recursion
178 if ($errorHandler === GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')) {
179 $reason = $reason ?: 'Page cannot be found.';
180 $reason .= LF . LF . 'Additionally, ' . $errorHandler . ' was not found while trying to retrieve the error document.';
181 throw new \RuntimeException(nl2br(htmlspecialchars($reason)), 1518472252);
182 }
183 // Prepare headers
184 $requestHeaders = [
185 'User-agent' => GeneralUtility::getIndpEnv('HTTP_USER_AGENT'),
186 'Referer' => GeneralUtility::getIndpEnv('TYPO3_REQUEST_URL')
187 ];
188 $report = [];
189 $res = GeneralUtility::getUrl($errorHandler, 1, $requestHeaders, $report);
190 if ((int)$report['error'] !== 0 && (int)$report['error'] !== 200) {
191 throw new \RuntimeException('Failed to fetch error page "' . $errorHandler . '", reason: ' . $report['message'], 1518472257);
192 }
193 if ($res === false) {
194 // Last chance -- redirect
195 $response = new RedirectResponse($errorHandler);
196 } else {
197 // Header and content are separated by an empty line
198 list($returnedHeaders, $content) = explode(CRLF . CRLF, $res, 2);
199 $content .= CRLF;
200 // Forward these response headers to the client
201 $forwardHeaders = [
202 'Content-Type:'
203 ];
204 $headerArr = preg_split('/\\r|\\n/', $returnedHeaders, -1, PREG_SPLIT_NO_EMPTY);
205 foreach ($headerArr as $headerLine) {
206 foreach ($forwardHeaders as $h) {
207 if (preg_match('/^' . $h . '/', $headerLine)) {
208 $header .= CRLF . $headerLine;
209 }
210 }
211 }
212 // Put <base> if necessary
213 if ($checkBaseTag) {
214 // If content already has <base> tag, we do not need to do anything
215 if (false === stristr($content, '<base ')) {
216 // Generate href for base tag
217 $base = $urlParts['scheme'] . '://';
218 if ($urlParts['user'] != '') {
219 $base .= $urlParts['user'];
220 if ($urlParts['pass'] != '') {
221 $base .= ':' . $urlParts['pass'];
222 }
223 $base .= '@';
224 }
225 $base .= $urlParts['host'];
226 // Add path portion skipping possible file name
227 $base .= preg_replace('/(.*\\/)[^\\/]*/', '${1}', $urlParts['path']);
228 // Put it into content (generate also <head> if necessary)
229 $replacement = LF . '<base href="' . htmlentities($base) . '" />' . LF;
230 if (stristr($content, '<head>')) {
231 $content = preg_replace('/(<head>)/i', '\\1' . $replacement, $content);
232 } else {
233 $content = preg_replace('/(<html[^>]*>)/i', '\\1<head>' . $replacement . '</head>', $content);
234 }
235 }
236 }
237 }
238 } else {
239 $content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
240 'Page Not Found',
241 $reason ? 'Reason: ' . $reason : 'Page cannot be found.'
242 );
243 }
244
245 if (!$response) {
246 $response = new HtmlResponse($content);
247 }
248 return $this->applySanitizedHeadersToResponse($response, $header);
249 }
250
251 /**
252 * Headers which have been requested, will be added to the response object.
253 * If a header is part of the HTTP Repsonse code, the response object will be annotated as well.
254 *
255 * @param ResponseInterface $response
256 * @param string $headers
257 * @return ResponseInterface
258 */
259 protected function applySanitizedHeadersToResponse(ResponseInterface $response, string $headers): ResponseInterface
260 {
261 if (!empty($headers)) {
262 $headerArr = preg_split('/\\r|\\n/', $headers, -1, PREG_SPLIT_NO_EMPTY);
263 foreach ($headerArr as $headerLine) {
264 if (strpos($headerLine, 'HTTP/') === 0 && strpos($headerLine, ':') === false) {
265 list($protocolVersion, $statusCode, $reasonPhrase) = explode(' ', $headerLine, 3);
266 list(, $protocolVersion) = explode('/', $protocolVersion, 2);
267 $response = $response
268 ->withProtocolVersion((int)$protocolVersion)
269 ->withStatus($statusCode, $reasonPhrase);
270 } else {
271 list($headerName, $value) = GeneralUtility::trimExplode(':', $headerLine, 2);
272 $response = $response->withHeader($headerName, $value);
273 }
274 }
275 }
276 return $response;
277 }
278 }