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

FluidTYPO3 / vhs / 12943389201

04 Dec 2024 09:54AM UTC coverage: 72.525%. First build
12943389201

push

github

NamelessCoder
[BUGFIX] Fix corrupted JavaScript assets loaded by website users

Website users loaded corrupted JavaScript assets for files generated by
EXT:vhs when EXT:vhs was in the middle of re-writing the contents of the
requested asset file. This happens because EXT:core
`GeneralUtility::writeFile()` isn't implemented in an atomic style
causing observers (such as visiting website users) to read incomplete
asset file contents. Incomplete JavaScript files likely cause syntax
errors when interpreted by the user's user-agent rendering front end
sites unable to reach JavaScript-based interactivity.

Fix the issue by (non-atomically) writing into a temporary file and
replacing the destination asset file with a single atomic `rename()`
operation instead. This ensures that observers of the destination asset
file either see the complete old contents or the complete new contents
but never any intermediate state of the asset content.

See: https://man7.org/linux/man-pages/man2/rename.2.html
See: https://github.com/TYPO3/typo3/blob/v10.4.37/typo3/sysext/core/Classes/Utility/GeneralUtility.php#L1835-L1861

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

5538 of 7636 relevant lines covered (72.52%)

13.45 hits per line

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

45.06
/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\Log\LoggerInterface;
15
use TYPO3\CMS\Core\Log\LogManager;
16
use TYPO3\CMS\Core\SingletonInterface;
17
use TYPO3\CMS\Core\Utility\ArrayUtility;
18
use TYPO3\CMS\Core\Utility\GeneralUtility;
19
use TYPO3\CMS\Core\Utility\PathUtility;
20
use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
21
use TYPO3\CMS\Extbase\Utility\DebuggerUtility;
22
use TYPO3\CMS\Fluid\View\StandaloneView;
23
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
24
use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder;
25

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

36
    /**
37
     * @var ConfigurationManagerInterface
38
     */
39
    protected $configurationManager;
40

41
    protected static bool $typoScriptAssetsBuilt = false;
42
    protected static ?array $settingsCache = null;
43
    protected static array $cachedDependencies = [];
44
    protected static bool $cacheCleared = false;
45

46
    public function injectConfigurationManager(ConfigurationManagerInterface $configurationManager): void
47
    {
48
        $this->configurationManager = $configurationManager;
×
49
    }
50

51
    public function usePageCache(object $caller, bool $shouldUsePageCache): bool
52
    {
53
        $this->buildAll([], $caller);
×
54
        return $shouldUsePageCache;
×
55
    }
56

57
    public function buildAll(array $parameters, object $caller, bool $cached = true, ?string &$content = null): void
58
    {
59
        if ($content === null) {
42✔
60
            $content = &$caller->content;
42✔
61
        }
62

63
        $settings = $this->getSettings();
42✔
64
        $buildTypoScriptAssets = (!static::$typoScriptAssetsBuilt && ($cached || $GLOBALS['TSFE']->no_cache));
42✔
65
        if ($buildTypoScriptAssets && isset($settings['asset']) && is_array($settings['asset'])) {
42✔
66
            foreach ($settings['asset'] as $name => $typoScriptAsset) {
×
67
                if (!isset($GLOBALS['VhsAssets'][$name]) && is_array($typoScriptAsset)) {
×
68
                    if (!isset($typoScriptAsset['name'])) {
×
69
                        $typoScriptAsset['name'] = $name;
×
70
                    }
71
                    if (isset($typoScriptAsset['dependencies']) && !is_array($typoScriptAsset['dependencies'])) {
×
72
                        $typoScriptAsset['dependencies'] = GeneralUtility::trimExplode(
×
73
                            ',',
×
74
                            (string) $typoScriptAsset['dependencies'],
×
75
                            true
×
76
                        );
×
77
                    }
78
                    Asset::createFromSettings($typoScriptAsset);
×
79
                }
80
            }
81
            static::$typoScriptAssetsBuilt = true;
×
82
        }
83
        if (empty($GLOBALS['VhsAssets']) || !is_array($GLOBALS['VhsAssets'])) {
42✔
84
            return;
12✔
85
        }
86
        $assets = $GLOBALS['VhsAssets'];
30✔
87
        $assets = $this->sortAssetsByDependency($assets);
30✔
88
        $assets = $this->manipulateAssetsByTypoScriptSettings($assets);
30✔
89
        $buildDebugRequested = (isset($settings['asset']['debugBuild']) && $settings['asset']['debugBuild'] > 0);
30✔
90
        $assetDebugRequested = (isset($settings['asset']['debug']) && $settings['asset']['debug'] > 0);
30✔
91
        $useDebugUtility = (isset($settings['asset']['useDebugUtility']) && $settings['asset']['useDebugUtility'] > 0)
30✔
92
            || !isset($settings['asset']['useDebugUtility']);
30✔
93
        if ($buildDebugRequested || $assetDebugRequested) {
30✔
94
            if ($useDebugUtility) {
×
95
                DebuggerUtility::var_dump($assets);
×
96
            } else {
97
                echo var_export($assets, true);
×
98
            }
99
        }
100
        $this->placeAssetsInHeaderAndFooter($assets, $cached, $content);
30✔
101
    }
