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