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