• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FluidTYPO3 / vhs / 14883190726

07 May 2025 12:20PM UTC coverage: 72.039% (-0.05%) from 72.089%
14883190726

Pull #1945

github

web-flow
Merge afe6cb89e into 24c5764dc
Pull Request #1945: Fix database deadlock exception in AssetService::getTypoScript

23 of 54 new or added lines in 1 file covered. (42.59%)

2 existing lines in 1 file now uncovered.

5650 of 7843 relevant lines covered (72.04%)

20.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.59
/Classes/Service/AssetService.php
1
<?php
2
namespace FluidTYPO3\Vhs\Service;
3

4
/*
5
 * This file is part of the FluidTYPO3/Vhs project under GPLv2 or later.
6
 *
7
 * For the full copyright and license information, please read the
8
 * LICENSE.md file that was distributed with this source code.
9
 */
10

11
use FluidTYPO3\Vhs\Asset;
12
use FluidTYPO3\Vhs\Utility\CoreUtility;
13
use FluidTYPO3\Vhs\ViewHelpers\Asset\AssetInterface;
14
use Psr\Http\Message\ServerRequestInterface;
15
use Psr\Log\LoggerInterface;
16
use TYPO3\CMS\Core\Cache\CacheManager;
17
use TYPO3\CMS\Core\Log\LogManager;
18
use TYPO3\CMS\Core\Routing\PageArguments;
19
use TYPO3\CMS\Core\Routing\RouteResultInterface;
20
use TYPO3\CMS\Core\SingletonInterface;
21
use TYPO3\CMS\Core\Utility\ArrayUtility;
22
use TYPO3\CMS\Core\Utility\GeneralUtility;
23
use TYPO3\CMS\Core\Utility\PathUtility;
24
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
25
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
26
use TYPO3\CMS\Fluid\View\StandaloneView;
27
use TYPO3\CMS\Frontend\Cache\CacheInstruction;
28
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
29
use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder;
30

31
/**
32
 * Asset Handling Service
33
 *
34
 * Inject this Service in your class to access VHS Asset
35
 * features - include assets etc.
36
 */
