View Javadoc
1   /*
2    * Copyright (c) 2002-2026 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.VarScope;
39  import org.htmlunit.corejs.javascript.debug.Debugger;
40  import org.htmlunit.html.HtmlElement;
41  import org.htmlunit.html.HtmlPage;
42  
43  /**
44   * ContextFactory that supports termination of scripts if they exceed a timeout. Based on example from
45   * <a href="http://www.mozilla.org/rhino/apidocs/org/mozilla/javascript/ContextFactory.html">ContextFactory</a>.
46   *
47   * @author Andre Soereng
48   * @author Ahmed Ashour
49   * @author Marc Guillemot
50   * @author Ronald Brill
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       * Creates a new instance of HtmlUnitContextFactory.
64       *
65       * @param webClient the web client using this factory
66       */
67      public HtmlUnitContextFactory(final WebClient webClient) {
68          super();
69          webClient_ = webClient;
70          browserVersion_ = webClient.getBrowserVersion();
71      }
72  
73      /**
74       * Sets the number of milliseconds a script is allowed to execute before
75       * being terminated. A value of 0 or less means no timeout.
76       *
77       * @param timeout the timeout value
78       */
79      public void setTimeout(final long timeout) {
80          timeout_ = timeout;
81      }
82  
83      /**
84       * Returns the number of milliseconds a script is allowed to execute before
85       * being terminated. A value of 0 or less means no timeout.
86       *
87       * @return the timeout value (default value is <code>0</code>)
88       */
89      public long getTimeout() {
90          return timeout_;
91      }
92  
93      /**
94       * Sets the JavaScript debugger to use to receive JavaScript execution debugging information.
95       * The HtmlUnit default implementation ({@link DebuggerImpl}, {@link DebugFrameImpl}) may be
96       * used, or a custom debugger may be used instead. By default, no debugger is used.
97       *
98       * @param debugger the JavaScript debugger to use (maybe {@code null})
99       */
100     public void setDebugger(final Debugger debugger) {
101         debugger_ = debugger;
102     }
103 
104     /**
105      * Returns the JavaScript debugger to use to receive JavaScript execution debugging information.
106      * By default, no debugger is used, and this method returns {@code null}.
107      *
108      * @return the JavaScript debugger to use to receive JavaScript execution debugging information
109      */
110     public Debugger getDebugger() {
111         return debugger_;
112     }
113 
114     /**
115      * Configures if the code of <code>new Function("...some code...")</code> should be deminified to be more readable
116      * when using the debugger. This is a small performance cost.
117      * @param deminify the new value
118      */
119     public void setDeminifyFunctionCode(final boolean deminify) {
120         deminifyFunctionCode_ = deminify;
121     }
122 
123     /**
124      * Indicates code of calls like <code>new Function("...some code...")</code> should be deminified to be more
125      * readable when using the debugger.
126      * @return the de-minify status
127      */
128     public boolean isDeminifyFunctionCode() {
129         return deminifyFunctionCode_;
130     }
131 
132     /**
133      * Custom context to store execution time and handle timeouts.
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                     // Terminate script by throwing an Error instance to ensure that the
151                     // script will never get control back through catch or finally.
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             // this method gets called by Context.compileString and by ScriptRuntime.evalSpecial
164             // which is used for window.eval. We have to take care in which case we are.
165             final boolean isWindowEval = compiler != null;
166 
167             // Remove HTML comments around the source if needed
168             if (!isWindowEval) {
169 
170                 // **** Memory Optimization ****
171                 // final String sourceCodeTrimmed = source.trim();
172                 // if (sourceCodeTrimmed.startsWith("<!--")) {
173                 // **** Memory Optimization ****
174                 // do not trim because this will create a copy of the
175                 // whole string (usually large for libs like jQuery
176                 // if there is whitespace to trim (e.g. cr at end)
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             // Pre process the source code
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      * Pre process the specified source code in the context of the given page using the processor specified
217      * in the {@link WebClient}. This method delegates to the pre processor handler specified in the
218      * <code>WebClient</code>. If no pre processor handler is defined, the original source code is returned
219      * unchanged.
220      * @param htmlPage the page
221      * @param sourceCode the code to process
222      * @param sourceName a name for the chunk of code (used in error messages)
223      * @param lineNumber the line number of the source code
224      * @param htmlElement the HTML element that will act as the context
225      * @return the source code after being pre processed
226      * @see org.htmlunit.ScriptPreProcessor
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      * {@inheritDoc}
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         // make sure no java classes are usable from js
254         cx.setClassShutter(fullClassName -> false);
255 
256         // Use pure interpreter mode to get observeInstructionCount() callbacks.
257         cx.setInterpretedMode(true);
258 
259         // Set threshold on how often we want to receive the callbacks
260         cx.setInstructionObserverThreshold(INSTRUCTION_COUNT_THRESHOLD);
261 
262         cx.setErrorReporter(new HtmlUnitErrorReporter(webClient_.getJavaScriptErrorListener()));
263         // We don't want to wrap String & Co.
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      * Run-time calls this when instruction counting is enabled and the counter
277      * reaches limit set by setInstructionObserverThreshold(). A script can be
278      * terminated by throwing an Error instance here.
279      *
280      * @param cx the context calling us
281      * @param instructionCount amount of script instruction executed since last call to observeInstructionCount
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      * {@inheritDoc}
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      * {@inheritDoc}
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      * Same as {@link ContextFactory}{@link #call(ContextAction)} but with handling
317      * of some exceptions.
318      *
319      * @param <T> return type of the action
320      * @param action the contextAction
321      * @param page the page
322      * @return the result of the call
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      * {@inheritDoc}
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          * Ctor.
360          *
361          * @param javaScriptErrorListener the listener to be used
362          */
363         HtmlUnitErrorReporter(final JavaScriptErrorListener javaScriptErrorListener) {
364             javaScriptErrorListener_ = javaScriptErrorListener;
365         }
366 
367         /**
368          * Logs a warning.
369          *
370          * @param message the message to be displayed
371          * @param sourceName the name of the source file
372          * @param line the line number
373          * @param lineSource the source code that failed
374          * @param lineOffset the line offset
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          * Logs an error.
385          *
386          * @param message the message to be displayed
387          * @param sourceName the name of the source file
388          * @param line the line number
389          * @param lineSource the source code that failed
390          * @param lineOffset the line offset
391          */
392         @Override
393         public void error(final String message, final String sourceName, final int line,
394                 final String lineSource, final int lineOffset) {
395             // no need to log here, this is only used to create the exception
396             // gets logged if not catched later on
397             throw new EvaluatorException(message, sourceName, line, lineSource, lineOffset);
398         }
399 
400         /**
401          * Logs a runtime error.
402          *
403          * @param message the message to be displayed
404          * @param sourceName the name of the source file
405          * @param line the line number
406          * @param lineSource the source code that failed
407          * @param lineOffset the line offset
408          * @return an evaluator exception
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             // no need to log here, this is only used to create the exception
415             // gets logged if not catched later on
416             return new EvaluatorException(message, sourceName, line, lineSource, lineOffset);
417         }
418     }
419 }