org.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 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.
/*
* Copyright (c) 2002-2023 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.activex.javascript.msxml;
import static org.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 org.htmlunit.AjaxController;
import org.htmlunit.FormEncodingType;
import org.htmlunit.HttpHeader;
import org.htmlunit.HttpMethod;
import org.htmlunit.WebClient;
import org.htmlunit.WebRequest;
import org.htmlunit.WebResponse;
import org.htmlunit.html.HtmlPage;
import org.htmlunit.javascript.JavaScriptEngine;
import org.htmlunit.javascript.background.BackgroundJavaScriptFactory;
import org.htmlunit.javascript.background.JavaScriptJob;
import org.htmlunit.javascript.configuration.JsxClass;
import org.htmlunit.javascript.configuration.JsxConstructor;
import org.htmlunit.javascript.configuration.JsxFunction;
import org.htmlunit.javascript.configuration.JsxGetter;
import org.htmlunit.javascript.configuration.JsxSetter;
import org.htmlunit.javascript.host.Window;
import org.htmlunit.javascript.host.xml.FormData;
import org.htmlunit.util.MimeType;
import org.htmlunit.util.NameValuePair;
import org.htmlunit.xml.XmlPage;
import org.htmlunit.corejs.javascript.Context;
import org.htmlunit.corejs.javascript.ContextAction;
import org.htmlunit.corejs.javascript.ContextFactory;
import org.htmlunit.corejs.javascript.Function;
import org.htmlunit.corejs.javascript.ScriptRuntime;
import org.htmlunit.corejs.javascript.Scriptable;
import org.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 final 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, UTF_8, containingPage_.getUrl());
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 ContextFactory cf = ((JavaScriptEngine) client.getJavaScriptEngine()).getContextFactory();
final ContextAction