diff --git a/src/Asset/EntrypointLookup.php b/src/Asset/EntrypointLookup.php
index 973c3a83..cbdf45ff 100644
--- a/src/Asset/EntrypointLookup.php
+++ b/src/Asset/EntrypointLookup.php
@@ -18,7 +18,7 @@
*
* @final
*/
-class EntrypointLookup implements EntrypointLookupInterface
+class EntrypointLookup implements EntrypointLookupInterface, IntegrityDataProviderInterface
{
private $entrypointJsonPath;
@@ -41,6 +41,17 @@ public function getCssFiles(string $entryName): array
return $this->getEntryFiles($entryName, 'css');
}
+ public function getIntegrityData(): array
+ {
+ $entriesData = $this->getEntriesData();
+
+ if (!array_key_exists('integrity', $entriesData)) {
+ return [];
+ }
+
+ return $entriesData['integrity'];
+ }
+
/**
* Resets the state of this service.
*/
diff --git a/src/Asset/IntegrityDataProviderInterface.php b/src/Asset/IntegrityDataProviderInterface.php
new file mode 100644
index 00000000..41a68f0e
--- /dev/null
+++ b/src/Asset/IntegrityDataProviderInterface.php
@@ -0,0 +1,29 @@
+
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\WebpackEncoreBundle\Asset;
+
+interface IntegrityDataProviderInterface
+{
+ /**
+ * Returns a map of integrity hashes indexed by asset paths.
+ *
+ * If multiples hashes are defined for a given asset they must
+ * be separated by a space.
+ *
+ * For instance:
+ * [
+ * 'path/to/file1.js' => 'sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc',
+ * 'path/to/styles.css' => 'sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J',
+ * ]
+ *
+ * @return string[]
+ */
+ public function getIntegrityData(): array;
+}
diff --git a/src/Asset/TagRenderer.php b/src/Asset/TagRenderer.php
index a7661b00..997938fd 100644
--- a/src/Asset/TagRenderer.php
+++ b/src/Asset/TagRenderer.php
@@ -42,10 +42,21 @@ public function __construct(
public function renderWebpackScriptTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
{
$scriptTags = [];
- foreach ($this->getEntrypointLookup($entrypointName)->getJavaScriptFiles($entryName) as $filename) {
+ $entryPointLookup = $this->getEntrypointLookup($entrypointName);
+ $integrityHashes = ($entryPointLookup instanceof IntegrityDataProviderInterface) ? $entryPointLookup->getIntegrityData() : [];
+
+ foreach ($entryPointLookup->getJavaScriptFiles($entryName) as $filename) {
+ $attributes = [
+ 'src' => $this->getAssetPath($filename, $packageName),
+ ];
+
+ if (isset($integrityHashes[$filename])) {
+ $attributes['integrity'] = $integrityHashes[$filename];
+ }
+
$scriptTags[] = sprintf(
- '',
- htmlentities($this->getAssetPath($filename, $packageName))
+ '',
+ $this->convertArrayToAttributes($attributes)
);
}
@@ -55,10 +66,22 @@ public function renderWebpackScriptTags(string $entryName, string $packageName =
public function renderWebpackLinkTags(string $entryName, string $packageName = null, string $entrypointName = '_default'): string
{
$scriptTags = [];
- foreach ($this->getEntrypointLookup($entrypointName)->getCssFiles($entryName) as $filename) {
+ $entryPointLookup = $this->getEntrypointLookup($entrypointName);
+ $integrityHashes = ($entryPointLookup instanceof IntegrityDataProviderInterface) ? $entryPointLookup->getIntegrityData() : [];
+
+ foreach ($entryPointLookup->getCssFiles($entryName) as $filename) {
+ $attributes = [
+ 'rel' => 'stylesheet',
+ 'href' => $this->getAssetPath($filename, $packageName),
+ ];
+
+ if (isset($integrityHashes[$filename])) {
+ $attributes['integrity'] = $integrityHashes[$filename];
+ }
+
$scriptTags[] = sprintf(
- '',
- htmlentities($this->getAssetPath($filename, $packageName))
+ '',
+ $this->convertArrayToAttributes($attributes)
);
}
@@ -81,4 +104,15 @@ private function getEntrypointLookup(string $buildName): EntrypointLookupInterfa
{
return $this->entrypointLookupCollection->getEntrypointLookup($buildName);
}
+
+ private function convertArrayToAttributes(array $attributesMap): string
+ {
+ return implode(' ', array_map(
+ function ($key, $value) {
+ return sprintf('%s="%s"', $key, htmlentities($value));
+ },
+ array_keys($attributesMap),
+ $attributesMap
+ ));
+ }
}
diff --git a/tests/Asset/EntrypointLookupTest.php b/tests/Asset/EntrypointLookupTest.php
index ce39b128..f98d35fc 100644
--- a/tests/Asset/EntrypointLookupTest.php
+++ b/tests/Asset/EntrypointLookupTest.php
@@ -30,6 +30,10 @@ class EntrypointLookupTest extends TestCase
],
"css": []
}
+ },
+ "integrity": {
+ "file1.js": "sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc",
+ "styles.css": "sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J"
}
}
EOF;
@@ -91,6 +95,23 @@ public function testEmptyReturnOnValidEntryNoJsOrCssFile()
);
}
+ public function testGetIntegrityData()
+ {
+ $this->assertEquals([
+ 'file1.js' => 'sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc',
+ 'styles.css' => 'sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J',
+ ], $this->entrypointLookup->getIntegrityData());
+ }
+
+ public function testMissingIntegrityData()
+ {
+ $filename = tempnam(sys_get_temp_dir(), 'WebpackEncoreBundle');
+ file_put_contents($filename, '{ "entrypoints": { "other_entry": { "js": { } } } }');
+
+ $this->entrypointLookup = new EntrypointLookup($filename);
+ $this->assertEquals([], $this->entrypointLookup->getIntegrityData());
+ }
+
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessageContains There was a problem JSON decoding the
diff --git a/tests/Asset/TagRendererTest.php b/tests/Asset/TagRendererTest.php
index 59feda16..0a14a858 100644
--- a/tests/Asset/TagRendererTest.php
+++ b/tests/Asset/TagRendererTest.php
@@ -6,6 +6,7 @@
use Symfony\Component\Asset\Packages;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollection;
+use Symfony\WebpackEncoreBundle\Asset\IntegrityDataProviderInterface;
use Symfony\WebpackEncoreBundle\Asset\TagRenderer;
class TagRendererTest extends TestCase
@@ -128,4 +129,47 @@ public function testRenderScriptTagsWithinAnEntryPointCollection()
);
}
+ public function testRenderScriptTagsWithHashes()
+ {
+ $entrypointLookup = $this->createMock([
+ EntrypointLookupInterface::class,
+ IntegrityDataProviderInterface::class,
+ ]);
+ $entrypointLookup->expects($this->once())
+ ->method('getJavaScriptFiles')
+ ->willReturn(['/build/file1.js', '/build/file2.js']);
+ $entrypointLookup->expects($this->once())
+ ->method('getIntegrityData')
+ ->willReturn([
+ '/build/file1.js' => 'sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc',
+ '/build/file2.js' => 'sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J',
+ ]);
+ $entrypointCollection = $this->createMock(EntrypointLookupCollection::class);
+ $entrypointCollection->expects($this->once())
+ ->method('getEntrypointLookup')
+ ->withConsecutive(['_default'])
+ ->will($this->onConsecutiveCalls($entrypointLookup));
+
+ $packages = $this->createMock(Packages::class);
+ $packages->expects($this->exactly(2))
+ ->method('getUrl')
+ ->withConsecutive(
+ ['/build/file1.js', 'custom_package'],
+ ['/build/file2.js', 'custom_package']
+ )
+ ->willReturnCallback(function ($path) {
+ return 'http://localhost:8080' . $path;
+ });
+ $renderer = new TagRenderer($entrypointCollection, $packages, true);
+
+ $output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package');
+ $this->assertContains(
+ '',
+ $output
+ );
+ $this->assertContains(
+ '',
+ $output
+ );
+ }
}
diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php
index 361cb83b..0eb2766b 100644
--- a/tests/IntegrationTest.php
+++ b/tests/IntegrationTest.php
@@ -20,12 +20,12 @@ public function testTwigIntegration()
$html1 = $container->get('twig')->render('@integration_test/template.twig');
$this->assertContains(
- '',
+ '',
$html1
);
$this->assertContains(
- ''.
- '',
+ '' .
+ '',
$html1
);
$this->assertContains(
diff --git a/tests/fixtures/build/entrypoints.json b/tests/fixtures/build/entrypoints.json
index 5f1c9902..32009663 100644
--- a/tests/fixtures/build/entrypoints.json
+++ b/tests/fixtures/build/entrypoints.json
@@ -16,5 +16,12 @@
"build/file3.js"
]
}
+ },
+ "integrity": {
+ "build/file1.js": "sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc",
+ "build/file2.js": "sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J",
+ "build/styles.css": "sha384-4g+Zv0iELStVvA4/B27g4TQHUMwZttA5TEojjUyB8Gl5p7sarU4y+VTSGMrNab8n",
+ "build/styles2.css": "sha384-hfZmq9+2oI5Cst4/F4YyS2tJAAYdGz7vqSMP8cJoa8bVOr2kxNRLxSw6P8UZjwUn",
+ "build/file3.js": "sha384-ZU3hiTN/+Va9WVImPi+cI0/j/Q7SzAVezqL1aEXha8sVgE5HU6/0wKUxj1LEnkC9"
}
}