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.lang3.ObjectUtils.Null;
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(scope, "URLSearchParams Iterator");
250         FormData.FormDataIterator.init(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 {
468             ScriptableObject.deleteProperty(errorObject, "stackTraceLimit");
469         }
470 
471         // add Intl
472         Intl.init(scope, globalThis, browserVersion);
473     }
474 
475     /**
476      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
477      *
478      * @param webClient the WebClient
479      * @param browserVersion the BrowserVersion
480      * @param context the current context
481      * @param scope the scope
482      * @param scriptable the window or the DedicatedWorkerGlobalScope
483      * @throws IOException in case of problems
484      */
485     public static void applyPolyfills(final WebClient webClient, final BrowserVersion browserVersion,
486             final Context context, final VarScope scope, final HtmlUnitScriptable scriptable) throws IOException {
487 
488         if (webClient.getOptions().isFetchPolyfillEnabled()) {
489             Polyfill.getFetchPolyfill().apply(context, scope, scriptable);
490         }
491     }
492 
493     private static void defineConstructor(final VarScope scope,
494             final Scriptable prototype, final ScriptableObject constructor) {
495         constructor.setParentScope(scope);
496         try {
497             ScriptableObject.defineProperty(prototype, "constructor", constructor,
498                     ScriptableObject.DONTENUM  | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
499         }
500         catch (final Exception e) {
501             // TODO see issue #1897
502             if (LOG.isWarnEnabled()) {
503                 final String newline = System.lineSeparator();
504                 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
505                         + e.getMessage() + newline
506                         + "prototype: " + prototype.getClassName(), e);
507             }
508         }
509 
510         try {
511             ScriptableObject.defineProperty(constructor, "prototype", prototype,
512                     ScriptableObject.DONTENUM  | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
513         }
514         catch (final Exception e) {
515             // TODO see issue #1897
516             if (LOG.isWarnEnabled()) {
517                 final String newline = System.lineSeparator();
518                 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
519                         + e.getMessage() + newline
520                         + "prototype: " + prototype.getClassName(), e);
521             }
522         }
523     }
524 
525     /**
526      * Deletes the properties with the provided names.
527      * @param destination the object from which the properties are to be removed
528      * @param propertiesToDelete the list of property names
529      */
530     private static void deleteProperties(final Scriptable destination, final String... propertiesToDelete) {
531         for (final String property : propertiesToDelete) {
532             destination.delete(property);
533         }
534     }
535 
536     /**
537      * Removes prototype properties.
538      * @param scope the scope to search for the prototype
539      * @param className the class for which properties should be removed
540      * @param properties the properties to remove
541      */
542     private static void removePrototypeProperties(final VarScope scope, final String className,
543             final String... properties) {
544         final ScriptableObject prototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, className);
545         for (final String property : properties) {
546             prototype.delete(property);
547         }
548     }
549 
550     /**
551      * Configures the specified class for access via JavaScript.
552      * @param config the configuration settings for the class to be configured
553      * @param scope the scope to configure within which to configure the class
554      * @throws InstantiationException if the new class cannot be instantiated
555      * @throws IllegalAccessException if we don't have access to create the new instance
556      * @return the created prototype
557      * @throws Exception in case of errors
558      */
559     public static HtmlUnitScriptable configureClass(final ClassConfiguration config,
560             final TopLevel scope)
561         throws Exception {
562 
563         final HtmlUnitScriptable prototype = config.getHostClass().getDeclaredConstructor().newInstance();
564         prototype.setParentScope(scope);
565         prototype.setClassName(config.getClassName());
566 
567         configureConstantsPropertiesAndFunctions(config, scope, prototype);
568 
569         return prototype;
570     }
571 
572     /**
573      * Configures constants, static properties and static functions on the object.
574      * @param config the configuration for the object
575      * @param scriptable the object to configure
576      */
577     private static void configureConstantsStaticPropertiesAndStaticFunctions(final ClassConfiguration config,
578             final TopLevel scope, final ScriptableObject scriptable) {
579         configureConstants(config, scriptable);
580         configureStaticProperties(config, scope, scriptable);
581         configureStaticFunctions(config, scope, scriptable);
582     }
583 
584     /**
585      * Configures constants, properties and functions on the object.
586      * @param config the configuration for the object
587      * @param scope the scope
588      * @param scriptable the object to configure
589      */
590     private static void configureConstantsPropertiesAndFunctions(final ClassConfiguration config,
591             final TopLevel scope, final ScriptableObject scriptable) {
592         configureConstants(config, scriptable);
593         configureProperties(config, scope, scriptable);
594         configureFunctions(config, scope, scriptable);
595         configureSymbolConstants(config, scriptable);
596         configureSymbols(config, scope, scriptable);
597     }
598 
599     private static void configureFunctions(final ClassConfiguration config,
600             final TopLevel scope, final ScriptableObject scriptable) {
601         // the functions
602         final Map<String, Method> functionMap = config.getFunctionMap();
603         if (functionMap != null) {
604             for (final Entry<String, Method> functionInfo : functionMap.entrySet()) {
605                 final String functionName = functionInfo.getKey();
606                 final Method method = functionInfo.getValue();
607                 final FunctionObject functionObject = new FunctionObject(functionName, method, scope);
608                 scriptable.defineProperty(functionName, functionObject, ScriptableObject.EMPTY);
609             }
610         }
611     }
612 
613     private static void configureConstants(final ClassConfiguration config, final ScriptableObject scriptable) {
614         final List<ConstantInfo> constants = config.getConstants();
615         if (constants != null) {
616             for (final ConstantInfo constantInfo : constants) {
617                 scriptable.defineProperty(constantInfo.getName(), constantInfo.getValue(), constantInfo.getFlag());
618             }
619         }
620     }
621 
622     private static void configureProperties(final ClassConfiguration config,
623             final TopLevel scope, final ScriptableObject scriptable) {
624         final Map<String, PropertyInfo> propertyMap = config.getPropertyMap();
625         if (propertyMap != null) {
626             for (final Entry<String, PropertyInfo> propertyEntry : propertyMap.entrySet()) {
627                 final PropertyInfo info = propertyEntry.getValue();
628                 final Method readMethod = info.getReadMethod();
629                 final Method writeMethod = info.getWriteMethod();
630                 scriptable.defineProperty(scope, propertyEntry.getKey(), null, readMethod, writeMethod, ScriptableObject.EMPTY);
631             }
632         }
633     }
634 
635     private static void configureStaticProperties(final ClassConfiguration config,
636             final TopLevel scope, final ScriptableObject scriptable) {
637         final Map<String, PropertyInfo> staticPropertyMap = config.getStaticPropertyMap();
638         if (staticPropertyMap != null) {
639             for (final Entry<String, ClassConfiguration.PropertyInfo> propertyEntry : staticPropertyMap.entrySet()) {
640                 final String propertyName = propertyEntry.getKey();
641                 final Method readMethod = propertyEntry.getValue().getReadMethod();
642                 final Method writeMethod = propertyEntry.getValue().getWriteMethod();
643                 final int flag = ScriptableObject.EMPTY;
644 
645                 scriptable.defineProperty(scope, propertyName, null, readMethod, writeMethod, flag);
646             }
647         }
648     }
649 
650     private static void configureStaticFunctions(final ClassConfiguration config,
651             final TopLevel scope, final ScriptableObject scriptable) {
652         final Map<String, Method> staticFunctionMap = config.getStaticFunctionMap();
653         if (staticFunctionMap != null) {
654             for (final Entry<String, Method> staticFunctionInfo : staticFunctionMap.entrySet()) {
655                 final String functionName = staticFunctionInfo.getKey();
656                 final Method method = staticFunctionInfo.getValue();
657                 final FunctionObject staticFunctionObject = new FunctionObject(functionName, method, scope);
658                 scriptable.defineProperty(functionName, staticFunctionObject, ScriptableObject.EMPTY);
659             }
660         }
661     }
662 
663     private static void configureSymbolConstants(final ClassConfiguration config, final ScriptableObject scriptable) {
664         final Map<Symbol, String> symbolConstantMap = config.getSymbolConstantMap();
665         if (symbolConstantMap != null) {
666             for (final Entry<Symbol, String> symbolInfo : symbolConstantMap.entrySet()) {
667                 scriptable.defineProperty(symbolInfo.getKey(), symbolInfo.getValue(), ScriptableObject.DONTENUM | ScriptableObject.READONLY);
668             }
669         }
670     }
671 
672     private static void configureSymbols(final ClassConfiguration config,
673             final TopLevel scope, final ScriptableObject scriptable) {
674         final Map<Symbol, Method> symbolMap = config.getSymbolMap();
675         if (symbolMap != null) {
676             for (final Entry<Symbol, Method> symbolInfo : symbolMap.entrySet()) {
677                 final Symbol symbol = symbolInfo.getKey();
678                 final Method method = symbolInfo.getValue();
679                 final String methodName = method.getName();
680 
681                 final Callable symbolFunction;
682                 // a bit strange but this avoid the has call and therefore saves one lookup
683                 final Object property = scriptable.get(methodName, scriptable);
684                 if (property == Scriptable.NOT_FOUND) {
685                     symbolFunction = new FunctionObject(methodName, method, scope);
686                 }
687                 else {
688                     symbolFunction = (Callable) property;
689                 }
690                 scriptable.defineProperty(symbol, symbolFunction, ScriptableObject.DONTENUM);
691             }
692         }
693     }
694 
695     /**
696      * Register WebWindow with the JavaScriptExecutor.
697      * @param webWindow the WebWindow to be registered.
698      */
699     @Override
700     public synchronized void registerWindowAndMaybeStartEventLoop(final WebWindow webWindow) {
701         if (shutdownPending_) {
702             return;
703         }
704 
705         final WebClient webClient = getWebClient();
706         if (webClient != null) {
707             if (javaScriptExecutor_ == null) {
708                 javaScriptExecutor_ = BackgroundJavaScriptFactory.theFactory().createJavaScriptExecutor(webClient);
709             }
710             javaScriptExecutor_.addWindow(webWindow);
711         }
712     }
713 
714     /**
715      * {@inheritDoc}
716      */
717     @Override
718     public void prepareShutdown() {
719         shutdownPending_ = true;
720     }
721 
722     /**
723      * Shutdown the JavaScriptEngine.
724      */
725     @Override
726     public void shutdown() {
727         webClient_ = null;
728         contextFactory_ = null;
729         jsConfig_ = null;
730 
731         if (javaScriptExecutor_ != null) {
732             javaScriptExecutor_.shutdown();
733             javaScriptExecutor_ = null;
734         }
735         if (postponedActions_ != null) {
736             postponedActions_.remove();
737         }
738         if (javaScriptRunning_ != null) {
739             javaScriptRunning_.remove();
740         }
741         holdPostponedActions_ = false;
742     }
743 
744     /**
745      * {@inheritDoc}
746      */
747     @Override
748     public Script compile(final HtmlPage owningPage, final VarScope scope, final String sourceCode,
749             final String sourceName, final int startLine) {
750         WebAssert.notNull("sourceCode", sourceCode);
751 
752         if (LOG.isTraceEnabled()) {
753             final String newline = System.lineSeparator();
754             LOG.trace("Javascript compile " + sourceName + newline + sourceCode + newline);
755         }
756 
757         final HtmlUnitCompileContextAction action = new HtmlUnitCompileContextAction(owningPage, sourceCode, sourceName, startLine);
758         return (Script) getContextFactory().callSecured(action, owningPage);
759     }
760 
761     /**
762      * Forwards this to the {@link HtmlUnitContextFactory} but with checking shutdown handling.
763      *
764      * @param <T> return type of the action
765      * @param action the contextAction
766      * @param page the page
767      * @return the result of the call
768      */
769     public final <T> T callSecured(final ContextAction<T> action, final HtmlPage page) {
770         if (shutdownPending_ || webClient_ == null) {
771             // shutdown was already called
772             return null;
773         }
774 
775         return getContextFactory().callSecured(action, page);
776     }
777 
778     /**
779      * {@inheritDoc}
780      */
781     @Override
782     public Object execute(final HtmlPage page,
783                            final VarScope scope,
784                            final String sourceCode,
785                            final String sourceName,
786                            final int startLine) {
787         final Script script = compile(page, scope, sourceCode, sourceName, startLine);
788         if (script == null) {
789             // happens with syntax error + throwExceptionOnScriptError = false
790             return null;
791         }
792         return execute(page, scope, script);
793     }
794 
795     /**
796      * {@inheritDoc}
797      */
798     @Override
799     public Object execute(final HtmlPage page, final VarScope scope, final Script script) {
800         if (shutdownPending_ || webClient_ == null) {
801             // shutdown was already called
802             return null;
803         }
804 
805         final HtmlUnitContextAction action = new HtmlUnitContextAction(page) {
806             @Override
807             public Object doRun(final Context cx) {
808                 return script.exec(cx, scope, ScriptableObject.getTopLevelScope(scope).getGlobalThis());
809             }
810 
811             @Override
812             protected String getSourceCode(final Context cx) {
813                 return null;
814             }
815         };
816 
817         return getContextFactory().callSecured(action, page);
818     }
819 
820     /**
821      * Calls a JavaScript function and return the result.
822      * @param page the page
823      * @param javaScriptFunction the function to call
824      * @param thisObject the this object for class method calls
825      * @param args the list of arguments to pass to the function
826      * @param node the HTML element that will act as the context
827      * @return the result of the function call
828      */
829     public Object callFunction(
830             final HtmlPage page,
831             final Function javaScriptFunction,
832             final Scriptable thisObject,
833             final Object[] args,
834             final DomNode node) {
835 
836         VarScope scope = ScriptableObject.getTopLevelScope(thisObject.getParentScope());
837 
838         if (node != null && node instanceof HtmlElement htmlElement) {
839             final HTMLElement elem = htmlElement.getScriptableObject();
840             scope = new WithScope(scope, elem.getOwnerDocument());
841 
842             if (htmlElement instanceof SubmittableElement) {
843                 final HtmlForm enclosingForm = htmlElement.getEnclosingForm();
844                 if (enclosingForm != null) {
845                     scope = new WithScope(scope, enclosingForm.getScriptableObject());
846                 }
847             }
848 
849             scope = new WithScope(scope, node.getScriptableObject());
850         }
851 
852         return callFunction(page, javaScriptFunction, scope, thisObject, args);
853     }
854 
855     /**
856      * Calls the given function taking care of synchronization issues.
857      * @param page the interactive page that caused this script to executed
858      * @param function the JavaScript function to execute
859      * @param scope the execution scope
860      * @param thisObject the 'this' object
861      * @param args the function's arguments
862      * @return the function result
863      */
864     public Object callFunction(final HtmlPage page, final Function function,
865             final VarScope scope, final Scriptable thisObject, final Object[] args) {
866         if (shutdownPending_ || webClient_ == null) {
867             // shutdown was already called
868             return null;
869         }
870 
871         final HtmlUnitContextAction action = new HtmlUnitContextAction(page) {
872             @Override
873             public Object doRun(final Context cx) {
874                 if (ScriptRuntime.hasTopCall(cx)) {
875                     return function.call(cx, scope, thisObject, args);
876                 }
877                 return ScriptRuntime.doTopCall(function, cx, scope, thisObject, args, cx.isStrictMode());
878             }
879             @Override
880             protected String getSourceCode(final Context cx) {
881                 return cx.decompileFunction(function, 2);
882             }
883         };
884         return getContextFactory().callSecured(action, page);
885     }
886 
887     /**
888      * Indicates if JavaScript is running in current thread.
889      * <p>This allows code to know if their own evaluation has been triggered by some JS code.
890      * @return {@code true} if JavaScript is running
891      */
892     @Override
893     public boolean isScriptRunning() {
894         return Boolean.TRUE.equals(javaScriptRunning_.get());
895     }
896 
897     /**
898      * Special ContextAction only for compiling. This reduces some code and avoid
899      * some calls.
900      */
901     private final class HtmlUnitCompileContextAction implements ContextAction<Object> {
902         private final HtmlPage page_;
903         private final String sourceCode_;
904         private final String sourceName_;
905         private final int startLine_;
906 
907         HtmlUnitCompileContextAction(final HtmlPage page, final String sourceCode, final String sourceName, final int startLine) {
908             page_ = page;
909             sourceCode_ = sourceCode;
910             sourceName_ = sourceName;
911             startLine_ = startLine;
912         }
913 
914         @Override
915         public Object run(final Context cx) {
916             try {
917                 final Object response;
918                 cx.putThreadLocal(KEY_STARTING_PAGE, page_);
919                 synchronized (page_) { // 2 scripts can't be executed in parallel for one page
920                     if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
921                         return null; // page has been unloaded
922                     }
923                     response = cx.compileString(sourceCode_, sourceName_, startLine_, null);
924 
925                 }
926 
927                 return response;
928             }
929             catch (final Exception e) {
930                 handleJavaScriptException(new ScriptException(page_, e, sourceCode_), true);
931                 return null;
932             }
933             catch (final TimeoutError e) {
934                 handleJavaScriptTimeoutError(page_, e);
935                 return null;
936             }
937         }
938     }
939 
940     /**
941      * Facility for ContextAction usage.
942      * ContextAction should be preferred because according to Rhino doc it
943      * "guarantees proper association of Context instances with the current thread and is faster".
944      */
945     private abstract class HtmlUnitContextAction implements ContextAction<Object> {
946         private final HtmlPage page_;
947 
948         HtmlUnitContextAction(final HtmlPage page) {
949             page_ = page;
950         }
951 
952         @Override
953         public final Object run(final Context cx) {
954             final Boolean javaScriptAlreadyRunning = javaScriptRunning_.get();
955             javaScriptRunning_.set(Boolean.TRUE);
956 
957             try {
958                 final Object response;
959                 synchronized (page_) { // 2 scripts can't be executed in parallel for one page
960                     if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
961                         return null; // page has been unloaded
962                     }
963                     response = doRun(cx);
964                 }
965 
966                 cx.processMicrotasks();
967 
968                 // doProcessPostponedActions is synchronized
969                 // moved out of the sync block to avoid deadlocks
970                 if (!holdPostponedActions_) {
971                     doProcessPostponedActions();
972                 }
973 
974                 return response;
975             }
976             catch (final Exception e) {
977                 handleJavaScriptException(new ScriptException(page_, e, getSourceCode(cx)), true);
978                 return null;
979             }
980             catch (final TimeoutError e) {
981                 handleJavaScriptTimeoutError(page_, e);
982                 return null;
983             }
984             finally {
985                 javaScriptRunning_.set(javaScriptAlreadyRunning);
986             }
987         }
988 
989         protected abstract Object doRun(Context cx);
990 
991         protected abstract String getSourceCode(Context cx);
992     }
993 
994     private void doProcessPostponedActions() {
995         holdPostponedActions_ = false;
996 
997         final WebClient webClient = getWebClient();
998         if (webClient == null) {
999             // shutdown was already called
1000             postponedActions_.set(null);
1001             return;
1002         }
1003 
1004         try {
1005             webClient.loadDownloadedResponses();
1006         }
1007         catch (final RuntimeException e) {
1008             throw e;
1009         }
1010         catch (final Exception e) {
1011             throw new RuntimeException(e);
1012         }
1013 
1014         final List<PostponedAction> actions = postponedActions_.get();
1015         if (actions != null && !actions.isEmpty()) {
1016             postponedActions_.set(new ArrayList<>());
1017             try {
1018                 for (final PostponedAction action : actions) {
1019                     if (LOG.isDebugEnabled()) {
1020                         LOG.debug("Processing PostponedAction " + action);
1021                     }
1022 
1023                     // verify that the page that registered this PostponedAction is still alive
1024                     if (action.isStillAlive()) {
1025                         action.execute();
1026                     }
1027                 }
1028             }
1029             catch (final RuntimeException e) {
1030                 throw e;
1031             }
1032             catch (final Exception e) {
1033                 throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
1034             }
1035         }
1036     }
1037 
1038     /**
1039      * Adds an action that should be executed first when the script currently being executed has finished.
1040      * @param action the action
1041      */
1042     @Override
1043     public void addPostponedAction(final PostponedAction action) {
1044         if (shutdownPending_) {
1045             return;
1046         }
1047 
1048         List<PostponedAction> actions = postponedActions_.get();
1049         if (actions == null) {
1050             actions = new ArrayList<>();
1051             postponedActions_.set(actions);
1052         }
1053         actions.add(action);
1054     }
1055 
1056     /**
1057      * Handles an exception that occurred during execution of JavaScript code.
1058      * @param scriptException the exception
1059      * @param triggerOnError if true, this triggers the onerror handler
1060      */
1061     protected void handleJavaScriptException(final ScriptException scriptException, final boolean triggerOnError) {
1062         final WebClient webClient = getWebClient();
1063         if (shutdownPending_ || webClient == null) {
1064             // shutdown was already called
1065             return;
1066         }
1067 
1068         // Trigger window.onerror, if it has been set.
1069         final HtmlPage page = scriptException.getPage();
1070         if (triggerOnError && page != null) {
1071             final WebWindow window = page.getEnclosingWindow();
1072             if (window != null) {
1073                 final Window w = window.getScriptableObject();
1074                 if (w != null) {
1075                     try {
1076                         w.triggerOnError(scriptException);
1077                     }
1078                     catch (final Exception e) {
1079                         handleJavaScriptException(new ScriptException(page, e, null), false);
1080                     }
1081                 }
1082             }
1083         }
1084 
1085         webClient.getJavaScriptErrorListener().scriptException(page, scriptException);
1086         // Throw a Java exception if the user wants us to.
1087         if (webClient.getOptions().isThrowExceptionOnScriptError()) {
1088             throw scriptException;
1089         }
1090     }
1091 
1092     /**
1093      * Handles an exception that occurred during execution of JavaScript code.
1094      * @param page the page in which the script causing this exception was executed
1095      * @param e the timeout error that was thrown from the script engine
1096      */
1097     protected void handleJavaScriptTimeoutError(final HtmlPage page, final TimeoutError e) {
1098         final WebClient webClient = getWebClient();
1099         if (shutdownPending_ || webClient == null) {
1100             // shutdown was already called
1101             return;
1102         }
1103 
1104         webClient.getJavaScriptErrorListener().timeoutError(page, e.getAllowedTime(), e.getExecutionTime());
1105         if (webClient.getOptions().isThrowExceptionOnScriptError()) {
1106             throw new RuntimeException(e);
1107         }
1108         LOG.info("Caught script timeout error", e);
1109     }
1110 
1111     /**
1112      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1113      * Indicates that no postponed action should be executed.
1114      */
1115     @Override
1116     public void holdPosponedActions() {
1117         holdPostponedActions_ = true;
1118     }
1119 
1120     /**
1121      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1122      * Process postponed actions, if any.
1123      */
1124     @Override
1125     public void processPostponedActions() {
1126         doProcessPostponedActions();
1127     }
1128 
1129     /**
1130      * Re-initializes transient fields when an object of this type is deserialized.
1131      */
1132     private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
1133         in.defaultReadObject();
1134         initTransientFields();
1135     }
1136 
1137     private void initTransientFields() {
1138         javaScriptRunning_ = new ThreadLocal<>();
1139         postponedActions_ = new ThreadLocal<>();
1140         holdPostponedActions_ = false;
1141         shutdownPending_ = false;
1142     }
1143 
1144     /**
1145      * Gets the class of the JavaScript object for the node class.
1146      * @param c the node class {@link DomNode} or some subclass.
1147      * @return {@code null} if none found
1148      */
1149     public Class<? extends HtmlUnitScriptable> getJavaScriptClass(final Class<?> c) {
1150         return jsConfig_.getDomJavaScriptMappingFor(c);
1151     }
1152 
1153     /**
1154      * Gets the associated configuration.
1155      * @return the configuration
1156      */
1157     @Override
1158     public JavaScriptConfiguration getJavaScriptConfiguration() {
1159         return jsConfig_;
1160     }
1161 
1162     /**
1163      * Returns the javascript timeout.
1164      * @return the javascript timeout
1165      */
1166     @Override
1167     public long getJavaScriptTimeout() {
1168         return getContextFactory().getTimeout();
1169     }
1170 
1171     /**
1172      * Sets the javascript timeout.
1173      * @param timeout the timeout
1174      */
1175     @Override
1176     public void setJavaScriptTimeout(final long timeout) {
1177         getContextFactory().setTimeout(timeout);
1178     }
1179 
1180     /**
1181      * Convert the value to a JavaScript Number value.
1182      *
1183      * @param value a JavaScript value
1184      * @return the corresponding double value converted using the ECMA rules
1185      */
1186     public static double toNumber(final Object value) {
1187         return ScriptRuntime.toNumber(value);
1188     }
1189 
1190     /**
1191      * Convert the value to a JavaScript String value.
1192      *
1193      * @param value a JavaScript value
1194      * @return the corresponding String value converted using the ECMA rules
1195      */
1196     public static String toString(final Object value) {
1197         return ScriptRuntime.toString(value);
1198     }
1199 
1200     /**
1201      * Convert the value to a JavaScript boolean value.
1202      *
1203      * @param value a JavaScript value
1204      * @return the corresponding boolean value converted using the ECMA rules
1205      */
1206     public static boolean toBoolean(final Object value) {
1207         return ScriptRuntime.toBoolean(value);
1208     }
1209 
1210     /**
1211      * Rethrow the exception wrapping it as the script runtime exception.
1212      *
1213      * @param e the exception to rethrow
1214      * @return RuntimeException as dummy the method always throws
1215      */
1216     public static RuntimeException throwAsScriptRuntimeEx(final Throwable e) {
1217         throw Context.throwAsScriptRuntimeEx(e);
1218     }
1219 
1220     /**
1221      * Report a runtime error using the error reporter for the current thread.
1222      *
1223      * @param message the error message to report
1224      * @return RuntimeException as dummy the method always throws
1225      */
1226     public static RuntimeException reportRuntimeError(final String message) {
1227         throw Context.reportRuntimeError(message);
1228     }
1229 
1230     /**
1231      * Report a runtime error using the error reporter for the current thread.
1232      *
1233      * @param message the error message to report
1234      * @return EcmaError
1235      */
1236     public static EcmaError syntaxError(final String message) {
1237         return ScriptRuntime.syntaxError(message);
1238     }
1239 
1240     /**
1241      * Report a runtime error using the error reporter for the current thread.
1242      *
1243      * @param message the error message to report
1244      * @return EcmaError
1245      */
1246     public static EcmaError typeError(final String message) {
1247         return ScriptRuntime.typeError(message);
1248     }
1249 
1250     /**
1251      * Report a TypeError with the message "Illegal constructor.".
1252      *
1253      * @return EcmaError
1254      */
1255     public static EcmaError typeErrorIllegalConstructor() {
1256         throw JavaScriptEngine.typeError("Illegal constructor.");
1257     }
1258 
1259     /**
1260      * Report a runtime error using the error reporter for the current thread.
1261      *
1262      * @param message the error message to report
1263      * @return EcmaError
1264      */
1265     public static EcmaError rangeError(final String message) {
1266         return ScriptRuntime.rangeError(message);
1267     }
1268 
1269     /**
1270      * @param error the error
1271      * @param message the message
1272      * @return a new EcmaError
1273      */
1274     public static EcmaError constructError(final String error, final String message) {
1275         return ScriptRuntime.constructError(error, message);
1276     }
1277 
1278     /**
1279      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1280      *
1281      * Creates a {@link DOMException} and encapsulates it into a Rhino-compatible exception.
1282      *
1283      * @param scriptable the scriptable triggering this
1284      * @param message the exception message
1285      * @param type the exception type
1286      * @return the created exception
1287      */
1288     public static RhinoException asJavaScriptException(final HtmlUnitScriptable scriptable, final String message, final int type) {
1289         final DOMException domException = new DOMException(message, type);
1290         domException.setParentScope(scriptable.getParentScope());
1291 
1292         final WindowOrWorkerGlobalScope wow = HtmlUnitScriptable.getWindowOrWorkerGlobalScope(scriptable);
1293         if (wow instanceof Window window) {
1294             domException.setPrototype(window.getPrototype(DOMException.class));
1295         }
1296         else if (wow instanceof WorkerGlobalScope w) {
1297             domException.setPrototype(w.getPrototype(DOMException.class));
1298         }
1299 
1300         final EcmaError helper = ScriptRuntime.syntaxError("helper");
1301         String fileName = helper.sourceName();
1302         if (fileName != null) {
1303             fileName = fileName.replaceFirst("script in (.*) from .*", "$1");
1304         }
1305         domException.setLocation(fileName, helper.lineNumber());
1306 
1307         return new JavaScriptException(domException, fileName, helper.lineNumber());
1308     }
1309 
1310     /**
1311      * Todo.
1312      *
1313      * @param fn the function
1314      * @param cx the context
1315      * @param scope the scope
1316      */
1317     public static void setFunctionProtoAndParent(final BaseFunction fn, final Context cx, final VarScope scope) {
1318         ScriptRuntime.setFunctionProtoAndParent(fn, cx, scope);
1319     }
1320 
1321     /**
1322      * Create a new javascript object by calling the ctor with the provided args.
1323      *
1324      * @param scope the scope to create the object in
1325      * @param constructorName the name of the ctor function to call
1326      * @param args the args
1327      * @return the new object
1328      */
1329     public static Scriptable newObject(final VarScope scope, final String constructorName, final Object[] args) {
1330         return ScriptRuntime.newObject(Context.getCurrentContext(), scope, constructorName, args);
1331     }
1332 
1333     /**
1334      * Create a new JavaScript object.
1335      *
1336      * <p>Equivalent to evaluating "new Object()".
1337      *
1338      * @param scope the scope to search for the constructor and to evaluate against
1339      * @return the new object
1340      */
1341     public static Scriptable newObject(final VarScope scope) {
1342         final NativeObject result = new NativeObject();
1343         ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Object);
1344         return result;
1345     }
1346 
1347     /**
1348      * Create an array with a specified initial length.
1349      *
1350      * @param scope the scope to create the object in
1351      * @param length the initial length (JavaScript arrays may have additional properties added
1352      *     dynamically).
1353      * @return the new array object
1354      */
1355     public static Scriptable newArray(final VarScope scope, final int length) {
1356         final NativeArray result = new NativeArray(length);
1357         ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1358         return result;
1359     }
1360 
1361     /**
1362      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1363      *
1364      * Create a new ArrayIterator of type NativeArrayIterator.ARRAY_ITERATOR_TYPE.KEYS
1365      *
1366      * @param scope the scope to create the object in
1367      * @param arrayLike the backend
1368      * @return the new NativeArrayIterator
1369      */
1370     public static Scriptable newArrayIteratorTypeKeys(final VarScope scope, final Scriptable arrayLike) {
1371         return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.KEYS);
1372     }
1373 
1374     /**
1375      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1376      *
1377      * Create a new ArrayIterator of type NativeArrayIterator.ARRAY_ITERATOR_TYPE.VALUES
1378      *
1379      * @param scope the scope to create the object in
1380      * @param arrayLike the backend
1381      * @return the new NativeArrayIterator
1382      */
1383     public static Scriptable newArrayIteratorTypeValues(final VarScope scope, final Scriptable arrayLike) {
1384         return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.VALUES);
1385     }
1386 
1387     /**
1388      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1389      *
1390      * Create a new ArrayIterator of type NativeArrayIterator.ARRAY_ITERATOR_TYPE.ENTRIES
1391      *
1392      * @param scope the scope to create the object in
1393      * @param arrayLike the backend
1394      * @return the new NativeArrayIterator
1395      */
1396     public static Scriptable newArrayIteratorTypeEntries(final VarScope scope, final Scriptable arrayLike) {
1397         return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.ENTRIES);
1398     }
1399 
1400     /**
1401      * Create an array with a specified initial length.
1402      *
1403      * @param scope the scope to create the object in
1404      * @param elements the initial elements. Each object in this array must be an acceptable
1405      *     JavaScript type and type of array should be exactly Object[], not SomeObjectSubclass[].
1406      * @return the new array object
1407      */
1408     public static Scriptable newArray(final VarScope scope, final Object[] elements) {
1409         if (elements.getClass().getComponentType() != ScriptRuntime.ObjectClass) {
1410             throw new IllegalArgumentException();
1411         }
1412 
1413         final NativeArray result = new NativeArray(elements);
1414         ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1415         return result;
1416     }
1417 
1418     /**
1419      * Create an Uint8Array with a specified elements.
1420      *
1421      * @param scope the scope to create the object in
1422      * @param elements the initial elements..
1423      * @return the new Uint8Array
1424      */
1425     public static NativeUint8Array newUint8Array(final VarScope scope, final byte[] elements) {
1426         final NativeArrayBuffer arrayBuffer = new NativeArrayBuffer(elements.length);
1427         ScriptRuntime.setBuiltinProtoAndParent(arrayBuffer, scope, TopLevel.Builtins.ArrayBuffer);
1428         System.arraycopy(elements, 0, arrayBuffer.getBuffer(), 0, elements.length);
1429 
1430         final NativeUint8Array uint8Array = new NativeUint8Array(arrayBuffer, 0, elements.length);
1431         ScriptRuntime.setBuiltinProtoAndParent(uint8Array, scope, TopLevel.Builtins.Uint8Array);
1432 
1433         return uint8Array;
1434     }
1435 
1436     /**
1437      * @param o the object to convert
1438      * @return int value
1439      */
1440     public static int toInt32(final Object o) {
1441         return ScriptRuntime.toInt32(o);
1442     }
1443 
1444     /**
1445      * @param o the object to convert
1446      * @return double value
1447      */
1448     public static double toInteger(final Object o) {
1449         return ScriptRuntime.toInteger(o);
1450     }
1451 
1452     /**
1453      * @param args an array
1454      * @param index the index in the array
1455      * @return double value
1456      */
1457     public static double toInteger(final Object[] args, final int index) {
1458         return ScriptRuntime.toInteger(args, index);
1459     }
1460 
1461     /**
1462      * @param obj the value to check
1463      * @return whether obj is undefined
1464      */
1465     public static boolean isUndefined(final Object obj) {
1466         return org.htmlunit.corejs.javascript.Undefined.isUndefined(obj);
1467     }
1468 
1469     /**
1470      * @param obj the value to check
1471      * @return whether obj is NAN
1472      */
1473     public static boolean isNaN(final Object obj) {
1474         return ScriptRuntime.isNaN(obj);
1475     }
1476 
1477     /**
1478      * @param obj the value to check
1479      * @return whether obj is an Array
1480      */
1481     public static boolean isArray(final Object obj) {
1482         return (obj instanceof Scriptable s)
1483                     && "Array".equals(s.getClassName());
1484     }
1485 
1486     /**
1487      * @param obj the value to check
1488      * @return true if the passed in {@link Scriptable} looks like an array
1489      */
1490     public static boolean isArrayLike(final Scriptable obj) {
1491         return ScriptRuntime.isArrayLike(obj);
1492     }
1493 
1494     /**
1495      * @param cx the Context
1496      * @param obj the value to check
1497      * @return the length of the array like {@link Scriptable}
1498      */
1499     public static long lengthOfArrayLike(final Context cx, final Scriptable obj) {
1500         return AbstractEcmaObjectOperations.lengthOfArrayLike(cx, obj);
1501     }
1502 
1503     /**
1504      * Iterates an arrayLike {@link Scriptable} calling the {@link Consumer} on
1505      * every item.
1506      * @param cx the context or {@link Null}
1507      * @param arrayLike the {@link Scriptable} to iterate
1508      * @param consumer the {@link Consumer} to call
1509      */
1510     public static void iterateArrayLike(final Context cx, final Scriptable arrayLike, final Consumer<Object> consumer) {
1511         final Context context = cx == null ? Context.getCurrentContext() : cx;
1512 
1513         final long len = lengthOfArrayLike(context, arrayLike);
1514         for (int i = 0; i < len; i++) {
1515             final Object item = arrayLike.get(i, arrayLike);
1516             consumer.accept(item);
1517         }
1518     }
1519 
1520     /**
1521      * @return the top call scope
1522      */
1523     public static TopLevel getTopCallScope() {
1524         return ScriptRuntime.getTopCallScope(Context.getCurrentContext());
1525     }
1526 
1527     /**
1528      * Tries to uncompress the JavaScript code in the provided response.
1529      * @param scriptSource the souce
1530      * @param scriptName the name
1531      * @return the uncompressed JavaScript code
1532      */
1533     public static String uncompressJavaScript(final String scriptSource, final String scriptName) {
1534         final ContextFactory factory = new ContextFactory();
1535         final ContextAction<Object> action = cx -> {
1536             cx.setInterpretedMode(true);
1537             final Script script = cx.compileString(scriptSource, scriptName, 0, null);
1538             return cx.decompileScript(script, 4);
1539         };
1540 
1541         return (String) factory.call(action);
1542     }
1543 
1544     /**
1545      * Evaluates the <code>FindProxyForURL</code> method of the specified content.
1546      * @param browserVersion the browser version to use
1547      * @param content the JavaScript content
1548      * @param url the URL to be retrieved
1549      * @return semicolon-separated result
1550      */
1551     public static String evaluateProxyAutoConfig(final BrowserVersion browserVersion, final String content, final URL url) {
1552         try (Context cx = Context.enter()) {
1553             final ProxyAutoConfigJavaScriptConfiguration jsConfig =
1554                     ProxyAutoConfigJavaScriptConfiguration.getInstance(browserVersion);
1555 
1556             final ScriptableObject globalThis = new NativeObject();
1557             final TopLevel scope = cx.initSafeStandardObjects(new TopLevel(globalThis));
1558 
1559             for (final ClassConfiguration config : jsConfig.getAll()) {
1560                 configureFunctions(config, scope, globalThis);
1561             }
1562 
1563             cx.evaluateString(scope, "var ProxyConfig = function() {}; ProxyConfig.bindings = {}; ProxyConfig", "<init>", 1, null);
1564             cx.evaluateString(scope, content, "<Proxy Auto-Config>", 1, null);
1565 
1566             final Object[] functionArgs = {url.toExternalForm(), url.getHost()};
1567             final Function f = (Function) scope.get("FindProxyForURL", scope);
1568             final Object result = f.call(cx, scope, globalThis, functionArgs);
1569             return toString(result);
1570         }
1571     }
1572 }