com.gargoylesoftware.htmlunit.activex.javascript.msxml.XMLHTTPRequest.java Source code

Java tutorial

Introduction

Here is the source code for com.gargoylesoftware.htmlunit.activex.javascript.msxml.XMLHTTPRequest.java

Source

/*
 * Copyright (c) 2002-2016 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.activex.javascript.msxml;

import static com.gargoylesoftware.htmlunit.javascript.configuration.BrowserName.IE;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Stack;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.auth.UsernamePasswordCredentials;

import com.gargoylesoftware.htmlunit.AjaxController;
import com.gargoylesoftware.htmlunit.FormEncodingType;
import com.gargoylesoftware.htmlunit.HttpMethod;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.WebRequest;
import com.gargoylesoftware.htmlunit.WebResponse;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
import com.gargoylesoftware.htmlunit.javascript.background.BackgroundJavaScriptFactory;
import com.gargoylesoftware.htmlunit.javascript.background.JavaScriptJob;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
import com.gargoylesoftware.htmlunit.javascript.configuration.JsxSetter;
import com.gargoylesoftware.htmlunit.javascript.configuration.WebBrowser;
import com.gargoylesoftware.htmlunit.javascript.host.xml.FormData;
import com.gargoylesoftware.htmlunit.util.NameValuePair;
import com.gargoylesoftware.htmlunit.xml.XmlPage;

import net.sourceforge.htmlunit.corejs.javascript.Context;
import net.sourceforge.htmlunit.corejs.javascript.ContextAction;
import net.sourceforge.htmlunit.corejs.javascript.ContextFactory;
import net.sourceforge.htmlunit.corejs.javascript.Function;
import net.sourceforge.htmlunit.corejs.javascript.ScriptRuntime;
import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
import net.sourceforge.htmlunit.corejs.javascript.Undefined;

/**
 * A JavaScript object for MSXML's (ActiveX) XMLHTTPRequest.<br>
 * Provides client-side protocol support for communication with HTTP servers.
 * @see <a href="http://msdn.microsoft.com/en-us/library/ms759148.aspx">MSDN documentation</a>
 *
 * @author Daniel Gredler
 * @author Marc Guillemot
 * @author Ahmed Ashour
 * @author Stuart Begg
 * @author Ronald Brill
 * @author Sebastian Cato
 * @author Frank Danek
 * @author Jake Cobb
 */
@JsxClass(browsers = @WebBrowser(IE))
public class XMLHTTPRequest extends MSXMLScriptable {

    private static final Log LOG = LogFactory.getLog(XMLHTTPRequest.class);

    /** The object has been created, but not initialized (the open() method has not been called). */
    public static final int STATE_UNSENT = 0;
    /** The object has been created, but the send() method has not been called. */
    public static final int STATE_OPENED = 1;
    /** The send() method has been called, but the status and headers are not yet available. */
    public static final int STATE_HEADERS_RECEIVED = 2;
    /** Some data has been received. */
    public static final int STATE_LOADING = 3;
    /** All the data has been received; the complete data is available in responseBody and responseText. */
    public static final int STATE_DONE = 4;

    private static final String HEADER_ORIGIN = "Origin";
    private static final char REQUEST_HEADERS_SEPARATOR = ',';

    private static final String HEADER_ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
    private static final String HEADER_ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
    private static final String HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
    private static final String HEADER_ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";

    private static final String ALLOW_ORIGIN_ALL = "*";

    private static final String[] ALL_PROPERTIES_ = { "onreadystatechange", "readyState", "responseText",
            "responseXML", "status", "statusText", "abort", "getAllResponseHeaders", "getResponseHeader", "open",
            "send", "setRequestHeader" };

    private static Collection<String> PROHIBITED_HEADERS_ = Arrays.asList("accept-charset", "accept-encoding",
            "connection", "content-length", "cookie", "cookie2", "content-transfer-encoding", "date", "expect",
            "host", "keep-alive", "referer", "te", "trailer", "transfer-encoding", "upgrade", "user-agent", "via");

    private int state_ = STATE_UNSENT;
    private Function stateChangeHandler_;
    private WebRequest webRequest_;
    private boolean async_;
    private int jobID_;
    private WebResponse webResponse_;
    private HtmlPage containingPage_;
    private boolean openedMultipleTimes_;
    private boolean sent_;

