Skip to content

Commit fc18456

Browse files
committed
feature #42 Add support for integrity hashes (Lyrkan)
This PR was squashed before being merged into the master branch (closes #42). Discussion ---------- Add support for integrity hashes This PR allows to automatically add `integrity` attributes on `<script>` and `<link>` tags based on the content of the `entrypoints.json` file (related to the following PR on Encore: symfony/webpack-encore#522). It requires the following configuration: ```js // webpack.config.js // Enable it for all builds with the // default hash algorithm (sha384) Encore.enableIntegrityHashes(); // Or enable it only in production // with a custom hash algorithm Encore.enableIntegrityHashes( Encore.isProduction(), 'sha384' ); // Or with multiple hash algorithms Encore.enableIntegrityHashes( Encore.isProduction(), ['sha384','sha512'] ); ``` Then, calling `yarn encore` then generates an entrypoints.json that contains hashes for all the files it references: ```js { "entrypoints": { // (...) }, "integrity": { "/build/runtime.fa8f03f5.js": "sha384-5WSgDNxkAY6j6/bzAcp3v//+PCXLgXCU3u5QgRXWiRfMnN4Ic/a/EF6HJnbRXik8", "/build/0.b70b772e.js": "sha384-FA3+8ecenjmV1Y751s0fKxGBNtyLBA8hDY4sqFoqvsCPOamLlA5ckhRBttBg1esp", // (...) } } ``` And these hashes are automatically added when calling `encore_entry_script_tags` and `encore_entry_link_tags`: ```html <html lang="en"> <head> <!-- ... --> <link rel="stylesheet" href="/build/css/app.2235bc2d.css" integrity="sha384-Jmd35HF93DFCXjisVeMi6U3lniH/mOdAF6wLtOMqhYMh2ZiBRUdtF7jXB55IAKfm"> <!-- ... --> </head> <body id="homepage"> <!-- ... --> <script src="/build/runtime.fa8f03f5.js" integrity="sha384-5WSgDNxkAY6j6/bzAcp3v//+PCXLgXCU3u5QgRXWiRfMnN4Ic/a/EF6HJnbRXik8"></script> <script src="/build/0.b70b772e.js" integrity="sha384-FA3+8ecenjmV1Y751s0fKxGBNtyLBA8hDY4sqFoqvsCPOamLlA5ckhRBttBg1esp"></script> <!-- ... --> </body> </html> ``` An example using Symfony Demo can be found here: Lyrkan/symfony-demo@91a06cd Commits ------- 84c41ed Add support for integrity hashes
2 parents cd9894a + 84c41ed commit fc18456

File tree

7 files changed

+156
-10
lines changed

7 files changed

+156
-10
lines changed

src/Asset/EntrypointLookup.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
*
2020
* @final
2121
*/
22-
class EntrypointLookup implements EntrypointLookupInterface
22+
class EntrypointLookup implements EntrypointLookupInterface, IntegrityDataProviderInterface
2323
{
2424
private $entrypointJsonPath;
2525

@@ -46,6 +46,17 @@ public function getCssFiles(string $entryName): array
4646
return $this->getEntryFiles($entryName, 'css');
4747
}
4848

49+
public function getIntegrityData(): array
50+
{
51+
$entriesData = $this->getEntriesData();
52+
53+
if (!array_key_exists('integrity', $entriesData)) {
54+
return [];
55+
}
56+
57+
return $entriesData['integrity'];
58+
}
59+
4960
/**
5061
* Resets the state of this service.
5162
*/
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony WebpackEncoreBundle package.
5+
* (c) Fabien Potencier <[email protected]>
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Symfony\WebpackEncoreBundle\Asset;
11+
12+
interface IntegrityDataProviderInterface
13+
{
14+
/**
15+
* Returns a map of integrity hashes indexed by asset paths.
16+
*
17+
* If multiples hashes are defined for a given asset they must
18+
* be separated by a space.
19+
*
20+
* For instance:
21+
* [
22+
* 'path/to/file1.js' => 'sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc',
23+
* 'path/to/styles.css' => 'sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J',
24+
* ]
25+
*
26+
* @return string[]
27+
*/
28+
public function getIntegrityData(): array;
29+
}

src/Asset/TagRenderer.php

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,21 @@ public function __construct(
4242
public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
4343
{
4444
$scriptTags = [];
45-
foreach ($this->getEntrypointLookup($entrypointName)->getJavaScriptFiles($entryName) as $filename) {
45+
$entryPointLookup = $this->getEntrypointLookup($entrypointName);
46+
$integrityHashes = ($entryPointLookup instanceof IntegrityDataProviderInterface) ? $entryPointLookup->getIntegrityData() : [];
47+
48+
foreach ($entryPointLookup->getJavaScriptFiles($entryName) as $filename) {
49+
$attributes = [
50+
'src' => $this->getAssetPath($filename, $packageName),
51+
];
52+
53+
if (isset($integrityHashes[$filename])) {
54+
$attributes['integrity'] = $integrityHashes[$filename];
55+
}
56+
4657
$scriptTags[] = sprintf(
47-
'<script src="%s"></script>',
48-
htmlentities($this->getAssetPath($filename, $packageName))
58+
'<script %s></script>',
59+
$this->convertArrayToAttributes($attributes)
4960
);
5061
}
5162

@@ -55,10 +66,22 @@ public function renderWebpackScriptTags(string $entryName, string $packageName =
5566
public function renderWebpackLinkTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
5667
{
5768
$scriptTags = [];
58-
foreach ($this->getEntrypointLookup($entrypointName)->getCssFiles($entryName) as $filename) {
69+
$entryPointLookup = $this->getEntrypointLookup($entrypointName);
70+
$integrityHashes = ($entryPointLookup instanceof IntegrityDataProviderInterface) ? $entryPointLookup->getIntegrityData() : [];
71+
72+
foreach ($entryPointLookup->getCssFiles($entryName) as $filename) {
73+
$attributes = [
74+
'rel' => 'stylesheet',
75+
'href' => $this->getAssetPath($filename, $packageName),
76+
];
77+
78+
if (isset($integrityHashes[$filename])) {
79+
$attributes['integrity'] = $integrityHashes[$filename];
80+
}
81+
5982
$scriptTags[] = sprintf(
60-
'<link rel="stylesheet" href="%s">',
61-
htmlentities($this->getAssetPath($filename, $packageName))
83+
'<link %s>',
84+
$this->convertArrayToAttributes($attributes)
6285
);
6386
}
6487

@@ -81,4 +104,15 @@ private function getEntrypointLookup(string $buildName): EntrypointLookupInterfa
81104
{
82105
return $this->entrypointLookupCollection->getEntrypointLookup($buildName);
83106
}
107+
108+
private function convertArrayToAttributes(array $attributesMap): string
109+
{
110+
return implode(' ', array_map(
111+
function ($key, $value) {
112+
return sprintf('%s="%s"', $key, htmlentities($value));
113+
},
114+
array_keys($attributesMap),
115+
$attributesMap
116+
));
117+
}
84118
}

tests/Asset/EntrypointLookupTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ class EntrypointLookupTest extends TestCase
3232
],
3333
"css": []
3434
}
35+
},
36+
"integrity": {
37+
"file1.js": "sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc",
38+
"styles.css": "sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J"
3539
}
3640
}
3741
EOF;
@@ -93,6 +97,23 @@ public function testEmptyReturnOnValidEntryNoJsOrCssFile()
9397
);
9498
}
9599

