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