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