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 prototypes.put(config.getHostClass(), classPrototype);
316 prototypesPerJSName.put(jsClassName, classPrototype);
317 prototype = classPrototype;
318
319 if (extendedClassName == null) {
320 classPrototype.setPrototype(objectPrototype);
321 }
322 else {
323 classPrototype.setPrototype(prototypesPerJSName.get(extendedClassName));
324 }
325
326
327 if (prototype != null) {
328 final Map.Entry<String, Member> jsConstructor = config.getJsConstructor();
329 if (jsConstructor == null) {
330 final ScriptableObject constructor = config.getHostClass().getDeclaredConstructor().newInstance();
331 ((HtmlUnitScriptable) constructor).setClassName(jsClassName);
332 defineConstructor(jsScope, prototype, constructor);
333 configureConstantsStaticPropertiesAndStaticFunctions(config, constructor);
334
335 if (config.isJsObject()) {
336 jsScope.defineProperty(jsClassName, constructor, ScriptableObject.DONTENUM);
337 }
338 }
339 else {
340 final FunctionObject function = new FunctionObject(jsConstructor.getKey(), jsConstructor.getValue(), jsScope);
341 ctorPrototypesPerJSName.put(jsClassName, function);
342
343 addAsConstructorAndAlias(function, jsScope, prototype, config);
344 configureConstantsStaticPropertiesAndStaticFunctions(config, function);
345
346 if (!config.isJsObject()) {
347
348
349
350 jsScope.delete(prototype.getClassName());
351 }
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
512
513
514
515
516
517 private static void deleteProperties(final Scriptable scope, final String... propertiesToDelete) {
518 for (final String property : propertiesToDelete) {
519 scope.delete(property);
520 }
521 }
522
523
524
525
526
527
528
529 private static void removePrototypeProperties(final Scriptable scope, final String className,
530 final String... properties) {
531 final ScriptableObject prototype = (ScriptableObject) ScriptableObject.getClassPrototype(scope, className);
532 for (final String property : properties) {
533 prototype.delete(property);
534 }
535 }
536
537
538
539
540
541
542
543
544
545
546 public static HtmlUnitScriptable configureClass(final ClassConfiguration config, final Scriptable window)
547 throws Exception {
548
549 final HtmlUnitScriptable prototype = config.getHostClass().getDeclaredConstructor().newInstance();
550 prototype.setParentScope(window);
551 prototype.setClassName(config.getClassName());
552
553 configureConstantsPropertiesAndFunctions(config, prototype);
554
555 return prototype;
556 }
557
558
559
560
561
562
563 private static void configureConstantsStaticPropertiesAndStaticFunctions(final ClassConfiguration config,
564 final ScriptableObject scriptable) {
565 configureConstants(config, scriptable);
566 configureStaticProperties(config, scriptable);
567 configureStaticFunctions(config, scriptable);
568 }
569
570
571
572
573
574
575 private static void configureConstantsPropertiesAndFunctions(final ClassConfiguration config,
576 final ScriptableObject scriptable) {
577 configureConstants(config, scriptable);
578 configureProperties(config, scriptable);
579 configureFunctions(config, scriptable);
580 configureSymbolConstants(config, scriptable);
581 configureSymbols(config, scriptable);
582 }
583
584 private static void configureFunctions(final ClassConfiguration config, final ScriptableObject scriptable) {
585
586 final Map<String, Method> functionMap = config.getFunctionMap();
587 if (functionMap != null) {
588 for (final Entry<String, Method> functionInfo : functionMap.entrySet()) {
589 final String functionName = functionInfo.getKey();
590 final Method method = functionInfo.getValue();
591 final FunctionObject functionObject = new FunctionObject(functionName, method, scriptable);
592 scriptable.defineProperty(functionName, functionObject, ScriptableObject.EMPTY);
593 }
594 }
595 }
596
597 private static void configureConstants(final ClassConfiguration config, final ScriptableObject scriptable) {
598 final List<ConstantInfo> constants = config.getConstants();
599 if (constants != null) {
600 for (final ConstantInfo constantInfo : constants) {
601 scriptable.defineProperty(constantInfo.getName(), constantInfo.getValue(), constantInfo.getFlag());
602 }
603 }
604 }
605
606 private static void configureProperties(final ClassConfiguration config, final ScriptableObject scriptable) {
607 final Map<String, PropertyInfo> propertyMap = config.getPropertyMap();
608 if (propertyMap != null) {
609 for (final Entry<String, PropertyInfo> propertyEntry : propertyMap.entrySet()) {
610 final PropertyInfo info = propertyEntry.getValue();
611 final Method readMethod = info.getReadMethod();
612 final Method writeMethod = info.getWriteMethod();
613 scriptable.defineProperty(propertyEntry.getKey(), null, readMethod, writeMethod, ScriptableObject.EMPTY);
614 }
615 }
616 }
617
618 private static void configureStaticProperties(final ClassConfiguration config, final ScriptableObject scriptable) {
619 final Map<String, PropertyInfo> staticPropertyMap = config.getStaticPropertyMap();
620 if (staticPropertyMap != null) {
621 for (final Entry<String, ClassConfiguration.PropertyInfo> propertyEntry : staticPropertyMap.entrySet()) {
622 final String propertyName = propertyEntry.getKey();
623 final Method readMethod = propertyEntry.getValue().getReadMethod();
624 final Method writeMethod = propertyEntry.getValue().getWriteMethod();
625 final int flag = ScriptableObject.EMPTY;
626
627 scriptable.defineProperty(propertyName, null, readMethod, writeMethod, flag);
628 }
629 }
630 }
631
632 private static void configureStaticFunctions(final ClassConfiguration config,
633 final ScriptableObject scriptable) {
634 final Map<String, Method> staticFunctionMap = config.getStaticFunctionMap();
635 if (staticFunctionMap != null) {
636 for (final Entry<String, Method> staticFunctionInfo : staticFunctionMap.entrySet()) {
637 final String functionName = staticFunctionInfo.getKey();
638 final Method method = staticFunctionInfo.getValue();
639 final FunctionObject staticFunctionObject = new FunctionObject(functionName, method,
640 scriptable);
641 scriptable.defineProperty(functionName, staticFunctionObject, ScriptableObject.EMPTY);
642 }
643 }
644 }
645
646 private static void configureSymbolConstants(final ClassConfiguration config, final ScriptableObject scriptable) {
647 final Map<Symbol, String> symbolConstantMap = config.getSymbolConstantMap();
648 if (symbolConstantMap != null) {
649 for (final Entry<Symbol, String> symbolInfo : symbolConstantMap.entrySet()) {
650 scriptable.defineProperty(symbolInfo.getKey(), symbolInfo.getValue(), ScriptableObject.DONTENUM | ScriptableObject.READONLY);
651 }
652 }
653 }
654
655 private static void configureSymbols(final ClassConfiguration config,
656 final ScriptableObject scriptable) {
657 final Map<Symbol, Method> symbolMap = config.getSymbolMap();
658 if (symbolMap != null) {
659 for (final Entry<Symbol, Method> symbolInfo : symbolMap.entrySet()) {
660 final Symbol symbol = symbolInfo.getKey();
661 final Method method = symbolInfo.getValue();
662 final String methodName = method.getName();
663
664 final Callable symbolFunction = scriptable.has(methodName, scriptable)
665 ? (Callable) scriptable.get(methodName, scriptable)
666 : new FunctionObject(methodName, method, scriptable);
667 scriptable.defineProperty(symbol, symbolFunction, ScriptableObject.DONTENUM);
668 }
669 }
670 }
671
672
673
674
675
676 @Override
677 public synchronized void registerWindowAndMaybeStartEventLoop(final WebWindow webWindow) {
678 if (shutdownPending_) {
679 return;
680 }
681
682 final WebClient webClient = getWebClient();
683 if (webClient != null) {
684 if (javaScriptExecutor_ == null) {
685 javaScriptExecutor_ = BackgroundJavaScriptFactory.theFactory().createJavaScriptExecutor(webClient);
686 }
687 javaScriptExecutor_.addWindow(webWindow);
688 }
689 }
690
691
692
693
694 @Override
695 public void prepareShutdown() {
696 shutdownPending_ = true;
697 }
698
699
700
701
702 @Override
703 public void shutdown() {
704 webClient_ = null;
705 contextFactory_ = null;
706 jsConfig_ = null;
707
708 if (javaScriptExecutor_ != null) {
709 javaScriptExecutor_.shutdown();
710 javaScriptExecutor_ = null;
711 }
712 if (postponedActions_ != null) {
713 postponedActions_.remove();
714 }
715 if (javaScriptRunning_ != null) {
716 javaScriptRunning_.remove();
717 }
718 holdPostponedActions_ = false;
719 }
720
721
722
723
724 @Override
725 public Script compile(final HtmlPage owningPage, final Scriptable scope, final String sourceCode,
726 final String sourceName, final int startLine) {
727 WebAssert.notNull("sourceCode", sourceCode);
728
729 if (LOG.isTraceEnabled()) {
730 final String newline = System.lineSeparator();
731 LOG.trace("Javascript compile " + sourceName + newline + sourceCode + newline);
732 }
733
734 final HtmlUnitCompileContextAction action = new HtmlUnitCompileContextAction(owningPage, sourceCode, sourceName, startLine);
735 return (Script) getContextFactory().callSecured(action, owningPage);
736 }
737
738
739
740
741
742
743
744
745
746 public final <T> T callSecured(final ContextAction<T> action, final HtmlPage page) {
747 if (shutdownPending_ || webClient_ == null) {
748
749 return null;
750 }
751
752 return getContextFactory().callSecured(action, page);
753 }
754
755
756
757
758 @Override
759 public Object execute(final HtmlPage page,
760 final Scriptable scope,
761 final String sourceCode,
762 final String sourceName,
763 final int startLine) {
764 final Script script = compile(page, scope, sourceCode, sourceName, startLine);
765 if (script == null) {
766
767 return null;
768 }
769 return execute(page, scope, script);
770 }
771
772
773
774
775 @Override
776 public Object execute(final HtmlPage page, final Scriptable scope, final Script script) {
777 if (shutdownPending_ || webClient_ == null) {
778
779 return null;
780 }
781
782 final HtmlUnitContextAction action = new HtmlUnitContextAction(page) {
783 @Override
784 public Object doRun(final Context cx) {
785 return script.exec(cx, scope);
786 }
787
788 @Override
789 protected String getSourceCode(final Context cx) {
790 return null;
791 }
792 };
793
794 return getContextFactory().callSecured(action, page);
795 }
796
797
798
799
800
801
802
803
804
805
806 public Object callFunction(
807 final HtmlPage page,
808 final Function javaScriptFunction,
809 final Scriptable thisObject,
810 final Object[] args,
811 final DomNode node) {
812
813 final Scriptable scope = getScope(page, node);
814
815 return callFunction(page, javaScriptFunction, scope, thisObject, args);
816 }
817
818
819
820
821
822
823
824
825
826
827 public Object callFunction(final HtmlPage page, final Function function,
828 final Scriptable scope, final Scriptable thisObject, final Object[] args) {
829 if (shutdownPending_ || webClient_ == null) {
830
831 return null;
832 }
833
834 final HtmlUnitContextAction action = new HtmlUnitContextAction(page) {
835 @Override
836 public Object doRun(final Context cx) {
837 if (ScriptRuntime.hasTopCall(cx)) {
838 return function.call(cx, scope, thisObject, args);
839 }
840 return ScriptRuntime.doTopCall(function, cx, scope, thisObject, args, cx.isStrictMode());
841 }
842 @Override
843 protected String getSourceCode(final Context cx) {
844 return cx.decompileFunction(function, 2);
845 }
846 };
847 return getContextFactory().callSecured(action, page);
848 }
849
850 private static Scriptable getScope(final HtmlPage page, final DomNode node) {
851 if (node != null) {
852 return node.getScriptableObject();
853 }
854 return page.getEnclosingWindow().getScriptableObject();
855 }
856
857
858
859
860
861
862 @Override
863 public boolean isScriptRunning() {
864 return Boolean.TRUE.equals(javaScriptRunning_.get());
865 }
866
867
868
869
870
871 private final class HtmlUnitCompileContextAction implements ContextAction<Object> {
872 private final HtmlPage page_;
873 private final String sourceCode_;
874 private final String sourceName_;
875 private final int startLine_;
876
877 HtmlUnitCompileContextAction(final HtmlPage page, final String sourceCode, final String sourceName, final int startLine) {
878 page_ = page;
879 sourceCode_ = sourceCode;
880 sourceName_ = sourceName;
881 startLine_ = startLine;
882 }
883
884 @Override
885 public Object run(final Context cx) {
886 try {
887 final Object response;
888 cx.putThreadLocal(KEY_STARTING_PAGE, page_);
889 synchronized (page_) {
890 if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
891 return null;
892 }
893 response = cx.compileString(sourceCode_, sourceName_, startLine_, null);
894
895 }
896
897 return response;
898 }
899 catch (final Exception e) {
900 handleJavaScriptException(new ScriptException(page_, e, sourceCode_), true);
901 return null;
902 }
903 catch (final TimeoutError e) {
904 handleJavaScriptTimeoutError(page_, e);
905 return null;
906 }
907 }
908 }
909
910
911
912
913
914
915 private abstract class HtmlUnitContextAction implements ContextAction<Object> {
916 private final HtmlPage page_;
917
918 HtmlUnitContextAction(final HtmlPage page) {
919 page_ = page;
920 }
921
922 @Override
923 public final Object run(final Context cx) {
924 final Boolean javaScriptAlreadyRunning = javaScriptRunning_.get();
925 javaScriptRunning_.set(Boolean.TRUE);
926
927 try {
928 final Object response;
929 synchronized (page_) {
930 if (page_ != page_.getEnclosingWindow().getEnclosedPage()) {
931 return null;
932 }
933 response = doRun(cx);
934 }
935
936 cx.processMicrotasks();
937
938
939
940 if (!holdPostponedActions_) {
941 doProcessPostponedActions();
942 }
943
944 return response;
945 }
946 catch (final Exception e) {
947 handleJavaScriptException(new ScriptException(page_, e, getSourceCode(cx)), true);
948 return null;
949 }
950 catch (final TimeoutError e) {
951 handleJavaScriptTimeoutError(page_, e);
952 return null;
953 }
954 finally {
955 javaScriptRunning_.set(javaScriptAlreadyRunning);
956 }
957 }
958
959 protected abstract Object doRun(Context cx);
960
961 protected abstract String getSourceCode(Context cx);
962 }
963
964 private void doProcessPostponedActions() {
965 holdPostponedActions_ = false;
966
967 final WebClient webClient = getWebClient();
968 if (webClient == null) {
969
970 postponedActions_.set(null);
971 return;
972 }
973
974 try {
975 webClient.loadDownloadedResponses();
976 }
977 catch (final RuntimeException e) {
978 throw e;
979 }
980 catch (final Exception e) {
981 throw new RuntimeException(e);
982 }
983
984 final List<PostponedAction> actions = postponedActions_.get();
985 if (actions != null && !actions.isEmpty()) {
986 postponedActions_.set(new ArrayList<>());
987 try {
988 for (final PostponedAction action : actions) {
989 if (LOG.isDebugEnabled()) {
990 LOG.debug("Processing PostponedAction " + action);
991 }
992
993
994 if (action.isStillAlive()) {
995 action.execute();
996 }
997 }
998 }
999 catch (final RuntimeException e) {
1000 throw e;
1001 }
1002 catch (final Exception e) {
1003 throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
1004 }
1005 }
1006 }
1007
1008
1009
1010
1011
1012 @Override
1013 public void addPostponedAction(final PostponedAction action) {
1014 if (shutdownPending_) {
1015 return;
1016 }
1017
1018 List<PostponedAction> actions = postponedActions_.get();
1019 if (actions == null) {
1020 actions = new ArrayList<>();
1021 postponedActions_.set(actions);
1022 }
1023 actions.add(action);
1024 }
1025
1026
1027
1028
1029
1030
1031 protected void handleJavaScriptException(final ScriptException scriptException, final boolean triggerOnError) {
1032 final WebClient webClient = getWebClient();
1033 if (shutdownPending_ || webClient == null) {
1034
1035 return;
1036 }
1037
1038
1039 final HtmlPage page = scriptException.getPage();
1040 if (triggerOnError && page != null) {
1041 final WebWindow window = page.getEnclosingWindow();
1042 if (window != null) {
1043 final Window w = window.getScriptableObject();
1044 if (w != null) {
1045 try {
1046 w.triggerOnError(scriptException);
1047 }
1048 catch (final Exception e) {
1049 handleJavaScriptException(new ScriptException(page, e, null), false);
1050 }
1051 }
1052 }
1053 }
1054
1055 webClient.getJavaScriptErrorListener().scriptException(page, scriptException);
1056
1057 if (webClient.getOptions().isThrowExceptionOnScriptError()) {
1058 throw scriptException;
1059 }
1060 }
1061
1062
1063
1064
1065
1066
1067 protected void handleJavaScriptTimeoutError(final HtmlPage page, final TimeoutError e) {
1068 final WebClient webClient = getWebClient();
1069 if (shutdownPending_ || webClient == null) {
1070
1071 return;
1072 }
1073
1074 webClient.getJavaScriptErrorListener().timeoutError(page, e.getAllowedTime(), e.getExecutionTime());
1075 if (webClient.getOptions().isThrowExceptionOnScriptError()) {
1076 throw new RuntimeException(e);
1077 }
1078 LOG.info("Caught script timeout error", e);
1079 }
1080
1081
1082
1083
1084
1085 @Override
1086 public void holdPosponedActions() {
1087 holdPostponedActions_ = true;
1088 }
1089
1090
1091
1092
1093
1094 @Override
1095 public void processPostponedActions() {
1096 doProcessPostponedActions();
1097 }
1098
1099
1100
1101
1102 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
1103 in.defaultReadObject();
1104 initTransientFields();
1105 }
1106
1107 private void initTransientFields() {
1108 javaScriptRunning_ = new ThreadLocal<>();
1109 postponedActions_ = new ThreadLocal<>();
1110 holdPostponedActions_ = false;
1111 shutdownPending_ = false;
1112 }
1113
1114
1115
1116
1117
1118
1119 public Class<? extends HtmlUnitScriptable> getJavaScriptClass(final Class<?> c) {
1120 return jsConfig_.getDomJavaScriptMappingFor(c);
1121 }
1122
1123
1124
1125
1126
1127 @Override
1128 public JavaScriptConfiguration getJavaScriptConfiguration() {
1129 return jsConfig_;
1130 }
1131
1132
1133
1134
1135
1136 @Override
1137 public long getJavaScriptTimeout() {
1138 return getContextFactory().getTimeout();
1139 }
1140
1141
1142
1143
1144
1145 @Override
1146 public void setJavaScriptTimeout(final long timeout) {
1147 getContextFactory().setTimeout(timeout);
1148 }
1149
1150
1151
1152
1153
1154
1155
1156 public static double toNumber(final Object value) {
1157 return ScriptRuntime.toNumber(value);
1158 }
1159
1160
1161
1162
1163
1164
1165
1166 public static String toString(final Object value) {
1167 return ScriptRuntime.toString(value);
1168 }
1169
1170
1171
1172
1173
1174
1175
1176 public static boolean toBoolean(final Object value) {
1177 return ScriptRuntime.toBoolean(value);
1178 }
1179
1180
1181
1182
1183
1184
1185
1186 public static RuntimeException throwAsScriptRuntimeEx(final Throwable e) {
1187 throw Context.throwAsScriptRuntimeEx(e);
1188 }
1189
1190
1191
1192
1193
1194
1195
1196 public static RuntimeException reportRuntimeError(final String message) {
1197 throw Context.reportRuntimeError(message);
1198 }
1199
1200
1201
1202
1203
1204
1205
1206 public static EcmaError syntaxError(final String message) {
1207 return ScriptRuntime.syntaxError(message);
1208 }
1209
1210
1211
1212
1213
1214
1215
1216 public static EcmaError typeError(final String message) {
1217 return ScriptRuntime.typeError(message);
1218 }
1219
1220
1221
1222
1223
1224
1225 public static EcmaError typeErrorIllegalConstructor() {
1226 throw JavaScriptEngine.typeError("Illegal constructor.");
1227 }
1228
1229
1230
1231
1232
1233
1234
1235 public static EcmaError rangeError(final String message) {
1236 return ScriptRuntime.rangeError(message);
1237 }
1238
1239
1240
1241
1242
1243
1244 public static EcmaError constructError(final String error, final String message) {
1245 return ScriptRuntime.constructError(error, message);
1246 }
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258 public static RhinoException asJavaScriptException(final HtmlUnitScriptable scope, final String message, final int type) {
1259 final DOMException domException = new DOMException(message, type);
1260 domException.setParentScope(scope);
1261 domException.setPrototype(scope.getPrototype(DOMException.class));
1262
1263 final EcmaError helper = ScriptRuntime.syntaxError("helper");
1264 String fileName = helper.sourceName();
1265 if (fileName != null) {
1266 fileName = fileName.replaceFirst("script in (.*) from .*", "$1");
1267 }
1268 domException.setLocation(fileName, helper.lineNumber());
1269
1270 return new JavaScriptException(domException, fileName, helper.lineNumber());
1271 }
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281 public static Scriptable newArray(final Scriptable scope, final int length) {
1282 final NativeArray result = new NativeArray(length);
1283 ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1284 return result;
1285 }
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296 public static Scriptable newArrayIteratorTypeKeys(final Scriptable scope, final Scriptable arrayLike) {
1297 return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.KEYS);
1298 }
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309 public static Scriptable newArrayIteratorTypeValues(final Scriptable scope, final Scriptable arrayLike) {
1310 return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.VALUES);
1311 }
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322 public static Scriptable newArrayIteratorTypeEntries(final Scriptable scope, final Scriptable arrayLike) {
1323 return new NativeArrayIterator(scope, arrayLike, NativeArrayIterator.ARRAY_ITERATOR_TYPE.ENTRIES);
1324 }
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334 public static Scriptable newArray(final Scriptable scope, final Object[] elements) {
1335 if (elements.getClass().getComponentType() != ScriptRuntime.ObjectClass) {
1336 throw new IllegalArgumentException();
1337 }
1338
1339 final NativeArray result = new NativeArray(elements);
1340 ScriptRuntime.setBuiltinProtoAndParent(result, scope, TopLevel.Builtins.Array);
1341 return result;
1342 }
1343
1344
1345
1346
1347
1348 public static int toInt32(final Object o) {
1349 return ScriptRuntime.toInt32(o);
1350 }
1351
1352
1353
1354
1355
1356 public static double toInteger(final Object o) {
1357 return ScriptRuntime.toInteger(o);
1358 }
1359
1360
1361
1362
1363
1364
1365 public static double toInteger(final Object[] args, final int index) {
1366 return ScriptRuntime.toInteger(args, index);
1367 }
1368
1369
1370
1371
1372
1373 public static boolean isUndefined(final Object obj) {
1374 return org.htmlunit.corejs.javascript.Undefined.isUndefined(obj);
1375 }
1376
1377
1378
1379
1380
1381 public static boolean isNaN(final Object obj) {
1382 return ScriptRuntime.isNaN(obj);
1383 }
1384
1385
1386
1387
1388 public static Scriptable getTopCallScope() {
1389 return ScriptRuntime.getTopCallScope(Context.getCurrentContext());
1390 }
1391
1392
1393
1394
1395
1396
1397
1398 public static String uncompressJavaScript(final String scriptSource, final String scriptName) {
1399 final ContextFactory factory = new ContextFactory();
1400 final ContextAction<Object> action = cx -> {
1401 cx.setInterpretedMode(true);
1402 final Script script = cx.compileString(scriptSource, scriptName, 0, null);
1403 return cx.decompileScript(script, 4);
1404 };
1405
1406 return (String) factory.call(action);
1407 }
1408
1409
1410
1411
1412
1413
1414
1415
1416 public static String evaluateProxyAutoConfig(final BrowserVersion browserVersion, final String content, final URL url) {
1417 try (Context cx = Context.enter()) {
1418 final ProxyAutoConfigJavaScriptConfiguration jsConfig =
1419 ProxyAutoConfigJavaScriptConfiguration.getInstance(browserVersion);
1420
1421 final ScriptableObject scope = cx.initSafeStandardObjects();
1422
1423 for (final ClassConfiguration config : jsConfig.getAll()) {
1424 configureFunctions(config, scope);
1425 }
1426
1427 cx.evaluateString(scope, "var ProxyConfig = function() {}; ProxyConfig.bindings = {}", "<init>", 1, null);
1428 cx.evaluateString(scope, content, "<Proxy Auto-Config>", 1, null);
1429
1430 final Object[] functionArgs = {url.toExternalForm(), url.getHost()};
1431 final NativeFunction f = (NativeFunction) scope.get("FindProxyForURL", scope);
1432 final Object result = f.call(cx, scope, scope, functionArgs);
1433 return toString(result);
1434 }
1435 }
1436 }