37
class AssetService implements SingletonInterface
38
{
39
    const ASSET_SIGNAL = 'writeAssetFile';
40

41
    /**
42
     * @var ConfigurationManagerInterface
43
     */
44
    protected $configurationManager;
45

46
    /**
47
     * @var CacheManager
48
     */
49
    protected $cacheManager;
50

51
    protected static bool $typoScriptAssetsBuilt = false;
52
    protected static ?array $typoScriptCache = null;
53
    protected static bool $currentlyBuildingCacheable = true;
54
    protected static array $cachedDependencies = [];
55
    protected static bool $cacheCleared = false;
56

57
    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
58
    {
59
        $this->configurationManager = $configurationManager;
×
60
    }
61

62
    public function injectCacheManager(CacheManager $cacheManager): void
63
    {
64
        $this->cacheManager = $cacheManager;
×
65
    }
66

67
    public function usePageCache(object $caller, bool $shouldUsePageCache): bool
68
    {
69
        $this->buildAll([], $caller);
×
70
        return $shouldUsePageCache;
×
71
    }
72

73
    public function buildAll(array $parameters, object $caller, bool $cached = true, ?string &$content = null): void
74
    {
75
        $wasBuildingCacheableBefore = static::$currentlyBuildingCacheable;
49✔
76
        if ($caller instanceof TypoScriptFrontendController && $caller->isINTincScript()) {
49✔
NEW
77
            static::$currentlyBuildingCacheable = false;
×
78
        }
79
        try {
80
            if ($content === null) {
49✔
81
                $content = &$caller->content;
49✔
82
            }
83

84
            $settings = $this->getSettings();
49✔
85
            $buildTypoScriptAssets = (
49✔
86
                !static::$typoScriptAssetsBuilt
49✔
87
                && ($cached || $this->readCacheDisabledInstructionFromContext())
49✔
88
            );
49✔
89
            if ($buildTypoScriptAssets && isset($settings['asset']) && is_array($settings['asset'])) {
49✔
NEW
90
                foreach ($settings['asset'] as $name => $typoScriptAsset) {
×
NEW
91
                    if (!isset($GLOBALS['VhsAssets'][$name]) && is_array($typoScriptAsset)) {
×
NEW
92
                        if (!isset($typoScriptAsset['name'])) {
×
NEW
93
                            $typoScriptAsset['name'] = $name;
×
94
                        }
NEW
95
                        if (isset($typoScriptAsset['dependencies']) && !is_array($typoScriptAsset['dependencies'])) {
×
NEW
96
                            $typoScriptAsset['dependencies'] = GeneralUtility::trimExplode(
×
NEW
97
                                ',',
×
NEW
98
                                (string) $typoScriptAsset['dependencies'],
×
NEW
99
                                true
×
NEW
100
                            );
×
101
                        }
NEW
102
                        Asset::createFromSettings($typoScriptAsset);
×
103
                    }
104
                }
NEW
105
                static::$typoScriptAssetsBuilt = true;
×
106
            }
107
            if (empty($GLOBALS['VhsAssets']) || !is_array($GLOBALS['VhsAssets'])) {
49✔
108
                return;
14✔
109
            }
110
            $assets = $GLOBALS['VhsAssets'];
35✔
111
            $assets = $this->sortAssetsByDependency($assets);
35✔
112
            $assets = $this->manipulateAssetsByTypoScriptSettings($assets);
35✔
113
            $buildDebugRequested = (isset($settings['asset']['debugBuild']) && $settings['asset']['debugBuild'] > 0);
35✔
114
            $assetDebugRequested = (isset($settings['asset']['debug']) && $settings['asset']['debug'] > 0);
35✔
115
            $useDebugUtility =
35✔
116
                (isset($settings['asset']['useDebugUtility']) && $settings['asset']['useDebugUtility'] > 0)
35✔
117
                || !isset($settings['asset']['useDebugUtility']);
35✔
118
            if ($buildDebugRequested || $assetDebugRequested) {
35✔
NEW
119
                if ($useDebugUtility) {
×
NEW
120
                    DebuggerUtility::var_dump($assets);
×
121
                } else {
NEW
122
                    echo var_export($assets, true);
×
123
                }
124
            }
125
            $this->placeAssetsInHeaderAndFooter($assets, $cached, $content);
35✔
126
        } finally {
127
            static::$currentlyBuildingCacheable = $wasBuildingCacheableBefore;
49✔
128
        }
129
    }
130

131
    public function buildAllUncached(array $parameters, object $caller, ?string &$content = null): void
132
    {
133
        if ($content === null) {
7✔
134
            $content = &$caller->content;
7✔
135
        }
136
        $matches = [];
7✔
137
        preg_match_all('/\<\![\-]+\ VhsAssetsDependenciesLoaded ([^ ]+) [\-]+\>/i', $content, $matches);
7✔
138
        foreach ($matches[1] as $key => $match) {
7✔
139
            $extractedDependencies = explode(',', $matches[1][$key]);
×
140
            static::$cachedDependencies = array_merge(static::$cachedDependencies, $extractedDependencies);
×
141
        }
142

143
        $this->buildAll($parameters, $caller, false, $content);
7✔
144
    }
145

146
    public function isAlreadyDefined(string $assetName): bool
147
    {
148
        return isset($GLOBALS['VhsAssets'][$assetName]) || in_array($assetName, self::$cachedDependencies, true);
×
149
    }
150

151
    /**
152
     * Returns the settings used by this particular Asset
153
     * during inclusion. Public access allows later inspection
154
     * of the TypoScript values which were applied to the Asset.
155
     */
156
    public function getSettings(): array
157
    {
NEW
158
        return $this->getTypoScript()['settings'] ?? [];
×
159
    }
160

161
    protected function getTypoScript(): array
162
    {
NEW
163
        if (static::$typoScriptCache !== null) {
×
NEW
164
            return static::$typoScriptCache;
×
165
        }
166

167
        try {
168
            $allTypoScript = $this->configurationManager->getConfiguration(
×
169
                ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT
×
170
            );
×
171
            $typoScript = GeneralUtility::removeDotsFromTS($allTypoScript['plugin.']['tx_vhs.'] ?? []);
×
172

173
            // If we are rendering cached content we need to persist the
174
            // TypoScript for future requests that should be answered from
175
            // cache. Newer TYPO3 versions no longer load TypoScript when
176
            // answering requests from cache, triggering the \RuntimeException
177
            // "Setup array has not been initialized" above.
NEW
178
            if (static::$currentlyBuildingCacheable) {
×
NEW
179
                $pageUid = $this->readPageUidFromContext();
×
NEW
180
                $cache = $this->cacheManager->getCache('vhs_main');
×
NEW
181
                $cacheId = 'vhs_asset_ts_' . $pageUid;
×
NEW
182
                $cacheTag = 'pageId_' . $pageUid;
×
NEW
183
                $cache->set($cacheId, $typoScript, [$cacheTag]);
×
184
            }
185
        } catch (\RuntimeException $exception) {
×
186
            if ($exception->getCode() !== 1666513645) {
×
187
                // Re-throw, but only if the exception is not the specific "Setup array has not been initialized" one.
188
                throw $exception;
×
189
            }
190

NEW
191
            $pageUid = $this->readPageUidFromContext();
×
NEW
192
            $cache = $this->cacheManager->getCache('vhs_main');
×
NEW
193
            $cacheId = 'vhs_asset_ts_' . $pageUid;
×
NEW
194
            $cacheTag = 'pageId_' . $pageUid;
×
195

196
            // Note: this case will only ever be entered on TYPO3v13 and above. Earlier versions will consistently
197
            // produce the necessary TS array from ConfigurationManager - and will not raise the specified exception.
198

199
            // We will only look in VHS's cache for a TS array if it wasn't already retrieved by ConfigurationManager.
200
            // This is for performance reasons: the TS array may be relatively large and the cache may be DB-based.
201
            // Whereas if the TS is already in ConfigurationManager, it costs nearly nothing to read. The TS is returned
202
            // even if it is empty.
203
            /** @var array|false $fromCache */
204
            $fromCache = $cache->get($cacheId);
×
205
            if (is_array($fromCache)) {
×
NEW
206
                static::$typoScriptCache = $fromCache;
×
UNCOV
207
                return $fromCache;
×
208
            }
209

210
            // Graceful: it's better to return empty settings than either adding massive code chunks dealing with
211
            // custom TS reading or allowing an exception to be raised. Note that reaching this case means that the
212
            // PAGE was cached, but VHS's cache for the page is empty. This can be caused by TTL skew. The solution is
213
            // to flush all caches tagged with the page's ID, so the next request will correctly regenerate the entry.
214
            $typoScript = [];
×
215
            $this->cacheManager->flushCachesByTag($cacheTag);
×
216
        }
217

NEW
218
        static::$typoScriptCache = $typoScript;
×
UNCOV
219
        return $typoScript;
×
220
    }
221

222
    /**
223
     * @param AssetInterface[]|array[] $assets
224
     */
225
    protected function placeAssetsInHeaderAndFooter(array $assets, bool $cached, ?string &$content): void
226
    {
227
        $settings = $this->getSettings();
35✔
228
        $header = [];
35✔
229
        $footer = [];
35✔
230
        $footerRelocationEnabled = (isset($settings['enableFooterRelocation']) && $settings['relocateToFooter'] > 0)
35✔
231
            || !isset($settings['enableFooterRelocation']);
35✔
232
        foreach ($assets as $name => $asset) {
35✔
233
            if ($asset instanceof AssetInterface) {
35✔
234
                $variables = $asset->getVariables();
35✔
235
            } else {
236
                $variables = (array) ($asset['variables'] ?? []);
×
237
            }
238

239
            if (0 < count($variables)) {
35✔
240
                $name .= '-' . md5(serialize($variables));
×
241
            }
242
            if ($this->assertAssetAllowedInFooter($asset) && $footerRelocationEnabled) {
35✔
243
                $footer[$name] = $asset;
35✔
244
            } else {
245
                $header[$name] = $asset;
21✔
246
            }
247
        }
248
        if (!$cached) {
35✔
249
            $uncachedSuffix = 'Uncached';
×
250
        } else {
251
            $uncachedSuffix = '';
35✔
252
            $dependenciesString = '<!-- VhsAssetsDependenciesLoaded ' . implode(',', array_keys($assets)) . ' -->';
35✔
253
            $this->insertAssetsAtMarker('DependenciesLoaded', $dependenciesString, $content);
35✔
254
        }
255
        $this->insertAssetsAtMarker('Header' . $uncachedSuffix, $header, $content);
35✔
256
        $this->insertAssetsAtMarker('Footer' . $uncachedSuffix, $footer, $content);
35✔
257
        $GLOBALS['VhsAssets'] = [];
35✔
258
    }
259

260
    /**
261
     * @param AssetInterface[]|array[]|string $assets
262
     */
263
    protected function insertAssetsAtMarker(string $markerName, $assets, ?string &$content): void
264
    {
265
        $assetMarker = '<!-- VhsAssets' . $markerName . ' -->';
35✔
266

267
        if (is_array($assets)) {
35✔
268
            $chunk = $this->buildAssetsChunk($assets);
35✔
269
        } else {
270
            $chunk = $assets;
35✔
271
        }
272

273
        if (false === strpos((string) $content, $assetMarker)) {
35✔
274
            $inFooter = false !== strpos($markerName, 'Footer');
35✔
275
            $tag = $inFooter ? '</body>' : '</head>';
35✔
276
            $position = strrpos((string) $content, $tag);
35✔
277

278
            if ($position) {
35✔
279
                $content = substr_replace((string) $content, LF . $chunk, $position, 0);
20✔
280
            }
281
        } else {
282
            $content = str_replace($assetMarker, $assetMarker . LF . $chunk, (string) $content);
×
283
        }
284
    }
285

286
    protected function buildAssetsChunk(array $assets): string
287
    {
288
        $spool = [];
35✔
289
        foreach ($assets as $name => $asset) {
35✔
290
            $assetSettings = $this->extractAssetSettings($asset);
35✔
291
            $type = $assetSettings['type'];
35✔
292
            if (!isset($spool[$type])) {
35✔
293
                $spool[$type] = [];
35✔
294
            }
295
            $spool[$type][$name] = $asset;
35✔
296
        }
297
        $chunks = [];
35✔
298
        /**
299
         * @var string $type
300
         * @var AssetInterface[] $spooledAssets
301
         */
302
        foreach ($spool as $type => $spooledAssets) {
35✔
303
            $chunk = [];
35✔
304
            foreach ($spooledAssets as $name => $asset) {
35✔
305
                $assetSettings = $this->extractAssetSettings($asset);
35✔
306
                $standalone = (boolean) $assetSettings['standalone'];
35✔
307
                $external = (boolean) $assetSettings['external'];
35✔
308
                $rewrite = (boolean) $assetSettings['rewrite'];
35✔
309
                $path = $assetSettings['path'];
35✔
310
                if (!$standalone) {
35✔
311
                    $chunk[$name] = $asset;
35✔
312
                } else {
313
                    if (0 < count($chunk)) {
7✔
314
                        $mergedFileTag = $this->writeCachedMergedFileAndReturnTag($chunk, $type);
7✔
315
                        $chunks[] = $mergedFileTag;
7✔
316
                        $chunk = [];
7✔
317
                    }
318
                    if (empty($path)) {
7✔
319
                        $assetContent = $this->extractAssetContent($asset);
7✔
320
                        $chunks[] = $this->generateTagForAssetType($type, $assetContent, null, null, $assetSettings);
7✔
321
                    } else {
322
                        if ($external) {
×
323
                            $chunks[] = $this->generateTagForAssetType($type, null, $path, null, $assetSettings);
×
324
                        } else {
325
                            if ($rewrite) {
×
326
                                $chunks[] = $this->writeCachedMergedFileAndReturnTag([$name => $asset], $type);
×
327
                            } else {
328
                                $chunks[] = $this->generateTagForAssetType(
×
329
                                    $type,
×
330
                                    null,
×
331
                                    $path,
×
332
                                    $this->getFileIntegrity($path),
×
333
                                    $assetSettings
×
334
                                );
×
335
                            }
336
                        }
337
                    }
338
                }
339
            }
340
            if (0 < count($chunk)) {
35✔
341
                $mergedFileTag = $this->writeCachedMergedFileAndReturnTag($chunk, $type);
35✔
342
                $chunks[] = $mergedFileTag;
35✔
343
            }
344
        }
345
        return implode(LF, $chunks);
35✔
346
    }
347

348
    protected function writeCachedMergedFileAndReturnTag(array $assets, string $type): ?string
349
    {
350
        $source = '';
35✔
351
        $keys = array_keys($assets);
35✔
352
        sort($keys);
35✔
353
        $assetName = implode('-', $keys);
35✔
354
        unset($keys);
35✔
355
        $typoScript = $this->getTypoScript();
35✔
356
        if (isset($typoScript['assets']['mergedAssetsUseHashedFilename'])) {
35✔
357
            if ($typoScript['assets']['mergedAssetsUseHashedFilename']) {
×
358
                $assetName = md5($assetName);
×
359
            }
360
        }
361
        $fileRelativePathAndFilename = $this->getTempPath() . 'vhs-assets-' . $assetName . '.' . $type;
35✔
362
        $fileAbsolutePathAndFilename = $this->resolveAbsolutePathForFile($fileRelativePathAndFilename);
35✔
363
        if (!file_exists($fileAbsolutePathAndFilename)
35✔
364
            || 0 === filemtime($fileAbsolutePathAndFilename)
×
365
            || isset($GLOBALS['BE_USER'])
×
366
            || $this->readCacheDisabledInstructionFromContext()
35✔
367
        ) {
368
            foreach ($assets as $name => $asset) {
35✔
369
                $assetSettings = $this->extractAssetSettings($asset);
35✔
370
                if ((isset($assetSettings['namedChunks']) && 0 < $assetSettings['namedChunks']) ||
35✔
371
                    !isset($assetSettings['namedChunks'])) {
35✔
372
                    $source .= '/* ' . $name . ' */' . LF;
×
373
                }
374
                $source .= $this->extractAssetContent($asset) . LF;
35✔
375
                // Put a return carriage between assets preventing broken content.
376
                $source .= "\n";
35✔
377
            }
378
            $this->writeFile($fileAbsolutePathAndFilename, $source);
35✔
379
        }
380
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'])) {
35✔
381
            $timestampMode = $GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'];
×
382
            if (file_exists($fileRelativePathAndFilename)) {
×
383
                $lastModificationTime = filemtime($fileRelativePathAndFilename);
×
384
                if ('querystring' === $timestampMode) {
×
385
                    $fileRelativePathAndFilename .= '?' . $lastModificationTime;
×
386
                } elseif ('embed' === $timestampMode) {
×
387
                    $fileRelativePathAndFilename = substr_replace(
×
388
                        $fileRelativePathAndFilename,
×
389
                        '.' . $lastModificationTime,
×
390
                        (int) strrpos($fileRelativePathAndFilename, '.'),
×
391
                        0
×
392
                    );
×
393
                }
394
            }
395
        }
396
        $fileRelativePathAndFilename = $this->prefixPath($fileRelativePathAndFilename);
35✔
397
        $integrity = $this->getFileIntegrity($fileAbsolutePathAndFilename);
35✔
398

399
        $assetSettings = null;
35✔
400
        if (count($assets) === 1) {
35✔
401
            $extractedAssetSettings = $this->extractAssetSettings($assets[array_keys($assets)[0]]);
35✔
402
            if ($extractedAssetSettings['standalone']) {
35✔
403
                $assetSettings = $extractedAssetSettings;
×
404
            }
405
        }
406

407
        return $this->generateTagForAssetType($type, null, $fileRelativePathAndFilename, $integrity, $assetSettings);
35✔
408
    }
