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

FluidTYPO3 / vhs / 14774248994

01 May 2025 10:54AM UTC coverage: 72.109%. Remained the same
14774248994

push

github

NamelessCoder
[BUGFIX] Write temp file within TYPO3 project path

Fixes an issue where a file written by TYPO3 but written outside the project
path would be written with default umask of PHP's user, and TYPO3 would
not adjust the file permissions according to configured group/permissions.

Caused by TYPO3's check for "is valid absolute path" returning false for any
path outside the project root and TYPO3's file permissions override not
reporting this as an error, but instead just silently continuing without actually
fixing the file permissions. Even though, there's technically nothing to hinder
adjusting the file ownership and permission even for files outside the project.

Avoided by using GU::tempnam() which writes the temporary file inside project.

Close: #1944

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

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);
×
NEW
755
                $temporaryAssetCacheFile = (string) GeneralUtility::tempnam(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');
×
864
        if (!$pageArguments instanceof PageArguments) {
×
865
            return 0;
×
866
        }
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 TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc