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