Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 37d5d36

Browse filesBrowse files
committed
[AssetMapper] Add integrity metadata to importmaps
1 parent d23581b commit 37d5d36
Copy full SHA for 37d5d36

File tree

14 files changed

+163
-27
lines changed
Filter options

14 files changed

+163
-27
lines changed

‎src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ CHANGELOG
5656
* Make `ValidatorCacheWarmer` use `kernel.build_dir` instead of `cache_dir`
5757
* Make `SerializeCacheWarmer` use `kernel.build_dir` instead of `cache_dir`
5858
* Support executing custom workflow validators during container compilation
59+
* Add new `framework.asset_mapper.importmap_integrity_algorithms` option to add integrity metadata to importmaps
5960

6061
7.2
6162
---

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -863,6 +863,7 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $
863863
->fixXmlConfig('excluded_pattern')
864864
->fixXmlConfig('extension')
865865
->fixXmlConfig('importmap_script_attribute')
866+
->fixXmlConfig('importmap_integrity_algorithm')
866867
->children()
867868
// add array node called "paths" that will be an array of strings
868869
->arrayNode('paths')
@@ -972,6 +973,12 @@ private function addAssetMapperSection(ArrayNodeDefinition $rootNode, callable $
972973
->end()
973974
->end()
974975
->end()
976+
->arrayNode('importmap_integrity_algorithms')
977+
->info('Array of algorithms used to compute importmap resources integrity.')
978+
->beforeNormalization()->castToArray()->end()
979+
->prototype('scalar')->end()
980+
->defaultValue(['sha384'])
981+
->end()
975982
->end()
976983
->end()
977984
->end()

‎src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,7 @@ private function registerAssetMapperConfiguration(array $config, ContainerBuilde
14821482
$container
14831483
->getDefinition('asset_mapper.mapped_asset_factory')
14841484
->replaceArgument(2, $config['vendor_dir'])
1485+
->setArgument(3, $config['importmap_integrity_algorithms'])
14851486
;
14861487

14871488
$container

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/asset_mapper.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
service('asset_mapper.public_assets_path_resolver'),
6868
service('asset_mapper_compiler'),
6969
abstract_arg('vendor directory'),
70+
abstract_arg('integrity hash algorithms'),
7071
])
7172

7273
->set('asset_mapper.cached_mapped_asset_factory', CachedMappedAssetFactory::class)

‎src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
<xsd:element name="extension" type="asset_mapper_extension" minOccurs="0" maxOccurs="unbounded" />
209209
<xsd:element name="importmap-script-attribute" type="asset_mapper_attribute" minOccurs="0" maxOccurs="unbounded" />
210210
<xsd:element name="precompress" type="asset_mapper_precompress" minOccurs="0" maxOccurs="1" />
211+
<xsd:element name="importmap-integrity-algorithm" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
211212
</xsd:sequence>
212213
<xsd:attribute name="enabled" type="xsd:boolean" />
213214
<xsd:attribute name="exclude-dotfiles" type="xsd:boolean" />

‎src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ public function testAssetMapperCanBeEnabled()
148148
'formats' => [],
149149
'extensions' => interface_exists(CompressorInterface::class) ? CompressorInterface::DEFAULT_EXTENSIONS : [],
150150
],
151+
'importmap_integrity_algorithms' => ['sha384'],
151152
];
152153