102

103
    public function buildAllUncached(array $parameters, object $caller, ?string &$content = null): void
104
    {
105
        if ($content === null) {
6✔
106
            $content = &$caller->content;
6✔
107
        }
108
        $matches = [];
6✔
109
        preg_match_all('/\<\![\-]+\ VhsAssetsDependenciesLoaded ([^ ]+) [\-]+\>/i', $content, $matches);
6✔
110
        foreach ($matches[1] as $key => $match) {
6✔
111
            $extractedDependencies = explode(',', $matches[1][$key]);
×
112
            static::$cachedDependencies = array_merge(static::$cachedDependencies, $extractedDependencies);
×
113
        }
114

115
        $this->buildAll($parameters, $caller, false, $content);
6✔
116
    }
117

118
    public function isAlreadyDefined(string $assetName): bool
119
    {
120
        return isset($GLOBALS['VhsAssets'][$assetName]) || in_array($assetName, self::$cachedDependencies, true);
×
121
    }
122

123
    /**
124
     * Returns the settings used by this particular Asset
125
     * during inclusion. Public access allows later inspection
126
     * of the TypoScript values which were applied to the Asset.
127
     */
128
    public function getSettings(): array
129
    {
130
        if (null === static::$settingsCache) {
×
131
            $allTypoScript = $this->configurationManager->getConfiguration(
×
132
                ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT
×
133
            );
×
134
            static::$settingsCache = GeneralUtility::removeDotsFromTS(
×
135
                $allTypoScript['plugin.']['tx_vhs.']['settings.'] ?? []
×
136
            );
×
137
        }
138
        $settings = (array) static::$settingsCache;
×
139
        return $settings;
×
140
    }
141

142
    /**
143
     * @param AssetInterface[]|array[] $assets
144
     */
145
    protected function placeAssetsInHeaderAndFooter(array $assets, bool $cached, ?string &$content): void
146
    {
147
        $settings = $this->getSettings();
30✔
148
        $header = [];
30✔
149
        $footer = [];
30✔
150
        $footerRelocationEnabled = (isset($settings['enableFooterRelocation']) && $settings['relocateToFooter'] > 0)
30✔
151
            || !isset($settings['enableFooterRelocation']);
30✔
152
        foreach ($assets as $name => $asset) {
30✔
153
            if ($asset instanceof AssetInterface) {
30✔
154
                $variables = $asset->getVariables();
30✔
155
            } else {
156
                $variables = (array) ($asset['variables'] ?? []);
×
157
            }
158

159
            if (0 < count($variables)) {
30✔
160
                $name .= '-' . md5(serialize($variables));
×
161
            }
162
            if ($this->assertAssetAllowedInFooter($asset) && $footerRelocationEnabled) {
30✔
163
                $footer[$name] = $asset;
30✔
164
            } else {
165
                $header[$name] = $asset;
18✔
166
            }
167
        }
168
        if (!$cached) {
30✔
169
            $uncachedSuffix = 'Uncached';
×
170
        } else {
171
            $uncachedSuffix = '';
30✔
172
            $dependenciesString = '<!-- VhsAssetsDependenciesLoaded ' . implode(',', array_keys($assets)) . ' -->';
30✔
173
            $this->insertAssetsAtMarker('DependenciesLoaded', $dependenciesString, $content);
30✔
174
        }
175
        $this->insertAssetsAtMarker('Header' . $uncachedSuffix, $header, $content);
30✔
176
        $this->insertAssetsAtMarker('Footer' . $uncachedSuffix, $footer, $content);
30✔
177
        $GLOBALS['VhsAssets'] = [];
30✔
178
    }
179

180
    /**
181
     * @param AssetInterface[]|array[]|string $assets
182
     */
183
    protected function insertAssetsAtMarker(string $markerName, $assets, ?string &$content): void
