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

FluidTYPO3 / vhs / 13566190336

27 Feb 2025 12:18PM UTC coverage: 72.127% (-0.6%) from 72.746%
13566190336

push

github

NamelessCoder
[TER] 7.1.0

5649 of 7832 relevant lines covered (72.13%)

20.01 hits per line

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

42.92
/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\SingletonInterface;
20
use TYPO3\CMS\Core\Utility\ArrayUtility;
21
use TYPO3\CMS\Core\Utility\GeneralUtility;
22
use TYPO3\CMS\Core\Utility\PathUtility;
23
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
24
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
25
use TYPO3\CMS\Fluid\View\StandaloneView;
26
use TYPO3\CMS\Frontend\Cache\CacheInstruction;
27
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
28
use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder;
29

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

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

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

50
    protected static bool $typoScriptAssetsBuilt = false;
51
    protected static ?array $settingsCache = null;
52
    protected static array $cachedDependencies = [];
53
    protected static bool $cacheCleared = false;
54

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

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

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

71
    public function buildAll(array $parameters, object $caller, bool $cached = true, ?string &$content = null): void
72
    {
73
        if ($content === null) {
49✔
74
            $content = &$caller->content;
49✔
75
        }
76

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

120
    public function buildAllUncached(array $parameters, object $caller, ?string &$content = null): void
121
    {
122
        if ($content === null) {
7✔
123
            $content = &$caller->content;
7✔
124
        }
125
        $matches = [];
7✔
126
        preg_match_all('/\<\![\-]+\ VhsAssetsDependenciesLoaded ([^ ]+) [\-]+\>/i', $content, $matches);
7✔
127
        foreach ($matches[1] as $key => $match) {
7✔
128
            $extractedDependencies = explode(',', $matches[1][$key]);
×
129
            static::$cachedDependencies = array_merge(static::$cachedDependencies, $extractedDependencies);
×
130
        }
131

132
        $this->buildAll($parameters, $caller, false, $content);
7✔
133
    }
134

135
    public function isAlreadyDefined(string $assetName): bool
136
    {
137
        return isset($GLOBALS['VhsAssets'][$assetName]) || in_array($assetName, self::$cachedDependencies, true);
×
138
    }
139

140
    /**
141
     * Returns the settings used by this particular Asset
142
     * during inclusion. Public access allows later inspection
143
     * of the TypoScript values which were applied to the Asset.
144
     */
145
    public function getSettings(): array
146
    {
147
        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 {
162
            $allTypoScript = $this->configurationManager->getConfiguration(
×
163
                ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT
×
164
            );
×
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);
×
192
        }
193

194
        return $typoScript;
×
195
    }
196

197
    /**
198
     * @param AssetInterface[]|array[] $assets
199
     */
200
    protected function placeAssetsInHeaderAndFooter(array $assets, bool $cached, ?string &$content): void
201
    {
202
        $settings = $this->getSettings();
35✔
203
        $header = [];
35✔
204
        $footer = [];
35✔
205
        $footerRelocationEnabled = (isset($settings['enableFooterRelocation']) && $settings['relocateToFooter'] > 0)
35✔
206
            || !isset($settings['enableFooterRelocation']);
35✔
207
        foreach ($assets as $name => $asset) {
35✔
208
            if ($asset instanceof AssetInterface) {
35✔
209
                $variables = $asset->getVariables();
35✔
210
            } else {
211
                $variables = (array) ($asset['variables'] ?? []);
×
212
            }
213

214
            if (0 < count($variables)) {
35✔
215
                $name .= '-' . md5(serialize($variables));
×
216
            }
217
            if ($this->assertAssetAllowedInFooter($asset) && $footerRelocationEnabled) {
35✔
218
                $footer[$name] = $asset;
35✔
219
            } else {
220
                $header[$name] = $asset;
21✔
221
            }
222
        }
223
        if (!$cached) {
35✔
224
            $uncachedSuffix = 'Uncached';
×
225
        } else {
226
            $uncachedSuffix = '';
35✔
227
            $dependenciesString = '<!-- VhsAssetsDependenciesLoaded ' . implode(',', array_keys($assets)) . ' -->';
35✔
228
            $this->insertAssetsAtMarker('DependenciesLoaded', $dependenciesString, $content);
35✔
229
        }
230
        $this->insertAssetsAtMarker('Header' . $uncachedSuffix, $header, $content);
35✔
231
        $this->insertAssetsAtMarker('Footer' . $uncachedSuffix, $footer, $content);
35✔
232
        $GLOBALS['VhsAssets'] = [];
35✔
233
    }