409

410
    protected function generateTagForAssetType(
411
        string $type,
412
        ?string $content,
413
        ?string $file = null,
414
        ?string $integrity = null,
415
        ?array $standaloneAssetSettings = null
416
    ): ?string {
417
        /** @var TagBuilder $tagBuilder */
418
        $tagBuilder = GeneralUtility::makeInstance(TagBuilder::class);
35✔
419
        if (null === $file && empty($content)) {
35✔
420
            $content = '<!-- Empty tag content -->';
×
421
        }
422
        if (empty($type) && !empty($file)) {
35✔
423
            $type = pathinfo($file, PATHINFO_EXTENSION);
×
424
        }
425
        if ($file !== null) {
35✔
426
            $file = PathUtility::getAbsoluteWebPath($file);
35✔
427
            $file = $this->prefixPath($file);
35✔
428
        }
429
        $settings = $this->getTypoScript();
35✔
430
        switch ($type) {
431
            case 'js':
35✔
432
                $tagBuilder->setTagName('script');
35✔
433
                $tagBuilder->forceClosingTag(true);
35✔
434
                $tagBuilder->addAttribute('type', 'text/javascript');
35✔
435
                if (null === $file) {
35✔
436
                    $tagBuilder->setContent((string) $content);
×
437
                } else {
438
                    $tagBuilder->addAttribute('src', (string) $file);
35✔
439
                }
440
                if (!empty($integrity)) {
35✔
441
                    if (!empty($settings['prependPath'])) {
×
442
                        $tagBuilder->addAttribute('crossorigin', 'anonymous');
×
443
                    }
444
                    $tagBuilder->addAttribute('integrity', $integrity);
×
445
                }
446
                if ($standaloneAssetSettings) {
35✔
447
                    // using async and defer simultaneously does not make sense technically, but do not enforce
448
                    if ($standaloneAssetSettings['async']) {
×
449
                        $tagBuilder->addAttribute('async', 'async');
×
450
                    }
451
                    if ($standaloneAssetSettings['defer']) {
×
452
                        $tagBuilder->addAttribute('defer', 'defer');
×
453
                    }
454
                }
455
                break;
35✔
456
            case 'css':
21✔
457
                if (null === $file) {
21✔
458
                    $tagBuilder->setTagName('style');
7✔
459
                    $tagBuilder->forceClosingTag(true);
7✔
460
                    $tagBuilder->addAttribute('type', 'text/css');
7✔
461
                    $tagBuilder->setContent((string) $content);
7✔
462
                } else {
463
                    $tagBuilder->forceClosingTag(false);
21✔
464
                    $tagBuilder->setTagName('link');
21✔
465
                    $tagBuilder->addAttribute('rel', 'stylesheet');
21✔
466
                    $tagBuilder->addAttribute('href', $file);
21✔
467
                }
468
                if (!empty($integrity)) {
21✔
469
                    if (!empty($settings['prependPath'])) {
×
470
                        $tagBuilder->addAttribute('crossorigin', 'anonymous');
×
471
                    }
472
                    $tagBuilder->addAttribute('integrity', $integrity);
×
473
                }
474
                break;
21✔
475
            case 'meta':
×
476
                $tagBuilder->forceClosingTag(false);
×
477
                $tagBuilder->setTagName('meta');
×
478
                break;
×
479
            default:
480
                if (null === $file) {
×
481
                    return $content;
×
482
                }
483
                throw new \RuntimeException(
×
484
                    'Attempt to include file based asset with unknown type ("' . $type . '")',
×
485
                    1358645219
×
486
                );
×
487
        }
488
        return $tagBuilder->render();
35✔
489
    }
