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_ARRAY_SORT_ACCEPTS_INCONSISTENT_COMPERATOR;
18 import static org.htmlunit.BrowserVersionFeatures.JS_PROPERTY_DESCRIPTOR_NAME;
19
20 import java.io.Serializable;
21 import java.util.function.Consumer;
22
23 import org.htmlunit.BrowserVersion;
24 import org.htmlunit.ScriptException;
25 import org.htmlunit.ScriptPreProcessor;
26 import org.htmlunit.WebClient;
27 import org.htmlunit.corejs.javascript.Callable;
28 import org.htmlunit.corejs.javascript.CompilerEnvirons;
29 import org.htmlunit.corejs.javascript.Context;
30 import org.htmlunit.corejs.javascript.ContextAction;
31 import org.htmlunit.corejs.javascript.ContextFactory;
32 import org.htmlunit.corejs.javascript.ErrorReporter;
33 import org.htmlunit.corejs.javascript.Evaluator;
34 import org.htmlunit.corejs.javascript.EvaluatorException;
35 import org.htmlunit.corejs.javascript.Function;
36 import org.htmlunit.corejs.javascript.Script;
37 import org.htmlunit.corejs.javascript.Scriptable;
38 import org.htmlunit.corejs.javascript.VarScope;
39 import org.htmlunit.corejs.javascript.debug.Debugger;
40 import org.htmlunit.html.HtmlElement;
41 import org.htmlunit.html.HtmlPage;
42
43
44
45
46
47
48
49
50
51
52 public class HtmlUnitContextFactory extends ContextFactory {
53
54 private static final int INSTRUCTION_COUNT_THRESHOLD = 10_000;
55
56 private final WebClient webClient_;
57 private final BrowserVersion browserVersion_;
58 private long timeout_;
59 private Debugger debugger_;
60 private boolean deminifyFunctionCode_;
61
62
63
64
65
66
67 public HtmlUnitContextFactory(final WebClient webClient) {
68 super();
69 webClient_ = webClient;
70 browserVersion_ = webClient.getBrowserVersion();
71 }
72
73
74
75
76
77
78
79 public void setTimeout(final long timeout) {
80 timeout_ = timeout;
81 }
82
83
84
85
86
87
88
89 public long getTimeout() {
90 return timeout_;
91 }
92
93
94
95
96
97
98
99
100 public void setDebugger(final Debugger debugger) {
101 debugger_ = debugger;
102 }
103
104
105
106
107
108
109
110 public Debugger getDebugger() {
111 return debugger_;
112 }
113
114
115
116
117
118
119 public void setDeminifyFunctionCode(final boolean deminify) {
120 deminifyFunctionCode_ = deminify;
121 }
122
123
124
125
126
127
128 public boolean isDeminifyFunctionCode() {
129 return deminifyFunctionCode_;
130 }
131
132
133
134
135 private class TimeoutContext extends Context {
136 private long startTime_;
137
138 protected TimeoutContext(final ContextFactory factory) {
139 super(factory);
140 }
141
142 public void startClock() {
143 startTime_ = System.currentTimeMillis();
144 }
145
146 public void terminateScriptIfNecessary() {
147 if (timeout_ > 0) {
148 final long currentTime = System.currentTimeMillis();
149 if (currentTime - startTime_ > timeout_) {
150
151
152 throw new TimeoutError(timeout_, currentTime - startTime_);
153 }
154 }
155 }
156
157 @Override
158 protected Script compileString(String source, final Evaluator compiler,
159 final ErrorReporter compilationErrorReporter, final String sourceName,
160 final int lineno, final Object securityDomain,
161 final Consumer<CompilerEnvirons> compilerEnvironsProcessor) {
162
163
164
165 final boolean isWindowEval = compiler != null;
166
167
168 if (!isWindowEval) {
169
170
171
172
173
174
175
176
177 final int length = source.length();
178 int start = 0;
179 while ((start < length) && (source.charAt(start) <= ' ')) {
180 start++;
181 }
182 if (start + 3 < length
183 && source.charAt(start++) == '<'
184 && source.charAt(start++) == '!'
185 && source.charAt(start++) == '-'
186 && source.charAt(start++) == '-') {
187 source = source.replaceFirst("<!--", "// <!--");
188 }
189 }
190
191
192 final HtmlPage page = (HtmlPage) Context.getCurrentContext()
193 .getThreadLocal(JavaScriptEngine.KEY_STARTING_PAGE);
194 source = preProcess(page, source, sourceName, lineno, null);
195
196 return super.compileString(source, compiler, compilationErrorReporter,
197 sourceName, lineno, securityDomain, compilerEnvironsProcessor);
198 }
199
200 @Override
201 protected Function compileFunction(final VarScope scope, String source,
202 final Evaluator compiler, final ErrorReporter compilationErrorReporter,
203 final String sourceName, final int lineno, final Object securityDomain) {
204
205 if (deminifyFunctionCode_) {
206 final Function f = super.compileFunction(scope, source, compiler,
207 compilationErrorReporter, sourceName, lineno, securityDomain);
208 source = decompileFunction(f, 4).trim().replace("\n ", "\n");
209 }
210 return super.compileFunction(scope, source, compiler,
211 compilationErrorReporter, sourceName, lineno, securityDomain);
212 }
213 }
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228 protected String preProcess(
229 final HtmlPage htmlPage, final String sourceCode, final String sourceName, final int lineNumber,
230 final HtmlElement htmlElement) {
231
232 String newSourceCode = sourceCode;
233 final ScriptPreProcessor preProcessor = webClient_.getScriptPreProcessor();
234 if (preProcessor != null) {
235 newSourceCode = preProcessor.preProcess(htmlPage, sourceCode, sourceName, lineNumber, htmlElement);
236 if (newSourceCode == null) {
237 newSourceCode = "";
238 }
239 }
240 return newSourceCode;
241 }
242
243
244
245
246 @Override
247 protected Context makeContext() {
248 final TimeoutContext cx = new TimeoutContext(this);
249 cx.setLanguageVersion(Context.VERSION_ES6);
250 cx.setLocale(browserVersion_.getBrowserLocale());
251 cx.setTimeZone(browserVersion_.getSystemTimezone());
252
253
254 cx.setClassShutter(fullClassName -> false);
255
256
257 cx.setInterpretedMode(true);
258
259
260 cx.setInstructionObserverThreshold(INSTRUCTION_COUNT_THRESHOLD);
261
262 cx.setErrorReporter(new HtmlUnitErrorReporter(webClient_.getJavaScriptErrorListener()));
263
264 cx.getWrapFactory().setJavaPrimitiveWrap(false);
265
266 if (debugger_ != null) {
267 cx.setDebugger(debugger_, null);
268 }
269
270 cx.setMaximumInterpreterStackDepth(5_000);
271
272 return cx;
273 }
274
275
276
277
278
279
280
281
282
283 @Override
284 protected void observeInstructionCount(final Context cx, final int instructionCount) {
285 final TimeoutContext tcx = (TimeoutContext) cx;
286 tcx.terminateScriptIfNecessary();
287 }
288
289
290
291
292 @Override
293 protected Object doTopCall(final Callable callable,
294 final Context cx, final VarScope scope,
295 final Scriptable thisObj, final Object[] args) {
296
297 final TimeoutContext tcx = (TimeoutContext) cx;
298 tcx.startClock();
299 return super.doTopCall(callable, cx, scope, thisObj, args);
300 }
301
302
303
304
305 @Override
306 protected Object doTopCall(final Script script,
307 final Context cx, final VarScope scope,
308 final Scriptable thisObj) {
309
310 final TimeoutContext tcx = (TimeoutContext) cx;
311 tcx.startClock();
312 return super.doTopCall(script, cx, scope, thisObj);
313 }
314
315
316
317
318
319
320
321
322
323
324 public final <T> T callSecured(final ContextAction<T> action, final HtmlPage page) {
325 try {
326 return call(action);
327 }
328 catch (final StackOverflowError e) {
329 webClient_.getJavaScriptErrorListener().scriptException(page, new ScriptException(page, e));
330 return null;
331 }
332 }
333
334
335
336
337 @Override
338 protected boolean hasFeature(final Context cx, final int featureIndex) {
339 return switch (featureIndex) {
340 case Context.FEATURE_RESERVED_KEYWORD_AS_IDENTIFIER,
341 Context.FEATURE_OLD_UNDEF_NULL_THIS,
342 Context.FEATURE_LITTLE_ENDIAN,
343 Context.FEATURE_LOCATION_INFORMATION_IN_ERROR,
344 Context.FEATURE_INTL_402,
345 Context.FEATURE_HTMLUNIT_FN_ARGUMENTS_IS_RO_VIEW -> true;
346 case Context.FEATURE_E4X -> false;
347 case Context.FEATURE_HTMLUNIT_MEMBERBOX_NAME -> browserVersion_.hasFeature(JS_PROPERTY_DESCRIPTOR_NAME);
348 case Context.FEATURE_HTMLUNIT_ARRAY_SORT_COMPERATOR_ACCEPTS_BOOL ->
349 browserVersion_.hasFeature(JS_ARRAY_SORT_ACCEPTS_INCONSISTENT_COMPERATOR);
350 default -> super.hasFeature(cx, featureIndex);
351 };
352 }
353
354 private static final class HtmlUnitErrorReporter implements ErrorReporter, Serializable {
355
356 private final JavaScriptErrorListener javaScriptErrorListener_;
357
358
359
360
361
362
363 HtmlUnitErrorReporter(final JavaScriptErrorListener javaScriptErrorListener) {
364 javaScriptErrorListener_ = javaScriptErrorListener;
365 }
366
367
368
369
370
371
372
373
374
375
376 @Override
377 public void warning(
378 final String message, final String sourceName, final int line,
379 final String lineSource, final int lineOffset) {
380 javaScriptErrorListener_.warn(message, sourceName, line, lineSource, lineOffset);
381 }
382
383
384
385
386
387
388
389
390
391
392 @Override
393 public void error(final String message, final String sourceName, final int line,
394 final String lineSource, final int lineOffset) {
395
396
397 throw new EvaluatorException(message, sourceName, line, lineSource, lineOffset);
398 }
399
400
401
402
403
404
405
406
407
408
409
410 @Override
411 public EvaluatorException runtimeError(
412 final String message, final String sourceName, final int line,
413 final String lineSource, final int lineOffset) {
414
415
416 return new EvaluatorException(message, sourceName, line, lineSource, lineOffset);
417 }
418 }
419 }