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.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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113 @JsxClass
114 public class XMLHttpRequest extends XMLHttpRequestEventTarget {
115
116 private static final Log LOG = LogFactory.getLog(XMLHttpRequest.class);
117
118
119 @JsxConstant
120 public static final int UNSENT = 0;
121
122
123 @JsxConstant
124 public static final int OPENED = 1;
125
126
127 @JsxConstant
128 public static final int HEADERS_RECEIVED = 2;
129
130
131 @JsxConstant
132 public static final int LOADING = 3;
133
134
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
170
171 public XMLHttpRequest() {
172 state_ = UNSENT;
173 responseType_ = RESPONSE_TYPE_DEFAULT;
174 }
175
176
177
178
179 @Override
180 @JsxConstructor
181 public void jsConstructor() {
182
183 }
184
185
186
187
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
242
243
244
245
246
247
248
249
250
251 @JsxGetter
252 public int getReadyState() {
253 return state_;
254 }
255
256
257
258
259 @JsxGetter
260 public String getResponseType() {
261 return responseType_;
262 }
263
264
265
266
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
294
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
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
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
447
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
496
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
536
537
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
557
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
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
593
594
595 setState(UNSENT);
596 webResponse_ = new NetworkErrorWebResponse(webRequest_, null);
597 aborted_ = true;
598 }
599
600
601
602
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
631
632
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
652
653
654
655
656
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
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
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
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
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
727 async_ = async;
728
729
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
752
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
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
806
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
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
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
891
892 void doSend() {
893 final WebClient wc = getWindow().getWebWindow().getWebClient();
894
895
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
921 preflightRequest.addHint(HttpHint.BlockCookies);
922
923
924 final String originHeaderValue = webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN);
925 preflightRequest.setAdditionalHeader(HttpHeader.ORIGIN, originHeaderValue);
926
927
928 preflightRequest.setAdditionalHeader(
929 HttpHeader.ACCESS_CONTROL_REQUEST_METHOD,
930 webRequest_.getHttpMethod().name());
931
932
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
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
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
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
1115
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
1145
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
1167
1168
1169
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
1194
1195
1196
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
1211
1212
1213
1214
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
1229
1230
1231 @JsxGetter
1232 public boolean isWithCredentials() {
1233 return withCredentials_;
1234 }
1235
1236
1237
1238
1239
1240 @JsxSetter
1241 public void setWithCredentials(final boolean withCredentials) {
1242 withCredentials_ = withCredentials;
1243 }
1244
1245
1246
1247
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
1259
1260 @JsxGetter
1261 @Override
1262 public Function getOnreadystatechange() {
1263 return super.getOnreadystatechange();
1264 }
1265
1266
1267
1268
1269 @JsxSetter
1270 @Override
1271 public void setOnreadystatechange(final Function readyStateChangeHandler) {
1272 super.setOnreadystatechange(readyStateChangeHandler);
1273 }
1274
1275
1276
1277
1278
1279 @JsxGetter
1280 public int getTimeout() {
1281 return timeout_;
1282 }
1283
1284
1285
1286
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
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 }