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