1
2
3
4
5
6
7
8
9
10
11
12
13
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121 public class JavaScriptEngine implements AbstractJavaScriptEngine<Script> {
122
123 private static final Log LOG = LogFactory.getLog(JavaScriptEngine.class);
124
125
126 public static final Object[] EMPTY_ARGS = ScriptRuntime.emptyArgs;
127
128
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
141 private transient JavaScriptExecutor javaScriptExecutor_;
142
143
144
145
146
147 public static final String KEY_STARTING_PAGE = "startingPage";
148
149
150
151
152
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
169
170
171 private WebClient getWebClient() {
172 return webClient_;
173 }
174
175
176
177
178 @Override
179 public HtmlUnitContextFactory getContextFactory() {
180 return contextFactory_;
181 }
182
183
184
185
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);
202 }
203 return null;
204 });
205 }
206
207
208
209
210
211
212 public JavaScriptExecutor getJavaScriptExecutor() {
213 return javaScriptExecutor_;
214 }
215
216
217
218
219
220
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
251 URLSearchParams.NativeParamsIterator.init(cx, scope, "URLSearchParams Iterator");
252 FormData.FormDataIterator.init(cx, scope, "FormData Iterator");
253
254
255
256
257
258
259 if (browserVersion.hasFeature(JS_WINDOW_INSTALL_TRIGGER_NULL)) {
260 jsWindow.put("InstallTrigger", jsWindow, null);
261 }
262
263
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
282
283
284
285
286
287
288
289
290
291
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
313 if (config == scopeConfig) {
314 if (extendedClassName == null) {
315 prototype.setPrototype(objectPrototype);
316 }
317 else {
318 prototype.setPrototype(prototypesPerJSName.get(extendedClassName));
319 }
320
321
322 addAsConstructorAndAlias(scopeContructorFunctionObject, scope, globalThis, prototype, config);
323 configureConstantsStaticPropertiesAndStaticFunctions(config, scope, scopeContructorFunctionObject);
324
325
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
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
365
366
367 globalThis.delete(prototype.getClassName());
368 }
369
370
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
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
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
426
427
428
429
430
431
432 public static void configureRhino(final WebClient webClient, final BrowserVersion browserVersion,
433 final TopLevel scope, final HtmlUnitScriptable globalThis) {
434
435
436
437
438 NativeConsole.init(scope, false, webClient.getWebConsole());
439
440
441
442 final ScriptableObject console = (ScriptableObject) ScriptableObject.getProperty(globalThis, "console");
443 console.defineFunctionProperties(scope, new String[] {"timeStamp"}, ConsoleCustom.class, ScriptableObject.DONTENUM);
444
445
446 deleteProperties(globalThis, "Continuation", "StopIteration", "uneval", "global", "__GeneratorFunction");
447
448
449 final ScriptableObject stringPrototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, "String");
450 deleteProperties(stringPrototype, "equals", "equalsIgnoreCase", "toSource");
451
452 final ScriptableObject numberPrototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, "Number");
453 deleteProperties(numberPrototype, "toSource");
454 final ScriptableObject datePrototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, "Date");
455 deleteProperties(datePrototype, "toSource");
456
457 removePrototypeProperties(scope, "Object", "toSource");
458 removePrototypeProperties(scope, "Array", "toSource");
459 removePrototypeProperties(scope, "Function", "toSource");
460
461 deleteProperties(globalThis, "isXMLName");
462
463 NativeFunctionToStringFunction.installFix(scope, browserVersion);
464
465 final ScriptableObject errorObject = (ScriptableObject) ScriptableObject.getProperty(globalThis, "Error");
466 if (browserVersion.hasFeature(JS_ERROR_STACK_TRACE_LIMIT)) {
467 errorObject.defineProperty("stackTraceLimit", 10, ScriptableObject.EMPTY);
468 }
469 else if (browserVersion.hasFeature(JS_ERROR_STACK_TRACE_LIMIT_128)) {
470 errorObject.defineProperty("stackTraceLimit", 128, ScriptableObject.EMPTY);
471 }
472 else {
473 ScriptableObject.deleteProperty(errorObject, "stackTraceLimit");
474 }
475
476
477 Intl.init(scope, globalThis, browserVersion);
478 }
479
480
481
482
483
484
485
486
487
488
489
490 public static void applyPolyfills(final WebClient webClient, final BrowserVersion browserVersion,
491 final Context context, final VarScope scope, final HtmlUnitScriptable scriptable) throws IOException {
492
493 if (webClient.getOptions().isFetchPolyfillEnabled()) {
494 Polyfill.getFetchPolyfill().apply(context, scope, scriptable);
495 }
496 }
497
498 private static void defineConstructor(final VarScope scope,
499 final Scriptable prototype, final ScriptableObject constructor) {
500 constructor.setParentScope(scope);
501 try {
502 ScriptableObject.defineProperty(prototype, "constructor", constructor,
503 ScriptableObject.DONTENUM | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
504 }
505 catch (final Exception e) {
506
507 if (LOG.isWarnEnabled()) {
508 final String newline = System.lineSeparator();
509 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
510 + e.getMessage() + newline
511 + "prototype: " + prototype.getClassName(), e);
512 }
513 }
514
515 try {
516 ScriptableObject.defineProperty(constructor, "prototype", prototype,
517 ScriptableObject.DONTENUM | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
518 }
519 catch (final Exception e) {
520
521 if (LOG.isWarnEnabled()) {
522 final String newline = System.lineSeparator();
523 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
524 + e.getMessage() + newline
525 + "prototype: " + prototype.getClassName(), e);
526 }
527 }
528 }
529
530
531
532
533
534
535 private static void deleteProperties(final Scriptable destination, final String... propertiesToDelete) {
536 for (final String property : propertiesToDelete) {
537 destination.delete(property);
538 }
539 }
540
541
542
543
544
545
546
547 private static void removePrototypeProperties(final VarScope scope, final String className,
548 final String... properties) {
549 final ScriptableObject prototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, className);
550 for (final String property : properties) {
551 prototype.delete(property);
552 }
553 }
554
555
556
557
558
559
560
561
562
563
564 public static HtmlUnitScriptable configureClass(final ClassConfiguration config,
565 final TopLevel scope)
566 throws Exception {
567
568 final HtmlUnitScriptable prototype = config.getHostClass().getDeclaredConstructor().newInstance();
569 prototype.setParentScope(scope);
570 prototype.setClassName(config.getClassName());
571
572 configureConstantsPropertiesAndFunctions(config, scope, prototype);
573
574 return prototype;
575 }
576
577
578
579
580
581
582 private static void configureConstantsStaticPropertiesAndStaticFunctions(final ClassConfiguration config,
583 final TopLevel scope, final ScriptableObject scriptable) {
584 configureConstants(config, scriptable);
585 configureStaticProperties(config, scope, scriptable);
586 configureStaticFunctions(config, scope, scriptable);
587 }
588
589
590
591
592
593
594
595 private static void configureConstantsPropertiesAndFunctions(final ClassConfiguration config,
596 final TopLevel scope, final ScriptableObject scriptable) {
597 configureConstants(config, scriptable);
598 configureProperties(config, scope, scriptable);
599 configureFunctions(config, scope, scriptable);
600 configureSymbolConstants(config, scriptable);
601 configureSymbols(config, scope, scriptable);
602 }
603
604 private static void configureFunctions(final ClassConfiguration config,
605 final TopLevel scope, final ScriptableObject scriptable) {
606
607 final Map<String, Method> functionMap = config.getFunctionMap();
608 if (functionMap != null) {
609 for (final Entry<String, Method> functionInfo : functionMap.entrySet()) {
610 final String functionName = functionInfo.getKey();
611 final Method method = functionInfo.getValue();
612 final FunctionObject functionObject = new FunctionObject(functionName, method, scope);
613 scriptable.defineProperty(functionName, functionObject, ScriptableObject.EMPTY);
614 }
615 }
616 }
617
618 private static void configureConstants(final ClassConfiguration config, final ScriptableObject scriptable) {
619 final List<ConstantInfo> constants = config.getConstants();
620 if (constants != null) {
621 for (final ConstantInfo constantInfo : constants) {
622 scriptable.defineProperty(constantInfo.getName(), constantInfo.getValue(), constantInfo.getFlag());
623 }
624 }
625 }
626
627 private static void configureProperties(final ClassConfiguration config,
628 final TopLevel scope, final ScriptableObject scriptable) {
629 final Map<String, PropertyInfo> propertyMap = config.getPropertyMap();
630 if (propertyMap != null) {
631 for (final Entry<String, PropertyInfo> propertyEntry : propertyMap.entrySet()) {
632 final PropertyInfo info = propertyEntry.getValue();
633 final Method readMethod = info.getReadMethod();
634 final Method writeMethod = info.getWriteMethod();
635 scriptable.defineProperty(scope, propertyEntry.getKey(), null, readMethod, writeMethod, ScriptableObject.EMPTY);
636 }
637 }
638 }
639
640 private static void configureStaticProperties(final ClassConfiguration config,
641 final TopLevel scope, final ScriptableObject scriptable) {
642 final Map<String, PropertyInfo> staticPropertyMap = config.getStaticPropertyMap();
643 if (staticPropertyMap != null) {
644 for (final Entry<String, ClassConfiguration.PropertyInfo> propertyEntry : staticPropertyMap.entrySet()) {
645 final String propertyName = propertyEntry.getKey();
646 final Method readMethod = propertyEntry.getValue().getReadMethod();
647 final Method writeMethod = propertyEntry.getValue().getWriteMethod();
648 final int flag = ScriptableObject.EMPTY;
649
650 scriptable.defineProperty(scope, propertyName, null, readMethod, writeMethod, flag);
651 }
652 }
653 }
654
655 private static void configureStaticFunctions(final ClassConfiguration config,
656 final TopLevel scope, final ScriptableObject scriptable) {
657 final Map<String, Method> staticFunctionMap = config.getStaticFunctionMap();
658 if (staticFunctionMap != null) {
659 for (final Entry<String, Method> staticFunctionInfo : staticFunctionMap.entrySet()) {
660 final String functionName = staticFunctionInfo.getKey();
661 final Method method = staticFunctionInfo.getValue();
662 final FunctionObject staticFunctionObject = new FunctionObject(functionName, method, scope);
663 scriptable.defineProperty(functionName, staticFunctionObject, ScriptableObject.EMPTY);
664 }
665 }
666 }
667
668 private static void configureSymbolConstants(final ClassConfiguration config, final ScriptableObject scriptable) {
669 final Map<Symbol, String> symbolConstantMap = config.getSymbolConstantMap();
670 if (symbolConstantMap != null) {
671 for (final Entry<Symbol, String> symbolInfo : symbolConstantMap.entrySet()) {
672 scriptable.defineProperty(symbolInfo.getKey(), symbolInfo.getValue(), ScriptableObject.DONTENUM | ScriptableObject.READONLY);
673 }
674 }
675 }
676
677 private static void configureSymbols(final ClassConfiguration config,
678 final TopLevel scope, final ScriptableObject scriptable) {
679 final Map<Symbol, Method> symbolMap = config.getSymbolMap();
680 if (symbolMap != null) {
681 for (final Entry<Symbol, Method> symbolInfo : symbolMap.entrySet()) {
682 final Symbol symbol = symbolInfo.getKey();
683 final Method method = symbolInfo.getValue();
684 final String methodName = method.getName();
685
686 final Callable symbolFunction;
687
688 final Object property = scriptable.get(methodName, scriptable);
689 if (property == Scriptable.NOT_FOUND) {
690 symbolFunction = new FunctionObject(methodName, method, scope);
691 }
692 else {
693 symbolFunction = (Callable) property;
694 }
695 scriptable.defineProperty(symbol, symbolFunction, ScriptableObject.DONTENUM);
696 }
697 }
698 }
699
700
701
702
703
704 @Override
705 public synchronized void registerWindowAndMaybeStartEventLoop(final WebWindow webWindow) {
706 if (shutdownPending_) {
707 return;
708 }
709
710 final WebClient webClient = getWebClient();
711 if (webClient != null) {
712 if (javaScriptExecutor_ == null) {
713 javaScriptExecutor_ = BackgroundJavaScriptFactory.theFactory().createJavaScriptExecutor(webClient);
714 }
715 javaScriptExecutor_.addWindow(webWindow);
716 }
717 }
718
719
720
721
722 @Override
723 public void prepareShutdown() {
724 shutdownPending_ = true;
725 }
726
727
728
729
730 @Override
731 public void shutdown() {
732 webClient_ = null;
733 contextFactory_ = null;
734 jsConfig_ = null;
735
736 if (javaScriptExecutor_ != null) {
737 javaScriptExecutor_.shutdown();
738 javaScriptExecutor_ = null;
739 }
740 if (postponedActions_ != null) {
741 postponedActions_.remove();
742 }
743 if (javaScriptRunning_ != null) {
744 javaScriptRunning_.remove();
745 }
746 holdPostponedActions_ = false;
747 }
748
749
750
751
752 @Override
753 public Script compile(final HtmlPage owningPage, final VarScope scope, final String sourceCode,
754 final String sourceName, final int startLine) {
755 WebAssert.notNull("sourceCode", sourceCode);
756
757 if (LOG.isTraceEnabled()) {
758 final String newline = System.lineSeparator();
759 LOG.trace("Javascript compile " + sourceName + newline + sourceCode + newline);
760 }
761
762 final HtmlUnitCompileContextAction action = new HtmlUnitCompileContextAction(owningPage, sourceCode, sourceName, startLine);
763 return (Script) getContextFactory().callSecured(action, owningPage);
764 }
765
766
767
768
769
770
771
772
773
774 public final <T> T callSecured(final ContextAction<T> action, final HtmlPage page) {
775 if (shutdownPending_ || webClient_ == null) {
776
777 return null;
778 }
779
780 return getContextFactory().callSecured(action, page);
781 }
782
783
784
785
786 @Override
787 public Object execute(final HtmlPage page,
788 final VarScope scope,
789 final String sourceCode,
790 final String sourceName,
791 final int startLine) {
792 final Script script = compile(page, scope, sourceCode, sourceName, startLine);
793 if (script == null) {
794
795 return null;
796 }
797 return execute(page, scope, script);
798 }
799
800
801
802
803 @Override
804 public Object execute(final HtmlPage page, final VarScope scope, final Script script) {
805 if (shutdownPending_ || webClient_ == null) {
806
807 return null;
808 }
809
810 final HtmlUnitContextAction action = new HtmlUnitContextAction(page) {
811 @Override
812 public Object doRun(final Context cx) {
813 return script.exec(cx, scope, ScriptableObject.getTopLevelScope(scope).getGlobalThis());
814 }
815
816 @Override
817 protected String getSourceCode(final Context cx) {
818 return null;
819 }
820 };
821
822 return getContextFactory().callSecured(action, page);
823 }
824
825
826
827
828
829
830
831
832
833
834 public Object callFunction(
835 final HtmlPage page,
836 final Function javaScriptFunction,
837 final Scriptable thisObject,
838 final Object[] args,
839 final DomNode node) {
840
841 VarScope scope = ScriptableObject.getTopLevelScope(thisObject.getParentScope());
842
843 if (node != null && node instanceof HtmlElement htmlElement) {
844 final HTMLElement elem = htmlElement.getScriptableObject();
845 scope = new WithScope(scope, elem.getOwnerDocument());
846
847 if (htmlElement instanceof SubmittableElement) {
848 final HtmlForm enclosingForm = htmlElement.getEnclosingForm();
849 if (enclosingForm != null) {
850 scope = new WithScope(scope, enclosingForm.getScriptableObject());
851 }
852 }
853
854 scope = new WithScope(scope, node.getScriptableObject());
855 }
856
857 return callFunction(page, javaScriptFunction, scope, thisObject, args);
858 }
859
860
861
862
863
864
865
866
867
868
869 public Object callFunction(final HtmlPage page, final Function function,
870 final VarScope scope, final Scriptable thisObject, final Object[] args) {
871 if (shutdownPending_ || webClient_ == null) {
872
873 return null;
874 }
875
876 final HtmlUnitContextAction action = new HtmlUnitContextAction(page) {
877 @Override
878 public Object doRun(final Context cx) {
879 if (ScriptRuntime.hasTopCall(cx)) {
880 return function.call(cx, scope, thisObject, args);
881 }
882 return ScriptRuntime.doTopCall(function, cx, scope, thisObject, args, cx.isStrictMode());
883 }
884
885 @Override
886 protected String getSourceCode(final Context cx) {
887 return cx.decompileFunction(function, 2);
888 }
889 };
890 return getContextFactory().callSecured(action, page);
891 }
892
893
894
895
896
897
898
899 @Override
900 public boolean isScriptRunning() {
901 return Boolean.TRUE.equals(javaScriptRunning_.get());
902 }
903
904
905
906
907
908 private final class HtmlUnitCompileContextAction implements ContextAction<Object> {
909 private final HtmlPage page_;
910 private final String sourceCode_;
911 private final String sourceName_;
912 private final int startLine_;
913
914 HtmlUnitCompileContextAction(final HtmlPage page, final String sourceCode, final String sourceName, final int startLine) {
915 page_ = page;
916 sourceCode_ = sourceCode;
917 sourceName_ = sourceName;
918 startLine_ = startLine;
919 }
920
921 @Override
922 public Object run(final Context cx) {
923 try {
924 final Object response;
925 cx.putThreadLocal(KEY_STARTING_PAGE, page_);
926 synchronized (page_) {
927 if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
928 return null;
929 }
930 response = cx.compileString(sourceCode_, sourceName_, startLine_, null);
931
932 }
933
934 return response;
935 }
936 catch (final Exception e) {
937 handleJavaScriptException(new ScriptException(page_, e, sourceCode_), true);
938 return null;
939 }
940 catch (final TimeoutError e) {
941 handleJavaScriptTimeoutError(page_, e);
942 return null;
943 }
944 }
945 }
946
947
948
949
950
951
952 private abstract class HtmlUnitContextAction implements ContextAction<Object> {
953 private final HtmlPage page_;
954
955 HtmlUnitContextAction(final HtmlPage page) {
956 page_ = page;
957 }
958
959 @Override
960 public final Object run(final Context cx) {
961 final Boolean javaScriptAlreadyRunning = javaScriptRunning_.get();
962 javaScriptRunning_.set(Boolean.TRUE);
963
964 try {
965 final Object response;
966 synchronized (page_) {
967 if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
968 return null;
969 }
970 response = doRun(cx);
971 }
972
973 cx.processMicrotasks();
974
975
976
977 if (!holdPostponedActions_) {
978 doProcessPostponedActions();
979 }
980
981 return response;
982 }
983 catch (final Exception e) {
984 handleJavaScriptException(new ScriptException(page_, e, getSourceCode(cx)), true);
985 return null;
986 }
987 catch (final TimeoutError e) {
988 handleJavaScriptTimeoutError(page_, e);
989 return null;
990 }
991 finally {
992 javaScriptRunning_.set(javaScriptAlreadyRunning);
993 }
994 }
995
996 protected abstract Object doRun(Context cx);
997
998 protected abstract String getSourceCode(Context cx);
999 }
1000
1001 private void doProcessPostponedActions() {
1002 holdPostponedActions_ = false;
1003
1004 final WebClient webClient = getWebClient();
1005 if (webClient == null) {
1006
1007 postponedActions_.set(null);
1008 return;
1009 }
1010
1011 try {
1012 webClient.loadDownloadedResponses();
1013 }
1014 catch (final RuntimeException e) {
1015 throw e;
1016 }
1017 catch (final Exception e) {
1018 throw new RuntimeException(e);
1019 }
1020
1021 final List<PostponedAction> actions = postponedActions_.get();
1022 if (actions != null && !actions.isEmpty()) {
1023 postponedActions_.set(new ArrayList<>());
1024 try {
1025 for (final PostponedAction action : actions) {
1026 if (LOG.isDebugEnabled()) {
1027 LOG.debug("Processing PostponedAction " + action);
1028 }
1029
1030
1031 if (action.isStillAlive()) {
1032 action.execute();
1033 }
1034 }
1035 }
1036 catch (final RuntimeException e) {
1037 throw e;
1038 }
1039 catch (final Exception e) {
1040 throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
1041 }
1042 }
1043 }
1044
1045
1046
1047
1048
1049 @Override
1050 public void addPostponedAction(final PostponedAction action) {
1051 if (shutdownPending_) {
1052 return;
1053 }
1054
1055 List<PostponedAction> actions = postponedActions_.get();
1056 if (actions == null) {
1057 actions = new ArrayList<>();
1058 postponedActions_.set(actions);
1059 }
1060 actions.add(action);
1061 }
1062
1063
1064
1065
1066
1067
1068 protected void handleJavaScriptException(final ScriptException scriptException, final boolean triggerOnError) {
1069 final WebClient webClient = getWebClient();
1070 if (shutdownPending_ || webClient == null) {
1071
1072 return;
1073 }
1074
1075
1076 final HtmlPage page = scriptException.getPage();
1077 if (triggerOnError && page != null) {
1078 final WebWindow window = page.getEnclosingWindow();
1079 if (window != null) {
1080 final Window w = window.getScriptableObject();
1081 if (w != null) {
1082 try {
1083 w.triggerOnError(scriptException);
1084 }
1085 catch (final Exception e) {
1086 handleJavaScriptException(new ScriptException(page, e, null), false);
1087 }
1088 }
1089 }
1090 }
1091
1092 webClient.getJavaScriptErrorListener().scriptException(page, scriptException);
1093
1094 if (webClient.getOptions().isThrowExceptionOnScriptError()) {
1095 throw scriptException;
1096 }
1097 }
1098
1099
1100
1101
1102
1103
1104 protected void handleJavaScriptTimeoutError(final HtmlPage page, final TimeoutError e) {
1105 final WebClient webClient = getWebClient();
1106 if (shutdownPending_ || webClient == null) {
1107
1108 return;
1109 }
1110
1111 webClient.getJavaScriptErrorListener().timeoutError(page, e.getAllowedTime(), e.getExecutionTime());
1112 if (webClient.getOptions().isThrowExceptionOnScriptError()) {
1113 throw new RuntimeException(e);
1114 }
1115 LOG.info("Caught script timeout error", e);
1116 }
1117
1118
1119
1120
1121
1122 @Override
1123 public void holdPosponedActions() {
1124 holdPostponedActions_ = true;
1125 }
1126
1127
1128
1129
1130
1131 @Override
1132 public void processPostponedActions() {
1133 doProcessPostponedActions();
1134 }
1135
1136
1137
1138
1139 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
1140 in.defaultReadObject();
1141 initTransientFields();
1142 }
1143
1144 private void initTransientFields() {
1145 javaScriptRunning_ = new ThreadLocal<>();
1146 postponedActions_ = new ThreadLocal<>();
1147 holdPostponedActions_ = false;
1148 shutdownPending_ = false;
1149 }
1150
1151
1152
1153
1154
1155
1156 public Class<? extends HtmlUnitScriptable> getJavaScriptClass(final Class<?> c) {
1157 return jsConfig_.getDomJavaScriptMappingFor(c);
1158 }
1159
1160
1161
1162
1163
1164 @Override
1165 public JavaScriptConfiguration getJavaScriptConfiguration() {
1166 return jsConfig_;
1167 }
1168
1169
1170
1171
1172
1173 @Override
1174 public long getJavaScriptTimeout() {
1175 return getContextFactory().getTimeout();
1176 }
1177
1178
1179
1180
1181
1182 @Override
1183 public void setJavaScriptTimeout(final long timeout) {
1184 getContextFactory().setTimeout(timeout);
1185 }
1186
1187
1188
1189
1190
1191
1192
1193 public static double toNumber(final Object value) {
1194 return ScriptRuntime.toNumber(value);
1195 }
1196
1197
1198
1199
1200
1201
1202
1203 public static String toString(final Object value) {
1204 return ScriptRuntime.toString(value);
1205 }
1206
1207
1208
1209
1210
1211
1212
1213 public static boolean toBoolean(final Object value) {
1214 return ScriptRuntime.toBoolean(value);
1215 }
1216
1217
1218
1219
1220
1221
1222
1223 public static RuntimeException throwAsScriptRuntimeEx(final Throwable e) {
1224 throw Context.throwAsScriptRuntimeEx(e);
1225 }
1226
1227
1228
1229
1230
1231
1232
1233 public static RuntimeException reportRuntimeError(final String message) {
1234 throw Context.reportRuntimeError(message);
1235 }
1236
1237
1238
1239
1240
1241
1242
1243 public static EcmaError syntaxError(final String message) {
1244 return ScriptRuntime.syntaxError(message);
1245 }
1246
1247
1248
1249
1250
1251
1252
1253 public static EcmaError typeError(final String message) {
1254 return ScriptRuntime.typeError(message);
1255 }
1256
1257
1258
1259
1260
1261
1262 public static EcmaError typeErrorIllegalConstructor() {
1263 throw JavaScriptEngine.typeError("Illegal constructor.");
1264 }
1265
1266
1267
1268
1269
1270
1271
1272 public static EcmaError rangeError(final String message) {
1273 return ScriptRuntime.rangeError(message);
1274 }
1275
1276
1277
1278
1279
1280
1281
1282
1283 public static EcmaError constructError(final String error, final String message) {
1284 return ScriptRuntime.constructError(error, message);
1285 }
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297 public static RhinoException asJavaScriptException(final HtmlUnitScriptable scriptable, final String message, final int type) {
1298 final DOMException domException = new DOMException(message, type);
1299 domException.setParentScope(scriptable.getParentScope());
1300
1301 final WindowOrWorkerGlobalScope wow = HtmlUnitScriptable.getWindowOrWorkerGlobalScope(scriptable);
1302 if (wow instanceof Window window) {
1303 domException.setPrototype(window.getPrototype(DOMException.class));
1304 }
1305 else if (wow instanceof WorkerGlobalScope w) {
1306 domException.setPrototype(w.getPrototype(DOMException.class));
1307 }
1308
1309 final EcmaError helper = ScriptRuntime.syntaxError("helper");
1310 String fileName = helper.sourceName();
1311 if (fileName != null) {
1312 fileName = fileName.replaceFirst("script in (.*) from .*", "$1");
1313 }
1314 domException.setLocation(fileName, helper.lineNumber());
1315
1316 return new JavaScriptException(domException, fileName, helper.lineNumber());
1317 }
1318
1319
1320
1321
1322
1323
1324
1325
1326 public static void setFunctionProtoAndParent(final BaseFunction fn, final Context cx, final VarScope scope) {
1327 ScriptRuntime.setFunctionProtoAndParent(fn, cx, scope);
1328 }
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338 public static Scriptable newObject(final VarScope scope, final String constructorName, final Object[] args) {
1339 return ScriptRuntime.newObject(Context.getCurrentContext(), scope, constructorName, args);
1340 }
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351 public static Scriptable newObject(final VarScope scope) {
1352 final NativeObject result = new NativeObject();
1353 ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Object);
1354 return result;
1355 }
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365 public static Scriptable newArray(final VarScope scope, final int length) {
1366 final NativeArray result = new NativeArray(length);
1367 ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1368 return result;
1369 }
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380 public static Scriptable newArrayIteratorTypeKeys(final VarScope scope, final Scriptable arrayLike) {
1381 return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.KEYS);
1382 }
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393 public static Scriptable newArrayIteratorTypeValues(final VarScope scope, final Scriptable arrayLike) {
1394 return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.VALUES);
1395 }
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406 public static Scriptable newArrayIteratorTypeEntries(final VarScope scope, final Scriptable arrayLike) {
1407 return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.ENTRIES);
1408 }
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418 public static Scriptable newArray(final VarScope scope, final Object[] elements) {
1419 if (elements.getClass().getComponentType() != ScriptRuntime.ObjectClass) {
1420 throw new IllegalArgumentException();
1421 }
1422
1423 final NativeArray result = new NativeArray(elements);
1424 ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1425 return result;
1426 }
1427
1428
1429
1430
1431
1432
1433
1434
1435 public static NativeUint8Array newUint8Array(final VarScope scope, final byte[] elements) {
1436 final NativeArrayBuffer arrayBuffer = new NativeArrayBuffer(elements.length);
1437 ScriptRuntime.setBuiltinProtoAndParent(arrayBuffer, scope, TopLevel.Builtins.ArrayBuffer);
1438 arrayBuffer.getByteBuffer().put(elements);
1439
1440 final NativeUint8Array uint8Array = new NativeUint8Array(arrayBuffer, 0, elements.length);
1441 ScriptRuntime.setBuiltinProtoAndParent(uint8Array, scope, TopLevel.Builtins.Uint8Array);
1442
1443 return uint8Array;
1444 }
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461 public static boolean iterate(final Context cx, final VarScope scope,
1462 final Scriptable scriptable, final Consumer<Object> consumer) {
1463 if (ScriptableObject.hasProperty(scriptable, SymbolKey.ITERATOR)) {
1464 final Object iterator = ScriptRuntime.callIterator(scriptable, cx, scope);
1465 try (IteratorLikeIterable itr = new IteratorLikeIterable(cx, scope, iterator)) {
1466 for (final Object elem : itr) {
1467 consumer.accept(elem);
1468 }
1469 }
1470
1471 return true;
1472 }
1473
1474 return false;
1475 }
1476
1477
1478
1479
1480
1481
1482
1483 public static int toInt32(final Object o) {
1484 return ScriptRuntime.toInt32(o);
1485 }
1486
1487
1488
1489
1490
1491
1492
1493 public static double toInteger(final Object o) {
1494 return ScriptRuntime.toInteger(o);
1495 }
1496
1497
1498
1499
1500
1501
1502
1503
1504 public static double toInteger(final Object[] args, final int index) {
1505 return ScriptRuntime.toInteger(args, index);
1506 }
1507
1508
1509
1510
1511
1512
1513
1514 public static boolean isUndefined(final Object obj) {
1515 return org.htmlunit.corejs.javascript.Undefined.isUndefined(obj);
1516 }
1517
1518
1519
1520
1521
1522
1523
1524 public static boolean isNaN(final Object obj) {
1525 return ScriptRuntime.isNaN(obj);
1526 }
1527
1528
1529
1530
1531
1532
1533
1534 public static boolean isArray(final Object obj) {
1535 return (obj instanceof Scriptable s)
1536 && "Array".equals(s.getClassName());
1537 }
1538
1539
1540
1541
1542
1543
1544
1545 public static boolean isArrayLike(final Scriptable obj) {
1546 return ScriptRuntime.isArrayLike(obj);
1547 }
1548
1549
1550
1551
1552
1553
1554
1555
1556 public static long lengthOfArrayLike(final Context cx, final Scriptable obj) {
1557 return AbstractEcmaObjectOperations.lengthOfArrayLike(cx, obj);
1558 }
1559
1560
1561
1562
1563
1564
1565
1566
1567 public static void iterateArrayLike(final Context cx, final Scriptable arrayLike, final Consumer<Object> consumer) {
1568 final Context context = cx == null ? Context.getCurrentContext() : cx;
1569
1570 final long len = lengthOfArrayLike(context, arrayLike);
1571 for (int i = 0; i < len; i++) {
1572 final Object item = arrayLike.get(i, arrayLike);
1573 consumer.accept(item);
1574 }
1575 }
1576
1577
1578
1579
1580
1581
1582 public static TopLevel getTopCallScope() {
1583 return ScriptRuntime.getTopCallScope(Context.getCurrentContext());
1584 }
1585
1586
1587
1588
1589
1590
1591
1592 public static String uncompressJavaScript(final String scriptSource, final String scriptName) {
1593 final ContextFactory factory = new ContextFactory();
1594 final ContextAction<Object> action = cx -> {
1595 cx.setInterpretedMode(true);
1596 final Script script = cx.compileString(scriptSource, scriptName, 0, null);
1597 return cx.decompileScript(script, 4);
1598 };
1599
1600 return (String) factory.call(action);
1601 }
1602
1603
1604
1605
1606
1607
1608
1609
1610 public static String evaluateProxyAutoConfig(final BrowserVersion browserVersion, final String content, final URL url) {
1611 try (Context cx = Context.enter()) {
1612 final ProxyAutoConfigJavaScriptConfiguration jsConfig =
1613 ProxyAutoConfigJavaScriptConfiguration.getInstance(browserVersion);
1614
1615 final ScriptableObject globalThis = new NativeObject();
1616 final TopLevel scope = cx.initSafeStandardObjects(new TopLevel(globalThis));
1617
1618 for (final ClassConfiguration config : jsConfig.getAll()) {
1619 configureFunctions(config, scope, globalThis);
1620 }
1621
1622 cx.evaluateString(scope, "var ProxyConfig = function() {}; ProxyConfig.bindings = {}; ProxyConfig", "<init>", 1, null);
1623 cx.evaluateString(scope, content, "<Proxy Auto-Config>", 1, null);
1624
1625 final Object[] functionArgs = {url.toExternalForm(), url.getHost()};
1626 final Function f = (Function) scope.get("FindProxyForURL", scope);
1627 final Object result = f.call(cx, scope, globalThis, functionArgs);
1628 return toString(result);
1629 }
1630 }
1631 }