490

491
    /**
492
     * @param AssetInterface[] $assets
493
     * @return AssetInterface[]
494
     */
495
    protected function manipulateAssetsByTypoScriptSettings(array $assets): array
496
    {
497
        $settings = $this->getSettings();
35✔
498
        if (!(isset($settings['asset']) || isset($settings['assetGroup']))) {
35✔
499
            return $assets;
35✔
500
        }
501
        $filtered = [];
×
502
        foreach ($assets as $name => $asset) {
×
503
            $assetSettings = $this->extractAssetSettings($asset);
×
504
            $groupName = $assetSettings['group'];
×
505
            $removed = $assetSettings['removed'] ?? false;
×
506
            if ($removed) {
×
507
                continue;
×
508
            }
509
            $localSettings = $assetSettings;
×
510
            if (isset($settings['asset'])) {
×
511
                $localSettings = $this->mergeArrays($localSettings, (array) $settings['asset']);
×
512
            }
513
            if (isset($settings['asset'][$name])) {
×
514
                $localSettings = $this->mergeArrays($localSettings, (array) $settings['asset'][$name]);
×
515
            }
516
            if (isset($settings['assetGroup'][$groupName])) {
×
517
                $localSettings = $this->mergeArrays($localSettings, (array) $settings['assetGroup'][$groupName]);
×
518
            }
519
            if ($asset instanceof AssetInterface) {
×
520
                if (method_exists($asset, 'setSettings')) {
×
521
                    $asset->setSettings($localSettings);
×
522
                }
523
                $filtered[$name] = $asset;
×
524
            } else {
525
                $filtered[$name] = Asset::createFromSettings($assetSettings);
×
526
            }
527
        }
528
        return $filtered;
×
529
    }
