Skip to content

Commit

Permalink
Add support for integrity hashes
Browse files Browse the repository at this point in the history
  • Loading branch information
Lyrkan authored and weaverryan committed Mar 29, 2019
1 parent ae7526c commit 84c41ed
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 10 deletions.
13 changes: 12 additions & 1 deletion src/Asset/EntrypointLookup.php
Expand Up @@ -18,7 +18,7 @@
*
* @final
*/
class EntrypointLookup implements EntrypointLookupInterface
class EntrypointLookup implements EntrypointLookupInterface, IntegrityDataProviderInterface
{
private $entrypointJsonPath;

Expand All @@ -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.
*/
Expand Down
29 changes: 29 additions & 0 deletions src/Asset/IntegrityDataProviderInterface.php
@@ -0,0 +1,29 @@
<?php

/*
* This file is part of the Symfony WebpackEncoreBundle package.
* (c) Fabien Potencier <fabien@symfony.com>
* 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;
}
46 changes: 40 additions & 6 deletions src/Asset/TagRenderer.php
Expand Up @@ -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(
'<script src="%s"></script>',
htmlentities($this->getAssetPath($filename, $packageName))
'<script %s></script>',
$this->convertArrayToAttributes($attributes)
);
}

Expand All @@ -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(
'<link rel="stylesheet" href="%s">',
htmlentities($this->getAssetPath($filename, $packageName))
'<link %s>',
$this->convertArrayToAttributes($attributes)
);
}

Expand All @@ -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
));
}
}
21 changes: 21 additions & 0 deletions tests/Asset/EntrypointLookupTest.php
Expand Up @@ -30,6 +30,10 @@ class EntrypointLookupTest extends TestCase
],
"css": []
}
},
"integrity": {
"file1.js": "sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc",
"styles.css": "sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J"
}
}
EOF;
Expand Down Expand Up @@ -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
Expand Down
44 changes: 44 additions & 0 deletions tests/Asset/TagRendererTest.php
Expand Up @@ -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
Expand Down Expand Up @@ -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(
'<script src="http://localhost:8080/build/file1.js" integrity="sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc"></script>',
$output
);
$this->assertContains(
'<script src="http://localhost:8080/build/file2.js" integrity="sha384-ymG7OyjISWrOpH9jsGvajKMDEOP/mKJq8bHC0XdjQA6P8sg2nu+2RLQxcNNwE/3J"></script>',
$output
);
}
}
6 changes: 3 additions & 3 deletions tests/IntegrationTest.php
Expand Up @@ -20,12 +20,12 @@ public function testTwigIntegration()

$html1 = $container->get('twig')->render('@integration_test/template.twig');
$this->assertContains(
'<script src="/build/file1.js"></script>',
'<script src="/build/file1.js" integrity="sha384-Q86c+opr0lBUPWN28BLJFqmLhho+9ZcJpXHorQvX6mYDWJ24RQcdDarXFQYN8HLc"></script>',
$html1
);
$this->assertContains(
'<link rel="stylesheet" href="/build/styles.css">'.
'<link rel="stylesheet" href="/build/styles2.css">',
'<link rel="stylesheet" href="/build/styles.css" integrity="sha384-4g+Zv0iELStVvA4/B27g4TQHUMwZttA5TEojjUyB8Gl5p7sarU4y+VTSGMrNab8n">' .
'<link rel="stylesheet" href="/build/styles2.css" integrity="sha384-hfZmq9+2oI5Cst4/F4YyS2tJAAYdGz7vqSMP8cJoa8bVOr2kxNRLxSw6P8UZjwUn">',
$html1
);
$this->assertContains(
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/build/entrypoints.json
Expand Up @@ -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"
}
}

0 comments on commit 84c41ed

Please sign in to comment.