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