530

531
    /**
532
     * @param AssetInterface[] $assets
533
     * @return AssetInterface[]
534
     */
535
    protected function sortAssetsByDependency(array $assets): array
536
    {
537
        $placed = [];
35✔
538
        $assetNames = (0 < count($assets)) ? array_combine(array_keys($assets), array_keys($assets)) : [];
35✔
539
        while ($asset = array_shift($assets)) {
35✔
540
            $postpone = false;
35✔
541
            /** @var AssetInterface $asset */
542
            $assetSettings = $this->extractAssetSettings($asset);
35✔
543
            $name = array_shift($assetNames);
35✔
544
            $dependencies = $assetSettings['dependencies'];
35✔
545
            if (!is_array($dependencies)) {
35✔
546
                $dependencies = GeneralUtility::trimExplode(',', $assetSettings['dependencies'] ?? '', true);
×
547
            }
548
            foreach ($dependencies as $dependency) {
35✔
549
                if (array_key_exists($dependency, $assets)
×
550
                    && !isset($placed[$dependency])
×
551
                    && !in_array($dependency, static::$cachedDependencies)
×
552
                ) {
553
                    // shove the Asset back to the end of the queue, the dependency has
554
                    // not yet been encountered and moving this item to the back of the
555
                    // queue ensures it will be encountered before re-encountering this
556
                    // specific Asset
557
                    if (0 === count($assets)) {
×
558
                        throw new \RuntimeException(
×
559
                            sprintf(
×
560
                                'Asset "%s" depends on "%s" but "%s" was not found',
×
561
                                $name,
×
562
                                $dependency,
×
563
                                $dependency
×
564
                            ),
×
565
                            1358603979
×
566
                        );
×
567
                    }
568
                    $assets[$name] = $asset;
×
569
                    $assetNames[$name] = $name;
×
570
                    $postpone = true;
×
571
                }
572
            }
573
            if (!$postpone) {
35✔
574
                $placed[$name] = $asset;
35✔
575
            }
576
        }
577
        return $placed;
35✔
578
    }