234

235
    /**
236
     * @param AssetInterface[]|array[]|string $assets
237
     */
238
    protected function insertAssetsAtMarker(string $markerName, $assets, ?string &$content): void
239
    {
240
        $assetMarker = '<!-- VhsAssets' . $markerName . ' -->';
35✔
241

242
        if (is_array($assets)) {
35✔
243
            $chunk = $this->buildAssetsChunk($assets);
35✔
244
        } else {
245
            $chunk = $assets;
35✔
246
        }
247

248
        if (false === strpos((string) $content, $assetMarker)) {
35✔
249
            $inFooter = false !== strpos($markerName, 'Footer');
35✔
250
            $tag = $inFooter ? '</body>' : '</head>';
35✔
251
            $position = strrpos((string) $content, $tag);
35✔
252

253
            if ($position) {
35✔
254
                $content = substr_replace((string) $content, LF . $chunk, $position, 0);
20✔
255
            }
256
        } else {
257
            $content = str_replace($assetMarker, $assetMarker . LF . $chunk, (string) $content);
×
258
        }
259
    }
260

261
    protected function buildAssetsChunk(array $assets): string
262
    {
263
        $spool = [];
35✔
264
        foreach ($assets as $name => $asset) {
35✔
265
            $assetSettings = $this->extractAssetSettings($asset);
35✔
266
            $type = $assetSettings['type'];
35✔
267
            if (!isset($spool[$type])) {
35✔
268
                $spool[$type] = [];
35✔
269
            }
270
            $spool[$type][$name] = $asset;
35✔
271
        }
272
        $chunks = [];
35✔
273
        /**
274
         * @var string $type
275
         * @var AssetInterface[] $spooledAssets
276
         */
277
        foreach ($spool as $type => $spooledAssets) {
35✔
278
            $chunk = [];
35✔
279
            foreach ($spooledAssets as $name => $asset) {
35✔
280
                $assetSettings = $this->extractAssetSettings($asset);
35✔
281
                $standalone = (boolean) $assetSettings['standalone'];
35✔
282
                $external = (boolean) $assetSettings['external'];
35✔
283
                $rewrite = (boolean) $assetSettings['rewrite'];
35✔
284
                $path = $assetSettings['path'];
35✔
285
                if (!$standalone) {
35✔
286
                    $chunk[$name] = $asset;
35✔
287
                } else {
288
                    if (0 < count($chunk)) {
7✔
289
                        $mergedFileTag = $this->writeCachedMergedFileAndReturnTag($chunk, $type);
7✔
290
                        $chunks[] = $mergedFileTag;
7✔
291
                        $chunk = [];
7✔
292
                    }
293
                    if (empty($path)) {
7✔
294
                        $assetContent = $this->extractAssetContent($asset);
7✔
295
                        $chunks[] = $this->generateTagForAssetType($type, $assetContent, null, null, $assetSettings);
7✔
296
                    } else {
297
                        if ($external) {
×
298
                            $chunks[] = $this->generateTagForAssetType($type, null, $path, null, $assetSettings);
×
299
                        } else {
300
                            if ($rewrite) {
×
301
                                $chunks[] = $this->writeCachedMergedFileAndReturnTag([$name => $asset], $type);
×
302
                            } else {
303
                                $chunks[] = $this->generateTagForAssetType(
×
304
                                    $type,
×
305
                                    null,
×
306
                                    $path,
×
307
                                    $this->getFileIntegrity($path),
×
308
                                    $assetSettings
×
309
                                );
×
310
                            }
311
                        }
312
                    }
313
                }
314
            }
315
            if (0 < count($chunk)) {
35✔
316
                $mergedFileTag = $this->writeCachedMergedFileAndReturnTag($chunk, $type);
35✔
317
                $chunks[] = $mergedFileTag;
35✔
318
            }
319
        }
320
        return implode(LF, $chunks);
35✔
321
    }
322

323
    protected function writeCachedMergedFileAndReturnTag(array $assets, string $type): ?string
