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