184
    {
185
        $assetMarker = '<!-- VhsAssets' . $markerName . ' -->';
30✔
186

187
        if (is_array($assets)) {
30✔
188
            $chunk = $this->buildAssetsChunk($assets);
30✔
189
        } else {
190
            $chunk = $assets;
30✔
191
        }
192

193
        if (false === strpos((string) $content, $assetMarker)) {
30✔
194
            $inFooter = false !== strpos($markerName, 'Footer');
30✔
195
            $tag = $inFooter ? '</body>' : '</head>';
30✔
196
            $position = strrpos((string) $content, $tag);
30✔
197

198
            if ($position) {
30✔
199
                $content = substr_replace((string) $content, LF . $chunk, $position, 0);
20✔
200
            }
201
        } else {
202
            $content = str_replace($assetMarker, $assetMarker . LF . $chunk, (string) $content);
×
203
        }
204
    }
205

206
    protected function buildAssetsChunk(array $assets): string
207
    {
208
        $spool = [];
30✔
209
        foreach ($assets as $name => $asset) {
30✔
210
            $assetSettings = $this->extractAssetSettings($asset);
30✔
211
            $type = $assetSettings['type'];
30✔
212
            if (!isset($spool[$type])) {
30✔
213
                $spool[$type] = [];
30✔
214
            }
215
            $spool[$type][$name] = $asset;
30✔
216
        }
217
        $chunks = [];
30✔
218
        /**
219
         * @var string $type
220
         * @var AssetInterface[] $spooledAssets
221
         */
222
        foreach ($spool as $type => $spooledAssets) {
30✔
223
            $chunk = [];
30✔
224
            foreach ($spooledAssets as $name => $asset) {
30✔
225
                $assetSettings = $this->extractAssetSettings($asset);
30✔
226
                $standalone = (boolean) $assetSettings['standalone'];
30✔
227
                $external = (boolean) $assetSettings['external'];
30✔
228
                $rewrite = (boolean) $assetSettings['rewrite'];
30✔
229
                $path = $assetSettings['path'];
30✔
230
                if (!$standalone) {
30✔
231
                    $chunk[$name] = $asset;
30✔
232
                } else {
233
                    if (0 < count($chunk)) {
6✔
234
                        $mergedFileTag = $this->writeCachedMergedFileAndReturnTag($chunk, $type);
6✔
235
                        $chunks[] = $mergedFileTag;
6✔
236
                        $chunk = [];
6✔
237
                    }
238
                    if (empty($path)) {
6✔
239
                        $assetContent = $this->extractAssetContent($asset);
6✔
240
                        $chunks[] = $this->generateTagForAssetType($type, $assetContent, null, null, $assetSettings);
6✔
241
                    } else {
242
                        if ($external) {
×
243
                            $chunks[] = $this->generateTagForAssetType($type, null, $path, null, $assetSettings);
×
244
                        } else {
245
                            if ($rewrite) {
×
246
                                $chunks[] = $this->writeCachedMergedFileAndReturnTag([$name => $asset], $type);
×
247
                            } else {
248
                                $chunks[] = $this->generateTagForAssetType(
×
249
                                    $type,
×
250
                                    null,
×
251
                                    $path,
×
252
                                    $this->getFileIntegrity($path),
×
253
                                    $assetSettings
×
254
                                );
×
255
                            }
256
                        }
257
                    }
258
                }
259
            }
260
            if (0 < count($chunk)) {
30✔
261
                $mergedFileTag = $this->writeCachedMergedFileAndReturnTag($chunk, $type);
30✔
262
                $chunks[] = $mergedFileTag;
30✔
263
            }
264
        }
265
        return implode(LF, $chunks);
30✔
266
    }
267

268
    protected function writeCachedMergedFileAndReturnTag(array $assets, string $type): ?string