324
    {
325
        $source = '';
35✔
326
        $keys = array_keys($assets);
35✔
327
        sort($keys);
35✔
328
        $assetName = implode('-', $keys);
35✔
329
        unset($keys);
35✔
330
        $typoScript = $this->getTypoScript();
35✔
331
        if (isset($typoScript['assets']['mergedAssetsUseHashedFilename'])) {
35✔
332
            if ($typoScript['assets']['mergedAssetsUseHashedFilename']) {
×
333
                $assetName = md5($assetName);
×
334
            }
335
        }
336
        $fileRelativePathAndFilename = $this->getTempPath() . 'vhs-assets-' . $assetName . '.' . $type;
35✔
337
        $fileAbsolutePathAndFilename = $this->resolveAbsolutePathForFile($fileRelativePathAndFilename);
35✔
338
        if (!file_exists($fileAbsolutePathAndFilename)
35✔
339
            || 0 === filemtime($fileAbsolutePathAndFilename)
×
340
            || isset($GLOBALS['BE_USER'])
×
341
            || $this->readCacheDisabledInstructionFromContext()
35✔
342
        ) {
343
            foreach ($assets as $name => $asset) {
35✔
344
                $assetSettings = $this->extractAssetSettings($asset);
35✔
345
                if ((isset($assetSettings['namedChunks']) && 0 < $assetSettings['namedChunks']) ||
35✔
346
                    !isset($assetSettings['namedChunks'])) {
35✔
347
                    $source .= '/* ' . $name . ' */' . LF;
×
348
                }
349
                $source .= $this->extractAssetContent($asset) . LF;
35✔
350
                // Put a return carriage between assets preventing broken content.
351
                $source .= "\n";
35✔
352
            }
353
            $this->writeFile($fileAbsolutePathAndFilename, $source);
35✔
354
        }
355
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'])) {
35✔
356
            $timestampMode = $GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'];
×
357
            if (file_exists($fileRelativePathAndFilename)) {
×
358
                $lastModificationTime = filemtime($fileRelativePathAndFilename);
×
359
                if ('querystring' === $timestampMode) {
×
360
                    $fileRelativePathAndFilename .= '?' . $lastModificationTime;
×
361
                } elseif ('embed' === $timestampMode) {
×
362
                    $fileRelativePathAndFilename = substr_replace(
×
363
                        $fileRelativePathAndFilename,
×
364
                        '.' . $lastModificationTime,
×
365
                        (int) strrpos($fileRelativePathAndFilename, '.'),
×
366
                        0
×
367
                    );
×
368
                }
369
            }
370
        }
371
        $fileRelativePathAndFilename = $this->prefixPath($fileRelativePathAndFilename);
35✔
372
        $integrity = $this->getFileIntegrity($fileAbsolutePathAndFilename);
35✔
373

374
        $assetSettings = null;
35✔
375
        if (count($assets) === 1) {
35✔
376
            $extractedAssetSettings = $this->extractAssetSettings($assets[array_keys($assets)[0]]);
35✔
377
            if ($extractedAssetSettings['standalone']) {
35✔
378
                $assetSettings = $extractedAssetSettings;
×
379
            }
380
        }
381

382
        return $this->generateTagForAssetType($type, null, $fileRelativePathAndFilename, $integrity, $assetSettings);
35✔
383
    }
384

