View Javadoc
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 (may be {@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                 return true;
328             case Context.FEATURE_E4X:
329                 return false;
330             case Context.FEATURE_OLD_UNDEF_NULL_THIS:
331                 return true;
332             case Context.FEATURE_NON_ECMA_GET_YEAR:
333                 return false;
334             case Context.FEATURE_LITTLE_ENDIAN:
335                 return true;
336             case Context.FEATURE_LOCATION_INFORMATION_IN_ERROR:
337                 return true;
338             case Context.FEATURE_INTL_402:
339                 return true;
340             case Context.FEATURE_HTMLUNIT_FN_ARGUMENTS_IS_RO_VIEW:
341                 return true;
342             case Context.FEATURE_HTMLUNIT_MEMBERBOX_NAME:
343                 return browserVersion_.hasFeature(JS_PROPERTY_DESCRIPTOR_NAME);
344             case Context.FEATURE_HTMLUNIT_ARRAY_SORT_COMPERATOR_ACCEPTS_BOOL:
345                 return browserVersion_.hasFeature(JS_ARRAY_SORT_ACCEPTS_INCONSISTENT_COMPERATOR);
346             default:
347                 return super.hasFeature(cx, featureIndex);
348         }
349     }
350 
351     private static final class HtmlUnitErrorReporter implements ErrorReporter, Serializable {
352 
353         private final JavaScriptErrorListener javaScriptErrorListener_;
354 
355         /**
356          * Ctor.
357          *
358          * @param javaScriptErrorListener the listener to be used
359          */
360         HtmlUnitErrorReporter(final JavaScriptErrorListener javaScriptErrorListener) {
361             javaScriptErrorListener_ = javaScriptErrorListener;
362         }
363 
364         /**
365          * Logs a warning.
366          *
367          * @param message the message to be displayed
368          * @param sourceName the name of the source file
369          * @param line the line number
370          * @param lineSource the source code that failed
371          * @param lineOffset the line offset
372          */
373         @Override
374         public void warning(
375                 final String message, final String sourceName, final int line,
376                 final String lineSource, final int lineOffset) {
377             javaScriptErrorListener_.warn(message, sourceName, line, lineSource, lineOffset);
378         }
379 
380         /**
381          * Logs an error.
382          *
383          * @param message the message to be displayed
384          * @param sourceName the name of the source file
385          * @param line the line number
386          * @param lineSource the source code that failed
387          * @param lineOffset the line offset
388          */
389         @Override
390         public void error(final String message, final String sourceName, final int line,
391                 final String lineSource, final int lineOffset) {
392             // no need to log here, this is only used to create the exception
393             // the exception gets logged if not catched later on
394             throw new EvaluatorException(message, sourceName, line, lineSource, lineOffset);
395         }
396 
397         /**
398          * Logs a runtime error.
399          *
400          * @param message the message to be displayed
401          * @param sourceName the name of the source file
402          * @param line the line number
403          * @param lineSource the source code that failed
404          * @param lineOffset the line offset
405          * @return an evaluator exception
406          */
407         @Override
408         public EvaluatorException runtimeError(
409                 final String message, final String sourceName, final int line,
410                 final String lineSource, final int lineOffset) {
411             // no need to log here, this is only used to create the exception
412             // the exception gets logged if not catched later on
413             return new EvaluatorException(message, sourceName, line, lineSource, lineOffset);
414         }
415     }
416 }