    /**
     * Creates an instance.
     */
    @JsxConstructor
    public XMLHTTPRequest() {
    }

    /**
     * Returns the event handler to be called when the <code>readyState</code> property changes.
     * @return the event handler to be called when the readyState property changes
     */
    @JsxGetter
    public Object getOnreadystatechange() {
        if (stateChangeHandler_ == null) {
            return Undefined.instance;
        }
        return stateChangeHandler_;
    }

    /**
     * Sets the event handler to be called when the <code>readyState</code> property changes.
     * @param stateChangeHandler the event handler to be called when the readyState property changes
     */
    @JsxSetter
    public void setOnreadystatechange(final Function stateChangeHandler) {
        stateChangeHandler_ = stateChangeHandler;
        if (state_ == STATE_OPENED) {
            setState(state_, null);
        }
    }

    /**
     * Sets the state as specified and invokes the state change handler if one has been set.
     * @param state the new state
     * @param context the context within which the state change handler is to be invoked;
     *     if {@code null}, the current thread's context is used
     */
    private void setState(final int state, Context context) {
        state_ = state;

        if (stateChangeHandler_ != null && !openedMultipleTimes_) {
            final Scriptable scope = stateChangeHandler_.getParentScope();
            final JavaScriptEngine jsEngine = containingPage_.getWebClient().getJavaScriptEngine();

            if (LOG.isDebugEnabled()) {
                LOG.debug("Calling onreadystatechange handler for state " + state);
            }
            final Object[] params = ArrayUtils.EMPTY_OBJECT_ARRAY;

            jsEngine.callFunction(containingPage_, stateChangeHandler_, scope, this, params);
            if (LOG.isDebugEnabled()) {
                if (context == null) {
                    context = Context.getCurrentContext();
                }
                LOG.debug("onreadystatechange handler: " + context.decompileFunction(stateChangeHandler_, 4));
                LOG.debug("Calling onreadystatechange handler for state " + state + ". Done.");
            }
        }
    }

    /**
     * Returns the state of the request. The possible values are:
     * <ul>
     *   <li>0 = unsent</li>
     *   <li>1 = opened</li>
     *   <li>2 = headers_received</li>
     *   <li>3 = loading</li>
     *   <li>4 = done</li>
     * </ul>
     * @return the state of the request
     */
    @JsxGetter
    public int getReadyState() {
        return state_;
    }

    /**
     * Returns the response entity body as a string.
     * @return the response entity body as a string
     */
    @JsxGetter
    public String getResponseText() {
        if (state_ == STATE_UNSENT) {
            throw Context.reportRuntimeError(
                    "The data necessary to complete this operation is not yet available (request not opened).");
        }
        if (state_ != STATE_DONE) {
            throw Context.reportRuntimeError("Unspecified error (request not sent).");
        }
        if (webResponse_ != null) {
            return webResponse_.getContentAsString();
        }
        if (LOG.isDebugEnabled()) {
            LOG.debug("XMLHTTPRequest.responseText was retrieved before the response was available.");
        }
        return "";
    }

    /**
     * Returns the parsed response entity body.
     * @return the parsed response entity body
     */
    @JsxGetter
    public Object getResponseXML() {
        if (state_ == STATE_UNSENT) {
            throw Context.reportRuntimeError("Unspecified error (request not opened).");
        }
        if (state_ == STATE_DONE && webResponse_ != null && !(webResponse_ instanceof NetworkErrorWebResponse)) {
            final String contentType = webResponse_.getContentType();
            if (contentType.contains("xml")) {
                try {
                    final XmlPage page = new XmlPage(webResponse_, getWindow().getWebWindow(), true, false);
                    final XMLDOMDocument doc = new XMLDOMDocument();
                    doc.setDomNode(page);
                    doc.setPrototype(getPrototype(doc.getClass()));
                    doc.setEnvironment(getEnvironment());
                    doc.setParentScope(getWindow());
                    return doc;
                } catch (final IOException e) {
                    LOG.warn("Failed parsing XML document " + webResponse_.getWebRequest().getUrl() + ": "
                            + e.getMessage());
                    return null;
                }
            }
        }
        final XMLDOMDocument doc = new XMLDOMDocument(getWindow().getWebWindow());
        doc.setPrototype(getPrototype(doc.getClass()));
        doc.setEnvironment(getEnvironment());
        return doc;
    }