385
    protected function generateTagForAssetType(
386
        string $type,
387
        ?string $content,
388
        ?string $file = null,
389
        ?string $integrity = null,
390
        ?array $standaloneAssetSettings = null
391
    ): ?string {
392
        /** @var TagBuilder $tagBuilder */
393
        $tagBuilder = GeneralUtility::makeInstance(TagBuilder::class);
35✔
394
        if (null === $file && empty($content)) {
35✔
395
            $content = '<!-- Empty tag content -->';
×
396
        }
397
        if (empty($type) && !empty($file)) {
35✔
398
            $type = pathinfo($file, PATHINFO_EXTENSION);
×
399
        }
400
        if ($file !== null) {
35✔
401
            $file = PathUtility::getAbsoluteWebPath($file);
35✔
402
            $file = $this->prefixPath($file);
35✔
403
        }
404
        $settings = $this->getTypoScript();
35✔
405
        switch ($type) {
406
            case 'js':
35✔
407
                $tagBuilder->setTagName('script');
35✔
408
                $tagBuilder->forceClosingTag(true);
35✔
409
                $tagBuilder->addAttribute('type', 'text/javascript');
35✔
410
                if (null === $file) {
35✔
411
                    $tagBuilder->setContent((string) $content);
×
412
                } else {
413
                    $tagBuilder->addAttribute('src', (string) $file);
35✔
414
                }
415
                if (!empty($integrity)) {
35✔
416
                    if (!empty($settings['prependPath'])) {
×
417
                        $tagBuilder->addAttribute('crossorigin', 'anonymous');
×
418
                    }
419
                    $tagBuilder->addAttribute('integrity', $integrity);
×
420
                }
421
                if ($standaloneAssetSettings) {
35✔
422
                    // using async and defer simultaneously does not make sense technically, but do not enforce
423
                    if ($standaloneAssetSettings['async']) {
×
424
                        $tagBuilder->addAttribute('async', 'async');
×
425
                    }
426
                    if ($standaloneAssetSettings['defer']) {
×
427
                        $tagBuilder->addAttribute('defer', 'defer');
×
428
                    }
429
                }
430
                break;
35✔
431
            case 'css':
21✔
432
                if (null === $file) {
21✔
433
                    $tagBuilder->setTagName('style');
7✔
434
                    $tagBuilder->forceClosingTag(true);
7✔
435
                    $tagBuilder->addAttribute('type', 'text/css');
7✔
436
                    $tagBuilder->setContent((string) $content);
7✔
437
                } else {
438
                    $tagBuilder->forceClosingTag(false);
21✔
439
                    $tagBuilder->setTagName('link');
21✔
440
                    $tagBuilder->addAttribute('rel', 'stylesheet');
21✔
441
                    $tagBuilder->addAttribute('href', $file);
21✔
442
                }
443
                if (!empty($integrity)) {
21✔
444
                    if (!empty($settings['prependPath'])) {
×
445
                        $tagBuilder->addAttribute('crossorigin', 'anonymous');
×
446
                    }
447
                    $tagBuilder->addAttribute('integrity', $integrity);
×
448
                }
449
                break;
21✔
450
            case 'meta':
×
451
                $tagBuilder->forceClosingTag(false);
×
452
                $tagBuilder->setTagName('meta');
×
453
                break;
×
454
            default:
455
                if (null === $file) {
×
456
                    return $content;
×
457
                }
458
                throw new \RuntimeException(
×
459
                    'Attempt to include file based asset with unknown type ("' . $type . '")',
×
460
                    1358645219
×
461
                );
×
462
        }
463
        return $tagBuilder->render();
35✔
464
    }
465

466
    /**
467
     * @param AssetInterface[] $assets
468
     * @return AssetInterface[]
469
     */
470
    protected function manipulateAssetsByTypoScriptSettings(array $assets): array
471
    {
472
        $settings = $this->getSettings();
35✔
473
        if (!(isset($settings['asset']) || isset($settings['assetGroup']))) {
35✔
474
            return $assets;
35✔
475
        }
476
        $filtered = [];
×
477
        foreach ($assets as $name => $asset) {
×
478
            $assetSettings = $this->extractAssetSettings($asset);
×
479
            $groupName = $assetSettings['group'];
×
480
            $removed = $assetSettings['removed'] ?? false;
×
481
            if ($removed) {
×
482
                continue;
×
483
            }
484
            $localSettings = $assetSettings;
×
485
            if (isset($settings['asset'])) {
×
486
                $localSettings = $this->mergeArrays($localSettings, (array) $settings['asset']);
×
487
            }
488
            if (isset($settings['asset'][$name])) {
×
489
                $localSettings = $this->mergeArrays($localSettings, (array) $settings['asset'][$name]);
×
490
            }
491
            if (isset($settings['assetGroup'][$groupName])) {
×
492
                $localSettings = $this->mergeArrays($localSettings, (array) $settings['assetGroup'][$groupName]);
×
493
            }
494
            if ($asset instanceof AssetInterface) {
×
495
                if (method_exists($asset, 'setSettings')) {
×
496
                    $asset->setSettings($localSettings);
×
497
                }
498
                $filtered[$name] = $asset;
×
499
            } else {
500
                $filtered[$name] = Asset::createFromSettings($assetSettings);
×
501
            }
502
        }
503
        return $filtered;
×
504
    }
