1
2
3
4
5
6
7
8
9
10
11
12
13
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117 @JsxClass
118 public class XMLHttpRequest extends XMLHttpRequestEventTarget {
119
120 private static final Log LOG = LogFactory.getLog(XMLHttpRequest.class);
121
122
123 @JsxConstant
124 public static final int UNSENT = 0;
125
126
127 @JsxConstant
128 public static final int OPENED = 1;
129
130
131 @JsxConstant
132 public static final int HEADERS_RECEIVED = 2;
133
134
135 @JsxConstant
136 public static final int LOADING = 3;
137
138
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
174
175 public XMLHttpRequest() {
176 state_ = UNSENT;
177 responseType_ = RESPONSE_TYPE_DEFAULT;
178 }
179
180
181
182
183 @Override
184 @JsxConstructor
185 public void jsConstructor() {
186
187 }
188
189
190
191
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
246
247
248
249
250
251
252
253
254
255 @JsxGetter
256 public int getReadyState() {
257 return state_;
258 }
259
260
261
262
263 @JsxGetter
264 public String getResponseType() {
265 return responseType_;
266 }
267
268
269
270
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
298
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
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
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
451
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
500
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
540
541
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
561
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
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
597
598
599 setState(UNSENT);
600 webResponse_ = new NetworkErrorWebResponse(webRequest_, null);
601 aborted_ = true;
602 }
603
604
605
606
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
635
636
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
656
657
658
659
660
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
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
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
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
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
738 async_ = async;
739
740
741 setState(OPENED);
742 fireJavascriptEvent(Event.TYPE_READY_STATE_CHANGE);
743 }
744
745
746
747
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
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
801
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
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
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
894
895 void doSend() {
896 final WebClient wc = getWindow().getWebWindow().getWebClient();
897
898
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
924 preflightRequest.addHint(HttpHint.BlockCookies);
925
926
927 final String originHeaderValue = webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN);
928 preflightRequest.setAdditionalHeader(HttpHeader.ORIGIN, originHeaderValue);
929
930
931 preflightRequest.setAdditionalHeader(
932 HttpHeader.ACCESS_CONTROL_REQUEST_METHOD,
933 webRequest_.getHttpMethod().name());
934
935
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
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
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
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
1118
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
1148
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
1170
1171
1172
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
1197
1198
1199
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
1214
1215
1216
1217
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
1232
1233
1234 @JsxGetter
1235 public boolean isWithCredentials() {
1236 return withCredentials_;
1237 }
1238
1239
1240
1241
1242
1243 @JsxSetter
1244 public void setWithCredentials(final boolean withCredentials) {
1245 withCredentials_ = withCredentials;
1246 }
1247
1248
1249
1250
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
1262
1263 @JsxGetter
1264 @Override
1265 public Function getOnreadystatechange() {
1266 return super.getOnreadystatechange();
1267 }
1268
1269
1270
1271
1272 @JsxSetter
1273 @Override
1274 public void setOnreadystatechange(final Function readyStateChangeHandler) {
1275 super.setOnreadystatechange(readyStateChangeHandler);
1276 }
1277
1278
1279
1280
1281
1282 @JsxGetter
1283 public int getTimeout() {
1284 return timeout_;
1285 }
1286
1287
1288
1289
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
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 }