com.gargoylesoftware.htmlunit.activex.javascript.msxml.XMLHTTPRequest Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of htmlunit Show documentation
Show all versions of htmlunit Show documentation
A headless browser intended for use in testing web-based applications.
/*
* Copyright (c) 2002-2021 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.SupportedBrowser.IE;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
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.HttpHeader;
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.host.Window;
import com.gargoylesoftware.htmlunit.javascript.host.xml.FormData;
import com.gargoylesoftware.htmlunit.util.MimeType;
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.
* Provides client-side protocol support for communication with HTTP servers.
* @see MSDN documentation
*
* @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(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 char REQUEST_HEADERS_SEPARATOR = ',';
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 PROHIBITED_HEADERS_ = Arrays.asList(
"accept-charset", HttpHeader.ACCEPT_ENCODING_LC,
HttpHeader.CONNECTION_LC, HttpHeader.CONTENT_LENGTH_LC, HttpHeader.COOKIE_LC, "cookie2",
"content-transfer-encoding", "date", "expect",
HttpHeader.HOST_LC, "keep-alive", HttpHeader.REFERER_LC, "te", "trailer", "transfer-encoding", "upgrade",
HttpHeader.USER_AGENT_LC, "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 readyState
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 readyState
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 = (JavaScriptEngine) 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:
*
* - 0 = unsent
* - 1 = opened
* - 2 = headers_received
* - 3 = loading
* - 4 = done
*
* @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) {
final String content = webResponse_.getContentAsString();
if (content == null) {
return "";
}
return content;
}
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).");
}
final Window w = getWindow();
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_, w.getWebWindow(), true, false);
final XMLDOMDocument doc = new XMLDOMDocument();
doc.setDomNode(page);
doc.setPrototype(getPrototype(doc.getClass()));
doc.setEnvironment(getEnvironment());
doc.setParentScope(w);
return doc;
}
catch (final IOException e) {
if (LOG.isWarnEnabled()) {
LOG.warn("Failed parsing XML document " + webResponse_.getWebRequest().getUrl() + ": "
+ e.getMessage());
}
return null;
}
}
}
final XMLDOMDocument doc = new XMLDOMDocument(w.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();
}
if (LOG.isErrorEnabled()) {
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();
}
if (LOG.isErrorEnabled()) {
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 builder = new StringBuilder();
for (final NameValuePair header : webResponse_.getResponseHeaders()) {
builder.append(header.getName()).append(": ").append(header.getValue()).append("\r\n");
}
return builder.append("\r\n").toString();
}
if (LOG.isErrorEnabled()) {
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;
}
if (LOG.isErrorEnabled()) {
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 onreadystatechange
property
* callback so that you can tell when the send
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 (!Undefined.isUndefined(asyncParam)) {
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(HttpHeader.REFERER, containingPage_.getUrl().toExternalForm());
request.setHttpMethod(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)));
// password is ignored if no user defined
if (user != null && !Undefined.isUndefined(user)) {
final String userCred = user.toString();
String passwordCred = "";
if (password != null && !Undefined.isUndefined(password)) {
passwordCred = password.toString();
}
request.setCredentials(new UsernamePasswordCredentials(userCred, passwordCred));
}
webRequest_ = request;
}
catch (final MalformedURLException e) {
if (LOG.isErrorEnabled()) {
LOG.error("Unable to initialize XMLHTTPRequest using malformed URL '" + urlAsString + "'.");
}
return;
}
catch (final IllegalArgumentException e) {
if (LOG.isErrorEnabled()) {
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 Window w = getWindow();
final WebClient client = w.getWebWindow().getWebClient();
final AjaxController ajaxController = client.getAjaxController();
final HtmlPage page = (HtmlPage) w.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 = w;
final ContextFactory cf = ((JavaScriptEngine) client.getJavaScriptEngine()).getContextFactory();
final ContextAction