269
    {
270
        $source = '';
30✔
271
        $keys = array_keys($assets);
30✔
272
        sort($keys);
30✔
273
        $assetName = implode('-', $keys);
30✔
274
        unset($keys);
30✔
275
        if (isset($GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_vhs.']['assets.']['mergedAssetsUseHashedFilename'])) {
30✔
276
            if ($GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_vhs.']['assets.']['mergedAssetsUseHashedFilename']) {
×
277
                $assetName = md5($assetName);
×
278
            }
279
        }
280
        $fileRelativePathAndFilename = $this->getTempPath() . 'vhs-assets-' . $assetName . '.' . $type;
30✔
281
        $fileAbsolutePathAndFilename = $this->resolveAbsolutePathForFile($fileRelativePathAndFilename);
30✔
282
        if (!file_exists($fileAbsolutePathAndFilename)
30✔
283
            || 0 === filemtime($fileAbsolutePathAndFilename)
×
284
            || isset($GLOBALS['BE_USER'])
×
285
            || ($GLOBALS['TSFE']->no_cache ?? false)
×
286
            || ($GLOBALS['TSFE']->page['no_cache'] ?? false)
30✔
287
        ) {
288
            foreach ($assets as $name => $asset) {
30✔
289
                $assetSettings = $this->extractAssetSettings($asset);
30✔
290
                if ((isset($assetSettings['namedChunks']) && 0 < $assetSettings['namedChunks']) ||
30✔
291
                    !isset($assetSettings['namedChunks'])) {
30✔
292
                    $source .= '/* ' . $name . ' */' . LF;
×
293
                }
294
                $source .= $this->extractAssetContent($asset) . LF;
30✔
295
                // Put a return carriage between assets preventing broken content.
296
                $source .= "\n";
30✔
297
            }
298
            $this->writeFile($fileAbsolutePathAndFilename, $source);
30✔
299
        }
300
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'])) {
30✔
301
            $timestampMode = $GLOBALS['TYPO3_CONF_VARS']['FE']['versionNumberInFilename'];
×
302
            if (file_exists($fileRelativePathAndFilename)) {
×
303
                $lastModificationTime = filemtime($fileRelativePathAndFilename);
×
304
                if ('querystring' === $timestampMode) {
×
305
                    $fileRelativePathAndFilename .= '?' . $lastModificationTime;
×
306
                } elseif ('embed' === $timestampMode) {
×
307
                    $fileRelativePathAndFilename = substr_replace(
×
308
                        $fileRelativePathAndFilename,
×
309
                        '.' . $lastModificationTime,
×
310
                        (int) strrpos($fileRelativePathAndFilename, '.'),
×
311
                        0
×
312
                    );
×
313
                }
314
            }
315
        }
316
        $fileRelativePathAndFilename = $this->prefixPath($fileRelativePathAndFilename);
30✔
317
        $integrity = $this->getFileIntegrity($fileAbsolutePathAndFilename);
30✔
318

319
        $assetSettings = null;
30✔
320
        if (count($assets) === 1) {
30✔
321
            $extractedAssetSettings = $this->extractAssetSettings($assets[array_keys($assets)[0]]);
30✔
322
            if ($extractedAssetSettings['standalone']) {
30✔
323
                $assetSettings = $extractedAssetSettings;
×
324
            }
325
        }
326

327
        return $this->generateTagForAssetType($type, null, $fileRelativePathAndFilename, $integrity, $assetSettings);
30✔
328
    }
329

