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

FluidTYPO3 / vhs / 14881595750

07 May 2025 10:54AM UTC coverage: 71.993% (-0.1%) from 72.089%
14881595750

Pull #1945

github

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

23 of 59 new or added lines in 1 file covered. (38.98%)

1 existing line in 1 file now uncovered.

5650 of 7848 relevant lines covered (71.99%)

19.99 hits per line

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

42.15
/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 array $pagesWithSavedTypoScript = [];
54
    protected static bool $currentlyBuildingCacheable = true;
55
    protected static array $cachedDependencies = [];
56
    protected static bool $cacheCleared = false;
57

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

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

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

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

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

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

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

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

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

162
    protected function getTypoScript(): array
163
    {
NEW
164
        if (static::$typoScriptCache !== null) {
×
NEW
165
            $this->saveTypoScriptIfNecessary(static::$typoScriptCache);
×
NEW
166
            return static::$typoScriptCache;
×
167
        }
168

169
        try {
170
            $allTypoScript = $this->configurationManager->getConfiguration(
×
171
                ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT
×
172
            );
×
173
            $typoScript = GeneralUtility::removeDotsFromTS($allTypoScript['plugin.']['tx_vhs.'] ?? []);
×
NEW
174
            $this->saveTypoScriptIfNecessary($typoScript);
×
175
        } catch (\RuntimeException $exception) {
×
176
            if ($exception->getCode() !== 1666513645) {
×
177
                // Re-throw, but only if the exception is not the specific "Setup array has not been initialized" one.
178
                throw $exception;
×
179
            }
180

NEW
181
            $pageUid = $this->readPageUidFromContext();
×
NEW
182
            $cache = $this->cacheManager->getCache('vhs_main');
×
NEW
183
            $cacheId = 'vhs_asset_ts_' . $pageUid;
×
NEW
184
            $cacheTag = 'pageId_' . $pageUid;
×
185

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

189
            // We will only look in VHS's cache for a TS array if it wasn't already retrieved by ConfigurationManager.
190
            // This is for performance reasons: the TS array may be relatively large and the cache may be DB-based.
191
            // Whereas if the TS is already in ConfigurationManager, it costs nearly nothing to read. The TS is returned
192
            // even if it is empty.
193
            /** @var array|false $fromCache */
194
            $fromCache = $cache->get($cacheId);
×
195
            if (is_array($fromCache)) {
×
196
                return $fromCache;
×
197
            }
198

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

NEW
207
        static::$typoScriptCache = $typoScript;
×
UNCOV
208
        return $typoScript;
×
209
    }
210

211
    /**
212
     * Save page TypoScript if required to prevent the
213
     * "Setup array has not been initialized" error when serving the next
214
     * request from cache.
215
     */
216
    private function saveTypoScriptIfNecessary(array $typoScript): void
217
    {
218
        // We don't need to save the TypoScript if we're rendering uncached
219
        // content, because the next request won't be answered from cache as no
220
        // cache entry will be created.
NEW
221
        if (!static::$currentlyBuildingCacheable) {
×
NEW
222
            return;
×
223
        }
224

NEW
225
        $pageUid = $this->readPageUidFromContext();
×
226

227
        // We don't need to save the TypoScript if we already did.
NEW
228
        if (in_array($pageUid, static::$pagesWithSavedTypoScript, true)) {
×
NEW
229
            return;
×
230
        }
231

NEW
232
        $cache = $this->cacheManager->getCache('vhs_main');
×
NEW
233
        $cacheId = 'vhs_asset_ts_' . $pageUid;
×
NEW
234
        $cacheTag = 'pageId_' . $pageUid;
×
NEW
235
        $cache->set($cacheId, $typoScript, [$cacheTag]);
×
NEW
236
        static::$pagesWithSavedTypoScript[] = $pageUid;
×
237
    }
238

239
    /**
240
     * @param AssetInterface[]|array[] $assets
241
     */
242
    protected function placeAssetsInHeaderAndFooter(array $assets, bool $cached, ?string &$content): void
243
    {
244
        $settings = $this->getSettings();
35✔
245
        $header = [];
35✔
246
        $footer = [];
35✔
247
        $footerRelocationEnabled = (isset($settings['enableFooterRelocation']) && $settings['relocateToFooter'] > 0)
35✔
248
            || !isset($settings['enableFooterRelocation']);
35✔
249
        foreach ($assets as $name => $asset) {
35✔
250
            if ($asset instanceof AssetInterface) {
35✔
251
                $variables = $asset->getVariables();
35✔
252
            } else {
253
                $variables = (array) ($asset['variables'] ?? []);
×
254
            }
255

256
            if (0 < count($variables)) {
35✔
257
                $name .= '-' . md5(serialize($variables));
×
258
            }
259
            if ($this->assertAssetAllowedInFooter($asset) && $footerRelocationEnabled) {
35✔
260
                $footer[$name] = $asset;
35✔
261
            } else {
262
                $header[$name] = $asset;
21✔
263
            }
264
        }
265
        if (!$cached) {
35✔
266
            $uncachedSuffix = 'Uncached';
×
267
        } else {
268
            $uncachedSuffix = '';
35✔
269
            $dependenciesString = '<!-- VhsAssetsDependenciesLoaded ' . implode(',', array_keys($assets)) . ' -->';
35✔
270
            $this->insertAssetsAtMarker('DependenciesLoaded', $dependenciesString, $content);
35✔
271
        }
272
        $this->insertAssetsAtMarker('Header' . $uncachedSuffix, $header, $content);
35✔
273
        $this->insertAssetsAtMarker('Footer' . $uncachedSuffix, $footer, $content);
35✔
274
        $GLOBALS['VhsAssets'] = [];
35✔
275
    }
276

277
    /**
278
     * @param AssetInterface[]|array[]|string $assets
279
     */
280
    protected function insertAssetsAtMarker(string $markerName, $assets, ?string &$content): void
281
    {
282
        $assetMarker = '<!-- VhsAssets' . $markerName . ' -->';
35✔
283

284
        if (is_array($assets)) {
35✔
285
            $chunk = $this->buildAssetsChunk($assets);
35✔
286
        } else {
287
            $chunk = $assets;
35✔
288
        }
289

290
        if (false === strpos((string) $content, $assetMarker)) {
35✔
291
            $inFooter = false !== strpos($markerName, 'Footer');
35✔
292
            $tag = $inFooter ? '</body>' : '</head>';
35✔
293
            $position = strrpos((string) $content, $tag);
35✔
294

295
            if ($position) {
35✔
296
                $content = substr_replace((string) $content, LF . $chunk, $position, 0);
20✔
297
            }
298
        } else {
299
            $content = str_replace($assetMarker, $assetMarker . LF . $chunk, (string) $content);
×
300
        }
301
    }
302

303
    protected function buildAssetsChunk(array $assets): string
304
    {
305
        $spool = [];
35✔
306
        foreach ($assets as $name => $asset) {
35✔
307
            $assetSettings = $this->extractAssetSettings($asset);
35✔
308
            $type = $assetSettings['type'];
35✔
309
            if (!isset($spool[$type])) {
35✔
310
                $spool[$type] = [];
35✔
311
            }
312
            $spool[$type][$name] = $asset;
35✔
313
        }
314
        $chunks = [];
35✔
315
        /**
316
         * @var string $type
317
         * @var AssetInterface[] $spooledAssets
318
         */
319
        foreach ($spool as $type => $spooledAssets) {
35✔
320
            $chunk = [];
35✔
321
            foreach ($spooledAssets as $name => $asset) {
35✔
322
                $assetSettings = $this->extractAssetSettings($asset);
35✔
323
                $standalone = (boolean) $assetSettings['standalone'];
35✔
324
                $external = (boolean) $assetSettings['external'];
35✔
325
                $rewrite = (boolean) $assetSettings['rewrite'];
35✔
326
                $path = $assetSettings['path'];
35✔
327
                if (!$standalone) {
35✔
328
                    $chunk[$name] = $asset;
35✔
329
                } else {
330
                    if (0 < count($chunk)) {
7✔
331
                        $mergedFileTag = $this->writeCachedMergedFileAndReturnTag($chunk, $type);
7✔
332
                        $chunks[] = $mergedFileTag;
7✔
333
                        $chunk = [];
7✔
334
                    }
335
                    if (empty($path)) {
7✔
336
                        $assetContent = $this->extractAssetContent($asset);
7✔
337
                        $chunks[] = $this->generateTagForAssetType($type, $assetContent, null, null, $assetSettings);
7✔
338
                    } else {
339
                        if ($external) {
×
340
                            $chunks[] = $this->generateTagForAssetType($type, null, $path, null, $assetSettings);
×
341
                        } else {
342
                            if ($rewrite) {
×
343
                                $chunks[] = $this->writeCachedMergedFileAndReturnTag([$name => $asset], $type);
×
344
                            } else {
345
                                $chunks[] = $this->generateTagForAssetType(
×
346
                                    $type,
×
347
                                    null,
×
348
                                    $path,
×
349
                                    $this->getFileIntegrity($path),
×
350
                                    $assetSettings
×
351
                                );
×
352
                            }
353
                        }
354
                    }
355
                }
356
            }
357
            if (0 < count($chunk)) {
35✔
358
                $mergedFileTag = $this->writeCachedMergedFileAndReturnTag($chunk, $type);
35✔
359
                $chunks[] = $mergedFileTag;
35✔
360
            }
361
        }
362
        return implode(LF, $chunks);
35✔
363
    }
364

365
    protected function writeCachedMergedFileAndReturnTag(array $assets, string $type): ?string
366
    {
367
        $source = '';
35✔
368
        $keys = array_keys($assets);
35✔
369
        sort($keys);
35✔
370
        $assetName = implode('-', $keys);
35✔
371
        unset($keys);
35✔
372
        $typoScript = $this->getTypoScript();
35✔
373
        if (isset($typoScript['assets']['mergedAssetsUseHashedFilename'])) {
35✔
374
            if ($typoScript['assets']['mergedAssetsUseHashedFilename']) {
×
375
                $assetName = md5($assetName);
×
376
            }
377
        }
378
        $fileRelativePathAndFilename = $this->getTempPath() . 'vhs-assets-' . $assetName . '.' . $type;
35✔
379
        $fileAbsolutePathAndFilename = $this->resolveAbsolutePathForFile($fileRelativePathAndFilename);
35✔
380
        if (!file_exists($fileAbsolutePathAndFilename)
35✔
381
            || 0 === filemtime($fileAbsolutePathAndFilename)
×
382
            || isset($GLOBALS['BE_USER'])
×
383
            || $this->readCacheDisabledInstructionFromContext()
35✔
384
        ) {
385
            foreach ($assets as $name => $asset) {
35✔
386
                $assetSettings = $this->extractAssetSettings($asset);
35✔
387
                if ((isset($assetSettings['namedChunks']) && 0 < $assetSettings['namedChunks']) ||
35✔
388
                    !isset($assetSettings['namedChunks'])) {
35✔
389
                    $source .= '/* ' . $name . ' */' . LF;
×
390
                }
391
                $source .= $this->extractAssetContent($asset) . LF;
35✔
392
                // Put a return carriage between assets preventing broken content.
393
                $source .= "\n";
35✔
394
            }
395
            $this->writeFile($fileAbsolutePathAndFilename, $source);
35✔
396
        }
397
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'])) {
35✔
398
            $timestampMode = $GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'];
×
399
            if (file_exists($fileRelativePathAndFilename)) {
×
400
                $lastModificationTime = filemtime($fileRelativePathAndFilename);
×
401
                if ('querystring' === $timestampMode) {
×
402
                    $fileRelativePathAndFilename .= '?' . $lastModificationTime;
×
403
                } elseif ('embed' === $timestampMode) {
×
404
                    $fileRelativePathAndFilename = substr_replace(
×
405
                        $fileRelativePathAndFilename,
×
406
                        '.' . $lastModificationTime,
×
407
                        (int) strrpos($fileRelativePathAndFilename, '.'),
×
408
                        0
×
409
                    );
×
410
                }
411
            }
412
        }
413
        $fileRelativePathAndFilename = $this->prefixPath($fileRelativePathAndFilename);
35✔
414
        $integrity = $this->getFileIntegrity($fileAbsolutePathAndFilename);
35✔
415

416
        $assetSettings = null;
35✔
417
        if (count($assets) === 1) {
35✔
418
            $extractedAssetSettings = $this->extractAssetSettings($assets[array_keys($assets)[0]]);
35✔
419
            if ($extractedAssetSettings['standalone']) {
35✔
420
                $assetSettings = $extractedAssetSettings;
×
421
            }
422
        }
423

424
        return $this->generateTagForAssetType($type, null, $fileRelativePathAndFilename, $integrity, $assetSettings);
35✔
425
    }