153154
$this->assertEquals($defaultConfig, $config['asset_mapper']);
@@ -877,6 +878,7 @@ protected static function getBundleDefaultConfig()
877878
'formats' => [],
878879
'extensions' => interface_exists(CompressorInterface::class) ? CompressorInterface::DEFAULT_EXTENSIONS : [],
879880
],
881+
'importmap_integrity_algorithms' => ['sha384'],
880882
],
881883
'cache' => [
882884
'pools' => [],

‎src/Symfony/Component/AssetMapper/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/AssetMapper/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add support for pre-compressing assets with Brotli, Zstandard, Zopfli, and gzip
88
* Add option `--dry-run` to `importmap:require` command
99
* `ImportMapRequireCommand` now takes `projectDir` as a required third constructor argument
10+
* Add integrity metadata to importmaps
1011

1112
7.2
1213
---

‎src/Symfony/Component/AssetMapper/Factory/MappedAssetFactory.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/AssetMapper/Factory/MappedAssetFactory.php
+23Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\AssetMapper\AssetMapperCompiler;
1515
use Symfony\Component\AssetMapper\Exception\CircularAssetsException;
16+
use Symfony\Component\AssetMapper\Exception\LogicException;
1617
use Symfony\Component\AssetMapper\Exception\RuntimeException;
1718
use Symfony\Component\AssetMapper\MappedAsset;
1819
use Symfony\Component\AssetMapper\Path\PublicAssetsPathResolverInterface;
@@ -25,6 +26,7 @@ class MappedAssetFactory implements MappedAssetFactoryInterface
2526
{
2627
private const PREDIGESTED_REGEX = '/-([0-9a-zA-Z]{7,128}\.digested)/';
2728
private const PUBLIC_DIGEST_LENGTH = 7;
29+
private const INTEGRITY_HASH_ALGORITHMS = ['sha256', 'sha384', 'sha512'];
2830

2931
private array $assetsCache = [];
3032
private array $assetsBeingCreated = [];
@@ -33,7 +35,11 @@ public function __construct(
3335
private readonly PublicAssetsPathResolverInterface $assetsPathResolver,
3436
private readonly AssetMapperCompiler $compiler,
3537
private readonly string $vendorDir,
38+
private readonly array $integrityHashAlgorithms = [],
3639
) {
40+
if ($unsupportedAlgorithms = array_diff($this->integrityHashAlgorithms, self::INTEGRITY_HASH_ALGORITHMS)) {
41+
throw new LogicException(sprintf('Unsupported "%s" algorithm(s). Supported ones are "%s".', implode('", "', $unsupportedAlgorithms), implode('", "', self::INTEGRITY_HASH_ALGORITHMS)));
42+
}
3743
}
3844

3945
public function createMappedAsset(string $logicalPath, string $sourcePath): ?MappedAsset
@@ -63,6 +69,7 @@ public function createMappedAsset(string $logicalPath, string $sourcePath): ?Map
6369
$asset->getDependencies(),
6470
$asset->getFileDependencies(),
6571
$asset->getJavaScriptImports(),
72+
$this->getIntegrity($asset, $content),
6673
);
6774

6875
$this->assetsCache[$logicalPath] = $asset;
@@ -131,4 +138,20 @@ private function isVendor(string $sourcePath): bool
131138

132139
return $sourcePath && $vendorDir && str_starts_with($sourcePath, $vendorDir);
133140
}
141+
142+
private function getIntegrity(MappedAsset $asset, ?string $content): ?string
143+
{
144+
$integrity = null;
145+
146+
foreach ($this->integrityHashAlgorithms as $algorithm) {
147+
$hash = null !== $content
148+
? hash($algorithm, $content, true)
149+
: hash_file($algorithm, $asset->sourcePath, true)
150+
;
151+
152+
$integrity .= \sprintf('%s%s-%s', $integrity ? ' ' : '', $algorithm, base64_encode($hash));
153+
}
154+
155+
return $integrity;
156+
}
134157
}

‎src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/AssetMapper/ImportMap/ImportMapGenerator.php
+6-5Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function getEntrypointNames(): array
5050
/**
5151
* @param string[] $entrypointNames
5252
*
53-
* @return array<string, array{path: string, type: string, preload?: bool}>
53+
* @return array<string, array{path: string, type: string, integrity?: string, preload?: bool}>
5454
*
5555
* @internal
5656
*/
@@ -83,7 +83,7 @@ public function getImportMapData(array $entrypointNames): array
8383
/**
8484
* @internal
8585
*
86-
* @return array<string, array{path: string, type: string}>
86+
* @return array<string, array{path: string, type: string, integrity?: string}>
8787
*/
8888
public function getRawImportMapData(): array
8989
{
@@ -104,9 +104,10 @@ public function getRawImportMapData(): array
104104
throw $this->createMissingImportMapAssetException($entry);
105105
}
106106

107-
$path = $asset->publicPath;
108-
$data = ['path' => $path, 'type' => $entry->type->value];
109-
$rawImportMapData[$entry->importName] = $data;
107+
$rawImportMapData[$entry->importName] = ['path' => $asset->publicPath, 'type' => $entry->type->value];
108+
if ($asset->integrity) {
109+
$rawImportMapData[$entry->importName]['integrity'] = $asset->integrity;
110+
}
110111
}
111112

