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