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