Propagate script nonces via ScriptInjector

This is necessary to turn on go/csp with GWT, otherwise any script inject will be blocked by the
browser.

This is essentially the same approach as from installScriptAlreadyIncludedCspSafe.js
Apps that do not use CSP will not be impacted as the nonce attribute is ignored

Change-Id: Ic7fd0ee2221cee6e218187d3fb77e49a89b7ff97
diff --git a/user/src/com/google/gwt/core/client/ScriptInjector.java b/user/src/com/google/gwt/core/client/ScriptInjector.java
index 6a63864..5c93590 100644
--- a/user/src/com/google/gwt/core/client/ScriptInjector.java
+++ b/user/src/com/google/gwt/core/client/ScriptInjector.java
@@ -85,6 +85,7 @@
       JavaScriptObject scriptElement = nativeMakeScriptElement(doc);
       assert scriptElement != null;
       nativeSetText(scriptElement, scriptBody);
+      nativePropagateScriptNonceIfPossible(doc, scriptElement);
       nativeAttachToHead(doc, scriptElement);
       if (removeTag) {
         nativeRemove(scriptElement);
@@ -144,6 +145,7 @@
         attachListeners(scriptElement, callback, removeTag);
       }
       nativeSetSrc(scriptElement, scriptUrl);
+      nativePropagateScriptNonceIfPossible(doc, scriptElement);
       nativeAttachToHead(doc, scriptElement);
       return scriptElement;
     }
@@ -314,6 +316,15 @@
     element.text = scriptBody;
   }-*/;
 
+  private static native void nativePropagateScriptNonceIfPossible(
+      JavaScriptObject doc, JavaScriptObject element) /*-{
+    if (doc.querySelector && doc.querySelector('script[nonce]')) {
+      var firstNoncedScript = doc.querySelector('script[nonce]');
+      var nonce = firstNoncedScript['nonce'] || firstNoncedScript.getAttribute('nonce');
+      element.setAttribute('nonce', nonce);
+    }
+  }-*/;
+
   private static native JavaScriptObject nativeTopWindow() /*-{
     return $wnd;
   }-*/;
diff --git a/user/test/com/google/gwt/core/client/ScriptInjectorTest.java b/user/test/com/google/gwt/core/client/ScriptInjectorTest.java
index 1b17008..5b236f2 100644
--- a/user/test/com/google/gwt/core/client/ScriptInjectorTest.java
+++ b/user/test/com/google/gwt/core/client/ScriptInjectorTest.java
@@ -97,6 +97,8 @@
     new FromString(scriptBody).setRemoveTag(false).inject();
     boolean worked = nativeTest3Worked();
     JavaScriptObject scriptElement = findScriptTextInThisWindow(scriptBody);
+    // Since we are not in the top window, there is no nonce.
+    assertNull(nativeGetScriptNonce(scriptElement));
     if (!isIE8Or9()) {
       cleanupThisWindow("__ti3_var__", scriptElement);
       assertFalse("cleanup failed", nativeTest3Worked());
@@ -201,30 +203,35 @@
 
     // We'll check using a callback in another test. This test will poll to see
     // that the script had an effect.
-    Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
-      int numLoops = 0;
+    Scheduler.get()
+        .scheduleFixedDelay(
+            new RepeatingCommand() {
+              int numLoops = 0;
 
-      @Override
-      public boolean execute() {
-        numLoops++;
-        boolean worked = nativeTest4Worked();
-        if (!worked && (numLoops * CHECK_DELAY < TEST_DELAY)) {
-          return true;
-        }
-        JavaScriptObject scriptElement = findScriptUrlInThisWindow(scriptUrl);
-        if (!isIE8Or9()) {
-          cleanupThisWindow("__ti4_var__", scriptElement);
-          assertFalse("cleanup failed", nativeTest4Worked());
-        }
-        assertTrue("__ti4_var not set in this window", worked);
-        assertNotNull("script element 4 not found", scriptElement);
-        assertEquals(injectedElement, scriptElement);
-        finishTest();
+              @Override
+              public boolean execute() {
+                numLoops++;
+                boolean worked = nativeTest4Worked();
+                if (!worked && (numLoops * CHECK_DELAY < TEST_DELAY)) {
+                  return true;
+                }
+                JavaScriptObject scriptElement = findScriptUrlInThisWindow(scriptUrl);
+                // Since we are not in the top window, there is no nonce.
+                assertNull(nativeGetScriptNonce(scriptElement));
+                if (!isIE8Or9()) {
+                  cleanupThisWindow("__ti4_var__", scriptElement);
+                  assertFalse("cleanup failed", nativeTest4Worked());
+                }
+                assertTrue("__ti4_var not set in this window", worked);
+                assertNotNull("script element 4 not found", scriptElement);
+                assertEquals(injectedElement, scriptElement);
+                finishTest();
 
-        // never reached
-        return false;
-      }
-    }, CHECK_DELAY);
+                // never reached
+                return false;
+              }
+            },
+            CHECK_DELAY);
     assertNotNull(injectedElement);
   }
 
