org.htmlunit.javascript.HtmlUnitContextFactory Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xlt Show documentation
Show all versions of xlt Show documentation
XLT (Xceptance LoadTest) is an extensive load and performance test tool developed and maintained by Xceptance.
The newest version!
/*
* Copyright (c) 2002-2024 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
* https://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 org.htmlunit.javascript;
import static org.htmlunit.BrowserVersionFeatures.JS_ARGUMENTS_READ_ONLY_ACCESSED_FROM_FUNCTION;
import static org.htmlunit.BrowserVersionFeatures.JS_IGNORES_LAST_LINE_CONTAINING_UNCOMMENTED;
import static org.htmlunit.BrowserVersionFeatures.JS_PROPERTY_DESCRIPTOR_NAME;
import static org.htmlunit.BrowserVersionFeatures.JS_PROPERTY_DESCRIPTOR_NEW_LINE;
import java.io.Serializable;
import java.util.Map;
import org.htmlunit.BrowserVersion;
import org.htmlunit.ScriptException;
import org.htmlunit.ScriptPreProcessor;
import org.htmlunit.WebClient;
import org.htmlunit.corejs.javascript.Callable;
import org.htmlunit.corejs.javascript.Context;
import org.htmlunit.corejs.javascript.ContextAction;
import org.htmlunit.corejs.javascript.ContextFactory;
import org.htmlunit.corejs.javascript.ErrorReporter;
import org.htmlunit.corejs.javascript.Evaluator;
import org.htmlunit.corejs.javascript.EvaluatorException;
import org.htmlunit.corejs.javascript.Function;
import org.htmlunit.corejs.javascript.Script;
import org.htmlunit.corejs.javascript.ScriptRuntime;
import org.htmlunit.corejs.javascript.Scriptable;
import org.htmlunit.corejs.javascript.debug.Debugger;
import org.htmlunit.html.HtmlElement;
import org.htmlunit.html.HtmlPage;
import org.htmlunit.javascript.regexp.HtmlUnitRegExpProxy;
/**
* ContextFactory that supports termination of scripts if they exceed a timeout. Based on example from
* ContextFactory.
*
* @author Andre Soereng
* @author Ahmed Ashour
* @author Marc Guillemot
* @author Ronald Brill
*/
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 boolean deminifyFunctionCode_;
/**
* Creates a new instance of HtmlUnitContextFactory.
*
* @param webClient the web client using this factory
*/
public HtmlUnitContextFactory(final WebClient webClient) {
webClient_ = webClient;
browserVersion_ = webClient.getBrowserVersion();
}
/**
* 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 0
)
*/
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 new Function("...some 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 new Function("...some 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("
// 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 {@link WebClient}. This method delegates to the pre processor handler specified in the
* WebClient
. 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 org.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);
cx.setLanguageVersion(Context.VERSION_ES6);
cx.setLocale(browserVersion_.getBrowserLocale());
// make sure no java classes are usable from js
cx.setClassShutter(fullClassName -> {
final Map activeXObjectMap = webClient_.getActiveXObjectMap();
if (activeXObjectMap != null) {
for (final String mappedClass : activeXObjectMap.values()) {
if (fullClassName.equals(mappedClass)) {
return true;
}
}
}
return false;
});
// 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);
cx.setErrorReporter(new HtmlUnitErrorReporter(webClient_.getJavaScriptErrorListener()));
// We don't want to wrap String & Co.
cx.getWrapFactory().setJavaPrimitiveWrap(false);
if (debugger_ != null) {
cx.setDebugger(debugger_, null);
}
// register custom RegExp processing
ScriptRuntime.setRegExpProxy(cx, new HtmlUnitRegExpProxy(ScriptRuntime.getRegExpProxy(cx), browserVersion_));
cx.setMaximumInterpreterStackDepth(5_000);
return cx;
}
/**
* 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);
}
/**
* Same as {@link ContextFactory}{@link #call(ContextAction)} but with handling
* of some exceptions.
*
* @param return type of the action
* @param action the contextAction
* @param page the page
* @return the result of the call
*/
public final T callSecured(final ContextAction action, final HtmlPage page) {
try {
return call(action);
}
catch (final StackOverflowError e) {
webClient_.getJavaScriptErrorListener().scriptException(page, new ScriptException(page, e));
return null;
}
}
/**
* {@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_E4X:
return false;
case Context.FEATURE_OLD_UNDEF_NULL_THIS:
return true;
case Context.FEATURE_NON_ECMA_GET_YEAR:
return false;
case Context.FEATURE_LITTLE_ENDIAN:
return true;
case Context.FEATURE_LOCATION_INFORMATION_IN_ERROR:
return true;
case Context.FEATURE_INTL_402:
return true;
case Context.FEATURE_HTMLUNIT_FN_ARGUMENTS_IS_RO_VIEW:
return browserVersion_.hasFeature(JS_ARGUMENTS_READ_ONLY_ACCESSED_FROM_FUNCTION);
case Context.FEATURE_HTMLUNIT_FUNCTION_DECLARED_FORWARD_IN_BLOCK:
return true;
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);
default:
return super.hasFeature(cx, featureIndex);
}
}
private static final class HtmlUnitErrorReporter implements ErrorReporter, Serializable {
private final JavaScriptErrorListener javaScriptErrorListener_;
/**
* Ctor.
*
* @param javaScriptErrorListener the listener to be used
*/
HtmlUnitErrorReporter(final JavaScriptErrorListener javaScriptErrorListener) {
javaScriptErrorListener_ = javaScriptErrorListener;
}
/**
* Logs a warning.
*
* @param message the message to be displayed
* @param sourceName the name of the source file
* @param line the line number
* @param lineSource the source code that failed
* @param lineOffset the line offset
*/
@Override
public void warning(
final String message, final String sourceName, final int line,
final String lineSource, final int lineOffset) {
javaScriptErrorListener_.warn(message, sourceName, line, lineSource, lineOffset);
}
/**
* Logs an error.
*
* @param message the message to be displayed
* @param sourceName the name of the source file
* @param line the line number
* @param lineSource the source code that failed
* @param lineOffset the line offset
*/
@Override
public void error(final String message, final String sourceName, final int line,
final String lineSource, final int lineOffset) {
// no need to log here, this is only used to create the exception
// the exception gets logged if not catched later on
throw new EvaluatorException(message, sourceName, line, lineSource, lineOffset);
}
/**
* Logs a runtime error.
*
* @param message the message to be displayed
* @param sourceName the name of the source file
* @param line the line number
* @param lineSource the source code that failed
* @param lineOffset the line offset
* @return an evaluator exception
*/
@Override
public EvaluatorException runtimeError(
final String message, final String sourceName, final int line,
final String lineSource, final int lineOffset) {
// no need to log here, this is only used to create the exception
// the exception gets logged if not catched later on
return new EvaluatorException(message, sourceName, line, lineSource, lineOffset);
}
}
}