    /**
     * Returns the HTTP status code returned by a request.
     * @return the HTTP status code returned by a request
     */
    @JsxGetter
    public int getStatus() {
        if (state_ != STATE_DONE) {
            throw Context.reportRuntimeError("Unspecified error (request not sent).");
        }
        if (webResponse_ != null) {
            return webResponse_.getStatusCode();
        }

        LOG.error("XMLHTTPRequest.status was retrieved without a response available (readyState: " + state_ + ").");
        return 0;
    }

    /**
     * Returns the HTTP response line status.
     * @return the HTTP response line status
     */
    @JsxGetter
    public String getStatusText() {
        if (state_ != STATE_DONE) {
            throw Context.reportRuntimeError("Unspecified error (request not sent).");
        }
        if (webResponse_ != null) {
            return webResponse_.getStatusMessage();
        }

        LOG.error("XMLHTTPRequest.statusText was retrieved without a response available (readyState: " + state_
                + ").");
        return null;
    }

    /**
     * Cancels the current HTTP request.
     */
    @JsxFunction
    public void abort() {
        getWindow().getWebWindow().getJobManager().stopJob(jobID_);
        setState(STATE_UNSENT, Context.getCurrentContext());
    }

    /**
     * Returns the values of all the HTTP headers.
     * @return the resulting header information
     */
    @JsxFunction
    public String getAllResponseHeaders() {
        if (state_ == STATE_UNSENT || state_ == STATE_OPENED) {
            throw Context.reportRuntimeError("Unspecified error (request not sent).");
        }
        if (webResponse_ != null) {
            final StringBuilder buffer = new StringBuilder();
            for (final NameValuePair header : webResponse_.getResponseHeaders()) {
                buffer.append(header.getName()).append(": ").append(header.getValue()).append("\r\n");
            }
            return buffer.append("\r\n").toString();
        }

        LOG.error("XMLHTTPRequest.getAllResponseHeaders() was called without a response available (readyState: "
                + state_ + ").");
        return null;
    }

    /**
     * Retrieves the value of an HTTP header from the response body.
     * @param header the case-insensitive header name
     * @return the resulting header information
     */
    @JsxFunction
    public String getResponseHeader(final String header) {
        if (state_ == STATE_UNSENT || state_ == STATE_OPENED) {
            throw Context.reportRuntimeError("Unspecified error (request not sent).");
        }
        if (header == null || "null".equals(header)) {
            throw Context.reportRuntimeError("Type mismatch (header is null).");
        }
        if ("".equals(header)) {
            throw Context.reportRuntimeError("Invalid argument (header is empty).");
        }
        if (webResponse_ != null) {
            final String value = webResponse_.getResponseHeaderValue(header);
            if (value == null) {
                return "";
            }
            return value;
        }

        LOG.error("XMLHTTPRequest.getResponseHeader(..) was called without a response available (readyState: "
                + state_ + ").");
        return null;
    }

    /**
     * Initializes the request and specifies the method, URL, and authentication information for the request.
     * @param method the HTTP method used to open the connection, such as GET, POST, PUT, or PROPFIND;
     *      for XMLHTTP, this parameter is not case-sensitive; the verbs TRACE and TRACK are not allowed.
     * @param url the requested URL; this can be either an absolute URL or a relative URL
     * @param asyncParam indicator of whether the call is asynchronous; the default is {@code true} (the call
     *     returns immediately); if set to {@code true}, attach an <code>onreadystatechange</code> property
     *     callback so that you can tell when the <code>send</code> call has completed
     * @param user the name of the user for authentication
     * @param password the password for authentication
     */
    @JsxFunction
    public void open(final String method, final Object url, final Object asyncParam, final Object user,
            final Object password) {
        if (method == null || "null".equals(method)) {
            throw Context.reportRuntimeError("Type mismatch (method is null).");
        }
        if (url == null || "null".equals(url)) {
            throw Context.reportRuntimeError("Type mismatch (url is null).");
        }
        state_ = STATE_UNSENT;
        openedMultipleTimes_ = webRequest_ != null;
        sent_ = false;
        webRequest_ = null;
        webResponse_ = null;
        if ("".equals(method) || "TRACE".equalsIgnoreCase(method)) {
            throw Context.reportRuntimeError("Invalid procedure call or argument (method is invalid).");
        }
        if ("".equals(url)) {
            throw Context.reportRuntimeError("Invalid procedure call or argument (url is empty).");
        }

        // defaults to true if not specified
        boolean async = true;
        if (asyncParam != Undefined.instance) {
            async = ScriptRuntime.toBoolean(asyncParam);
        }

        final String urlAsString = Context.toString(url);

        // (URL + Method + User + Password) become a WebRequest instance.
        containingPage_ = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();

        try {
            final URL fullUrl = containingPage_.getFullyQualifiedUrl(urlAsString);

            final WebRequest request = new WebRequest(fullUrl);
            request.setCharset("UTF-8");
            request.setAdditionalHeader("Referer", containingPage_.getUrl().toExternalForm());

            request.setHttpMethod(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)));

