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      * @return {@code true} if JavaScript is running
890      */
891     @Override
892     public boolean isScriptRunning() {
893         return Boolean.TRUE.equals(javaScriptRunning_.get());
894     }
895 
896     /**
897      * Special ContextAction only for compiling. This reduces some code and avoid
898      * some calls.
899      */
900     private final class HtmlUnitCompileContextAction implements ContextAction<Object> {
901         private final HtmlPage page_;
902         private final String sourceCode_;
903         private final String sourceName_;
904         private final int startLine_;
905 
906         HtmlUnitCompileContextAction(final HtmlPage page, final String sourceCode, final String sourceName, final int startLine) {
907             page_ = page;
908             sourceCode_ = sourceCode;
909             sourceName_ = sourceName;
910             startLine_ = startLine;
911         }
912 
913         @Override
914         public Object run(final Context cx) {
915             try {
916                 final Object response;
917                 cx.putThreadLocal(KEY_STARTING_PAGE, page_);
918                 synchronized (page_) { // 2 scripts can't be executed in parallel for one page
919                     if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
920                         return null; // page has been unloaded
921                     }
922                     response = cx.compileString(sourceCode_, sourceName_, startLine_, null);
923 
924                 }
925 
926                 return response;
927             }
928             catch (final Exception e) {
929                 handleJavaScriptException(new ScriptException(page_, e, sourceCode_), true);
930                 return null;
931             }
932             catch (final TimeoutError e) {
933                 handleJavaScriptTimeoutError(page_, e);
934                 return null;
935             }
936         }
937     }
938 
939     /**
940      * Facility for ContextAction usage.
941      * ContextAction should be preferred because according to Rhino doc it
942      * "guarantees proper association of Context instances with the current thread and is faster".
943      */
944     private abstract class HtmlUnitContextAction implements ContextAction<Object> {
945         private final HtmlPage page_;
946 
947         HtmlUnitContextAction(final HtmlPage page) {
948             page_ = page;
949         }
950 
951         @Override
952         public final Object run(final Context cx) {
953             final Boolean javaScriptAlreadyRunning = javaScriptRunning_.get();
954             javaScriptRunning_.set(Boolean.TRUE);
955 
956             try {
957                 final Object response;
958                 synchronized (page_) { // 2 scripts can't be executed in parallel for one page
959                     if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
960                         return null; // page has been unloaded
961                     }
962                     response = doRun(cx);
963                 }
964 
965                 cx.processMicrotasks();
966 
967                 // doProcessPostponedActions is synchronized
968                 // moved out of the sync block to avoid deadlocks
969                 if (!holdPostponedActions_) {
970                     doProcessPostponedActions();
971                 }
972 
973                 return response;
974             }
975             catch (final Exception e) {
976                 handleJavaScriptException(new ScriptException(page_, e, getSourceCode(cx)), true);
977                 return null;
978             }
979             catch (final TimeoutError e) {
980                 handleJavaScriptTimeoutError(page_, e);
981                 return null;
982             }
983             finally {
984                 javaScriptRunning_.set(javaScriptAlreadyRunning);
985             }
986         }
987 
988         protected abstract Object doRun(Context cx);
989 
990         protected abstract String getSourceCode(Context cx);
991     }
992 
993     private void doProcessPostponedActions() {
994         holdPostponedActions_ = false;
995 
996         final WebClient webClient = getWebClient();
997         if (webClient == null) {
998             // shutdown was already called
999             postponedActions_.set(null);
1000             return;
1001         }
1002 
1003         try {
1004             webClient.loadDownloadedResponses();
1005         }
1006         catch (final RuntimeException e) {
1007             throw e;
1008         }
1009         catch (final Exception e) {
1010             throw new RuntimeException(e);
1011         }
1012 
1013         final List<PostponedAction> actions = postponedActions_.get();
1014         if (actions != null && !actions.isEmpty()) {
1015             postponedActions_.set(new ArrayList<>());
1016             try {
1017                 for (final PostponedAction action : actions) {
1018                     if (LOG.isDebugEnabled()) {
1019                         LOG.debug("Processing PostponedAction " + action);
1020                     }
1021 
1022                     // verify that the page that registered this PostponedAction is still alive
1023                     if (action.isStillAlive()) {
1024                         action.execute();
1025                     }
1026                 }
1027             }
1028             catch (final RuntimeException e) {
1029                 throw e;
1030             }
1031             catch (final Exception e) {
1032                 throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
1033             }
1034         }
1035     }
1036 
1037     /**
1038      * Adds an action that should be executed first when the script currently being executed has finished.
1039      * @param action the action
1040      */
1041     @Override
1042     public void addPostponedAction(final PostponedAction action) {
1043         if (shutdownPending_) {
1044             return;
1045         }
1046 
1047         List<PostponedAction> actions = postponedActions_.get();
1048         if (actions == null) {
1049             actions = new ArrayList<>();
1050             postponedActions_.set(actions);
1051         }
1052         actions.add(action);
1053     }
1054 
1055     /**
1056      * Handles an exception that occurred during execution of JavaScript code.
1057      * @param scriptException the exception
1058      * @param triggerOnError if true, this triggers the onerror handler
1059      */
1060     protected void handleJavaScriptException(final ScriptException scriptException, final boolean triggerOnError) {
1061         final WebClient webClient = getWebClient();
1062         if (shutdownPending_ || webClient == null) {
1063             // shutdown was already called
1064             return;
1065         }
1066 
1067         // Trigger window.onerror, if it has been set.
1068         final HtmlPage page = scriptException.getPage();
1069         if (triggerOnError && page != null) {
1070             final WebWindow window = page.getEnclosingWindow();
1071             if (window != null) {
1072                 final Window w = window.getScriptableObject();
1073                 if (w != null) {
1074                     try {
1075                         w.triggerOnError(scriptException);
1076                     }
1077                     catch (final Exception e) {
1078                         handleJavaScriptException(new ScriptException(page, e, null), false);
1079                     }
1080                 }
1081             }
1082         }
1083 
1084         webClient.getJavaScriptErrorListener().scriptException(page, scriptException);
1085         // Throw a Java exception if the user wants us to.
1086         if (webClient.getOptions().isThrowExceptionOnScriptError()) {
1087             throw scriptException;
1088         }
1089     }
1090 
1091     /**
1092      * Handles an exception that occurred during execution of JavaScript code.
1093      * @param page the page in which the script causing this exception was executed
1094      * @param e the timeout error that was thrown from the script engine
1095      */
1096     protected void handleJavaScriptTimeoutError(final HtmlPage page, final TimeoutError e) {
1097         final WebClient webClient = getWebClient();
1098         if (shutdownPending_ || webClient == null) {
1099             // shutdown was already called
1100             return;
1101         }
1102 
1103         webClient.getJavaScriptErrorListener().timeoutError(page, e.getAllowedTime(), e.getExecutionTime());
1104         if (webClient.getOptions().isThrowExceptionOnScriptError()) {
1105             throw new RuntimeException(e);
1106         }
1107         LOG.info("Caught script timeout error", e);
1108     }
1109 
1110     /**
1111      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1112      * Indicates that no postponed action should be executed.
1113      */
1114     @Override
1115     public void holdPosponedActions() {
1116         holdPostponedActions_ = true;
1117     }
1118 
1119     /**
1120      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1121      * Process postponed actions, if any.
1122      */
1123     @Override
1124     public void processPostponedActions() {
1125         doProcessPostponedActions();
1126     }
1127 
1128     /**
1129      * Re-initializes transient fields when an object of this type is deserialized.
1130      */
1131     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
1132         in.defaultReadObject();
1133         initTransientFields();
1134     }
1135 
1136     private void initTransientFields() {
1137         javaScriptRunning_ = new ThreadLocal<>();
1138         postponedActions_ = new ThreadLocal<>();
1139         holdPostponedActions_ = false;
1140         shutdownPending_ = false;
1141     }
1142 
1143     /**
1144      * Gets the class of the JavaScript object for the node class.
1145      * @param c the node class {@link DomNode} or some subclass.
1146      * @return {@code null} if none found
1147      */
1148     public Class<? extends HtmlUnitScriptable> getJavaScriptClass(final Class<?> c) {
1149         return jsConfig_.getDomJavaScriptMappingFor(c);
1150     }
1151 
1152     /**
1153      * Gets the associated configuration.
1154      * @return the configuration
1155      */
1156     @Override
1157     public JavaScriptConfiguration getJavaScriptConfiguration() {
1158         return jsConfig_;
1159     }
1160 
1161     /**
1162      * Returns the javascript timeout.
1163      * @return the javascript timeout
1164      */
1165     @Override
1166     public long getJavaScriptTimeout() {
1167         return getContextFactory().getTimeout();
1168     }
1169 
1170     /**
1171      * Sets the javascript timeout.
1172      * @param timeout the timeout
1173      */
1174     @Override
1175     public void setJavaScriptTimeout(final long timeout) {
1176         getContextFactory().setTimeout(timeout);
1177     }
1178 
1179     /**
1180      * Convert the value to a JavaScript Number value.
1181      *
1182      * @param value a JavaScript value
1183      * @return the corresponding double value converted using the ECMA rules
1184      */
1185     public static double toNumber(final Object value) {
1186         return ScriptRuntime.toNumber(value);
1187     }
1188 
1189     /**
1190      * Convert the value to a JavaScript String value.
1191      *
1192      * @param value a JavaScript value
1193      * @return the corresponding String value converted using the ECMA rules
1194      */
1195     public static String toString(final Object value) {
1196         return ScriptRuntime.toString(value);
1197     }
1198 
1199     /**
1200      * Convert the value to a JavaScript boolean value.
1201      *
1202      * @param value a JavaScript value
1203      * @return the corresponding boolean value converted using the ECMA rules
1204      */
1205     public static boolean toBoolean(final Object value) {
1206         return ScriptRuntime.toBoolean(value);
1207     }
1208 
1209     /**
1210      * Rethrow the exception wrapping it as the script runtime exception.
1211      *
1212      * @param e the exception to rethrow
1213      * @return RuntimeException as dummy the method always throws
1214      */
1215     public static RuntimeException throwAsScriptRuntimeEx(final Throwable e) {
1216         throw Context.throwAsScriptRuntimeEx(e);
1217     }
1218 
1219     /**
1220      * Report a runtime error using the error reporter for the current thread.
1221      *
1222      * @param message the error message to report
1223      * @return RuntimeException as dummy the method always throws
1224      */
1225     public static RuntimeException reportRuntimeError(final String message) {
1226         throw Context.reportRuntimeError(message);
1227     }
1228 
1229     /**
1230      * Report a runtime error using the error reporter for the current thread.
1231      *
1232      * @param message the error message to report
1233      * @return EcmaError
1234      */
1235     public static EcmaError syntaxError(final String message) {
1236         return ScriptRuntime.syntaxError(message);
1237     }
1238 
1239     /**
1240      * Report a runtime error using the error reporter for the current thread.
1241      *
1242      * @param message the error message to report
1243      * @return EcmaError
1244      */
1245     public static EcmaError typeError(final String message) {
1246         return ScriptRuntime.typeError(message);
1247     }
1248 
1249     /**
1250      * Report a TypeError with the message "Illegal constructor.".
1251      *
1252      * @return EcmaError
1253      */
1254     public static EcmaError typeErrorIllegalConstructor() {
1255         throw JavaScriptEngine.typeError("Illegal constructor.");
1256     }
1257 
1258     /**
1259      * Report a runtime error using the error reporter for the current thread.
1260      *
1261      * @param message the error message to report
1262      * @return EcmaError
1263      */
1264     public static EcmaError rangeError(final String message) {
1265         return ScriptRuntime.rangeError(message);
1266     }
1267 
1268     /**
1269      * @param error the error
1270      * @param message the message
1271      * @return a new EcmaError
1272      */
1273     public static EcmaError constructError(final String error, final String message) {
1274         return ScriptRuntime.constructError(error, message);
1275     }
1276 
1277     /**
1278      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1279      *
1280      * Creates a {@link DOMException} and encapsulates it into a Rhino-compatible exception.
1281      *
1282      * @param scriptable the scriptable triggering this
1283      * @param message the exception message
1284      * @param type the exception type
1285      * @return the created exception
1286      */
1287     public static RhinoException asJavaScriptException(final HtmlUnitScriptable scriptable, final String message, final int type) {
1288         final DOMException domException = new DOMException(message, type);
1289         domException.setParentScope(scriptable.getParentScope());
1290 
1291         final WindowOrWorkerGlobalScope wow = HtmlUnitScriptable.getWindowOrWorkerGlobalScope(scriptable);
1292         if (wow instanceof Window window) {
1293             domException.setPrototype(window.getPrototype(DOMException.class));
1294         }
1295         else if (wow instanceof WorkerGlobalScope w) {
1296             domException.setPrototype(w.getPrototype(DOMException.class));
1297         }
1298 
1299         final EcmaError helper = ScriptRuntime.syntaxError("helper");
1300         String fileName = helper.sourceName();
1301         if (fileName != null) {
1302             fileName = fileName.replaceFirst("script in (.*) from .*", "$1");
1303         }
1304         domException.setLocation(fileName, helper.lineNumber());
1305 
1306         return new JavaScriptException(domException, fileName, helper.lineNumber());
1307     }
1308 
1309     /**
1310      * Todo.
1311      *
1312      * @param fn the function
1313      * @param cx the context
1314      * @param scope the scope
1315      */
1316     public static void setFunctionProtoAndParent(final BaseFunction fn, final Context cx, final VarScope scope) {
1317         ScriptRuntime.setFunctionProtoAndParent(fn, cx, scope);
1318     }
1319 
1320     /**
1321      * Create a new javascript object by calling the ctor with the provided args.
1322      *
1323      * @param scope the scope to create the object in
1324      * @param constructorName the name of the ctor function to call
1325      * @param args the args
1326      * @return the new object
1327      */
1328     public static Scriptable newObject(final VarScope scope, final String constructorName, final Object[] args) {
1329         return ScriptRuntime.newObject(Context.getCurrentContext(), scope, constructorName, args);
1330     }
1331 
1332     /**
1333      * Create a new JavaScript object.
1334      *
1335      * <p>Equivalent to evaluating "new Object()".
1336      *
1337      * @param scope the scope to search for the constructor and to evaluate against
1338      * @return the new object
1339      */
1340     public static Scriptable newObject(final VarScope scope) {
1341         final NativeObject result = new NativeObject();
1342         ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Object);
1343         return result;
1344     }
1345 
1346     /**
1347      * Create an array with a specified initial length.
1348      *
1349      * @param scope the scope to create the object in
1350      * @param length the initial length (JavaScript arrays may have additional properties added
1351      *     dynamically).
1352      * @return the new array object
1353      */
1354     public static Scriptable newArray(final VarScope scope, final int length) {
1355         final NativeArray result = new NativeArray(length);
1356         ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1357         return result;
1358     }
1359 
1360     /**
1361      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1362      *
1363      * Create a new ArrayIterator of type NativeArrayIterator.ARRAY_ITERATOR_TYPE.KEYS
1364      *
1365      * @param scope the scope to create the object in
1366      * @param arrayLike the backend
1367      * @return the new NativeArrayIterator
1368      */
1369     public static Scriptable newArrayIteratorTypeKeys(final VarScope scope, final Scriptable arrayLike) {
1370         return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.KEYS);
1371     }
1372 
1373     /**
1374      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1375      *
1376      * Create a new ArrayIterator of type NativeArrayIterator.ARRAY_ITERATOR_TYPE.VALUES
1377      *
1378      * @param scope the scope to create the object in
1379      * @param arrayLike the backend
1380      * @return the new NativeArrayIterator
1381      */
1382     public static Scriptable newArrayIteratorTypeValues(final VarScope scope, final Scriptable arrayLike) {
1383         return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.VALUES);
1384     }
1385 
1386     /**
1387      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1388      *
1389      * Create a new ArrayIterator of type NativeArrayIterator.ARRAY_ITERATOR_TYPE.ENTRIES
1390      *
1391      * @param scope the scope to create the object in
1392      * @param arrayLike the backend
1393      * @return the new NativeArrayIterator
1394      */
1395     public static Scriptable newArrayIteratorTypeEntries(final VarScope scope, final Scriptable arrayLike) {
1396         return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.ENTRIES);
1397     }
1398 
1399     /**
1400      * Create an array with a specified initial length.
1401      *
1402      * @param scope the scope to create the object in
1403      * @param elements the initial elements. Each object in this array must be an acceptable
1404      *     JavaScript type and type of array should be exactly Object[], not SomeObjectSubclass[].
1405      * @return the new array object
1406      */
1407     public static Scriptable newArray(final VarScope scope, final Object[] elements) {
1408         if (elements.getClass().getComponentType() != ScriptRuntime.ObjectClass) {
1409             throw new IllegalArgumentException();
1410         }
1411 
1412         final NativeArray result = new NativeArray(elements);
1413         ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1414         return result;
1415     }
1416 
1417     /**
1418      * Create an Uint8Array with a specified elements.
1419      *
1420      * @param scope the scope to create the object in
1421      * @param elements the initial elements..
1422      * @return the new Uint8Array
1423      */
1424     public static NativeUint8Array newUint8Array(final VarScope scope, final byte[] elements) {
1425         final NativeArrayBuffer arrayBuffer = new NativeArrayBuffer(elements.length);
1426         ScriptRuntime.setBuiltinProtoAndParent(arrayBuffer, scope, TopLevel.Builtins.ArrayBuffer);
1427         System.arraycopy(elements, 0, arrayBuffer.getBuffer(), 0, elements.length);
1428 
1429         final NativeUint8Array uint8Array = new NativeUint8Array(arrayBuffer, 0, elements.length);
1430         ScriptRuntime.setBuiltinProtoAndParent(uint8Array, scope, TopLevel.Builtins.Uint8Array);
1431 
1432         return uint8Array;
1433     }
1434 
1435     /**
1436      * @param o the object to convert
1437      * @return int value
1438      */
1439     public static int toInt32(final Object o) {
1440         return ScriptRuntime.toInt32(o);
1441     }
1442 
1443     /**
1444      * @param o the object to convert
1445      * @return double value
1446      */
1447     public static double toInteger(final Object o) {
1448         return ScriptRuntime.toInteger(o);
1449     }
1450 
1451     /**
1452      * @param args an array
1453      * @param index the index in the array
1454      * @return double value
1455      */
1456     public static double toInteger(final Object[] args, final int index) {
1457         return ScriptRuntime.toInteger(args, index);
1458     }
1459 
1460     /**
1461      * @param obj the value to check
1462      * @return whether obj is undefined
1463      */
1464     public static boolean isUndefined(final Object obj) {
1465         return org.htmlunit.corejs.javascript.Undefined.isUndefined(obj);
1466     }
1467 
1468     /**
1469      * @param obj the value to check
1470      * @return whether obj is NAN
1471      */
1472     public static boolean isNaN(final Object obj) {
1473         return ScriptRuntime.isNaN(obj);
1474     }
1475 
1476     /**
1477      * @param obj the value to check
1478      * @return whether obj is an Array
1479      */
1480     public static boolean isArray(final Object obj) {
1481         return (obj instanceof Scriptable s)
1482                     && "Array".equals(s.getClassName());
1483     }
1484 
1485     /**
1486      * @param obj the value to check
1487      * @return true if the passed in {@link Scriptable} looks like an array
1488      */
1489     public static boolean isArrayLike(final Scriptable obj) {
1490         return ScriptRuntime.isArrayLike(obj);
1491     }
1492 
1493     /**
1494      * @param cx the Context
1495      * @param obj the value to check
1496      * @return the length of the array like {@link Scriptable}
1497      */
1498     public static long lengthOfArrayLike(final Context cx, final Scriptable obj) {
1499         return AbstractEcmaObjectOperations.lengthOfArrayLike(cx, obj);
1500     }
1501 
1502     /**
1503      * Iterates an arrayLike {@link Scriptable} calling the {@link Consumer} on
1504      * every item.
1505      * @param cx the context or null
1506      * @param arrayLike the {@link Scriptable} to iterate
1507      * @param consumer the {@link Consumer} to call
1508      */
1509     public static void iterateArrayLike(final Context cx, final Scriptable arrayLike, final Consumer<Object> consumer) {
1510         final Context context = cx == null ? Context.getCurrentContext() : cx;
1511 
1512         final long len = lengthOfArrayLike(context, arrayLike);
1513         for (int i = 0; i < len; i++) {
1514             final Object item = arrayLike.get(i, arrayLike);
1515             consumer.accept(item);
1516         }
1517     }
1518 
1519     /**
1520      * @return the top call scope
1521      */
1522     public static TopLevel getTopCallScope() {
1523         return ScriptRuntime.getTopCallScope(Context.getCurrentContext());
1524     }
1525 
1526     /**
1527      * Tries to uncompress the JavaScript code in the provided response.
1528      * @param scriptSource the souce
1529      * @param scriptName the name
1530      * @return the uncompressed JavaScript code
1531      */
1532     public static String uncompressJavaScript(final String scriptSource, final String scriptName) {
1533         final ContextFactory factory = new ContextFactory();
1534         final ContextAction<Object> action = cx -> {
1535             cx.setInterpretedMode(true);
1536             final Script script = cx.compileString(scriptSource, scriptName, 0, null);
1537             return cx.decompileScript(script, 4);
1538         };
1539 
1540         return (String) factory.call(action);
1541     }
1542 
1543     /**
1544      * Evaluates the <code>FindProxyForURL</code> method of the specified content.
1545      * @param browserVersion the browser version to use
1546      * @param content the JavaScript content
1547      * @param url the URL to be retrieved
1548      * @return semicolon-separated result
1549      */
1550     public static String evaluateProxyAutoConfig(final BrowserVersion browserVersion, final String content, final URL url) {
1551         try (Context cx = Context.enter()) {
1552             final ProxyAutoConfigJavaScriptConfiguration jsConfig =
1553                     ProxyAutoConfigJavaScriptConfiguration.getInstance(browserVersion);
1554 
1555             final ScriptableObject globalThis = new NativeObject();
1556             final TopLevel scope = cx.initSafeStandardObjects(new TopLevel(globalThis));
1557 
1558             for (final ClassConfiguration config : jsConfig.getAll()) {
1559                 configureFunctions(config, scope, globalThis);
1560             }
1561 
1562             cx.evaluateString(scope, "var ProxyConfig = function() {}; ProxyConfig.bindings = {}; ProxyConfig", "<init>", 1, null);
1563             cx.evaluateString(scope, content, "<Proxy Auto-Config>", 1, null);
1564 
1565             final Object[] functionArgs = {url.toExternalForm(), url.getHost()};
1566             final Function f = (Function) scope.get("FindProxyForURL", scope);
1567             final Object result = f.call(cx, scope, globalThis, functionArgs);
1568             return toString(result);
1569         }
1570     }
1571 }