‪TYPO3CMS  ‪main
PrepareTypoScriptFrontendRendering.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 Psr\EventDispatcher\EventDispatcherInterface;
21 use Psr\Http\Message\ResponseInterface;
22 use Psr\Http\Message\ServerRequestInterface;
23 use Psr\Http\Server\MiddlewareInterface;
24 use Psr\Http\Server\RequestHandlerInterface;
25 use Psr\Log\LoggerInterface;
31 use TYPO3\CMS\Core\TypoScript\FrontendTypoScriptFactory;
40 
47 final readonly class ‪PrepareTypoScriptFrontendRendering implements MiddlewareInterface
48 {
49  public function ‪__construct(
50  private EventDispatcherInterface $eventDispatcher,
51  private FrontendTypoScriptFactory $frontendTypoScriptFactory,
52  private ‪PhpFrontend $typoScriptCache,
53  private ‪FrontendInterface $pageCache,
54  private ‪ResourceMutex $lock,
55  private ‪Context $context,
56  private LoggerInterface $logger,
57  private ‪ErrorController $errorController,
58  ) {}
59 
60  public function ‪process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
61  {
62  $site = $request->getAttribute('site');
63  $sysTemplateRows = $request->getAttribute('frontend.page.information')->getSysTemplateRows();
64  $isCachingAllowed = $request->getAttribute('frontend.cache.instruction')->isCachingAllowed();
65 
66  // Create FrontendTypoScript with essential info for page cache identifier
67  $conditionMatcherVariables = $this->‪prepareConditionMatcherVariables($request);
68  $frontendTypoScript = $this->frontendTypoScriptFactory->createSettingsAndSetupConditions(
69  $site,
70  $sysTemplateRows,
71  $conditionMatcherVariables,
72  $isCachingAllowed ? $this->typoScriptCache : null,
73  );
74 
75  $isUsingPageCacheAllowed = $this->eventDispatcher
76  ->dispatch(new ‪ShouldUseCachedPageDataIfAvailableEvent($request, $isCachingAllowed))
77  ->shouldUseCachedPageData();
78  $pageCacheIdentifier = $this->‪createPageCacheIdentifier($request, $frontendTypoScript);
79 
80  $pageCacheRow = null;
81  if (!$isUsingPageCacheAllowed) {
82  // Caching is not allowed. We'll rebuild the page. Lock this.
83  $this->lock->acquireLock('pages', $pageCacheIdentifier);
84  } else {
85  // Try to get a page cache row.
86  $pageCacheRow = $this->pageCache->get($pageCacheIdentifier);
87  if (!is_array($pageCacheRow)) {
88  // Nothing in the cache, we acquire an exclusive lock now.
89  // There are two scenarios when locking: We're either the first process acquiring this lock. This means we'll
90  // "immediately" get it and can continue with page rendering. Or, another process acquired the lock already. In
91  // this case, the below call will wait until the lock is released again. The other process then probably wrote
92  // a page cache entry, which we can use.
93  // To handle the second case - if our process had to wait for another one creating the content for us - we
94  // simply query the page cache again to see if there is a page cache now.
95  $hadToWaitForLock = $this->lock->acquireLock('pages', $pageCacheIdentifier);
96  // From this point on we're the only one working on that page.
97  if ($hadToWaitForLock) {
98  // Query the cache again to see if the data is there meanwhile: We did not get the lock
99  // immediately, chances are high the other process created a page cache for us.
100  // There is a small chance the other process actually pageCache->set() the content,
101  // but pageCache->get() still returns false, for instance when a database returned "done"
102  // for the INSERT, but SELECT still does not return the new row - may happen in multi-head
103  // DB instances, and with some other distributed cache backends as well. The worst that
104  // can happen here is the page generation is done too often, which we accept as trade-off.
105  $pageCacheRow = $this->pageCache->get($pageCacheIdentifier);
106  if (is_array($pageCacheRow)) {
107  // We have the content, some other process did the work for us, release our lock again.
108  $this->lock->releaseLock('pages');
109  }
110  }
111  // Keep the lock set, because we are the ones generating the page now and filling the cache.
112  }
113  }
114 
115  $controller = $request->getAttribute('frontend.controller');
116  $controller->newHash = $pageCacheIdentifier;
117  $pageContentWasLoadedFromCache = false;
118  if (is_array($pageCacheRow)) {
119  $controller->config['INTincScript'] = $pageCacheRow['INTincScript'];
120  $controller->config['INTincScript_ext'] = $pageCacheRow['INTincScript_ext'];
121  $controller->config['pageTitleCache'] = $pageCacheRow['pageTitleCache'];
122  $controller->content = $pageCacheRow['content'];
123  $controller->setContentType($pageCacheRow['contentType']);
124  $controller->cacheExpires = $pageCacheRow['expires'];
125  $controller->pageCacheTags = $pageCacheRow['cacheTags'];
126  $controller->cacheGenerated = $pageCacheRow['tstamp'];
127  $controller->pageContentWasLoadedFromCache = true;
128  $pageContentWasLoadedFromCache = true;
129  }
130 
131  try {
132  $needsFullSetup = !$pageContentWasLoadedFromCache || $controller->isINTincScript();
133  $pageType = $request->getAttribute('routing')->getPageType();
134  $frontendTypoScript = $this->frontendTypoScriptFactory->createSetupConfigOrFullSetup(
135  $needsFullSetup,
136  $frontendTypoScript,
137  $site,
138  $sysTemplateRows,
139  $conditionMatcherVariables,
140  $pageType,
141  $isCachingAllowed ? $this->typoScriptCache : null,
142  $request,
143  );
144  if ($needsFullSetup && !$frontendTypoScript->hasPage()) {
145  $this->logger->error('No page configured for type={type}. There is no TypoScript object of type PAGE with typeNum={type}.', ['type' => $pageType]);
146  return $this->errorController->internalErrorAction(
147  $request,
148  'No page configured for type=' . $pageType . '.',
150  );
151  }
152  $setupConfigAst = $frontendTypoScript->getConfigTree();
153  if ($pageContentWasLoadedFromCache && ($setupConfigAst->getChildByName('debug')?->getValue() || !empty(‪$GLOBALS['TYPO3_CONF_VARS']['FE']['debug']))) {
154  // Prepare X-TYPO3-Debug-Cache HTTP header
155  $dateFormat = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'];
156  $timeFormat = ‪$GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
157  $controller->debugInformationHeader = 'Cached page generated ' . date($dateFormat . ' ' . $timeFormat, $controller->cacheGenerated)
158  . '. Expires ' . date($dateFormat . ' ' . $timeFormat, $controller->cacheExpires);
159  }
160  if ($setupConfigAst->getChildByName('no_cache')?->getValue()) {
161  // Disable cache if config.no_cache is set!
162  $cacheInstruction = $request->getAttribute('frontend.cache.instruction');
163  $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to TypoScript "config.no_cache = 1"');
164  }
165  $this->eventDispatcher->dispatch(new ‪AfterTypoScriptDeterminedEvent($frontendTypoScript));
166  $request = $request->withAttribute('frontend.typoscript', $frontendTypoScript);
167 
168  // b/w compat
169  $controller->config['config'] = $frontendTypoScript->getConfigArray();
170  ‪$GLOBALS['TYPO3_REQUEST'] = $request;
171 
172  $response = $handler->handle($request);
173  } finally {
174  // Whatever happens in a below middleware, this finally is called, even when exceptions
175  // are raised by a lower middleware. This ensures locks are released no matter what.
176  $this->lock->releaseLock('pages');
177  }
178 
179  return $response;
180  }
181 
185  private function ‪prepareConditionMatcherVariables(ServerRequestInterface $request): array
186  {
187  $pageInformation = $request->getAttribute('frontend.page.information');
188  $topDownRootLine = $pageInformation->getRootLine();
189  $localRootline = $pageInformation->getLocalRootLine();
190  ksort($topDownRootLine);
191  return [
192  'request' => $request,
193  'pageId' => $pageInformation->getId(),
194  'page' => $pageInformation->getPageRecord(),
195  'fullRootLine' => $topDownRootLine,
196  'localRootLine' => $localRootline,
197  'site' => $request->getAttribute('site'),
198  'siteLanguage' => $request->getAttribute('language'),
199  'tsfe' => $request->getAttribute('frontend.controller'),
200  ];
201  }
202 
209  private function ‪createPageCacheIdentifier(ServerRequestInterface $request, ‪FrontendTypoScript $frontendTypoScript): string
210  {
211  $pageInformation = $request->getAttribute('frontend.page.information');
212  $pageId = $pageInformation->getId();
213  $pageArguments = $request->getAttribute('routing');
214  $site = $request->getAttribute('site');
215 
216  $dynamicArguments = [];
217  $queryParams = $pageArguments->getDynamicArguments();
218  if (!empty($queryParams) && ($pageArguments->getArguments()['cHash'] ?? false)) {
219  // Fetch arguments relevant for creating the page cache identifier from the PageArguments object.
220  // Excluded parameters are not taken into account when calculating the hash base.
221  $queryParams['id'] = $pageArguments->getPageId();
222  // @todo: Make CacheHashCalculator and CacheHashConfiguration stateless and get it injected.
223  $dynamicArguments = GeneralUtility::makeInstance(CacheHashCalculator::class)
224  ->getRelevantParameters(‪HttpUtility::buildQueryString($queryParams));
225  }
226 
227  $pageCacheIdentifierParameters = [
228  'id' => $pageId,
229  'type' => $pageArguments->getPageType(),
230  'groupIds' => implode(',', $this->context->getAspect('frontend.user')->getGroupIds()),
231  'MP' => $pageInformation->getMountPoint(),
232  'site' => $site->getIdentifier(),
233  // Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering
234  // is not cached properly as we don't have any language-specific conditions anymore
235  'siteBase' => (string)$request->getAttribute('language', $site->getDefaultLanguage())->getBase(),
236  // additional variation trigger for static routes
237  'staticRouteArguments' => $pageArguments->getStaticArguments(),
238  // dynamic route arguments (if route was resolved)
239  'dynamicArguments' => $dynamicArguments,
240  'sysTemplateRows' => $pageInformation->getSysTemplateRows(),
241  'constantConditionList' => $frontendTypoScript->‪getSettingsConditionList(),
242  'setupConditionList' => $frontendTypoScript->‪getSetupConditionList(),
243  ];
244  $pageCacheIdentifierParameters = $this->eventDispatcher
245  ->dispatch(new ‪BeforePageCacheIdentifierIsHashedEvent($request, $pageCacheIdentifierParameters))
246  ->getPageCacheIdentifierParameters();
247 
248  return $pageId . '_' . hash('xxh3', serialize($pageCacheIdentifierParameters));
249  }
250 }
‪TYPO3\CMS\Core\TypoScript\FrontendTypoScript\getSettingsConditionList
‪getSettingsConditionList()
Definition: FrontendTypoScript.php:62
‪TYPO3\CMS\Core\Locking\ResourceMutex
Definition: ResourceMutex.php:54
‪TYPO3\CMS\Frontend\Middleware\PrepareTypoScriptFrontendRendering
Definition: PrepareTypoScriptFrontendRendering.php:48
‪TYPO3\CMS\Frontend\Middleware\PrepareTypoScriptFrontendRendering\process
‪process(ServerRequestInterface $request, RequestHandlerInterface $handler)
Definition: PrepareTypoScriptFrontendRendering.php:60
‪TYPO3\CMS\Core\Cache\Frontend\PhpFrontend
Definition: PhpFrontend.php:25
‪TYPO3\CMS\Frontend\Middleware\PrepareTypoScriptFrontendRendering\prepareConditionMatcherVariables
‪prepareConditionMatcherVariables(ServerRequestInterface $request)
Definition: PrepareTypoScriptFrontendRendering.php:185
‪TYPO3\CMS\Frontend\Controller\ErrorController
Definition: ErrorController.php:38
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Frontend\Event\BeforePageCacheIdentifierIsHashedEvent
Definition: BeforePageCacheIdentifierIsHashedEvent.php:37
‪TYPO3\CMS\Frontend\Middleware
Definition: BackendUserAuthenticator.php:18
‪TYPO3\CMS\Core\Utility\HttpUtility\buildQueryString
‪static string buildQueryString(array $parameters, string $prependCharacter='', bool $skipEmptyParameters=false)
Definition: HttpUtility.php:124
‪TYPO3\CMS\Frontend\Page\PageAccessFailureReasons\RENDERING_INSTRUCTIONS_NOT_CONFIGURED
‪const RENDERING_INSTRUCTIONS_NOT_CONFIGURED
Definition: PageAccessFailureReasons.php:34
‪TYPO3\CMS\Frontend\Event\AfterTypoScriptDeterminedEvent
Definition: AfterTypoScriptDeterminedEvent.php:41
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Frontend\Middleware\PrepareTypoScriptFrontendRendering\__construct
‪__construct(private EventDispatcherInterface $eventDispatcher, private FrontendTypoScriptFactory $frontendTypoScriptFactory, private PhpFrontend $typoScriptCache, private FrontendInterface $pageCache, private ResourceMutex $lock, private Context $context, private LoggerInterface $logger, private ErrorController $errorController,)
Definition: PrepareTypoScriptFrontendRendering.php:49
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Frontend\Page\CacheHashCalculator
Definition: CacheHashCalculator.php:25
‪TYPO3\CMS\Core\Utility\HttpUtility
Definition: HttpUtility.php:24
‪TYPO3\CMS\Core\TypoScript\FrontendTypoScript
Definition: FrontendTypoScript.php:30
‪TYPO3\CMS\Frontend\Event\ShouldUseCachedPageDataIfAvailableEvent
Definition: ShouldUseCachedPageDataIfAvailableEvent.php:28
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Frontend\Page\PageAccessFailureReasons
Definition: PageAccessFailureReasons.php:25
‪TYPO3\CMS\Core\TypoScript\FrontendTypoScript\getSetupConditionList
‪getSetupConditionList()
Definition: FrontendTypoScript.php:99
‪TYPO3\CMS\Frontend\Middleware\PrepareTypoScriptFrontendRendering\createPageCacheIdentifier
‪createPageCacheIdentifier(ServerRequestInterface $request, FrontendTypoScript $frontendTypoScript)
Definition: PrepareTypoScriptFrontendRendering.php:209