505

506
    /**
507
     * @param AssetInterface[] $assets
508
     * @return AssetInterface[]
509
     */
510
    protected function sortAssetsByDependency(array $assets): array
511
    {
512
        $placed = [];
35✔
513
        $assetNames = (0 < count($assets)) ? array_combine(array_keys($assets), array_keys($assets)) : [];
35✔
514
        while ($asset = array_shift($assets)) {
35✔
515
            $postpone = false;
35✔
516
            /** @var AssetInterface $asset */
517
            $assetSettings = $this->extractAssetSettings($asset);
35✔
518
            $name = array_shift($assetNames);
35✔
519
            $dependencies = $assetSettings['dependencies'];
35✔
520
            if (!is_array($dependencies)) {
35✔
521
                $dependencies = GeneralUtility::trimExplode(',', $assetSettings['dependencies'] ?? '', true);
×
522
            }
523
            foreach ($dependencies as $dependency) {
35✔
524
                if (array_key_exists($dependency, $assets)
×
525
                    && !isset($placed[$dependency])
×
526
                    && !in_array($dependency, static::$cachedDependencies)
×
527
                ) {
528
                    // shove the Asset back to the end of the queue, the dependency has
529
                    // not yet been encountered and moving this item to the back of the
530
                    // queue ensures it will be encountered before re-encountering this
531
                    // specific Asset
532
                    if (0 === count($assets)) {
×
533
                        throw new \RuntimeException(
×
534
                            sprintf(
×
535
                                'Asset "%s" depends on "%s" but "%s" was not found',
×
536
                                $name,
×
537
                                $dependency,
×
538
                                $dependency
×
539
                            ),
×
540
                            1358603979
×
541
                        );
×
542
                    }
543
                    $assets[$name] = $asset;
×
544
                    $assetNames[$name] = $name;
×
545
                    $postpone = true;
×
546
                }
547
            }
548
            if (!$postpone) {
35✔
549
                $placed[$name] = $asset;
35✔
550
            }
551
        }
552
        return $placed;
35✔
553
    }
554

555
    /**
556
     * @param AssetInterface|array $asset
557
     */
558
    protected function renderAssetAsFluidTemplate($asset): string
559
    {
560
        $settings = $this->extractAssetSettings($asset);
×
561
        if (isset($settings['variables']) && is_array($settings['variables'])) {
×
562
            $variables =  $settings['variables'];
×
563
        } else {
564
            $variables = [];
×
565
        }
566
        $contents = $this->buildAsset($asset);
×
567
        if ($contents === null) {
×
568
            return '';
×
569
        }
570
        $variables = GeneralUtility::removeDotsFromTS($variables);
×
571
        /** @var StandaloneView $view */
572
        $view = GeneralUtility::makeInstance(StandaloneView::class);
×
573
        $view->setTemplateSource($contents);
×
574
        $view->assignMultiple($variables);
×
575
        $content = $view->render();
×
576
        return is_string($content) ? $content : '';
×
577
    }
578

579
    /**
580
     * Prefix a path according to "absRefPrefix" TS configuration.
581
     */
582
    protected function prefixPath(string $fileRelativePathAndFilename): string
583
    {
584
        $settings = $this->getSettings();
35✔
585
        $prefixPath = $settings['prependPath'] ?? '';
35✔
586
        if (!empty($prefixPath)) {
35✔
587
            $fileRelativePathAndFilename = $prefixPath . $fileRelativePathAndFilename;
×
588
        }
589
        return $fileRelativePathAndFilename;
35✔
590
    }
591

592
    /**
593
     * Fixes the relative paths inside of url() references in CSS files
594
     */
595
    protected function detectAndCopyFileReferences(string $contents, string $originalDirectory): string
596
    {
597
        if (false !== stripos($contents, 'url')) {
21✔
598
            $regex = '/url(\\(\\s*["\']?(?!\\/)([^"\']+)["\']?\\s*\\))/iU';
×
599
            $contents = $this->copyReferencedFilesAndReplacePaths($contents, $regex, $originalDirectory, '(\'|\')');
×
600
        }
601
        if (false !== stripos($contents, '@import')) {
21✔
602
            $regex = '/@import\\s*(["\']?(?!\\/)([^"\']+)["\']?)/i';
×
603
            $contents = $this->copyReferencedFilesAndReplacePaths($contents, $regex, $originalDirectory, '"|"');
×
604
        }
605
        return $contents;
21✔
606
    }
