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