1 /*
2 * Copyright (c) 2002-2025 Gargoyle Software Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 * https://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
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.debug.Debugger;
39 import org.htmlunit.html.HtmlElement;
40 import org.htmlunit.html.HtmlPage;
41
42 /**
43 * ContextFactory that supports termination of scripts if they exceed a timeout. Based on example from
44 * <a href="http://www.mozilla.org/rhino/apidocs/org/mozilla/javascript/ContextFactory.html">ContextFactory</a>.
45 *
46 * @author Andre Soereng
47 * @author Ahmed Ashour
48 * @author Marc Guillemot
49 * @author Ronald Brill
50 */
51 public class HtmlUnitContextFactory extends ContextFactory {
52
53 private static final int INSTRUCTION_COUNT_THRESHOLD = 10_000;
54
55 private final WebClient webClient_;
56 private final BrowserVersion browserVersion_;
57 private long timeout_;
58 private Debugger debugger_;
59 private boolean deminifyFunctionCode_;
60
61 /**
62 * Creates a new instance of HtmlUnitContextFactory.
63 *
64 * @param webClient the web client using this factory
65 */
66 public HtmlUnitContextFactory(final WebClient webClient) {
67 super();
68 webClient_ = webClient;
69 browserVersion_ = webClient.getBrowserVersion();
70 }
71
72 /**
73 * Sets the number of milliseconds a script is allowed to execute before
74 * being terminated. A value of 0 or less means no timeout.
75 *
76 * @param timeout the timeout value
77 */
78 public void setTimeout(final long timeout) {
79 timeout_ = timeout;
80 }
81
82 /**
83 * Returns the number of milliseconds a script is allowed to execute before
84 * being terminated. A value of 0 or less means no timeout.
85 *
86 * @return the timeout value (default value is <code>0</code>)
87 */
88 public long getTimeout() {
89 return timeout_;
90 }
91
92 /**
93 * Sets the JavaScript debugger to use to receive JavaScript execution debugging information.
94 * The HtmlUnit default implementation ({@link DebuggerImpl}, {@link DebugFrameImpl}) may be
95 * used, or a custom debugger may be used instead. By default, no debugger is used.
96 *
97 * @param debugger the JavaScript debugger to use (maybe {@code null})
98 */
99 public void setDebugger(final Debugger debugger) {
100 debugger_ = debugger;
101 }
102
103 /**
104 * Returns the JavaScript debugger to use to receive JavaScript execution debugging information.
105 * By default, no debugger is used, and this method returns {@code null}.
106 *
107 * @return the JavaScript debugger to use to receive JavaScript execution debugging information
108 */
109 public Debugger getDebugger() {
110 return debugger_;
111 }
112
113 /**
114 * Configures if the code of <code>new Function("...some code...")</code> should be deminified to be more readable
115 * when using the debugger. This is a small performance cost.
116 * @param deminify the new value
117 */
118 public void setDeminifyFunctionCode(final boolean deminify) {
119 deminifyFunctionCode_ = deminify;
120 }
121
122 /**
123 * Indicates code of calls like <code>new Function("...some code...")</code> should be deminified to be more
124 * readable when using the debugger.
125 * @return the de-minify status
126 */
127 public boolean isDeminifyFunctionCode() {
128 return deminifyFunctionCode_;
129 }
130
131 /**
132 * Custom context to store execution time and handle timeouts.
133 */
134 private class TimeoutContext extends Context {
135 private long startTime_;
136
137 protected TimeoutContext(final ContextFactory factory) {
138 super(factory);
139 }
140
141 public void startClock() {
142 startTime_ = System.currentTimeMillis();
143 }
144
145 public void terminateScriptIfNecessary() {
146 if (timeout_ > 0) {
147 final long currentTime = System.currentTimeMillis();
148 if (currentTime - startTime_ > timeout_) {
149 // Terminate script by throwing an Error instance to ensure that the
150 // script will never get control back through catch or finally.
151 throw new TimeoutError(timeout_, currentTime - startTime_);
152 }
153 }
154 }
155
156 @Override
157 protected Script compileString(String source, final Evaluator compiler,
158 final ErrorReporter compilationErrorReporter, final String sourceName,
159 final int lineno, final Object securityDomain,
160 final Consumer<CompilerEnvirons> compilerEnvironsProcessor) {
161
162 // this method gets called by Context.compileString and by ScriptRuntime.evalSpecial
163 // which is used for window.eval. We have to take care in which case we are.
164 final boolean isWindowEval = compiler != null;
165
166 // Remove HTML comments around the source if needed
167 if (!isWindowEval) {
168
169 // **** Memory Optimization ****
170 // final String sourceCodeTrimmed = source.trim();
171 // if (sourceCodeTrimmed.startsWith("<!--")) {
172 // **** Memory Optimization ****
173 // do not trim because this will create a copy of the
174 // whole string (usually large for libs like jQuery
175 // if there is whitespace to trim (e.g. cr at end)
176 final int length = source.length();
177 int start = 0;
178 while ((start < length) && (source.charAt(start) <= ' ')) {
179 start++;
180 }
181 if (start + 3 < length
182 && source.charAt(start++) == '<'
183 && source.charAt(start++) == '!'
184 && source.charAt(start++) == '-'
185 && source.charAt(start++) == '-') {
186 source = source.replaceFirst("<!--", "// <!--");
187 }
188 }
189
190 // Pre process the source code
191 final HtmlPage page = (HtmlPage) Context.getCurrentContext()
192 .getThreadLocal(JavaScriptEngine.KEY_STARTING_PAGE);
193 source = preProcess(page, source, sourceName, lineno, null);
194
195 return super.compileString(source, compiler, compilationErrorReporter,
196 sourceName, lineno, securityDomain, compilerEnvironsProcessor);
197 }
198
199 @Override
200 protected Function compileFunction(final Scriptable scope, String source,
201 final Evaluator compiler, final ErrorReporter compilationErrorReporter,
202 final String sourceName, final int lineno, final Object securityDomain) {
203
204 if (deminifyFunctionCode_) {
205 final Function f = super.compileFunction(scope, source, compiler,
206 compilationErrorReporter, sourceName, lineno, securityDomain);
207 source = decompileFunction(f, 4).trim().replace("\n ", "\n");
208 }
209 return super.compileFunction(scope, source, compiler,
210 compilationErrorReporter, sourceName, lineno, securityDomain);
211 }
212 }
213
214 /**
215 * Pre process the specified source code in the context of the given page using the processor specified
216 * in the {@link WebClient}. This method delegates to the pre processor handler specified in the
217 * <code>WebClient</code>. If no pre processor handler is defined, the original source code is returned
218 * unchanged.
219 * @param htmlPage the page
220 * @param sourceCode the code to process
221 * @param sourceName a name for the chunk of code (used in error messages)
222 * @param lineNumber the line number of the source code
223 * @param htmlElement the HTML element that will act as the context
224 * @return the source code after being pre processed
225 * @see org.htmlunit.ScriptPreProcessor
226 */
227 protected String preProcess(
228 final HtmlPage htmlPage, final String sourceCode, final String sourceName, final int lineNumber,
229 final HtmlElement htmlElement) {
230
231 String newSourceCode = sourceCode;
232 final ScriptPreProcessor preProcessor = webClient_.getScriptPreProcessor();
233 if (preProcessor != null) {
234 newSourceCode = preProcessor.preProcess(htmlPage, sourceCode, sourceName, lineNumber, htmlElement);
235 if (newSourceCode == null) {
236 newSourceCode = "";
237 }
238 }
239 return newSourceCode;
240 }
241
242 /**
243 * {@inheritDoc}
244 */
245 @Override
246 protected Context makeContext() {
247 final TimeoutContext cx = new TimeoutContext(this);
248 cx.setLanguageVersion(Context.VERSION_ES6);
249 cx.setLocale(browserVersion_.getBrowserLocale());
250 cx.setTimeZone(browserVersion_.getSystemTimezone());
251
252 // make sure no java classes are usable from js
253 cx.setClassShutter(fullClassName -> false);
254
255 // Use pure interpreter mode to get observeInstructionCount() callbacks.
256 cx.setInterpretedMode(true);
257
258 // Set threshold on how often we want to receive the callbacks
259 cx.setInstructionObserverThreshold(INSTRUCTION_COUNT_THRESHOLD);
260
261 cx.setErrorReporter(new HtmlUnitErrorReporter(webClient_.getJavaScriptErrorListener()));
262 // We don't want to wrap String & Co.
263 cx.getWrapFactory().setJavaPrimitiveWrap(false);
264
265 if (debugger_ != null) {
266 cx.setDebugger(debugger_, null);
267 }
268
269 cx.setMaximumInterpreterStackDepth(5_000);
270
271 return cx;
272 }
273
274 /**
275 * Run-time calls this when instruction counting is enabled and the counter
276 * reaches limit set by setInstructionObserverThreshold(). A script can be
277 * terminated by throwing an Error instance here.
278 *
279 * @param cx the context calling us
280 * @param instructionCount amount of script instruction executed since last call to observeInstructionCount
281 */
282 @Override
283 protected void observeInstructionCount(final Context cx, final int instructionCount) {
284 final TimeoutContext tcx = (TimeoutContext) cx;
285 tcx.terminateScriptIfNecessary();
286 }
287
288 /**
289 * {@inheritDoc}
290 */
291 @Override
292 protected Object doTopCall(final Callable callable,
293 final Context cx, final Scriptable scope,
294 final Scriptable thisObj, final Object[] args) {
295
296 final TimeoutContext tcx = (TimeoutContext) cx;
297 tcx.startClock();
298 return super.doTopCall(callable, cx, scope, thisObj, args);
299 }
300
301 /**
302 * Same as {@link ContextFactory}{@link #call(ContextAction)} but with handling
303 * of some exceptions.
304 *
305 * @param <T> return type of the action
306 * @param action the contextAction
307 * @param page the page
308 * @return the result of the call
309 */
310 public final <T> T callSecured(final ContextAction<T> action, final HtmlPage page) {
311 try {
312 return call(action);
313 }
314 catch (final StackOverflowError e) {
315 webClient_.getJavaScriptErrorListener().scriptException(page, new ScriptException(page, e));
316 return null;
317 }
318 }
319
320 /**
321 * {@inheritDoc}
322 */
323 @Override
324 protected boolean hasFeature(final Context cx, final int featureIndex) {
325 switch (featureIndex) {
326 case Context.FEATURE_RESERVED_KEYWORD_AS_IDENTIFIER:
327 case Context.FEATURE_OLD_UNDEF_NULL_THIS:
328 case Context.FEATURE_LITTLE_ENDIAN:
329 case Context.FEATURE_LOCATION_INFORMATION_IN_ERROR:
330 case Context.FEATURE_INTL_402:
331 case Context.FEATURE_HTMLUNIT_FN_ARGUMENTS_IS_RO_VIEW:
332 return true;
333 case Context.FEATURE_E4X:
334 case Context.FEATURE_NON_ECMA_GET_YEAR:
335 return false;
336 case Context.FEATURE_HTMLUNIT_MEMBERBOX_NAME:
337 return browserVersion_.hasFeature(JS_PROPERTY_DESCRIPTOR_NAME);
338 case Context.FEATURE_HTMLUNIT_ARRAY_SORT_COMPERATOR_ACCEPTS_BOOL:
339 return browserVersion_.hasFeature(JS_ARRAY_SORT_ACCEPTS_INCONSISTENT_COMPERATOR);
340 default:
341 return super.hasFeature(cx, featureIndex);
342 }
343 }
344
345 private static final class HtmlUnitErrorReporter implements ErrorReporter, Serializable {
346
347 private final JavaScriptErrorListener javaScriptErrorListener_;
348
349 /**
350 * Ctor.
351 *
352 * @param javaScriptErrorListener the listener to be used
353 */
354 HtmlUnitErrorReporter(final JavaScriptErrorListener javaScriptErrorListener) {
355 javaScriptErrorListener_ = javaScriptErrorListener;
356 }
357
358 /**
359 * Logs a warning.
360 *
361 * @param message the message to be displayed
362 * @param sourceName the name of the source file
363 * @param line the line number
364 * @param lineSource the source code that failed
365 * @param lineOffset the line offset
366 */
367 @Override
368 public void warning(
369 final String message, final String sourceName, final int line,
370 final String lineSource, final int lineOffset) {
371 javaScriptErrorListener_.warn(message, sourceName, line, lineSource, lineOffset);
372 }
373
374 /**
375 * Logs an error.
376 *
377 * @param message the message to be displayed
378 * @param sourceName the name of the source file
379 * @param line the line number
380 * @param lineSource the source code that failed
381 * @param lineOffset the line offset
382 */
383 @Override
384 public void error(final String message, final String sourceName, final int line,
385 final String lineSource, final int lineOffset) {
386 // no need to log here, this is only used to create the exception
387 // gets logged if not catched later on
388 throw new EvaluatorException(message, sourceName, line, lineSource, lineOffset);
389 }
390
391 /**
392 * Logs a runtime error.
393 *
394 * @param message the message to be displayed
395 * @param sourceName the name of the source file
396 * @param line the line number
397 * @param lineSource the source code that failed
398 * @param lineOffset the line offset
399 * @return an evaluator exception
400 */
401 @Override
402 public EvaluatorException runtimeError(
403 final String message, final String sourceName, final int line,
404 final String lineSource, final int lineOffset) {
405 // no need to log here, this is only used to create the exception
406 // gets logged if not catched later on
407 return new EvaluatorException(message, sourceName, line, lineSource, lineOffset);
408 }
409 }
410 }