From 84c41ed23d6d86c894fae7680ccf921af4ad2b52 Mon Sep 17 00:00:00 2001 From: Lyrkan Date: Thu, 14 Feb 2019 18:59:50 +0100 Subject: [PATCH] Add support for integrity hashes --- src/Asset/EntrypointLookup.php | 13 +++++- src/Asset/IntegrityDataProviderInterface.php | 29 ++++++++++++ src/Asset/TagRenderer.php | 46 +++++++++++++++++--- tests/Asset/EntrypointLookupTest.php | 21 +++++++++ tests/Asset/TagRendererTest.php | 44 +++++++++++++++++++ tests/IntegrationTest.php | 6 +-- tests/fixtures/build/entrypoints.json | 7 +++ 7 files changed, 156 insertions(+), 10 deletions(-) create mode 100644 src/Asset/IntegrityDataProviderInterface.php 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" } }