diff --git a/src/React.AspNet/HtmlHelperExtensions.cs b/src/React.AspNet/HtmlHelperExtensions.cs
index b83f1ad6c..a3d796eb2 100644
--- a/src/React.AspNet/HtmlHelperExtensions.cs
+++ b/src/React.AspNet/HtmlHelperExtensions.cs
@@ -1,4 +1,4 @@
-/*
+/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
@@ -8,14 +8,14 @@
*/
using System;
-using React.Exceptions;
-using React.TinyIoC;
+using System.IO;
#if LEGACYASPNET
using System.Web;
using System.Web.Mvc;
using IHtmlHelper = System.Web.Mvc.HtmlHelper;
#else
+using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Rendering;
using IHtmlString = Microsoft.AspNetCore.Html.IHtmlContent;
using Microsoft.AspNetCore.Html;
@@ -129,16 +129,7 @@ public static IHtmlString ReactWithInit(
}
var html = reactComponent.RenderHtml(clientOnly, exceptionHandler: exceptionHandler);
-#if LEGACYASPNET
- var script = new TagBuilder("script")
- {
- InnerHtml = reactComponent.RenderJavaScript()
- };
-#else
- var script = new TagBuilder("script");
- script.InnerHtml.AppendHtml(reactComponent.RenderJavaScript());
-#endif
- return new HtmlString(html + System.Environment.NewLine + script.ToString());
+ return new HtmlString(html + System.Environment.NewLine + RenderToString(GetScriptTag(reactComponent.RenderJavaScript())));
}
finally
{
@@ -155,23 +146,53 @@ public static IHtmlString ReactInitJavaScript(this IHtmlHelper htmlHelper, bool
{
try
{
- var script = Environment.GetInitJavaScript(clientOnly);
+ return GetScriptTag(Environment.GetInitJavaScript(clientOnly));
+ }
+ finally
+ {
+ Environment.ReturnEngineToPool();
+ }
+ }
+
+ private static IHtmlString GetScriptTag(string script)
+ {
#if LEGACYASPNET
- var tag = new TagBuilder("script")
- {
- InnerHtml = script
- };
- return new HtmlString(tag.ToString());
+ var tag = new TagBuilder("script")
+ {
+ InnerHtml = script,
+ };
+
+ if (Environment.Configuration.ScriptNonceProvider != null)
+ {
+ tag.Attributes.Add("nonce", Environment.Configuration.ScriptNonceProvider());
+ }
+
+ return new HtmlString(tag.ToString());
#else
var tag = new TagBuilder("script");
tag.InnerHtml.AppendHtml(script);
+
+ if (Environment.Configuration.ScriptNonceProvider != null)
+ {
+ tag.Attributes.Add("nonce", Environment.Configuration.ScriptNonceProvider());
+ }
+
return tag;
#endif
- }
- finally
+ }
+
+ // In ASP.NET Core, you can no longer call `.ToString` on `IHtmlString`
+ private static string RenderToString(IHtmlString source)
+ {
+#if LEGACYASPNET
+ return source.ToString();
+#else
+ using (var writer = new StringWriter())
{
- Environment.ReturnEngineToPool();
+ source.WriteTo(writer, HtmlEncoder.Default);
+ return writer.ToString();
}
+#endif
}
}
}
diff --git a/src/React.Core/IReactEnvironment.cs b/src/React.Core/IReactEnvironment.cs
index 599ffd1f8..67991360f 100644
--- a/src/React.Core/IReactEnvironment.cs
+++ b/src/React.Core/IReactEnvironment.cs
@@ -1,4 +1,4 @@
-/*
+/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
@@ -7,7 +7,6 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
-using System;
namespace React
{
@@ -110,5 +109,10 @@ public interface IReactEnvironment
/// Returns the currently held JS engine to the pool. (no-op if engine pooling is disabled)
///
void ReturnEngineToPool();
+
+ ///
+ /// Gets the site-wide configuration.
+ ///
+ IReactSiteConfiguration Configuration { get; }
}
}
diff --git a/src/React.Core/IReactSiteConfiguration.cs b/src/React.Core/IReactSiteConfiguration.cs
index 8c3eed6da..b414e5d25 100644
--- a/src/React.Core/IReactSiteConfiguration.cs
+++ b/src/React.Core/IReactSiteConfiguration.cs
@@ -8,8 +8,8 @@
*/
using System;
-using Newtonsoft.Json;
using System.Collections.Generic;
+using Newtonsoft.Json;
namespace React
{
@@ -193,5 +193,19 @@ public interface IReactSiteConfiguration
///
///
IReactSiteConfiguration SetExceptionHandler(Action handler);
+
+ ///
+ /// A provider that returns a nonce to be used on any script tags on the page.
+ /// This value must match the nonce used in the Content Security Policy header on the response.
+ ///
+ Func ScriptNonceProvider { get; set; }
+
+ ///
+ /// Sets a provider that returns a nonce to be used on any script tags on the page.
+ /// This value must match the nonce used in the Content Security Policy header on the response.
+ ///
+ ///
+ ///
+ IReactSiteConfiguration SetScriptNonceProvider(Func provider);
}
}
diff --git a/src/React.Core/ReactSiteConfiguration.cs b/src/React.Core/ReactSiteConfiguration.cs
index 879a92db9..dd735b1c9 100644
--- a/src/React.Core/ReactSiteConfiguration.cs
+++ b/src/React.Core/ReactSiteConfiguration.cs
@@ -326,5 +326,23 @@ public IReactSiteConfiguration SetExceptionHandler(Action
+ /// A provider that returns a nonce to be used on any script tags on the page.
+ /// This value must match the nonce used in the Content Security Policy header on the response.
+ ///
+ public Func ScriptNonceProvider { get; set; }
+
+ ///
+ /// Sets a provider that returns a nonce to be used on any script tags on the page.
+ /// This value must match the nonce used in the Content Security Policy header on the response.
+ ///
+ ///
+ ///
+ public IReactSiteConfiguration SetScriptNonceProvider(Func provider)
+ {
+ ScriptNonceProvider = provider;
+ return this;
+ }
}
}
diff --git a/src/React.Sample.Mvc4/Content/Sample.jsx b/src/React.Sample.Mvc4/Content/Sample.jsx
index 8875ef8f1..97d078f09 100644
--- a/src/React.Sample.Mvc4/Content/Sample.jsx
+++ b/src/React.Sample.Mvc4/Content/Sample.jsx
@@ -1,4 +1,4 @@
-/**
+/**
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
@@ -9,8 +9,8 @@
class CommentsBox extends React.Component {
static propTypes = {
- initialComments: React.PropTypes.array.isRequired,
- page: React.PropTypes.number
+ initialComments: PropTypes.array.isRequired,
+ page: PropTypes.number
};
state = {
@@ -76,7 +76,7 @@ class CommentsBox extends React.Component {
class Comment extends React.Component {
static propTypes = {
- author: React.PropTypes.object.isRequired
+ author: PropTypes.object.isRequired
};
render() {
@@ -92,7 +92,7 @@ class Comment extends React.Component {
class Avatar extends React.Component {
static propTypes = {
- author: React.PropTypes.object.isRequired
+ author: PropTypes.object.isRequired
};
render() {
diff --git a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
index 1b2a59619..b1937b58c 100644
--- a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
+++ b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
@@ -7,9 +7,11 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/
+using System;
+using System.Security.Cryptography;
using Moq;
-using Xunit;
using React.Web.Mvc;
+using Xunit;
namespace React.Tests.Mvc
{
@@ -20,9 +22,10 @@ public class HtmlHelperExtensionsTests
/// This is only required because can not be
/// injected :(
///
- private Mock ConfigureMockEnvironment()
+ private Mock ConfigureMockEnvironment(IReactSiteConfiguration configuration = null)
{
var environment = new Mock();
+ environment.Setup(x => x.Configuration).Returns(configuration ?? new ReactSiteConfiguration());
AssemblyRegistration.Container.Register(environment.Object);
return environment;
}
@@ -54,6 +57,61 @@ public void ReactWithInitShouldReturnHtmlAndScript()
);
}
+ [Fact]
+ public void ScriptNonceIsReturned()
+ {
+ string nonce;
+ using (var random = new RNGCryptoServiceProvider())
+ {
+ byte[] nonceBytes = new byte[16];
+ random.GetBytes(nonceBytes);
+ nonce = Convert.ToBase64String(nonceBytes);
+ }
+
+ var component = new Mock();
+ component.Setup(x => x.RenderHtml(false, false, null)).Returns("HTML");
+ component.Setup(x => x.RenderJavaScript()).Returns("JS");
+
+ var config = new Mock();
+
+ var environment = ConfigureMockEnvironment(config.Object);
+
+ environment.Setup(x => x.Configuration).Returns(config.Object);
+ environment.Setup(x => x.CreateComponent(
+ "ComponentName",
+ new { },
+ null,
+ false,
+ false
+ )).Returns(component.Object);
+
+ // without nonce
+ var result = HtmlHelperExtensions.ReactWithInit(
+ htmlHelper: null,
+ componentName: "ComponentName",
+ props: new { },
+ htmlTag: "span"
+ );
+ Assert.Equal(
+ "HTML" + System.Environment.NewLine + "",
+ result.ToString()
+ );
+
+ config.Setup(x => x.ScriptNonceProvider).Returns(() => nonce);
+
+ // with nonce
+ result = HtmlHelperExtensions.ReactWithInit(
+ htmlHelper: null,
+ componentName: "ComponentName",
+ props: new { },
+ htmlTag: "span"
+ );
+ Assert.Equal(
+ "HTML" + System.Environment.NewLine + "",
+ result.ToString()
+ );
+ }
+
[Fact]
public void EngineIsReturnedToPoolAfterRender()
{