579

580
    /**
581
     * @param AssetInterface|array $asset
582
     */
583
    protected function renderAssetAsFluidTemplate($asset): string
584
    {
585
        $settings = $this->extractAssetSettings($asset);
×
586
        if (isset($settings['variables']) && is_array($settings['variables'])) {
×
587
            $variables =  $settings['variables'];
×
588
        } else {
589
            $variables = [];
×
590
        }
591
        $contents = $this->buildAsset($asset);
×
592
        if ($contents === null) {
×
593
            return '';
×
594
        }
595
        $variables = GeneralUtility::removeDotsFromTS($variables);
×
596
        /** @var StandaloneView $view */
597
        $view = GeneralUtility::makeInstance(StandaloneView::class);
×
598
        $view->setTemplateSource($contents);
×
599
        $view->assignMultiple($variables);
×
600
        $content = $view->render();
×
601
        return is_string($content) ? $content : '';
×
602
    }
603

604
    /**
605
     * Prefix a path according to "absRefPrefix" TS configuration.
606
     */
607
    protected function prefixPath(string $fileRelativePathAndFilename): string
608
    {
609
        $settings = $this->getSettings();
35✔
610
        $prefixPath = $settings['prependPath'] ?? '';
35✔
611
        if (!empty($prefixPath)) {
35✔
612
            $fileRelativePathAndFilename = $prefixPath . $fileRelativePathAndFilename;
×
613
        }
614
        return $fileRelativePathAndFilename;
35✔
615
    }
616

617
    /**
618
     * Fixes the relative paths inside of url() references in CSS files
619
     */
620
    protected function detectAndCopyFileReferences(string $contents, string $originalDirectory): string
621
    {
622
        if (false !== stripos($contents, 'url')) {
21✔
623
            $regex = '/url(\\(\\s*["\']?(?!\\/)([^"\']+)["\']?\\s*\\))/iU';
×
624
            $contents = $this->copyReferencedFilesAndReplacePaths($contents, $regex, $originalDirectory, '(\'|\')');
×
625
        }
626
        if (false !== stripos($contents, '@import')) {
21✔
627
            $regex = '/@import\\s*(["\']?(?!\\/)([^"\']+)["\']?)/i';
×
628
            $contents = $this->copyReferencedFilesAndReplacePaths($contents, $regex, $originalDirectory, '"|"');
×
629
        }
630
        return $contents;
21✔
631
    }
632

633
    /**
634
     * Finds and replaces all URLs by using a given regex
635
     */
636
    protected function copyReferencedFilesAndReplacePaths(
637
        string $contents,
638
        string $regex,
639
        string $originalDirectory,
640
        string $wrap = '|'
641
    ): string {
642
        $matches = [];
×
643
        $replacements = [];
×
644
        $wrap = explode('|', $wrap);
×
645
        preg_match_all($regex, $contents, $matches);
×
646
        $logger = null;
×
647
        if (class_exists(LogManager::class)) {
×
648
            /** @var LogManager $logManager */
649
            $logManager = GeneralUtility::makeInstance(LogManager::class);
×
650
            $logger = $logManager->getLogger(__CLASS__);
×
651
        }
652
        foreach ($matches[2] as $matchCount => $match) {
×
653
            $match = trim($match, '\'" ');
×
654
            if (false === strpos($match, ':') && !preg_match('/url\\s*\\(/i', $match)) {
×
655
                $checksum = md5($originalDirectory . $match);
×
656
                if (0 < preg_match('/([^\?#]+)(.+)?/', $match, $items)) {
×
657
                    $path = $items[1] ?? '';
×
658
                    $suffix = $items[2] ?? '';
×
659
                } else {
660
                    $path = $match;
×
661
                    $suffix = '';
×
662
                }
663
                $newPath = basename($path);
×
664
                $extension = pathinfo($newPath, PATHINFO_EXTENSION);
×
665
                $temporaryFileName = 'vhs-assets-css-' . $checksum . '.' . $extension;
×
666
                $temporaryFile = CoreUtility::getSitePath() . $this->getTempPath() . $temporaryFileName;
×
667
                $rawPath = GeneralUtility::getFileAbsFileName(
×
668
                    $originalDirectory . (empty($originalDirectory) ? '' : '/')
×
669
                ) . $path;
×
670
                $realPath = realpath($rawPath);
×
671
                if (false === $realPath) {
×
672
                    $message = 'Asset at path "' . $rawPath . '" not found. Processing skipped.';
×
673
                    if ($logger instanceof LoggerInterface) {
×
674
                        $logger->warning($message, ['rawPath' => $rawPath]);
×
675
                    } else {
676
                        GeneralUtility::sysLog($message, 'vhs', GeneralUtility::SYSLOG_SEVERITY_WARNING);
×
677
                    }
678
                } else {
679
                    if (!file_exists($temporaryFile)) {
×
680
                        copy($realPath, $temporaryFile);
×
681
                        GeneralUtility::fixPermissions($temporaryFile);
×
682
                    }
683
                    $replacements[$matches[1][$matchCount]] = $wrap[0] . $temporaryFileName . $suffix . $wrap[1];
×
684
                }
685
            }
686
        }
687
        if (!empty($replacements)) {
×
688
            $contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
×
689
        }
690
        return $contents;
×
691
    }
