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