607

608
    /**
609
     * Finds and replaces all URLs by using a given regex
610
     */
611
    protected function copyReferencedFilesAndReplacePaths(
612
        string $contents,
613
        string $regex,
614
        string $originalDirectory,
615
        string $wrap = '|'
616
    ): string {
617
        $matches = [];
×
618
        $replacements = [];
×
619
        $wrap = explode('|', $wrap);
×
620
        preg_match_all($regex, $contents, $matches);
×
621
        $logger = null;
×
622
        if (class_exists(LogManager::class)) {
×
623
            /** @var LogManager $logManager */
624
            $logManager = GeneralUtility::makeInstance(LogManager::class);
×
625
            $logger = $logManager->getLogger(__CLASS__);
×
626
        }
627
        foreach ($matches[2] as $matchCount => $match) {
×
628
            $match = trim($match, '\'" ');
×
629
            if (false === strpos($match, ':') && !preg_match('/url\\s*\\(/i', $match)) {
×
630
                $checksum = md5($originalDirectory . $match);
×
631
                if (0 < preg_match('/([^\?#]+)(.+)?/', $match, $items)) {
×
632
                    $path = $items[1] ?? '';
×
633
                    $suffix = $items[2] ?? '';
×
634
                } else {
635
                    $path = $match;
×
636
                    $suffix = '';
×
637
                }
638
                $newPath = basename($path);
×
639
                $extension = pathinfo($newPath, PATHINFO_EXTENSION);
×
640
                $temporaryFileName = 'vhs-assets-css-' . $checksum . '.' . $extension;
×
641
                $temporaryFile = CoreUtility::getSitePath() . $this->getTempPath() . $temporaryFileName;
×
642
                $rawPath = GeneralUtility::getFileAbsFileName(
×
643
                    $originalDirectory . (empty($originalDirectory) ? '' : '/')
×
644
                ) . $path;
×
645
                $realPath = realpath($rawPath);
×
646
                if (false === $realPath) {
×
647
                    $message = 'Asset at path "' . $rawPath . '" not found. Processing skipped.';
×
648
                    if ($logger instanceof LoggerInterface) {
×
649
                        $logger->warning($message, ['rawPath' => $rawPath]);
×
650
                    } else {
651
                        GeneralUtility::sysLog($message, 'vhs', GeneralUtility::SYSLOG_SEVERITY_WARNING);
×
652
                    }
653
                } else {
654
                    if (!file_exists($temporaryFile)) {
×
655
                        copy($realPath, $temporaryFile);
×
656
                        GeneralUtility::fixPermissions($temporaryFile);
×
657
                    }
658
                    $replacements[$matches[1][$matchCount]] = $wrap[0] . $temporaryFileName . $suffix . $wrap[1];
×
659
                }
660
            }
661
        }
662
        if (!empty($replacements)) {
×
663
            $contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
×
664
        }
665
        return $contents;
×
666
    }
667

668
    /**
669
     * @param AssetInterface|array $asset An Asset ViewHelper instance or an array containing an Asset definition
670
     */
671
    protected function assertAssetAllowedInFooter($asset): bool
672
    {
673
        if ($asset instanceof AssetInterface) {
35✔
674
            return $asset->assertAllowedInFooter();
35✔
675
        }
676
        return (boolean) ($asset['movable'] ?? true);
×
677
    }
678

679
    /**
680
     * @param AssetInterface|array $asset An Asset ViewHelper instance or an array containing an Asset definition
681
     */
682
    protected function extractAssetSettings($asset): array
683
    {
684
        if ($asset instanceof AssetInterface) {
35✔
685
            return $asset->getAssetSettings();
35✔
686
        }
687
        return $asset;
×
688
    }
689

690
    /**
691
     * @param AssetInterface|array $asset An Asset ViewHelper instance or an array containing an Asset definition
692
     */
693
    protected function buildAsset($asset): ?string