@@ -235,27 +242,33 @@
     delayTestFinish(TEST_DELAY);
     final String scriptUrl = "script_injector_test5.js";
     assertFalse(nativeTest5Worked());
-    JavaScriptObject injectedElement = ScriptInjector.fromUrl(scriptUrl).setRemoveTag(false)
-        .setCallback(new Callback<Void, Exception>() {
-          @Override
-          public void onFailure(Exception reason) {
-            assertNotNull(reason);
-            fail("Injection failed: " + reason.toString());
-          }
+    JavaScriptObject injectedElement =
+        ScriptInjector.fromUrl(scriptUrl)
+            .setRemoveTag(false)
+            .setCallback(
+                new Callback<Void, Exception>() {
+                  @Override
+                  public void onFailure(Exception reason) {
+                    assertNotNull(reason);
+                    fail("Injection failed: " + reason.toString());
+                  }
 
-          @Override
-          public void onSuccess(Void result) {
-            boolean worked = nativeTest5Worked();
-            JavaScriptObject scriptElement = findScriptUrlInThisWindow(scriptUrl);
-            if (!isIE8Or9()) {
-              cleanupThisWindow("__ti5_var__", scriptElement);
-              assertFalse("cleanup failed", nativeTest5Worked());
-            }
-            assertTrue("__ti5_var not set in this window", worked);
-            assertNotNull("script element 5 not found", scriptElement);
-            finishTest();
-          }
-        }).inject();
+                  @Override
+                  public void onSuccess(Void result) {
+                    boolean worked = nativeTest5Worked();
+                    JavaScriptObject scriptElement = findScriptUrlInThisWindow(scriptUrl);
+                    // Since we are not in the top window, there is no nonce.
+                    assertNull(nativeGetScriptNonce(scriptElement));
+                    if (!isIE8Or9()) {
+                      cleanupThisWindow("__ti5_var__", scriptElement);
+                      assertFalse("cleanup failed", nativeTest5Worked());
+                    }
+                    assertTrue("__ti5_var not set in this window", worked);
+                    assertNotNull("script element 5 not found", scriptElement);
+                    finishTest();
+                  }
+                })
+            .inject();
     assertNotNull(injectedElement);
   }
 
@@ -269,29 +282,33 @@
         .setWindow(ScriptInjector.TOP_WINDOW).inject();
     // We'll check using a callback in another test. This test will poll to see
     // that the script had an effect.
-    Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
-      int numLoops = 0;
+    Scheduler.get()
+        .scheduleFixedDelay(
+            new RepeatingCommand() {
+              int numLoops = 0;
 
-      @Override
-      public boolean execute() {
-        numLoops++;
+              @Override
+              public boolean execute() {
+                numLoops++;
 
-        boolean worked = nativeTest6Worked();
-        if (!worked && (numLoops * CHECK_DELAY < TEST_DELAY)) {
-          return true;
-        }
-        JavaScriptObject scriptElement = findScriptUrlInTopWindow(scriptUrl);
-        if (!isIE8Or9()) {
-          cleanupTopWindow("__ti6_var__", scriptElement);
-          assertFalse("cleanup failed", nativeTest6Worked());
-        }
-        assertTrue("__ti6_var not set in top window", worked);
-        assertNotNull("script element 6 not found", scriptElement);
-        finishTest();
-        // never reached
-        return false;
-      }
-    }, CHECK_DELAY);
+                boolean worked = nativeTest6Worked();
+                if (!worked && (numLoops * CHECK_DELAY < TEST_DELAY)) {
+                  return true;
+                }
+                JavaScriptObject scriptElement = findScriptUrlInTopWindow(scriptUrl);
+                assertDefaultNonce(scriptElement);
+                if (!isIE8Or9()) {
+                  cleanupTopWindow("__ti6_var__", scriptElement);
+                  assertFalse("cleanup failed", nativeTest6Worked());
+                }
+                assertTrue("__ti6_var not set in top window", worked);
+                assertNotNull("script element 6 not found", scriptElement);
+                finishTest();
+                // never reached
+                return false;
+              }
+            },
+            CHECK_DELAY);
     assertNotNull(injectedElement);
   }
 
