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

FluidTYPO3 / vhs / 14244746724

03 Apr 2025 01:52PM UTC coverage: 72.109% (-0.02%) from 72.127%
14244746724

push

github

NamelessCoder
[BUGFIX] Avoid relying on PageArguments in AssetService

The routing request argument isn't necessarily a PageArguments instance.

0 of 2 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

5649 of 7834 relevant lines covered (72.11%)

20.01 hits per line

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

42.74
/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 $settingsCache = null;
53
    protected static array $cachedDependencies = [];
54
    protected static bool $cacheCleared = false;
55

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

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

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

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

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

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

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

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

141
    /**
142
     * Returns the settings used by this particular Asset
143
     * during inclusion. Public access allows later inspection
144
     * of the TypoScript values which were applied to the Asset.
145
     */
146
    public function getSettings(): array
147
    {
148
        if (null === static::$settingsCache) {
×
149
            static::$settingsCache = $this->getTypoScript()['settings'] ?? [];
×
150
        }
151
        $settings = (array) static::$settingsCache;
×
152
        return $settings;
×
153
    }
154

155
    protected function getTypoScript(): array
156
    {
157
        $cache = $this->cacheManager->getCache('vhs_main');
×
158
        $pageUid = $this->readPageUidFromContext();
×
159
        $cacheId = 'vhs_asset_ts_' . $pageUid;
×
160
        $cacheTag = 'pageId_' . $pageUid;
×
161

162
        try {
163
            $allTypoScript = $this->configurationManager->getConfiguration(
×
164
                ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT
×
165
            );
×
166
            $typoScript = GeneralUtility::removeDotsFromTS($allTypoScript['plugin.']['tx_vhs.'] ?? []);
×
167
            $cache->set($cacheId, $typoScript, [$cacheTag]);
×
168
        } catch (\RuntimeException $exception) {
×
169
            if ($exception->getCode() !== 1666513645) {
×
170
                // Re-throw, but only if the exception is not the specific "Setup array has not been initialized" one.
171
                throw $exception;
×
172
            }
173

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

177
            // We will only look in VHS's cache for a TS array if it wasn't already retrieved by ConfigurationManager.
178
            // This is for performance reasons: the TS array may be relatively large and the cache may be DB-based.
179
            // Whereas if the TS is already in ConfigurationManager, it costs nearly nothing to read. The TS is returned
180
            // even if it is empty.
181
            /** @var array|false $fromCache */
182
            $fromCache = $cache->get($cacheId);
×
183
            if (is_array($fromCache)) {
×
184
                return $fromCache;
×
185
            }
186

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

195
        return $typoScript;
×
196
    }
197

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

857
    protected function readPageUidFromContext(): int
858
    {
859
        /** @var ServerRequestInterface $serverRequest */
860
        $serverRequest = $GLOBALS['TYPO3_REQUEST'];
×
861

862
        /** @var RouteResultInterface $pageArguments */
863
        $pageArguments = $serverRequest->getAttribute('routing');
×
NEW
864
        if (!$pageArguments instanceof PageArguments) {
×
NEW
865
            return 0;
×
866
        }
UNCOV
867
        return $pageArguments->getPageId();
×
868
    }
869

870
    protected function readCacheDisabledInstructionFromContext(): bool
871
    {
872
        $hasDisabledInstructionInRequest = false;
×
873

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

881
        /** @var TypoScriptFrontendController $typoScriptFrontendController */
882
        $typoScriptFrontendController = $GLOBALS['TSFE'];
×
883

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