694
    {
695
        if ($asset instanceof AssetInterface) {
35✔
696
            return $asset->build();
35✔
697
        }
698
        if (!isset($asset['path']) || empty($asset['path'])) {
×
699
            return $asset['content'] ?? null;
×
700
        }
701
        if (isset($asset['external']) && $asset['external']) {
×
702
            $path = $asset['path'];
×
703
        } else {
704
            $path = GeneralUtility::getFileAbsFileName($asset['path']);
×
705
        }
706
        $content = file_get_contents($path);
×
707
        return $content ?: null;
×
708
    }
709

710
    /**
711
     * @param AssetInterface|array $asset
712
     */
713
    protected function extractAssetContent($asset): ?string
714
    {
715
        $assetSettings = $this->extractAssetSettings($asset);
35✔
716
        $fileRelativePathAndFilename = $assetSettings['path'] ?? null;
35✔
717
        if (!empty($fileRelativePathAndFilename)) {
35✔
718
            $isExternal = $assetSettings['external'] ?? false;
×
719
            $isFluidTemplate = $assetSettings['fluid'] ?? false;
×
720
            $absolutePathAndFilename = GeneralUtility::getFileAbsFileName($fileRelativePathAndFilename);
×
721
            if (!$isExternal && !file_exists($absolutePathAndFilename)) {
×
722
                throw new \RuntimeException('Asset "' . $absolutePathAndFilename . '" does not exist.');
×
723
            }
724
            if ($isFluidTemplate) {
×
725
                $content = $this->renderAssetAsFluidTemplate($asset);
×
726
            } else {
727
                $content = $this->buildAsset($asset);
×
728
            }
729
        } else {
730
            $content = $this->buildAsset($asset);
35✔
731
        }
732
        if ($content !== null && 'css' === $assetSettings['type'] && ($assetSettings['rewrite'] ?? false)) {
35✔
733
            $fileRelativePath = dirname($assetSettings['path'] ?? '');
21✔
734
            $content = $this->detectAndCopyFileReferences($content, $fileRelativePath);
21✔
735
        }
736
        return $content;
35✔
737
    }
738

739
    public function clearCacheCommand(array $parameters): void
740
    {
741
        if (static::$cacheCleared) {
×
742
            return;
×
743
        }
744
        if ('all' !== ($parameters['cacheCmd'] ?? '')) {
×
745
            return;
×
746
        }
747
        $assetCacheFiles = glob(GeneralUtility::getFileAbsFileName($this->getTempPath() . 'vhs-assets-*'));
×
748
        if (!$assetCacheFiles) {
×
749
            return;
×
750
        }
751
        foreach ($assetCacheFiles as $assetCacheFile) {
×
752
            if (!@touch($assetCacheFile, 0)) {
×
753
                $content = (string) file_get_contents($assetCacheFile);
×
754
                $temporaryAssetCacheFile = (string) tempnam(dirname($assetCacheFile), basename($assetCacheFile) . '.');
×
755
                $this->writeFile($temporaryAssetCacheFile, $content);
×
756
                rename($temporaryAssetCacheFile, $assetCacheFile);
×
757
                touch($assetCacheFile, 0);
×
758
            }
759
        }
760
        static::$cacheCleared = true;
×
761
    }
762

763
    protected function writeFile(string $file, string $contents): void
764
    {
765
        ///** @var Dispatcher $signalSlotDispatcher */
766
        /*
767
        $signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
768
        $signalSlotDispatcher->dispatch(__CLASS__, static::ASSET_SIGNAL, [&$file, &$contents]);
769
        */
770

771
        $tmpFile = @tempnam(dirname($file), basename($file));
×
772
        if ($tmpFile === false) {
×
773
            $error = error_get_last();
×
774
            $details = $error !== null ? ": {$error['message']}" : ".";
×
775
            throw new \RuntimeException(
×
776
                "Failed to create temporary file for writing asset {$file}{$details}",
×
777
                1733258066
×
778
            );
×
779
        }
780
        GeneralUtility::writeFile($tmpFile, $contents, true);
×
781
        if (@rename($tmpFile, $file) === false) {
×
782
            $error = error_get_last();
×
783
            $details = $error !== null ? ": {$error['message']}" : ".";
×
784
            throw new \RuntimeException(
×
785
                "Failed to move asset-backing file {$file} into final destination{$details}",
×
786
                1733258156
×
787
            );
×
788
        }
789
    }