@@ -302,29 +319,34 @@
     delayTestFinish(TEST_DELAY);
     final String scriptUrl = "script_injector_test7.js";
     assertFalse(nativeTest7Worked());
-    JavaScriptObject injectedElement = ScriptInjector.fromUrl(scriptUrl).setRemoveTag(false)
-        .setWindow(ScriptInjector.TOP_WINDOW).setCallback(
-            new Callback<Void, Exception>() {
+    JavaScriptObject injectedElement =
+        ScriptInjector.fromUrl(scriptUrl)
+            .setRemoveTag(false)
+            .setWindow(ScriptInjector.TOP_WINDOW)
+            .setCallback(
+                new Callback<Void, Exception>() {
 
-              @Override
-              public void onFailure(Exception reason) {
-                assertNotNull(reason);
-                fail("Injection failed: " + reason.toString());
-              }
+                  @Override
+                  public void onFailure(Exception reason) {
+                    assertNotNull(reason);
+                    fail("Injection failed: " + reason.toString());
+                  }
 
-              @Override
-              public void onSuccess(Void result) {
-                boolean worked = nativeTest7Worked();
-                JavaScriptObject scriptElement = findScriptUrlInTopWindow(scriptUrl);
-                if (!isIE8Or9()) {
-                  cleanupTopWindow("__ti7_var__", scriptElement);
-                  assertFalse("cleanup failed", nativeTest7Worked());
-                }
-                assertTrue("__ti7_var not set in top window", worked);
-                assertNotNull("script element 7 not found", scriptElement);
-                finishTest();
-              }
-            }).inject();
+                  @Override
+                  public void onSuccess(Void result) {
+                    boolean worked = nativeTest7Worked();
+                    JavaScriptObject scriptElement = findScriptUrlInTopWindow(scriptUrl);
+                    assertDefaultNonce(scriptElement);
+                    if (!isIE8Or9()) {
+                      cleanupTopWindow("__ti7_var__", scriptElement);
+                      assertFalse("cleanup failed", nativeTest7Worked());
+                    }
+                    assertTrue("__ti7_var not set in top window", worked);
+                    assertNotNull("script element 7 not found", scriptElement);
+                    finishTest();
+                  }
+                })
+            .inject();
     assertNotNull(injectedElement);
   }
 
@@ -335,29 +357,115 @@
     delayTestFinish(TEST_DELAY);
     final String scriptUrl = "script_injector_test_utf8.js";
     assertEquals("", nativeGetTestUtf8Var());
