Skip to content

Commit 84c41ed

Browse files
Lyrkanweaverryan
authored andcommitted
Add support for integrity hashes
1 parent ae7526c commit 84c41ed

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
@@ -18,7 +18,7 @@
1818
*
1919
* @final
2020
*/
21-
class EntrypointLookup implements EntrypointLookupInterface
21+
class EntrypointLookup implements EntrypointLookupInterface, IntegrityDataProviderInterface
2222
{
2323
private $entrypointJsonPath;
2424

@@ -41,6 +41,17 @@ public function getCssFiles(string $entryName): array
4141
return $this->getEntryFiles($entryName, 'css');
4242
}
4343

44+
public function getIntegrityData(): array
45+
{
46+
$entriesData = $this->getEntriesData();
47+
48+
if (!array_key_exists('integrity', $entriesData)) {
49+
return [];
50+
}
51+
52+
return $entriesData['integrity'];
53+
}
54+
4455
/**
4556
* Resets the state of this service.
4657
*/
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
@@ -30,6 +30,10 @@ class EntrypointLookupTest extends TestCase
3030
],
3131
"css": []
3232
}
33+
},
34+
"integrity": {
35+
"file1.js": "sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc",
36+
"styles.css": "sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J"
3337
}
3438
}
3539
EOF;
@@ -91,6 +95,23 @@ public function testEmptyReturnOnValidEntryNoJsOrCssFile()
9195
);
9296
}
9397

98+
public function testGetIntegrityData()
99+
{
100+
$this->assertEquals([
101+
'file1.js' => 'sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc',
102+
'styles.css' => 'sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J',
103+
], $this->entrypointLookup->getIntegrityData());
104+
}
105+
106+
public function testMissingIntegrityData()
107+
{
108+
$filename = tempnam(sys_get_temp_dir(), 'WebpackEncoreBundle');
109+
file_put_contents($filename, '{ "entrypoints": { "other_entry": { "js": { } } } }');
110+
111+
$this->entrypointLookup = new EntrypointLookup($filename);
112+
$this->assertEquals([], $this->entrypointLookup->getIntegrityData());
113+
}
114+
94115
/**
95116
* @expectedException \InvalidArgumentException
96117
* @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
@@ -20,12 +20,12 @@ public function testTwigIntegration()
2020

2121
$html1 = $container->get('twig')->render('@integration_test/template.twig');
2222
$this->assertContains(
23-
'<script src="/build/file1.js"></script>',
23+
'<script src="/build/file1.js" integrity="sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc"></script>',
2424
$html1
2525
);
2626
$this->assertContains(
27-
'<link rel="stylesheet" href="/build/styles.css">'.
28-
'<link rel="stylesheet" href="/build/styles2.css">',
27+
'<link rel="stylesheet" href="/build/styles.css" integrity="sha384-4g+Zv0iELStVvA4/B27g4TQHUMwZttA5TEojjUyB8Gl5p7sarU4y+VTSGMrNab8n">' .
28+
'<link rel="stylesheet" href="/build/styles2.css" integrity="sha384-hfZmq9+2oI5Cst4/F4YyS2tJAAYdGz7vqSMP8cJoa8bVOr2kxNRLxSw6P8UZjwUn">',
2929
$html1
3030
);
3131
$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)