View Javadoc
1   /*
2    * Copyright (c) 2002-2017 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package com.gargoylesoftware.htmlunit.javascript.host.xml;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.XHR_ALL_RESPONSE_HEADERS_APPEND_CRLF;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.XHR_FIRE_STATE_OPENED_AGAIN_IN_ASYNC_MODE;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.XHR_IGNORE_PORT_FOR_SAME_ORIGIN;
20  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.XHR_LENGTH_COMPUTABLE;
21  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.XHR_NO_CROSS_ORIGIN_TO_ABOUT;
22  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.XHR_OPEN_ALLOW_EMTPY_URL;
23  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.XHR_OPEN_WITHCREDENTIALS_TRUE_IN_SYNC_EXCEPTION;
24  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.XHR_OVERRIDE_MIME_TYPE_BEFORE_SEND;
25  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.XHR_USE_CONTENT_CHARSET;
26  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.XHR_USE_DEFAULT_CHARSET_FROM_PAGE;
27  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.XHR_WITHCREDENTIALS_ALLOW_ORIGIN_ALL;
28  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.XHR_WITHCREDENTIALS_NOT_WRITEABLE_IN_SYNC_EXCEPTION;
29  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
30  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
31  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.IE;
32  import static java.nio.charset.StandardCharsets.UTF_8;
33  
34  import java.io.IOException;
35  import java.io.InputStream;
36  import java.net.MalformedURLException;
37  import java.net.URL;
38  import java.nio.charset.Charset;
39  import java.util.Arrays;
40  import java.util.Collection;
41  import java.util.Collections;
42  import java.util.List;
43  import java.util.Locale;
44  import java.util.Map.Entry;
45  import java.util.Stack;
46  import java.util.TreeMap;
47  
48  import org.apache.commons.logging.Log;
49  import org.apache.commons.logging.LogFactory;
50  import org.apache.http.auth.UsernamePasswordCredentials;
51  
52  import com.gargoylesoftware.htmlunit.AjaxController;
53  import com.gargoylesoftware.htmlunit.BrowserVersion;
54  import com.gargoylesoftware.htmlunit.FormEncodingType;
55  import com.gargoylesoftware.htmlunit.HttpMethod;
56  import com.gargoylesoftware.htmlunit.WebClient;
57  import com.gargoylesoftware.htmlunit.WebRequest;
58  import com.gargoylesoftware.htmlunit.WebResponse;
59  import com.gargoylesoftware.htmlunit.WebWindow;
60  import com.gargoylesoftware.htmlunit.html.HtmlPage;
61  import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
62  import com.gargoylesoftware.htmlunit.javascript.background.BackgroundJavaScriptFactory;
63  import com.gargoylesoftware.htmlunit.javascript.background.JavaScriptJob;
64  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
65  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstant;
66  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
67  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
68  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
69  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxSetter;
70  import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
71  import com.gargoylesoftware.htmlunit.javascript.host.event.ProgressEvent;
72  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDocument;
73  import com.gargoylesoftware.htmlunit.util.EncodingSniffer;
74  import com.gargoylesoftware.htmlunit.util.NameValuePair;
75  import com.gargoylesoftware.htmlunit.util.WebResponseWrapper;
76  import com.gargoylesoftware.htmlunit.xml.XmlPage;
77  
78  import net.sourceforge.htmlunit.corejs.javascript.Context;
79  import net.sourceforge.htmlunit.corejs.javascript.ContextAction;
80  import net.sourceforge.htmlunit.corejs.javascript.ContextFactory;
81  import net.sourceforge.htmlunit.corejs.javascript.Function;
82  import net.sourceforge.htmlunit.corejs.javascript.ScriptRuntime;
83  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
84  import net.sourceforge.htmlunit.corejs.javascript.Undefined;
85  
86  /**
87   * A JavaScript object for an {@code XMLHttpRequest}.
88   *
89   * @author Daniel Gredler
90   * @author Marc Guillemot
91   * @author Ahmed Ashour
92   * @author Stuart Begg
93   * @author Ronald Brill
94   * @author Sebastian Cato
95   * @author Frank Danek
96   * @author Jake Cobb
97   *
98   * @see <a href="http://www.w3.org/TR/XMLHttpRequest/">W3C XMLHttpRequest</a>
99   * @see <a href="http://developer.apple.com/internet/webcontent/xmlhttpreq.html">Safari documentation</a>
100  */
101 @JsxClass
102 public class XMLHttpRequest extends XMLHttpRequestEventTarget {
103 
104     private static final Log LOG = LogFactory.getLog(XMLHttpRequest.class);
105 
106     /** The object has been created, but not initialized (the open() method has not been called). */
107     @JsxConstant
108     public static final int UNSENT = 0;
109     /** The object has been created, but the send() method has not been called. */
110     @JsxConstant
111     public static final int OPENED = 1;
112     /** The send() method has been called, but the status and headers are not yet available. */
113     @JsxConstant
114     public static final int HEADERS_RECEIVED = 2;
115     /** Some data has been received. */
116     @JsxConstant
117     public static final int LOADING = 3;
118     /** All the data has been received; the complete data is available in responseBody and responseText. */
119     @JsxConstant
120     public static final int DONE = 4;
121 
122     private static final String HEADER_ORIGIN = "Origin";
123 
124     private static final String HEADER_ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
125     private static final String HEADER_ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
126     private static final String HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
127     private static final String HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials";
128     private static final String HEADER_ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
129 
130     private static final String ALLOW_ORIGIN_ALL = "*";
131 
132     private static final String[] ALL_PROPERTIES_ = {"onreadystatechange", "readyState", "responseText", "responseXML",
133         "status", "statusText", "abort", "getAllResponseHeaders", "getResponseHeader", "open", "send",
134         "setRequestHeader"};
135 
136     private static Collection<String> PROHIBITED_HEADERS_ = Arrays.asList("accept-charset", "accept-encoding",
137         "connection", "content-length", "cookie", "cookie2", "content-transfer-encoding", "date", "expect",
138         "host", "keep-alive", "referer", "te", "trailer", "transfer-encoding", "upgrade", "user-agent", "via");
139 
140     private int state_;
141     private Function stateChangeHandler_;
142     private Function loadHandler_;
143     private Function errorHandler_;
144     private WebRequest webRequest_;
145     private boolean async_;
146     private int jobID_;
147     private WebResponse webResponse_;
148     private String overriddenMimeType_;
149     private HtmlPage containingPage_;
150     private final boolean caseSensitiveProperties_;
151     private boolean withCredentials_;
152 
153     /**
154      * Creates a new instance.
155      */
156     @JsxConstructor
157     public XMLHttpRequest() {
158         this(true);
159     }
160 
161     /**
162      * Creates a new instance.
163      * @param caseSensitiveProperties if properties and methods are case sensitive
164      */
165     public XMLHttpRequest(final boolean caseSensitiveProperties) {
166         caseSensitiveProperties_ = caseSensitiveProperties;
167         state_ = UNSENT;
168     }
169 
170     /**
171      * Returns the event handler that fires on every state change.
172      * @return the event handler that fires on every state change
173      */
174     @JsxGetter
175     public Function getOnreadystatechange() {
176         return stateChangeHandler_;
177     }
178 
179     /**
180      * Sets the event handler that fires on every state change.
181      * @param stateChangeHandler the event handler that fires on every state change
182      */
183     @JsxSetter
184     public void setOnreadystatechange(final Function stateChangeHandler) {
185         stateChangeHandler_ = stateChangeHandler;
186         if (state_ == OPENED) {
187             setState(state_, null);
188         }
189     }
190 
191     /**
192      * Sets the state as specified and invokes the state change handler if one has been set.
193      * @param state the new state
194      * @param context the context within which the state change handler is to be invoked;
195      *                if {@code null}, the current thread's context is used.
196      */
197     private void setState(final int state, Context context) {
198         state_ = state;
199 
200         final BrowserVersion browser = getBrowserVersion();
201         if (stateChangeHandler_ != null && (async_ || state == DONE)) {
202             final Scriptable scope = stateChangeHandler_.getParentScope();
203             final JavaScriptEngine jsEngine = (JavaScriptEngine) containingPage_.getWebClient().getJavaScriptEngine();
204 
205             if (LOG.isDebugEnabled()) {
206                 LOG.debug("Calling onreadystatechange handler for state " + state);
207             }
208             final Object[] params = new Event[] {new Event(this, Event.TYPE_READY_STATE_CHANGE)};
209             jsEngine.callFunction(containingPage_, stateChangeHandler_, scope, this, params);
210             if (LOG.isDebugEnabled()) {
211                 if (context == null) {
212                     context = Context.getCurrentContext();
213                 }
214                 LOG.debug("onreadystatechange handler: " + context.decompileFunction(stateChangeHandler_, 4));
215                 LOG.debug("Calling onreadystatechange handler for state " + state + ". Done.");
216             }
217         }
218 
219         if (state == DONE) {
220             final JavaScriptEngine jsEngine = (JavaScriptEngine) containingPage_.getWebClient().getJavaScriptEngine();
221 
222             final ProgressEvent event = new ProgressEvent(this, Event.TYPE_LOAD);
223             final Object[] params = new Event[] {event};
224             final boolean lengthComputable = browser.hasFeature(XHR_LENGTH_COMPUTABLE);
225             if (lengthComputable) {
226                 event.setLengthComputable(true);
227             }
228 
229             if (webResponse_ != null) {
230                 final long contentLength = webResponse_.getContentLength();
231                 event.setLoaded(contentLength);
232                 if (lengthComputable) {
233                     event.setTotal(contentLength);
234                 }
235             }
236 
237             if (loadHandler_ != null) {
238                 jsEngine.callFunction(containingPage_, loadHandler_, loadHandler_.getParentScope(), this, params);
239             }
240 
241             List<Scriptable> handlers = getEventListenersContainer().getListeners(Event.TYPE_LOAD, false);
242             if (handlers != null) {
243                 for (final Scriptable scriptable : handlers) {
244                     if (scriptable instanceof Function) {
245                         final Function function = (Function) scriptable;
246                         jsEngine.callFunction(containingPage_, function, function.getParentScope(), this, params);
247                     }
248                 }
249             }
250 
251             handlers = getEventListenersContainer().getListeners(Event.TYPE_LOAD, true);
252             if (handlers != null) {
253                 for (final Scriptable scriptable : handlers) {
254                     if (scriptable instanceof Function) {
255                         final Function function = (Function) scriptable;
256                         jsEngine.callFunction(containingPage_, function, function.getParentScope(), this, params);
257                     }
258                 }
259             }
260         }
261     }
262 
263     /**
264      * Returns the event handler that fires on load.
265      * @return the event handler that fires on load
266      */
267     @JsxGetter
268     public Function getOnload() {
269         return loadHandler_;
270     }
271 
272     /**
273      * Sets the event handler that fires on load.
274      * @param loadHandler the event handler that fires on load
275      */
276     @JsxSetter
277     public void setOnload(final Function loadHandler) {
278         loadHandler_ = loadHandler;
279     }
280 
281     /**
282      * Returns the event handler that fires on error.
283      * @return the event handler that fires on error
284      */
285     @JsxGetter
286     public Function getOnerror() {
287         return errorHandler_;
288     }
289 
290     /**
291      * Sets the event handler that fires on error.
292      * @param errorHandler the event handler that fires on error
293      */
294     @JsxSetter
295     public void setOnerror(final Function errorHandler) {
296         errorHandler_ = errorHandler;
297     }
298 
299     /**
300      * Invokes the onerror handler if one has been set.
301      * @param context the context within which the onerror handler is to be invoked;
302      *                if {@code null}, the current thread's context is used.
303      */
304     private void processError(Context context) {
305         if (errorHandler_ != null) {
306             final Scriptable scope = errorHandler_.getParentScope();
307             final JavaScriptEngine jsEngine = (JavaScriptEngine) containingPage_.getWebClient().getJavaScriptEngine();
308 
309             final Object[] params = new Event[] {new ProgressEvent(this, Event.TYPE_ERROR)};
310 
311             if (LOG.isDebugEnabled()) {
312                 LOG.debug("Calling onerror handler");
313             }
314             jsEngine.callFunction(containingPage_, errorHandler_, this, scope, params);
315             if (LOG.isDebugEnabled()) {
316                 if (context == null) {
317                     context = Context.getCurrentContext();
318                 }
319                 LOG.debug("onerror handler: " + context.decompileFunction(errorHandler_, 4));
320                 LOG.debug("Calling onerror handler done.");
321             }
322         }
323     }
324 
325     /**
326      * Returns the current state of the HTTP request. The possible values are:
327      * <ul>
328      *   <li>0 = unsent</li>
329      *   <li>1 = opened</li>
330      *   <li>2 = headers_received</li>
331      *   <li>3 = loading</li>
332      *   <li>4 = done</li>
333      * </ul>
334      * @return the current state of the HTTP request
335      */
336     @JsxGetter
337     public int getReadyState() {
338         return state_;
339     }
340 
341     /**
342      * Returns a string version of the data retrieved from the server.
343      * @return a string version of the data retrieved from the server
344      */
345     @JsxGetter
346     public String getResponseText() {
347         if (state_ == UNSENT || state_ == OPENED) {
348             return "";
349         }
350         if (webResponse_ != null) {
351             final Charset encoding = webResponse_.getContentCharset();
352             if (encoding == null) {
353                 return "";
354             }
355             final String content = webResponse_.getContentAsString(encoding);
356             if (content == null) {
357                 return "";
358             }
359             return content;
360         }
361         if (LOG.isDebugEnabled()) {
362             LOG.debug("XMLHttpRequest.responseText was retrieved before the response was available.");
363         }
364         return "";
365     }
366 
367     /**
368      * Returns a DOM-compatible document object version of the data retrieved from the server.
369      * @return a DOM-compatible document object version of the data retrieved from the server
370      */
371     @JsxGetter
372     public Object getResponseXML() {
373         if (webResponse_ == null) {
374             if (LOG.isDebugEnabled()) {
375                 LOG.debug("XMLHttpRequest.responseXML returns null because there "
376                         + "in no web resonse so far (has send() been called?)");
377             }
378             return null;
379         }
380         if (webResponse_ instanceof NetworkErrorWebResponse) {
381             if (LOG.isDebugEnabled()) {
382                 LOG.debug("XMLHttpRequest.responseXML returns of an network error ("
383                         + ((NetworkErrorWebResponse) webResponse_).getError() + ")");
384             }
385             return null;
386         }
387         final String contentType = webResponse_.getContentType();
388         if (contentType.isEmpty() || contentType.contains("xml")) {
389             final WebWindow webWindow = getWindow().getWebWindow();
390             try {
391                 final XmlPage page = new XmlPage(webResponse_, webWindow);
392                 final XMLDocument document = new XMLDocument();
393                 document.setPrototype(getPrototype(document.getClass()));
394                 document.setParentScope(getWindow());
395                 document.setDomNode(page);
396                 return document;
397             }
398             catch (final IOException e) {
399                 LOG.warn("Failed parsing XML document " + webResponse_.getWebRequest().getUrl() + ": "
400                         + e.getMessage());
401                 return null;
402             }
403         }
404         if (LOG.isDebugEnabled()) {
405             LOG.debug("XMLHttpRequest.responseXML was called but the response is "
406                 + webResponse_.getContentType());
407         }
408         return null;
409     }
410 
411     /**
412      * Returns the numeric status returned by the server, such as 404 for "Not Found"
413      * or 200 for "OK".
414      * @return the numeric status returned by the server
415      */
416     @JsxGetter
417     public int getStatus() {
418         if (state_ == UNSENT || state_ == OPENED) {
419             return 0;
420         }
421         if (webResponse_ != null) {
422             return webResponse_.getStatusCode();
423         }
424 
425         LOG.error("XMLHttpRequest.status was retrieved without a response available (readyState: "
426             + state_ + ").");
427         return 0;
428     }
429 
430     /**
431      * Returns the string message accompanying the status code, such as "Not Found" or "OK".
432      * @return the string message accompanying the status code
433      */
434     @JsxGetter
435     public String getStatusText() {
436         if (state_ == UNSENT || state_ == OPENED) {
437             return "";
438         }
439         if (webResponse_ != null) {
440             return webResponse_.getStatusMessage();
441         }
442 
443         LOG.error("XMLHttpRequest.statusText was retrieved without a response available (readyState: "
444             + state_ + ").");
445         return null;
446     }
447 
448     /**
449      * Cancels the current HTTP request.
450      */
451     @JsxFunction
452     public void abort() {
453         getWindow().getWebWindow().getJobManager().stopJob(jobID_);
454     }
455 
456     /**
457      * Returns the labels and values of all the HTTP headers.
458      * @return the labels and values of all the HTTP headers
459      */
460     @JsxFunction
461     public String getAllResponseHeaders() {
462         if (state_ == UNSENT || state_ == OPENED) {
463             return "";
464         }
465         if (webResponse_ != null) {
466             final StringBuilder builder = new StringBuilder();
467             for (final NameValuePair header : webResponse_.getResponseHeaders()) {
468                 builder.append(header.getName()).append(": ").append(header.getValue()).append("\r\n");
469             }
470             if (getBrowserVersion().hasFeature(XHR_ALL_RESPONSE_HEADERS_APPEND_CRLF)) {
471                 builder.append("\r\n");
472             }
473             return builder.toString();
474         }
475 
476         LOG.error("XMLHttpRequest.getAllResponseHeaders() was called without a response available (readyState: "
477             + state_ + ").");
478         return null;
479     }
480 
481     /**
482      * Retrieves the value of an HTTP header from the response body.
483      * @param headerName the (case-insensitive) name of the header to retrieve
484      * @return the value of the specified HTTP header
485      */
486     @JsxFunction
487     public String getResponseHeader(final String headerName) {
488         if (state_ == UNSENT || state_ == OPENED) {
489             return null;
490         }
491         if (webResponse_ != null) {
492             return webResponse_.getResponseHeaderValue(headerName);
493         }
494 
495         LOG.error("XMLHttpRequest.getAllResponseHeaders(..) was called without a response available (readyState: "
496             + state_ + ").");
497         return null;
498     }
499 
500     /**
501      * Assigns the destination URL, method and other optional attributes of a pending request.
502      * @param method the method to use to send the request to the server (GET, POST, etc)
503      * @param urlParam the URL to send the request to
504      * @param asyncParam Whether or not to send the request to the server asynchronously, defaults to {@code true}
505      * @param user If authentication is needed for the specified URL, the username to use to authenticate
506      * @param password If authentication is needed for the specified URL, the password to use to authenticate
507      */
508     @JsxFunction
509     public void open(final String method, final Object urlParam, final Object asyncParam,
510         final Object user, final Object password) {
511         if ((urlParam == null || "".equals(urlParam)) && !getBrowserVersion().hasFeature(XHR_OPEN_ALLOW_EMTPY_URL)) {
512             throw Context.reportRuntimeError("URL for XHR.open can't be empty!");
513         }
514 
515         // async defaults to true if not specified
516         boolean async = true;
517         if (asyncParam != Undefined.instance) {
518             async = ScriptRuntime.toBoolean(asyncParam);
519         }
520 
521         if (!async
522                 && isWithCredentials()
523                 && getBrowserVersion().hasFeature(XHR_OPEN_WITHCREDENTIALS_TRUE_IN_SYNC_EXCEPTION)) {
524             throw Context.reportRuntimeError(
525                             "open() in sync mode is not possible because 'withCredentials' is set to true");
526         }
527 
528         final String url = Context.toString(urlParam);
529 
530         // (URL + Method + User + Password) become a WebRequest instance.
531         containingPage_ = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
532 
533         try {
534             final URL fullUrl = containingPage_.getFullyQualifiedUrl(url);
535             final URL originUrl = containingPage_.getFullyQualifiedUrl("");
536             if (!isAllowCrossDomainsFor(fullUrl)) {
537                 throw Context.reportRuntimeError("Access to restricted URI denied");
538             }
539 
540             final WebRequest request = new WebRequest(fullUrl, getBrowserVersion().getXmlHttpRequestAcceptHeader());
541             request.setCharset(UTF_8);
542             request.setAdditionalHeader("Referer", containingPage_.getUrl().toExternalForm());
543 
544             if (!isSameOrigin(originUrl, fullUrl)) {
545                 final StringBuilder origin = new StringBuilder().append(originUrl.getProtocol()).append("://")
546                         .append(originUrl.getHost());
547                 if (originUrl.getPort() != -1) {
548                     origin.append(':').append(originUrl.getPort());
549                 }
550                 request.setAdditionalHeader(HEADER_ORIGIN, origin.toString());
551             }
552 
553             try {
554                 request.setHttpMethod(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)));
555             }
556             catch (final IllegalArgumentException e) {
557                 LOG.info("Incorrect HTTP Method '" + method + "'");
558                 return;
559             }
560 
561             // password is ignored if no user defined
562             if (user != null && user != Undefined.instance) {
563                 final String userCred = user.toString();
564 
565                 String passwordCred = "";
566                 if (password != null && password != Undefined.instance) {
567                     passwordCred = password.toString();
568                 }
569 
570                 request.setCredentials(new UsernamePasswordCredentials(userCred, passwordCred));
571             }
572             webRequest_ = request;
573         }
574         catch (final MalformedURLException e) {
575             LOG.error("Unable to initialize XMLHttpRequest using malformed URL '" + url + "'.");
576             return;
577         }
578         // Async stays a boolean.
579         async_ = async;
580         // Change the state!
581         setState(OPENED, null);
582     }
583 
584     private boolean isAllowCrossDomainsFor(final URL newUrl) {
585         final BrowserVersion browser = getBrowserVersion();
586         if (browser.hasFeature(XHR_NO_CROSS_ORIGIN_TO_ABOUT)
587                 && "about".equals(newUrl.getProtocol())) {
588             return false;
589         }
590 
591         return true;
592     }
593 
594     private boolean isSameOrigin(final URL originUrl, final URL newUrl) {
595         if (!originUrl.getHost().equals(newUrl.getHost())) {
596             return false;
597         }
598 
599         if (getBrowserVersion().hasFeature(XHR_IGNORE_PORT_FOR_SAME_ORIGIN)) {
600             return true;
601         }
602 
603         int originPort = originUrl.getPort();
604         if (originPort == -1) {
605             originPort = originUrl.getDefaultPort();
606         }
607         int newPort = newUrl.getPort();
608         if (newPort == -1) {
609             newPort = newUrl.getDefaultPort();
610         }
611         return originPort == newPort;
612     }
613 
614     /**
615      * Sends the specified content to the server in an HTTP request and receives the response.
616      * @param content the body of the message being sent with the request
617      */
618     @JsxFunction
619     public void send(final Object content) {
620         if (webRequest_ == null) {
621             return;
622         }
623         prepareRequest(content);
624 
625         final WebClient client = getWindow().getWebWindow().getWebClient();
626         final AjaxController ajaxController = client.getAjaxController();
627         final HtmlPage page = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
628         final boolean synchron = ajaxController.processSynchron(page, webRequest_, async_);
629         if (synchron) {
630             doSend(Context.getCurrentContext());
631         }
632         else {
633             if (getBrowserVersion().hasFeature(XHR_FIRE_STATE_OPENED_AGAIN_IN_ASYNC_MODE)) {
634                 // quite strange but IE seems to fire state loading twice
635                 // in async mode (at least with HTML of the unit tests)
636                 setState(OPENED, Context.getCurrentContext());
637             }
638 
639             // Create and start a thread in which to execute the request.
640             final Scriptable startingScope = getWindow();
641             final ContextFactory cf = ((JavaScriptEngine) client.getJavaScriptEngine()).getContextFactory();
642             final ContextAction action = new ContextAction() {
643                 @Override
644                 public Object run(final Context cx) {
645                     // KEY_STARTING_SCOPE maintains a stack of scopes
646                     @SuppressWarnings("unchecked")
647                     Stack<Scriptable> stack =
648                             (Stack<Scriptable>) cx.getThreadLocal(JavaScriptEngine.KEY_STARTING_SCOPE);
649                     if (null == stack) {
650                         stack = new Stack<>();
651                         cx.putThreadLocal(JavaScriptEngine.KEY_STARTING_SCOPE, stack);
652                     }
653                     stack.push(startingScope);
654 
655                     try {
656                         doSend(cx);
657                     }
658                     finally {
659                         stack.pop();
660                     }
661                     return null;
662                 }
663 
664                 @Override
665                 public String toString() {
666                     return "XMLHttpRequest "
667                                 + webRequest_.getHttpMethod().toString()
668                                 + " '" + webRequest_.getUrl().toExternalForm() + "'";
669                 }
670             };
671             final JavaScriptJob job = BackgroundJavaScriptFactory.theFactory().
672                     createJavascriptXMLHttpRequestJob(cf, action);
673             if (LOG.isDebugEnabled()) {
674                 LOG.debug("Starting XMLHttpRequest thread for asynchronous request");
675             }
676             jobID_ = getWindow().getWebWindow().getJobManager().addJob(job, page);
677         }
678     }
679 
680     /**
681      * Prepares the WebRequest that will be sent.
682      * @param content the content to send
683      */
684     private void prepareRequest(final Object content) {
685         if (content != null
686             && (HttpMethod.POST == webRequest_.getHttpMethod()
687                     || HttpMethod.PUT == webRequest_.getHttpMethod()
688                     || HttpMethod.PATCH == webRequest_.getHttpMethod())
689             && !Undefined.instance.equals(content)) {
690             if (content instanceof FormData) {
691                 ((FormData) content).fillRequest(webRequest_);
692             }
693             else {
694                 final String body = Context.toString(content);
695                 if (!body.isEmpty()) {
696                     if (LOG.isDebugEnabled()) {
697                         LOG.debug("Setting request body to: " + body);
698                     }
699                     webRequest_.setRequestBody(body);
700                 }
701             }
702         }
703     }
704 
705     /**
706      * The real send job.
707      * @param context the current context
708      */
709     private void doSend(final Context context) {
710         final WebClient wc = getWindow().getWebWindow().getWebClient();
711         try {
712             final String originHeaderValue = webRequest_.getAdditionalHeaders().get(HEADER_ORIGIN);
713             if (originHeaderValue != null && isPreflight()) {
714                 final WebRequest preflightRequest = new WebRequest(webRequest_.getUrl(), HttpMethod.OPTIONS);
715 
716                 // header origin
717                 preflightRequest.setAdditionalHeader(HEADER_ORIGIN, originHeaderValue);
718 
719                 // header request-method
720                 preflightRequest.setAdditionalHeader(
721                         HEADER_ACCESS_CONTROL_REQUEST_METHOD,
722                         webRequest_.getHttpMethod().name());
723 
724                 // header request-headers
725                 final StringBuilder builder = new StringBuilder();
726                 for (final Entry<String, String> header
727                         : new TreeMap<>(webRequest_.getAdditionalHeaders()).entrySet()) {
728                     final String name = header.getKey().toLowerCase(Locale.ROOT);
729                     if (isPreflightHeader(name, header.getValue())) {
730                         if (builder.length() != 0) {
731                             builder.append(',');
732                         }
733                         builder.append(name);
734                     }
735                 }
736                 preflightRequest.setAdditionalHeader(HEADER_ACCESS_CONTROL_REQUEST_HEADERS, builder.toString());
737 
738                 // do the preflight request
739                 final WebResponse preflightResponse = wc.loadWebResponse(preflightRequest);
740                 if (!isPreflightAuthorized(preflightResponse)) {
741                     setState(HEADERS_RECEIVED, context);
742                     setState(LOADING, context);
743                     setState(DONE, context);
744                     if (LOG.isDebugEnabled()) {
745                         LOG.debug("No permitted request for URL " + webRequest_.getUrl());
746                     }
747                     Context.throwAsScriptRuntimeEx(
748                             new RuntimeException("No permitted \"Access-Control-Allow-Origin\" header."));
749                     return;
750                 }
751             }
752             final WebResponse webResponse = wc.loadWebResponse(webRequest_);
753             if (LOG.isDebugEnabled()) {
754                 LOG.debug("Web response loaded successfully.");
755             }
756             boolean allowOriginResponse = true;
757             if (originHeaderValue != null) {
758                 String value = webResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN);
759                 allowOriginResponse = originHeaderValue.equals(value);
760                 if (isWithCredentials()) {
761                     allowOriginResponse = allowOriginResponse
762                             || (getBrowserVersion().hasFeature(XHR_WITHCREDENTIALS_ALLOW_ORIGIN_ALL)
763                             && ALLOW_ORIGIN_ALL.equals(value));
764 
765                     // second step: check the allow-credentials header for true
766                     value = webResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS);
767                     allowOriginResponse = allowOriginResponse && Boolean.parseBoolean(value);
768                 }
769                 else {
770                     allowOriginResponse = allowOriginResponse || ALLOW_ORIGIN_ALL.equals(value);
771                 }
772             }
773             if (allowOriginResponse) {
774                 if (overriddenMimeType_ == null) {
775                     webResponse_ = webResponse;
776                 }
777                 else {
778                     final int index = overriddenMimeType_.toLowerCase(Locale.ROOT).indexOf("charset=");
779                     String charsetName = "";
780                     if (index != -1) {
781                         charsetName = overriddenMimeType_.substring(index + "charset=".length());
782                     }
783                     Charset charset = EncodingSniffer.toCharset(charsetName);
784                     if (charset == null
785                             && getBrowserVersion().hasFeature(XHR_USE_DEFAULT_CHARSET_FROM_PAGE)) {
786                         final HTMLDocument doc = (HTMLDocument) containingPage_.getScriptableObject();
787                         charset = Charset.forName(doc.getDefaultCharset());
788                     }
789                     final String charsetNameFinal = charsetName;
790                     final Charset charsetFinal = charset;
791                     webResponse_ = new WebResponseWrapper(webResponse) {
792                         @Override
793                         public String getContentType() {
794                             return overriddenMimeType_;
795                         }
796                         @Override
797                         public Charset getContentCharset() {
798                             if (charsetNameFinal.isEmpty()
799                                     || (charsetFinal == null && getBrowserVersion()
800                                                 .hasFeature(XHR_USE_CONTENT_CHARSET))) {
801                                 return super.getContentCharset();
802                             }
803                             return charsetFinal;
804                         }
805                     };
806                 }
807             }
808             if (allowOriginResponse) {
809                 setState(HEADERS_RECEIVED, context);
810                 setState(LOADING, context);
811                 setState(DONE, context);
812             }
813             else {
814                 if (LOG.isDebugEnabled()) {
815                     LOG.debug("No permitted \"Access-Control-Allow-Origin\" header for URL " + webRequest_.getUrl());
816                 }
817                 throw new IOException("No permitted \"Access-Control-Allow-Origin\" header.");
818             }
819         }
820         catch (final IOException e) {
821             if (LOG.isDebugEnabled()) {
822                 LOG.debug("IOException: returning a network error response.", e);
823             }
824             webResponse_ = new NetworkErrorWebResponse(webRequest_, e);
825             setState(HEADERS_RECEIVED, context);
826             setState(DONE, context);
827             if (async_) {
828                 processError(context);
829             }
830             else {
831                 Context.throwAsScriptRuntimeEx(e);
832             }
833         }
834     }
835 
836     private boolean isPreflight() {
837         final HttpMethod method = webRequest_.getHttpMethod();
838         if (method != HttpMethod.GET && method != HttpMethod.HEAD && method != HttpMethod.POST) {
839             return true;
840         }
841         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
842             if (isPreflightHeader(header.getKey().toLowerCase(Locale.ROOT), header.getValue())) {
843                 return true;
844             }
845         }
846         return false;
847     }
848 
849     private boolean isPreflightAuthorized(final WebResponse preflightResponse) {
850         final String originHeader = preflightResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN);
851         if (!ALLOW_ORIGIN_ALL.equals(originHeader)
852                 && !webRequest_.getAdditionalHeaders().get(HEADER_ORIGIN).equals(originHeader)) {
853             return false;
854         }
855         String headersHeader = preflightResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_HEADERS);
856         if (headersHeader == null) {
857             headersHeader = "";
858         }
859         else {
860             headersHeader = headersHeader.toLowerCase(Locale.ROOT);
861         }
862         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
863             final String key = header.getKey().toLowerCase(Locale.ROOT);
864             if (isPreflightHeader(key, header.getValue())
865                     && !headersHeader.contains(key)) {
866                 return false;
867             }
868         }
869         return true;
870     }
871 
872     /**
873      * @param name header name (MUST be lower-case for performance reasons)
874      * @param value header value
875      */
876     private static boolean isPreflightHeader(final String name, final String value) {
877         if ("content-type".equals(name)) {
878             final String lcValue = value.toLowerCase(Locale.ROOT);
879             if (lcValue.startsWith(FormEncodingType.URL_ENCODED.getName())
880                 || lcValue.startsWith(FormEncodingType.MULTIPART.getName())
881                 || lcValue.startsWith("text/plain")) {
882                 return false;
883             }
884             return true;
885         }
886         if ("accept".equals(name) || "accept-language".equals(name) || "content-language".equals(name)
887                 || "referer".equals(name) || "accept-encoding".equals(name) || "origin".equals(name)) {
888             return false;
889         }
890         return true;
891     }
892 
893     /**
894      * Sets the specified header to the specified value. The <tt>open</tt> method must be
895      * called before this method, or an error will occur.
896      * @param name the name of the header being set
897      * @param value the value of the header being set
898      */
899     @JsxFunction
900     public void setRequestHeader(final String name, final String value) {
901         if (!isAuthorizedHeader(name)) {
902             LOG.warn("Ignoring XMLHttpRequest.setRequestHeader for " + name
903                 + ": it is a restricted header");
904             return;
905         }
906 
907         if (webRequest_ != null) {
908             webRequest_.setAdditionalHeader(name, value);
909         }
910         else {
911             throw Context.reportRuntimeError("The open() method must be called before setRequestHeader().");
912         }
913     }
914 
915     /**
916      * Not all request headers can be set from JavaScript.
917      * @see <a href="http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method">W3C doc</a>
918      * @param name the header name
919      * @return {@code true} if the header can be set from JavaScript
920      */
921     static boolean isAuthorizedHeader(final String name) {
922         final String nameLowerCase = name.toLowerCase(Locale.ROOT);
923         if (PROHIBITED_HEADERS_.contains(nameLowerCase)) {
924             return false;
925         }
926         else if (nameLowerCase.startsWith("proxy-") || nameLowerCase.startsWith("sec-")) {
927             return false;
928         }
929         return true;
930     }
931 
932     /**
933      * Override the mime type returned by the server (if any). This may be used, for example, to force a stream
934      * to be treated and parsed as text/xml, even if the server does not report it as such.
935      * This must be done before the send method is invoked.
936      * @param mimeType the type used to override that returned by the server (if any)
937      * @see <a href="http://xulplanet.com/references/objref/XMLHttpRequest.html#method_overrideMimeType">XUL Planet</a>
938      */
939     @JsxFunction
940     public void overrideMimeType(final String mimeType) {
941         if (getBrowserVersion().hasFeature(XHR_OVERRIDE_MIME_TYPE_BEFORE_SEND)
942                 && state_ != UNSENT && state_ != OPENED) {
943             throw Context.reportRuntimeError("Property 'overrideMimeType' not writable after sent.");
944         }
945         overriddenMimeType_ = mimeType;
946     }
947 
948     /**
949      * Returns the {@code withCredentials} property.
950      * @return the {@code withCredentials} property
951      */
952     @JsxGetter
953     public boolean isWithCredentials() {
954         return withCredentials_;
955     }
956 
957     /**
958      * Sets the {@code withCredentials} property.
959      * @param withCredentials the {@code withCredentials} property.
960      */
961     @JsxSetter
962     public void setWithCredentials(final boolean withCredentials) {
963         if (!async_ && state_ != UNSENT) {
964             if (getBrowserVersion().hasFeature(XHR_WITHCREDENTIALS_NOT_WRITEABLE_IN_SYNC_EXCEPTION)) {
965                 throw Context.reportRuntimeError("Property 'withCredentials' not writable in sync mode.");
966             }
967         }
968         withCredentials_ = withCredentials;
969     }
970 
971     /**
972      * {@inheritDoc}
973      */
974     @Override
975     public Object get(String name, final Scriptable start) {
976         if (!caseSensitiveProperties_) {
977             for (final String property : ALL_PROPERTIES_) {
978                 if (property.equalsIgnoreCase(name)) {
979                     name = property;
980                     break;
981                 }
982             }
983         }
984         return super.get(name, start);
985     }
986 
987     /**
988      * {@inheritDoc}
989      */
990     @Override
991     public void put(String name, final Scriptable start, final Object value) {
992         if (!caseSensitiveProperties_) {
993             for (final String property : ALL_PROPERTIES_) {
994                 if (property.equalsIgnoreCase(name)) {
995                     name = property;
996                     break;
997                 }
998             }
999         }
1000         super.put(name, start, value);
1001     }
1002 
1003     /**
1004      * Returns the {@code upload} property.
1005      * @return the {@code upload} property
1006      */
1007     @JsxGetter({CHROME, FF})
1008     public XMLHttpRequestUpload getUpload() {
1009         final XMLHttpRequestUpload upload = new XMLHttpRequestUpload();
1010         upload.setParentScope(getParentScope());
1011         upload.setPrototype(getPrototype(upload.getClass()));
1012         return upload;
1013     }
1014 
1015     /**
1016      * Returns the {@code upload} property - IE version.
1017      * @return the {@code upload} property
1018      */
1019     @JsxGetter(value = IE, propertyName = "upload")
1020     public XMLHttpRequestEventTarget getUploadIE() {
1021         final XMLHttpRequestEventTarget upload = new XMLHttpRequestEventTarget();
1022         upload.setParentScope(getParentScope());
1023         upload.setPrototype(getPrototype(upload.getClass()));
1024         return upload;
1025     }
1026 
1027     private static final class NetworkErrorWebResponse extends WebResponse {
1028         private final WebRequest request_;
1029         private final IOException error_;
1030 
1031         private NetworkErrorWebResponse(final WebRequest webRequest, final IOException error) {
1032             super(null, null, 0);
1033             request_ = webRequest;
1034             error_ = error;
1035         }
1036 
1037         @Override
1038         public int getStatusCode() {
1039             return 0;
1040         }
1041 
1042         @Override
1043         public String getStatusMessage() {
1044             return "";
1045         }
1046 
1047         @Override
1048         public String getContentType() {
1049             return "";
1050         }
1051 
1052         @Override
1053         public String getContentAsString() {
1054             return "";
1055         }
1056 
1057         @Override
1058         public InputStream getContentAsStream() {
1059             return null;
1060         }
1061 
1062         @Override
1063         public List<NameValuePair> getResponseHeaders() {
1064             return Collections.emptyList();
1065         }
1066 
1067         @Override
1068         public String getResponseHeaderValue(final String headerName) {
1069             return "";
1070         }
1071 
1072         @Override
1073         public long getLoadTime() {
1074             return 0;
1075         }
1076 
1077         @Override
1078         public Charset getContentCharset() {
1079             return null;
1080         }
1081 
1082         @Override
1083         public Charset getContentCharsetOrNull() {
1084             return null;
1085         }
1086 
1087         @Override
1088         public WebRequest getWebRequest() {
1089             return request_;
1090         }
1091 
1092         /**
1093          * @return the error
1094          */
1095         public IOException getError() {
1096             return error_;
1097         }
1098     }
1099 }