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