-    JavaScriptObject injectedElement = ScriptInjector.fromUrl(scriptUrl).setRemoveTag(false)
-        .setWindow(ScriptInjector.TOP_WINDOW).setCallback(
-            new Callback<Void, Exception>() {
+    JavaScriptObject injectedElement =
+        ScriptInjector.fromUrl(scriptUrl)
+            .setRemoveTag(false)
+            .setWindow(ScriptInjector.TOP_WINDOW)
+            .setCallback(
+                new Callback<Void, Exception>() {
 
-              @Override
-              public void onFailure(Exception reason) {
-                assertNotNull(reason);
-                fail("Injection failed: " + reason.toString());
-              }
+                  @Override
+                  public void onFailure(Exception reason) {
+                    assertNotNull(reason);
+                    fail("Injection failed: " + reason.toString());
+                  }
 
-              @Override
-              public void onSuccess(Void result) {
-                String testVar = nativeGetTestUtf8Var();
-                JavaScriptObject scriptElement = findScriptUrlInTopWindow(scriptUrl);
-                if (!isIE8Or9()) {
-                  cleanupTopWindow("__ti_utf8_var__", scriptElement);
-                  assertEquals("cleanup failed", "", nativeGetTestUtf8Var());
-                }
-                assertEquals("__ti_utf8_var not set in top window", "à", testVar);
-                assertNotNull("script element not found", scriptElement);
-                finishTest();
-              }
-            }).inject();
+                  @Override
+                  public void onSuccess(Void result) {
+                    String testVar = nativeGetTestUtf8Var();
+                    JavaScriptObject scriptElement = findScriptUrlInTopWindow(scriptUrl);
+                    assertDefaultNonce(scriptElement);
+                    if (!isIE8Or9()) {
+                      cleanupTopWindow("__ti_utf8_var__", scriptElement);
+                      assertEquals("cleanup failed", "", nativeGetTestUtf8Var());
+                    }
+                    assertEquals("__ti_utf8_var not set in top window", "à", testVar);
+                    assertNotNull("script element not found", scriptElement);
+                    finishTest();
+                  }
+                })
+            .inject();
+    assertNotNull(injectedElement);
+  }
+
+  public void testInjectUrlTopWindowNoncePropegated() {
+    delayTestFinish(TEST_DELAY);
+    final String scriptUrl = "script_injector_test9.js";
+    final String nonceScriptUrl = "script_injector_test9_nonce.js";
+    String nonce = "IAlwaysGetTheShemp";
+    JavaScriptObject noncedScript =
+        nativeAddScriptWithNonceToWindow(nativeTopWindow(), nonce, nonceScriptUrl);
+    assertEquals(nonce, nativeGetScriptNonce(noncedScript));
+    assertFalse(nativeTest9Worked());
+    JavaScriptObject injectedElement =
+        ScriptInjector.fromUrl(scriptUrl)
+            .setRemoveTag(false)
+            .setWindow(ScriptInjector.TOP_WINDOW)
+            .setCallback(
+                new Callback<Void, Exception>() {
+
+                  @Override
+                  public void onFailure(Exception reason) {
+                    assertNotNull(reason);
+                    fail("Injection failed: " + reason.toString());
+                  }
+
+                  @Override
+                  public void onSuccess(Void result) {
+                    boolean worked = nativeTest9Worked();
+                    JavaScriptObject scriptElement = findScriptUrlInTopWindow(scriptUrl);
+                    assertEquals(nonce, nativeGetScriptNonce(scriptElement));
+                    if (!isIE8Or9()) {
+                      cleanupTopWindow("__ti9_var__", scriptElement);
+                      cleanupTopWindow("__ti9_nonce_var__", noncedScript);
+                      assertFalse("cleanup failed", nativeTest9Worked());
+                    }
+                    assertTrue("__ti9_var not set in top window", worked);
+                    assertNotNull("script element 9 not found", scriptElement);
+                    finishTest();
+                  }
+                })
+            .inject();
+    assertNotNull(injectedElement);
+  }
+
+  public void testInjectUrlThisWindowNoncePropegated() {
+    delayTestFinish(TEST_DELAY);
+    final String scriptUrl = "script_injector_test10.js";
+    final String nonceScriptUrl = "script_injector_test10_nonce.js";
+    String nonce = "ANearbyRoosterIsInHighDef";
+    JavaScriptObject noncedScript =
+        nativeAddScriptWithNonceToWindow(nativeThisWindow(), nonce, nonceScriptUrl);
+    assertEquals(nonce, nativeGetScriptNonce(noncedScript));
+    assertFalse(nativeTest10Worked());
+    JavaScriptObject injectedElement =
+        ScriptInjector.fromUrl(scriptUrl)
+            .setRemoveTag(false)
+            .setCallback(
+                new Callback<Void, Exception>() {
+
+                  @Override
+                  public void onFailure(Exception reason) {
+                    assertNotNull(reason);
+                    fail("Injection failed: " + reason.toString());
+                  }
+
+                  @Override
+                  public void onSuccess(Void result) {
+                    boolean worked = nativeTest10Worked();
+                    JavaScriptObject scriptElement = findScriptUrlInThisWindow(scriptUrl);
+                    assertEquals(nonce, nativeGetScriptNonce(scriptElement));
+                    if (!isIE8Or9()) {
+                      cleanupThisWindow("__ti10_var__", scriptElement);
+                      cleanupThisWindow("__ti10_nonce_var__", noncedScript);
+                      assertFalse("cleanup failed", nativeTest10Worked());
+                    }
+                    assertTrue("__ti10_var not set in top window", worked);
+                    assertNotNull("script element 10 not found", scriptElement);
+                    finishTest();
+                  }
+                })
+            .inject();
     assertNotNull(injectedElement);
   }
 
@@ -377,6 +485,23 @@
     }
   }-*/;
 
+  private void assertDefaultNonce(JavaScriptObject scriptElement) {
+    assertEquals(
+        "Expected script to have the default nonce value",
+        "gwt-nonce",
+        nativeGetScriptNonce(scriptElement));
+  }
+
+  private native JavaScriptObject nativeAddScriptWithNonceToWindow(
+      JavaScriptObject wnd, String nonce, String scriptUrl) /*-{
+    var scriptToAdd = wnd.document.createElement('script');
+    scriptToAdd.setAttribute('nonce', nonce);
+    scriptToAdd.src = scriptUrl;
+    var head = wnd.document.head || wnd.document.getElementsByTagName("head")[0];
+    head.insertBefore(scriptToAdd, head.firstChild);
+    return scriptToAdd;
+  }-*/;
+
   private JavaScriptObject findScriptTextInThisWindow(String text) {
     return nativeFindScriptText(nativeThisWindow(), text);
   }
@@ -420,6 +545,10 @@
     return null;
   }-*/;
 
+  private native String nativeGetScriptNonce(JavaScriptObject scriptElement) /*-{
+    return scriptElement['nonce'] || scriptElement.getAttribute('nonce');
+  }-*/;
+
   private native boolean nativeInjectUrlAbsoluteWorked() /*-{
     return !!window["__tiabsolute_var__"] && window["__tiabsolute_var__"] == 101;
   }-*/;
@@ -456,6 +585,14 @@
     return $wnd["__ti_utf8_var__"] || "";
   }-*/;
 
+  private native boolean nativeTest9Worked() /*-{
+    return !!$wnd["__ti9_var__"] && $wnd["__ti9_var__"] == 9;
+  }-*/;
+
+  private native boolean nativeTest10Worked() /*-{
+    return !!window["__ti10_var__"] && window["__ti10_var__"] == 10;
+  }-*/;
+
   private native JavaScriptObject nativeThisWindow() /*-{
     return window;
   }-*/;
diff --git a/user/test/com/google/gwt/core/public/script_injector_test10.js b/user/test/com/google/gwt/core/public/script_injector_test10.js
new file mode 100644
index 0000000..c29de24
--- /dev/null
+++ b/user/test/com/google/gwt/core/public/script_injector_test10.js
@@ -0,0 +1 @@
+__ti10_var__ = 10;
diff --git a/user/test/com/google/gwt/core/public/script_injector_test10_nonce.js b/user/test/com/google/gwt/core/public/script_injector_test10_nonce.js
new file mode 100644
index 0000000..25a8b7b
--- /dev/null
+++ b/user/test/com/google/gwt/core/public/script_injector_test10_nonce.js
@@ -0,0 +1 @@
+__ti10_nonce_var__ = 9;
diff --git a/user/test/com/google/gwt/core/public/script_injector_test9.js b/user/test/com/google/gwt/core/public/script_injector_test9.js
new file mode 100644
index 0000000..028dc8f
--- /dev/null
+++ b/user/test/com/google/gwt/core/public/script_injector_test9.js
@@ -0,0 +1 @@
+__ti9_var__ = 9;
diff --git a/user/test/com/google/gwt/core/public/script_injector_test9_nonce.js b/user/test/com/google/gwt/core/public/script_injector_test9_nonce.js
new file mode 100644
index 0000000..ec59b72
--- /dev/null
+++ b/user/test/com/google/gwt/core/public/script_injector_test9_nonce.js
@@ -0,0 +1 @@
+__ti9_nonce_var__ = 9;