View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.host.xml;
16  
17  import static java.nio.charset.StandardCharsets.UTF_8;
18  import static org.htmlunit.BrowserVersionFeatures.XHR_HANDLE_SYNC_NETWORK_ERRORS;
19  import static org.htmlunit.BrowserVersionFeatures.XHR_LOAD_ALWAYS_AFTER_DONE;
20  import static org.htmlunit.BrowserVersionFeatures.XHR_RESPONSE_TEXT_EMPTY_UNSENT;
21  import static org.htmlunit.BrowserVersionFeatures.XHR_SEND_NETWORK_ERROR_IF_ABORTED;
22  
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.StringWriter;
26  import java.net.MalformedURLException;
27  import java.net.SocketTimeoutException;
28  import java.net.URL;
29  import java.nio.charset.Charset;
30  import java.util.Arrays;
31  import java.util.Collections;
32  import java.util.HashSet;
33  import java.util.List;
34  import java.util.Locale;
35  import java.util.Map.Entry;
36  import java.util.TreeMap;
37  
38  import javax.xml.transform.OutputKeys;
39  import javax.xml.transform.Transformer;
40  import javax.xml.transform.TransformerFactory;
41  import javax.xml.transform.dom.DOMSource;
42  import javax.xml.transform.stream.StreamResult;
43  
44  import org.apache.commons.io.IOUtils;
45  import org.apache.commons.logging.Log;
46  import org.apache.commons.logging.LogFactory;
47  import org.htmlunit.AjaxController;
48  import org.htmlunit.BrowserVersion;
49  import org.htmlunit.FormEncodingType;
50  import org.htmlunit.HttpHeader;
51  import org.htmlunit.HttpMethod;
52  import org.htmlunit.SgmlPage;
53  import org.htmlunit.WebClient;
54  import org.htmlunit.WebRequest;
55  import org.htmlunit.WebRequest.HttpHint;
56  import org.htmlunit.WebResponse;
57  import org.htmlunit.WebWindow;
58  import org.htmlunit.corejs.javascript.Context;
59  import org.htmlunit.corejs.javascript.ContextAction;
60  import org.htmlunit.corejs.javascript.Function;
61  import org.htmlunit.corejs.javascript.ScriptableObject;
62  import org.htmlunit.corejs.javascript.json.JsonParser;
63  import org.htmlunit.corejs.javascript.json.JsonParser.ParseException;
64  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBuffer;
65  import org.htmlunit.corejs.javascript.typedarrays.NativeArrayBufferView;
66  import org.htmlunit.html.HtmlPage;
67  import org.htmlunit.httpclient.HtmlUnitUsernamePasswordCredentials;
68  import org.htmlunit.javascript.HtmlUnitContextFactory;
69  import org.htmlunit.javascript.JavaScriptEngine;
70  import org.htmlunit.javascript.background.BackgroundJavaScriptFactory;
71  import org.htmlunit.javascript.background.JavaScriptJob;
72  import org.htmlunit.javascript.configuration.JsxClass;
73  import org.htmlunit.javascript.configuration.JsxConstant;
74  import org.htmlunit.javascript.configuration.JsxConstructor;
75  import org.htmlunit.javascript.configuration.JsxFunction;
76  import org.htmlunit.javascript.configuration.JsxGetter;
77  import org.htmlunit.javascript.configuration.JsxSetter;
78  import org.htmlunit.javascript.host.Element;
79  import org.htmlunit.javascript.host.URLSearchParams;
80  import org.htmlunit.javascript.host.Window;
81  import org.htmlunit.javascript.host.dom.DOMException;
82  import org.htmlunit.javascript.host.dom.DOMParser;
83  import org.htmlunit.javascript.host.dom.Document;
84  import org.htmlunit.javascript.host.event.Event;
85  import org.htmlunit.javascript.host.event.ProgressEvent;
86  import org.htmlunit.javascript.host.file.Blob;
87  import org.htmlunit.javascript.host.html.HTMLDocument;
88  import org.htmlunit.util.EncodingSniffer;
89  import org.htmlunit.util.MimeType;
90  import org.htmlunit.util.NameValuePair;
91  import org.htmlunit.util.StringUtils;
92  import org.htmlunit.util.UrlUtils;
93  import org.htmlunit.util.WebResponseWrapper;
94  import org.htmlunit.util.XUserDefinedCharset;
95  import org.htmlunit.xml.XmlPage;
96  import org.w3c.dom.DocumentType;
97  
98  /**
99   * A JavaScript object for an {@code XMLHttpRequest}.
100  *
101  * @author Daniel Gredler
102  * @author Marc Guillemot
103  * @author Ahmed Ashour
104  * @author Stuart Begg
105  * @author Ronald Brill
106  * @author Sebastian Cato
107  * @author Frank Danek
108  * @author Jake Cobb
109  * @author Thorsten Wendelmuth
110  * @author Lai Quang Duong
111  * @author Sven Strickroth
112  *
113  * @see <a href="http://www.w3.org/TR/XMLHttpRequest/">W3C XMLHttpRequest</a>
114  * @see <a href="http://developer.apple.com/internet/webcontent/xmlhttpreq.html">Safari documentation</a>
115  */
116 @JsxClass
117 public class XMLHttpRequest extends XMLHttpRequestEventTarget {
118 
119     private static final Log LOG = LogFactory.getLog(XMLHttpRequest.class);
120 
121     /** The object has been created, but not initialized (the open() method has not been called). */
122     @JsxConstant
123     public static final int UNSENT = 0;
124 
125     /** The object has been created, but the send() method has not been called. */
126     @JsxConstant
127     public static final int OPENED = 1;
128 
129     /** The send() method has been called, but the status and headers are not yet available. */
130     @JsxConstant
131     public static final int HEADERS_RECEIVED = 2;
132 
133     /** Some data has been received. */
134     @JsxConstant
135     public static final int LOADING = 3;
136 
137     /** All the data has been received; the complete data is available in responseBody and responseText. */
138     @JsxConstant
139     public static final int DONE = 4;
140 
141     private static final String RESPONSE_TYPE_DEFAULT = "";
142     private static final String RESPONSE_TYPE_ARRAYBUFFER = "arraybuffer";
143     private static final String RESPONSE_TYPE_BLOB = "blob";
144     private static final String RESPONSE_TYPE_DOCUMENT = "document";
145     private static final String RESPONSE_TYPE_JSON = "json";
146     private static final String RESPONSE_TYPE_TEXT = "text";
147 
148     private static final String ALLOW_ORIGIN_ALL = "*";
149 
150     private static final HashSet<String> PROHIBITED_HEADERS_ = new HashSet<>(Arrays.asList(
151         "accept-charset", HttpHeader.ACCEPT_ENCODING_LC,
152         HttpHeader.CONNECTION_LC, HttpHeader.CONTENT_LENGTH_LC, HttpHeader.COOKIE_LC, "cookie2",
153         "content-transfer-encoding", "date", "expect",
154         HttpHeader.HOST_LC, "keep-alive", HttpHeader.REFERER_LC, "te", "trailer", "transfer-encoding",
155         "upgrade", HttpHeader.USER_AGENT_LC, "via"));
156 
157     private int state_;
158     private WebRequest webRequest_;
159     private boolean async_;
160     private int jobID_;
161     private WebResponse webResponse_;
162     private String overriddenMimeType_;
163     private boolean withCredentials_;
164     private boolean isSameOrigin_;
165     private int timeout_;
166     private boolean aborted_;
167     private String responseType_;
168 
169     private Document responseXML_;
170 
171     /**
172      * Creates a new instance.
173      */
174     public XMLHttpRequest() {
175         state_ = UNSENT;
176         responseType_ = RESPONSE_TYPE_DEFAULT;
177     }
178 
179     /**
180      * JavaScript constructor.
181      */
182     @Override
183     @JsxConstructor
184     public void jsConstructor() {
185         // don't call super here
186     }
187 
188     /**
189      * Sets the state as specified and invokes the state change handler if one has been set.
190      * @param state the new state
191      */
192     private void setState(final int state) {
193         if (state == UNSENT
194                 || state == OPENED
195                 || state == HEADERS_RECEIVED
196                 || state == LOADING
197                 || state == DONE) {
198             state_ = state;
199             if (LOG.isDebugEnabled()) {
200                 LOG.debug("State changed to : " + state);
201             }
202             return;
203         }
204 
205         LOG.error("Received an unknown state " + state
206                         + ", the state is not implemented, please check setState() implementation.");
207     }
208 
209     private void fireJavascriptEvent(final String eventName) {
210         if (aborted_) {
211             if (LOG.isDebugEnabled()) {
212                 LOG.debug("Firing javascript XHR event: " + eventName + " for an already aborted request - ignored.");
213             }
214 
215             return;
216         }
217         fireJavascriptEventIgnoreAbort(eventName);
218     }
219 
220     private void fireJavascriptEventIgnoreAbort(final String eventName) {
221         if (LOG.isDebugEnabled()) {
222             LOG.debug("Firing javascript XHR event: " + eventName);
223         }
224 
225         final boolean isReadyStateChange = Event.TYPE_READY_STATE_CHANGE.equalsIgnoreCase(eventName);
226         final Event event;
227         if (isReadyStateChange) {
228             event = new Event(this, Event.TYPE_READY_STATE_CHANGE);
229         }
230         else {
231             final ProgressEvent progressEvent = new ProgressEvent(this, eventName);
232 
233             if (webResponse_ != null) {
234                 final long contentLength = webResponse_.getContentLength();
235                 progressEvent.setLoaded(contentLength);
236             }
237             event = progressEvent;
238         }
239 
240         executeEventLocally(event);
241     }
242 
243     /**
244      * Returns the current state of the HTTP request. The possible values are:
245      * <ul>
246      *   <li>0 = unsent</li>
247      *   <li>1 = opened</li>
248      *   <li>2 = headers_received</li>
249      *   <li>3 = loading</li>
250      *   <li>4 = done</li>
251      * </ul>
252      * @return the current state of the HTTP request
253      */
254     @JsxGetter
255     public int getReadyState() {
256         return state_;
257     }
258 
259     /**
260      * @return the {@code responseType} property
261      */
262     @JsxGetter
263     public String getResponseType() {
264         return responseType_;
265     }
266 
267     /**
268      * Sets the {@code responseType} property.
269      * @param responseType the {@code responseType} property.
270      */
271     @JsxSetter
272     public void setResponseType(final String responseType) {
273         if (state_ == LOADING || state_ == DONE) {
274             throw JavaScriptEngine.reportRuntimeError("InvalidStateError");
275         }
276 
277         if (RESPONSE_TYPE_DEFAULT.equals(responseType)
278                 || RESPONSE_TYPE_ARRAYBUFFER.equals(responseType)
279                 || RESPONSE_TYPE_BLOB.equals(responseType)
280                 || RESPONSE_TYPE_DOCUMENT.equals(responseType)
281                 || RESPONSE_TYPE_JSON.equals(responseType)
282                 || RESPONSE_TYPE_TEXT.equals(responseType)) {
283 
284             if (state_ == OPENED && !async_) {
285                 throw JavaScriptEngine.asJavaScriptException(
286                         getWindow(),
287                         "synchronous XMLHttpRequests do not support responseType",
288                         DOMException.INVALID_ACCESS_ERR);
289             }
290 
291             responseType_ = responseType;
292         }
293     }
294 
295     /**
296      * @return returns the response's body content as an ArrayBuffer, Blob, Document, JavaScript Object,
297      *         or DOMString, depending on the value of the request's responseType property.
298      */
299     @JsxGetter
300     public Object getResponse() {
301         if (RESPONSE_TYPE_DEFAULT.equals(responseType_) || RESPONSE_TYPE_TEXT.equals(responseType_)) {
302             if (webResponse_ != null) {
303                 final Charset encoding = webResponse_.getContentCharset();
304                 final String content = webResponse_.getContentAsString(encoding);
305                 if (content == null) {
306                     return "";
307                 }
308                 return content;
309             }
310         }
311 
312         if (state_ != DONE) {
313             return null;
314         }
315 
316         if (webResponse_ instanceof NetworkErrorWebResponse) {
317             if (LOG.isDebugEnabled()) {
318                 LOG.debug("XMLHttpRequest.responseXML returns of a network error ("
319                         + ((NetworkErrorWebResponse) webResponse_).getError() + ")");
320             }
321             return null;
322         }
323 
324         if (RESPONSE_TYPE_ARRAYBUFFER.equals(responseType_)) {
325             long contentLength = webResponse_.getContentLength();
326             NativeArrayBuffer nativeArrayBuffer = new NativeArrayBuffer(contentLength);
327 
328             try {
329                 final int bufferLength = Math.min(1024, (int) contentLength);
330                 final byte[] buffer = new byte[bufferLength];
331                 int offset = 0;
332                 try (InputStream inputStream = webResponse_.getContentAsStream()) {
333                     int readLen;
334                     while ((readLen = inputStream.read(buffer, 0, bufferLength)) != -1) {
335                         final long newLength = offset + readLen;
336                         // gzip content and the unzipped content is larger
337                         if (newLength > contentLength) {
338                             final NativeArrayBuffer expanded = new NativeArrayBuffer(newLength);
339                             System.arraycopy(nativeArrayBuffer.getBuffer(), 0,
340                                     expanded.getBuffer(), 0, (int) contentLength);
341                             contentLength = newLength;
342                             nativeArrayBuffer = expanded;
343                         }
344                         System.arraycopy(buffer, 0, nativeArrayBuffer.getBuffer(), offset, readLen);
345                         offset = (int) newLength;
346                     }
347                 }
348 
349                 // for small responses the gzipped content might be larger than the original
350                 if (offset < contentLength) {
351                     final NativeArrayBuffer shrinked = new NativeArrayBuffer(offset);
352                     System.arraycopy(nativeArrayBuffer.getBuffer(), 0, shrinked.getBuffer(), 0, offset);
353                     nativeArrayBuffer = shrinked;
354                 }
355 
356                 nativeArrayBuffer.setParentScope(getParentScope());
357                 nativeArrayBuffer.setPrototype(
358                         ScriptableObject.getClassPrototype(getWindow(), nativeArrayBuffer.getClassName()));
359 
360                 return nativeArrayBuffer;
361             }
362             catch (final IOException e) {
363                 webResponse_ = new NetworkErrorWebResponse(webRequest_, e);
364                 return null;
365             }
366         }
367         else if (RESPONSE_TYPE_BLOB.equals(responseType_)) {
368             try {
369                 if (webResponse_ != null) {
370                     try (InputStream inputStream = webResponse_.getContentAsStream()) {
371                         final Blob blob = new Blob(IOUtils.toByteArray(inputStream), webResponse_.getContentType());
372                         blob.setParentScope(getParentScope());
373                         blob.setPrototype(ScriptableObject.getClassPrototype(getWindow(), blob.getClassName()));
374 
375                         return blob;
376                     }
377                 }
378             }
379             catch (final IOException e) {
380                 webResponse_ = new NetworkErrorWebResponse(webRequest_, e);
381                 return null;
382             }
383         }
384         else if (RESPONSE_TYPE_DOCUMENT.equals(responseType_)) {
385             if (responseXML_ != null) {
386                 return responseXML_;
387             }
388 
389             if (webResponse_ != null) {
390                 String contentType = webResponse_.getContentType();
391                 if (org.htmlunit.util.StringUtils.isEmptyOrNull(contentType)) {
392                     contentType = MimeType.TEXT_XML;
393                 }
394                 return buildResponseXML(contentType);
395             }
396         }
397         else if (RESPONSE_TYPE_JSON.equals(responseType_)) {
398             if (webResponse_ != null) {
399                 final Charset encoding = webResponse_.getContentCharset();
400                 final String content = webResponse_.getContentAsString(encoding);
401                 if (content == null) {
402                     return null;
403                 }
404 
405                 try {
406                     return new JsonParser(Context.getCurrentContext(), this).parseValue(content);
407                 }
408                 catch (final ParseException e) {
409                     webResponse_ = new NetworkErrorWebResponse(webRequest_, new IOException(e));
410                     return null;
411                 }
412             }
413         }
414 
415         return "";
416     }
417 
418     private Document buildResponseXML(final String contentType) {
419         try {
420             if (MimeType.TEXT_XML.equals(contentType)
421                     || MimeType.APPLICATION_XML.equals(contentType)
422                     || MimeType.APPLICATION_XHTML.equals(contentType)
423                     || "image/svg+xml".equals(contentType)) {
424                 final XMLDocument document = new XMLDocument();
425                 document.setParentScope(getParentScope());
426                 document.setPrototype(getPrototype(XMLDocument.class));
427                 final XmlPage page = new XmlPage(webResponse_, getWindow().getWebWindow(), false);
428                 if (!page.hasChildNodes()) {
429                     return null;
430                 }
431                 document.setDomNode(page);
432                 responseXML_ = document;
433                 return responseXML_;
434             }
435 
436             if (MimeType.TEXT_HTML.equals(contentType)) {
437                 responseXML_ = DOMParser.parseHtmlDocument(this, webResponse_, getWindow().getWebWindow());
438                 return responseXML_;
439             }
440             return null;
441         }
442         catch (final IOException e) {
443             webResponse_ = new NetworkErrorWebResponse(webRequest_, e);
444             return null;
445         }
446     }
447 
448     /**
449      * Returns a string version of the data retrieved from the server.
450      * @return a string version of the data retrieved from the server
451      */
452     @JsxGetter
453     public String getResponseText() {
454         if ((state_ == UNSENT || state_ == OPENED) && getBrowserVersion().hasFeature(XHR_RESPONSE_TEXT_EMPTY_UNSENT)) {
455             return "";
456         }
457 
458         if (!RESPONSE_TYPE_DEFAULT.equals(responseType_) && !RESPONSE_TYPE_TEXT.equals(responseType_)) {
459             throw JavaScriptEngine.asJavaScriptException(
460                     getWindow(),
461                     "InvalidStateError: Failed to read the 'responseText' property from 'XMLHttpRequest': "
462                             + "The value is only accessible if the object's 'responseType' is '' or 'text' "
463                             + "(was '" + getResponseType() + "').",
464                     DOMException.INVALID_STATE_ERR);
465         }
466 
467         if (state_ == UNSENT || state_ == OPENED) {
468             return "";
469         }
470 
471         if (webResponse_ instanceof NetworkErrorWebResponse) {
472             if (LOG.isDebugEnabled()) {
473                 LOG.debug("XMLHttpRequest.responseXML returns of a network error ("
474                         + ((NetworkErrorWebResponse) webResponse_).getError() + ")");
475             }
476 
477             final NetworkErrorWebResponse resp = (NetworkErrorWebResponse) webResponse_;
478             if (resp.getError() instanceof NoPermittedHeaderException) {
479                 return "";
480             }
481             return null;
482         }
483 
484         if (webResponse_ != null) {
485             final Charset encoding = webResponse_.getContentCharset();
486             final String content = webResponse_.getContentAsString(encoding);
487             if (content == null) {
488                 return "";
489             }
490             return content;
491         }
492 
493         LOG.debug("XMLHttpRequest.responseText was retrieved before the response was available.");
494         return "";
495     }
496 
497     /**
498      * Returns a DOM-compatible document object version of the data retrieved from the server.
499      * @return a DOM-compatible document object version of the data retrieved from the server
500      */
501     @JsxGetter
502     public Object getResponseXML() {
503         if (responseXML_ != null) {
504             return responseXML_;
505         }
506 
507         if (webResponse_ == null) {
508             if (LOG.isDebugEnabled()) {
509                 LOG.debug("XMLHttpRequest.responseXML returns null because there "
510                         + "in no web resonse so far (has send() been called?)");
511             }
512             return null;
513         }
514 
515         if (webResponse_ instanceof NetworkErrorWebResponse) {
516             if (LOG.isDebugEnabled()) {
517                 LOG.debug("XMLHttpRequest.responseXML returns of a network error ("
518                         + ((NetworkErrorWebResponse) webResponse_).getError() + ")");
519             }
520             return null;
521         }
522 
523         String contentType = webResponse_.getContentType();
524         if (org.htmlunit.util.StringUtils.isEmptyOrNull(contentType)) {
525             contentType = MimeType.TEXT_XML;
526         }
527 
528         if (MimeType.TEXT_HTML.equalsIgnoreCase(contentType)) {
529             if (!async_ || !RESPONSE_TYPE_DOCUMENT.equals(responseType_)) {
530                 return null;
531             }
532         }
533 
534         return buildResponseXML(contentType);
535     }
536 
537     /**
538      * Returns the numeric status returned by the server, such as 404 for "Not Found"
539      * or 200 for "OK".
540      * @return the numeric status returned by the server
541      */
542     @JsxGetter
543     public int getStatus() {
544         if (state_ == UNSENT || state_ == OPENED) {
545             return 0;
546         }
547         if (webResponse_ != null) {
548             return webResponse_.getStatusCode();
549         }
550 
551         if (LOG.isErrorEnabled()) {
552             LOG.error("XMLHttpRequest.status was retrieved without a response available (readyState: "
553                 + state_ + ").");
554         }
555         return 0;
556     }
557 
558     /**
559      * Returns the string message accompanying the status code, such as "Not Found" or "OK".
560      * @return the string message accompanying the status code
561      */
562     @JsxGetter
563     public String getStatusText() {
564         if (state_ == UNSENT || state_ == OPENED) {
565             return "";
566         }
567         if (webResponse_ != null) {
568             return webResponse_.getStatusMessage();
569         }
570 
571         if (LOG.isErrorEnabled()) {
572             LOG.error("XMLHttpRequest.statusText was retrieved without a response available (readyState: "
573                 + state_ + ").");
574         }
575         return null;
576     }
577 
578     /**
579      * Cancels the current HTTP request.
580      */
581     @JsxFunction
582     public void abort() {
583         getWindow().getWebWindow().getJobManager().stopJob(jobID_);
584 
585         if (state_ == OPENED
586                 || state_ == HEADERS_RECEIVED
587                 || state_ == LOADING) {
588             setState(DONE);
589             webResponse_ = new NetworkErrorWebResponse(webRequest_, null);
590             fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
591             fireJavascriptEvent(Event.TYPE_ABORT);
592             fireJavascriptEvent(Event.TYPE_LOAD_END);
593         }
594 
595         // JavaScriptEngine.constructError("NetworkError",
596         //         "Failed to execute 'send' on 'XMLHttpRequest': Failed to load '" + webRequest_.getUrl() + "'");
597 
598         setState(UNSENT);
599         webResponse_ = new NetworkErrorWebResponse(webRequest_, null);
600         aborted_ = true;
601     }
602 
603     /**
604      * Returns the labels and values of all the HTTP headers.
605      * @return the labels and values of all the HTTP headers
606      */
607     @JsxFunction
608     public String getAllResponseHeaders() {
609         if (state_ == UNSENT || state_ == OPENED) {
610             return "";
611         }
612         if (webResponse_ != null) {
613             final StringBuilder builder = new StringBuilder();
614             for (final NameValuePair header : webResponse_.getResponseHeaders()) {
615                 builder
616                     .append(header.getName())
617                     .append(": ")
618                     .append(header.getValue())
619                     .append("\r\n");
620             }
621             return builder.toString();
622         }
623 
624         if (LOG.isErrorEnabled()) {
625             LOG.error("XMLHttpRequest.getAllResponseHeaders() was called without a response available (readyState: "
626                 + state_ + ").");
627         }
628         return null;
629     }
630 
631     /**
632      * Retrieves the value of an HTTP header from the response body.
633      * @param headerName the (case-insensitive) name of the header to retrieve
634      * @return the value of the specified HTTP header
635      */
636     @JsxFunction
637     public String getResponseHeader(final String headerName) {
638         if (state_ == UNSENT || state_ == OPENED) {
639             return null;
640         }
641         if (webResponse_ != null) {
642             return webResponse_.getResponseHeaderValue(headerName);
643         }
644 
645         if (LOG.isErrorEnabled()) {
646             LOG.error("XMLHttpRequest.getAllResponseHeaders(..) was called without a response available (readyState: "
647                 + state_ + ").");
648         }
649         return null;
650     }
651 
652     /**
653      * Assigns the destination URL, method and other optional attributes of a pending request.
654      * @param method the method to use to send the request to the server (GET, POST, etc)
655      * @param urlParam the URL to send the request to
656      * @param asyncParam Whether or not to send the request to the server asynchronously, defaults to {@code true}
657      * @param user If authentication is needed for the specified URL, the username to use to authenticate
658      * @param password If authentication is needed for the specified URL, the password to use to authenticate
659      */
660     @JsxFunction
661     public void open(final String method, final Object urlParam, final Object asyncParam,
662         final Object user, final Object password) {
663 
664         // async defaults to true if not specified
665         boolean async = true;
666         if (!JavaScriptEngine.isUndefined(asyncParam)) {
667             async = JavaScriptEngine.toBoolean(asyncParam);
668         }
669 
670         final String url = JavaScriptEngine.toString(urlParam);
671 
672         // (URL + Method + User + Password) become a WebRequest instance.
673         final HtmlPage containingPage = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
674 
675         try {
676             final URL pageUrl = containingPage.getUrl();
677             final URL fullUrl = containingPage.getFullyQualifiedUrl(url);
678             final WebRequest request = new WebRequest(fullUrl, getBrowserVersion().getXmlHttpRequestAcceptHeader(),
679                                                                 getBrowserVersion().getAcceptEncodingHeader());
680             request.setCharset(UTF_8);
681             // https://xhr.spec.whatwg.org/#response-body
682             request.setDefaultResponseContentCharset(UTF_8);
683             request.setRefererHeader(pageUrl);
684 
685             try {
686                 request.setHttpMethod(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)));
687             }
688             catch (final IllegalArgumentException e) {
689                 if (LOG.isInfoEnabled()) {
690                     LOG.info("Incorrect HTTP Method '" + method + "'");
691                 }
692                 return;
693             }
694 
695             final boolean isDataUrl = "data".equals(fullUrl.getProtocol());
696             if (isDataUrl) {
697                 isSameOrigin_ = true;
698             }
699             else {
700                 isSameOrigin_ = UrlUtils.isSameOrigin(pageUrl, fullUrl);
701                 final boolean alwaysAddOrigin = HttpMethod.GET != request.getHttpMethod()
702                                                 && HttpMethod.PATCH != request.getHttpMethod()
703                                                 && HttpMethod.HEAD != request.getHttpMethod();
704                 if (alwaysAddOrigin || !isSameOrigin_) {
705                     final StringBuilder origin = new StringBuilder().append(pageUrl.getProtocol()).append("://")
706                             .append(pageUrl.getHost());
707                     if (pageUrl.getPort() != -1) {
708                         origin.append(':').append(pageUrl.getPort());
709                     }
710                     request.setAdditionalHeader(HttpHeader.ORIGIN, origin.toString());
711                 }
712 
713                 // password is ignored if no user defined
714                 if (user != null && !JavaScriptEngine.isUndefined(user)) {
715                     final String userCred = user.toString();
716 
717                     String passwordCred = "";
718                     if (password != null && !JavaScriptEngine.isUndefined(password)) {
719                         passwordCred = password.toString();
720                     }
721 
722                     request.setCredentials(
723                                 new HtmlUnitUsernamePasswordCredentials(userCred, passwordCred.toCharArray()));
724                 }
725             }
726             webRequest_ = request;
727         }
728         catch (final MalformedURLException e) {
729             if (LOG.isErrorEnabled()) {
730                 LOG.error("Unable to initialize XMLHttpRequest using malformed URL '" + url + "'.");
731             }
732             return;
733         }
734 
735         // Async stays a boolean.
736         async_ = async;
737 
738         // Change the state!
739         setState(OPENED);
740         fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
741     }
742 
743     /**
744      * Sends the specified content to the server in an HTTP request and receives the response.
745      * @param content the body of the message being sent with the request
746      */
747     @JsxFunction
748     public void send(final Object content) {
749         responseXML_ = null;
750 
751         if (webRequest_ == null) {
752             return;
753         }
754         if (!async_ && timeout_ > 0) {
755             throw JavaScriptEngine.throwAsScriptRuntimeEx(
756                     new RuntimeException("Synchronous requests must not set a timeout."));
757         }
758 
759         prepareRequestContent(content);
760         if (timeout_ > 0) {
761             webRequest_.setTimeout(timeout_);
762         }
763 
764         final Window w = getWindow();
765         final WebWindow ww = w.getWebWindow();
766         final WebClient client = ww.getWebClient();
767         final AjaxController ajaxController = client.getAjaxController();
768         final HtmlPage page = (HtmlPage) ww.getEnclosedPage();
769         final boolean synchron = ajaxController.processSynchron(page, webRequest_, async_);
770         if (synchron) {
771             doSend();
772         }
773         else {
774             // Create and start a thread in which to execute the request.
775             final HtmlUnitContextFactory cf = client.getJavaScriptEngine().getContextFactory();
776             final ContextAction<Object> action = new ContextAction<Object>() {
777                 @Override
778                 public Object run(final Context cx) {
779                     doSend();
780                     return null;
781                 }
782 
783                 @Override
784                 public String toString() {
785                     return "XMLHttpRequest " + webRequest_.getHttpMethod() + " '" + webRequest_.getUrl() + "'";
786                 }
787             };
788             final JavaScriptJob job = BackgroundJavaScriptFactory.theFactory().
789                     createJavascriptXMLHttpRequestJob(cf, action);
790             LOG.debug("Starting XMLHttpRequest thread for asynchronous request");
791             jobID_ = ww.getJobManager().addJob(job, page);
792 
793             fireJavascriptEvent(Event.TYPE_LOAD_START);
794         }
795     }
796 
797     /**
798      * Prepares the WebRequest that will be sent.
799      * @param content the content to send
800      */
801     private void prepareRequestContent(final Object content) {
802         if (content != null
803             && (HttpMethod.POST == webRequest_.getHttpMethod()
804                     || HttpMethod.PUT == webRequest_.getHttpMethod()
805                     || HttpMethod.PATCH == webRequest_.getHttpMethod()
806                     || HttpMethod.DELETE == webRequest_.getHttpMethod()
807                     || HttpMethod.OPTIONS == webRequest_.getHttpMethod())
808             && !JavaScriptEngine.isUndefined(content)) {
809 
810             final boolean setEncodingType = webRequest_.getAdditionalHeader(HttpHeader.CONTENT_TYPE) == null;
811 
812             if (content instanceof HTMLDocument) {
813                 // final String body = ((HTMLDocument) content).getDomNodeOrDie().asXml();
814                 String body = new XMLSerializer().serializeToString((HTMLDocument) content);
815                 if (LOG.isDebugEnabled()) {
816                     LOG.debug("Setting request body to: " + body);
817                 }
818 
819                 final Element docElement = ((Document) content).getDocumentElement();
820                 final SgmlPage page = docElement.getDomNodeOrDie().getPage();
821                 final DocumentType doctype = page.getDoctype();
822                 if (doctype != null && !StringUtils.isEmptyOrNull(doctype.getName())) {
823                     body = "<!DOCTYPE " + doctype.getName() + ">" + body;
824                 }
825 
826                 webRequest_.setRequestBody(body);
827                 if (setEncodingType) {
828                     webRequest_.setAdditionalHeader(HttpHeader.CONTENT_TYPE, "text/html;charset=UTF-8");
829                 }
830             }
831             else if (content instanceof XMLDocument) {
832                 // this output differs from real browsers but it seems to be a good starting point
833                 try (StringWriter writer = new StringWriter()) {
834                     final XMLDocument xmlDocument = (XMLDocument) content;
835 
836                     final Transformer transformer = TransformerFactory.newInstance().newTransformer();
837                     transformer.setOutputProperty(OutputKeys.METHOD, "xml");
838                     transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
839                     transformer.setOutputProperty(OutputKeys.INDENT, "no");
840                     transformer.transform(
841                             new DOMSource(xmlDocument.getDomNodeOrDie().getFirstChild()), new StreamResult(writer));
842 
843                     final String body = writer.toString();
844                     if (LOG.isDebugEnabled()) {
845                         LOG.debug("Setting request body to: " + body);
846                     }
847                     webRequest_.setRequestBody(body);
848                     if (setEncodingType) {
849                         webRequest_.setAdditionalHeader(HttpHeader.CONTENT_TYPE,
850                                         MimeType.APPLICATION_XML + ";charset=UTF-8");
851                     }
852                 }
853                 catch (final Exception e) {
854                     throw JavaScriptEngine.throwAsScriptRuntimeEx(e);
855                 }
856             }
857             else if (content instanceof FormData) {
858                 ((FormData) content).fillRequest(webRequest_);
859             }
860             else if (content instanceof NativeArrayBufferView) {
861                 final NativeArrayBufferView view = (NativeArrayBufferView) content;
862                 webRequest_.setRequestBody(new String(view.getBuffer().getBuffer(), UTF_8));
863                 if (setEncodingType) {
864                     webRequest_.setEncodingType(null);
865                 }
866             }
867             else if (content instanceof URLSearchParams) {
868                 ((URLSearchParams) content).fillRequest(webRequest_);
869                 webRequest_.addHint(HttpHint.IncludeCharsetInContentTypeHeader);
870             }
871             else if (content instanceof Blob) {
872                 ((Blob) content).fillRequest(webRequest_);
873             }
874             else {
875                 final String body = JavaScriptEngine.toString(content);
876                 if (!body.isEmpty()) {
877                     if (LOG.isDebugEnabled()) {
878                         LOG.debug("Setting request body to: " + body);
879                     }
880                     webRequest_.setRequestBody(body);
881                     webRequest_.setCharset(UTF_8);
882                     if (setEncodingType) {
883                         webRequest_.setEncodingType(FormEncodingType.TEXT_PLAIN);
884                     }
885                 }
886             }
887         }
888     }
889 
890     /**
891      * The real send job.
892      */
893     void doSend() {
894         final WebClient wc = getWindow().getWebWindow().getWebClient();
895 
896         // accessing to local resource is forbidden for security reason
897         if (!wc.getOptions().isFileProtocolForXMLHttpRequestsAllowed()
898                 && "file".equals(webRequest_.getUrl().getProtocol())) {
899 
900             if (async_) {
901                 setState(DONE);
902                 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
903                 fireJavascriptEvent(Event.TYPE_ERROR);
904                 fireJavascriptEvent(Event.TYPE_LOAD_END);
905             }
906 
907             if (LOG.isDebugEnabled()) {
908                 LOG.debug("Not allowed to load local resource: " + webRequest_.getUrl());
909             }
910             throw JavaScriptEngine.asJavaScriptException(
911                     getWindow(),
912                     "Not allowed to load local resource: " + webRequest_.getUrl(),
913                     DOMException.NETWORK_ERR);
914         }
915 
916         final BrowserVersion browserVersion = getBrowserVersion();
917         try {
918             if (!isSameOrigin_ && isPreflight()) {
919                 final WebRequest preflightRequest = new WebRequest(webRequest_.getUrl(), HttpMethod.OPTIONS);
920 
921                 // preflight request shouldn't have cookies
922                 preflightRequest.addHint(HttpHint.BlockCookies);
923 
924                 // header origin
925                 final String originHeaderValue = webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN);
926                 preflightRequest.setAdditionalHeader(HttpHeader.ORIGIN, originHeaderValue);
927 
928                 // header request-method
929                 preflightRequest.setAdditionalHeader(
930                         HttpHeader.ACCESS_CONTROL_REQUEST_METHOD,
931                         webRequest_.getHttpMethod().name());
932 
933                 // header request-headers
934                 final StringBuilder builder = new StringBuilder();
935                 for (final Entry<String, String> header
936                         : new TreeMap<>(webRequest_.getAdditionalHeaders()).entrySet()) {
937                     final String name = org.htmlunit.util.StringUtils
938                                             .toRootLowerCase(header.getKey());
939                     if (isPreflightHeader(name, header.getValue())) {
940                         if (builder.length() != 0) {
941                             builder.append(',');
942                         }
943                         builder.append(name);
944                     }
945                 }
946                 preflightRequest.setAdditionalHeader(HttpHeader.ACCESS_CONTROL_REQUEST_HEADERS, builder.toString());
947                 if (timeout_ > 0) {
948                     preflightRequest.setTimeout(timeout_);
949                 }
950 
951                 // do the preflight request
952                 final WebResponse preflightResponse = wc.loadWebResponse(preflightRequest);
953                 if (!preflightResponse.isSuccessOrUseProxyOrNotModified()
954                         || !isPreflightAuthorized(preflightResponse)) {
955                     setState(DONE);
956                     if (async_ || browserVersion.hasFeature(XHR_HANDLE_SYNC_NETWORK_ERRORS)) {
957                         fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
958                         fireJavascriptEvent(Event.TYPE_ERROR);
959                         fireJavascriptEvent(Event.TYPE_LOAD_END);
960                     }
961 
962                     if (LOG.isDebugEnabled()) {
963                         LOG.debug("No permitted request for URL " + webRequest_.getUrl());
964                     }
965                     throw JavaScriptEngine.asJavaScriptException(
966                             getWindow(),
967                             "No permitted \"Access-Control-Allow-Origin\" header.",
968                             DOMException.NETWORK_ERR);
969                 }
970             }
971 
972             if (!isSameOrigin_) {
973                 // Cookies should not be sent for cross-origin requests when withCredentials is false
974                 if (!isWithCredentials()) {
975                     webRequest_.addHint(HttpHint.BlockCookies);
976                 }
977             }
978 
979             webResponse_ = wc.loadWebResponse(webRequest_);
980             LOG.debug("Web response loaded successfully.");
981 
982             boolean allowOriginResponse = true;
983             if (!isSameOrigin_) {
984                 String value = webResponse_.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN);
985                 allowOriginResponse = webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN).equals(value);
986                 if (isWithCredentials()) {
987                     // second step: check the allow-credentials header for true
988                     value = webResponse_.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS);
989                     allowOriginResponse = allowOriginResponse && Boolean.parseBoolean(value);
990                 }
991                 else {
992                     allowOriginResponse = allowOriginResponse || ALLOW_ORIGIN_ALL.equals(value);
993                 }
994             }
995             if (allowOriginResponse) {
996                 if (overriddenMimeType_ != null) {
997                     final int index = overriddenMimeType_.toLowerCase(Locale.ROOT).indexOf("charset=");
998                     String charsetName = "";
999                     if (index != -1) {
1000                         charsetName = overriddenMimeType_.substring(index + "charset=".length());
1001                     }
1002 
1003                     final String charsetNameFinal = charsetName;
1004                     final Charset charset;
1005                     if (XUserDefinedCharset.NAME.equalsIgnoreCase(charsetName)) {
1006                         charset = XUserDefinedCharset.INSTANCE;
1007                     }
1008                     else {
1009                         charset = EncodingSniffer.toCharset(charsetName);
1010                     }
1011                     webResponse_ = new WebResponseWrapper(webResponse_) {
1012                         @Override
1013                         public String getContentType() {
1014                             return overriddenMimeType_;
1015                         }
1016 
1017                         @Override
1018                         public Charset getContentCharset() {
1019                             if (charsetNameFinal.isEmpty() || charset == null) {
1020                                 return super.getContentCharset();
1021                             }
1022                             return charset;
1023                         }
1024                     };
1025                 }
1026             }
1027             if (!allowOriginResponse) {
1028                 if (LOG.isDebugEnabled()) {
1029                     LOG.debug("No permitted \"Access-Control-Allow-Origin\" header for URL " + webRequest_.getUrl());
1030                 }
1031                 throw new NoPermittedHeaderException("No permitted \"Access-Control-Allow-Origin\" header.");
1032             }
1033 
1034             setState(HEADERS_RECEIVED);
1035             if (async_) {
1036                 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1037 
1038                 setState(LOADING);
1039                 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1040                 fireJavascriptEvent(Event.TYPE_PROGRESS);
1041             }
1042 
1043             setState(DONE);
1044             fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1045 
1046             if (!async_ && aborted_
1047                     && browserVersion.hasFeature(XHR_SEND_NETWORK_ERROR_IF_ABORTED)) {
1048                 throw JavaScriptEngine.constructError("Error",
1049                         "Failed to execute 'send' on 'XMLHttpRequest': Failed to load '" + webRequest_.getUrl() + "'");
1050             }
1051 
1052             if (browserVersion.hasFeature(XHR_LOAD_ALWAYS_AFTER_DONE)) {
1053                 fireJavascriptEventIgnoreAbort(Event.TYPE_LOAD);
1054                 fireJavascriptEventIgnoreAbort(Event.TYPE_LOAD_END);
1055             }
1056             else {
1057                 fireJavascriptEvent(Event.TYPE_LOAD);
1058                 fireJavascriptEvent(Event.TYPE_LOAD_END);
1059             }
1060         }
1061         catch (final IOException e) {
1062             LOG.debug("IOException: returning a network error response.", e);
1063 
1064             webResponse_ = new NetworkErrorWebResponse(webRequest_, e);
1065             if (async_) {
1066                 setState(DONE);
1067                 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1068                 if (e instanceof SocketTimeoutException) {
1069                     fireJavascriptEvent(Event.TYPE_TIMEOUT);
1070                 }
1071                 else {
1072                     fireJavascriptEvent(Event.TYPE_ERROR);
1073                 }
1074                 fireJavascriptEvent(Event.TYPE_LOAD_END);
1075             }
1076             else {
1077                 setState(DONE);
1078                 if (browserVersion.hasFeature(XHR_HANDLE_SYNC_NETWORK_ERRORS)) {
1079                     fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
1080                     if (e instanceof SocketTimeoutException) {
1081                         fireJavascriptEvent(Event.TYPE_TIMEOUT);
1082                     }
1083                     else {
1084                         fireJavascriptEvent(Event.TYPE_ERROR);
1085                     }
1086                     fireJavascriptEvent(Event.TYPE_LOAD_END);
1087                 }
1088 
1089                 throw JavaScriptEngine.asJavaScriptException(getWindow(),
1090                         e.getMessage(), DOMException.NETWORK_ERR);
1091             }
1092         }
1093     }
1094 
1095     private boolean isPreflight() {
1096         final HttpMethod method = webRequest_.getHttpMethod();
1097         if (method != HttpMethod.GET && method != HttpMethod.HEAD && method != HttpMethod.POST) {
1098             return true;
1099         }
1100         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
1101             if (isPreflightHeader(header.getKey().toLowerCase(Locale.ROOT), header.getValue())) {
1102                 return true;
1103             }
1104         }
1105         return false;
1106     }
1107 
1108     private boolean isPreflightAuthorized(final WebResponse preflightResponse) {
1109         final String originHeader = preflightResponse.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN);
1110         if (!ALLOW_ORIGIN_ALL.equals(originHeader)
1111                 && !webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN).equals(originHeader)) {
1112             return false;
1113         }
1114 
1115         // there is no test case for this because the servlet API has no support
1116         // for adding the same header twice
1117         final HashSet<String> accessControlValues = new HashSet<>();
1118         for (final NameValuePair pair : preflightResponse.getResponseHeaders()) {
1119             if (HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS.equalsIgnoreCase(pair.getName())) {
1120                 String value = pair.getValue();
1121                 if (value != null) {
1122                     value = org.htmlunit.util.StringUtils.toRootLowerCase(value);
1123                     final String[] values = org.htmlunit.util.StringUtils.splitAtComma(value);
1124                     for (String part : values) {
1125                         part = part.trim();
1126                         if (!org.htmlunit.util.StringUtils.isEmptyOrNull(part)) {
1127                             accessControlValues.add(part);
1128                         }
1129                     }
1130                 }
1131             }
1132         }
1133 
1134         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
1135             final String key = org.htmlunit.util.StringUtils.toRootLowerCase(header.getKey());
1136             if (isPreflightHeader(key, header.getValue())
1137                     && !accessControlValues.contains(key)) {
1138                 return false;
1139             }
1140         }
1141         return true;
1142     }
1143 
1144     /**
1145      * @param name header name (MUST be lower-case for performance reasons)
1146      * @param value header value
1147      */
1148     private static boolean isPreflightHeader(final String name, final String value) {
1149         if (HttpHeader.CONTENT_TYPE_LC.equals(name)) {
1150             final String lcValue = value.toLowerCase(Locale.ROOT);
1151             return !lcValue.startsWith(FormEncodingType.URL_ENCODED.getName())
1152                     && !lcValue.startsWith(FormEncodingType.MULTIPART.getName())
1153                     && !lcValue.startsWith(FormEncodingType.TEXT_PLAIN.getName());
1154         }
1155         if (HttpHeader.ACCEPT_LC.equals(name)
1156                 || HttpHeader.ACCEPT_LANGUAGE_LC.equals(name)
1157                 || HttpHeader.CONTENT_LANGUAGE_LC.equals(name)
1158                 || HttpHeader.REFERER_LC.equals(name)
1159                 || "accept-encoding".equals(name)
1160                 || HttpHeader.ORIGIN_LC.equals(name)) {
1161             return false;
1162         }
1163         return true;
1164     }
1165 
1166     /**
1167      * Sets the specified header to the specified value. The <code>open</code> method must be
1168      * called before this method, or an error will occur.
1169      * @param name the name of the header being set
1170      * @param value the value of the header being set
1171      */
1172     @JsxFunction
1173     public void setRequestHeader(final String name, final String value) {
1174         if (!isAuthorizedHeader(name)) {
1175             if (LOG.isWarnEnabled()) {
1176                 LOG.warn("Ignoring XMLHttpRequest.setRequestHeader for " + name
1177                     + ": it is a restricted header");
1178             }
1179             return;
1180         }
1181 
1182         if (webRequest_ != null) {
1183             webRequest_.setAdditionalHeader(name, value);
1184         }
1185         else {
1186             throw JavaScriptEngine.asJavaScriptException(
1187                     getWindow(),
1188                     "The open() method must be called before setRequestHeader().",
1189                     DOMException.INVALID_STATE_ERR);
1190         }
1191     }
1192 
1193     /**
1194      * Not all request headers can be set from JavaScript.
1195      * @see <a href="http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method">W3C doc</a>
1196      * @param name the header name
1197      * @return {@code true} if the header can be set from JavaScript
1198      */
1199     static boolean isAuthorizedHeader(final String name) {
1200         final String nameLowerCase = org.htmlunit.util.StringUtils.toRootLowerCase(name);
1201         if (PROHIBITED_HEADERS_.contains(nameLowerCase)) {
1202             return false;
1203         }
1204         if (nameLowerCase.startsWith("proxy-") || nameLowerCase.startsWith("sec-")) {
1205             return false;
1206         }
1207         return true;
1208     }
1209 
1210     /**
1211      * Override the mime type returned by the server (if any). This may be used, for example, to force a stream
1212      * to be treated and parsed as text/xml, even if the server does not report it as such.
1213      * This must be done before the send method is invoked.
1214      * @param mimeType the type used to override that returned by the server (if any)
1215      * @see <a href="http://xulplanet.com/references/objref/XMLHttpRequest.html#method_overrideMimeType">XUL Planet</a>
1216      */
1217     @JsxFunction
1218     public void overrideMimeType(final String mimeType) {
1219         if (state_ != UNSENT && state_ != OPENED) {
1220             throw JavaScriptEngine.asJavaScriptException(
1221                     getWindow(),
1222                     "Property 'overrideMimeType' not writable after sent.",
1223                     DOMException.INVALID_STATE_ERR);
1224         }
1225         overriddenMimeType_ = mimeType;
1226     }
1227 
1228     /**
1229      * Returns the {@code withCredentials} property.
1230      * @return the {@code withCredentials} property
1231      */
1232     @JsxGetter
1233     public boolean isWithCredentials() {
1234         return withCredentials_;
1235     }
1236 
1237     /**
1238      * Sets the {@code withCredentials} property.
1239      * @param withCredentials the {@code withCredentials} property.
1240      */
1241     @JsxSetter
1242     public void setWithCredentials(final boolean withCredentials) {
1243         withCredentials_ = withCredentials;
1244     }
1245 
1246     /**
1247      * Returns the {@code upload} property.
1248      * @return the {@code upload} property
1249      */
1250     @JsxGetter
1251     public XMLHttpRequestUpload getUpload() {
1252         final XMLHttpRequestUpload upload = new XMLHttpRequestUpload();
1253         upload.setParentScope(getParentScope());
1254         upload.setPrototype(getPrototype(upload.getClass()));
1255         return upload;
1256     }
1257 
1258     /**
1259      * {@inheritDoc}
1260      */
1261     @JsxGetter
1262     @Override
1263     public Function getOnreadystatechange() {
1264         return super.getOnreadystatechange();
1265     }
1266 
1267     /**
1268      * {@inheritDoc}
1269      */
1270     @JsxSetter
1271     @Override
1272     public void setOnreadystatechange(final Function readyStateChangeHandler) {
1273         super.setOnreadystatechange(readyStateChangeHandler);
1274     }
1275 
1276     /**
1277      * @return the number of milliseconds a request can take before automatically being terminated.
1278      *         The default value is 0, which means there is no timeout.
1279      */
1280     @JsxGetter
1281     public int getTimeout() {
1282         return timeout_;
1283     }
1284 
1285     /**
1286      * Sets the number of milliseconds a request can take before automatically being terminated.
1287      * @param timeout the timeout in milliseconds
1288      */
1289     @JsxSetter
1290     public void setTimeout(final int timeout) {
1291         timeout_ = timeout;
1292     }
1293 
1294     private static final class NetworkErrorWebResponse extends WebResponse {
1295         private final WebRequest request_;
1296         private final IOException error_;
1297 
1298         NetworkErrorWebResponse(final WebRequest webRequest, final IOException error) {
1299             super(null, null, 0);
1300             request_ = webRequest;
1301             error_ = error;
1302         }
1303 
1304         @Override
1305         public int getStatusCode() {
1306             return 0;
1307         }
1308 
1309         @Override
1310         public String getStatusMessage() {
1311             return "";
1312         }
1313 
1314         @Override
1315         public String getContentType() {
1316             return "";
1317         }
1318 
1319         @Override
1320         public String getContentAsString() {
1321             return "";
1322         }
1323 
1324         @Override
1325         public InputStream getContentAsStream() {
1326             return null;
1327         }
1328 
1329         @Override
1330         public List<NameValuePair> getResponseHeaders() {
1331             return Collections.emptyList();
1332         }
1333 
1334         @Override
1335         public String getResponseHeaderValue(final String headerName) {
1336             return "";
1337         }
1338 
1339         @Override
1340         public long getLoadTime() {
1341             return 0;
1342         }
1343 
1344         @Override
1345         public Charset getContentCharset() {
1346             return null;
1347         }
1348 
1349         @Override
1350         public WebRequest getWebRequest() {
1351             return request_;
1352         }
1353 
1354         /**
1355          * @return the error
1356          */
1357         public IOException getError() {
1358             return error_;
1359         }
1360     }
1361 
1362     private static final class NoPermittedHeaderException extends IOException {
1363         NoPermittedHeaderException(final String msg) {
1364             super(msg);
1365         }
1366     }
1367 }