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