692

693
    /**
694
     * @param AssetInterface|array $asset An Asset ViewHelper instance or an array containing an Asset definition
695
     */
696
    protected function assertAssetAllowedInFooter($asset): bool
697
    {
698
        if ($asset instanceof AssetInterface) {
35✔
699
            return $asset->assertAllowedInFooter();
35✔
700
        }
701
        return (boolean) ($asset['movable'] ?? true);
×
702
    }
703

704
    /**
705
     * @param AssetInterface|array $asset An Asset ViewHelper instance or an array containing an Asset definition
706
     */
707
    protected function extractAssetSettings($asset): array
708
    {
709
        if ($asset instanceof AssetInterface) {
35✔
710
            return $asset->getAssetSettings();
35✔
711
        }
712
        return $asset;
×
713
    }
714

715
    /**
716
     * @param AssetInterface|array $asset An Asset ViewHelper instance or an array containing an Asset definition
717
     */
718
    protected function buildAsset($asset): ?string
719
    {
720
        if ($asset instanceof AssetInterface) {
35✔
721
            return $asset->build();
35✔
722
        }
723
        if (!isset($asset['path']) || empty($asset['path'])) {
×
724
            return $asset['content'] ?? null;
×
725
        }
726
        if (isset($asset['external']) && $asset['external']) {
×
727
            $path = $asset['path'];
×
728
        } else {
729
            $path = GeneralUtility::getFileAbsFileName($asset['path']);
×
730
        }
731
        $content = file_get_contents($path);
×
732
        return $content ?: null;
×
733
    }
734

735
    /**
736
     * @param AssetInterface|array $asset
737
     */
738
    protected function extractAssetContent($asset): ?string
739
    {
740
        $assetSettings = $this->extractAssetSettings($asset);
35✔
741
        $fileRelativePathAndFilename = $assetSettings['path'] ?? null;
35✔
742
        if (!empty($fileRelativePathAndFilename)) {
35✔
743
            $isExternal = $assetSettings['external'] ?? false;
×
744
            $isFluidTemplate = $assetSettings['fluid'] ?? false;
×
745
            $absolutePathAndFilename = GeneralUtility::getFileAbsFileName($fileRelativePathAndFilename);
×
746
            if (!$isExternal && !file_exists($absolutePathAndFilename)) {
×
747
                throw new \RuntimeException('Asset "' . $absolutePathAndFilename . '" does not exist.');
×
748
            }
749
            if ($isFluidTemplate) {
×
750
                $content = $this->renderAssetAsFluidTemplate($asset);
×
751
            } else {
752
                $content = $this->buildAsset($asset);
×
753
            }
754
        } else {
755
            $content = $this->buildAsset($asset);
35✔
756
        }
757
        if ($content !== null && 'css' === $assetSettings['type'] && ($assetSettings['rewrite'] ?? false)) {
35✔
758
            $fileRelativePath = dirname($assetSettings['path'] ?? '');
21✔
759
            $content = $this->detectAndCopyFileReferences($content, $fileRelativePath);
21✔
760
        }
761
        return $content;
35✔
762
    }
763

764
    public function clearCacheCommand(array $parameters): void
765
    {
766
        if (static::$cacheCleared) {
×
767
            return;
×
768
        }
769
        if ('all' !== ($parameters['cacheCmd'] ?? '')) {
×
770
            return;
×
771
        }
772
        $assetCacheFiles = glob(GeneralUtility::getFileAbsFileName($this->getTempPath() . 'vhs-assets-*'));
×
773
        if (!$assetCacheFiles) {
×
774
            return;
×
775
        }
776
        foreach ($assetCacheFiles as $assetCacheFile) {
×
777
            if (!@touch($assetCacheFile, 0)) {
×
778
                $content = (string) file_get_contents($assetCacheFile);
×
779
                $temporaryAssetCacheFile = (string) GeneralUtility::tempnam(basename($assetCacheFile) . '.');
×
780
                $this->writeFile($temporaryAssetCacheFile, $content);
×
781
                rename($temporaryAssetCacheFile, $assetCacheFile);
×
782
                touch($assetCacheFile, 0);
×
783
            }
784
        }
785
        static::$cacheCleared = true;
×
786
    }
787

788
    protected function writeFile(string $file, string $contents): void
