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