100+
public function testGetIntegrityData()
101+
{
102+
$this->assertEquals([
103+
'file1.js' => 'sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc',
104+
'styles.css' => 'sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J',
105+
], $this->entrypointLookup->getIntegrityData());
106+
}
107+
108+
public function testMissingIntegrityData()
109+
{
110+
$filename = tempnam(sys_get_temp_dir(), 'WebpackEncoreBundle');
111+
file_put_contents($filename, '{ "entrypoints": { "other_entry": { "js": { } } } }');
112+
113+
$this->entrypointLookup = new EntrypointLookup($filename);
114+
$this->assertEquals([], $this->entrypointLookup->getIntegrityData());
115+
}
116+
96117
/**
97118
* @expectedException \InvalidArgumentException
98119
* @expectedExceptionMessageContains There was a problem JSON decoding the

tests/Asset/TagRendererTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Symfony\Component\Asset\Packages;
77
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
88
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollection;
9+
use Symfony\WebpackEncoreBundle\Asset\IntegrityDataProviderInterface;
910
use Symfony\WebpackEncoreBundle\Asset\TagRenderer;
1011

1112
class TagRendererTest extends TestCase
@@ -128,4 +129,47 @@ public function testRenderScriptTagsWithinAnEntryPointCollection()
128129
);
129130
}
130131

132+
public function testRenderScriptTagsWithHashes()
133+
{
134+
$entrypointLookup = $this->createMock([
135+
EntrypointLookupInterface::class,
136+
IntegrityDataProviderInterface::class,
137+
]);
138+
$entrypointLookup->expects($this->once())
139+
->method('getJavaScriptFiles')
140+
->willReturn(['/build/file1.js', '/build/file2.js']);
141+
$entrypointLookup->expects($this->once())
142+
->method('getIntegrityData')
143+
->willReturn([
144+
'/build/file1.js' => 'sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc',
145+
'/build/file2.js' => 'sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J',
146+
]);
147+
$entrypointCollection = $this->createMock(EntrypointLookupCollection::class);
148+
$entrypointCollection->expects($this->once())
149+
->method('getEntrypointLookup')
150+
->withConsecutive(['_default'])
151+
->will($this->onConsecutiveCalls($entrypointLookup));
152+
153+
$packages = $this->createMock(Packages::class);
154+
$packages->expects($this->exactly(2))
155+
->method('getUrl')
156+
->withConsecutive(
157+
['/build/file1.js', 'custom_package'],
158+
['/build/file2.js', 'custom_package']
159+
)
160+
->willReturnCallback(function ($path) {
161+
return 'http://localhost:8080' . $path;
162+
});
163+
$renderer = new TagRenderer($entrypointCollection, $packages, true);
164+
165+
$output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package');
166+
$this->assertContains(
167+
'<script src="http://localhost:8080/build/file1.js" integrity="sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc"></script>',
168+
$output
169+
);
170+
$this->assertContains(
171+
'<script src="http://localhost:8080/build/file2.js" integrity="sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J"></script>',
172+
$output
173+
);
174+
}
131175
}

tests/IntegrationTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ public function testTwigIntegration()
2424

2525
$html1 = $container->get('twig')->render('@integration_test/template.twig');
2626
$this->assertContains(
27-
'<script src="/build/file1.js"></script>',
27+
'<script src="/build/file1.js" integrity="sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc"></script>',
2828
$html1
2929
);
3030
$this->assertContains(
31-
'<link rel="stylesheet" href="/build/styles.css">'.
32-
'<link rel="stylesheet" href="/build/styles2.css">',
31+
'<link rel="stylesheet" href="/build/styles.css" integrity="sha384-4g+Zv0iELStVvA4/B27g4TQHUMwZttA5TEojjUyB8Gl5p7sarU4y+VTSGMrNab8n">' .
32+
'<link rel="stylesheet" href="/build/styles2.css" integrity="sha384-hfZmq9+2oI5Cst4/F4YyS2tJAAYdGz7vqSMP8cJoa8bVOr2kxNRLxSw6P8UZjwUn">',
3333
$html1
3434
);
3535
$this->assertContains(

tests/fixtures/build/entrypoints.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,12 @@
1616
"build/file3.js"
1717
]
1818
}
19+
},
20+
"integrity": {
21+
"build/file1.js": "sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc",
22+
"build/file2.js": "sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J",
23+
"build/styles.css": "sha384-4g+Zv0iELStVvA4/B27g4TQHUMwZttA5TEojjUyB8Gl5p7sarU4y+VTSGMrNab8n",
24+
"build/styles2.css": "sha384-hfZmq9+2oI5Cst4/F4YyS2tJAAYdGz7vqSMP8cJoa8bVOr2kxNRLxSw6P8UZjwUn",
25+
"build/file3.js": "sha384-ZU3hiTN/+Va9WVImPi+cI0/j/Q7SzAVezqL1aEXha8sVgE5HU6/0wKUxj1LEnkC9"
1926
}
2027
}

0 commit comments

Comments
 (0)