View Javadoc
1   /*
2    * Copyright (c) 2002-2017 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    * http://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 com.gargoylesoftware.htmlunit.javascript;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_ARGUMENTS_READ_ONLY_ACCESSED_FROM_FUNCTION;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_ENUM_NUMBERS_FIRST;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_ERROR_STACK;
20  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_FUNCTION_DECLARED_FORWARD_IN_BLOCK;
21  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_GET_PROTOTYPE_OF_STRING;
22  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_IGNORES_LAST_LINE_CONTAINING_UNCOMMENTED;
23  
24  import com.gargoylesoftware.htmlunit.BrowserVersion;
25  import com.gargoylesoftware.htmlunit.ScriptPreProcessor;
26  import com.gargoylesoftware.htmlunit.WebClient;
27  import com.gargoylesoftware.htmlunit.html.HtmlElement;
28  import com.gargoylesoftware.htmlunit.html.HtmlPage;
29  import com.gargoylesoftware.htmlunit.javascript.regexp.HtmlUnitRegExpProxy;
30  
31  import net.sourceforge.htmlunit.corejs.javascript.Callable;
32  import net.sourceforge.htmlunit.corejs.javascript.Context;
33  import net.sourceforge.htmlunit.corejs.javascript.ContextFactory;
34  import net.sourceforge.htmlunit.corejs.javascript.ErrorReporter;
35  import net.sourceforge.htmlunit.corejs.javascript.Evaluator;
36  import net.sourceforge.htmlunit.corejs.javascript.Function;
37  import net.sourceforge.htmlunit.corejs.javascript.Script;
38  import net.sourceforge.htmlunit.corejs.javascript.ScriptRuntime;
39  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
40  import net.sourceforge.htmlunit.corejs.javascript.WrapFactory;
41  import net.sourceforge.htmlunit.corejs.javascript.debug.Debugger;
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   */
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 final ErrorReporter errorReporter_;
60      private final WrapFactory wrapFactory_ = new HtmlUnitWrapFactory();
61      private boolean deminifyFunctionCode_ = false;
62  
63      /**
64       * Creates a new instance of HtmlUnitContextFactory.
65       *
66       * @param webClient the web client using this factory
67       */
68      public HtmlUnitContextFactory(final WebClient webClient) {
69          webClient_ = webClient;
70          browserVersion_ = webClient.getBrowserVersion();
71          errorReporter_ = new StrictErrorReporter();
72      }
73  
74      /**
75       * Sets the number of milliseconds a script is allowed to execute before
76       * being terminated. A value of 0 or less means no timeout.
77       *
78       * @param timeout the timeout value
79       */
80      public void setTimeout(final long timeout) {
81          timeout_ = timeout;
82      }
83  
84      /**
85       * Returns the number of milliseconds a script is allowed to execute before
86       * being terminated. A value of 0 or less means no timeout.
87       *
88       * @return the timeout value (default value is <tt>0</tt>)
89       */
90      public long getTimeout() {
91          return timeout_;
92      }
93  
94      /**
95       * Sets the JavaScript debugger to use to receive JavaScript execution debugging information.
96       * The HtmlUnit default implementation ({@link DebuggerImpl}, {@link DebugFrameImpl}) may be
97       * used, or a custom debugger may be used instead. By default, no debugger is used.
98       *
99       * @param debugger the JavaScript debugger to use (may be {@code null})
100      */
101     public void setDebugger(final Debugger debugger) {
102         debugger_ = debugger;
103     }
104 
105     /**
106      * Returns the JavaScript debugger to use to receive JavaScript execution debugging information.
107      * By default, no debugger is used, and this method returns {@code null}.
108      *
109      * @return the JavaScript debugger to use to receive JavaScript execution debugging information
110      */
111     public Debugger getDebugger() {
112         return debugger_;
113     }
114 
115     /**
116      * Configures if the code of <code>new Function("...some code...")</code> should be deminified to be more readable
117      * when using the debugger. This is a small performance cost.
118      * @param deminify the new value
119      */
120     public void setDeminifyFunctionCode(final boolean deminify) {
121         deminifyFunctionCode_ = deminify;
122     }
123 
124     /**
125      * Indicates code of calls like <code>new Function("...some code...")</code> should be deminified to be more
126      * readable when using the debugger.
127      * @return the de-minify status
128      */
129     public boolean isDeminifyFunctionCode() {
130         return deminifyFunctionCode_;
131     }
132 
133     /**
134      * Custom context to store execution time and handle timeouts.
135      */
136     private class TimeoutContext extends Context {
137         private long startTime_;
138         protected TimeoutContext(final ContextFactory factory) {
139             super(factory);
140         }
141         public void startClock() {
142             startTime_ = System.currentTimeMillis();
143         }
144         public void terminateScriptIfNecessary() {
145             if (timeout_ > 0) {
146                 final long currentTime = System.currentTimeMillis();
147                 if (currentTime - startTime_ > timeout_) {
148                     // Terminate script by throwing an Error instance to ensure that the
149                     // script will never get control back through catch or finally.
150                     throw new TimeoutError(timeout_, currentTime - startTime_);
151                 }
152             }
153         }
154         @Override
155         protected Script compileString(String source, final Evaluator compiler,
156                 final ErrorReporter compilationErrorReporter, final String sourceName,
157                 final int lineno, final Object securityDomain) {
158 
159             // this method gets called by Context.compileString and by ScriptRuntime.evalSpecial
160             // which is used for window.eval. We have to take care in which case we are.
161             final boolean isWindowEval = compiler != null;
162 
163             // Remove HTML comments around the source if needed
164             if (!isWindowEval) {
165 
166                 // **** Memory Optimization ****
167                 // final String sourceCodeTrimmed = source.trim();
168                 // if (sourceCodeTrimmed.startsWith("<!--")) {
169                 // **** Memory Optimization ****
170                 // do not trim because this will create a copy of the
171                 // whole string (usually large for libs like jQuery
172                 // if there is whitespace to trim (e.g. cr at end)
173                 final int length = source.length();
174                 int start = 0;
175                 while ((start < length) && (source.charAt(start) <= ' ')) {
176                     start++;
177                 }
178                 if (start + 3 < length
179                         && source.charAt(start++) == '<'
180                         && source.charAt(start++) == '!'
181                         && source.charAt(start++) == '-'
182                         && source.charAt(start++) == '-') {
183                     source = source.replaceFirst("<!--", "// <!--");
184                 }
185 
186                 // IE ignores the last line containing uncommented -->
187                 // if (browserVersion_.hasFeature(JS_IGNORES_LAST_LINE_CONTAINING_UNCOMMENTED)
188                 //         && sourceCodeTrimmed.endsWith("-->")) {
189                 // **** Memory Optimization ****
190                 // see above
191                 if (browserVersion_.hasFeature(JS_IGNORES_LAST_LINE_CONTAINING_UNCOMMENTED)) {
192                     int end = source.length() - 1;
193                     while ((end > -1) && (source.charAt(end) <= ' ')) {
194                         end--;
195                     }
196                     if (1 < end
197                             && source.charAt(end--) == '>'
198                             && source.charAt(end--) == '-'
199                             && source.charAt(end--) == '-') {
200                         final int lastDoubleSlash = source.lastIndexOf("//");
201                         final int lastNewLine = Math.max(source.lastIndexOf('\n'), source.lastIndexOf('\r'));
202                         if (lastNewLine > lastDoubleSlash) {
203                             source = source.substring(0, lastNewLine);
204                         }
205                     }
206                 }
207             }
208 
209             // Pre process the source code
210             final HtmlPage page = (HtmlPage) Context.getCurrentContext()
211                 .getThreadLocal(JavaScriptEngine.KEY_STARTING_PAGE);
212             source = preProcess(page, source, sourceName, lineno, null);
213 
214             return super.compileString(source, compiler, compilationErrorReporter,
215                     sourceName, lineno, securityDomain);
216         }
217 
218         @Override
219         protected Function compileFunction(final Scriptable scope, String source,
220                 final Evaluator compiler, final ErrorReporter compilationErrorReporter,
221                 final String sourceName, final int lineno, final Object securityDomain) {
222 
223             if (deminifyFunctionCode_) {
224                 final Function f = super.compileFunction(scope, source, compiler,
225                         compilationErrorReporter, sourceName, lineno, securityDomain);
226                 source = decompileFunction(f, 4).trim().replace("\n    ", "\n");
227             }
228             return super.compileFunction(scope, source, compiler,
229                     compilationErrorReporter, sourceName, lineno, securityDomain);
230         }
231     }
232 
233     /**
234      * Pre process the specified source code in the context of the given page using the processor specified
235      * in the webclient. This method delegates to the pre processor handler specified in the
236      * <code>WebClient</code>. If no pre processor handler is defined, the original source code is returned
237      * unchanged.
238      * @param htmlPage the page
239      * @param sourceCode the code to process
240      * @param sourceName a name for the chunk of code (used in error messages)
241      * @param lineNumber the line number of the source code
242      * @param htmlElement the HTML element that will act as the context
243      * @return the source code after being pre processed
244      * @see com.gargoylesoftware.htmlunit.ScriptPreProcessor
245      */
246     protected String preProcess(
247         final HtmlPage htmlPage, final String sourceCode, final String sourceName, final int lineNumber,
248         final HtmlElement htmlElement) {
249 
250         String newSourceCode = sourceCode;
251         final ScriptPreProcessor preProcessor = webClient_.getScriptPreProcessor();
252         if (preProcessor != null) {
253             newSourceCode = preProcessor.preProcess(htmlPage, sourceCode, sourceName, lineNumber, htmlElement);
254             if (newSourceCode == null) {
255                 newSourceCode = "";
256             }
257         }
258         return newSourceCode;
259     }
260 
261     /**
262      * {@inheritDoc}
263      */
264     @Override
265     protected Context makeContext() {
266         final TimeoutContext cx = new TimeoutContext(this);
267 
268         // Use pure interpreter mode to get observeInstructionCount() callbacks.
269         cx.setOptimizationLevel(-1);
270 
271         // Set threshold on how often we want to receive the callbacks
272         cx.setInstructionObserverThreshold(INSTRUCTION_COUNT_THRESHOLD);
273 
274         configureErrorReporter(cx);
275         cx.setWrapFactory(wrapFactory_);
276 
277         if (debugger_ != null) {
278             cx.setDebugger(debugger_, null);
279         }
280 
281         // register custom RegExp processing
282         ScriptRuntime.setRegExpProxy(cx, new HtmlUnitRegExpProxy(ScriptRuntime.getRegExpProxy(cx), browserVersion_));
283 
284         cx.setMaximumInterpreterStackDepth(10_000);
285 
286         return cx;
287     }
288 
289     /**
290      * Configures the {@link ErrorReporter} on the context.
291      * @param context the context to configure
292      * @see Context#setErrorReporter(ErrorReporter)
293      */
294     protected void configureErrorReporter(final Context context) {
295         context.setErrorReporter(errorReporter_);
296     }
297 
298     /**
299      * Run-time calls this when instruction counting is enabled and the counter
300      * reaches limit set by setInstructionObserverThreshold(). A script can be
301      * terminated by throwing an Error instance here.
302      *
303      * @param cx the context calling us
304      * @param instructionCount amount of script instruction executed since last call to observeInstructionCount
305      */
306     @Override
307     protected void observeInstructionCount(final Context cx, final int instructionCount) {
308         final TimeoutContext tcx = (TimeoutContext) cx;
309         tcx.terminateScriptIfNecessary();
310     }
311 
312     /**
313      * {@inheritDoc}
314      */
315     @Override
316     protected Object doTopCall(final Callable callable,
317             final Context cx, final Scriptable scope,
318             final Scriptable thisObj, final Object[] args) {
319 
320         final TimeoutContext tcx = (TimeoutContext) cx;
321         tcx.startClock();
322         return super.doTopCall(callable, cx, scope, thisObj, args);
323     }
324 
325     /**
326      * {@inheritDoc}
327      */
328     @Override
329     protected boolean hasFeature(final Context cx, final int featureIndex) {
330         switch (featureIndex) {
331             case Context.FEATURE_RESERVED_KEYWORD_AS_IDENTIFIER:
332                 return true;
333             case Context.FEATURE_NON_ECMA_GET_YEAR:
334                 return false;
335             case Context.FEATURE_HTMLUNIT_ASK_OBJECT_TO_WRITE_READONLY:
336                 return true;
337             case Context.FEATURE_HTMLUNIT_JS_CATCH_JAVA_EXCEPTION:
338                 return false;
339             case Context.FEATURE_HTMLUNIT_ARGUMENTS_IS_OBJECT:
340                 return false;
341             case Context.FEATURE_HTMLUNIT_FUNCTION_NULL_SETTER:
342                 return true;
343             case Context.FEATURE_HTMLUNIT_FN_ARGUMENTS_IS_RO_VIEW:
344                 return browserVersion_.hasFeature(JS_ARGUMENTS_READ_ONLY_ACCESSED_FROM_FUNCTION);
345             case Context.FEATURE_HTMLUNIT_EVAL_LOCAL_SCOPE:
346                 return false;
347             case Context.FEATURE_HTMLUNIT_ERROR_STACK:
348                 return browserVersion_.hasFeature(JS_ERROR_STACK);
349             case Context.FEATURE_HTMLUNIT_CONSTRUCTOR:
350                 return true;
351             case Context.FEATURE_HTMLUNIT_FUNCTION_OBJECT_METHOD:
352                 return false;
353             case Context.FEATURE_HTMLUNIT_FUNCTION_DECLARED_FORWARD_IN_BLOCK:
354                 return browserVersion_.hasFeature(JS_FUNCTION_DECLARED_FORWARD_IN_BLOCK);
355             case Context.FEATURE_HTMLUNIT_PARSE_INT_RADIX_10:
356                 return true;
357             case Context.FEATURE_HTMLUNIT_ENUM_NUMBERS_FIRST:
358                 return browserVersion_.hasFeature(JS_ENUM_NUMBERS_FIRST);
359             case Context.FEATURE_HTMLUNIT_GET_PROTOTYPE_OF_STRING:
360                 return browserVersion_.hasFeature(JS_GET_PROTOTYPE_OF_STRING);
361             default:
362                 return super.hasFeature(cx, featureIndex);
363         }
364     }
365 }