426

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

508
    /**
509
     * @param AssetInterface[] $assets
510
     * @return AssetInterface[]
511
     */
512
    protected function manipulateAssetsByTypoScriptSettings(array $assets): array
513
    {
514
        $settings = $this->getSettings();
35✔
515
        if (!(isset($settings['asset']) || isset($settings['assetGroup']))) {
35✔
516
            return $assets;
35✔
517
        }
518
        $filtered = [];
×
519
        foreach ($assets as $name => $asset) {
×
520
            $assetSettings = $this->extractAssetSettings($asset);
×
521
            $groupName = $assetSettings['group'];
×
522
            $removed = $assetSettings['removed'] ?? false;
×
523
            if ($removed) {
×
524
                continue;
×
525
            }
526
            $localSettings = $assetSettings;
×
527
            if (isset($settings['asset'])) {
×
528
                $localSettings = $this->mergeArrays($localSettings, (array) $settings['asset']);
×
529
            }
530
            if (isset($settings['asset'][$name])) {
×
531
                $localSettings = $this->mergeArrays($localSettings, (array) $settings['asset'][$name]);
×
532
            }
533
            if (isset($settings['assetGroup'][$groupName])) {
×
534
                $localSettings = $this->mergeArrays($localSettings, (array) $settings['assetGroup'][$groupName]);
×
535
            }
536
            if ($asset instanceof AssetInterface) {
×
537
                if (method_exists($asset, 'setSettings')) {
×
538
                    $asset->setSettings($localSettings);
×
539
                }
540
                $filtered[$name] = $asset;
×
541
            } else {
542
                $filtered[$name] = Asset::createFromSettings($assetSettings);
×
543
            }
544
        }
545
        return $filtered;
×
546
    }
