/* * Copyright (c) 2002-2018 Gargoyle Software Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gargoylesoftware.htmlunit.javascript; import com.gargoylesoftware.htmlunit.BrowserVersion; import com.gargoylesoftware.htmlunit.ScriptPreProcessor; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.html.HtmlElement; import com.gargoylesoftware.htmlunit.html.HtmlPage; import com.gargoylesoftware.htmlunit.javascript.regexp.HtmlUnitRegExpProxy; import net.sourceforge.htmlunit.corejs.javascript.Callable; import net.sourceforge.htmlunit.corejs.javascript.Context; import net.sourceforge.htmlunit.corejs.javascript.ContextFactory; import net.sourceforge.htmlunit.corejs.javascript.ErrorReporter; import net.sourceforge.htmlunit.corejs.javascript.Evaluator; import net.sourceforge.htmlunit.corejs.javascript.Function; import net.sourceforge.htmlunit.corejs.javascript.Script; import net.sourceforge.htmlunit.corejs.javascript.ScriptRuntime; import net.sourceforge.htmlunit.corejs.javascript.Scriptable; import net.sourceforge.htmlunit.corejs.javascript.WrapFactory; import net.sourceforge.htmlunit.corejs.javascript.debug.Debugger; import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_ARGUMENTS_READ_ONLY_ACCESSED_FROM_FUNCTION; import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_ARRAY_CONSTRUCTION_PROPERTIES; import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_ENUM_NUMBERS_FIRST; import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_ERROR_STACK; import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_FUNCTION_DECLARED_FORWARD_IN_BLOCK; import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_GET_PROTOTYPE_OF_STRING; import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_IGNORES_LAST_LINE_CONTAINING_UNCOMMENTED; import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_PROPERTY_DESCRIPTOR_NAME; import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_PROPERTY_DESCRIPTOR_NEW_LINE; /** * ContextFactory that supports termination of scripts if they exceed a timeout. Based on example from * <a href="http://www.mozilla.org/rhino/apidocs/org/mozilla/javascript/ContextFactory.html">ContextFactory</a>. * * @author Andre Soereng * @author Ahmed Ashour * @author Marc Guillemot */ public class HtmlUnitContextFactory extends ContextFactory { private static final int INSTRUCTION_COUNT_THRESHOLD = 10_000; private final WebClient webClient_; private final BrowserVersion browserVersion_; private long timeout_; private Debugger debugger_; private final ErrorReporter errorReporter_; private final WrapFactory wrapFactory_ = new HtmlUnitWrapFactory(); private boolean deminifyFunctionCode_ = false; /** * Creates a new instance of HtmlUnitContextFactory. * * @param webClient the web client using this factory */ public HtmlUnitContextFactory(final WebClient webClient) { webClient_ = webClient; browserVersion_ = webClient.getBrowserVersion(); errorReporter_ = new StrictErrorReporter(); } /** * Sets the number of milliseconds a script is allowed to execute before * being terminated. A value of 0 or less means no timeout. * * @param timeout the timeout value */ public void setTimeout(final long timeout) { timeout_ = timeout; } /** * Returns the number of milliseconds a script is allowed to execute before * being terminated. A value of 0 or less means no timeout. * * @return the timeout value (default value is <tt>0</tt>) */ public long getTimeout() { return timeout_; } /** * Sets the JavaScript debugger to use to receive JavaScript execution debugging information. * The HtmlUnit default implementation ({@link DebuggerImpl}, {@link DebugFrameImpl}) may be * used, or a custom debugger may be used instead. By default, no debugger is used. * * @param debugger the JavaScript debugger to use (may be {@code null}) */ public void setDebugger(final Debugger debugger) { debugger_ = debugger; } /** * Returns the JavaScript debugger to use to receive JavaScript execution debugging information. * By default, no debugger is used, and this method returns {@code null}. * * @return the JavaScript debugger to use to receive JavaScript execution debugging information */ public Debugger getDebugger() { return debugger_; } /** * Configures if the code of <code>new Function("...some code...")</code> should be deminified to be more readable * when using the debugger. This is a small performance cost. * @param deminify the new value */ public void setDeminifyFunctionCode(final boolean deminify) { deminifyFunctionCode_ = deminify; } /** * Indicates code of calls like <code>new Function("...some code...")</code> should be deminified to be more * readable when using the debugger. * @return the de-minify status */ public boolean isDeminifyFunctionCode() { return deminifyFunctionCode_; } /** * Custom context to store execution time and handle timeouts. */ private class TimeoutContext extends Context { private long startTime_; protected TimeoutContext(final ContextFactory factory) { super(factory); } public void startClock() { startTime_ = System.currentTimeMillis(); } public void terminateScriptIfNecessary() { if (timeout_ > 0) { final long currentTime = System.currentTimeMillis(); if (currentTime - startTime_ > timeout_) { // Terminate script by throwing an Error instance to ensure that the // script will never get control back through catch or finally. throw new TimeoutError(timeout_, currentTime - startTime_); } } } @Override protected Script compileString(String source, final Evaluator compiler, final ErrorReporter compilationErrorReporter, final String sourceName, final int lineno, final Object securityDomain) { // this method gets called by Context.compileString and by ScriptRuntime.evalSpecial // which is used for window.eval. We have to take care in which case we are. final boolean isWindowEval = compiler != null; // Remove HTML comments around the source if needed if (!isWindowEval) { // **** Memory Optimization **** // final String sourceCodeTrimmed = source.trim(); // if (sourceCodeTrimmed.startsWith("<!--")) { // **** Memory Optimization **** // do not trim because this will create a copy of the // whole string (usually large for libs like jQuery // if there is whitespace to trim (e.g. cr at end) final int length = source.length(); int start = 0; while ((start < length) && (source.charAt(start) <= ' ')) { start++; } if (start + 3 < length && source.charAt(start++) == '<' && source.charAt(start++) == '!' && source.charAt(start++) == '-' && source.charAt(start++) == '-') { source = source.replaceFirst("<!--", "// <!--"); } // IE ignores the last line containing uncommented --> // if (browserVersion_.hasFeature(JS_IGNORES_LAST_LINE_CONTAINING_UNCOMMENTED) // && sourceCodeTrimmed.endsWith("-->")) { // **** Memory Optimization **** // see above if (browserVersion_.hasFeature(JS_IGNORES_LAST_LINE_CONTAINING_UNCOMMENTED)) { int end = source.length() - 1; while ((end > -1) && (source.charAt(end) <= ' ')) { end--; } if (1 < end && source.charAt(end--) == '>' && source.charAt(end--) == '-' && source.charAt(end--) == '-') { final int lastDoubleSlash = source.lastIndexOf("//"); final int lastNewLine = Math.max(source.lastIndexOf('\n'), source.lastIndexOf('\r')); if (lastNewLine > lastDoubleSlash) { source = source.substring(0, lastNewLine); } } } } // Pre process the source code final HtmlPage page = (HtmlPage) Context.getCurrentContext() .getThreadLocal(JavaScriptEngine.KEY_STARTING_PAGE); source = preProcess(page, source, sourceName, lineno, null); return super.compileString(source, compiler, compilationErrorReporter, sourceName, lineno, securityDomain); } @Override protected Function compileFunction(final Scriptable scope, String source, final Evaluator compiler, final ErrorReporter compilationErrorReporter, final String sourceName, final int lineno, final Object securityDomain) { if (deminifyFunctionCode_) { final Function f = super.compileFunction(scope, source, compiler, compilationErrorReporter, sourceName, lineno, securityDomain); source = decompileFunction(f, 4).trim().replace("\n ", "\n"); } return super.compileFunction(scope, source, compiler, compilationErrorReporter, sourceName, lineno, securityDomain); } } /** * Pre process the specified source code in the context of the given page using the processor specified * in the webclient. This method delegates to the pre processor handler specified in the * <code>WebClient</code>. If no pre processor handler is defined, the original source code is returned * unchanged. * @param htmlPage the page * @param sourceCode the code to process * @param sourceName a name for the chunk of code (used in error messages) * @param lineNumber the line number of the source code * @param htmlElement the HTML element that will act as the context * @return the source code after being pre processed * @see com.gargoylesoftware.htmlunit.ScriptPreProcessor */ protected String preProcess( final HtmlPage htmlPage, final String sourceCode, final String sourceName, final int lineNumber, final HtmlElement htmlElement) { String newSourceCode = sourceCode; final ScriptPreProcessor preProcessor = webClient_.getScriptPreProcessor(); if (preProcessor != null) { newSourceCode = preProcessor.preProcess(htmlPage, sourceCode, sourceName, lineNumber, htmlElement); if (newSourceCode == null) { newSourceCode = ""; } } return newSourceCode; } /** * {@inheritDoc} */ @Override protected Context makeContext() { final TimeoutContext cx = new TimeoutContext(this); // Use pure interpreter mode to get observeInstructionCount() callbacks. cx.setOptimizationLevel(-1); // Set threshold on how often we want to receive the callbacks cx.setInstructionObserverThreshold(INSTRUCTION_COUNT_THRESHOLD); configureErrorReporter(cx); cx.setWrapFactory(wrapFactory_); if (debugger_ != null) { cx.setDebugger(debugger_, null); } // register custom RegExp processing ScriptRuntime.setRegExpProxy(cx, new HtmlUnitRegExpProxy(ScriptRuntime.getRegExpProxy(cx), browserVersion_)); cx.setMaximumInterpreterStackDepth(10_000); return cx; } /** * Configures the {@link ErrorReporter} on the context. * @param context the context to configure * @see Context#setErrorReporter(ErrorReporter) */ protected void configureErrorReporter(final Context context) { context.setErrorReporter(errorReporter_); } /** * Run-time calls this when instruction counting is enabled and the counter * reaches limit set by setInstructionObserverThreshold(). A script can be * terminated by throwing an Error instance here. * * @param cx the context calling us * @param instructionCount amount of script instruction executed since last call to observeInstructionCount */ @Override protected void observeInstructionCount(final Context cx, final int instructionCount) { final TimeoutContext tcx = (TimeoutContext) cx; tcx.terminateScriptIfNecessary(); } /** * {@inheritDoc} */ @Override protected Object doTopCall(final Callable callable, final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) { final TimeoutContext tcx = (TimeoutContext) cx; tcx.startClock(); return super.doTopCall(callable, cx, scope, thisObj, args); } /** * {@inheritDoc} */ @Override protected boolean hasFeature(final Context cx, final int featureIndex) { switch (featureIndex) { case Context.FEATURE_RESERVED_KEYWORD_AS_IDENTIFIER: return true; case Context.FEATURE_NON_ECMA_GET_YEAR: return false; case Context.FEATURE_HTMLUNIT_FN_ARGUMENTS_IS_RO_VIEW: return browserVersion_.hasFeature(JS_ARGUMENTS_READ_ONLY_ACCESSED_FROM_FUNCTION); case Context.FEATURE_HTMLUNIT_ERROR_STACK: return browserVersion_.hasFeature(JS_ERROR_STACK); case Context.FEATURE_HTMLUNIT_FUNCTION_DECLARED_FORWARD_IN_BLOCK: return browserVersion_.hasFeature(JS_FUNCTION_DECLARED_FORWARD_IN_BLOCK); case Context.FEATURE_HTMLUNIT_ENUM_NUMBERS_FIRST: return browserVersion_.hasFeature(JS_ENUM_NUMBERS_FIRST); case Context.FEATURE_HTMLUNIT_GET_PROTOTYPE_OF_STRING: return browserVersion_.hasFeature(JS_GET_PROTOTYPE_OF_STRING); case Context.FEATURE_HTMLUNIT_MEMBERBOX_NAME: return browserVersion_.hasFeature(JS_PROPERTY_DESCRIPTOR_NAME); case Context.FEATURE_HTMLUNIT_MEMBERBOX_NEWLINE: return browserVersion_.hasFeature(JS_PROPERTY_DESCRIPTOR_NEW_LINE); case Context.FEATURE_HTMLUNIT_ARRAY_PROPERTIES: return browserVersion_.hasFeature(JS_ARRAY_CONSTRUCTION_PROPERTIES); default: return super.hasFeature(cx, featureIndex); } } }