790

791
    protected function mergeArrays(array $array1, array $array2): array
792
    {
793
        ArrayUtility::mergeRecursiveWithOverrule($array1, $array2);
×
794
        return $array1;
×
795
    }
796

797
    protected function getFileIntegrity(string $file): ?string
798
    {
799
        $typoScript = $this->getTypoScript();
42✔
800
        if (isset($typoScript['assets']['tagsAddSubresourceIntegrity'])) {
42✔
801
            // Note: 3 predefined hashing strategies (the ones suggestes in the rfc sheet)
802
            if (0 < $typoScript['assets']['tagsAddSubresourceIntegrity']
7✔
803
                && $typoScript['assets']['tagsAddSubresourceIntegrity'] < 4
7✔
804
            ) {
805
                if (!file_exists($file)) {
7✔
806
                    return null;
×
807
                }
808

809
                $integrity = null;
7✔
810
                $integrityMethod = ['sha256','sha384','sha512'][
7✔
811
                    $typoScript['assets']['tagsAddSubresourceIntegrity'] - 1
7✔
812
                ];
7✔
813
                $integrityFile = sprintf(
7✔
814
                    $this->getTempPath() . 'vhs-assets-%s.%s',
7✔
815
                    str_replace('vhs-assets-', '', pathinfo($file, PATHINFO_BASENAME)),
7✔
816
                    $integrityMethod
7✔
817
                );
7✔
818

819
                if (!file_exists($integrityFile)
7✔
820
                    || 0 === filemtime($integrityFile)
×
821
                    || isset($GLOBALS['BE_USER'])
×
822
                    || $this->readCacheDisabledInstructionFromContext()
7✔
823
                ) {
824
                    if (extension_loaded('hash') && function_exists('hash_file')) {
7✔
825
                        $integrity = base64_encode((string) hash_file($integrityMethod, $file, true));
7✔
826
                    } elseif (extension_loaded('openssl') && function_exists('openssl_digest')) {
×
827
                        $integrity = base64_encode(
×
828
                            (string) openssl_digest((string) file_get_contents($file), $integrityMethod, true)
×
829
                        );
×
830
                    } else {
831
                        return null; // Sadly, no integrity generation possible
×
832
                    }
833
                    $this->writeFile($integrityFile, $integrity);
7✔
834
                }
835
                return sprintf('%s-%s', $integrityMethod, $integrity ?: (string) file_get_contents($integrityFile));
7✔
836
            }
837
        }
838
        return null;
42✔
839
    }
840

841
    private function getTempPath(): string
842
    {
843
        $publicDirectory = CoreUtility::getSitePath();
42✔
844
        $directory = 'typo3temp/assets/vhs/';
42✔
845
        if (!file_exists($publicDirectory . $directory)) {
42✔
846
            GeneralUtility::mkdir($publicDirectory . $directory);
42✔
847
        }
848
        return $directory;
42✔
849
    }
850

851
    protected function resolveAbsolutePathForFile(string $filename): string
852
    {
853
        return GeneralUtility::getFileAbsFileName($filename);
×
854
    }
855

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

866
    protected function readCacheDisabledInstructionFromContext(): bool
867
    {
868
        $hasDisabledInstructionInRequest = false;
×
869

870
        /** @var ServerRequestInterface $serverRequest */
871
        $serverRequest = $GLOBALS['TYPO3_REQUEST'];
×
872
        $instruction = $serverRequest->getAttribute('frontend.cache.instruction');
×
873
        if ($instruction instanceof CacheInstruction) {
×
874
            $hasDisabledInstructionInRequest = !$instruction->isCachingAllowed();
×
875
        }
876

877
        /** @var TypoScriptFrontendController $typoScriptFrontendController */
878
        $typoScriptFrontendController = $GLOBALS['TSFE'];
×
879

880
        return $hasDisabledInstructionInRequest
×
881
            || (property_exists($typoScriptFrontendController, 'no_cache') && $typoScriptFrontendController->no_cache)
×
882
            || (
×
883
                is_array($typoScriptFrontendController->page)
×
884
                && ($typoScriptFrontendController->page['no_cache'] ?? false)
×
885
            );
×
886
    }
887
}
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