547

548
    /**
549
     * @param AssetInterface[] $assets
550
     * @return AssetInterface[]
551
     */
552
    protected function sortAssetsByDependency(array $assets): array
553
    {
554
        $placed = [];
35✔
555
        $assetNames = (0 < count($assets)) ? array_combine(array_keys($assets), array_keys($assets)) : [];
35✔
556
        while ($asset = array_shift($assets)) {
35✔
557
            $postpone = false;
35✔
558
            /** @var AssetInterface $asset */
559
            $assetSettings = $this->extractAssetSettings($asset);
35✔
560
            $name = array_shift($assetNames);
35✔
561
            $dependencies = $assetSettings['dependencies'];
35✔
562
            if (!is_array($dependencies)) {
35✔
563
                $dependencies = GeneralUtility::trimExplode(',', $assetSettings['dependencies'] ?? '', true);
×
564
            }
565
            foreach ($dependencies as $dependency) {
35✔
566
                if (array_key_exists($dependency, $assets)
×
567
                    && !isset($placed[$dependency])
×
568
                    && !in_array($dependency, static::$cachedDependencies)
×
569
                ) {
570
                    // shove the Asset back to the end of the queue, the dependency has
571
                    // not yet been encountered and moving this item to the back of the
572
                    // queue ensures it will be encountered before re-encountering this
573
                    // specific Asset
574
                    if (0 === count($assets)) {
×
575
                        throw new \RuntimeException(
×
576
                            sprintf(
×
577
                                'Asset "%s" depends on "%s" but "%s" was not found',
×
578
                                $name,
×
579
                                $dependency,
×
580
                                $dependency
×
581
                            ),
×
582
                            1358603979
×
583
                        );
×
584
                    }
585
                    $assets[$name] = $asset;
×
586
                    $assetNames[$name] = $name;
×
587
                    $postpone = true;
×
588
                }
589
            }
590
            if (!$postpone) {
35✔
591
                $placed[$name] = $asset;
35✔
592
            }
593
        }
594
        return $placed;
35✔
595
    }
596

597
    /**
598
     * @param AssetInterface|array $asset
599
     */
600
    protected function renderAssetAsFluidTemplate($asset): string
601
    {
602
        $settings = $this->extractAssetSettings($asset);
×
603
        if (isset($settings['variables']) && is_array($settings['variables'])) {
×
604
            $variables =  $settings['variables'];
×
605
        } else {
606
            $variables = [];
×
607
        }
608
        $contents = $this->buildAsset($asset);
×
609
        if ($contents === null) {
×
610
            return '';
×
611
        }
612
        $variables = GeneralUtility::removeDotsFromTS($variables);
×
613
        /** @var StandaloneView $view */
614
        $view = GeneralUtility::makeInstance(StandaloneView::class);
×
615
        $view->setTemplateSource($contents);
×
616
        $view->assignMultiple($variables);
×
617
        $content = $view->render();
×
618
        return is_string($content) ? $content : '';
×
619
    }
620

621
    /**
622
     * Prefix a path according to "absRefPrefix" TS configuration.
623
     */
624
    protected function prefixPath(string $fileRelativePathAndFilename): string
625
    {
626
        $settings = $this->getSettings();
35✔
627
        $prefixPath = $settings['prependPath'] ?? '';
35✔
628
        if (!empty($prefixPath)) {
35✔
629
            $fileRelativePathAndFilename = $prefixPath . $fileRelativePathAndFilename;
×
630
        }
631
        return $fileRelativePathAndFilename;
35✔
632
    }
633

634
    /**
635
     * Fixes the relative paths inside of url() references in CSS files
636
     */
637
    protected function detectAndCopyFileReferences(string $contents, string $originalDirectory): string
638
    {
639
        if (false !== stripos($contents, 'url')) {
21✔
640
            $regex = '/url(\\(\\s*["\']?(?!\\/)([^"\']+)["\']?\\s*\\))/iU';
×
641
            $contents = $this->copyReferencedFilesAndReplacePaths($contents, $regex, $originalDirectory, '(\'|\')');
×
642
        }
643
        if (false !== stripos($contents, '@import')) {
21✔
644
            $regex = '/@import\\s*(["\']?(?!\\/)([^"\']+)["\']?)/i';
×
645
            $contents = $this->copyReferencedFilesAndReplacePaths($contents, $regex, $originalDirectory, '"|"');
×
646
        }
647
        return $contents;
21✔
648
    }
649

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

710
    /**
711
     * @param AssetInterface|array $asset An Asset ViewHelper instance or an array containing an Asset definition
712
     */
713
    protected function assertAssetAllowedInFooter($asset): bool
714
    {
715
        if ($asset instanceof AssetInterface) {
35✔
716
            return $asset->assertAllowedInFooter();
35✔
717
        }
718
        return (boolean) ($asset['movable'] ?? true);
×
719
    }
720

721
    /**
722
     * @param AssetInterface|array $asset An Asset ViewHelper instance or an array containing an Asset definition
723
     */
724
    protected function extractAssetSettings($asset): array
725
    {
726
        if ($asset instanceof AssetInterface) {
35✔
727
            return $asset->getAssetSettings();
35✔
728
        }
729
        return $asset;
×
730
    }
731

732
    /**
733
     * @param AssetInterface|array $asset An Asset ViewHelper instance or an array containing an Asset definition
734
     */
735
    protected function buildAsset($asset): ?string
736
    {
737
        if ($asset instanceof AssetInterface) {
35✔
738
            return $asset->build();
35✔
739
        }
740
        if (!isset($asset['path']) || empty($asset['path'])) {
×
741
            return $asset['content'] ?? null;
×
742
        }
743
        if (isset($asset['external']) && $asset['external']) {
×
744
            $path = $asset['path'];
×
745
        } else {
746
            $path = GeneralUtility::getFileAbsFileName($asset['path']);
×
747
        }
748
        $content = file_get_contents($path);
×
749
        return $content ?: null;
×
750
    }
751

752
    /**
753
     * @param AssetInterface|array $asset
754
     */
755
    protected function extractAssetContent($asset): ?string
756
    {
757
        $assetSettings = $this->extractAssetSettings($asset);
35✔
758
        $fileRelativePathAndFilename = $assetSettings['path'] ?? null;
35✔
759
        if (!empty($fileRelativePathAndFilename)) {
35✔
760
            $isExternal = $assetSettings['external'] ?? false;
×
761
            $isFluidTemplate = $assetSettings['fluid'] ?? false;
×
762
            $absolutePathAndFilename = GeneralUtility::getFileAbsFileName($fileRelativePathAndFilename);
×
763
            if (!$isExternal && !file_exists($absolutePathAndFilename)) {
×
764
                throw new \RuntimeException('Asset "' . $absolutePathAndFilename . '" does not exist.');
×
765
            }
766
            if ($isFluidTemplate) {
×
767
                $content = $this->renderAssetAsFluidTemplate($asset);
×
768
            } else {
769
                $content = $this->buildAsset($asset);
×
770
            }
771
        } else {
772
            $content = $this->buildAsset($asset);
35✔
773
        }
774
        if ($content !== null && 'css' === $assetSettings['type'] && ($assetSettings['rewrite'] ?? false)) {
35✔
775
            $fileRelativePath = dirname($assetSettings['path'] ?? '');
21✔
776
            $content = $this->detectAndCopyFileReferences($content, $fileRelativePath);
21✔
777
        }
778
        return $content;
35✔
779
    }
780

781
    public function clearCacheCommand(array $parameters): void
782
    {
783
        if (static::$cacheCleared) {
×
784
            return;
×
785
        }
786
        if ('all' !== ($parameters['cacheCmd'] ?? '')) {
×
787
            return;
×
788
        }
789
        $assetCacheFiles = glob(GeneralUtility::getFileAbsFileName($this->getTempPath() . 'vhs-assets-*'));
×
790
        if (!$assetCacheFiles) {
×
791
            return;
×
792
        }
793
        foreach ($assetCacheFiles as $assetCacheFile) {
×
794
            if (!@touch($assetCacheFile, 0)) {
×
795
                $content = (string) file_get_contents($assetCacheFile);
×
796
                $temporaryAssetCacheFile = (string) GeneralUtility::tempnam(basename($assetCacheFile) . '.');
×
797
                $this->writeFile($temporaryAssetCacheFile, $content);
×
798
                rename($temporaryAssetCacheFile, $assetCacheFile);
×
799
                touch($assetCacheFile, 0);
×
800
            }
801
        }
802
        static::$cacheCleared = true;
×
803
    }
804

805
    protected function writeFile(string $file, string $contents): void
806
    {
807
        ///** @var Dispatcher $signalSlotDispatcher */
808
        /*
809
        $signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
810
        $signalSlotDispatcher->dispatch(__CLASS__, static::ASSET_SIGNAL, [&$file, &$contents]);
811
        */
812

813
        $tmpFile = @tempnam(dirname($file), basename($file));
×
814
        if ($tmpFile === false) {
×
815
            $error = error_get_last();
×
816
            $details = $error !== null ? ": {$error['message']}" : ".";
×
817
            throw new \RuntimeException(
×
818
                "Failed to create temporary file for writing asset {$file}{$details}",
×
819
                1733258066
×
820
            );
×
821
        }
822
        GeneralUtility::writeFile($tmpFile, $contents, true);
×
823
        if (@rename($tmpFile, $file) === false) {
×
824
            $error = error_get_last();
×
825
            $details = $error !== null ? ": {$error['message']}" : ".";
×
826
            throw new \RuntimeException(
×
827
                "Failed to move asset-backing file {$file} into final destination{$details}",
×
828
                1733258156
×
829
            );
×
830
        }
831
    }
832

833
    protected function mergeArrays(array $array1, array $array2): array
834
    {
835
        ArrayUtility::mergeRecursiveWithOverrule($array1, $array2);
×
836
        return $array1;
×
837
    }
838

839
    protected function getFileIntegrity(string $file): ?string
840
    {
841
        $typoScript = $this->getTypoScript();
42✔
842
        if (isset($typoScript['assets']['tagsAddSubresourceIntegrity'])) {
42✔
843
            // Note: 3 predefined hashing strategies (the ones suggestes in the rfc sheet)
844
            if (0 < $typoScript['assets']['tagsAddSubresourceIntegrity']
7✔
845
                && $typoScript['assets']['tagsAddSubresourceIntegrity'] < 4
7✔
846
            ) {
847
                if (!file_exists($file)) {
7✔
848
                    return null;
×
849
                }
850

851
                $integrity = null;
7✔
852
                $integrityMethod = ['sha256','sha384','sha512'][
7✔
853
                    $typoScript['assets']['tagsAddSubresourceIntegrity'] - 1
7✔
854
                ];
7✔
855
                $integrityFile = sprintf(
7✔
856
                    $this->getTempPath() . 'vhs-assets-%s.%s',
7✔
857
                    str_replace('vhs-assets-', '', pathinfo($file, PATHINFO_BASENAME)),
7✔
858
                    $integrityMethod
7✔
859
                );
7✔
860

861
                if (!file_exists($integrityFile)
7✔
862
                    || 0 === filemtime($integrityFile)
×
863
                    || isset($GLOBALS['BE_USER'])
×
864
                    || $this->readCacheDisabledInstructionFromContext()
7✔
865
                ) {
866
                    if (extension_loaded('hash') && function_exists('hash_file')) {
7✔
867
                        $integrity = base64_encode((string) hash_file($integrityMethod, $file, true));
7✔
868
                    } elseif (extension_loaded('openssl') && function_exists('openssl_digest')) {
×
869
                        $integrity = base64_encode(
×
870
                            (string) openssl_digest((string) file_get_contents($file), $integrityMethod, true)
×
871
                        );
×
872
                    } else {
873
                        return null; // Sadly, no integrity generation possible
×
874
                    }
875
                    $this->writeFile($integrityFile, $integrity);
7✔
876
                }
877
                return sprintf('%s-%s', $integrityMethod, $integrity ?: (string) file_get_contents($integrityFile));
7✔
878
            }
879
        }
880
        return null;
42✔
881
    }
882

883
    private function getTempPath(): string
884
    {
885
        $publicDirectory = CoreUtility::getSitePath();
42✔
886
        $directory = 'typo3temp/assets/vhs/';
42✔
887
        if (!file_exists($publicDirectory . $directory)) {
42✔
888
            GeneralUtility::mkdir($publicDirectory . $directory);
42✔
889
        }
890
        return $directory;
42✔
891
    }
892

893
    protected function resolveAbsolutePathForFile(string $filename): string
894
    {
895
        return GeneralUtility::getFileAbsFileName($filename);
×
896
    }
897

898
    protected function readPageUidFromContext(): int
899
    {
900
        /** @var ServerRequestInterface $serverRequest */
901
        $serverRequest = $GLOBALS['TYPO3_REQUEST'];
×
902

903
        /** @var RouteResultInterface $pageArguments */
904
        $pageArguments = $serverRequest->getAttribute('routing');
×
905
        if (!$pageArguments instanceof PageArguments) {
×
906
            return 0;
×
907
        }
908
        return $pageArguments->getPageId();
×
909
    }
910

911
    protected function readCacheDisabledInstructionFromContext(): bool
912
    {
913
        $hasDisabledInstructionInRequest = false;
×
914

915
        /** @var ServerRequestInterface $serverRequest */
916
        $serverRequest = $GLOBALS['TYPO3_REQUEST'];
×
917
        $instruction = $serverRequest->getAttribute('frontend.cache.instruction');
×
918
        if ($instruction instanceof CacheInstruction) {
×
919
            $hasDisabledInstructionInRequest = !$instruction->isCachingAllowed();
×
920
        }
921

922
        /** @var TypoScriptFrontendController $typoScriptFrontendController */
923
        $typoScriptFrontendController = $GLOBALS['TSFE'];
×
924

925
        return $hasDisabledInstructionInRequest
×
926
            || (property_exists($typoScriptFrontendController, 'no_cache') && $typoScriptFrontendController->no_cache)
×
927
            || (
×
928
                is_array($typoScriptFrontendController->page)
×
929
                && ($typoScriptFrontendController->page['no_cache'] ?? false)
×
930
            );
×
931
    }
932
}
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