112113
return $rawImportMapData;

‎src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/AssetMapper/ImportMap/ImportMapRenderer.php
+6-1Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public function render(string|array $entryPoint, array $attributes = []): string
4949
$importMap = [];
5050
$modulePreloads = [];
5151
$cssLinks = [];
52+
$integrity = [];
5253
$polyfillPath = null;
5354
foreach ($importMapData as $importName => $data) {
5455
$path = $data['path'];
@@ -70,8 +71,12 @@ public function render(string|array $entryPoint, array $attributes = []): string
7071
}
7172

7273
$preload = $data['preload'] ?? false;
74+
$assetIntegrity = $data['integrity'] ?? false;
7375
if ('css' !== $data['type']) {
7476
$importMap[$importName] = $path;
77+
if ($assetIntegrity) {
78+
$integrity[$path] = $assetIntegrity;
79+
}
7580
if ($preload) {
7681
$modulePreloads[] = $path;
7782
}
@@ -96,7 +101,7 @@ public function render(string|array $entryPoint, array $attributes = []): string
96101
}
97102

98103
$scriptAttributes = $attributes || $this->scriptAttributes ? ' '.$this->createAttributesString($attributes) : '';
99-
$importMapJson = json_encode(['imports' => $importMap], \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG);
104+
$importMapJson = json_encode(['imports' => $importMap, ...$integrity ? ['integrity' => $integrity] : []], \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_HEX_TAG);
100105
$output .= <<<HTML
101106
102107
<script type="importmap"$scriptAttributes>

‎src/Symfony/Component/AssetMapper/MappedAsset.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/AssetMapper/MappedAsset.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public function __construct(
5252
private array $dependencies = [],
5353
private array $fileDependencies = [],
5454
private array $javaScriptImports = [],
55+
public ?string $integrity = null,
5556
) {
5657
if (null !== $sourcePath) {
5758
$this->sourcePath = $sourcePath;

‎src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/AssetMapper/Tests/Factory/MappedAssetFactoryTest.php
+31-1Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\AssetMapper\Compiler\CssAssetUrlCompiler;
2020
use Symfony\Component\AssetMapper\Compiler\JavaScriptImportPathCompiler;
2121
use Symfony\Component\AssetMapper\Exception\CircularAssetsException;
22+
use Symfony\Component\AssetMapper\Exception\LogicException;
2223
use Symfony\Component\AssetMapper\Factory\MappedAssetFactory;
2324
use Symfony\Component\AssetMapper\ImportMap\ImportMapConfigReader;
2425
use Symfony\Component\AssetMapper\MappedAsset;
@@ -148,7 +149,35 @@ public function testCreateMappedAssetInMissingVendor()
148149
$this->assertFalse($asset->isVendor);
149150
}
150151

151-
private function createFactory(?AssetCompilerInterface $extraCompiler = null, ?string $vendorDir = self::VENDOR_FIXTURES_DIR): MappedAssetFactory
152+
public function testCreateMappedAssetWithoutIntegrity()
153+
{
154+
$factory = $this->createFactory();
155+
$asset = $factory->createMappedAsset('file2.js', self::FIXTURES_DIR.'/dir1/file2.js');
156+
$this->assertNull($asset->integrity);
157+
}
158+
159+
public function testCreateMappedAssetWithOneIntegrityAlgorithm()
160+
{
161+
$factory = $this->createFactory(integrityHashAlgorithms: ['sha256']);
162+
$asset = $factory->createMappedAsset('file2.js', self::FIXTURES_DIR.'/dir1/file2.js');
163+
$this->assertSame('sha256-b8bze+0OP5qLVVEG0aUh25UkvNjZXLeugH9Jg7MvSz8=', $asset->integrity);
164+
}
165+
166+
public function testCreateMappedAssetWithManyIntegrityAlgorithms()
167+
{
168+
$factory = $this->createFactory(integrityHashAlgorithms: ['sha256', 'sha384']);
169+
$asset = $factory->createMappedAsset('file2.js', self::FIXTURES_DIR.'/dir1/file2.js');
170+
$this->assertSame('sha256-b8bze+0OP5qLVVEG0aUh25UkvNjZXLeugH9Jg7MvSz8= sha384-2cpbxkWC8I4PKAhlQ+LaFmVek6qd8w35xUZ+QRGMzcSvX9SP2EgjLvKSawSmS9J7', $asset->integrity);
171+
}
172+
173+
public function testCreateMappedAssetWithInvalidIntegrityAlgorithm()
174+
{
175+
$this->expectException(LogicException::class);
176+
$this->expectExceptionMessage('Unsupported "sha1" algorithm(s). Supported ones are "sha256", "sha384", "sha512".');
177+
$this->createFactory(integrityHashAlgorithms: ['sha1']);
178+
}
179+
180+
private function createFactory(?AssetCompilerInterface $extraCompiler = null, ?string $vendorDir = self::VENDOR_FIXTURES_DIR, array $integrityHashAlgorithms = []): MappedAssetFactory
152181
{
153182
$compilers = [
154183
new JavaScriptImportPathCompiler($this->createMock(ImportMapConfigReader::class)),
@@ -174,6 +203,7 @@ private function createFactory(?AssetCompilerInterface $extraCompiler = null, ?s
174203
$pathResolver,
175204
$compiler,
176205
$vendorDir,
206+
$integrityHashAlgorithms,
177207
);
178208

179209
// mock the AssetMapper to behave like normal: by calling back to the factory

‎src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/AssetMapper/Tests/ImportMap/ImportMapGeneratorTest.php
+43Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ public function testGetImportMapData()
9292
path: 'styles/never_imported_css.css',
9393
type: ImportMapType::CSS,
9494
),
95+
self::createLocalEntry(
96+
'js_file_with_integrity',
97+
path: 'js_file_with_integrity.js',
98+
),
9599
]);
96100

