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