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