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
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
1275
1276 public static EcmaError constructError(final String error, final String message) {
1277 return ScriptRuntime.constructError(error, message);
1278 }
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290 public static RhinoException asJavaScriptException(final HtmlUnitScriptable scriptable, final String message, final int type) {
1291 final DOMException domException = new DOMException(message, type);
1292 domException.setParentScope(scriptable.getParentScope());
1293
1294 final WindowOrWorkerGlobalScope wow = HtmlUnitScriptable.getWindowOrWorkerGlobalScope(scriptable);
1295 if (wow instanceof Window window) {
1296 domException.setPrototype(window.getPrototype(DOMException.class));
1297 }
1298 else if (wow instanceof WorkerGlobalScope w) {
1299 domException.setPrototype(w.getPrototype(DOMException.class));
1300 }
1301
1302 final EcmaError helper = ScriptRuntime.syntaxError("helper");
1303 String fileName = helper.sourceName();
1304 if (fileName != null) {
1305 fileName = fileName.replaceFirst("script in (.*) from .*", "$1");
1306 }
1307 domException.setLocation(fileName, helper.lineNumber());
1308
1309 return new JavaScriptException(domException, fileName, helper.lineNumber());
1310 }
1311
1312
1313
1314
1315
1316
1317
1318
1319 public static void setFunctionProtoAndParent(final BaseFunction fn, final Context cx, final VarScope scope) {
1320 ScriptRuntime.setFunctionProtoAndParent(fn, cx, scope);
1321 }
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331 public static Scriptable newObject(final VarScope scope, final String constructorName, final Object[] args) {
1332 return ScriptRuntime.newObject(Context.getCurrentContext(), scope, constructorName, args);
1333 }
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344 public static Scriptable newObject(final VarScope scope) {
1345 final NativeObject result = new NativeObject();
1346 ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Object);
1347 return result;
1348 }
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358 public static Scriptable newArray(final VarScope scope, final int length) {
1359 final NativeArray result = new NativeArray(length);
1360 ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1361 return result;
1362 }
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373 public static Scriptable newArrayIteratorTypeKeys(final VarScope scope, final Scriptable arrayLike) {
1374 return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.KEYS);
1375 }
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386 public static Scriptable newArrayIteratorTypeValues(final VarScope scope, final Scriptable arrayLike) {
1387 return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.VALUES);
1388 }
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399 public static Scriptable newArrayIteratorTypeEntries(final VarScope scope, final Scriptable arrayLike) {
1400 return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.ENTRIES);
1401 }
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411 public static Scriptable newArray(final VarScope scope, final Object[] elements) {
1412 if (elements.getClass().getComponentType() != ScriptRuntime.ObjectClass) {
1413 throw new IllegalArgumentException();
1414 }
1415
1416 final NativeArray result = new NativeArray(elements);
1417 ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1418 return result;
1419 }
1420
1421
1422
1423
1424
1425
1426
1427
1428 public static NativeUint8Array newUint8Array(final VarScope scope, final byte[] elements) {
1429 final NativeArrayBuffer arrayBuffer = new NativeArrayBuffer(elements.length);
1430 ScriptRuntime.setBuiltinProtoAndParent(arrayBuffer, scope, TopLevel.Builtins.ArrayBuffer);
1431 System.arraycopy(elements, 0, arrayBuffer.getBuffer(), 0, elements.length);
1432
1433 final NativeUint8Array uint8Array = new NativeUint8Array(arrayBuffer, 0, elements.length);
1434 ScriptRuntime.setBuiltinProtoAndParent(uint8Array, scope, TopLevel.Builtins.Uint8Array);
1435
1436 return uint8Array;
1437 }
1438
1439
1440
1441
1442
1443
1444
1445 public static int toInt32(final Object o) {
1446 return ScriptRuntime.toInt32(o);
1447 }
1448
1449
1450
1451
1452
1453
1454
1455 public static double toInteger(final Object o) {
1456 return ScriptRuntime.toInteger(o);
1457 }
1458
1459
1460
1461
1462
1463
1464
1465
1466 public static double toInteger(final Object[] args, final int index) {
1467 return ScriptRuntime.toInteger(args, index);
1468 }
1469
1470
1471
1472
1473
1474
1475
1476 public static boolean isUndefined(final Object obj) {
1477 return org.htmlunit.corejs.javascript.Undefined.isUndefined(obj);
1478 }
1479
1480
1481
1482
1483
1484
1485
1486 public static boolean isNaN(final Object obj) {
1487 return ScriptRuntime.isNaN(obj);
1488 }
1489
1490
1491
1492
1493
1494
1495
1496 public static boolean isArray(final Object obj) {
1497 return (obj instanceof Scriptable s)
1498 && "Array".equals(s.getClassName());
1499 }
1500
1501
1502
1503
1504
1505
1506
1507 public static boolean isArrayLike(final Scriptable obj) {
1508 return ScriptRuntime.isArrayLike(obj);
1509 }
1510
1511
1512
1513
1514
1515
1516
1517
1518 public static long lengthOfArrayLike(final Context cx, final Scriptable obj) {
1519 return AbstractEcmaObjectOperations.lengthOfArrayLike(cx, obj);
1520 }
1521
1522
1523
1524
1525
1526
1527
1528
1529 public static void iterateArrayLike(final Context cx, final Scriptable arrayLike, final Consumer<Object> consumer) {
1530 final Context context = cx == null ? Context.getCurrentContext() : cx;
1531
1532 final long len = lengthOfArrayLike(context, arrayLike);
1533 for (int i = 0; i < len; i++) {
1534 final Object item = arrayLike.get(i, arrayLike);
1535 consumer.accept(item);
1536 }
1537 }
1538
1539
1540
1541
1542
1543
1544 public static TopLevel getTopCallScope() {
1545 return ScriptRuntime.getTopCallScope(Context.getCurrentContext());
1546 }
1547
1548
1549
1550
1551
1552
1553
1554 public static String uncompressJavaScript(final String scriptSource, final String scriptName) {
1555 final ContextFactory factory = new ContextFactory();
1556 final ContextAction<Object> action = cx -> {
1557 cx.setInterpretedMode(true);
1558 final Script script = cx.compileString(scriptSource, scriptName, 0, null);
1559 return cx.decompileScript(script, 4);
1560 };
1561
1562 return (String) factory.call(action);
1563 }
1564
1565
1566
1567
1568
1569
1570
1571
1572 public static String evaluateProxyAutoConfig(final BrowserVersion browserVersion, final String content, final URL url) {
1573 try (Context cx = Context.enter()) {
1574 final ProxyAutoConfigJavaScriptConfiguration jsConfig =
1575 ProxyAutoConfigJavaScriptConfiguration.getInstance(browserVersion);
1576
1577 final ScriptableObject globalThis = new NativeObject();
1578 final TopLevel scope = cx.initSafeStandardObjects(new TopLevel(globalThis));
1579
1580 for (final ClassConfiguration config : jsConfig.getAll()) {
1581 configureFunctions(config, scope, globalThis);
1582 }
1583
1584 cx.evaluateString(scope, "var ProxyConfig = function() {}; ProxyConfig.bindings = {}; ProxyConfig", "<init>", 1, null);
1585 cx.evaluateString(scope, content, "<Proxy Auto-Config>", 1, null);
1586
1587 final Object[] functionArgs = {url.toExternalForm(), url.getHost()};
1588 final Function f = (Function) scope.get("FindProxyForURL", scope);
1589 final Object result = f.call(cx, scope, globalThis, functionArgs);
1590 return toString(result);
1591 }
1592 }
1593 }