Forward window.onerror into the UncaughtExceptionHandler.

This change allows GWT to trap window.onerror and forward
errors to the uncaught exception handler. This is necessary
for libraries like elemental2 so that users do not constantly
have to call the entry function manually.
Right now this is still disabled and a subsequent change will
enable it by default.

For this change there are three modes:
 - Trap window on error at all times and forward
 - Only trap window.onerror if there is no current handler
 - Do not trap anything (old behavior)

With this change some applications will see more calls
to their UncaughtExceptionHandler since these would get
ignored beforehand, but we strongly suggest that this is
still the right way forward and users should fix those errors.
For the transition period we added the above switches.

Change-Id: I54f979c1887f44791f5bd2f32190b692f421fcce
Review-Link: https://gwt-review.googlesource.com/#/c/18540/
diff --git a/user/src/com/google/gwt/core/Core.gwt.xml b/user/src/com/google/gwt/core/Core.gwt.xml
index 67ab57f..cff43a5 100644
--- a/user/src/com/google/gwt/core/Core.gwt.xml
+++ b/user/src/com/google/gwt/core/Core.gwt.xml
@@ -83,6 +83,17 @@
   <define-property name="jre.logging.simpleConsoleHandler" values="ENABLED, DISABLED" />
   <set-property name="jre.logging.simpleConsoleHandler" value="ENABLED"/>
 
+  <!--
+   Configures if the UncaughtExceptionHandler will trap errors coming from
+   listening on window.error.
+   Three different flags are:
+     - Trap window on error at all times and forward (REPORT)
+     - Only trap window.onerror if there is no current window.onerror handler (REPORT_IF_NO_HANDLER)
+     - Do not trap anything (legacy behavior) (IGNORE)
+  -->
+  <define-property name="gwt.uncaughtexceptionhander.windowonerror" values="IGNORE, REPORT, REPORT_IF_NO_HANDLER" />
+  <set-property name="gwt.uncaughtexceptionhander.windowonerror" value="IGNORE"/>
+
   <!-- To support legacy logging flags -->
   <inherits name="com.google.gwt.logging.LogImpl"/>
 </module>
diff --git a/user/src/com/google/gwt/core/client/GWT.java b/user/src/com/google/gwt/core/client/GWT.java
index 037ecaa..bdcd1ca 100644
--- a/user/src/com/google/gwt/core/client/GWT.java
+++ b/user/src/com/google/gwt/core/client/GWT.java
@@ -295,6 +295,10 @@
   public static void setUncaughtExceptionHandler(
       UncaughtExceptionHandler handler) {
     uncaughtExceptionHandler = handler;
+    // Dev mode does not do this
+    if (GWT.isScript() && handler != null) {
+      Impl.maybeInitializeWindowOnError();
+    }
   }
 
   /**
diff --git a/user/src/com/google/gwt/core/client/impl/Impl.java b/user/src/com/google/gwt/core/client/impl/Impl.java
index 7236f13..5ca81a0 100644
--- a/user/src/com/google/gwt/core/client/impl/Impl.java
+++ b/user/src/com/google/gwt/core/client/impl/Impl.java
@@ -18,6 +18,7 @@
 import com.google.gwt.core.client.Duration;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
+import com.google.gwt.core.client.GwtScriptOnly;
 import com.google.gwt.core.client.JavaScriptException;
 import com.google.gwt.core.client.JavaScriptObject;
 
@@ -169,7 +170,76 @@
     uncaughtExceptionHandlerForTest = handler;
   }
 
+  private static boolean onErrorInitialized;
+
+  public static void maybeInitializeWindowOnError() {
+    if ("IGNORE".equals(System.getProperty("gwt.uncaughtexceptionhander.windowonerror"))) {
+      return;
+    }
+    if (onErrorInitialized) {
+      return;
+    }
+    onErrorInitialized = true;
+    boolean alwaysReport =
+        "REPORT"
+            .equals(System.getProperty("gwt.uncaughtexceptionhander.windowonerror"));
+    registerWindowOnError(alwaysReport);
+  }
+
+  // Make sure dev mode does not try to parse the JSNI method since it contains a reference to
+  // Throwable.of which is not standard Java
+  @SuppressWarnings("deprecation")
+  @GwtScriptOnly
+  public static native void registerWindowOnError(boolean reportAlways) /*-{
+    function errorHandler(msg, url, line, column, error) {
+      // IE8, IE9, IE10, safari 9, do not have an error passed
+      if (!error) {
+        error = msg + " (" + url + ":" + line
+        // IE8 and IE9 do not have the column number
+        if (column) {
+          error += ":" + column
+        }
+        error += ")";
+      }
+
+      var throwable = @java.lang.Throwable::of(*)(error);
+      @Impl::reportWindowOnError(*)(throwable);
+    };
+
+    function addOnErrorHandler(windowRef) {
+      var origHandler = windowRef.onerror;
+      if (origHandler && !reportAlways) {
+        return;
+      }
+
+      windowRef.onerror = function() {
+        errorHandler.apply(this, arguments);
+        if (origHandler) {
+          origHandler.apply(this, arguments);
+        }
+        return false;
+      };
+    }
+
+    // Note we need to trap both window.onerror and $wnd.onerror
+    // Chrome 58 & Safari (10.1) & HtmlUnit uses $wnd.error,
+    // while FF (53) /IE (even edge) listens on window.error
+    addOnErrorHandler($wnd);
+    addOnErrorHandler(window);
+  }-*/;
+
+  private static void reportWindowOnError(Throwable e) {
+    // If the error is coming from window.onerror that we registered on, we can not report it
+    // back to the browser since we would end up being called again
+    reportUncaughtException(e, false);
+  }
+
   public static void reportUncaughtException(Throwable e) {
+    reportUncaughtException(e, true);
+  }
+
+  private static void reportUncaughtException(
+      Throwable e, boolean reportSwallowedExceptionToBrowser) {
     if (Impl.uncaughtExceptionHandlerForTest != null) {
       Impl.uncaughtExceptionHandlerForTest.onUncaughtException(e);
     }
@@ -188,7 +258,7 @@
     }
 
     // Make sure that the exception is not swallowed
-    if (GWT.isClient()) {
+    if (GWT.isClient() && reportSwallowedExceptionToBrowser) {
       reportToBrowser(e);
     } else {
       System.err.print("Uncaught exception ");
diff --git a/user/src/com/google/gwt/jsonp/client/JsonpRequest.java b/user/src/com/google/gwt/jsonp/client/JsonpRequest.java
index a90e822..4b410d4 100644
--- a/user/src/com/google/gwt/jsonp/client/JsonpRequest.java
+++ b/user/src/com/google/gwt/jsonp/client/JsonpRequest.java
@@ -329,7 +329,7 @@
           // don't want to unregister the callback since there may be pending
           // requests that have not yet come back and we don't want them to
           // have an undefined callback function.
-          unregisterCallbacks(CALLBACKS);
+          unregisterCallbacks(CALLBACKS, callbackId);
         }
         Node script = Document.get().getElementById(callbackId);
         if (script != null) {
@@ -340,7 +340,7 @@
     });
   }
 
-  private native void unregisterCallbacks(JavaScriptObject callbacks) /*-{
-    delete callbacks[this.@com.google.gwt.jsonp.client.JsonpRequest::callbackId];
+  private native void unregisterCallbacks(JavaScriptObject callbacks, String callbackId) /*-{
+    callbacks[callbackId].onSuccess = callbacks[callbackId].onFailure = function() {};
   }-*/;
 }
diff --git a/user/test/com/google/gwt/core/CoreSuite.java b/user/test/com/google/gwt/core/CoreSuite.java
index aef8f7c..49e051c 100644
--- a/user/test/com/google/gwt/core/CoreSuite.java
+++ b/user/test/com/google/gwt/core/CoreSuite.java
@@ -25,6 +25,7 @@
 import com.google.gwt.core.client.JsonUtilsTest;
 import com.google.gwt.core.client.SchedulerTest;
 import com.google.gwt.core.client.ScriptInjectorTest;
+import com.google.gwt.core.client.WindowOnErrorTest;
 import com.google.gwt.core.client.impl.ImplTest;
 import com.google.gwt.core.client.impl.SchedulerImplTest;
 import com.google.gwt.core.client.impl.StackTraceCreatorCollectorTest;
@@ -62,6 +63,7 @@
     suite.addTestSuite(StackTraceEmulTest.class);
     suite.addTestSuite(StackTraceNativeTest.class);
     suite.addTestSuite(StackTraceStripTest.class);
+    suite.addTestSuite(WindowOnErrorTest.class);
 
     // Uncomment to print native stack traces for different platforms
     // suite.addTestSuite(com.google.gwt.core.client.impl.StackTraceGenerator.class);
diff --git a/user/test/com/google/gwt/core/WindowOnError.gwt.xml b/user/test/com/google/gwt/core/WindowOnError.gwt.xml
new file mode 100644
index 0000000..0f34c54
--- /dev/null
+++ b/user/test/com/google/gwt/core/WindowOnError.gwt.xml
@@ -0,0 +1,18 @@
+<!--                                                                        -->
+<!-- Copyright 2017 Google Inc.                                             -->
+<!-- Licensed under the Apache License, Version 2.0 (the "License"); you    -->
+<!-- may not use this file except in compliance with the License. You may   -->
+<!-- may obtain a copy of the License at                                    -->
+<!--                                                                        -->
+<!-- http://www.apache.org/licenses/LICENSE-2.0                             -->
+<!--                                                                        -->
+<!-- Unless required by applicable law or agreed to in writing, software    -->
+<!-- distributed under the License is distributed on an "AS IS" BASIS,      -->
+<!-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or        -->
+<!-- implied. License for the specific language governing permissions and   -->
+<!-- limitations under the License.                                         -->
+
+<module>
+  <inherits name="com.google.gwt.core.Core" />
+  <set-property name="gwt.uncaughtexceptionhander.windowonerror" value="REPORT_IF_NO_HANDLER"/>
+</module>
diff --git a/user/test/com/google/gwt/core/client/WindowOnErrorTest.java b/user/test/com/google/gwt/core/client/WindowOnErrorTest.java
new file mode 100644
index 0000000..6752b84
--- /dev/null
+++ b/user/test/com/google/gwt/core/client/WindowOnErrorTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.core.client;
+
+import com.google.gwt.junit.DoNotRunWith;
+import com.google.gwt.junit.Platform;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.Timer;
+
+/** Test for window.onerror reporting to {@link UncaughtExceptionHandler}. */
+public class WindowOnErrorTest extends GWTTestCase {
+
+  private int reportedJsExceptionCount;
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.core.WindowOnError";
+  }
+
+  @Override
+  protected void reportUncaughtException(Throwable ex) {
+    // We need to distinguish here between the exceptions that we are deliberately creating
+    // and ones that should make the test fail.
+    // All our exceptions will contain "from_js" in their message
+    if (ex.getMessage().contains("from_js")) {
+      // Do not let the test fail
+      reportedJsExceptionCount++;
+      return;
+    }
+    super.reportUncaughtException(ex);
+  }
+
+  // Does not work in dev mode, since JNSI code for setting up window.onerror needs Throwable.of
+  // from super sourced code.
+  @DoNotRunWith({Platform.Devel})
+  public void testFailViaWindowOnError() {
+    delayTestFinish(2000);
+
+    new Timer() {
+      @Override
+      public void run() {
+        assertEquals(1, reportedJsExceptionCount);
+        finishTest();
+      }
+    }.schedule(1000);
+
+    throwInNonEntryMethod();
+  }
+
+  private native void throwInNonEntryMethod() /*-{
+    $wnd.setTimeout(function() {
+      throw new Error("from_js");
+    }, 0);
+  }-*/;
+}