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