View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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    * https://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 org.htmlunit.javascript;
16  
17  import static org.htmlunit.BrowserVersionFeatures.JS_ERROR_STACK_TRACE_LIMIT;
18  import static org.htmlunit.BrowserVersionFeatures.JS_WINDOW_INSTALL_TRIGGER_NULL;
19  
20  import java.io.IOException;
21  import java.io.ObjectInputStream;
22  import java.lang.reflect.Member;
23  import java.lang.reflect.Method;
24  import java.net.URL;
25  import java.util.ArrayList;
26  import java.util.HashMap;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Map.Entry;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  import org.htmlunit.BrowserVersion;
34  import org.htmlunit.Page;
35  import org.htmlunit.ScriptException;
36  import org.htmlunit.WebAssert;
37  import org.htmlunit.WebClient;
38  import org.htmlunit.WebWindow;
39  import org.htmlunit.corejs.javascript.Callable;
40  import org.htmlunit.corejs.javascript.Context;
41  import org.htmlunit.corejs.javascript.ContextAction;
42  import org.htmlunit.corejs.javascript.ContextFactory;
43  import org.htmlunit.corejs.javascript.EcmaError;
44  import org.htmlunit.corejs.javascript.Function;
45  import org.htmlunit.corejs.javascript.FunctionObject;
46  import org.htmlunit.corejs.javascript.JavaScriptException;
47  import org.htmlunit.corejs.javascript.NativeArray;
48  import org.htmlunit.corejs.javascript.NativeArrayIterator;
49  import org.htmlunit.corejs.javascript.NativeConsole;
50  import org.htmlunit.corejs.javascript.NativeObject;
51  import org.htmlunit.corejs.javascript.RhinoException;
52  import org.htmlunit.corejs.javascript.Script;
53  import org.htmlunit.corejs.javascript.ScriptRuntime;
54  import org.htmlunit.corejs.javascript.Scriptable;
55  import org.htmlunit.corejs.javascript.ScriptableObject;
56  import org.htmlunit.corejs.javascript.StackStyle;
57  import org.htmlunit.corejs.javascript.Symbol;
58  import org.htmlunit.corejs.javascript.TopLevel;
59  import org.htmlunit.html.DomNode;
60  import org.htmlunit.html.HtmlPage;
61  import org.htmlunit.javascript.background.BackgroundJavaScriptFactory;
62  import org.htmlunit.javascript.background.JavaScriptExecutor;
63  import org.htmlunit.javascript.configuration.AbstractJavaScriptConfiguration;
64  import org.htmlunit.javascript.configuration.ClassConfiguration;
65  import org.htmlunit.javascript.configuration.ClassConfiguration.ConstantInfo;
66  import org.htmlunit.javascript.configuration.ClassConfiguration.PropertyInfo;
67  import org.htmlunit.javascript.configuration.JavaScriptConfiguration;
68  import org.htmlunit.javascript.configuration.ProxyAutoConfigJavaScriptConfiguration;
69  import org.htmlunit.javascript.host.ConsoleCustom;
70  import org.htmlunit.javascript.host.NumberCustom;
71  import org.htmlunit.javascript.host.URLSearchParams;
72  import org.htmlunit.javascript.host.Window;
73  import org.htmlunit.javascript.host.dom.DOMException;
74  import org.htmlunit.javascript.host.html.HTMLImageElement;
75  import org.htmlunit.javascript.host.html.HTMLOptionElement;
76  import org.htmlunit.javascript.host.intl.Intl;
77  import org.htmlunit.javascript.host.xml.FormData;
78  import org.htmlunit.javascript.polyfill.Polyfill;
79  import org.htmlunit.util.StringUtils;
80  
81  /**
82   * A wrapper for the <a href="http://www.mozilla.org/rhino">Rhino JavaScript engine</a>
83   * that provides browser specific features.
84   *
85   * <p>Like all classes in this package, this class is not intended for direct use
86   * and may change without notice.</p>
87   *
88   * @author Mike Bowler
89   * @author Chen Jun
90   * @author David K. Taylor
91   * @author Chris Erskine
92   * @author Ben Curren
93   * @author David D. Kilzer
94   * @author Marc Guillemot
95   * @author Daniel Gredler
96   * @author Ahmed Ashour
97   * @author Amit Manjhi
98   * @author Ronald Brill
99   * @author Frank Danek
100  * @author Lai Quang Duong
101  * @author Sven Strickroth
102  *
103  * @see <a href="http://groups-beta.google.com/group/netscape.public.mozilla.jseng/browse_thread/thread/b4edac57329cf49f/069e9307ec89111f">
104  *     Rhino and Java Browser</a>
105  */
106 public class JavaScriptEngine implements AbstractJavaScriptEngine<Script> {
107 
108     private static final Log LOG = LogFactory.getLog(JavaScriptEngine.class);
109 
110     /** ScriptRuntime.emptyArgs. */
111     public static final Object[] EMPTY_ARGS = ScriptRuntime.emptyArgs;
112 
113     /** org.htmlunit.corejs.javascript.Undefined.instance. */
114     public static final Object UNDEFINED = org.htmlunit.corejs.javascript.Undefined.instance;
115 
116     private WebClient webClient_;
117     private HtmlUnitContextFactory contextFactory_;
118     private JavaScriptConfiguration jsConfig_;
119 
120     private transient ThreadLocal<Boolean> javaScriptRunning_;
121     private transient ThreadLocal<List<PostponedAction>> postponedActions_;
122     private transient boolean holdPostponedActions_;
123     private transient boolean shutdownPending_;
124 
125     /** The JavaScriptExecutor corresponding to all windows of this Web client */
126     private transient JavaScriptExecutor javaScriptExecutor_;
127 
128     /**
129      * Key used to place the {@link HtmlPage} for which the JavaScript code is executed
130      * as thread local attribute in current context.
131      */
132     public static final String KEY_STARTING_PAGE = "startingPage";
133 
134     /**
135      * Creates an instance for the specified {@link WebClient}.
136      *
137      * @param webClient the client that will own this engine
138      */
139     public JavaScriptEngine(final WebClient webClient) {
140         if (webClient == null) {
141             throw new IllegalArgumentException("JavaScriptEngine ctor requires a webClient");
142         }
143 
144         webClient_ = webClient;
145         contextFactory_ = new HtmlUnitContextFactory(webClient);
146         initTransientFields();
147 
148         jsConfig_ = JavaScriptConfiguration.getInstance(webClient.getBrowserVersion());
149         RhinoException.setStackStyle(StackStyle.MOZILLA_LF);
150     }
151 
152     /**
153      * Returns the web client that this engine is associated with.
154      * @return the web client
155      */
156     private WebClient getWebClient() {
157         return webClient_;
158     }
159 
160     /**
161      * {@inheritDoc}
162      */
163     @Override
164     public HtmlUnitContextFactory getContextFactory() {
165         return contextFactory_;
166     }
167 
168     /**
169      * Performs initialization for the given webWindow.
170      * @param webWindow the web window to initialize for
171      */
172     @Override
173     public void initialize(final WebWindow webWindow, final Page page) {
174         WebAssert.notNull("webWindow", webWindow);
175 
176         if (shutdownPending_) {
177             return;
178         }
179 
180         getContextFactory().call(cx -> {
181             try {
182                 init(webWindow, page, cx);
183             }
184             catch (final Exception e) {
185                 LOG.error("Exception while initializing JavaScript for the page", e);
186                 throw new ScriptException(null, e); // BUG: null is not useful.
187             }
188             return null;
189         });
190     }
191 
192     /**
193      * Returns the JavaScriptExecutor.
194      * @return the JavaScriptExecutor or null if javascript is disabled
195      *         or no executor was required so far.
196      */
197     public JavaScriptExecutor getJavaScriptExecutor() {
198         return javaScriptExecutor_;
199     }
200 
201     /**
202      * Initializes all the JS stuff for the window.
203      * @param webWindow the web window
204      * @param context the current context
205      * @throws Exception if something goes wrong
206      */
207     private void init(final WebWindow webWindow, final Page page, final Context context) throws Exception {
208         final WebClient webClient = getWebClient();
209         final BrowserVersion browserVersion = webClient.getBrowserVersion();
210 
211         final Window jsWindowScope = new Window();
212         jsWindowScope.setClassName("Window");
213 
214         final Scriptable scope = context.initSafeStandardObjects(jsWindowScope);
215         configureRhino(webClient, browserVersion, jsWindowScope);
216 
217         final Map<Class<? extends Scriptable>, Scriptable> prototypes = new HashMap<>();
218         final Map<String, Scriptable> prototypesPerJSName = new HashMap<>();
219 
220         final ClassConfiguration windowConfig = jsConfig_.getWindowClassConfiguration();
221         final FunctionObject functionObject = new FunctionObject(jsWindowScope.getClassName(),
222                         windowConfig.getJsConstructor().getValue(), jsWindowScope);
223         ScriptableObject.defineProperty(jsWindowScope, "constructor", functionObject,
224                 ScriptableObject.DONTENUM  | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
225 
226         configureConstantsPropertiesAndFunctions(windowConfig, jsWindowScope);
227 
228         final HtmlUnitScriptable windowPrototype = configureClass(windowConfig, jsWindowScope);
229         jsWindowScope.setPrototype(windowPrototype);
230         prototypes.put(windowConfig.getHostClass(), windowPrototype);
231         prototypesPerJSName.put(windowConfig.getClassName(), windowPrototype);
232 
233         configureScope(jsWindowScope, windowConfig, functionObject, jsConfig_, browserVersion, prototypes, prototypesPerJSName);
234 
235         URLSearchParams.NativeParamsIterator.init(jsWindowScope, "URLSearchParams Iterator");
236         FormData.FormDataIterator.init(jsWindowScope, "FormData Iterator");
237 
238         // strange but this is the reality for browsers
239         // because there will be still some sites using this for browser detection the property is
240         // set to null
241         // https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browsers
242         // https://bugzilla.mozilla.org/show_bug.cgi?id=1442035
243         if (browserVersion.hasFeature(JS_WINDOW_INSTALL_TRIGGER_NULL)) {
244             jsWindowScope.put("InstallTrigger", jsWindowScope, null);
245         }
246 
247         // special handling for image/option
248         final Method imageCtor = HTMLImageElement.class.getDeclaredMethod("jsConstructorImage");
249         additionalCtor(jsWindowScope, prototypesPerJSName.get("HTMLImageElement"), imageCtor, "Image", "HTMLImageElement");
250         final Method optionCtor = HTMLOptionElement.class.getDeclaredMethod("jsConstructorOption",
251                 Object.class, String.class, boolean.class, boolean.class);
252         additionalCtor(jsWindowScope, prototypesPerJSName.get("HTMLOptionElement"), optionCtor, "Option", "HTMLOptionElement");
253 
254         if (!webClient.getOptions().isWebSocketEnabled()) {
255             deleteProperties(jsWindowScope, "WebSocket");
256         }
257 
258         jsWindowScope.setPrototypes(prototypes);
259         jsWindowScope.initialize(webWindow, page);
260 
261         applyPolyfills(webClient, browserVersion, context, jsWindowScope);
262     }
263 
264     /**
265      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
266      *
267      * @param jsScope the js scope to set up
268      * @param scopeConfig the {@link ClassConfiguration} that is used for the scope
269      * @param scopeContructorFunctionObject the (already registered) ctor
270      * @param jsConfig the complete jsConfig
271      * @param browserVersion the {@link BrowserVersion}
272      * @param prototypes map of prototypes
273      * @param prototypesPerJSName map of prototypes with the class name as key
274      * @throws Exception in case of error
275      */
276     public static void configureScope(final HtmlUnitScriptable jsScope,
277             final ClassConfiguration scopeConfig,
278             final FunctionObject scopeContructorFunctionObject,
279             final AbstractJavaScriptConfiguration jsConfig,
280             final BrowserVersion browserVersion,
281             final Map<Class<? extends Scriptable>, Scriptable> prototypes,
282             final Map<String, Scriptable> prototypesPerJSName) throws Exception {
283 
284         final Scriptable objectPrototype = ScriptableObject.getObjectPrototype(jsScope);
285 
286         final Map<String, Function> ctorPrototypesPerJSName = new HashMap<>();
287         for (final ClassConfiguration config : jsConfig.getAll()) {
288             final String jsClassName = config.getClassName();
289             Scriptable prototype = prototypesPerJSName.get(jsClassName);
290             final String extendedClassName =
291                     StringUtils.isEmptyOrNull(config.getExtendedClassName()) ? null : config.getExtendedClassName();
292 
293             // setup the prototypes
294             if (config == scopeConfig) {
295                 if (extendedClassName == null) {
296                     prototype.setPrototype(objectPrototype);
297                 }
298                 else {
299                     prototype.setPrototype(prototypesPerJSName.get(extendedClassName));
300                 }
301 
302                 // setup constructors
303                 addAsConstructorAndAlias(scopeContructorFunctionObject, jsScope, prototype, config);
304                 configureConstantsStaticPropertiesAndStaticFunctions(config, scopeContructorFunctionObject);
305 
306                 // adjust prototype if needed
307                 if (extendedClassName != null) {
308                     scopeContructorFunctionObject.setPrototype(ctorPrototypesPerJSName.get(extendedClassName));
309                 }
310             }
311             else {
312                 final HtmlUnitScriptable classPrototype = configureClass(config, jsScope);
313                 prototypes.put(config.getHostClass(), classPrototype);
314                 prototypesPerJSName.put(jsClassName, classPrototype);
315                 prototype = classPrototype;
316 
317                 if (extendedClassName == null) {
318                     classPrototype.setPrototype(objectPrototype);
319                 }
320                 else {
321                     classPrototype.setPrototype(prototypesPerJSName.get(extendedClassName));
322                 }
323 
324                 // setup constructors
325                 if (prototype != null) {
326                     final Map.Entry<String, Member> jsConstructor = config.getJsConstructor();
327                     if (jsConstructor == null) {
328                         final HtmlUnitScriptable constructor = config.getHostClass().getDeclaredConstructor().newInstance();
329                         constructor.setClassName(jsClassName);
330                         defineConstructor(jsScope, prototype, constructor);
331                         configureConstantsStaticPropertiesAndStaticFunctions(config, constructor);
332 
333                         if (config.isJsObject()) {
334                             jsScope.defineProperty(jsClassName, constructor, ScriptableObject.DONTENUM);
335                         }
336                     }
337                     else {
338                         final FunctionObject function = new FunctionObject(jsConstructor.getKey(), jsConstructor.getValue(), jsScope);
339                         ctorPrototypesPerJSName.put(jsClassName, function);
340 
341                         addAsConstructorAndAlias(function, jsScope, prototype, config);
342                         configureConstantsStaticPropertiesAndStaticFunctions(config, function);
343 
344                         if (!config.isJsObject()) {
345                             // addAsConstructorAndAlias(..) calls addAsConstructor() from core-js
346                             // addAsConstructor(..) registeres the ctor in the scope already
347                             // therefore we have to remove here
348                             jsScope.delete(prototype.getClassName());
349                         }
350 
351                         // adjust prototype if needed
352                         if (extendedClassName != null) {
353                             function.setPrototype(ctorPrototypesPerJSName.get(extendedClassName));
354                         }
355                     }
356                 }
357             }
358         }
359     }
360 
361     private static void addAsConstructorAndAlias(final FunctionObject function,
362             final Scriptable scope, final Scriptable prototype, final ClassConfiguration config) {
363         try {
364             function.addAsConstructor(scope, prototype, ScriptableObject.DONTENUM);
365 
366             final String alias = config.getJsConstructorAlias();
367             if (alias != null) {
368                 ScriptableObject.defineProperty(scope, alias, function, ScriptableObject.DONTENUM);
369             }
370         }
371         catch (final Exception e) {
372             // TODO see issue #1897
373             if (LOG.isWarnEnabled()) {
374                 final String newline = System.lineSeparator();
375                 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
376                         + e.getMessage() + newline
377                         + "prototype: " + prototype.getClassName(), e);
378             }
379         }
380     }
381 
382     private static void additionalCtor(final Window window, final Scriptable proto,
383             final Method ctorMethod, final String prop, final String clazzName) throws Exception {
384         final FunctionObject function = new FunctionObject(prop, ctorMethod, window);
385         final Object prototypeProperty = ScriptableObject.getProperty(window, clazzName);
386         try {
387             function.addAsConstructor(window, proto, ScriptableObject.DONTENUM);
388         }
389         catch (final Exception e) {
390             // TODO see issue #1897
391             if (LOG.isWarnEnabled()) {
392                 final String newline = System.lineSeparator();
393                 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
394                         + e.getMessage() + newline
395                         + "prototype: " + proto.getClassName(), e);
396             }
397         }
398         ScriptableObject.defineProperty(window, prop, function, ScriptableObject.DONTENUM);
399         ScriptableObject.defineProperty(window, clazzName, prototypeProperty, ScriptableObject.DONTENUM);
400     }
401 
402     /**
403      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
404      *
405      * @param webClient the WebClient
406      * @param browserVersion the BrowserVersion
407      * @param scope the window or the DedicatedWorkerGlobalScope
408      */
409     public static void configureRhino(final WebClient webClient,
410             final BrowserVersion browserVersion, final HtmlUnitScriptable scope) {
411 
412         NativeConsole.init(scope, false, webClient.getWebConsole());
413         final ScriptableObject console = (ScriptableObject) ScriptableObject.getProperty(scope, "console");
414         console.defineFunctionProperties(new String[] {"timeStamp"}, ConsoleCustom.class, ScriptableObject.DONTENUM);
415 
416         // Rhino defines too many methods for us, particularly since implementation of ECMAScript5
417         final ScriptableObject stringPrototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, "String");
418         deleteProperties(stringPrototype, "equals", "equalsIgnoreCase", "toSource");
419 
420         final ScriptableObject numberPrototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, "Number");
421         deleteProperties(numberPrototype, "toSource");
422         final ScriptableObject datePrototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, "Date");
423         deleteProperties(datePrototype, "toSource");
424 
425         deleteProperties(scope, "uneval");
426         removePrototypeProperties(scope, "Object", "toSource");
427         removePrototypeProperties(scope, "Array", "toSource");
428         removePrototypeProperties(scope, "Function", "toSource");
429 
430         deleteProperties(scope, "isXMLName");
431 
432         NativeFunctionToStringFunction.installFix(scope, browserVersion);
433 
434         numberPrototype.defineFunctionProperties(new String[] {"toLocaleString"},
435                 NumberCustom.class, ScriptableObject.DONTENUM);
436 
437         // remove some objects, that Rhino defines in top scope but that we don't want
438         deleteProperties(scope, "Continuation", "StopIteration");
439 
440         final ScriptableObject errorObject = (ScriptableObject) ScriptableObject.getProperty(scope, "Error");
441         if (browserVersion.hasFeature(JS_ERROR_STACK_TRACE_LIMIT)) {
442             errorObject.defineProperty("stackTraceLimit", 10, ScriptableObject.EMPTY);
443         }
444         else {
445             ScriptableObject.deleteProperty(errorObject, "stackTraceLimit");
446         }
447 
448         // add Intl
449         final Intl intl = new Intl();
450         intl.setParentScope(scope);
451         scope.defineProperty(intl.getClassName(), intl, ScriptableObject.DONTENUM);
452         intl.defineProperties(browserVersion);
453     }
454 
455     /**
456      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
457      *
458      * @param webClient the WebClient
459      * @param browserVersion the BrowserVersion
460      * @param context the current context
461      * @param scriptable the window or the DedicatedWorkerGlobalScope
462      * @throws IOException in case of problems
463      */
464     public static void applyPolyfills(final WebClient webClient, final BrowserVersion browserVersion,
465             final Context context, final HtmlUnitScriptable scriptable) throws IOException {
466 
467         if (webClient.getOptions().isFetchPolyfillEnabled()) {
468             Polyfill.getFetchPolyfill().apply(context, scriptable);
469         }
470     }
471 
472     private static void defineConstructor(final HtmlUnitScriptable window,
473             final Scriptable prototype, final ScriptableObject constructor) {
474         constructor.setParentScope(window);
475         try {
476             ScriptableObject.defineProperty(prototype, "constructor", constructor,
477                     ScriptableObject.DONTENUM  | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
478         }
479         catch (final Exception e) {
480             // TODO see issue #1897
481             if (LOG.isWarnEnabled()) {
482                 final String newline = System.lineSeparator();
483                 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
484                         + e.getMessage() + newline
485                         + "prototype: " + prototype.getClassName(), e);
486             }
487         }
488 
489         try {
490             ScriptableObject.defineProperty(constructor, "prototype", prototype,
491                     ScriptableObject.DONTENUM  | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
492         }
493         catch (final Exception e) {
494             // TODO see issue #1897
495             if (LOG.isWarnEnabled()) {
496                 final String newline = System.lineSeparator();
497                 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
498                         + e.getMessage() + newline
499                         + "prototype: " + prototype.getClassName(), e);
500             }
501         }
502     }
503 
504     /**
505      * Deletes the properties with the provided names.
506      * @param scope the scope from which properties have to be removed
507      * @param propertiesToDelete the list of property names
508      */
509     private static void deleteProperties(final Scriptable scope, final String... propertiesToDelete) {
510         for (final String property : propertiesToDelete) {
511             scope.delete(property);
512         }
513     }
514 
515     /**
516      * Removes prototype properties.
517      * @param scope the scope
518      * @param className the class for which properties should be removed
519      * @param properties the properties to remove
520      */
521     private static void removePrototypeProperties(final Scriptable scope, final String className,
522             final String... properties) {
523         final ScriptableObject prototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, className);
524         for (final String property : properties) {
525             prototype.delete(property);
526         }
527     }
528 
529     /**
530      * Configures the specified class for access via JavaScript.
531      * @param config the configuration settings for the class to be configured
532      * @param window the scope within which to configure the class
533      * @throws InstantiationException if the new class cannot be instantiated
534      * @throws IllegalAccessException if we don't have access to create the new instance
535      * @return the created prototype
536      * @throws Exception in case of errors
537      */
538     public static HtmlUnitScriptable configureClass(final ClassConfiguration config, final Scriptable window)
539         throws Exception {
540 
541         final HtmlUnitScriptable prototype = config.getHostClass().getDeclaredConstructor().newInstance();
542         prototype.setParentScope(window);
543         prototype.setClassName(config.getClassName());
544 
545         configureConstantsPropertiesAndFunctions(config, prototype);
546 
547         return prototype;
548     }
549 
550     /**
551      * Configures constants, static properties and static functions on the object.
552      * @param config the configuration for the object
553      * @param scriptable the object to configure
554      */
555     private static void configureConstantsStaticPropertiesAndStaticFunctions(final ClassConfiguration config,
556             final ScriptableObject scriptable) {
557         configureConstants(config, scriptable);
558         configureStaticProperties(config, scriptable);
559         configureStaticFunctions(config, scriptable);
560     }
561 
562     /**
563      * Configures constants, properties and functions on the object.
564      * @param config the configuration for the object
565      * @param scriptable the object to configure
566      */
567     private static void configureConstantsPropertiesAndFunctions(final ClassConfiguration config,
568             final ScriptableObject scriptable) {
569         configureConstants(config, scriptable);
570         configureProperties(config, scriptable);
571         configureFunctions(config, scriptable);
572         configureSymbolConstants(config, scriptable);
573         configureSymbols(config, scriptable);
574     }
575 
576     private static void configureFunctions(final ClassConfiguration config, final ScriptableObject scriptable) {
577         // the functions
578         final Map<String, Method> functionMap = config.getFunctionMap();
579         if (functionMap != null) {
580             for (final Entry<String, Method> functionInfo : functionMap.entrySet()) {
581                 final String functionName = functionInfo.getKey();
582                 final Method method = functionInfo.getValue();
583                 final FunctionObject functionObject = new FunctionObject(functionName, method, scriptable);
584                 scriptable.defineProperty(functionName, functionObject, ScriptableObject.EMPTY);
585             }
586         }
587     }
588 
589     private static void configureConstants(final ClassConfiguration config, final ScriptableObject scriptable) {
590         final List<ConstantInfo> constants = config.getConstants();
591         if (constants != null) {
592             for (final ConstantInfo constantInfo : constants) {
593                 scriptable.defineProperty(constantInfo.getName(), constantInfo.getValue(), constantInfo.getFlag());
594             }
595         }
596     }
597 
598     private static void configureProperties(final ClassConfiguration config, final ScriptableObject scriptable) {
599         final Map<String, PropertyInfo> propertyMap = config.getPropertyMap();
600         if (propertyMap != null) {
601             for (final Entry<String, PropertyInfo> propertyEntry : propertyMap.entrySet()) {
602                 final PropertyInfo info = propertyEntry.getValue();
603                 final Method readMethod = info.getReadMethod();
604                 final Method writeMethod = info.getWriteMethod();
605                 scriptable.defineProperty(propertyEntry.getKey(), null, readMethod, writeMethod, ScriptableObject.EMPTY);
606             }
607         }
608     }
609 
610     private static void configureStaticProperties(final ClassConfiguration config, final ScriptableObject scriptable) {
611         final Map<String, PropertyInfo> staticPropertyMap = config.getStaticPropertyMap();
612         if (staticPropertyMap != null) {
613             for (final Entry<String, ClassConfiguration.PropertyInfo> propertyEntry : staticPropertyMap.entrySet()) {
614                 final String propertyName = propertyEntry.getKey();
615                 final Method readMethod = propertyEntry.getValue().getReadMethod();
616                 final Method writeMethod = propertyEntry.getValue().getWriteMethod();
617                 final int flag = ScriptableObject.EMPTY;
618 
619                 scriptable.defineProperty(propertyName, null, readMethod, writeMethod, flag);
620             }
621         }
622     }
623 
624     private static void configureStaticFunctions(final ClassConfiguration config,
625             final ScriptableObject scriptable) {
626         final Map<String, Method> staticFunctionMap = config.getStaticFunctionMap();
627         if (staticFunctionMap != null) {
628             for (final Entry<String, Method> staticFunctionInfo : staticFunctionMap.entrySet()) {
629                 final String functionName = staticFunctionInfo.getKey();
630                 final Method method = staticFunctionInfo.getValue();
631                 final FunctionObject staticFunctionObject = new FunctionObject(functionName, method,
632                         scriptable);
633                 scriptable.defineProperty(functionName, staticFunctionObject, ScriptableObject.EMPTY);
634             }
635         }
636     }
637 
638     private static void configureSymbolConstants(final ClassConfiguration config, final ScriptableObject scriptable) {
639         final Map<Symbol, String> symbolConstantMap = config.getSymbolConstantMap();
640         if (symbolConstantMap != null) {
641             for (final Entry<Symbol, String> symbolInfo : symbolConstantMap.entrySet()) {
642                 scriptable.defineProperty(symbolInfo.getKey(), symbolInfo.getValue(), ScriptableObject.DONTENUM | ScriptableObject.READONLY);
643             }
644         }
645     }
646 
647     private static void configureSymbols(final ClassConfiguration config,
648             final ScriptableObject scriptable) {
649         final Map<Symbol, Method> symbolMap = config.getSymbolMap();
650         if (symbolMap != null) {
651             for (final Entry<Symbol, Method> symbolInfo : symbolMap.entrySet()) {
652                 final Symbol symbol = symbolInfo.getKey();
653                 final Method method = symbolInfo.getValue();
654                 final String methodName = method.getName();
655 
656                 final Callable symbolFunction = scriptable.has(methodName, scriptable)
657                         ? (Callable) scriptable.get(methodName, scriptable)
658                         : new FunctionObject(methodName, method, scriptable);
659                 scriptable.defineProperty(symbol, symbolFunction, ScriptableObject.DONTENUM);
660             }
661         }
662     }
663 
664     /**
665      * Register WebWindow with the JavaScriptExecutor.
666      * @param webWindow the WebWindow to be registered.
667      */
668     @Override
669     public synchronized void registerWindowAndMaybeStartEventLoop(final WebWindow webWindow) {
670         if (shutdownPending_) {
671             return;
672         }
673 
674         final WebClient webClient = getWebClient();
675         if (webClient != null) {
676             if (javaScriptExecutor_ == null) {
677                 javaScriptExecutor_ = BackgroundJavaScriptFactory.theFactory().createJavaScriptExecutor(webClient);
678             }
679             javaScriptExecutor_.addWindow(webWindow);
680         }
681     }
682 
683     /**
684      * {@inheritDoc}
685      */
686     @Override
687     public void prepareShutdown() {
688         shutdownPending_ = true;
689     }
690 
691     /**
692      * Shutdown the JavaScriptEngine.
693      */
694     @Override
695     public void shutdown() {
696         webClient_ = null;
697         contextFactory_ = null;
698         jsConfig_ = null;
699 
700         if (javaScriptExecutor_ != null) {
701             javaScriptExecutor_.shutdown();
702             javaScriptExecutor_ = null;
703         }
704         if (postponedActions_ != null) {
705             postponedActions_.remove();
706         }
707         if (javaScriptRunning_ != null) {
708             javaScriptRunning_.remove();
709         }
710         holdPostponedActions_ = false;
711     }
712 
713     /**
714      * {@inheritDoc}
715      */
716     @Override
717     public Script compile(final HtmlPage owningPage, final Scriptable scope, final String sourceCode,
718             final String sourceName, final int startLine) {
719         WebAssert.notNull("sourceCode", sourceCode);
720 
721         if (LOG.isTraceEnabled()) {
722             final String newline = System.lineSeparator();
723             LOG.trace("Javascript compile " + sourceName + newline + sourceCode + newline);
724         }
725 
726         final HtmlUnitCompileContextAction action = new HtmlUnitCompileContextAction(owningPage, sourceCode, sourceName, startLine);
727         return (Script) getContextFactory().callSecured(action, owningPage);
728     }
729 
730     /**
731      * Forwards this to the {@link HtmlUnitContextFactory} but with checking shutdown handling.
732      *
733      * @param <T> return type of the action
734      * @param action the contextAction
735      * @param page the page
736      * @return the result of the call
737      */
738     public final <T> T callSecured(final ContextAction<T> action, final HtmlPage page) {
739         if (shutdownPending_ || webClient_ == null) {
740             // shutdown was already called
741             return null;
742         }
743 
744         return getContextFactory().callSecured(action, page);
745     }
746 
747     /**
748      * {@inheritDoc}
749      */
750     @Override
751     public Object execute(final HtmlPage page,
752                            final Scriptable scope,
753                            final String sourceCode,
754                            final String sourceName,
755                            final int startLine) {
756         final Script script = compile(page, scope, sourceCode, sourceName, startLine);
757         if (script == null) {
758             // happens with syntax error + throwExceptionOnScriptError = false
759             return null;
760         }
761         return execute(page, scope, script);
762     }
763 
764     /**
765      * {@inheritDoc}
766      */
767     @Override
768     public Object execute(final HtmlPage page, final Scriptable scope, final Script script) {
769         if (shutdownPending_ || webClient_ == null) {
770             // shutdown was already called
771             return null;
772         }
773 
774         final HtmlUnitContextAction action = new HtmlUnitContextAction(page) {
775             @Override
776             public Object doRun(final Context cx) {
777                 return script.exec(cx, scope, scope);
778             }
779 
780             @Override
781             protected String getSourceCode(final Context cx) {
782                 return null;
783             }
784         };
785 
786         return getContextFactory().callSecured(action, page);
787     }
788 
789     /**
790      * Calls a JavaScript function and return the result.
791      * @param page the page
792      * @param javaScriptFunction the function to call
793      * @param thisObject the this object for class method calls
794      * @param args the list of arguments to pass to the function
795      * @param node the HTML element that will act as the context
796      * @return the result of the function call
797      */
798     public Object callFunction(
799             final HtmlPage page,
800             final Function javaScriptFunction,
801             final Scriptable thisObject,
802             final Object[] args,
803             final DomNode node) {
804 
805         final Scriptable scope = getScope(page, node);
806 
807         return callFunction(page, javaScriptFunction, scope, thisObject, args);
808     }
809 
810     /**
811      * Calls the given function taking care of synchronization issues.
812      * @param page the interactive page that caused this script to executed
813      * @param function the JavaScript function to execute
814      * @param scope the execution scope
815      * @param thisObject the 'this' object
816      * @param args the function's arguments
817      * @return the function result
818      */
819     public Object callFunction(final HtmlPage page, final Function function,
820             final Scriptable scope, final Scriptable thisObject, final Object[] args) {
821         if (shutdownPending_ || webClient_ == null) {
822             // shutdown was already called
823             return null;
824         }
825 
826         final HtmlUnitContextAction action = new HtmlUnitContextAction(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().callSecured(action, page);
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 their own evaluation 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      * Special ContextAction only for compiling. This reduces some code and avoid
861      * some calls.
862      */
863     private final class HtmlUnitCompileContextAction implements ContextAction<Object> {
864         private final HtmlPage page_;
865         private final String sourceCode_;
866         private final String sourceName_;
867         private final int startLine_;
868 
869         HtmlUnitCompileContextAction(final HtmlPage page, final String sourceCode, final String sourceName, final int startLine) {
870             page_ = page;
871             sourceCode_ = sourceCode;
872             sourceName_ = sourceName;
873             startLine_ = startLine;
874         }
875 
876         @Override
877         public Object run(final Context cx) {
878             try {
879                 final Object response;
880                 cx.putThreadLocal(KEY_STARTING_PAGE, page_);
881                 synchronized (page_) { // 2 scripts can't be executed in parallel for one page
882                     if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
883                         return null; // page has been unloaded
884                     }
885                     response = cx.compileString(sourceCode_, sourceName_, startLine_, null);
886 
887                 }
888 
889                 return response;
890             }
891             catch (final Exception e) {
892                 handleJavaScriptException(new ScriptException(page_, e, sourceCode_), true);
893                 return null;
894             }
895             catch (final TimeoutError e) {
896                 handleJavaScriptTimeoutError(page_, e);
897                 return null;
898             }
899         }
900     }
901 
902     /**
903      * Facility for ContextAction usage.
904      * ContextAction should be preferred because according to Rhino doc it
905      * "guarantees proper association of Context instances with the current thread and is faster".
906      */
907     private abstract class HtmlUnitContextAction implements ContextAction<Object> {
908         private final HtmlPage page_;
909 
910         HtmlUnitContextAction(final HtmlPage page) {
911             page_ = page;
912         }
913 
914         @Override
915         public final Object run(final Context cx) {
916             final Boolean javaScriptAlreadyRunning = javaScriptRunning_.get();
917             javaScriptRunning_.set(Boolean.TRUE);
918 
919             try {
920                 final Object response;
921                 synchronized (page_) { // 2 scripts can't be executed in parallel for one page
922                     if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
923                         return null; // page has been unloaded
924                     }
925                     response = doRun(cx);
926                 }
927 
928                 cx.processMicrotasks();
929 
930                 // doProcessPostponedActions is synchronized
931                 // moved out of the sync block to avoid deadlocks
932                 if (!holdPostponedActions_) {
933                     doProcessPostponedActions();
934                 }
935 
936                 return response;
937             }
938             catch (final Exception e) {
939                 handleJavaScriptException(new ScriptException(page_, e, getSourceCode(cx)), true);
940                 return null;
941             }
942             catch (final TimeoutError e) {
943                 handleJavaScriptTimeoutError(page_, e);
944                 return null;
945             }
946             finally {
947                 javaScriptRunning_.set(javaScriptAlreadyRunning);
948             }
949         }
950 
951         protected abstract Object doRun(Context cx);
952 
953         protected abstract String getSourceCode(Context cx);
954     }
955 
956     private void doProcessPostponedActions() {
957         holdPostponedActions_ = false;
958 
959         final WebClient webClient = getWebClient();
960         if (webClient == null) {
961             // shutdown was already called
962             postponedActions_.set(null);
963             return;
964         }
965 
966         try {
967             webClient.loadDownloadedResponses();
968         }
969         catch (final RuntimeException e) {
970             throw e;
971         }
972         catch (final Exception e) {
973             throw new RuntimeException(e);
974         }
975 
976         final List<PostponedAction> actions = postponedActions_.get();
977         if (actions != null && !actions.isEmpty()) {
978             postponedActions_.set(new ArrayList<>());
979             try {
980                 for (final PostponedAction action : actions) {
981                     if (LOG.isDebugEnabled()) {
982                         LOG.debug("Processing PostponedAction " + action);
983                     }
984 
985                     // verify that the page that registered this PostponedAction is still alive
986                     if (action.isStillAlive()) {
987                         action.execute();
988                     }
989                 }
990             }
991             catch (final RuntimeException e) {
992                 throw e;
993             }
994             catch (final Exception e) {
995                 throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
996             }
997         }
998     }
999 
1000     /**
1001      * Adds an action that should be executed first when the script currently being executed has finished.
1002      * @param action the action
1003      */
1004     @Override
1005     public void addPostponedAction(final PostponedAction action) {
1006         if (shutdownPending_) {
1007             return;
1008         }
1009 
1010         List<PostponedAction> actions = postponedActions_.get();
1011         if (actions == null) {
1012             actions = new ArrayList<>();
1013             postponedActions_.set(actions);
1014         }
1015         actions.add(action);
1016     }
1017 
1018     /**
1019      * Handles an exception that occurred during execution of JavaScript code.
1020      * @param scriptException the exception
1021      * @param triggerOnError if true, this triggers the onerror handler
1022      */
1023     protected void handleJavaScriptException(final ScriptException scriptException, final boolean triggerOnError) {
1024         final WebClient webClient = getWebClient();
1025         if (shutdownPending_ || webClient == null) {
1026             // shutdown was already called
1027             return;
1028         }
1029 
1030         // Trigger window.onerror, if it has been set.
1031         final HtmlPage page = scriptException.getPage();
1032         if (triggerOnError && page != null) {
1033             final WebWindow window = page.getEnclosingWindow();
1034             if (window != null) {
1035                 final Window w = window.getScriptableObject();
1036                 if (w != null) {
1037                     try {
1038                         w.triggerOnError(scriptException);
1039                     }
1040                     catch (final Exception e) {
1041                         handleJavaScriptException(new ScriptException(page, e, null), false);
1042                     }
1043                 }
1044             }
1045         }
1046 
1047         webClient.getJavaScriptErrorListener().scriptException(page, scriptException);
1048         // Throw a Java exception if the user wants us to.
1049         if (webClient.getOptions().isThrowExceptionOnScriptError()) {
1050             throw scriptException;
1051         }
1052     }
1053 
1054     /**
1055      * Handles an exception that occurred during execution of JavaScript code.
1056      * @param page the page in which the script causing this exception was executed
1057      * @param e the timeout error that was thrown from the script engine
1058      */
1059     protected void handleJavaScriptTimeoutError(final HtmlPage page, final TimeoutError e) {
1060         final WebClient webClient = getWebClient();
1061         if (shutdownPending_ || webClient == null) {
1062             // shutdown was already called
1063             return;
1064         }
1065 
1066         webClient.getJavaScriptErrorListener().timeoutError(page, e.getAllowedTime(), e.getExecutionTime());
1067         if (webClient.getOptions().isThrowExceptionOnScriptError()) {
1068             throw new RuntimeException(e);
1069         }
1070         LOG.info("Caught script timeout error", e);
1071     }
1072 
1073     /**
1074      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1075      * Indicates that no postponed action should be executed.
1076      */
1077     @Override
1078     public void holdPosponedActions() {
1079         holdPostponedActions_ = true;
1080     }
1081 
1082     /**
1083      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1084      * Process postponed actions, if any.
1085      */
1086     @Override
1087     public void processPostponedActions() {
1088         doProcessPostponedActions();
1089     }
1090 
1091     /**
1092      * Re-initializes transient fields when an object of this type is deserialized.
1093      */
1094     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
1095         in.defaultReadObject();
1096         initTransientFields();
1097     }
1098 
1099     private void initTransientFields() {
1100         javaScriptRunning_ = new ThreadLocal<>();
1101         postponedActions_ = new ThreadLocal<>();
1102         holdPostponedActions_ = false;
1103         shutdownPending_ = false;
1104     }
1105 
1106     /**
1107      * Gets the class of the JavaScript object for the node class.
1108      * @param c the node class {@link DomNode} or some subclass.
1109      * @return {@code null} if none found
1110      */
1111     public Class<? extends HtmlUnitScriptable> getJavaScriptClass(final Class<?> c) {
1112         return jsConfig_.getDomJavaScriptMappingFor(c);
1113     }
1114 
1115     /**
1116      * Gets the associated configuration.
1117      * @return the configuration
1118      */
1119     @Override
1120     public JavaScriptConfiguration getJavaScriptConfiguration() {
1121         return jsConfig_;
1122     }
1123 
1124     /**
1125      * Returns the javascript timeout.
1126      * @return the javascript timeout
1127      */
1128     @Override
1129     public long getJavaScriptTimeout() {
1130         return getContextFactory().getTimeout();
1131     }
1132 
1133     /**
1134      * Sets the javascript timeout.
1135      * @param timeout the timeout
1136      */
1137     @Override
1138     public void setJavaScriptTimeout(final long timeout) {
1139         getContextFactory().setTimeout(timeout);
1140     }
1141 
1142     /**
1143      * Convert the value to a JavaScript Number value.
1144      *
1145      * @param value a JavaScript value
1146      * @return the corresponding double value converted using the ECMA rules
1147      */
1148     public static double toNumber(final Object value) {
1149         return ScriptRuntime.toNumber(value);
1150     }
1151 
1152     /**
1153      * Convert the value to a JavaScript String value.
1154      *
1155      * @param value a JavaScript value
1156      * @return the corresponding String value converted using the ECMA rules
1157      */
1158     public static String toString(final Object value) {
1159         return ScriptRuntime.toString(value);
1160     }
1161 
1162     /**
1163      * Convert the value to a JavaScript boolean value.
1164      *
1165      * @param value a JavaScript value
1166      * @return the corresponding boolean value converted using the ECMA rules
1167      */
1168     public static boolean toBoolean(final Object value) {
1169         return ScriptRuntime.toBoolean(value);
1170     }
1171 
1172     /**
1173      * Rethrow the exception wrapping it as the script runtime exception.
1174      *
1175      * @param e the exception to rethrow
1176      * @return RuntimeException as dummy the method always throws
1177      */
1178     public static RuntimeException throwAsScriptRuntimeEx(final Throwable e) {
1179         throw Context.throwAsScriptRuntimeEx(e);
1180     }
1181 
1182     /**
1183      * Report a runtime error using the error reporter for the current thread.
1184      *
1185      * @param message the error message to report
1186      * @return RuntimeException as dummy the method always throws
1187      */
1188     public static RuntimeException reportRuntimeError(final String message) {
1189         throw Context.reportRuntimeError(message);
1190     }
1191 
1192     /**
1193      * Report a runtime error using the error reporter for the current thread.
1194      *
1195      * @param message the error message to report
1196      * @return EcmaError
1197      */
1198     public static EcmaError syntaxError(final String message) {
1199         return ScriptRuntime.syntaxError(message);
1200     }
1201 
1202     /**
1203      * Report a runtime error using the error reporter for the current thread.
1204      *
1205      * @param message the error message to report
1206      * @return EcmaError
1207      */
1208     public static EcmaError typeError(final String message) {
1209         return ScriptRuntime.typeError(message);
1210     }
1211 
1212     /**
1213      * Report a TypeError with the message "Illegal constructor.".
1214      *
1215      * @return EcmaError
1216      */
1217     public static EcmaError typeErrorIllegalConstructor() {
1218         throw JavaScriptEngine.typeError("Illegal constructor.");
1219     }
1220 
1221     /**
1222      * Report a runtime error using the error reporter for the current thread.
1223      *
1224      * @param message the error message to report
1225      * @return EcmaError
1226      */
1227     public static EcmaError rangeError(final String message) {
1228         return ScriptRuntime.rangeError(message);
1229     }
1230 
1231     /**
1232      * @param error the error
1233      * @param message the message
1234      * @return a new EcmaError
1235      */
1236     public static EcmaError constructError(final String error, final String message) {
1237         return ScriptRuntime.constructError(error, message);
1238     }
1239 
1240     /**
1241      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1242      *
1243      * Creates a {@link DOMException} and encapsulates it into a Rhino-compatible exception.
1244      *
1245      * @param scope the parent scope
1246      * @param message the exception message
1247      * @param type the exception type
1248      * @return the created exception
1249      */
1250     public static RhinoException asJavaScriptException(final HtmlUnitScriptable scope, final String message, final int type) {
1251         final DOMException domException = new DOMException(message, type);
1252         domException.setParentScope(scope);
1253         domException.setPrototype(scope.getPrototype(DOMException.class));
1254 
1255         final EcmaError helper = ScriptRuntime.syntaxError("helper");
1256         String fileName = helper.sourceName();
1257         if (fileName != null) {
1258             fileName = fileName.replaceFirst("script in (.*) from .*", "$1");
1259         }
1260         domException.setLocation(fileName, helper.lineNumber());
1261 
1262         return new JavaScriptException(domException, fileName, helper.lineNumber());
1263     }
1264 
1265     /**
1266      * Create a new javascript object by calling the ctor with the provided args.
1267      *
1268      * @param scope the scope to create the object in
1269      * @param constructorName the name of the ctor function to call
1270      * @param args the args
1271      * @return the new object
1272      */
1273     public static Scriptable newObject(final Scriptable scope, final String constructorName, final Object[] args) {
1274         return ScriptRuntime.newObject(Context.getCurrentContext(), scope, constructorName, args);
1275     }
1276 
1277     /**
1278      * Create a new JavaScript object.
1279      *
1280      * <p>Equivalent to evaluating "new Object()".
1281      *
1282      * @param scope the scope to search for the constructor and to evaluate against
1283      * @return the new object
1284      */
1285     public static Scriptable newObject(final Scriptable scope) {
1286         final NativeObject result = new NativeObject();
1287         ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Object);
1288         return result;
1289     }
1290 
1291     /**
1292      * Create an array with a specified initial length.
1293      *
1294      * @param scope the scope to create the object in
1295      * @param length the initial length (JavaScript arrays may have additional properties added
1296      *     dynamically).
1297      * @return the new array object
1298      */
1299     public static Scriptable newArray(final Scriptable scope, final int length) {
1300         final NativeArray result = new NativeArray(length);
1301         ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1302         return result;
1303     }
1304 
1305     /**
1306      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1307      *
1308      * Create a new ArrayIterator of type NativeArrayIterator.ARRAY_ITERATOR_TYPE.KEYS
1309      *
1310      * @param scope the scope to create the object in
1311      * @param arrayLike the backend
1312      * @return the new NativeArrayIterator
1313      */
1314     public static Scriptable newArrayIteratorTypeKeys(final Scriptable scope, final Scriptable arrayLike) {
1315         return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.KEYS);
1316     }
1317 
1318     /**
1319      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1320      *
1321      * Create a new ArrayIterator of type NativeArrayIterator.ARRAY_ITERATOR_TYPE.VALUES
1322      *
1323      * @param scope the scope to create the object in
1324      * @param arrayLike the backend
1325      * @return the new NativeArrayIterator
1326      */
1327     public static Scriptable newArrayIteratorTypeValues(final Scriptable scope, final Scriptable arrayLike) {
1328         return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.VALUES);
1329     }
1330 
1331     /**
1332      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1333      *
1334      * Create a new ArrayIterator of type NativeArrayIterator.ARRAY_ITERATOR_TYPE.ENTRIES
1335      *
1336      * @param scope the scope to create the object in
1337      * @param arrayLike the backend
1338      * @return the new NativeArrayIterator
1339      */
1340     public static Scriptable newArrayIteratorTypeEntries(final Scriptable scope, final Scriptable arrayLike) {
1341         return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.ENTRIES);
1342     }
1343 
1344     /**
1345      * Create an array with a specified initial length.
1346      *
1347      * @param scope the scope to create the object in
1348      * @param elements the initial elements. Each object in this array must be an acceptable
1349      *     JavaScript type and type of array should be exactly Object[], not SomeObjectSubclass[].
1350      * @return the new array object
1351      */
1352     public static Scriptable newArray(final Scriptable scope, final Object[] elements) {
1353         if (elements.getClass().getComponentType() != ScriptRuntime.ObjectClass) {
1354             throw new IllegalArgumentException();
1355         }
1356 
1357         final NativeArray result = new NativeArray(elements);
1358         ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1359         return result;
1360     }
1361 
1362     /**
1363      * @param o the object to convert
1364      * @return int value
1365      */
1366     public static int toInt32(final Object o) {
1367         return ScriptRuntime.toInt32(o);
1368     }
1369 
1370     /**
1371      * @param o the object to convert
1372      * @return double value
1373      */
1374     public static double toInteger(final Object o) {
1375         return ScriptRuntime.toInteger(o);
1376     }
1377 
1378     /**
1379      * @param args an array
1380      * @param index the index in the array
1381      * @return double value
1382      */
1383     public static double toInteger(final Object[] args, final int index) {
1384         return ScriptRuntime.toInteger(args, index);
1385     }
1386 
1387     /**
1388      * @param obj the value to check
1389      * @return whether obj is undefined
1390      */
1391     public static boolean isUndefined(final Object obj) {
1392         return org.htmlunit.corejs.javascript.Undefined.isUndefined(obj);
1393     }
1394 
1395     /**
1396      * @param obj the value to check
1397      * @return whether obj is NAN
1398      */
1399     public static boolean isNaN(final Object obj) {
1400         return ScriptRuntime.isNaN(obj);
1401     }
1402 
1403     /**
1404      * @param obj the value to check
1405      * @return whether obj is an Array
1406      */
1407     public static boolean isArray(final Object obj) {
1408         return (obj instanceof Scriptable)
1409                     && "Array".equals(((Scriptable) obj).getClassName());
1410     }
1411 
1412     /**
1413      * @return the top call scope
1414      */
1415     public static Scriptable getTopCallScope() {
1416         return ScriptRuntime.getTopCallScope(Context.getCurrentContext());
1417     }
1418 
1419     /**
1420      * Tries to uncompress the JavaScript code in the provided response.
1421      * @param scriptSource the souce
1422      * @param scriptName the name
1423      * @return the uncompressed JavaScript code
1424      */
1425     public static String uncompressJavaScript(final String scriptSource, final String scriptName) {
1426         final ContextFactory factory = new ContextFactory();
1427         final ContextAction<Object> action = cx -> {
1428             cx.setInterpretedMode(true);
1429             final Script script = cx.compileString(scriptSource, scriptName, 0, null);
1430             return cx.decompileScript(script, 4);
1431         };
1432 
1433         return (String) factory.call(action);
1434     }
1435 
1436     /**
1437      * Evaluates the <code>FindProxyForURL</code> method of the specified content.
1438      * @param browserVersion the browser version to use
1439      * @param content the JavaScript content
1440      * @param url the URL to be retrieved
1441      * @return semicolon-separated result
1442      */
1443     public static String evaluateProxyAutoConfig(final BrowserVersion browserVersion, final String content, final URL url) {
1444         try (Context cx = Context.enter()) {
1445             final ProxyAutoConfigJavaScriptConfiguration jsConfig =
1446                     ProxyAutoConfigJavaScriptConfiguration.getInstance(browserVersion);
1447 
1448             final ScriptableObject scope = cx.initSafeStandardObjects();
1449 
1450             for (final ClassConfiguration config : jsConfig.getAll()) {
1451                 configureFunctions(config, scope);
1452             }
1453 
1454             cx.evaluateString(scope, "var ProxyConfig = function() {}; ProxyConfig.bindings = {}; ProxyConfig", "<init>", 1, null);
1455             cx.evaluateString(scope, content, "<Proxy Auto-Config>", 1, null);
1456 
1457             final Object[] functionArgs = {url.toExternalForm(), url.getHost()};
1458             final Function f = (Function) scope.get("FindProxyForURL", scope);
1459             final Object result = f.call(cx, scope, scope, functionArgs);
1460             return toString(result);
1461         }
1462     }
1463 }