diff --git a/src/Asset/NonceNullProvider.php b/src/Asset/NonceNullProvider.php
new file mode 100644
index 00000000..9502a385
--- /dev/null
+++ b/src/Asset/NonceNullProvider.php
@@ -0,0 +1,18 @@
+
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\WebpackEncoreBundle\Asset;
+
+
+
+interface NonceProviderInterface
+{
+ /**
+ * Returns a nonce attribute value
+ *
+ * @return string
+ */
+ public function getNonceValue(): string;
+}
diff --git a/src/Asset/TagRenderer.php b/src/Asset/TagRenderer.php
index 4e5ffc25..42cb0645 100644
--- a/src/Asset/TagRenderer.php
+++ b/src/Asset/TagRenderer.php
@@ -18,6 +18,8 @@
*/
class TagRenderer implements ResetInterface
{
+ private const NONCE_ATTRIBUTE_NAME = 'nonce';
+
private $entrypointLookupCollection;
private $packages;
@@ -26,10 +28,13 @@ class TagRenderer implements ResetInterface
private $renderedFiles = [];
+ private $nonceProvider;
+
public function __construct(
$entrypointLookupCollection,
Packages $packages,
- array $defaultAttributes = []
+ array $defaultAttributes = [],
+ NonceProviderInterface $nonceProvider = null
) {
if ($entrypointLookupCollection instanceof EntrypointLookupInterface) {
@trigger_error(sprintf('The "$entrypointLookupCollection" argument in method "%s()" must be an instance of EntrypointLookupCollection.', __METHOD__), E_USER_DEPRECATED);
@@ -47,7 +52,7 @@ public function __construct(
$this->packages = $packages;
$this->defaultAttributes = $defaultAttributes;
-
+ $this->nonceProvider = $nonceProvider;
$this->reset();
}
@@ -67,7 +72,7 @@ public function renderWebpackScriptTags(string $entryName, string $packageName =
$scriptTags[] = sprintf(
'',
- $this->convertArrayToAttributes($attributes)
+ $this->convertArrayToAttributes($attributes,'script')
);
$this->renderedFiles['scripts'][] = $attributes['src'];
@@ -93,7 +98,7 @@ public function renderWebpackLinkTags(string $entryName, string $packageName = n
$scriptTags[] = sprintf(
'',
- $this->convertArrayToAttributes($attributes)
+ $this->convertArrayToAttributes($attributes,'link')
);
$this->renderedFiles['styles'][] = $attributes['href'];
@@ -142,8 +147,12 @@ private function getEntrypointLookup(string $buildName): EntrypointLookupInterfa
return $this->entrypointLookupCollection->getEntrypointLookup($buildName);
}
- private function convertArrayToAttributes(array $attributesMap): string
+ private function convertArrayToAttributes(array $attributesMap,string $targetTag=''): string
{
+ if (\in_array($targetTag,['script','style'],true) && null !== $this->nonceProvider && $this->nonceProvider->getNonceValue() != '') {
+ $attributesMap = array_merge([self::NONCE_ATTRIBUTE_NAME => $this->nonceProvider->getNonceValue()], $attributesMap);
+ }
+
return implode(' ', array_map(
function ($key, $value) {
return sprintf('%s="%s"', $key, htmlentities($value));
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 9dce21d2..cce5c044 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -47,6 +47,14 @@ public function getConfigTreeBuilder()
->info('Enable caching of the entry point file(s)')
->defaultFalse()
->end()
+ ->booleanNode('nonce_enable')
+ ->info('Enable attrbute in script and style tag')
+ ->defaultFalse()
+ ->end()
+ ->scalarNode('nonce_provider')
+ ->info('Nonce provider class')
+ ->defaultNull()
+ ->end()
->booleanNode('strict_mode')
->info('Throw an exception if the entrypoints.json file is missing or an entry is missing from the data')
->defaultTrue()
diff --git a/src/DependencyInjection/WebpackEncoreExtension.php b/src/DependencyInjection/WebpackEncoreExtension.php
index 521a2e97..ee1f2437 100644
--- a/src/DependencyInjection/WebpackEncoreExtension.php
+++ b/src/DependencyInjection/WebpackEncoreExtension.php
@@ -69,6 +69,8 @@ public function load(array $configs, ContainerBuilder $container)
$container->getDefinition('webpack_encore.tag_renderer')
->replaceArgument(2, $defaultAttributes);
+
+
if ($config['preload']) {
if (!class_exists(AddLinkHeaderListener::class)) {
@@ -77,6 +79,21 @@ public function load(array $configs, ContainerBuilder $container)
} else {
$container->removeDefinition('webpack_encore.preload_assets_event_listener');
}
+
+ if (false !== $config['nonce_enable']) {
+
+ if (empty($config['nonce_provider'])) {
+ throw new \LogicException('If nonce_enable it is true must be provide nonce_provider service class');
+ }
+
+ $serviceId = $config['nonce_provider'];
+ $serviceNonceProvider = new Reference($serviceId);
+
+ $container->getDefinition('webpack_encore.tag_renderer')
+ ->replaceArgument(3, $serviceNonceProvider);
+
+ }
+
}
private function entrypointFactory(ContainerBuilder $container, string $name, string $path, bool $cacheEnabled, bool $strictMode): Reference
diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml
index 2464c368..9d0204ea 100644
--- a/src/Resources/config/services.xml
+++ b/src/Resources/config/services.xml
@@ -13,11 +13,14 @@
+
+
+
diff --git a/tests/Asset/TagRendererTest.php b/tests/Asset/TagRendererTest.php
index 25131807..f4652ae7 100644
--- a/tests/Asset/TagRendererTest.php
+++ b/tests/Asset/TagRendererTest.php
@@ -14,32 +14,17 @@
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupCollection;
use Symfony\WebpackEncoreBundle\Asset\EntrypointLookupInterface;
use Symfony\WebpackEncoreBundle\Asset\IntegrityDataProviderInterface;
+use Symfony\WebpackEncoreBundle\Asset\NonceNullProvider;
+use Symfony\WebpackEncoreBundle\Asset\NonceProviderInterface;
use Symfony\WebpackEncoreBundle\Asset\TagRenderer;
class TagRendererTest extends TestCase
{
public function testRenderScriptTagsWithDefaultAttributes()
{
- $entrypointLookup = $this->createMock(EntrypointLookupInterface::class);
- $entrypointLookup->expects($this->once())
- ->method('getJavaScriptFiles')
- ->willReturn(['/build/file1.js', '/build/file2.js']);
- $entrypointCollection = $this->createMock(EntrypointLookupCollection::class);
- $entrypointCollection->expects($this->once())
- ->method('getEntrypointLookup')
- ->withConsecutive(['_default'])
- ->will($this->onConsecutiveCalls($entrypointLookup));
+ $entrypointCollection = $this->getMockEntryPointLookup();
- $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;
- });
+ $packages = $this->getMockPackges();
$renderer = new TagRenderer($entrypointCollection, $packages, []);
$output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package');
@@ -69,7 +54,7 @@ public function testRenderScriptTagsWithBadFilename()
$packages->expects($this->once())
->method('getUrl')
->willReturnCallback(function ($path) {
- return 'http://localhost:8080'.$path;
+ return 'http://localhost:8080' . $path;
});
$renderer = new TagRenderer($entrypointCollection, $packages, ['crossorigin' => 'anonymous']);
@@ -115,7 +100,7 @@ public function testRenderScriptTagsWithinAnEntryPointCollection()
['/build/file3.js', 'specific_package']
)
->willReturnCallback(function ($path) {
- return 'http://localhost:8080'.$path;
+ return 'http://localhost:8080' . $path;
});
$renderer = new TagRenderer($entrypointCollection, $packages, ['crossorigin' => 'anonymous']);
@@ -157,16 +142,7 @@ public function testRenderScriptTagsWithHashes()
->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;
- });
+ $packages = $this->getMockPackges();
$renderer = new TagRenderer($entrypointCollection, $packages, ['crossorigin' => 'anonymous']);
$output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package');
@@ -198,7 +174,7 @@ public function testGetRenderedFilesAndReset()
$packages->expects($this->any())
->method('getUrl')
->willReturnCallback(function ($path) {
- return 'http://localhost:8080'.$path;
+ return 'http://localhost:8080' . $path;
});
$renderer = new TagRenderer($entrypointCollection, $packages);
@@ -211,4 +187,95 @@ public function testGetRenderedFilesAndReset()
$this->assertEmpty($renderer->getRenderedScripts());
$this->assertEmpty($renderer->getRenderedStyles());
}
+
+ /**
+ * @dataProvider nonceProvider()
+ */
+ public function testRenderScriptWithNonceNullNonce($nonceProvider): void
+ {
+ $entrypointCollection = $this->getMockEntryPointLookup();
+
+ $packages = $this->getMockPackges();
+
+ $renderer = new TagRenderer($entrypointCollection, $packages, [], $nonceProvider);
+
+ $output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package');
+ $this->assertStringContainsString(
+ '',
+ $output
+ );
+
+ $this->assertStringContainsString(
+ '',
+ $output
+ );
+
+ }
+
+ public function testRenderScriptWithCustomNonce(): void
+ {
+ $entrypointCollection = $this->getMockEntryPointLookup();
+
+ $packages = $this->getMockPackges();
+
+ $customNonce = (new class implements NonceProviderInterface {
+ /**
+ * @inheritDoc
+ */
+ public function getNonceValue(): string
+ {
+ return '123456-nonce';
+ }
+ });
+
+ $renderer = new TagRenderer($entrypointCollection, $packages, [], $customNonce);
+
+ $output = $renderer->renderWebpackScriptTags('my_entry', 'custom_package');
+ $this->assertStringContainsString(
+ '',
+ $output
+ );
+
+ $this->assertStringContainsString(
+ '',
+ $output
+ );
+
+ }
+
+
+ private function getMockEntryPointLookup()
+ {
+ $entrypointLookup = $this->createMock(EntrypointLookupInterface::class);
+ $entrypointLookup->expects($this->once())
+ ->method('getJavaScriptFiles')
+ ->willReturn(['/build/file1.js', '/build/file2.js']);
+ $entrypointCollection = $this->createMock(EntrypointLookupCollection::class);
+ $entrypointCollection->expects($this->once())
+ ->method('getEntrypointLookup')
+ ->withConsecutive(['_default'])
+ ->will($this->onConsecutiveCalls($entrypointLookup));
+ return $entrypointCollection;
+ }
+
+ private function getMockPackges()
+ {
+ $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;
+ });
+ return $packages;
+ }
+
+ public function nonceProvider(): \Generator
+ {
+ yield [ new NonceNullProvider()];
+ yield [ null ];
+ }
}