            // password is ignored if no user defined
            final boolean userIsNull = null == user || Undefined.instance == user;
            if (!userIsNull) {
                final String userCred = user.toString();

                final boolean passwordIsNull = null == password || Undefined.instance == password;
                String passwordCred = "";
                if (!passwordIsNull) {
                    passwordCred = password.toString();
                }

                request.setCredentials(new UsernamePasswordCredentials(userCred, passwordCred));
            }
            webRequest_ = request;
        } catch (final MalformedURLException e) {
            LOG.error("Unable to initialize XMLHTTPRequest using malformed URL '" + urlAsString + "'.");
            return;
        } catch (final IllegalArgumentException e) {
            LOG.error("Unable to initialize XMLHTTPRequest using illegal argument '" + e.getMessage() + "'.");
            webRequest_ = null;
        }
        // Async stays a boolean.
        async_ = async;
        // Change the state!
        setState(STATE_OPENED, null);
    }

    /**
     * Sends an HTTP request to the server and receives a response.
     * @param body the body of the message being sent with the request.
     */
    @JsxFunction
    public void send(final Object body) {
        if (webRequest_ == null) {
            setState(STATE_DONE, Context.getCurrentContext());
            return;
        }
        if (sent_) {
            throw Context.reportRuntimeError("Unspecified error (request already sent).");
        }
        sent_ = true;

        prepareRequest(body);

        // quite strange but IE seems to fire state loading twice
        setState(STATE_OPENED, Context.getCurrentContext());

        final WebClient client = getWindow().getWebWindow().getWebClient();
        final AjaxController ajaxController = client.getAjaxController();
        final HtmlPage page = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
        final boolean synchron = ajaxController.processSynchron(page, webRequest_, async_);
        if (synchron) {
            doSend(Context.getCurrentContext());
        } else {
            // Create and start a thread in which to execute the request.
            final Scriptable startingScope = getWindow();
            final ContextFactory cf = client.getJavaScriptEngine().getContextFactory();
            final ContextAction action = new ContextAction() {
                @Override
                public Object run(final Context cx) {
                    // KEY_STARTING_SCOPE maintains a stack of scopes
                    @SuppressWarnings("unchecked")
                    Stack<Scriptable> stack = (Stack<Scriptable>) cx
                            .getThreadLocal(JavaScriptEngine.KEY_STARTING_SCOPE);
                    if (null == stack) {
                        stack = new Stack<>();
                        cx.putThreadLocal(JavaScriptEngine.KEY_STARTING_SCOPE, stack);
                    }
                    stack.push(startingScope);

                    try {
                        doSend(cx);
                    } finally {
                        stack.pop();
                    }
                    return null;
                }
            };
            final JavaScriptJob job = BackgroundJavaScriptFactory.theFactory().createJavascriptXMLHttpRequestJob(cf,
                    action);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Starting XMLHTTPRequest thread for asynchronous request");
            }
            jobID_ = getWindow().getWebWindow().getJobManager().addJob(job, page);
        }
    }

    /**
     * Prepares the WebRequest that will be sent.
     * @param content the content to send
     */
    private void prepareRequest(final Object content) {
        if (content != null && !Context.getUndefinedValue().equals(content)) {
            if (!"".equals(content) && HttpMethod.GET == webRequest_.getHttpMethod()) {
                webRequest_.setHttpMethod(HttpMethod.POST);
            }
            if (HttpMethod.POST == webRequest_.getHttpMethod() || HttpMethod.PUT == webRequest_.getHttpMethod()
                    || HttpMethod.PATCH == webRequest_.getHttpMethod()) {
                if (content instanceof FormData) {
                    ((FormData) content).fillRequest(webRequest_);
                } else {
                    final String body = Context.toString(content);
                    if (!body.isEmpty()) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Setting request body to: " + body);
                        }
                        webRequest_.setRequestBody(body);
                    }
                }
            }
        }
    }

    /**
     * The real send job.
     * @param context the current context
     */
    private void doSend(final Context context) {
        final WebClient wc = getWindow().getWebWindow().getWebClient();
        try {
            final String originHeaderValue = webRequest_.getAdditionalHeaders().get(HEADER_ORIGIN);
            final boolean crossOriginResourceSharing = originHeaderValue != null;
            if (crossOriginResourceSharing && isPreflight()) {
                final WebRequest preflightRequest = new WebRequest(webRequest_.getUrl(), HttpMethod.OPTIONS);

                // header origin
                preflightRequest.setAdditionalHeader(HEADER_ORIGIN, originHeaderValue);

                // header request-method
                preflightRequest.setAdditionalHeader(HEADER_ACCESS_CONTROL_REQUEST_METHOD,
                        webRequest_.getHttpMethod().name());

                // header request-headers
                final StringBuilder builder = new StringBuilder();
                for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
                    final String name = header.getKey().toLowerCase(Locale.ROOT);
                    if (isPreflightHeader(name, header.getValue())) {
                        if (builder.length() != 0) {
                            builder.append(REQUEST_HEADERS_SEPARATOR);
                        }
                        builder.append(name);
                    }
                }
                preflightRequest.setAdditionalHeader(HEADER_ACCESS_CONTROL_REQUEST_HEADERS, builder.toString());

                // do the preflight request
                final WebResponse preflightResponse = wc.loadWebResponse(preflightRequest);
                if (!isPreflightAuthorized(preflightResponse)) {
                    setState(STATE_HEADERS_RECEIVED, context);
                    setState(STATE_LOADING, context);
                    setState(STATE_DONE, context);
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("No permitted request for URL " + webRequest_.getUrl());
                    }
                    Context.throwAsScriptRuntimeEx(
                            new RuntimeException("No permitted \"Access-Control-Allow-Origin\" header."));
                    return;
                }
            }
            final WebResponse webResponse = wc.loadWebResponse(webRequest_);
            if (LOG.isDebugEnabled()) {
                LOG.debug("Web response loaded successfully.");
            }
            boolean allowOriginResponse = true;
            if (crossOriginResourceSharing) {
                final String value = webResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN);
                allowOriginResponse = originHeaderValue.equals(value);
                allowOriginResponse = allowOriginResponse || ALLOW_ORIGIN_ALL.equals(value);
            }
            if (allowOriginResponse) {
                webResponse_ = webResponse;
            }
            if (allowOriginResponse) {
                setState(STATE_HEADERS_RECEIVED, context);
                setState(STATE_LOADING, context);
                setState(STATE_DONE, context);
            } else {
                if (LOG.isDebugEnabled()) {
                    LOG.debug(
                            "No permitted \"Access-Control-Allow-Origin\" header for URL " + webRequest_.getUrl());
                }
                throw new IOException("No permitted \"Access-Control-Allow-Origin\" header.");
            }
        } catch (final IOException e) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("IOException: returning a network error response.", e);
            }
            webResponse_ = new NetworkErrorWebResponse(webRequest_);
            setState(STATE_HEADERS_RECEIVED, context);
            setState(STATE_DONE, context);
            if (!async_) {
                throw Context.reportRuntimeError("Object not found.");
            }
        }
    }

    private boolean isPreflight() {
        final HttpMethod method = webRequest_.getHttpMethod();
        if (method != HttpMethod.GET && method != HttpMethod.HEAD && method != HttpMethod.POST) {
            return true;
        }
        for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
            if (isPreflightHeader(header.getKey().toLowerCase(Locale.ROOT), header.getValue())) {
                return true;
            }
        }
        return false;
    }

    private boolean isPreflightAuthorized(final WebResponse preflightResponse) {
        final String originHeader = preflightResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN);
        if (!ALLOW_ORIGIN_ALL.equals(originHeader)
                && !webRequest_.getAdditionalHeaders().get(HEADER_ORIGIN).equals(originHeader)) {
            return false;
        }
        String headersHeader = preflightResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_HEADERS);
        if (headersHeader == null) {
            headersHeader = "";
        } else {
            headersHeader = headersHeader.toLowerCase(Locale.ROOT);
        }
        for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
            final String key = header.getKey().toLowerCase(Locale.ROOT);
            if (isPreflightHeader(key, header.getValue()) && !headersHeader.contains(key)) {
                return false;
            }
        }
        return true;
    }

    /**
     * @param name header name (MUST be lower-case for performance reasons)
     * @param value header value
     */
    private boolean isPreflightHeader(final String name, final String value) {
        if ("content-type".equals(name)) {
            final String lcValue = value.toLowerCase(Locale.ROOT);
            if (FormEncodingType.URL_ENCODED.getName().equals(lcValue)
                    || FormEncodingType.MULTIPART.getName().equals(lcValue) || "text/plain".equals(lcValue)
                    || lcValue.startsWith("text/plain;charset=")) {
                return false;
            }
            return true;
        }
        if ("accept".equals(name) || "accept-language".equals(name) || "content-language".equals(name)
                || "referer".equals(name) || "accept-encoding".equals(name) || "origin".equals(name)) {
            return false;
        }
        return true;
    }

    /**
     * Sets the specified header to the specified value.<br>
     * The <code>open</code> method must be called before this method, or an error will occur.
     * @param name the header name to set
     * @param value the value of the header
     */
    @JsxFunction
    public void setRequestHeader(final String name, final String value) {
        if (name == null || "null".equals(name)) {
            throw Context.reportRuntimeError("Type mismatch (name is null).");
        }
        if ("".equals(name)) {
            throw Context.reportRuntimeError("Invalid argument (name is empty).");
        }
        if (value == null || "null".equals(value)) {
            throw Context.reportRuntimeError("Type mismatch (value is null).");
        }
        if (StringUtils.isBlank(value)) {
            return;
        }
        if (!isAuthorizedHeader(name)) {
            LOG.warn("Ignoring XMLHTTPRequest.setRequestHeader for " + name + ": it is a restricted header");
            return;
        }

        if (webRequest_ == null) {
            throw Context.reportRuntimeError("Unspecified error (request not opened).");
        }
        webRequest_.setAdditionalHeader(name, value);
    }

    /**
     * Not all request headers can be set from JavaScript.
     * @see <a href="http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method">W3C doc</a>
     * @param name the header name
     * @return {@code true} if the header can be set from JavaScript
     */
    static boolean isAuthorizedHeader(final String name) {
        final String nameLowerCase = name.toLowerCase(Locale.ROOT);
        if (PROHIBITED_HEADERS_.contains(nameLowerCase)) {
            return false;
        } else if (nameLowerCase.startsWith("proxy-") || nameLowerCase.startsWith("sec-")) {
            return false;
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object get(String name, final Scriptable start) {
        for (final String property : ALL_PROPERTIES_) {
            if (property.equalsIgnoreCase(name)) {
                name = property;
                break;
            }
        }
        return super.get(name, start);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void put(String name, final Scriptable start, final Object value) {
        for (final String property : ALL_PROPERTIES_) {
            if (property.equalsIgnoreCase(name)) {
                name = property;
                break;
            }
        }
        super.put(name, start, value);
    }

    private static final class NetworkErrorWebResponse extends WebResponse {
        private final WebRequest request_;

        private NetworkErrorWebResponse(final WebRequest webRequest) {
            super(null, null, 0);
            request_ = webRequest;
        }

        @Override
        public int getStatusCode() {
            return 0;
        }

        @Override
        public String getStatusMessage() {
            return "";
        }

        @Override
        public String getContentType() {
            return "";
        }

        @Override
        public String getContentAsString() {
            return "";
        }

        @Override
        public String getContentAsString(final String encoding) {
            return "";
        }

        @Override
        public InputStream getContentAsStream() {
            return null;
        }

        @Override
        public List<NameValuePair> getResponseHeaders() {
            return Collections.emptyList();
        }

        @Override
        public String getResponseHeaderValue(final String headerName) {
            return "";
        }

        @Override
        public long getLoadTime() {
            return 0;
        }

        @Override
        public String getContentCharset() {
            return "";
        }

        @Override
        public String getContentCharsetOrNull() {
            return "";
        }

        @Override
        public WebRequest getWebRequest() {
            return request_;
        }
    }
}