View Javadoc
1   /*
2    * Copyright (c) 2002-2017 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package com.gargoylesoftware.htmlunit.javascript;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_ARRAY_FROM;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_ERROR_CAPTURE_STACK_TRACE;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_ERROR_STACK_TRACE_LIMIT;
20  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_FUNCTION_TOSOURCE;
21  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_IMAGE_PROTOTYPE_SAME_AS_HTML_IMAGE;
22  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_Iterator;
23  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_OBJECT_GET_OWN_PROPERTY_SYMBOLS;
24  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_REFLECT;
25  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_WINDOW_ACTIVEXOBJECT_HIDDEN;
26  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_XML;
27  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.STRING_CONTAINS;
28  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.STRING_INCLUDES;
29  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.STRING_REPEAT;
30  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.STRING_STARTS_ENDS_WITH;
31  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.STRING_TRIM_LEFT_RIGHT;
32  
33  import java.io.IOException;
34  import java.io.ObjectInputStream;
35  import java.lang.reflect.Member;
36  import java.lang.reflect.Method;
37  import java.util.ArrayList;
38  import java.util.HashMap;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Map.Entry;
42  import java.util.Stack;
43  
44  import org.apache.commons.lang3.StringUtils;
45  import org.apache.commons.logging.Log;
46  import org.apache.commons.logging.LogFactory;
47  
48  import com.gargoylesoftware.htmlunit.BrowserVersion;
49  import com.gargoylesoftware.htmlunit.ScriptException;
50  import com.gargoylesoftware.htmlunit.WebAssert;
51  import com.gargoylesoftware.htmlunit.WebClient;
52  import com.gargoylesoftware.htmlunit.WebWindow;
53  import com.gargoylesoftware.htmlunit.html.DomNode;
54  import com.gargoylesoftware.htmlunit.html.HtmlPage;
55  import com.gargoylesoftware.htmlunit.javascript.background.BackgroundJavaScriptFactory;
56  import com.gargoylesoftware.htmlunit.javascript.background.JavaScriptExecutor;
57  import com.gargoylesoftware.htmlunit.javascript.configuration.ClassConfiguration;
58  import com.gargoylesoftware.htmlunit.javascript.configuration.ClassConfiguration.ConstantInfo;
59  import com.gargoylesoftware.htmlunit.javascript.configuration.ClassConfiguration.PropertyInfo;
60  import com.gargoylesoftware.htmlunit.javascript.configuration.JavaScriptConfiguration;
61  import com.gargoylesoftware.htmlunit.javascript.host.ActiveXObject;
62  import com.gargoylesoftware.htmlunit.javascript.host.ArrayCustom;
63  import com.gargoylesoftware.htmlunit.javascript.host.DateCustom;
64  import com.gargoylesoftware.htmlunit.javascript.host.NumberCustom;
65  import com.gargoylesoftware.htmlunit.javascript.host.ObjectCustom;
66  import com.gargoylesoftware.htmlunit.javascript.host.Reflect;
67  import com.gargoylesoftware.htmlunit.javascript.host.StringCustom;
68  import com.gargoylesoftware.htmlunit.javascript.host.Window;
69  import com.gargoylesoftware.htmlunit.javascript.host.intl.Intl;
70  
71  import net.sourceforge.htmlunit.corejs.javascript.BaseFunction;
72  import net.sourceforge.htmlunit.corejs.javascript.Context;
73  import net.sourceforge.htmlunit.corejs.javascript.ContextAction;
74  import net.sourceforge.htmlunit.corejs.javascript.Function;
75  import net.sourceforge.htmlunit.corejs.javascript.FunctionObject;
76  import net.sourceforge.htmlunit.corejs.javascript.IdFunctionObject;
77  import net.sourceforge.htmlunit.corejs.javascript.Script;
78  import net.sourceforge.htmlunit.corejs.javascript.ScriptRuntime;
79  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
80  import net.sourceforge.htmlunit.corejs.javascript.ScriptableObject;
81  import net.sourceforge.htmlunit.corejs.javascript.UniqueTag;
82  
83  /**
84   * A wrapper for the <a href="http://www.mozilla.org/rhino">Rhino JavaScript engine</a>
85   * that provides browser specific features.
86   *
87   * <p>Like all classes in this package, this class is not intended for direct use
88   * and may change without notice.</p>
89   *
90   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
91   * @author <a href="mailto:chen_jun@users.sourceforge.net">Chen Jun</a>
92   * @author David K. Taylor
93   * @author Chris Erskine
94   * @author <a href="mailto:bcurren@esomnie.com">Ben Curren</a>
95   * @author David D. Kilzer
96   * @author Marc Guillemot
97   * @author Daniel Gredler
98   * @author Ahmed Ashour
99   * @author Amit Manjhi
100  * @author Ronald Brill
101  * @author Frank Danek
102  * @see <a href="http://groups-beta.google.com/group/netscape.public.mozilla.jseng/browse_thread/thread/b4edac57329cf49f/069e9307ec89111f">
103  * Rhino and Java Browser</a>
104  */
105 public class JavaScriptEngine implements AbstractJavaScriptEngine<Script> {
106 
107     private static final Log LOG = LogFactory.getLog(JavaScriptEngine.class);
108 
109     private WebClient webClient_;
110     private final HtmlUnitContextFactory contextFactory_;
111     private final JavaScriptConfiguration jsConfig_;
112 
113     private transient ThreadLocal<Boolean> javaScriptRunning_;
114     private transient ThreadLocal<List<PostponedAction>> postponedActions_;
115     private transient boolean holdPostponedActions_;
116 
117     /** The JavaScriptExecutor corresponding to all windows of this Web client */
118     private transient JavaScriptExecutor javaScriptExecutor_;
119 
120     /**
121      * Key used to place the scope in which the execution of some JavaScript code
122      * started as thread local attribute in current context.
123      * <p>This is needed to resolve some relative locations relatively to the page
124      * in which the script is executed and not to the page which location is changed.
125      */
126     public static final String KEY_STARTING_SCOPE = "startingScope";
127 
128     /**
129      * Key used to place the {@link HtmlPage} for which the JavaScript code is executed
130      * as thread local attribute in current context.
131      */
132     public static final String KEY_STARTING_PAGE = "startingPage";
133 
134     /**
135      * Creates an instance for the specified {@link WebClient}.
136      *
137      * @param webClient the client that will own this engine
138      */
139     public JavaScriptEngine(final WebClient webClient) {
140         webClient_ = webClient;
141         contextFactory_ = new HtmlUnitContextFactory(webClient);
142         initTransientFields();
143         jsConfig_ = JavaScriptConfiguration.getInstance(webClient.getBrowserVersion());
144     }
145 
146     /**
147      * Returns the web client that this engine is associated with.
148      * @return the web client
149      */
150     public final WebClient getWebClient() {
151         return webClient_;
152     }
153 
154     /**
155      * Returns this JavaScript engine's Rhino {@link net.sourceforge.htmlunit.corejs.javascript.ContextFactory}.
156      * @return this JavaScript engine's Rhino {@link net.sourceforge.htmlunit.corejs.javascript.ContextFactory}
157      */
158     public HtmlUnitContextFactory getContextFactory() {
159         return contextFactory_;
160     }
161 
162     /**
163      * Performs initialization for the given webWindow.
164      * @param webWindow the web window to initialize for
165      */
166     @Override
167     public void initialize(final WebWindow webWindow) {
168         WebAssert.notNull("webWindow", webWindow);
169 
170         final ContextAction action = new ContextAction() {
171             @Override
172             public Object run(final Context cx) {
173                 try {
174                     init(webWindow, cx);
175                 }
176                 catch (final Exception e) {
177                     LOG.error("Exception while initializing JavaScript for the page", e);
178                     throw new ScriptException(null, e); // BUG: null is not useful.
179                 }
180 
181                 return null;
182             }
183         };
184 
185         getContextFactory().call(action);
186     }
187 
188     /**
189      * Returns the JavaScriptExecutor.
190      * @return the JavaScriptExecutor.
191      */
192     public JavaScriptExecutor getJavaScriptExecutor() {
193         return javaScriptExecutor_;
194     }
195 
196     /**
197      * Initializes all the JS stuff for the window.
198      * @param webWindow the web window
199      * @param context the current context
200      * @throws Exception if something goes wrong
201      */
202     private void init(final WebWindow webWindow, final Context context) throws Exception {
203         final WebClient webClient = webWindow.getWebClient();
204         final BrowserVersion browserVersion = webClient.getBrowserVersion();
205         final Map<Class<? extends Scriptable>, Scriptable> prototypes = new HashMap<>();
206         final Map<String, Scriptable> prototypesPerJSName = new HashMap<>();
207 
208         final Window window = new Window();
209         ((SimpleScriptable) window).setClassName("Window");
210         context.initStandardObjects(window);
211 
212         final ClassConfiguration windowConfig = jsConfig_.getClassConfiguration("Window");
213         if (windowConfig.getJsConstructor() != null) {
214             final FunctionObject functionObject = new RecursiveFunctionObject("Window",
215                     windowConfig.getJsConstructor(), window);
216             ScriptableObject.defineProperty(window, "constructor", functionObject,
217                     ScriptableObject.DONTENUM  | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
218         }
219         else {
220             defineConstructor(window, window, new Window());
221         }
222 
223         // remove some objects, that Rhino defines in top scope but that we don't want
224         deleteProperties(window, "java", "javax", "org", "com", "edu", "net",
225                 "JavaAdapter", "JavaImporter", "Continuation", "Packages", "getClass");
226         if (!browserVersion.hasFeature(JS_XML)) {
227             deleteProperties(window, "XML", "XMLList", "Namespace", "QName");
228         }
229 
230         if (!browserVersion.hasFeature(JS_Iterator)) {
231             deleteProperties(window, "Iterator", "StopIteration");
232         }
233 
234         final ScriptableObject errorObject = (ScriptableObject) ScriptableObject.getProperty(window, "Error");
235         if (browserVersion.hasFeature(JS_ERROR_STACK_TRACE_LIMIT)) {
236             errorObject.defineProperty("stackTraceLimit", 10, ScriptableObject.EMPTY);
237         }
238         else {
239             ScriptableObject.deleteProperty(errorObject, "stackTraceLimit");
240         }
241         if (!browserVersion.hasFeature(JS_ERROR_CAPTURE_STACK_TRACE)) {
242             ScriptableObject.deleteProperty(errorObject, "captureStackTrace");
243         }
244 
245         final Intl intl = new Intl();
246         intl.setParentScope(window);
247         window.defineProperty(intl.getClassName(), intl, ScriptableObject.DONTENUM);
248         intl.defineProperties(browserVersion);
249 
250         if (browserVersion.hasFeature(JS_REFLECT)) {
251             final Reflect reflect = new Reflect();
252             reflect.setParentScope(window);
253             window.defineProperty(reflect.getClassName(), reflect, ScriptableObject.DONTENUM);
254             reflect.defineProperties();
255         }
256 
257         for (final ClassConfiguration config : jsConfig_.getAll()) {
258             final boolean isWindow = Window.class.getName().equals(config.getHostClass().getName());
259             if (isWindow) {
260                 configureConstantsPropertiesAndFunctions(config, window);
261 
262                 final HtmlUnitScriptable prototype = configureClass(config, window, browserVersion);
263                 prototypesPerJSName.put(config.getClassName(), prototype);
264             }
265             else {
266                 final HtmlUnitScriptable prototype = configureClass(config, window, browserVersion);
267                 if (config.isJsObject()) {
268                     // Place object with prototype property in Window scope
269                     final HtmlUnitScriptable obj = config.getHostClass().newInstance();
270                     prototype.defineProperty("__proto__", prototype, ScriptableObject.DONTENUM);
271                     obj.defineProperty("prototype", prototype, ScriptableObject.DONTENUM); // but not setPrototype!
272                     obj.setParentScope(window);
273                     obj.setClassName(config.getClassName());
274                     ScriptableObject.defineProperty(window, obj.getClassName(), obj, ScriptableObject.DONTENUM);
275                     // this obj won't have prototype, constants need to be configured on it again
276                     configureConstants(config, obj);
277                 }
278                 prototypes.put(config.getHostClass(), prototype);
279                 prototypesPerJSName.put(config.getClassName(), prototype);
280             }
281         }
282 
283         for (final ClassConfiguration config : jsConfig_.getAll()) {
284             final Member jsConstructor = config.getJsConstructor();
285             final String jsClassName = config.getClassName();
286             Scriptable prototype = prototypesPerJSName.get(jsClassName);
287             final String hostClassSimpleName = config.getHostClassSimpleName();
288 
289             if ("Image".equals(hostClassSimpleName)
290                     && browserVersion.hasFeature(JS_IMAGE_PROTOTYPE_SAME_AS_HTML_IMAGE)) {
291                 prototype = prototypesPerJSName.get("HTMLImageElement");
292             }
293             if ("Option".equals(hostClassSimpleName)) {
294                 prototype = prototypesPerJSName.get("HTMLOptionElement");
295             }
296 
297             switch (hostClassSimpleName) {
298                 case "WebKitAnimationEvent":
299                     prototype = prototypesPerJSName.get("AnimationEvent");
300                     break;
301 
302                 case "WebKitMutationObserver":
303                     prototype = prototypesPerJSName.get("MutationObserver");
304                     break;
305 
306                 case "WebKitTransitionEvent":
307                     prototype = prototypesPerJSName.get("TransitionEvent");
308                     break;
309 
310                 case "webkitURL":
311                     prototype = prototypesPerJSName.get("URL");
312                     break;
313 
314                 default:
315             }
316             if (prototype != null && config.isJsObject()) {
317                 if (jsConstructor == null) {
318                     final ScriptableObject constructor;
319                     if ("Window".equals(jsClassName)) {
320                         constructor = (ScriptableObject) ScriptableObject.getProperty(window, "constructor");
321                     }
322                     else {
323                         constructor = config.getHostClass().newInstance();
324                         ((SimpleScriptable) constructor).setClassName(config.getClassName());
325                     }
326                     defineConstructor(window, prototype, constructor);
327                     configureConstantsStaticPropertiesAndStaticFunctions(config, constructor);
328                 }
329                 else {
330                     final BaseFunction function;
331                     if ("Window".equals(jsClassName)) {
332                         function = (BaseFunction) ScriptableObject.getProperty(window, "constructor");
333                     }
334                     else {
335                         function = new RecursiveFunctionObject(jsClassName, jsConstructor, window);
336                     }
337 
338                     if ("WebKitAnimationEvent".equals(hostClassSimpleName)
339                             || "WebKitMutationObserver".equals(hostClassSimpleName)
340                             || "WebKitTransitionEvent".equals(hostClassSimpleName)
341                             || "webkitURL".equals(hostClassSimpleName)
342                             || "Image".equals(hostClassSimpleName)
343                             || "Option".equals(hostClassSimpleName)) {
344                         final Object prototypeProperty = ScriptableObject.getProperty(window, prototype.getClassName());
345 
346                         if (function instanceof FunctionObject) {
347                             try {
348                                 ((FunctionObject) function).addAsConstructor(window, prototype);
349                             }
350                             catch (final Exception e) {
351                                 // TODO see issue #1897
352                                 if (LOG.isWarnEnabled()) {
353                                     final String newline = System.lineSeparator();
354                                     LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
355                                             + e.getMessage() + newline
356                                             + "prototype: " + prototype.getClassName());
357                                 }
358                             }
359                         }
360 
361                         ScriptableObject.defineProperty(window, hostClassSimpleName, function,
362                                 ScriptableObject.DONTENUM);
363 
364                         // the prototype class name is set as a side effect of functionObject.addAsConstructor
365                         // so we restore its value
366                         if (!hostClassSimpleName.equals(prototype.getClassName())) {
367                             if (prototypeProperty == UniqueTag.NOT_FOUND) {
368                                 ScriptableObject.deleteProperty(window, prototype.getClassName());
369                             }
370                             else {
371                                 ScriptableObject.defineProperty(window, prototype.getClassName(),
372                                         prototypeProperty, ScriptableObject.DONTENUM);
373                             }
374                         }
375                     }
376                     else {
377                         if (function instanceof FunctionObject) {
378                             try {
379                                 ((FunctionObject) function).addAsConstructor(window, prototype);
380                             }
381                             catch (final Exception e) {
382                                 // TODO see issue #1897
383                                 if (LOG.isWarnEnabled()) {
384                                     final String newline = System.lineSeparator();
385                                     LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
386                                             + e.getMessage() + newline
387                                             + "prototype: " + prototype.getClassName());
388                                 }
389                             }
390                         }
391                     }
392 
393                     configureConstantsStaticPropertiesAndStaticFunctions(config, function);
394                 }
395             }
396         }
397         window.setPrototype(prototypesPerJSName.get(Window.class.getSimpleName()));
398 
399         // once all prototypes have been build, it's possible to configure the chains
400         final Scriptable objectPrototype = ScriptableObject.getObjectPrototype(window);
401         for (final Map.Entry<String, Scriptable> entry : prototypesPerJSName.entrySet()) {
402             final String name = entry.getKey();
403             final ClassConfiguration config = jsConfig_.getClassConfiguration(name);
404             final Scriptable prototype = entry.getValue();
405             if (!StringUtils.isEmpty(config.getExtendedClassName())) {
406                 final Scriptable parentPrototype = prototypesPerJSName.get(config.getExtendedClassName());
407                 prototype.setPrototype(parentPrototype);
408             }
409             else {
410                 prototype.setPrototype(objectPrototype);
411             }
412         }
413 
414         // IE ActiveXObject simulation
415         // see http://msdn.microsoft.com/en-us/library/ie/dn423948%28v=vs.85%29.aspx
416         // DEV Note: this is at the moment the only usage of HiddenFunctionObject
417         //           if we need more in the future, we have to enhance our JSX annotations
418         if (browserVersion.hasFeature(JS_WINDOW_ACTIVEXOBJECT_HIDDEN)) {
419             final Scriptable prototype = prototypesPerJSName.get("ActiveXObject");
420             if (null != prototype) {
421                 final Method jsConstructor = ActiveXObject.class.getDeclaredMethod("jsConstructor",
422                         Context.class, Object[].class, Function.class, boolean.class);
423                 final FunctionObject functionObject = new HiddenFunctionObject("ActiveXObject", jsConstructor, window);
424                 try {
425                     functionObject.addAsConstructor(window, prototype);
426                 }
427                 catch (final Exception e) {
428                     // TODO see issue #1897
429                     if (LOG.isWarnEnabled()) {
430                         final String newline = System.lineSeparator();
431                         LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
432                                 + e.getMessage() + newline
433                                 + "prototype: " + prototype.getClassName());
434                     }
435                 }
436             }
437         }
438 
439         // Rhino defines too much methods for us, particularly since implementation of ECMAScript5
440         removePrototypeProperties(window, "String", "equals", "equalsIgnoreCase");
441         if (!browserVersion.hasFeature(STRING_INCLUDES)) {
442             removePrototypeProperties(window, "String", "includes");
443         }
444         if (!browserVersion.hasFeature(STRING_REPEAT)) {
445             removePrototypeProperties(window, "String", "repeat");
446         }
447         if (!browserVersion.hasFeature(STRING_STARTS_ENDS_WITH)) {
448             removePrototypeProperties(window, "String", "startsWith");
449             removePrototypeProperties(window, "String", "endsWith");
450         }
451         if (!browserVersion.hasFeature(STRING_TRIM_LEFT_RIGHT)) {
452             removePrototypeProperties(window, "String", "trimLeft");
453             removePrototypeProperties(window, "String", "trimRight");
454         }
455         if (browserVersion.hasFeature(STRING_CONTAINS)) {
456             final ScriptableObject stringPrototype =
457                 (ScriptableObject) ScriptableObject.getClassPrototype(window, "String");
458             stringPrototype.defineFunctionProperties(new String[] {"contains"},
459                 StringCustom.class, ScriptableObject.EMPTY);
460         }
461 
462         // only FF has toSource
463         if (!browserVersion.hasFeature(JS_FUNCTION_TOSOURCE)) {
464             deleteProperties(window, "uneval");
465             removePrototypeProperties(window, "Object", "toSource");
466             removePrototypeProperties(window, "Array", "toSource");
467             removePrototypeProperties(window, "Date", "toSource");
468             removePrototypeProperties(window, "Function", "toSource");
469             removePrototypeProperties(window, "Number", "toSource");
470             removePrototypeProperties(window, "String", "toSource");
471         }
472         if (browserVersion.hasFeature(JS_WINDOW_ACTIVEXOBJECT_HIDDEN)) {
473             ((IdFunctionObject) ScriptableObject.getProperty(window, "Object")).delete("assign");
474         }
475         deleteProperties(window, "isXMLName");
476 
477         NativeFunctionToStringFunction.installFix(window, webClient.getBrowserVersion());
478 
479         final ScriptableObject datePrototype = (ScriptableObject) ScriptableObject.getClassPrototype(window, "Date");
480         datePrototype.defineFunctionProperties(new String[] {"toLocaleDateString", "toLocaleTimeString"},
481                 DateCustom.class, ScriptableObject.DONTENUM);
482 
483         if (browserVersion.hasFeature(JS_OBJECT_GET_OWN_PROPERTY_SYMBOLS)) {
484             ((ScriptableObject) objectPrototype).defineFunctionProperties(new String[] {"getOwnPropertySymbols"},
485                     ObjectCustom.class, ScriptableObject.DONTENUM);
486         }
487 
488         if (browserVersion.hasFeature(JS_ARRAY_FROM)) {
489             final ScriptableObject arrayPrototype = (ScriptableObject) ScriptRuntime.name(context, window, "Array");
490             arrayPrototype.defineFunctionProperties(new String[] {"from"},
491                     ArrayCustom.class, ScriptableObject.DONTENUM);
492         }
493 
494         final ScriptableObject numberPrototype
495             = (ScriptableObject) ScriptableObject.getClassPrototype(window, "Number");
496         numberPrototype.defineFunctionProperties(new String[] {"toLocaleString"},
497                 NumberCustom.class, ScriptableObject.DONTENUM);
498 
499         window.setPrototypes(prototypes, prototypesPerJSName);
500         window.initialize(webWindow);
501     }
502 
503     private static void defineConstructor(final Window window,
504             final Scriptable prototype, final ScriptableObject constructor) {
505         constructor.setParentScope(window);
506         try {
507             ScriptableObject.defineProperty(prototype, "constructor", constructor,
508                     ScriptableObject.DONTENUM  | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
509         }
510         catch (final Exception e) {
511             // TODO see issue #1897
512             if (LOG.isWarnEnabled()) {
513                 final String newline = System.lineSeparator();
514                 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
515                         + e.getMessage() + newline
516                         + "prototype: " + prototype.getClassName());
517             }
518         }
519 
520         try {
521             ScriptableObject.defineProperty(constructor, "prototype", prototype,
522                     ScriptableObject.DONTENUM  | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
523         }
524         catch (final Exception e) {
525             // TODO see issue #1897
526             if (LOG.isWarnEnabled()) {
527                 final String newline = System.lineSeparator();
528                 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
529                         + e.getMessage() + newline
530                         + "prototype: " + prototype.getClassName());
531             }
532         }
533 
534         window.defineProperty(constructor.getClassName(), constructor, ScriptableObject.DONTENUM);
535     }
536 
537     /**
538      * Deletes the properties with the provided names.
539      * @param scope the scope from which properties have to be removed
540      * @param propertiesToDelete the list of property names
541      */
542     private static void deleteProperties(final Scriptable scope, final String... propertiesToDelete) {
543         for (final String property : propertiesToDelete) {
544             scope.delete(property);
545         }
546     }
547 
548     /**
549      * Removes prototype properties.
550      * @param scope the scope
551      * @param className the class for which properties should be removed
552      * @param properties the properties to remove
553      */
554     private static void removePrototypeProperties(final Scriptable scope, final String className,
555             final String... properties) {
556         final ScriptableObject prototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, className);
557         for (final String property : properties) {
558             prototype.delete(property);
559         }
560     }
561 
562     /**
563      * Configures the specified class for access via JavaScript.
564      * @param config the configuration settings for the class to be configured
565      * @param window the scope within which to configure the class
566      * @param browserVersion the browser version
567      * @throws InstantiationException if the new class cannot be instantiated
568      * @throws IllegalAccessException if we don't have access to create the new instance
569      * @return the created prototype
570      */
571     public static HtmlUnitScriptable configureClass(final ClassConfiguration config, final Scriptable window,
572             final BrowserVersion browserVersion)
573         throws InstantiationException, IllegalAccessException {
574 
575         final HtmlUnitScriptable prototype = config.getHostClass().newInstance();
576         prototype.setParentScope(window);
577         prototype.setClassName(config.getClassName());
578 
579         configureConstantsPropertiesAndFunctions(config, prototype);
580 
581         return prototype;
582     }
583 
584     /**
585      * Configures constants, static properties and static functions on the object.
586      * @param config the configuration for the object
587      * @param scriptable the object to configure
588      */
589     private static void configureConstantsStaticPropertiesAndStaticFunctions(final ClassConfiguration config,
590             final ScriptableObject scriptable) {
591         configureConstants(config, scriptable);
592         configureStaticProperties(config, scriptable);
593         configureStaticFunctions(config, scriptable);
594     }
595 
596     /**
597      * Configures constants, properties and functions on the object.
598      * @param config the configuration for the object
599      * @param scriptable the object to configure
600      */
601     private static void configureConstantsPropertiesAndFunctions(final ClassConfiguration config,
602             final ScriptableObject scriptable) {
603         configureConstants(config, scriptable);
604         configureProperties(config, scriptable);
605         configureFunctions(config, scriptable);
606     }
607 
608     private static void configureFunctions(final ClassConfiguration config, final ScriptableObject scriptable) {
609         final int attributes = ScriptableObject.EMPTY;
610         // the functions
611         for (final Entry<String, Method> functionInfo : config.getFunctionEntries()) {
612             final String functionName = functionInfo.getKey();
613             final Method method = functionInfo.getValue();
614             final FunctionObject functionObject = new FunctionObject(functionName, method, scriptable);
615             scriptable.defineProperty(functionName, functionObject, attributes);
616         }
617     }
618 
619     private static void configureConstants(final ClassConfiguration config, final ScriptableObject scriptable) {
620         for (final ConstantInfo constantInfo : config.getConstants()) {
621             scriptable.defineProperty(constantInfo.getName(), constantInfo.getValue(), constantInfo.getFlag());
622         }
623     }
624 
625     private static void configureProperties(final ClassConfiguration config, final ScriptableObject scriptable) {
626         final Map<String, PropertyInfo> propertyMap = config.getPropertyMap();
627         for (final String propertyName : propertyMap.keySet()) {
628             final PropertyInfo info = propertyMap.get(propertyName);
629             final Method readMethod = info.getReadMethod();
630             final Method writeMethod = info.getWriteMethod();
631             scriptable.defineProperty(propertyName, null, readMethod, writeMethod, ScriptableObject.EMPTY);
632         }
633     }
634 
635     private static void configureStaticProperties(final ClassConfiguration config, final ScriptableObject scriptable) {
636         for (final Entry<String, ClassConfiguration.PropertyInfo> propertyEntry
637                 : config.getStaticPropertyEntries()) {
638             final String propertyName = propertyEntry.getKey();
639             final Method readMethod = propertyEntry.getValue().getReadMethod();
640             final Method writeMethod = propertyEntry.getValue().getWriteMethod();
641             final int flag = ScriptableObject.EMPTY;
642 
643             scriptable.defineProperty(propertyName, null, readMethod, writeMethod, flag);
644         }
645     }
646 
647     private static void configureStaticFunctions(final ClassConfiguration config,
648             final ScriptableObject scriptable) {
649         for (final Entry<String, Method> staticfunctionInfo : config.getStaticFunctionEntries()) {
650             final String functionName = staticfunctionInfo.getKey();
651             final Method method = staticfunctionInfo.getValue();
652             final FunctionObject staticFunctionObject = new FunctionObject(functionName, method,
653                     scriptable);
654             scriptable.defineProperty(functionName, staticFunctionObject, ScriptableObject.EMPTY);
655         }
656     }
657 
658     /**
659      * Register WebWindow with the JavaScriptExecutor.
660      * @param webWindow the WebWindow to be registered.
661      */
662     @Override
663     public synchronized void registerWindowAndMaybeStartEventLoop(final WebWindow webWindow) {
664         if (webClient_ != null) {
665             if (javaScriptExecutor_ == null) {
666                 javaScriptExecutor_ = BackgroundJavaScriptFactory.theFactory().createJavaScriptExecutor(webClient_);
667             }
668             javaScriptExecutor_.addWindow(webWindow);
669         }
670     }
671 
672     /**
673      * Executes the jobs in the eventLoop till timeoutMillis expires or the eventLoop becomes empty.
674      * No use in non-GAE mode (see {@link com.gargoylesoftware.htmlunit.gae.GAEUtils#isGaeMode}.
675      * @param timeoutMillis the timeout in milliseconds
676      * @return the number of jobs executed
677      */
678     public int pumpEventLoop(final long timeoutMillis) {
679         if (javaScriptExecutor_ == null) {
680             return 0;
681         }
682         return javaScriptExecutor_.pumpEventLoop(timeoutMillis);
683     }
684 
685     /**
686      * Shutdown the JavaScriptEngine.
687      */
688     @Override
689     public void shutdown() {
690         webClient_ = null;
691         if (javaScriptExecutor_ != null) {
692             javaScriptExecutor_.shutdown();
693             javaScriptExecutor_ = null;
694         }
695         if (postponedActions_ != null) {
696             postponedActions_.remove();
697         }
698         if (javaScriptRunning_ != null) {
699             javaScriptRunning_.remove();
700         }
701         holdPostponedActions_ = false;
702     }
703 
704     /**
705      * {@inheritDoc}
706      */
707     @Override
708     public Script compile(final HtmlPage page, final String sourceCode,
709                            final String sourceName, final int startLine) {
710         final Scriptable scope = getScope(page, null);
711         return compile(page, scope, sourceCode, sourceName, startLine);
712     }
713 
714     /**
715      * Compiles the specified JavaScript code in the context of a given scope.
716      *
717      * @param owningPage the page from which the code started
718      * @param scope the scope in which to execute the javascript code
719      * @param sourceCode the JavaScript code to execute
720      * @param sourceName the name that will be displayed on error conditions
721      * @param startLine the line at which the script source starts
722      * @return the result of executing the specified code
723      */
724     public Script compile(final HtmlPage owningPage, final Scriptable scope, final String sourceCode,
725             final String sourceName, final int startLine) {
726         WebAssert.notNull("sourceCode", sourceCode);
727 
728         if (LOG.isTraceEnabled()) {
729             final String newline = System.lineSeparator();
730             LOG.trace("Javascript compile " + sourceName + newline + sourceCode + newline);
731         }
732 
733         final ContextAction action = new HtmlUnitContextAction(scope, owningPage) {
734             @Override
735             public Object doRun(final Context cx) {
736                 return cx.compileString(sourceCode, sourceName, startLine, null);
737             }
738 
739             @Override
740             protected String getSourceCode(final Context cx) {
741                 return sourceCode;
742             }
743         };
744 
745         return (Script) getContextFactory().call(action);
746     }
747 
748     /**
749      * {@inheritDoc}
750      */
751     @Override
752     public Object execute(final HtmlPage page,
753                            final String sourceCode,
754                            final String sourceName,
755                            final int startLine) {
756 
757         final Script script = compile(page, sourceCode, sourceName, startLine);
758         if (script == null) { // happens with syntax error + throwExceptionOnScriptError = false
759             return null;
760         }
761         return execute(page, script);
762     }
763 
764     /**
765      * {@inheritDoc}
766      */
767     @Override
768     public Object execute(final HtmlPage page, final Script script) {
769         final Scriptable scope = getScope(page, null);
770         return execute(page, scope, script);
771     }
772 
773     /**
774      * Executes the specified JavaScript code in the given scope.
775      *
776      * @param page the page that started the execution
777      * @param scope the scope in which to execute
778      * @param script the script to execute
779      * @return the result of executing the specified code
780      */
781     public Object execute(final HtmlPage page, final Scriptable scope, final Script script) {
782         final ContextAction action = new HtmlUnitContextAction(scope, page) {
783             @Override
784             public Object doRun(final Context cx) {
785                 return script.exec(cx, scope);
786             }
787 
788             @Override
789             protected String getSourceCode(final Context cx) {
790                 return null;
791             }
792         };
793 
794         return getContextFactory().call(action);
795     }
796 
797     /**
798      * Calls a JavaScript function and return the result.
799      * @param page the page
800      * @param javaScriptFunction the function to call
801      * @param thisObject the this object for class method calls
802      * @param args the list of arguments to pass to the function
803      * @param node the HTML element that will act as the context
804      * @return the result of the function call
805      */
806     public Object callFunction(
807             final HtmlPage page,
808             final Function javaScriptFunction,
809             final Scriptable thisObject,
810             final Object[] args,
811             final DomNode node) {
812 
813         final Scriptable scope = getScope(page, node);
814 
815         return callFunction(page, javaScriptFunction, scope, thisObject, args);
816     }
817 
818     /**
819      * Calls the given function taking care of synchronization issues.
820      * @param page the interactive page that caused this script to executed
821      * @param function the JavaScript function to execute
822      * @param scope the execution scope
823      * @param thisObject the 'this' object
824      * @param args the function's arguments
825      * @return the function result
826      */
827     public Object callFunction(final HtmlPage page, final Function function,
828             final Scriptable scope, final Scriptable thisObject, final Object[] args) {
829 
830         final ContextAction action = new HtmlUnitContextAction(scope, page) {
831             @Override
832             public Object doRun(final Context cx) {
833                 if (ScriptRuntime.hasTopCall(cx)) {
834                     return function.call(cx, scope, thisObject, args);
835                 }
836                 return ScriptRuntime.doTopCall(function, cx, scope, thisObject, args, cx.isStrictMode());
837             }
838             @Override
839             protected String getSourceCode(final Context cx) {
840                 return cx.decompileFunction(function, 2);
841             }
842         };
843         return getContextFactory().call(action);
844     }
845 
846     private static Scriptable getScope(final HtmlPage page, final DomNode node) {
847         if (node != null) {
848             return node.getScriptableObject();
849         }
850         return page.getEnclosingWindow().getScriptableObject();
851     }
852 
853     /**
854      * Indicates if JavaScript is running in current thread.
855      * <p>This allows code to know if there own evaluation is has been triggered by some JS code.
856      * @return {@code true} if JavaScript is running
857      */
858     @Override
859     public boolean isScriptRunning() {
860         return Boolean.TRUE.equals(javaScriptRunning_.get());
861     }
862 
863     /**
864      * Facility for ContextAction usage.
865      * ContextAction should be preferred because according to Rhino doc it
866      * "guarantees proper association of Context instances with the current thread and is faster".
867      */
868     private abstract class HtmlUnitContextAction implements ContextAction {
869         private final Scriptable scope_;
870         private final HtmlPage page_;
871 
872         HtmlUnitContextAction(final Scriptable scope, final HtmlPage page) {
873             scope_ = scope;
874             page_ = page;
875         }
876 
877         @Override
878         public final Object run(final Context cx) {
879             final Boolean javaScriptAlreadyRunning = javaScriptRunning_.get();
880             javaScriptRunning_.set(Boolean.TRUE);
881 
882             try {
883                 // KEY_STARTING_SCOPE maintains a stack of scopes
884                 @SuppressWarnings("unchecked")
885                 Stack<Scriptable> stack = (Stack<Scriptable>) cx.getThreadLocal(JavaScriptEngine.KEY_STARTING_SCOPE);
886                 if (null == stack) {
887                     stack = new Stack<>();
888                     cx.putThreadLocal(KEY_STARTING_SCOPE, stack);
889                 }
890 
891                 final Object response;
892                 stack.push(scope_);
893                 try {
894                     cx.putThreadLocal(KEY_STARTING_PAGE, page_);
895                     synchronized (page_) { // 2 scripts can't be executed in parallel for one page
896                         if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
897                             return null; // page has been unloaded
898                         }
899                         response = doRun(cx);
900                     }
901                 }
902                 finally {
903                     stack.pop();
904                 }
905 
906                 // doProcessPostponedActions is synchronized
907                 // moved out of the sync block to avoid deadlocks
908                 if (!holdPostponedActions_) {
909                     doProcessPostponedActions();
910                 }
911                 return response;
912             }
913             catch (final Exception e) {
914                 handleJavaScriptException(new ScriptException(page_, e, getSourceCode(cx)), true);
915                 return null;
916             }
917             catch (final TimeoutError e) {
918                 getWebClient().getJavaScriptErrorListener().timeoutError(page_, e.getAllowedTime(), e.getExecutionTime());
919                 if (getWebClient().getOptions().isThrowExceptionOnScriptError()) {
920                     throw new RuntimeException(e);
921                 }
922                 LOG.info("Caught script timeout error", e);
923                 return null;
924             }
925             finally {
926                 javaScriptRunning_.set(javaScriptAlreadyRunning);
927             }
928         }
929 
930         protected abstract Object doRun(Context cx);
931 
932         protected abstract String getSourceCode(Context cx);
933     }
934 
935     private void doProcessPostponedActions() {
936         holdPostponedActions_ = false;
937 
938         final WebClient webClient = getWebClient();
939         if (webClient == null) {
940             postponedActions_.set(null);
941             return;
942         }
943 
944         try {
945             webClient.loadDownloadedResponses();
946         }
947         catch (final RuntimeException e) {
948             throw e;
949         }
950         catch (final Exception e) {
951             throw new RuntimeException(e);
952         }
953 
954         final List<PostponedAction> actions = postponedActions_.get();
955         if (actions != null) {
956             postponedActions_.set(null);
957             try {
958                 for (final PostponedAction action : actions) {
959                     if (LOG.isDebugEnabled()) {
960                         LOG.debug("Processing PostponedAction " + action);
961                     }
962 
963                     // verify that the page that registered this PostponedAction is still alive
964                     if (action.isStillAlive()) {
965                         action.execute();
966                     }
967                 }
968             }
969             catch (final Exception e) {
970                 Context.throwAsScriptRuntimeEx(e);
971             }
972         }
973     }
974 
975     /**
976      * Adds an action that should be executed first when the script currently being executed has finished.
977      * @param action the action
978      */
979     @Override
980     public void addPostponedAction(final PostponedAction action) {
981         List<PostponedAction> actions = postponedActions_.get();
982         if (actions == null) {
983             actions = new ArrayList<>();
984             postponedActions_.set(actions);
985         }
986         actions.add(action);
987     }
988 
989     /**
990      * Handles an exception that occurred during execution of JavaScript code.
991      * @param scriptException the exception
992      * @param triggerOnError if true, this triggers the onerror handler
993      */
994     protected void handleJavaScriptException(final ScriptException scriptException, final boolean triggerOnError) {
995         // Trigger window.onerror, if it has been set.
996         final HtmlPage page = scriptException.getPage();
997         if (triggerOnError && page != null) {
998             final WebWindow window = page.getEnclosingWindow();
999             if (window != null) {
1000                 final Window w = window.getScriptableObject();
1001                 if (w != null) {
1002                     try {
1003                         w.triggerOnError(scriptException);
1004                     }
1005                     catch (final Exception e) {
1006                         handleJavaScriptException(new ScriptException(page, e, null), false);
1007                     }
1008                 }
1009             }
1010         }
1011         getWebClient().getJavaScriptErrorListener().scriptException(page, scriptException);
1012         // Throw a Java exception if the user wants us to.
1013         if (getWebClient().getOptions().isThrowExceptionOnScriptError()) {
1014             throw scriptException;
1015         }
1016         // Log the error; ScriptException instances provide good debug info.
1017         LOG.info("Caught script exception", scriptException);
1018     }
1019 
1020     /**
1021      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1022      * Indicates that no postponed action should be executed.
1023      */
1024     @Override
1025     public void holdPosponedActions() {
1026         holdPostponedActions_ = true;
1027     }
1028 
1029     /**
1030      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1031      * Process postponed actions, if any.
1032      */
1033     @Override
1034     public void processPostponedActions() {
1035         doProcessPostponedActions();
1036     }
1037 
1038     /**
1039      * Re-initializes transient fields when an object of this type is deserialized.
1040      */
1041     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
1042         in.defaultReadObject();
1043         initTransientFields();
1044     }
1045 
1046     private void initTransientFields() {
1047         javaScriptRunning_ = new ThreadLocal<>();
1048         postponedActions_ = new ThreadLocal<>();
1049         holdPostponedActions_ = false;
1050     }
1051 
1052     /**
1053      * Gets the class of the JavaScript object for the node class.
1054      * @param c the node class {@link DomNode} or some subclass.
1055      * @return {@code null} if none found
1056      */
1057     public Class<? extends HtmlUnitScriptable> getJavaScriptClass(final Class<?> c) {
1058         return jsConfig_.getDomJavaScriptMapping().get(c);
1059     }
1060 
1061     /**
1062      * Gets the associated configuration.
1063      * @return the configuration
1064      */
1065     @Override
1066     public JavaScriptConfiguration getJavaScriptConfiguration() {
1067         return jsConfig_;
1068     }
1069 
1070     /**
1071      * Returns the javascript timeout.
1072      * @return the javascript timeout
1073      */
1074     @Override
1075     public long getJavaScriptTimeout() {
1076         return getContextFactory().getTimeout();
1077     }
1078 
1079     /**
1080      * Sets the javascript timeout.
1081      * @param timeout the timeout
1082      */
1083     @Override
1084     public void setJavaScriptTimeout(final long timeout) {
1085         getContextFactory().setTimeout(timeout);
1086     }
1087 }