330
    protected function generateTagForAssetType(
331
        string $type,
332
        ?string $content,
333
        ?string $file = null,
334
        ?string $integrity = null,
335
        ?array $standaloneAssetSettings = null
336
    ): ?string {
337
        /** @var TagBuilder $tagBuilder */
338
        $tagBuilder = GeneralUtility::makeInstance(TagBuilder::class);
30✔
339
        if (null === $file && empty($content)) {
30✔
340
            $content = '<!-- Empty tag content -->';
×
341
        }
342
        if (empty($type) && !empty($file)) {
30✔
343
            $type = pathinfo($file, PATHINFO_EXTENSION);
×
344
        }
345
        if ($file !== null) {
30✔
346
            $file = PathUtility::getAbsoluteWebPath($file);
30✔
347
            $file = $this->prefixPath($file);
30✔
348
        }
349
        switch ($type) {
350
            case 'js':
30✔
351
                $tagBuilder->setTagName('script');
30✔
352
                $tagBuilder->forceClosingTag(true);
30✔
353
                $tagBuilder->addAttribute('type', 'text/javascript');
30✔
354
                if (null === $file) {
30✔
355
                    $tagBuilder->setContent((string) $content);
×
356
                } else {
357
                    $tagBuilder->addAttribute('src', (string) $file);
30✔
358
                }
359
                if (!empty($integrity)) {
30✔
360
                    if (!empty($GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_vhs.']['settings.']['prependPath'])) {
×
361
                        $tagBuilder->addAttribute('crossorigin', 'anonymous');
×
362
                    }
363
                    $tagBuilder->addAttribute('integrity', $integrity);
×
364
                }
365
                if ($standaloneAssetSettings) {
30✔
366
                    // using async and defer simultaneously does not make sense technically, but do not enforce
367
                    if ($standaloneAssetSettings['async']) {
×
368
                        $tagBuilder->addAttribute('async', 'async');
×
369
                    }
370
                    if ($standaloneAssetSettings['defer']) {
×
371
                        $tagBuilder->addAttribute('defer', 'defer');
×
372
                    }
373
                }
374
                break;
30✔
375
            case 'css':
18✔
376
                if (null === $file) {
18✔
377
                    $tagBuilder->setTagName('style');
6✔
378
                    $tagBuilder->forceClosingTag(true);
6✔
379
                    $tagBuilder->addAttribute('type', 'text/css');
6✔
380
                    $tagBuilder->setContent((string) $content);
6✔
381
                } else {
382
                    $tagBuilder->forceClosingTag(false);
18✔
383
                    $tagBuilder->setTagName('link');
18✔
384
                    $tagBuilder->addAttribute('rel', 'stylesheet');
18✔
385
                    $tagBuilder->addAttribute('href', $file);
18✔
386
                }
387
                if (!empty($integrity)) {
18✔
388
                    if (!empty($GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_vhs.']['settings.']['prependPath'])) {
×
389
                        $tagBuilder->addAttribute('crossorigin', 'anonymous');
×
390
                    }
391
                    $tagBuilder->addAttribute('integrity', $integrity);
×
392
                }
393
                break;
18✔
394
            case 'meta':
×
395
                $tagBuilder->forceClosingTag(false);
×
396
                $tagBuilder->setTagName('meta');
×
397
                break;
×
398
            default:
399
                if (null === $file) {
×
400
                    return $content;
×
401
                }
402
                throw new \RuntimeException(
×
403
                    'Attempt to include file based asset with unknown type ("' . $type . '")',
×
404
                    1358645219
×
405
                );
×
406
        }
407
        return $tagBuilder->render();
30✔
408
    }
409

410
    /**
411
     * @param AssetInterface[] $assets
412
     * @return AssetInterface[]
413
     */
414
    protected function manipulateAssetsByTypoScriptSettings(array $assets): array
415
    {
416
        $settings = $this->getSettings();
30✔
417
        if (!(isset($settings['asset']) || isset($settings['assetGroup']))) {
30✔
418
            return $assets;
30✔
419
        }
420
        $filtered = [];
×
421
        foreach ($assets as $name => $asset) {
×
422
            $assetSettings = $this->extractAssetSettings($asset);
×
423
            $groupName = $assetSettings['group'];
×
424
            $removed = $assetSettings['removed'] ?? false;
×
425
            if ($removed) {
×
426
                continue;
×
427
            }
428
            $localSettings = $assetSettings;
×
429
            if (isset($settings['asset'])) {
×
430
                $localSettings = $this->mergeArrays($localSettings, (array) $settings['asset']);
×
431
            }
432
            if (isset($settings['asset'][$name])) {
×
433
                $localSettings = $this->mergeArrays($localSettings, (array) $settings['asset'][$name]);
×
434
            }
435
            if (isset($settings['assetGroup'][$groupName])) {
×
436
                $localSettings = $this->mergeArrays($localSettings, (array) $settings['assetGroup'][$groupName]);
×
437
            }
438
            if ($asset instanceof AssetInterface) {
×
439
                if (method_exists($asset, 'setSettings')) {
×
440
                    $asset->setSettings($localSettings);
×
441
                }
442
                $filtered[$name] = $asset;
×
443
            } else {
444
                $filtered[$name] = Asset::createFromSettings($assetSettings);
×
445
            }
446
        }
447
        return $filtered;
×
448
    }
449

450
    /**
451
     * @param AssetInterface[] $assets
452
     * @return AssetInterface[]
453
     */
454
    protected function sortAssetsByDependency(array $assets): array
455
    {
456
        $placed = [];
30✔
457
        $assetNames = (0 < count($assets)) ? array_combine(array_keys($assets), array_keys($assets)) : [];
30✔
458
        while ($asset = array_shift($assets)) {
30✔
459
            $postpone = false;
30✔
460
            /** @var AssetInterface $asset */
461
            $assetSettings = $this->extractAssetSettings($asset);
30✔
462
            $name = array_shift($assetNames);
30✔
463
            $dependencies = $assetSettings['dependencies'];
30✔
464
            if (!is_array($dependencies)) {
30✔
465
                $dependencies = GeneralUtility::trimExplode(',', $assetSettings['dependencies'], true);
×
466
            }
467
            foreach ($dependencies as $dependency) {
30✔
468
                if (array_key_exists($dependency, $assets)
×
469
                    && !isset($placed[$dependency])
×
470
                    && !in_array($dependency, static::$cachedDependencies)
×
471
                ) {
472
                    // shove the Asset back to the end of the queue, the dependency has
473
                    // not yet been encountered and moving this item to the back of the
474
                    // queue ensures it will be encountered before re-encountering this
475
                    // specific Asset
476
                    if (0 === count($assets)) {
×
477
                        throw new \RuntimeException(
×
478
                            sprintf(
×
479
                                'Asset "%s" depends on "%s" but "%s" was not found',
×
480
                                $name,
×
481
                                $dependency,
×
482
                                $dependency
×
483
                            ),
×
484
                            1358603979
×
485
                        );
×
486
                    }
487
                    $assets[$name] = $asset;
×
488
                    $assetNames[$name] = $name;
×
489
                    $postpone = true;
×
490
                }
491
            }
492
            if (!$postpone) {
30✔
493
                $placed[$name] = $asset;
30✔
494
            }
495
        }
496
        return $placed;
30✔
497
    }
498

499
    /**
500
     * @param AssetInterface|array $asset
501
     */
502
    protected function renderAssetAsFluidTemplate($asset): string
503
    {
504
        $settings = $this->extractAssetSettings($asset);
×
505
        if (isset($settings['variables']) && is_array($settings['variables'])) {
×
506
            $variables =  $settings['variables'];
×
507
        } else {
508
            $variables = [];
×
509
        }
510
        $contents = $this->buildAsset($asset);
×
511
        if ($contents === null) {
×
512
            return '';
×
513
        }
514
        $variables = GeneralUtility::removeDotsFromTS($variables);
×
515
        /** @var StandaloneView $view */
516
        $view = GeneralUtility::makeInstance(StandaloneView::class);
×
517
        $view->setTemplateSource($contents);
×
518
        $view->assignMultiple($variables);
×
519
        $content = $view->render();
×
520
        return $content;
×
521
    }
522

523
    /**
524
     * Prefix a path according to "absRefPrefix" TS configuration.
525
     */
526
    protected function prefixPath(string $fileRelativePathAndFilename): string
527
    {
528
        $settings = $this->getSettings();
30✔
529
        $prefixPath = $settings['prependPath'] ?? '';
30✔
530
        if (!empty($prefixPath)) {
30✔
531
            $fileRelativePathAndFilename = $prefixPath . $fileRelativePathAndFilename;
×
532
        }
533
        return $fileRelativePathAndFilename;
30✔
534
    }
535

536
    /**
537
     * Fixes the relative paths inside of url() references in CSS files
538
     */
539
    protected function detectAndCopyFileReferences(string $contents, string $originalDirectory): string
540
    {
541
        if (false !== stripos($contents, 'url')) {
18✔
542
            $regex = '/url(\\(\\s*["\']?(?!\\/)([^"\']+)["\']?\\s*\\))/iU';
×
543
            $contents = $this->copyReferencedFilesAndReplacePaths($contents, $regex, $originalDirectory, '(\'|\')');
×
544
        }
545
        if (false !== stripos($contents, '@import')) {
18✔
546
            $regex = '/@import\\s*(["\']?(?!\\/)([^"\']+)["\']?)/i';
×
547
            $contents = $this->copyReferencedFilesAndReplacePaths($contents, $regex, $originalDirectory, '"|"');
×
548
        }
549
        return $contents;
18✔
550
    }
551

552
    /**
553
     * Finds and replaces all URLs by using a given regex
554
     */
555
    protected function copyReferencedFilesAndReplacePaths(
556
        string $contents,
557
        string $regex,
558
        string $originalDirectory,
559
        string $wrap = '|'
560
    ): string {
561
        $matches = [];
×
562
        $replacements = [];
×
563
        $wrap = explode('|', $wrap);
×
564
        preg_match_all($regex, $contents, $matches);
×
565
        $logger = null;
×
566
        if (class_exists(LogManager::class)) {
×
567
            /** @var LogManager $logManager */
568
            $logManager = GeneralUtility::makeInstance(LogManager::class);
×
569
            $logger = $logManager->getLogger(__CLASS__);
×
570
        }
571
        foreach ($matches[2] as $matchCount => $match) {
×
572
            $match = trim($match, '\'" ');
×
573
            if (false === strpos($match, ':') && !preg_match('/url\\s*\\(/i', $match)) {
×
574
                $checksum = md5($originalDirectory . $match);
×
575
                if (0 < preg_match('/([^\?#]+)(.+)?/', $match, $items)) {
×
576
                    $path = $items[1] ?? '';
×
577
                    $suffix = $items[2] ?? '';
×
578
                } else {
579
                    $path = $match;
×
580
                    $suffix = '';
×
581
                }
582
                $newPath = basename($path);
×
583
                $extension = pathinfo($newPath, PATHINFO_EXTENSION);
×
584
                $temporaryFileName = 'vhs-assets-css-' . $checksum . '.' . $extension;
×
585
                $temporaryFile = CoreUtility::getSitePath() . $this->getTempPath() . $temporaryFileName;
×
586
                $rawPath = GeneralUtility::getFileAbsFileName(
×
587
                    $originalDirectory . (empty($originalDirectory) ? '' : '/')
×
588
                ) . $path;
×
589
                $realPath = realpath($rawPath);
×
590
                if (false === $realPath) {
×
591
                    $message = 'Asset at path "' . $rawPath . '" not found. Processing skipped.';
×
592
                    if ($logger instanceof LoggerInterface) {
×
593
                        $logger->warning($message, ['rawPath' => $rawPath]);
×
594
                    } else {
595
                        GeneralUtility::sysLog($message, 'vhs', GeneralUtility::SYSLOG_SEVERITY_WARNING);
×
596
                    }
597
                } else {
598
                    if (!file_exists($temporaryFile)) {
×
599
                        copy($realPath, $temporaryFile);
×
600
                        GeneralUtility::fixPermissions($temporaryFile);
×
601
                    }
602
                    $replacements[$matches[1][$matchCount]] = $wrap[0] . $temporaryFileName . $suffix . $wrap[1];
×
603
                }
604
            }
605
        }
606
        if (!empty($replacements)) {
×
607
            $contents = str_replace(array_keys($replacements), array_values($replacements), $contents);
×
608
        }
609
        return $contents;
×
610
    }
611

612
    /**
613
     * @param AssetInterface|array $asset An Asset ViewHelper instance or an array containing an Asset definition
614
     */
615
    protected function assertAssetAllowedInFooter($asset): bool
616
    {
617
        if ($asset instanceof AssetInterface) {
30✔
618
            return $asset->assertAllowedInFooter();
30✔
619
        }
620
        return (boolean) ($asset['movable'] ?? true);
×
621
    }
622

623
    /**
624
     * @param AssetInterface|array $asset An Asset ViewHelper instance or an array containing an Asset definition
625
     */
626
    protected function extractAssetSettings($asset): array
627
    {
628
        if ($asset instanceof AssetInterface) {
30✔
629
            return $asset->getAssetSettings();
30✔
630
        }
631
        return $asset;
×
632
    }
633

634
    /**
635
     * @param AssetInterface|array $asset An Asset ViewHelper instance or an array containing an Asset definition
636
     */
637
    protected function buildAsset($asset): ?string
638
    {
639
        if ($asset instanceof AssetInterface) {
30✔
640
            return $asset->build();
30✔
641
        }
642
        if (!isset($asset['path']) || empty($asset['path'])) {
×
643
            return $asset['content'] ?? null;
×
644
        }
645
        if (isset($asset['external']) && $asset['external']) {
×
646
            $path = $asset['path'];
×
647
        } else {
648
            $path = GeneralUtility::getFileAbsFileName($asset['path']);
×
649
        }
650
        $content = file_get_contents($path);
×
651
        return $content ?: null;
×
652
    }
653

654
    /**
655
     * @param AssetInterface|array $asset
656
     */
657
    protected function extractAssetContent($asset): ?string
658
    {
659
        $assetSettings = $this->extractAssetSettings($asset);
30✔
660
        $fileRelativePathAndFilename = $assetSettings['path'] ?? null;
30✔
661
        if (!empty($fileRelativePathAndFilename)) {
30✔
662
            $isExternal = $assetSettings['external'] ?? false;
×
663
            $isFluidTemplate = $assetSettings['fluid'] ?? false;
×
664
            $absolutePathAndFilename = GeneralUtility::getFileAbsFileName($fileRelativePathAndFilename);
×
665
            if (!$isExternal && !file_exists($absolutePathAndFilename)) {
×
666
                throw new \RuntimeException('Asset "' . $absolutePathAndFilename . '" does not exist.');
×
667
            }
668
            if ($isFluidTemplate) {
×
669
                $content = $this->renderAssetAsFluidTemplate($asset);
×
670
            } else {
671
                $content = $this->buildAsset($asset);
×
672
            }
673
        } else {
674
            $content = $this->buildAsset($asset);
30✔
675
        }
676
        if ($content !== null && 'css' === $assetSettings['type'] && ($assetSettings['rewrite'] ?? false)) {
30✔
677
            $fileRelativePath = dirname($assetSettings['path'] ?? '');
18✔
678
            $content = $this->detectAndCopyFileReferences($content, $fileRelativePath);
18✔
679
        }
680
        return $content;
30✔
681
    }
682

683
    public function clearCacheCommand(array $parameters): void
684
    {
685
        if (static::$cacheCleared) {
×
686
            return;
×
687
        }
688
        if ('all' !== ($parameters['cacheCmd'] ?? '')) {
×
689
            return;
×
690
        }
691
        $assetCacheFiles = glob(GeneralUtility::getFileAbsFileName($this->getTempPath() . 'vhs-assets-*'));
×
692
        if (!$assetCacheFiles) {
×
693
            return;
×
694
        }
695
        foreach ($assetCacheFiles as $assetCacheFile) {
×
696
            if (!@touch($assetCacheFile, 0)) {
×
697
                $content = (string) file_get_contents($assetCacheFile);
×
698
                $temporaryAssetCacheFile = (string) tempnam(dirname($assetCacheFile), basename($assetCacheFile) . '.');
×
699
                $this->writeFile($temporaryAssetCacheFile, $content);
×
700
                rename($temporaryAssetCacheFile, $assetCacheFile);
×
701
                touch($assetCacheFile, 0);
×
702
            }
703
        }
704
        static::$cacheCleared = true;
×
705
    }
706

707
    protected function writeFile(string $file, string $contents): void
708
    {
709
        ///** @var Dispatcher $signalSlotDispatcher */
710
        /*
711
        $signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
712
        $signalSlotDispatcher->dispatch(__CLASS__, static::ASSET_SIGNAL, [&$file, &$contents]);
713
        */
714

NEW
715
        $tmpFile = @tempnam(dirname($file), basename($file));
×
NEW
716
        if ($tmpFile === false) {
×
NEW
717
            $error = error_get_last();
×
NEW
718
            $details = $error !== null ? ": {$error['message']}" : ".";
×
NEW
719
            throw new \RuntimeException(
×
NEW
720
                "Failed to create temporary file for writing asset {$file}{$details}",
×
NEW
721
                1733258066
×
NEW
722
            );
×
723
        }
NEW
724
        GeneralUtility::writeFile($tmpFile, $contents, true);
×
NEW
725
        if (@rename($tmpFile, $file) === false) {
×
NEW
726
            $error = error_get_last();
×
NEW
727
            $details = $error !== null ? ": {$error['message']}" : ".";
×
NEW
728
            throw new \RuntimeException(
×
NEW
729
                "Failed to move asset-backing file {$file} into final destination{$details}",
×
NEW
730
                1733258156
×
NEW
731
            );
×
732
        }
733
    }
734

735
    protected function mergeArrays(array $array1, array $array2): array
736
    {
737
        ArrayUtility::mergeRecursiveWithOverrule($array1, $array2);
×
738
        return $array1;
×
739
    }
740

741
    protected function getFileIntegrity(string $file): ?string
742
    {
743
        $typoScript = $GLOBALS['TSFE']->tmpl->setup['plugin.']['tx_vhs.'] ?? null;
36✔
744
        if (isset($typoScript['assets.']['tagsAddSubresourceIntegrity'])) {
36✔
745
            // Note: 3 predefined hashing strategies (the ones suggestes in the rfc sheet)
746
            if (0 < $typoScript['assets.']['tagsAddSubresourceIntegrity']
6✔
747
                && $typoScript['assets.']['tagsAddSubresourceIntegrity'] < 4
6✔
748
            ) {
749
                if (!file_exists($file)) {
6✔
750
                    return null;
×
751
                }
752

753
                /** @var TypoScriptFrontendController $typoScriptFrontendController */
754
                $typoScriptFrontendController = $GLOBALS['TSFE'];
6✔
755

756
                $integrity = null;
6✔
757
                $integrityMethod = ['sha256','sha384','sha512'][
6✔
758
                    $typoScript['assets.']['tagsAddSubresourceIntegrity'] - 1
6✔
759
                ];
6✔
760
                $integrityFile = sprintf(
6✔
761
                    $this->getTempPath() . 'vhs-assets-%s.%s',
6✔
762
                    str_replace('vhs-assets-', '', pathinfo($file, PATHINFO_BASENAME)),
6✔
763
                    $integrityMethod
6✔
764
                );
6✔
765

766
                if (!file_exists($integrityFile)
6✔
767
                    || 0 === filemtime($integrityFile)
×
768
                    || isset($GLOBALS['BE_USER'])
×
769
                    || $typoScriptFrontendController->no_cache
×
770
                    || $typoScriptFrontendController->page['no_cache']
6✔
771
                ) {
772
                    if (extension_loaded('hash') && function_exists('hash_file')) {
6✔
773
                        $integrity = base64_encode((string) hash_file($integrityMethod, $file, true));
6✔
774
                    } elseif (extension_loaded('openssl') && function_exists('openssl_digest')) {
×
775
                        $integrity = base64_encode(
×
776
                            (string) openssl_digest((string) file_get_contents($file), $integrityMethod, true)
×
777
                        );
×
778
                    } else {
779
                        return null; // Sadly, no integrity generation possible
×
780
                    }
781
                    $this->writeFile($integrityFile, $integrity);
6✔
782
                }
783
                return sprintf('%s-%s', $integrityMethod, $integrity ?: (string) file_get_contents($integrityFile));
6✔
784
            }
785
        }
786
        return null;
36✔
787
    }
788

789
    private function getTempPath(): string
790
    {
791
        $publicDirectory = CoreUtility::getSitePath();
36✔
792
        $directory = 'typo3temp/assets/vhs/';
36✔
793
        if (!file_exists($publicDirectory . $directory)) {
36✔
794
            GeneralUtility::mkdir($publicDirectory . $directory);
36✔
795
        }
796
        return $directory;
36✔
797
    }
798

799
    protected function resolveAbsolutePathForFile(string $filename): string
800
    {
801
        return GeneralUtility::getFileAbsFileName($filename);
×
802
    }
803
}
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