97101
$importedFile1 = new MappedAsset(
@@ -142,6 +146,13 @@ public function testGetImportMapData()
142146
publicPathWithoutDigest: '/assets/styles/never_imported_css.css',
143147
publicPath: '/assets/styles/never_imported_css-d1g35t.css',
144148
);
149+
$jsFileWithIntegrity = new MappedAsset(
150+
'js_file_with_integrity.js',
151+
'/path/to/js_file_with_integrity.js',
152+
publicPathWithoutDigest: '/assets/js_file_with_integrity.js',
153+
publicPath: '/assets/js_file_with_integrity-d1g35t.js',
154+
integrity: 'sha384-base64-hash'
155+
);
145156
$this->mockAssetMapper([
146157
new MappedAsset(
147158
'entry1.js',
@@ -179,6 +190,7 @@ public function testGetImportMapData()
179190
$importedCss2,
180191
$importedCssInImportmap,
181192
$neverImportedCss,
193+
$jsFileWithIntegrity,
182194
]);
183195

184196
$actualImportMapData = $manager->getImportMapData(['entry2', 'entry1']);
@@ -232,6 +244,11 @@ public function testGetImportMapData()
232244
'path' => '/assets/styles/never_imported_css-d1g35t.css',
233245
'type' => 'css',
234246
],
247+
'js_file_with_integrity' => [
248+
'path' => '/assets/js_file_with_integrity-d1g35t.js',
249+
'type' => 'js',
250+
'integrity' => 'sha384-base64-hash',
251+
],
235252
], $actualImportMapData);
236253

237254
// now check the order
@@ -251,6 +268,7 @@ public function testGetImportMapData()
251268
// importmap entries never imported
252269
'entry3',
253270
'never_imported_css',
271+
'js_file_with_integrity',
254272
], array_keys($actualImportMapData));
255273
}
256274

@@ -570,6 +588,31 @@ public static function getRawImportMapDataTests(): iterable
570588
],
571589
],
572590
];
591+
592+
yield 'it adds integrity when it exists' => [
593+
[
594+
self::createLocalEntry(
595+
'app',
596+
path: './assets/app.js',
597+
),
598+
],
599+
[
600+
new MappedAsset(
601+
'app.js',
602+
// /fake/root is the mocked root directory
603+
'/fake/root/assets/app.js',
604+
publicPath: '/assets/app-d1g3st.js',
605+
integrity: 'sha384-base64-hash',
606+
),
607+
],
608+
[
609+
'app' => [
610+
'path' => '/assets/app-d1g3st.js',
611+
'type' => 'js',
612+
'integrity' => 'sha384-base64-hash',
613+
],
614+
],
615+
];
573616
}
574617

575618
public function testGetRawImportDataUsesCacheFile()

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.