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