789
    {
790
        ///** @var Dispatcher $signalSlotDispatcher */
791
        /*
792
        $signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
793
        $signalSlotDispatcher->dispatch(__CLASS__, static::ASSET_SIGNAL, [&$file, &$contents]);
794
        */
795

796
        $tmpFile = @tempnam(dirname($file), basename($file));
×
797
        if ($tmpFile === false) {
×
798
            $error = error_get_last();
×
799
            $details = $error !== null ? ": {$error['message']}" : ".";
×
800
            throw new \RuntimeException(
×
801
                "Failed to create temporary file for writing asset {$file}{$details}",
×
802
                1733258066
×
803
            );
×
804
        }
805
        GeneralUtility::writeFile($tmpFile, $contents, true);
×
806
        if (@rename($tmpFile, $file) === false) {
×
807
            $error = error_get_last();
×
808
            $details = $error !== null ? ": {$error['message']}" : ".";
×
809
            throw new \RuntimeException(
×
810
                "Failed to move asset-backing file {$file} into final destination{$details}",
×
811
                1733258156
×
812
            );
×
813
        }
814
    }
815

816
    protected function mergeArrays(array $array1, array $array2): array
817
    {
818
        ArrayUtility::mergeRecursiveWithOverrule($array1, $array2);
×
819
        return $array1;
×
820
    }
821

822
    protected function getFileIntegrity(string $file): ?string
823
    {
824
        $typoScript = $this->getTypoScript();
42✔
825
        if (isset($typoScript['assets']['tagsAddSubresourceIntegrity'])) {
42✔
826
            // Note: 3 predefined hashing strategies (the ones suggestes in the rfc sheet)
827
            if (0 < $typoScript['assets']['tagsAddSubresourceIntegrity']
7✔
828
                && $typoScript['assets']['tagsAddSubresourceIntegrity'] < 4
7✔
829
            ) {
830
                if (!file_exists($file)) {
7✔
831
                    return null;
×
832
                }
833

834
                $integrity = null;
7✔
835
                $integrityMethod = ['sha256','sha384','sha512'][
7✔
836
                    $typoScript['assets']['tagsAddSubresourceIntegrity'] - 1
7✔
837
                ];
7✔
838
                $integrityFile = sprintf(
7✔
839
                    $this->getTempPath() . 'vhs-assets-%s.%s',
7✔
840
                    str_replace('vhs-assets-', '', pathinfo($file, PATHINFO_BASENAME)),
7✔
841
                    $integrityMethod
7✔
842
                );
7✔
843

844
                if (!file_exists($integrityFile)
7✔
845
                    || 0 === filemtime($integrityFile)
×
846
                    || isset($GLOBALS['BE_USER'])
×
847
                    || $this->readCacheDisabledInstructionFromContext()
7✔
848
                ) {
849
                    if (extension_loaded('hash') && function_exists('hash_file')) {
7✔
850
                        $integrity = base64_encode((string) hash_file($integrityMethod, $file, true));
7✔
851
                    } elseif (extension_loaded('openssl') && function_exists('openssl_digest')) {
×
852
                        $integrity = base64_encode(
×
853
                            (string) openssl_digest((string) file_get_contents($file), $integrityMethod, true)
×
854
                        );
×
855
                    } else {
856
                        return null; // Sadly, no integrity generation possible
×
857
                    }
858
                    $this->writeFile($integrityFile, $integrity);
7✔
859
                }
860
                return sprintf('%s-%s', $integrityMethod, $integrity ?: (string) file_get_contents($integrityFile));
7✔
861
            }
862
        }
863
        return null;
42✔
864
    }
865

866
    private function getTempPath(): string
867
    {
868
        $publicDirectory = CoreUtility::getSitePath();
42✔
869
        $directory = 'typo3temp/assets/vhs/';
42✔
870
        if (!file_exists($publicDirectory . $directory)) {
42✔
871
            GeneralUtility::mkdir($publicDirectory . $directory);
42✔
872
        }
873
        return $directory;
42✔
874
    }
875

876
    protected function resolveAbsolutePathForFile(string $filename): string
877
    {
878
        return GeneralUtility::getFileAbsFileName($filename);
×
879
    }
880

881
    protected function readPageUidFromContext(): int
882
    {
883
        /** @var ServerRequestInterface $serverRequest */
884
        $serverRequest = $GLOBALS['TYPO3_REQUEST'];
×
885

886
        /** @var RouteResultInterface $pageArguments */
887
        $pageArguments = $serverRequest->getAttribute('routing');
×
888
        if (!$pageArguments instanceof PageArguments) {
×
889
            return 0;
×
890
        }
891
        return $pageArguments->getPageId();
×
892
    }
893

894
    protected function readCacheDisabledInstructionFromContext(): bool
895
    {
896
        $hasDisabledInstructionInRequest = false;
×
897

898
        /** @var ServerRequestInterface $serverRequest */
899
        $serverRequest = $GLOBALS['TYPO3_REQUEST'];
×
900
        $instruction = $serverRequest->getAttribute('frontend.cache.instruction');
×
901
        if ($instruction instanceof CacheInstruction) {
×
902
            $hasDisabledInstructionInRequest = !$instruction->isCachingAllowed();
×
903
        }
904

905
        /** @var TypoScriptFrontendController $typoScriptFrontendController */
906
        $typoScriptFrontendController = $GLOBALS['TSFE'];
×
907

908
        return $hasDisabledInstructionInRequest
×
909
            || (property_exists($typoScriptFrontendController, 'no_cache') && $typoScriptFrontendController->no_cache)
×
910
            || (
×
911
                is_array($typoScriptFrontendController->page)
×
912
                && ($typoScriptFrontendController->page['no_cache'] ?? false)
×
913
            );
×
914
    }
915
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc