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_WINDOW_INSTALL_TRIGGER_NULL;
19
20 import java.io.IOException;
21 import java.io.ObjectInputStream;
22 import java.lang.reflect.Member;
23 import java.lang.reflect.Method;
24 import java.net.URL;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Map.Entry;
30 import java.util.function.Consumer;
31
32 import org.apache.commons.lang3.ObjectUtils.Null;
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.JavaScriptException;
51 import org.htmlunit.corejs.javascript.NativeArray;
52 import org.htmlunit.corejs.javascript.NativeArrayIterator;
53 import org.htmlunit.corejs.javascript.NativeConsole;
54 import org.htmlunit.corejs.javascript.NativeObject;
55 import org.htmlunit.corejs.javascript.RhinoException;
56 import org.htmlunit.corejs.javascript.Script;
57 import org.htmlunit.corejs.javascript.ScriptRuntime;
58 import org.htmlunit.corejs.javascript.Scriptable;
59 import org.htmlunit.corejs.javascript.ScriptableObject;
60 import org.htmlunit.corejs.javascript.StackStyle;
61 import org.htmlunit.corejs.javascript.Symbol;
62 import org.htmlunit.corejs.javascript.TopLevel;
63 import org.htmlunit.corejs.javascript.VarScope;
64 import org.htmlunit.corejs.javascript.WithScope;
65 import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBuffer;
66 import org.htmlunit.corejs.javascript.typedarrays.NativeUint8Array;
67 import org.htmlunit.html.DomNode;
68 import org.htmlunit.html.HtmlElement;
69 import org.htmlunit.html.HtmlForm;
70 import org.htmlunit.html.HtmlPage;
71 import org.htmlunit.html.SubmittableElement;
72 import org.htmlunit.javascript.background.BackgroundJavaScriptFactory;
73 import org.htmlunit.javascript.background.JavaScriptExecutor;
74 import org.htmlunit.javascript.configuration.AbstractJavaScriptConfiguration;
75 import org.htmlunit.javascript.configuration.ClassConfiguration;
76 import org.htmlunit.javascript.configuration.ClassConfiguration.ConstantInfo;
77 import org.htmlunit.javascript.configuration.ClassConfiguration.PropertyInfo;
78 import org.htmlunit.javascript.configuration.JavaScriptConfiguration;
79 import org.htmlunit.javascript.configuration.ProxyAutoConfigJavaScriptConfiguration;
80 import org.htmlunit.javascript.host.ConsoleCustom;
81 import org.htmlunit.javascript.host.URLSearchParams;
82 import org.htmlunit.javascript.host.Window;
83 import org.htmlunit.javascript.host.WindowOrWorkerGlobalScope;
84 import org.htmlunit.javascript.host.dom.DOMException;
85 import org.htmlunit.javascript.host.html.HTMLElement;
86 import org.htmlunit.javascript.host.html.HTMLImageElement;
87 import org.htmlunit.javascript.host.html.HTMLOptionElement;
88 import org.htmlunit.javascript.host.intl.Intl;
89 import org.htmlunit.javascript.host.worker.WorkerGlobalScope;
90 import org.htmlunit.javascript.host.xml.FormData;
91 import org.htmlunit.javascript.polyfill.Polyfill;
92 import org.htmlunit.util.StringUtils;
93
94
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 public class JavaScriptEngine implements AbstractJavaScriptEngine<Script> {
120
121 private static final Log LOG = LogFactory.getLog(JavaScriptEngine.class);
122
123
124 public static final Object[] EMPTY_ARGS = ScriptRuntime.emptyArgs;
125
126
127 public static final Object UNDEFINED = org.htmlunit.corejs.javascript.Undefined.instance;
128
129 private WebClient webClient_;
130 private HtmlUnitContextFactory contextFactory_;
131 private JavaScriptConfiguration jsConfig_;
132
133 private transient ThreadLocal<Boolean> javaScriptRunning_;
134 private transient ThreadLocal<List<PostponedAction>> postponedActions_;
135 private transient boolean holdPostponedActions_;
136 private transient boolean shutdownPending_;
137
138
139 private transient JavaScriptExecutor javaScriptExecutor_;
140
141
142
143
144
145 public static final String KEY_STARTING_PAGE = "startingPage";
146
147
148
149
150
151
152 public JavaScriptEngine(final WebClient webClient) {
153 if (webClient == null) {
154 throw new IllegalArgumentException("JavaScriptEngine ctor requires a webClient");
155 }
156
157 webClient_ = webClient;
158 contextFactory_ = new HtmlUnitContextFactory(webClient);
159 initTransientFields();
160
161 jsConfig_ = JavaScriptConfiguration.getInstance(webClient.getBrowserVersion());
162 RhinoException.setStackStyle(StackStyle.MOZILLA_LF);
163 }
164
165
166
167
168
169 private WebClient getWebClient() {
170 return webClient_;
171 }
172
173
174
175
176 @Override
177 public HtmlUnitContextFactory getContextFactory() {
178 return contextFactory_;
179 }
180
181
182
183
184
185 @Override
186 public void initialize(final WebWindow webWindow, final Page page) {
187 WebAssert.notNull("webWindow", webWindow);
188
189 if (shutdownPending_) {
190 return;
191 }
192
193 getContextFactory().call(cx -> {
194 try {
195 init(webWindow, page, cx);
196 }
197 catch (final Exception e) {
198 LOG.error("Exception while initializing JavaScript for the page", e);
199 throw new ScriptException(null, e);
200 }
201 return null;
202 });
203 }
204
205
206
207
208
209
210 public JavaScriptExecutor getJavaScriptExecutor() {
211 return javaScriptExecutor_;
212 }
213
214
215
216
217
218
219
220 private void init(final WebWindow webWindow, final Page page, final Context cx) throws Exception {
221 final WebClient webClient = getWebClient();
222 final BrowserVersion browserVersion = webClient.getBrowserVersion();
223
224 final Window jsWindow = new Window();
225 jsWindow.setClassName("Window");
226
227 final TopLevel scope = cx.initSafeStandardObjects(new TopLevel(jsWindow));
228 jsWindow.setParentScope(scope);
229 configureRhino(webClient, browserVersion, scope, jsWindow);
230
231 final Map<Class<? extends Scriptable>, Scriptable> prototypes = new HashMap<>();
232 final Map<String, Scriptable> prototypesPerJSName = new HashMap<>();
233
234 final ClassConfiguration windowConfig = jsConfig_.getWindowClassConfiguration();
235 final FunctionObject functionObject = new FunctionObject(jsWindow.getClassName(), windowConfig.getJsConstructor().getValue(), scope);
236 ScriptableObject.defineProperty(jsWindow, "constructor", functionObject,
237 ScriptableObject.DONTENUM | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
238
239 configureConstantsPropertiesAndFunctions(windowConfig, scope, jsWindow);
240
241 final HtmlUnitScriptable windowPrototype = configureClass(windowConfig, scope);
242 jsWindow.setPrototype(windowPrototype);
243 prototypes.put(windowConfig.getHostClass(), windowPrototype);
244 prototypesPerJSName.put(windowConfig.getClassName(), windowPrototype);
245
246 configureGlobalThis(scope, jsWindow, windowConfig, functionObject, jsConfig_, browserVersion, prototypes, prototypesPerJSName);
247
248
249 URLSearchParams.NativeParamsIterator.init(scope, "URLSearchParams Iterator");
250 FormData.FormDataIterator.init(scope, "FormData Iterator");
251
252
253
254
255
256
257 if (browserVersion.hasFeature(JS_WINDOW_INSTALL_TRIGGER_NULL)) {
258 jsWindow.put("InstallTrigger", jsWindow, null);
259 }
260
261
262 final Method imageCtor = HTMLImageElement.class.getDeclaredMethod("jsConstructorImage");
263 additionalCtor(scope, jsWindow, prototypesPerJSName.get("HTMLImageElement"), imageCtor, "Image", "HTMLImageElement");
264 final Method optionCtor = HTMLOptionElement.class.getDeclaredMethod("jsConstructorOption",
265 Object.class, String.class, boolean.class, boolean.class);
266 additionalCtor(scope, jsWindow, prototypesPerJSName.get("HTMLOptionElement"), optionCtor, "Option", "HTMLOptionElement");
267
268 if (!webClient.getOptions().isWebSocketEnabled()) {
269 deleteProperties(jsWindow, "WebSocket");
270 }
271
272 jsWindow.setPrototypes(prototypes);
273 jsWindow.initialize(scope, webWindow, page);
274
275 applyPolyfills(webClient, browserVersion, cx, scope, jsWindow);
276 }
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291 public static void configureGlobalThis(
292 final TopLevel scope,
293 final HtmlUnitScriptable globalThis,
294 final ClassConfiguration scopeConfig,
295 final FunctionObject scopeContructorFunctionObject,
296 final AbstractJavaScriptConfiguration jsConfig,
297 final BrowserVersion browserVersion,
298 final Map<Class<? extends Scriptable>, Scriptable> prototypes,
299 final Map<String, Scriptable> prototypesPerJSName) throws Exception {
300
301 final Scriptable objectPrototype = ScriptableObject.getObjectPrototype(scope);
302
303 final Map<String, Function> ctorPrototypesPerJSName = new HashMap<>();
304 for (final ClassConfiguration config : jsConfig.getAll()) {
305 final String jsClassName = config.getClassName();
306 Scriptable prototype = prototypesPerJSName.get(jsClassName);
307 final String extendedClassName =
308 StringUtils.isEmptyOrNull(config.getExtendedClassName()) ? null : config.getExtendedClassName();
309
310
311 if (config == scopeConfig) {
312 if (extendedClassName == null) {
313 prototype.setPrototype(objectPrototype);
314 }
315 else {
316 prototype.setPrototype(prototypesPerJSName.get(extendedClassName));
317 }
318
319
320 addAsConstructorAndAlias(scopeContructorFunctionObject, scope, globalThis, prototype, config);
321 configureConstantsStaticPropertiesAndStaticFunctions(config, scope, scopeContructorFunctionObject);
322
323
324 if (extendedClassName != null) {
325 scopeContructorFunctionObject.setPrototype(ctorPrototypesPerJSName.get(extendedClassName));
326 }
327 }
328 else {
329 final HtmlUnitScriptable classPrototype = configureClass(config, scope);
330 prototypes.put(config.getHostClass(), classPrototype);
331 prototypesPerJSName.put(jsClassName, classPrototype);
332 prototype = classPrototype;
333
334 if (extendedClassName == null) {
335 classPrototype.setPrototype(objectPrototype);
336 }
337 else {
338 classPrototype.setPrototype(prototypesPerJSName.get(extendedClassName));
339 }
340
341
342 if (prototype != null) {
343 final Map.Entry<String, Member> jsConstructor = config.getJsConstructor();
344 if (jsConstructor == null) {
345 final HtmlUnitScriptable constructor = config.getHostClass().getDeclaredConstructor().newInstance();
346 constructor.setClassName(jsClassName);
347 defineConstructor(scope, prototype, constructor);
348 configureConstantsStaticPropertiesAndStaticFunctions(config, scope, constructor);
349
350 if (config.isJsObject()) {
351 globalThis.defineProperty(jsClassName, constructor, ScriptableObject.DONTENUM);
352 }
353 }
354 else {
355 final FunctionObject function = new FunctionObject(jsConstructor.getKey(), jsConstructor.getValue(), scope);
356 ctorPrototypesPerJSName.put(jsClassName, function);
357
358 addAsConstructorAndAlias(function, scope, globalThis, prototype, config);
359 configureConstantsStaticPropertiesAndStaticFunctions(config, scope, function);
360
361 if (!config.isJsObject()) {
362
363
364
365 globalThis.delete(prototype.getClassName());
366 }
367
368
369 if (extendedClassName != null) {
370 function.setPrototype(ctorPrototypesPerJSName.get(extendedClassName));
371 }
372 }
373 }
374 }
375 }
376 }
377
378 private static void addAsConstructorAndAlias(final FunctionObject function,
379 final VarScope scope,
380 final HtmlUnitScriptable destination,
381 final Scriptable prototype,
382 final ClassConfiguration config) {
383 try {
384 function.addAsConstructor(scope, prototype, ScriptableObject.DONTENUM);
385
386 final String alias = config.getJsConstructorAlias();
387 if (alias != null) {
388 ScriptableObject.defineProperty(destination, alias, function, ScriptableObject.DONTENUM);
389 }
390 }
391 catch (final Exception e) {
392
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: " + prototype.getClassName(), e);
398 }
399 }
400 }
401
402 private static void additionalCtor(final VarScope scope, final Window window, final Scriptable proto,
403 final Method ctorMethod, final String prop, final String clazzName) throws Exception {
404 final FunctionObject function = new FunctionObject(prop, ctorMethod, scope);
405 final Object prototypeProperty = ScriptableObject.getProperty(window, clazzName);
406 try {
407 function.addAsConstructor(scope, proto, ScriptableObject.DONTENUM);
408 }
409 catch (final Exception e) {
410
411 if (LOG.isWarnEnabled()) {
412 final String newline = System.lineSeparator();
413 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
414 + e.getMessage() + newline
415 + "prototype: " + proto.getClassName(), e);
416 }
417 }
418 ScriptableObject.defineProperty(window, prop, function, ScriptableObject.DONTENUM);
419 ScriptableObject.defineProperty(window, clazzName, prototypeProperty, ScriptableObject.DONTENUM);
420 }
421
422
423
424
425
426
427
428
429
430 public static void configureRhino(final WebClient webClient, final BrowserVersion browserVersion,
431 final TopLevel scope, final HtmlUnitScriptable globalThis) {
432
433
434
435
436 NativeConsole.init(scope, false, webClient.getWebConsole());
437
438
439
440 final ScriptableObject console = (ScriptableObject) ScriptableObject.getProperty(globalThis, "console");
441 console.defineFunctionProperties(scope, new String[] {"timeStamp"}, ConsoleCustom.class, ScriptableObject.DONTENUM);
442
443
444 deleteProperties(globalThis, "Continuation", "StopIteration", "uneval", "global", "__GeneratorFunction");
445
446
447 final ScriptableObject stringPrototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, "String");
448 deleteProperties(stringPrototype, "equals", "equalsIgnoreCase", "toSource");
449
450 final ScriptableObject numberPrototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, "Number");
451 deleteProperties(numberPrototype, "toSource");
452 final ScriptableObject datePrototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, "Date");
453 deleteProperties(datePrototype, "toSource");
454
455 removePrototypeProperties(scope, "Object", "toSource");
456 removePrototypeProperties(scope, "Array", "toSource");
457 removePrototypeProperties(scope, "Function", "toSource");
458
459 deleteProperties(globalThis, "isXMLName");
460
461 NativeFunctionToStringFunction.installFix(scope, browserVersion);
462
463 final ScriptableObject errorObject = (ScriptableObject) ScriptableObject.getProperty(globalThis, "Error");
464 if (browserVersion.hasFeature(JS_ERROR_STACK_TRACE_LIMIT)) {
465 errorObject.defineProperty("stackTraceLimit", 10, ScriptableObject.EMPTY);
466 }
467 else {
468 ScriptableObject.deleteProperty(errorObject, "stackTraceLimit");
469 }
470
471
472 Intl.init(scope, globalThis, browserVersion);
473 }
474
475
476
477
478
479
480
481
482
483
484
485 public static void applyPolyfills(final WebClient webClient, final BrowserVersion browserVersion,
486 final Context context, final VarScope scope, final HtmlUnitScriptable scriptable) throws IOException {
487
488 if (webClient.getOptions().isFetchPolyfillEnabled()) {
489 Polyfill.getFetchPolyfill().apply(context, scope, scriptable);
490 }
491 }
492
493 private static void defineConstructor(final VarScope scope,
494 final Scriptable prototype, final ScriptableObject constructor) {
495 constructor.setParentScope(scope);
496 try {
497 ScriptableObject.defineProperty(prototype, "constructor", constructor,
498 ScriptableObject.DONTENUM | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
499 }
500 catch (final Exception e) {
501
502 if (LOG.isWarnEnabled()) {
503 final String newline = System.lineSeparator();
504 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
505 + e.getMessage() + newline
506 + "prototype: " + prototype.getClassName(), e);
507 }
508 }
509
510 try {
511 ScriptableObject.defineProperty(constructor, "prototype", prototype,
512 ScriptableObject.DONTENUM | ScriptableObject.PERMANENT | ScriptableObject.READONLY);
513 }
514 catch (final Exception e) {
515
516 if (LOG.isWarnEnabled()) {
517 final String newline = System.lineSeparator();
518 LOG.warn("Error during JavaScriptEngine.init(WebWindow, Context)" + newline
519 + e.getMessage() + newline
520 + "prototype: " + prototype.getClassName(), e);
521 }
522 }
523 }
524
525
526
527
528
529
530 private static void deleteProperties(final Scriptable destination, final String... propertiesToDelete) {
531 for (final String property : propertiesToDelete) {
532 destination.delete(property);
533 }
534 }
535
536
537
538
539
540
541
542 private static void removePrototypeProperties(final VarScope scope, final String className,
543 final String... properties) {
544 final ScriptableObject prototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, className);
545 for (final String property : properties) {
546 prototype.delete(property);
547 }
548 }
549
550
551
552
553
554
555
556
557
558
559 public static HtmlUnitScriptable configureClass(final ClassConfiguration config,
560 final TopLevel scope)
561 throws Exception {
562
563 final HtmlUnitScriptable prototype = config.getHostClass().getDeclaredConstructor().newInstance();
564 prototype.setParentScope(scope);
565 prototype.setClassName(config.getClassName());
566
567 configureConstantsPropertiesAndFunctions(config, scope, prototype);
568
569 return prototype;
570 }
571
572
573
574
575
576
577 private static void configureConstantsStaticPropertiesAndStaticFunctions(final ClassConfiguration config,
578 final TopLevel scope, final ScriptableObject scriptable) {
579 configureConstants(config, scriptable);
580 configureStaticProperties(config, scope, scriptable);
581 configureStaticFunctions(config, scope, scriptable);
582 }
583
584
585
586
587
588
589
590 private static void configureConstantsPropertiesAndFunctions(final ClassConfiguration config,
591 final TopLevel scope, final ScriptableObject scriptable) {
592 configureConstants(config, scriptable);
593 configureProperties(config, scope, scriptable);
594 configureFunctions(config, scope, scriptable);
595 configureSymbolConstants(config, scriptable);
596 configureSymbols(config, scope, scriptable);
597 }
598
599 private static void configureFunctions(final ClassConfiguration config,
600 final TopLevel scope, final ScriptableObject scriptable) {
601
602 final Map<String, Method> functionMap = config.getFunctionMap();
603 if (functionMap != null) {
604 for (final Entry<String, Method> functionInfo : functionMap.entrySet()) {
605 final String functionName = functionInfo.getKey();
606 final Method method = functionInfo.getValue();
607 final FunctionObject functionObject = new FunctionObject(functionName, method, scope);
608 scriptable.defineProperty(functionName, functionObject, ScriptableObject.EMPTY);
609 }
610 }
611 }
612
613 private static void configureConstants(final ClassConfiguration config, final ScriptableObject scriptable) {
614 final List<ConstantInfo> constants = config.getConstants();
615 if (constants != null) {
616 for (final ConstantInfo constantInfo : constants) {
617 scriptable.defineProperty(constantInfo.getName(), constantInfo.getValue(), constantInfo.getFlag());
618 }
619 }
620 }
621
622 private static void configureProperties(final ClassConfiguration config,
623 final TopLevel scope, final ScriptableObject scriptable) {
624 final Map<String, PropertyInfo> propertyMap = config.getPropertyMap();
625 if (propertyMap != null) {
626 for (final Entry<String, PropertyInfo> propertyEntry : propertyMap.entrySet()) {
627 final PropertyInfo info = propertyEntry.getValue();
628 final Method readMethod = info.getReadMethod();
629 final Method writeMethod = info.getWriteMethod();
630 scriptable.defineProperty(scope, propertyEntry.getKey(), null, readMethod, writeMethod, ScriptableObject.EMPTY);
631 }
632 }
633 }
634
635 private static void configureStaticProperties(final ClassConfiguration config,
636 final TopLevel scope, final ScriptableObject scriptable) {
637 final Map<String, PropertyInfo> staticPropertyMap = config.getStaticPropertyMap();
638 if (staticPropertyMap != null) {
639 for (final Entry<String, ClassConfiguration.PropertyInfo> propertyEntry : staticPropertyMap.entrySet()) {
640 final String propertyName = propertyEntry.getKey();
641 final Method readMethod = propertyEntry.getValue().getReadMethod();
642 final Method writeMethod = propertyEntry.getValue().getWriteMethod();
643 final int flag = ScriptableObject.EMPTY;
644
645 scriptable.defineProperty(scope, propertyName, null, readMethod, writeMethod, flag);
646 }
647 }
648 }
649
650 private static void configureStaticFunctions(final ClassConfiguration config,
651 final TopLevel scope, final ScriptableObject scriptable) {
652 final Map<String, Method> staticFunctionMap = config.getStaticFunctionMap();
653 if (staticFunctionMap != null) {
654 for (final Entry<String, Method> staticFunctionInfo : staticFunctionMap.entrySet()) {
655 final String functionName = staticFunctionInfo.getKey();
656 final Method method = staticFunctionInfo.getValue();
657 final FunctionObject staticFunctionObject = new FunctionObject(functionName, method, scope);
658 scriptable.defineProperty(functionName, staticFunctionObject, ScriptableObject.EMPTY);
659 }
660 }
661 }
662
663 private static void configureSymbolConstants(final ClassConfiguration config, final ScriptableObject scriptable) {
664 final Map<Symbol, String> symbolConstantMap = config.getSymbolConstantMap();
665 if (symbolConstantMap != null) {
666 for (final Entry<Symbol, String> symbolInfo : symbolConstantMap.entrySet()) {
667 scriptable.defineProperty(symbolInfo.getKey(), symbolInfo.getValue(), ScriptableObject.DONTENUM | ScriptableObject.READONLY);
668 }
669 }
670 }
671
672 private static void configureSymbols(final ClassConfiguration config,
673 final TopLevel scope, final ScriptableObject scriptable) {
674 final Map<Symbol, Method> symbolMap = config.getSymbolMap();
675 if (symbolMap != null) {
676 for (final Entry<Symbol, Method> symbolInfo : symbolMap.entrySet()) {
677 final Symbol symbol = symbolInfo.getKey();
678 final Method method = symbolInfo.getValue();
679 final String methodName = method.getName();
680
681 final Callable symbolFunction;
682
683 final Object property = scriptable.get(methodName, scriptable);
684 if (property == Scriptable.NOT_FOUND) {
685 symbolFunction = new FunctionObject(methodName, method, scope);
686 }
687 else {
688 symbolFunction = (Callable) property;
689 }
690 scriptable.defineProperty(symbol, symbolFunction, ScriptableObject.DONTENUM);
691 }
692 }
693 }
694
695
696
697
698
699 @Override
700 public synchronized void registerWindowAndMaybeStartEventLoop(final WebWindow webWindow) {
701 if (shutdownPending_) {
702 return;
703 }
704
705 final WebClient webClient = getWebClient();
706 if (webClient != null) {
707 if (javaScriptExecutor_ == null) {
708 javaScriptExecutor_ = BackgroundJavaScriptFactory.theFactory().createJavaScriptExecutor(webClient);
709 }
710 javaScriptExecutor_.addWindow(webWindow);
711 }
712 }
713
714
715
716
717 @Override
718 public void prepareShutdown() {
719 shutdownPending_ = true;
720 }
721
722
723
724
725 @Override
726 public void shutdown() {
727 webClient_ = null;
728 contextFactory_ = null;
729 jsConfig_ = null;
730
731 if (javaScriptExecutor_ != null) {
732 javaScriptExecutor_.shutdown();
733 javaScriptExecutor_ = null;
734 }
735 if (postponedActions_ != null) {
736 postponedActions_.remove();
737 }
738 if (javaScriptRunning_ != null) {
739 javaScriptRunning_.remove();
740 }
741 holdPostponedActions_ = false;
742 }
743
744
745
746
747 @Override
748 public Script compile(final HtmlPage owningPage, final VarScope scope, final String sourceCode,
749 final String sourceName, final int startLine) {
750 WebAssert.notNull("sourceCode", sourceCode);
751
752 if (LOG.isTraceEnabled()) {
753 final String newline = System.lineSeparator();
754 LOG.trace("Javascript compile " + sourceName + newline + sourceCode + newline);
755 }
756
757 final HtmlUnitCompileContextAction action = new HtmlUnitCompileContextAction(owningPage, sourceCode, sourceName, startLine);
758 return (Script) getContextFactory().callSecured(action, owningPage);
759 }
760
761
762
763
764
765
766
767
768
769 public final <T> T callSecured(final ContextAction<T> action, final HtmlPage page) {
770 if (shutdownPending_ || webClient_ == null) {
771
772 return null;
773 }
774
775 return getContextFactory().callSecured(action, page);
776 }
777
778
779
780
781 @Override
782 public Object execute(final HtmlPage page,
783 final VarScope scope,
784 final String sourceCode,
785 final String sourceName,
786 final int startLine) {
787 final Script script = compile(page, scope, sourceCode, sourceName, startLine);
788 if (script == null) {
789
790 return null;
791 }
792 return execute(page, scope, script);
793 }
794
795
796
797
798 @Override
799 public Object execute(final HtmlPage page, final VarScope scope, final Script script) {
800 if (shutdownPending_ || webClient_ == null) {
801
802 return null;
803 }
804
805 final HtmlUnitContextAction action = new HtmlUnitContextAction(page) {
806 @Override
807 public Object doRun(final Context cx) {
808 return script.exec(cx, scope, ScriptableObject.getTopLevelScope(scope).getGlobalThis());
809 }
810
811 @Override
812 protected String getSourceCode(final Context cx) {
813 return null;
814 }
815 };
816
817 return getContextFactory().callSecured(action, page);
818 }
819
820
821
822
823
824
825
826
827
828
829 public Object callFunction(
830 final HtmlPage page,
831 final Function javaScriptFunction,
832 final Scriptable thisObject,
833 final Object[] args,
834 final DomNode node) {
835
836 VarScope scope = ScriptableObject.getTopLevelScope(thisObject.getParentScope());
837
838 if (node != null && node instanceof HtmlElement htmlElement) {
839 final HTMLElement elem = htmlElement.getScriptableObject();
840 scope = new WithScope(scope, elem.getOwnerDocument());
841
842 if (htmlElement instanceof SubmittableElement) {
843 final HtmlForm enclosingForm = htmlElement.getEnclosingForm();
844 if (enclosingForm != null) {
845 scope = new WithScope(scope, enclosingForm.getScriptableObject());
846 }
847 }
848
849 scope = new WithScope(scope, node.getScriptableObject());
850 }
851
852 return callFunction(page, javaScriptFunction, scope, thisObject, args);
853 }
854
855
856
857
858
859
860
861
862
863
864 public Object callFunction(final HtmlPage page, final Function function,
865 final VarScope scope, final Scriptable thisObject, final Object[] args) {
866 if (shutdownPending_ || webClient_ == null) {
867
868 return null;
869 }
870
871 final HtmlUnitContextAction action = new HtmlUnitContextAction(page) {
872 @Override
873 public Object doRun(final Context cx) {
874 if (ScriptRuntime.hasTopCall(cx)) {
875 return function.call(cx, scope, thisObject, args);
876 }
877 return ScriptRuntime.doTopCall(function, cx, scope, thisObject, args, cx.isStrictMode());
878 }
879 @Override
880 protected String getSourceCode(final Context cx) {
881 return cx.decompileFunction(function, 2);
882 }
883 };
884 return getContextFactory().callSecured(action, page);
885 }
886
887
888
889
890
891
892 @Override
893 public boolean isScriptRunning() {
894 return Boolean.TRUE.equals(javaScriptRunning_.get());
895 }
896
897
898
899
900
901 private final class HtmlUnitCompileContextAction implements ContextAction<Object> {
902 private final HtmlPage page_;
903 private final String sourceCode_;
904 private final String sourceName_;
905 private final int startLine_;
906
907 HtmlUnitCompileContextAction(final HtmlPage page, final String sourceCode, final String sourceName, final int startLine) {
908 page_ = page;
909 sourceCode_ = sourceCode;
910 sourceName_ = sourceName;
911 startLine_ = startLine;
912 }
913
914 @Override
915 public Object run(final Context cx) {
916 try {
917 final Object response;
918 cx.putThreadLocal(KEY_STARTING_PAGE, page_);
919 synchronized (page_) {
920 if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
921 return null;
922 }
923 response = cx.compileString(sourceCode_, sourceName_, startLine_, null);
924
925 }
926
927 return response;
928 }
929 catch (final Exception e) {
930 handleJavaScriptException(new ScriptException(page_, e, sourceCode_), true);
931 return null;
932 }
933 catch (final TimeoutError e) {
934 handleJavaScriptTimeoutError(page_, e);
935 return null;
936 }
937 }
938 }
939
940
941
942
943
944
945 private abstract class HtmlUnitContextAction implements ContextAction<Object> {
946 private final HtmlPage page_;
947
948 HtmlUnitContextAction(final HtmlPage page) {
949 page_ = page;
950 }
951
952 @Override
953 public final Object run(final Context cx) {
954 final Boolean javaScriptAlreadyRunning = javaScriptRunning_.get();
955 javaScriptRunning_.set(Boolean.TRUE);
956
957 try {
958 final Object response;
959 synchronized (page_) {
960 if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
961 return null;
962 }
963 response = doRun(cx);
964 }
965
966 cx.processMicrotasks();
967
968
969
970 if (!holdPostponedActions_) {
971 doProcessPostponedActions();
972 }
973
974 return response;
975 }
976 catch (final Exception e) {
977 handleJavaScriptException(new ScriptException(page_, e, getSourceCode(cx)), true);
978 return null;
979 }
980 catch (final TimeoutError e) {
981 handleJavaScriptTimeoutError(page_, e);
982 return null;
983 }
984 finally {
985 javaScriptRunning_.set(javaScriptAlreadyRunning);
986 }
987 }
988
989 protected abstract Object doRun(Context cx);
990
991 protected abstract String getSourceCode(Context cx);
992 }
993
994 private void doProcessPostponedActions() {
995 holdPostponedActions_ = false;
996
997 final WebClient webClient = getWebClient();
998 if (webClient == null) {
999
1000 postponedActions_.set(null);
1001 return;
1002 }
1003
1004 try {
1005 webClient.loadDownloadedResponses();
1006 }
1007 catch (final RuntimeException e) {
1008 throw e;
1009 }
1010 catch (final Exception e) {
1011 throw new RuntimeException(e);
1012 }
1013
1014 final List<PostponedAction> actions = postponedActions_.get();
1015 if (actions != null && !actions.isEmpty()) {
1016 postponedActions_.set(new ArrayList<>());
1017 try {
1018 for (final PostponedAction action : actions) {
1019 if (LOG.isDebugEnabled()) {
1020 LOG.debug("Processing PostponedAction " + action);
1021 }
1022
1023
1024 if (action.isStillAlive()) {
1025 action.execute();
1026 }
1027 }
1028 }
1029 catch (final RuntimeException e) {
1030 throw e;
1031 }
1032 catch (final Exception e) {
1033 throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
1034 }
1035 }
1036 }
1037
1038
1039
1040
1041
1042 @Override
1043 public void addPostponedAction(final PostponedAction action) {
1044 if (shutdownPending_) {
1045 return;
1046 }
1047
1048 List<PostponedAction> actions = postponedActions_.get();
1049 if (actions == null) {
1050 actions = new ArrayList<>();
1051 postponedActions_.set(actions);
1052 }
1053 actions.add(action);
1054 }
1055
1056
1057
1058
1059
1060
1061 protected void handleJavaScriptException(final ScriptException scriptException, final boolean triggerOnError) {
1062 final WebClient webClient = getWebClient();
1063 if (shutdownPending_ || webClient == null) {
1064
1065 return;
1066 }
1067
1068
1069 final HtmlPage page = scriptException.getPage();
1070 if (triggerOnError && page != null) {
1071 final WebWindow window = page.getEnclosingWindow();
1072 if (window != null) {
1073 final Window w = window.getScriptableObject();
1074 if (w != null) {
1075 try {
1076 w.triggerOnError(scriptException);
1077 }
1078 catch (final Exception e) {
1079 handleJavaScriptException(new ScriptException(page, e, null), false);
1080 }
1081 }
1082 }
1083 }
1084
1085 webClient.getJavaScriptErrorListener().scriptException(page, scriptException);
1086
1087 if (webClient.getOptions().isThrowExceptionOnScriptError()) {
1088 throw scriptException;
1089 }
1090 }
1091
1092
1093
1094
1095
1096
1097 protected void handleJavaScriptTimeoutError(final HtmlPage page, final TimeoutError e) {
1098 final WebClient webClient = getWebClient();
1099 if (shutdownPending_ || webClient == null) {
1100
1101 return;
1102 }
1103
1104 webClient.getJavaScriptErrorListener().timeoutError(page, e.getAllowedTime(), e.getExecutionTime());
1105 if (webClient.getOptions().isThrowExceptionOnScriptError()) {
1106 throw new RuntimeException(e);
1107 }
1108 LOG.info("Caught script timeout error", e);
1109 }
1110
1111
1112
1113
1114
1115 @Override
1116 public void holdPosponedActions() {
1117 holdPostponedActions_ = true;
1118 }
1119
1120
1121
1122
1123
1124 @Override
1125 public void processPostponedActions() {
1126 doProcessPostponedActions();
1127 }
1128
1129
1130
1131
1132 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
1133 in.defaultReadObject();
1134 initTransientFields();
1135 }
1136
1137 private void initTransientFields() {
1138 javaScriptRunning_ = new ThreadLocal<>();
1139 postponedActions_ = new ThreadLocal<>();
1140 holdPostponedActions_ = false;
1141 shutdownPending_ = false;
1142 }
1143
1144
1145
1146
1147
1148
1149 public Class<? extends HtmlUnitScriptable> getJavaScriptClass(final Class<?> c) {
1150 return jsConfig_.getDomJavaScriptMappingFor(c);
1151 }
1152
1153
1154
1155
1156
1157 @Override
1158 public JavaScriptConfiguration getJavaScriptConfiguration() {
1159 return jsConfig_;
1160 }
1161
1162
1163
1164
1165
1166 @Override
1167 public long getJavaScriptTimeout() {
1168 return getContextFactory().getTimeout();
1169 }
1170
1171
1172
1173
1174
1175 @Override
1176 public void setJavaScriptTimeout(final long timeout) {
1177 getContextFactory().setTimeout(timeout);
1178 }
1179
1180
1181
1182
1183
1184
1185
1186 public static double toNumber(final Object value) {
1187 return ScriptRuntime.toNumber(value);
1188 }
1189
1190
1191
1192
1193
1194
1195
1196 public static String toString(final Object value) {
1197 return ScriptRuntime.toString(value);
1198 }
1199
1200
1201
1202
1203
1204
1205
1206 public static boolean toBoolean(final Object value) {
1207 return ScriptRuntime.toBoolean(value);
1208 }
1209
1210
1211
1212
1213
1214
1215
1216 public static RuntimeException throwAsScriptRuntimeEx(final Throwable e) {
1217 throw Context.throwAsScriptRuntimeEx(e);
1218 }
1219
1220
1221
1222
1223
1224
1225
1226 public static RuntimeException reportRuntimeError(final String message) {
1227 throw Context.reportRuntimeError(message);
1228 }
1229
1230
1231
1232
1233
1234
1235
1236 public static EcmaError syntaxError(final String message) {
1237 return ScriptRuntime.syntaxError(message);
1238 }
1239
1240
1241
1242
1243
1244
1245
1246 public static EcmaError typeError(final String message) {
1247 return ScriptRuntime.typeError(message);
1248 }
1249
1250
1251
1252
1253
1254
1255 public static EcmaError typeErrorIllegalConstructor() {
1256 throw JavaScriptEngine.typeError("Illegal constructor.");
1257 }
1258
1259
1260
1261
1262
1263
1264
1265 public static EcmaError rangeError(final String message) {
1266 return ScriptRuntime.rangeError(message);
1267 }
1268
1269
1270
1271
1272
1273
1274 public static EcmaError constructError(final String error, final String message) {
1275 return ScriptRuntime.constructError(error, message);
1276 }
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288 public static RhinoException asJavaScriptException(final HtmlUnitScriptable scriptable, final String message, final int type) {
1289 final DOMException domException = new DOMException(message, type);
1290 domException.setParentScope(scriptable.getParentScope());
1291
1292 final WindowOrWorkerGlobalScope wow = HtmlUnitScriptable.getWindowOrWorkerGlobalScope(scriptable);
1293 if (wow instanceof Window window) {
1294 domException.setPrototype(window.getPrototype(DOMException.class));
1295 }
1296 else if (wow instanceof WorkerGlobalScope w) {
1297 domException.setPrototype(w.getPrototype(DOMException.class));
1298 }
1299
1300 final EcmaError helper = ScriptRuntime.syntaxError("helper");
1301 String fileName = helper.sourceName();
1302 if (fileName != null) {
1303 fileName = fileName.replaceFirst("script in (.*) from .*", "$1");
1304 }
1305 domException.setLocation(fileName, helper.lineNumber());
1306
1307 return new JavaScriptException(domException, fileName, helper.lineNumber());
1308 }
1309
1310
1311
1312
1313
1314
1315
1316
1317 public static void setFunctionProtoAndParent(final BaseFunction fn, final Context cx, final VarScope scope) {
1318 ScriptRuntime.setFunctionProtoAndParent(fn, cx, scope);
1319 }
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329 public static Scriptable newObject(final VarScope scope, final String constructorName, final Object[] args) {
1330 return ScriptRuntime.newObject(Context.getCurrentContext(), scope, constructorName, args);
1331 }
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341 public static Scriptable newObject(final VarScope scope) {
1342 final NativeObject result = new NativeObject();
1343 ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Object);
1344 return result;
1345 }
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355 public static Scriptable newArray(final VarScope scope, final int length) {
1356 final NativeArray result = new NativeArray(length);
1357 ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1358 return result;
1359 }
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370 public static Scriptable newArrayIteratorTypeKeys(final VarScope scope, final Scriptable arrayLike) {
1371 return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.KEYS);
1372 }
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383 public static Scriptable newArrayIteratorTypeValues(final VarScope scope, final Scriptable arrayLike) {
1384 return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.VALUES);
1385 }
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396 public static Scriptable newArrayIteratorTypeEntries(final VarScope scope, final Scriptable arrayLike) {
1397 return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.ENTRIES);
1398 }
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408 public static Scriptable newArray(final VarScope scope, final Object[] elements) {
1409 if (elements.getClass().getComponentType() != ScriptRuntime.ObjectClass) {
1410 throw new IllegalArgumentException();
1411 }
1412
1413 final NativeArray result = new NativeArray(elements);
1414 ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1415 return result;
1416 }
1417
1418
1419
1420
1421
1422
1423
1424
1425 public static NativeUint8Array newUint8Array(final VarScope scope, final byte[] elements) {
1426 final NativeArrayBuffer arrayBuffer = new NativeArrayBuffer(elements.length);
1427 ScriptRuntime.setBuiltinProtoAndParent(arrayBuffer, scope, TopLevel.Builtins.ArrayBuffer);
1428 System.arraycopy(elements, 0, arrayBuffer.getBuffer(), 0, elements.length);
1429
1430 final NativeUint8Array uint8Array = new NativeUint8Array(arrayBuffer, 0, elements.length);
1431 ScriptRuntime.setBuiltinProtoAndParent(uint8Array, scope, TopLevel.Builtins.Uint8Array);
1432
1433 return uint8Array;
1434 }
1435
1436
1437
1438
1439
1440 public static int toInt32(final Object o) {
1441 return ScriptRuntime.toInt32(o);
1442 }
1443
1444
1445
1446
1447
1448 public static double toInteger(final Object o) {
1449 return ScriptRuntime.toInteger(o);
1450 }
1451
1452
1453
1454
1455
1456
1457 public static double toInteger(final Object[] args, final int index) {
1458 return ScriptRuntime.toInteger(args, index);
1459 }
1460
1461
1462
1463
1464
1465 public static boolean isUndefined(final Object obj) {
1466 return org.htmlunit.corejs.javascript.Undefined.isUndefined(obj);
1467 }
1468
1469
1470
1471
1472
1473 public static boolean isNaN(final Object obj) {
1474 return ScriptRuntime.isNaN(obj);
1475 }
1476
1477
1478
1479
1480
1481 public static boolean isArray(final Object obj) {
1482 return (obj instanceof Scriptable s)
1483 && "Array".equals(s.getClassName());
1484 }
1485
1486
1487
1488
1489
1490 public static boolean isArrayLike(final Scriptable obj) {
1491 return ScriptRuntime.isArrayLike(obj);
1492 }
1493
1494
1495
1496
1497
1498
1499 public static long lengthOfArrayLike(final Context cx, final Scriptable obj) {
1500 return AbstractEcmaObjectOperations.lengthOfArrayLike(cx, obj);
1501 }
1502
1503
1504
1505
1506
1507
1508
1509
1510 public static void iterateArrayLike(final Context cx, final Scriptable arrayLike, final Consumer<Object> consumer) {
1511 final Context context = cx == null ? Context.getCurrentContext() : cx;
1512
1513 final long len = lengthOfArrayLike(context, arrayLike);
1514 for (int i = 0; i < len; i++) {
1515 final Object item = arrayLike.get(i, arrayLike);
1516 consumer.accept(item);
1517 }
1518 }
1519
1520
1521
1522
1523 public static TopLevel getTopCallScope() {
1524 return ScriptRuntime.getTopCallScope(Context.getCurrentContext());
1525 }
1526
1527
1528
1529
1530
1531
1532
1533 public static String uncompressJavaScript(final String scriptSource, final String scriptName) {
1534 final ContextFactory factory = new ContextFactory();
1535 final ContextAction<Object> action = cx -> {
1536 cx.setInterpretedMode(true);
1537 final Script script = cx.compileString(scriptSource, scriptName, 0, null);
1538 return cx.decompileScript(script, 4);
1539 };
1540
1541 return (String) factory.call(action);
1542 }
1543
1544
1545
1546
1547
1548
1549
1550
1551 public static String evaluateProxyAutoConfig(final BrowserVersion browserVersion, final String content, final URL url) {
1552 try (Context cx = Context.enter()) {
1553 final ProxyAutoConfigJavaScriptConfiguration jsConfig =
1554 ProxyAutoConfigJavaScriptConfiguration.getInstance(browserVersion);
1555
1556 final ScriptableObject globalThis = new NativeObject();
1557 final TopLevel scope = cx.initSafeStandardObjects(new TopLevel(globalThis));
1558
1559 for (final ClassConfiguration config : jsConfig.getAll()) {
1560 configureFunctions(config, scope, globalThis);
1561 }
1562
1563 cx.evaluateString(scope, "var ProxyConfig = function() {}; ProxyConfig.bindings = {}; ProxyConfig", "<init>", 1, null);
1564 cx.evaluateString(scope, content, "<Proxy Auto-Config>", 1, null);
1565
1566 final Object[] functionArgs = {url.toExternalForm(), url.getHost()};
1567 final Function f = (Function) scope.get("FindProxyForURL", scope);
1568 final Object result = f.call(cx, scope, globalThis, functionArgs);
1569 return toString(result);
1570 }
1571 }
1572 }