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