context->hasAspect('frontend.preview')) { $this->context->setAspect('frontend.preview', new PreviewAspect()); } // Verify crucial request attributes exist at this point if (!$request->getAttribute('routing') instanceof PageArguments || !$request->getAttribute('normalizedParams') instanceof NormalizedParams) { throw new \RuntimeException('Request attribute "routing" or "normalizedParams" not found. Error in previous middleware.', 1703150865); } $site = $request->getAttribute('site'); // Cache instruction attribute may have been set by previous middlewares $cacheInstruction = $request->getAttribute('frontend.cache.instruction', new CacheInstruction()); $cacheDataCollector = $request->getAttribute('frontend.cache.collector'); $language = $request->getAttribute('language') ?? $site->getDefaultLanguage(); $routing = $request->getAttribute('routing'); // Add the (internal!) response data "collector" attribute to allow single content elements // to register HTTP header data. We do not check for attribute existence here, since // middlewares *before* could just add their own headers directly. $request = $request->withAttribute('frontend.response.data', new ResponseData()); if ($this->context->getPropertyFromAspect('frontend.preview', 'isPreview', false)) { // Disable cache if this is a preview $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to enabled frontend.preview aspect isPreview.'); } // Make sure cache instruction attribute is always set from now on $request = $request->withAttribute('frontend.cache.instruction', $cacheInstruction); // Did above code or some previous call disable cache? $isCachingAllowed = $cacheInstruction->isCachingAllowed(); // Create and add PageInformation try { $this->timeTracker->push('Create PageInformation'); $pageInformation = $this->pageInformationFactory->create($request); } catch (PageInformationCreationFailedException $exception) { return $exception->getResponse(); } finally { $this->timeTracker->pull(); } $request = $request->withAttribute('frontend.page.information', $pageInformation); $sysTemplateRows = $pageInformation->getSysTemplateRows(); // Init and add register and page parts $request = $request->withAttribute('frontend.register.stack', new RegisterStack()); $pageParts = new PageParts(); // Init "last changed" with "tstamp" of the page record, or SYS_LASTCHANGED if it's younger $lastChanged = (int)$pageInformation->getPageRecord()['tstamp']; if ($lastChanged < (int)$pageInformation->getPageRecord()['SYS_LASTCHANGED']) { $lastChanged = (int)$pageInformation->getPageRecord()['SYS_LASTCHANGED']; } $pageParts->setLastChanged($lastChanged); $request = $request->withAttribute('frontend.page.parts', $pageParts); // Create FrontendTypoScript with essential info for page cache identifier $conditionMatcherVariables = $this->prepareConditionMatcherVariables($request); $frontendTypoScript = $this->frontendTypoScriptFactory->createSettingsAndSetupConditions( $site, $sysTemplateRows, $conditionMatcherVariables, $isCachingAllowed ? $this->typoScriptCache : null, ); // Set up cache relevant information $isUsingPageCacheAllowed = $this->eventDispatcher->dispatch(new ShouldUseCachedPageDataIfAvailableEvent($request, $isCachingAllowed))->shouldUseCachedPageData(); $pageCacheIdentifier = $this->createPageCacheIdentifier($request, $frontendTypoScript); $cacheDataCollector->setPageCacheIdentifier($pageCacheIdentifier); // Get page cache row or lock rendering $pageCacheRow = null; if (!$isUsingPageCacheAllowed) { // Caching not allowed. We'll rebuild the page. Lock this. $this->lock->acquireLock('pages', $pageCacheIdentifier); } else { // Try to get a page cache row. $pageCacheRow = $this->pageCache->get($pageCacheIdentifier); if (!is_array($pageCacheRow)) { // Nothing in the cache, we acquire an exclusive lock now. // There are two scenarios when locking: We're either the first process acquiring this lock. This means we'll // "immediately" get it and can continue with page rendering. Or, another process acquired the lock already. In // this case, the below call will wait until the lock is released again. The other process then probably wrote // a page cache entry, which we can use. // To handle the second case - if our process had to wait for another one creating the content for us - we // simply query the page cache again to see if there is a page cache now. $hadToWaitForLock = $this->lock->acquireLock('pages', $pageCacheIdentifier); // From this point on we're the only one working on that page. if ($hadToWaitForLock) { // Query the cache again to see if the data is there meanwhile: We did not get the lock // immediately, chances are high the other process created a page cache for us. // There is a small chance the other process actually pageCache->set() the content, // but pageCache->get() still returns false, for instance when a database returned "done" // for the INSERT, but SELECT still does not return the new row - may happen in multi-head // DB instances, and with some other distributed cache backends as well. The worst that // can happen here is the page generation is done too often, which we accept as trade-off. $pageCacheRow = $this->pageCache->get($pageCacheIdentifier); if (is_array($pageCacheRow)) { // Got cache row, some other process did the work for us, release our lock again. $this->lock->releaseLock('pages'); } } // Keep lock, we are the one generating the page now and fill cache. } } if (is_array($pageCacheRow)) { // Got page from cache. Set up system state with it. $pageParts->setPageContentWasLoadedFromCache(); $pageParts->setPageCacheGeneratedTimestamp($pageCacheRow['pageCacheGeneratedTimestamp']); $pageParts->setPageCacheExpireTimestamp($pageCacheRow['pageCacheExpireTimestamp']); foreach ($pageCacheRow['INTincScript'] as $intIncScript) { $pageParts->addNotCachedContentElement($intIncScript); } $pageParts->setPageTitle($pageCacheRow['pageTitleCache']); $pageParts->setContent($pageCacheRow['content']); $pageParts->setHttpContentType($pageCacheRow['contentType']); $pageParts->setPageRendererSubstitutionHash($pageCacheRow['pageRendererSubstitutionHash']); if ($pageCacheRow['pageRendererState'] ?? false) { $pageRendererState = unserialize($pageCacheRow['pageRendererState'], ['allowed_classes' => [Locale::class]]); $this->pageRenderer->updateState($pageRendererState); } if ($pageCacheRow['assetCollectorState'] ?? false) { $assetCollectorState = unserialize($pageCacheRow['assetCollectorState'], ['allowed_classes' => false]); $assetCollector = GeneralUtility::makeInstance(AssetCollector::class); $assetCollector->updateState($assetCollectorState); } // Restore the current tags and add them to the CacheTageCollector $lifetime = $pageParts->getPageCacheExpireTimestamp() - $GLOBALS['EXEC_TIME']; $cacheTags = array_map(fn(string $cacheTag) => new CacheTag($cacheTag, $lifetime), $pageCacheRow['cacheTags'] ?? []); $cacheDataCollector->addCacheTags(...$cacheTags); // Restore meta-data state if (is_array($pageCacheRow['metaDataState'] ?? null)) { GeneralUtility::makeInstance(MetaDataState::class)->updateState($pageCacheRow['metaDataState']); } } else { // Init FE PageRenderer defaults when this page needs to be generated if ($language->hasCustomTypo3Language()) { $locale = $this->locales->createLocale($language->getTypo3Language()); } else { // The createLocale() call is needed in order to resolve dependencies from the Locales class $locale = $this->locales->createLocale((string)$language->getLocale()); } $this->pageRenderer->setLanguage($locale, $request); $pageParts->setPageRendererSubstitutionHash(md5(StringUtility::getUniqueId())); $pageParts->setPageCacheGeneratedTimestamp($GLOBALS['EXEC_TIME']); } // Processing of page cache row done. Do not use this variable anymore. unset($pageCacheRow); try { $needsFullSetup = !$pageParts->hasPageContentBeenLoadedFromCache() || $pageParts->hasNotCachedContentElements(); $pageType = $routing->getPageType(); $frontendTypoScript = $this->frontendTypoScriptFactory->createSetupConfigOrFullSetup( $needsFullSetup, $frontendTypoScript, $site, $sysTemplateRows, $conditionMatcherVariables, $pageType, $isCachingAllowed ? $this->typoScriptCache : null, $request, ); if ($needsFullSetup && !$frontendTypoScript->hasPage()) { $this->logger->error('No page configured for type={type}. There is no TypoScript object of type PAGE with typeNum={type}.', ['type' => $pageType]); return $this->errorController->internalErrorAction( $request, 'No page configured for type=' . $pageType . '.', ['code' => PageAccessFailureReasons::RENDERING_INSTRUCTIONS_NOT_CONFIGURED] ); } $setupConfigAst = $frontendTypoScript->getConfigTree(); if ($setupConfigAst->getChildByName('no_cache')?->getValue()) { // Disable cache if config.no_cache is set! $cacheInstruction = $request->getAttribute('frontend.cache.instruction'); $cacheInstruction->disableCache('EXT:frontend: Disabled cache due to TypoScript "config.no_cache = 1"'); } $this->eventDispatcher->dispatch(new AfterTypoScriptDeterminedEvent($frontendTypoScript)); $request = $request->withAttribute('frontend.typoscript', $frontendTypoScript); return $handler->handle($request); } finally { // Whatever happens in a middleware below, this finally is called, even when exceptions // are raised by a lower one. This ensures locks are released no matter what. $this->lock->releaseLock('pages'); } } /** * Data available in TypoScript "condition" matching. */ private function prepareConditionMatcherVariables(ServerRequestInterface $request): array { $pageInformation = $request->getAttribute('frontend.page.information'); $topDownRootLine = $pageInformation->getRootLine(); $localRootline = $pageInformation->getLocalRootLine(); ksort($topDownRootLine); return [ 'request' => $request, 'pageId' => $pageInformation->getId(), 'page' => $pageInformation->getPageRecord(), 'fullRootLine' => $topDownRootLine, 'localRootLine' => $localRootline, 'site' => $request->getAttribute('site'), 'siteLanguage' => $request->getAttribute('language'), ]; } /** * This creates a hash used as page cache entry identifier and as page generation lock. * When multiple requests try to render the same page that will result in the same page cache entry, * this lock allows creation by one request which typically puts the result into page cache, while * the other requests wait until this finished and re-use the result. */ private function createPageCacheIdentifier(ServerRequestInterface $request, FrontendTypoScript $frontendTypoScript): string { $pageInformation = $request->getAttribute('frontend.page.information'); $pageId = $pageInformation->getId(); $pageArguments = $request->getAttribute('routing'); $site = $request->getAttribute('site'); $dynamicArguments = []; $queryParams = $pageArguments->getDynamicArguments(); if (!empty($queryParams) && ($pageArguments->getArguments()['cHash'] ?? false)) { // Fetch arguments relevant for creating the page cache identifier from the PageArguments object. // Excluded parameters are not taken into account when calculating the hash base. $queryParams['id'] = $pageArguments->getPageId(); // @todo: Make CacheHashCalculator and CacheHashConfiguration stateless and get it injected. $dynamicArguments = GeneralUtility::makeInstance(CacheHashCalculator::class) ->getRelevantParameters(HttpUtility::buildQueryString($queryParams)); } $pageCacheIdentifierParameters = [ 'projectPath' => Environment::getProjectPath(), 'id' => $pageId, 'type' => $pageArguments->getPageType(), 'groupIds' => implode(',', $this->context->getAspect('frontend.user')->getGroupIds()), 'MP' => $pageInformation->getMountPoint(), 'site' => $site->getIdentifier(), // Ensure the language base is used for the hash base calculation as well, otherwise TypoScript and page-related rendering // is not cached properly as we don't have any language-specific conditions anymore 'siteBase' => (string)$request->getAttribute('language', $site->getDefaultLanguage())->getBase(), // additional variation trigger for static routes 'staticRouteArguments' => $pageArguments->getStaticArguments(), // dynamic route arguments (if route was resolved) 'dynamicArguments' => $dynamicArguments, 'sysTemplateRows' => $pageInformation->getSysTemplateRows(), 'constantConditionList' => $frontendTypoScript->getSettingsConditionList(), 'setupConditionList' => $frontendTypoScript->getSetupConditionList(), ]; $pageCacheIdentifierParameters = $this->eventDispatcher ->dispatch(new BeforePageCacheIdentifierIsHashedEvent($request, $pageCacheIdentifierParameters)) ->getPageCacheIdentifierParameters(); return $pageId . '_' . hash('xxh3', serialize($pageCacheIdentifierParameters)); } }