All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.htmlunit.javascript.HtmlUnitContextFactory Maven / Gradle / Ivy

Go to download

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);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy