‪TYPO3CMS  11.5
PageContentErrorHandler.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
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 
19 
20 use GuzzleHttp\Exception\ClientException;
21 use Psr\Http\Message\ResponseFactoryInterface;
22 use Psr\Http\Message\ResponseInterface;
23 use Psr\Http\Message\ServerRequestInterface;
45 
51 {
52  protected int ‪$statusCode;
53 
55 
56  protected int ‪$pageUid = 0;
57 
59 
61 
62  protected ResponseFactoryInterface ‪$responseFactory;
63 
65 
67 
69 
70  protected bool ‪$useSubrequest;
71 
78  public function ‪__construct(int ‪$statusCode, array $configuration)
79  {
80  $this->statusCode = ‪$statusCode;
81  if (empty($configuration['errorContentSource'])) {
82  throw new \InvalidArgumentException('PageContentErrorHandler needs to have a proper link set.', 1522826413);
83  }
84  $this->errorHandlerConfiguration = $configuration;
85 
86  // @todo Convert this to DI once this class can be injected properly.
87  $container = GeneralUtility::getContainer();
88  $this->application = $container->get(Application::class);
89  $this->requestFactory = $container->get(RequestFactory::class);
90  $this->responseFactory = $container->get(ResponseFactoryInterface::class);
91  $this->siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
92  $this->link = $container->get(LinkService::class);
93  $this->cache = $container->get(CacheManager::class)->getCache('pages');
94  $this->useSubrequest = GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('subrequestPageErrors');
95  }
96 
97  public function ‪handlePageError(ServerRequestInterface $request, string $message, array $reasons = []): ResponseInterface
98  {
99  try {
100  $urlParams = $this->link->resolve($this->errorHandlerConfiguration['errorContentSource']);
101  $this->pageUid = $urlParams['pageuid'] = (int)($urlParams['pageuid'] ?? 0);
102  $resolvedUrl = $this->‪resolveUrl($request, $urlParams);
103 
104  // avoid denial-of-service amplification scenario
105  if ($resolvedUrl === (string)$request->getUri()) {
106  return new ‪HtmlResponse(
107  'The error page could not be resolved, as the error page itself is not accessible',
108  $this->statusCode
109  );
110  }
111  if ($this->useSubrequest) {
112  // Create a sub-request and do not take any special query parameters into account
113  $subRequest = $request->withQueryParams([])->withUri(new ‪Uri($resolvedUrl))->withMethod('GET');
114  $subResponse = $this->‪stashEnvironment(fn(): ResponseInterface => $this->‪sendSubRequest($subRequest, $this->pageUid, $request));
115  } else {
116  $cacheIdentifier = 'errorPage_' . md5($resolvedUrl);
117  try {
118  $subResponse = $this->‪cachePageRequest(
119  $this->pageUid,
120  fn() => $this->‪sendRawRequest($resolvedUrl),
121  $cacheIdentifier
122  );
123  } catch (\‪Exception $e) {
124  throw new \RuntimeException(sprintf('Error handler could not fetch error page "%s", reason: %s', $resolvedUrl, $e->getMessage()), 1544172838, $e);
125  }
126  // Ensure that 503 status code is kept, and not changed to 500.
127  if ($subResponse->getStatusCode() === 503) {
128  return $this->responseFactory->createResponse($subResponse->getStatusCode())
129  ->withHeader('content-type', $subResponse->getHeader('content-type'))
130  ->withBody($subResponse->getBody());
131  }
132  }
133 
134  if ($subResponse->getStatusCode() >= 300) {
135  throw new \RuntimeException(sprintf('Error handler could not fetch error page "%s", status code: %s', $resolvedUrl, $subResponse->getStatusCode()), 1544172839);
136  }
137 
138  return $this->responseFactory->createResponse($this->statusCode)
139  ->withHeader('content-type', $subResponse->getHeader('content-type'))
140  ->withBody($subResponse->getBody());
142  return new ‪HtmlResponse('Invalid error handler configuration: ' . $this->errorHandlerConfiguration['errorContentSource']);
143  }
144  }
145 
149  protected function ‪stashEnvironment(callable $fetcher): ResponseInterface
150  {
151  $parkedTsfe = ‪$GLOBALS['TSFE'] ?? null;
152  ‪$GLOBALS['TSFE'] = null;
153 
154  $result = $fetcher();
155 
156  ‪$GLOBALS['TSFE'] = $parkedTsfe;
157 
158  return $result;
159  }
160 
164  protected function ‪cachePageRequest(int $pageId, callable $fetcher, string $cacheIdentifier): ResponseInterface
165  {
166  $responseData = $this->cache->get($cacheIdentifier);
167  if (is_array($responseData) && $responseData !== []) {
168  return $this->‪createCachedPageRequestResponse($responseData);
169  }
170  $cacheTags = [];
171  $cacheTags[] = 'errorPage';
172  if ($pageId > 0) {
173  // Cache Tag "pageId_" ensures, cache is purged when content of 404 page changes
174  $cacheTags[] = 'pageId_' . $pageId;
175  }
176  $lockFactory = GeneralUtility::makeInstance(LockFactory::class);
177  $lock = $lockFactory->createLocker(
178  $cacheIdentifier,
180  );
181  try {
182  $locked = $lock->acquire(
184  );
185  if (!$locked) {
186  return $this->‪createGenericErrorResponse('Lock could not be acquired.');
187  }
189  $response = $fetcher();
190  if ($response->getStatusCode() !== 200) {
191  // External request lead to an error. Create a generic error response,
192  // cache and use that instead of the external error response.
193  $response = $this->‪createGenericErrorResponse('External error page could not be retrieved.');
194  }
195  $responseData = [
196  'statuscode' => $response->getStatusCode(),
197  'headers' => $response->getHeaders(),
198  'body' => $response->getBody()->getContents(),
199  'reasonPhrase' => $response->getReasonPhrase(),
200  ];
201  $this->cache->set($cacheIdentifier, $responseData, $cacheTags);
202  $lock->release();
203  } catch (ClientException $e) {
204  $response = $this->‪createGenericErrorResponse('External error page could not be retrieved. ' . $e->getMessage());
205  $responseData = [
206  'statuscode' => $response->getStatusCode(),
207  'headers' => $response->getHeaders(),
208  'body' => $response->getBody()->getContents(),
209  'reasonPhrase' => $response->getReasonPhrase(),
210  ];
211  $this->cache->set($cacheIdentifier, $responseData, $cacheTags);
212  } catch (‪LockAcquireWouldBlockException $e) {
213  // Currently a lock is active, thus returning a generic error directly to avoid
214  // long wait times and thus consuming too much php worker processes. Caching is
215  // not done here, as we do not know if the error page can be retrieved or not.
216  $lock->release();
217  return $this->‪createGenericErrorResponse('Lock could not be acquired. ' . $e->getMessage());
218  } catch (\Throwable $e) {
219  // Any other error happened
220  $lock->release();
221  return $this->‪createGenericErrorResponse('Error page could not be retrieved' . $e->getMessage());
222  }
223  $lock->release();
224  return $this->‪createCachedPageRequestResponse($responseData);
225  }
226 
227  protected function ‪createGenericErrorResponse(string $message = ''): ResponseInterface
228  {
229  $content = GeneralUtility::makeInstance(ErrorPageController::class)->errorAction(
230  'Page Not Found',
231  $message ?: 'Error page is being generated',
233  0,
234  503
235  );
236  return new ‪HtmlResponse($content, 503);
237  }
238 
239  protected function ‪createCachedPageRequestResponse(array $responseData): ResponseInterface
240  {
241  $body = new ‪Stream('php://temp', 'wb+');
242  $body->write($responseData['body'] ?? '');
243  $body->rewind();
244  $response = new ‪Response(
245  $body,
246  $responseData['statuscode'] ?? 200,
247  $responseData['headers'] ?? [],
248  $responseData['reasonPhrase'] ?? ''
249  );
250  return $response;
251  }
252 
256  protected function ‪sendRawRequest(string $resolvedUrl): ResponseInterface
257  {
258  return $this->requestFactory->request($resolvedUrl, 'GET', $this->‪getSubRequestOptions());
259  }
260 
266  protected function ‪sendSubRequest(ServerRequestInterface $request, int $pageId, ServerRequestInterface $originalRequest): ResponseInterface
267  {
268  $site = $request->getAttribute('site', null);
269  if (!$site instanceof ‪Site) {
270  $site = $this->siteFinder->getSiteByPageId($pageId);
271  $request = $request->withAttribute('site', $site);
272  }
273 
274  $request = $request->withAttribute('originalRequest', $originalRequest);
275 
276  return $this->application->handle($request);
277  }
278 
284  protected function ‪getSubRequestOptions(): array
285  {
286  $options = [];
287  if ((int)‪$GLOBALS['TYPO3_CONF_VARS']['HTTP']['timeout'] === 0) {
288  $options = [
289  'timeout' => 10,
290  ];
291  }
292  return $options;
293  }
294 
298  protected function ‪resolveUrl(ServerRequestInterface $request, array $urlParams): string
299  {
300  if (!in_array($urlParams['type'], ['page', 'url'])) {
301  throw new \InvalidArgumentException('PageContentErrorHandler can only handle TYPO3 urls of types "page" or "url"', 1522826609);
302  }
303  if ($urlParams['type'] === 'url') {
304  return $urlParams['url'];
305  }
306 
307  // Get the site related to the configured error page
308  $site = $this->siteFinder->getSiteByPageId($urlParams['pageuid']);
309  // Fall back to current request for the site
310  if (!$site instanceof ‪Site) {
311  $site = $request->getAttribute('site', null);
312  }
314  $requestLanguage = $request->getAttribute('language', null);
315  // Try to get the current request language from the site that was found above
316  if ($requestLanguage instanceof ‪SiteLanguage && $requestLanguage->‪isEnabled()) {
317  try {
318  $language = $site->getLanguageById($requestLanguage->getLanguageId());
319  } catch (\InvalidArgumentException $e) {
320  $language = $site->getDefaultLanguage();
321  }
322  } else {
323  $language = $site->getDefaultLanguage();
324  }
325 
326  // Requested language or default language is disabled in current site => Fetch first "enabled" language
327  if (!$language->isEnabled()) {
328  $enabledLanguages = $site->getLanguages();
329  if ($enabledLanguages === []) {
330  throw new \RuntimeException(
331  'Site ' . $site->getIdentifier() . ' does not define any enabled language.',
332  1674487171
333  );
334  }
335  $language = reset($enabledLanguages);
336  }
337 
338  // Build Url
339  $uri = $site->getRouter()->generateUri(
340  (int)$urlParams['pageuid'],
341  ['_language' => $language]
342  );
343 
344  // Fallback to the current URL if the site is not having a proper scheme and host
345  $currentUri = $request->getUri();
346  if (empty($uri->getScheme())) {
347  $uri = $uri->withScheme($currentUri->getScheme());
348  }
349  if (empty($uri->getUserInfo())) {
350  $uri = $uri->withUserInfo($currentUri->getUserInfo());
351  }
352  if (empty($uri->getHost())) {
353  $uri = $uri->withHost($currentUri->getHost());
354  }
355  if ($uri->getPort() === null) {
356  $uri = $uri->withPort($currentUri->getPort());
357  }
358 
359  return (string)$uri;
360  }
361 }
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\$requestFactory
‪RequestFactory $requestFactory
Definition: PageContentErrorHandler.php:60
‪TYPO3\CMS\Core\Messaging\AbstractMessage
Definition: AbstractMessage.php:26
‪TYPO3\CMS\Core\Controller\ErrorPageController
Definition: ErrorPageController.php:30
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\createGenericErrorResponse
‪createGenericErrorResponse(string $message='')
Definition: PageContentErrorHandler.php:227
‪TYPO3\CMS\Core\Locking\LockingStrategyInterface\LOCK_CAPABILITY_NOBLOCK
‪const LOCK_CAPABILITY_NOBLOCK
Definition: LockingStrategyInterface.php:40
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\cachePageRequest
‪cachePageRequest(int $pageId, callable $fetcher, string $cacheIdentifier)
Definition: PageContentErrorHandler.php:164
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\$link
‪LinkService $link
Definition: PageContentErrorHandler.php:66
‪TYPO3\CMS\Core\Exception\SiteNotFoundException
Definition: SiteNotFoundException.php:25
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Core\Locking\LockingStrategyInterface
Definition: LockingStrategyInterface.php:26
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\createCachedPageRequestResponse
‪createCachedPageRequestResponse(array $responseData)
Definition: PageContentErrorHandler.php:239
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\$pageUid
‪int $pageUid
Definition: PageContentErrorHandler.php:56
‪TYPO3\CMS\Core\Locking\LockingStrategyInterface\LOCK_CAPABILITY_EXCLUSIVE
‪const LOCK_CAPABILITY_EXCLUSIVE
Definition: LockingStrategyInterface.php:30
‪TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException
Definition: LockAcquireWouldBlockException.php:21
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\$cache
‪FrontendInterface $cache
Definition: PageContentErrorHandler.php:68
‪TYPO3\CMS\Core\Http\Uri
Definition: Uri.php:29
‪TYPO3\CMS\Core\Site\Entity\Site
Definition: Site.php:42
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage
Definition: SiteLanguage.php:26
‪TYPO3\CMS\Core\Http\Response
Definition: Response.php:30
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\sendSubRequest
‪sendSubRequest(ServerRequestInterface $request, int $pageId, ServerRequestInterface $originalRequest)
Definition: PageContentErrorHandler.php:266
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\$useSubrequest
‪bool $useSubrequest
Definition: PageContentErrorHandler.php:70
‪TYPO3\CMS\Core\Configuration\Features
Definition: Features.php:56
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\handlePageError
‪handlePageError(ServerRequestInterface $request, string $message, array $reasons=[])
Definition: PageContentErrorHandler.php:97
‪TYPO3\CMS\Core\Http\Stream
Definition: Stream.php:29
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:36
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\getSubRequestOptions
‪array int[] getSubRequestOptions()
Definition: PageContentErrorHandler.php:284
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler
Definition: PageContentErrorHandler.php:51
‪TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException
Definition: InvalidRouteArgumentsException.php:25
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\$errorHandlerConfiguration
‪array $errorHandlerConfiguration
Definition: PageContentErrorHandler.php:54
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\stashEnvironment
‪stashEnvironment(callable $fetcher)
Definition: PageContentErrorHandler.php:149
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\$responseFactory
‪ResponseFactoryInterface $responseFactory
Definition: PageContentErrorHandler.php:62
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\__construct
‪__construct(int $statusCode, array $configuration)
Definition: PageContentErrorHandler.php:78
‪TYPO3\CMS\Core\Error\Exception
Definition: Exception.php:21
‪TYPO3\CMS\Core\Http\RequestFactory
Definition: RequestFactory.php:31
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage\isEnabled
‪bool isEnabled()
Definition: SiteLanguage.php:304
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageErrorHandlerInterface
Definition: PageErrorHandlerInterface.php:29
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\$siteFinder
‪SiteFinder $siteFinder
Definition: PageContentErrorHandler.php:64
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\$statusCode
‪int $statusCode
Definition: PageContentErrorHandler.php:52
‪TYPO3\CMS\Core\Locking\LockFactory
Definition: LockFactory.php:25
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\$application
‪Application $application
Definition: PageContentErrorHandler.php:58
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Frontend\Http\Application
Definition: Application.php:38
‪TYPO3\CMS\Core\Error\PageErrorHandler
Definition: FluidPageErrorHandler.php:18
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\sendRawRequest
‪sendRawRequest(string $resolvedUrl)
Definition: PageContentErrorHandler.php:256
‪TYPO3\CMS\Core\Messaging\AbstractMessage\ERROR
‪const ERROR
Definition: AbstractMessage.php:31
‪TYPO3\CMS\Core\Http\HtmlResponse
Definition: HtmlResponse.php:26
‪TYPO3\CMS\Core\Error\PageErrorHandler\PageContentErrorHandler\resolveUrl
‪resolveUrl(ServerRequestInterface $request, array $urlParams)
Definition: PageContentErrorHandler.php:298