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