Skip to content

Commit e84af96

Browse files
[BUGFIX] Handle new TS-not-set limitation on v13 (#1928)
Uncached rendering context no longer provides TS through ConfigurationManager. An exception is raised instead. To avoid this, we store a representation of the plugin.tx_vhs TS scope in a separate cache when the page is rendered before it is cached - and retrieve this cached TS when needed.
1 parent 2411ce6 commit e84af96

3 files changed

Lines changed: 103 additions & 22 deletions

File tree

Classes/Service/AssetService.php

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
use FluidTYPO3\Vhs\ViewHelpers\Asset\AssetInterface;
1414
use Psr\Http\Message\ServerRequestInterface;
1515
use Psr\Log\LoggerInterface;
16+
use TYPO3\CMS\Core\Cache\CacheManager;
1617
use TYPO3\CMS\Core\Log\LogManager;
18+
use TYPO3\CMS\Core\Routing\PageArguments;
1719
use TYPO3\CMS\Core\SingletonInterface;
1820
use TYPO3\CMS\Core\Utility\ArrayUtility;
1921
use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -40,6 +42,11 @@ class AssetService implements SingletonInterface
4042
*/
4143
protected $configurationManager;
4244

45+
/**
46+
* @var CacheManager
47+
*/
48+
protected $cacheManager;
49+
4350
protected static bool $typoScriptAssetsBuilt = false;
4451
protected static ?array $settingsCache = null;
4552
protected static array $cachedDependencies = [];
@@ -50,6 +57,11 @@ public function injectConfigurationManager(ConfigurationManagerInterface $config
5057
$this->configurationManager = $configurationManager;
5158
}
5259

60+
public function injectCacheManager(CacheManager $cacheManager): void
61+
{
62+
$this->cacheManager = $cacheManager;
63+
}
64+
5365
public function usePageCache(object $caller, bool $shouldUsePageCache): bool
5466
{
5567
$this->buildAll([], $caller);
@@ -63,7 +75,10 @@ public function buildAll(array $parameters, object $caller, bool $cached = true,
6375
}
6476

6577
$settings = $this->getSettings();
66-
$buildTypoScriptAssets = (!static::$typoScriptAssetsBuilt && ($cached || $GLOBALS['TSFE']->no_cache));
78+
$buildTypoScriptAssets = (
79+
!static::$typoScriptAssetsBuilt
80+
&& ($cached || $this->readCacheDisabledInstructionFromContext())
81+
);
6782
if ($buildTypoScriptAssets && isset($settings['asset']) && is_array($settings['asset'])) {
6883
foreach ($settings['asset'] as $name => $typoScriptAsset) {
6984
if (!isset($GLOBALS['VhsAssets'][$name]) && is_array($typoScriptAsset)) {
@@ -130,15 +145,53 @@ public function isAlreadyDefined(string $assetName): bool
130145
public function getSettings(): array
131146
{
132147
if (null === static::$settingsCache) {
148+
static::$settingsCache = $this->getTypoScript()['settings'] ?? [];
149+
}
150+
$settings = (array) static::$settingsCache;
151+
return $settings;
152+
}
153+
154+
protected function getTypoScript(): array
155+
{
156+
$cache = $this->cacheManager->getCache('vhs_main');
157+
$pageUid = $this->readPageUidFromContext();
158+
$cacheId = 'vhs_asset_ts_' . $pageUid;
159+
$cacheTag = 'pageId_' . $pageUid;
160+
161+
try {
133162
$allTypoScript = $this->configurationManager->getConfiguration(
134163
ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT
135164
);
136-
static::$settingsCache = GeneralUtility::removeDotsFromTS(
137-
$allTypoScript['plugin.']['tx_vhs.']['settings.'] ?? []
138-
);
165+
$typoScript = GeneralUtility::removeDotsFromTS($allTypoScript['plugin.']['tx_vhs.'] ?? []);
166+
$cache->set($cacheId, $typoScript, [$cacheTag]);
167+
} catch (\RuntimeException $exception) {
168+
if ($exception->getCode() !== 1666513645) {
169+
// Re-throw, but only if the exception is not the specific "Setup array has not been initialized" one.
170+
throw $exception;
171+
}
172+
173+
// Note: this case will only ever be entered on TYPO3v13 and above. Earlier versions will consistently
174+
// produce the necessary TS array from ConfigurationManager - and will not raise the specified exception.
175+
176+
// We will only look in VHS's cache for a TS array if it wasn't already retrieved by ConfigurationManager.
177+
// This is for performance reasons: the TS array may be relatively large and the cache may be DB-based.
178+
// Whereas if the TS is already in ConfigurationManager, it costs nearly nothing to read. The TS is returned
179+
// even if it is empty.
180+
/** @var array|false $fromCache */
181+
$fromCache = $cache->get($cacheId);
182+
if (is_array($fromCache)) {
183+
return $fromCache;
184+
}
185+
186+
// Graceful: it's better to return empty settings than either adding massive code chunks dealing with
187+
// custom TS reading or allowing an exception to be raised. Note that reaching this case means that the
188+
// PAGE was cached, but VHS's cache for the page is empty. This can be caused by TTL skew. The solution is
189+
// to flush all caches tagged with the page's ID, so the next request will correctly regenerate the entry.
190+
$typoScript = [];
191+
$this->cacheManager->flushCachesByTag($cacheTag);
139192
}
140-
$settings = (array) static::$settingsCache;
141-
return $settings;
193+
194+
return $typoScript;
142195
}
143196

144197
/**
@@ -274,8 +327,9 @@ protected function writeCachedMergedFileAndReturnTag(array $assets, string $type
274327
sort($keys);
275328
$assetName = implode('-', $keys);
276329
unset($keys);
277-
if (isset($GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_vhs.']['assets.']['mergedAssetsUseHashedFilename'])) {
278-
if ($GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_vhs.']['assets.']['mergedAssetsUseHashedFilename']) {
330+
$typoScript = $this->getTypoScript();
331+
if (isset($typoScript['assets']['mergedAssetsUseHashedFilename'])) {
332+
if ($typoScript['assets']['mergedAssetsUseHashedFilename']) {
279333
$assetName = md5($assetName);
280334
}
281335
}
@@ -284,8 +338,7 @@ protected function writeCachedMergedFileAndReturnTag(array $assets, string $type
284338
if (!file_exists($fileAbsolutePathAndFilename)
285339
|| 0 === filemtime($fileAbsolutePathAndFilename)
286340
|| isset($GLOBALS['BE_USER'])
287-
|| ($GLOBALS['TSFE']->no_cache ?? false)
288-
|| ($GLOBALS['TSFE']->page['no_cache'] ?? false)
341+
|| $this->readCacheDisabledInstructionFromContext()
289342
) {
290343
foreach ($assets as $name => $asset) {
291344
$assetSettings = $this->extractAssetSettings($asset);
@@ -348,6 +401,7 @@ protected function generateTagForAssetType(
348401
$file = PathUtility::getAbsoluteWebPath($file);
349402
$file = $this->prefixPath($file);
350403
}
404+
$settings = $this->getTypoScript();
351405
switch ($type) {
352406
case 'js':
353407
$tagBuilder->setTagName('script');
@@ -359,7 +413,7 @@ protected function generateTagForAssetType(
359413
$tagBuilder->addAttribute('src', (string) $file);
360414
}
361415
if (!empty($integrity)) {
362-
if (!empty($GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_vhs.']['settings.']['prependPath'])) {
416+
if (!empty($settings['prependPath'])) {
363417
$tagBuilder->addAttribute('crossorigin', 'anonymous');
364418
}
365419
$tagBuilder->addAttribute('integrity', $integrity);
@@ -387,7 +441,7 @@ protected function generateTagForAssetType(
387441
$tagBuilder->addAttribute('href', $file);
388442
}
389443
if (!empty($integrity)) {
390-
if (!empty($GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_vhs.']['settings.']['prependPath'])) {
444+
if (!empty($settings['prependPath'])) {
391445
$tagBuilder->addAttribute('crossorigin', 'anonymous');
392446
}
393447
$tagBuilder->addAttribute('integrity', $integrity);
@@ -742,19 +796,19 @@ protected function mergeArrays(array $array1, array $array2): array
742796

743797
protected function getFileIntegrity(string $file): ?string
744798
{
745-
$typoScript = $GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_vhs.'] ?? null;
746-
if (isset($typoScript['assets.']['tagsAddSubresourceIntegrity'])) {
799+
$typoScript = $this->getTypoScript();
800+
if (isset($typoScript['assets']['tagsAddSubresourceIntegrity'])) {
747801
// Note: 3 predefined hashing strategies (the ones suggestes in the rfc sheet)
748-
if (0 < $typoScript['assets.']['tagsAddSubresourceIntegrity']
749-
&& $typoScript['assets.']['tagsAddSubresourceIntegrity'] < 4
802+
if (0 < $typoScript['assets']['tagsAddSubresourceIntegrity']
803+
&& $typoScript['assets']['tagsAddSubresourceIntegrity'] < 4
750804
) {
751805
if (!file_exists($file)) {
752806
return null;
753807
}
754808

755809
$integrity = null;
756810
$integrityMethod = ['sha256','sha384','sha512'][
757-
$typoScript['assets.']['tagsAddSubresourceIntegrity'] - 1
811+
$typoScript['assets']['tagsAddSubresourceIntegrity'] - 1
758812
];
759813
$integrityFile = sprintf(
760814
$this->getTempPath() . 'vhs-assets-%s.%s',
@@ -799,6 +853,16 @@ protected function resolveAbsolutePathForFile(string $filename): string
799853
return GeneralUtility::getFileAbsFileName($filename);
800854
}
801855

856+
protected function readPageUidFromContext(): int
857+
{
858+
/** @var ServerRequestInterface $serverRequest */
859+
$serverRequest = $GLOBALS['TYPO3_REQUEST'];
860+
861+
/** @var PageArguments $pageArguments */
862+
$pageArguments = $serverRequest->getAttribute('routing');
863+
return $pageArguments->getPageId();
864+
}
865+
802866
protected function readCacheDisabledInstructionFromContext(): bool
803867
{
804868
$hasDisabledInstructionInRequest = false;

Tests/Unit/Service/AssetServiceTest.php

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,21 @@ public function testBuildAll(array $assets, $cached, $expectedFiles)
4747
->getMock();
4848
$GLOBALS['TSFE']->content = 'content';
4949
$instance = $this->getMockBuilder(AssetService::class)
50-
->setMethods(['writeFile', 'getSettings', 'resolveAbsolutePathForFile'])
50+
->onlyMethods(
51+
[
52+
'writeFile',
53+
'getSettings',
54+
'resolveAbsolutePathForFile',
55+
'getTypoScript',
56+
'readCacheDisabledInstructionFromContext'
57+
]
58+
)
5159
->getMock();
5260
$instance->expects($this->exactly($expectedFiles))
5361
->method('writeFile')
5462
->with($this->anything(), $this->anything());
5563
$instance->method('getSettings')->willReturn([]);
64+
$instance->method('getTypoScript')->willReturn([]);
5665
$instance->method('resolveAbsolutePathForFile')->willReturnArgument(0);
5766
if (true === $cached) {
5867
$instance->buildAll([], $this, $cached);
@@ -118,13 +127,20 @@ public function testIntegrityCalculation()
118127
'sha384-aieE32yQSOy7uEhUkUvR9bVgfJgMsP+B9TthbxbjDDZ2hd4tjV5jMUoj9P8aeSHI',
119128
'sha512-0bz2YVKEoytikWIUFpo6lK/k2cVVngypgaItFoRvNfux/temtdCVxsu+HxmdRT8aNOeJxxREUphbkcAK8KpkWg==',
120129
];
130+
121131
$file = 'Tests/Fixtures/Files/dummy.js';
122-
$method = (new \ReflectionClass(AssetService::class))->getMethod('getFileIntegrity');
123-
$instance = $this->getMockBuilder(AssetService::class)->setMethods(['writeFile'])->getMock();
124132

125-
$method->setAccessible(true);
126133
foreach ($expectedIntegrities as $settingLevel => $expectedIntegrity) {
127-
$GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_vhs.']['assets.']['tagsAddSubresourceIntegrity'] = $settingLevel;
134+
$method = (new \ReflectionClass(AssetService::class))->getMethod('getFileIntegrity');
135+
$instance = $this->getMockBuilder(AssetService::class)->onlyMethods(['writeFile', 'getTypoScript'])->getMock();
136+
$instance->method('getTypoScript')->willReturn(
137+
[
138+
'assets' => [
139+
'tagsAddSubresourceIntegrity' => $settingLevel,
140+
],
141+
]
142+
);
143+
$method->setAccessible(true);
128144
$this->assertEquals($expectedIntegrity, $method->invokeArgs($instance, [$file]));
129145
}
130146
}

ext_localconf.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['vhs_markdown'] = [
2828
'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
2929
'options' => [
30+
// You should keep this value HIGHER than the lifetime of TYPO3's page caches at all times.
3031
'defaultLifetime' => 804600
3132
],
3233
'groups' => ['pages', 'all']

0 commit comments

Comments
 (0)