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 a 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 " + webRequest_.getHttpMethod() + " '" + webRequest_.getUrl() + "'";
667                 }
668             };
669             final JavaScriptJob job = BackgroundJavaScriptFactory.theFactory().
670                     createJavascriptXMLHttpRequestJob(cf, action);
671             if (LOG.isDebugEnabled()) {
672                 LOG.debug("Starting XMLHttpRequest thread for asynchronous request");
673             }
674             jobID_ = getWindow().getWebWindow().getJobManager().addJob(job, page);
675         }
676     }
677 
678     /**
679      * Prepares the WebRequest that will be sent.
680      * @param content the content to send
681      */
682     private void prepareRequest(final Object content) {
683         if (content != null
684             && (HttpMethod.POST == webRequest_.getHttpMethod()
685                     || HttpMethod.PUT == webRequest_.getHttpMethod()
686                     || HttpMethod.PATCH == webRequest_.getHttpMethod())
687             && content != Undefined.instance) {
688             if (content instanceof FormData) {
689                 ((FormData) content).fillRequest(webRequest_);
690             }
691             else {
692                 final String body = Context.toString(content);
693                 if (!body.isEmpty()) {
694                     if (LOG.isDebugEnabled()) {
695                         LOG.debug("Setting request body to: " + body);
696                     }
697                     webRequest_.setRequestBody(body);
698                 }
699             }
700         }
701     }
702 
703     /**
704      * The real send job.
705      * @param context the current context
706      */
707     private void doSend(final Context context) {
708         final WebClient wc = getWindow().getWebWindow().getWebClient();
709         try {
710             final String originHeaderValue = webRequest_.getAdditionalHeaders().get(HEADER_ORIGIN);
711             if (originHeaderValue != null && isPreflight()) {
712                 final WebRequest preflightRequest = new WebRequest(webRequest_.getUrl(), HttpMethod.OPTIONS);
713 
714                 // header origin
715                 preflightRequest.setAdditionalHeader(HEADER_ORIGIN, originHeaderValue);
716 
717                 // header request-method
718                 preflightRequest.setAdditionalHeader(
719                         HEADER_ACCESS_CONTROL_REQUEST_METHOD,
720                         webRequest_.getHttpMethod().name());
721 
722                 // header request-headers
723                 final StringBuilder builder = new StringBuilder();
724                 for (final Entry<String, String> header
725                         : new TreeMap<>(webRequest_.getAdditionalHeaders()).entrySet()) {
726                     final String name = header.getKey().toLowerCase(Locale.ROOT);
727                     if (isPreflightHeader(name, header.getValue())) {
728                         if (builder.length() != 0) {
729                             builder.append(',');
730                         }
731                         builder.append(name);
732                     }
733                 }
734                 preflightRequest.setAdditionalHeader(HEADER_ACCESS_CONTROL_REQUEST_HEADERS, builder.toString());
735 
736                 // do the preflight request
737                 final WebResponse preflightResponse = wc.loadWebResponse(preflightRequest);
738                 if (!isPreflightAuthorized(preflightResponse)) {
739                     setState(HEADERS_RECEIVED, context);
740                     setState(LOADING, context);
741                     setState(DONE, context);
742                     if (LOG.isDebugEnabled()) {
743                         LOG.debug("No permitted request for URL " + webRequest_.getUrl());
744                     }
745                     Context.throwAsScriptRuntimeEx(
746                             new RuntimeException("No permitted \"Access-Control-Allow-Origin\" header."));
747                     return;
748                 }
749             }
750             final WebResponse webResponse = wc.loadWebResponse(webRequest_);
751             if (LOG.isDebugEnabled()) {
752                 LOG.debug("Web response loaded successfully.");
753             }
754             boolean allowOriginResponse = true;
755             if (originHeaderValue != null) {
756                 String value = webResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN);
757                 allowOriginResponse = originHeaderValue.equals(value);
758                 if (isWithCredentials()) {
759                     allowOriginResponse = allowOriginResponse
760                             || (getBrowserVersion().hasFeature(XHR_WITHCREDENTIALS_ALLOW_ORIGIN_ALL)
761                             && ALLOW_ORIGIN_ALL.equals(value));
762 
763                     // second step: check the allow-credentials header for true
764                     value = webResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS);
765                     allowOriginResponse = allowOriginResponse && Boolean.parseBoolean(value);
766                 }
767                 else {
768                     allowOriginResponse = allowOriginResponse || ALLOW_ORIGIN_ALL.equals(value);
769                 }
770             }
771             if (allowOriginResponse) {
772                 if (overriddenMimeType_ == null) {
773                     webResponse_ = webResponse;
774                 }
775                 else {
776                     final int index = overriddenMimeType_.toLowerCase(Locale.ROOT).indexOf("charset=");
777                     String charsetName = "";
778                     if (index != -1) {
779                         charsetName = overriddenMimeType_.substring(index + "charset=".length());
780                     }
781                     Charset charset = EncodingSniffer.toCharset(charsetName);
782                     if (charset == null
783                             && getBrowserVersion().hasFeature(XHR_USE_DEFAULT_CHARSET_FROM_PAGE)) {
784                         final HTMLDocument doc = (HTMLDocument) containingPage_.getScriptableObject();
785                         charset = Charset.forName(doc.getDefaultCharset());
786                     }
787                     final String charsetNameFinal = charsetName;
788                     final Charset charsetFinal = charset;
789                     webResponse_ = new WebResponseWrapper(webResponse) {
790                         @Override
791                         public String getContentType() {
792                             return overriddenMimeType_;
793                         }
794                         @Override
795                         public Charset getContentCharset() {
796                             if (charsetNameFinal.isEmpty()
797                                     || (charsetFinal == null && getBrowserVersion()
798                                                 .hasFeature(XHR_USE_CONTENT_CHARSET))) {
799                                 return super.getContentCharset();
800                             }
801                             return charsetFinal;
802                         }
803                     };
804                 }
805             }
806             if (allowOriginResponse) {
807                 setState(HEADERS_RECEIVED, context);
808                 setState(LOADING, context);
809                 setState(DONE, context);
810             }
811             else {
812                 if (LOG.isDebugEnabled()) {
813                     LOG.debug("No permitted \"Access-Control-Allow-Origin\" header for URL " + webRequest_.getUrl());
814                 }
815                 throw new IOException("No permitted \"Access-Control-Allow-Origin\" header.");
816             }
817         }
818         catch (final IOException e) {
819             if (LOG.isDebugEnabled()) {
820                 LOG.debug("IOException: returning a network error response.", e);
821             }
822             webResponse_ = new NetworkErrorWebResponse(webRequest_, e);
823             setState(HEADERS_RECEIVED, context);
824             setState(DONE, context);
825             if (async_) {
826                 processError(context);
827             }
828             else {
829                 Context.throwAsScriptRuntimeEx(e);
830             }
831         }
832     }
833 
834     private boolean isPreflight() {
835         final HttpMethod method = webRequest_.getHttpMethod();
836         if (method != HttpMethod.GET && method != HttpMethod.HEAD && method != HttpMethod.POST) {
837             return true;
838         }
839         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
840             if (isPreflightHeader(header.getKey().toLowerCase(Locale.ROOT), header.getValue())) {
841                 return true;
842             }
843         }
844         return false;
845     }
846 
847     private boolean isPreflightAuthorized(final WebResponse preflightResponse) {
848         final String originHeader = preflightResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN);
849         if (!ALLOW_ORIGIN_ALL.equals(originHeader)
850                 && !webRequest_.getAdditionalHeaders().get(HEADER_ORIGIN).equals(originHeader)) {
851             return false;
852         }
853         String headersHeader = preflightResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_HEADERS);
854         if (headersHeader == null) {
855             headersHeader = "";
856         }
857         else {
858             headersHeader = headersHeader.toLowerCase(Locale.ROOT);
859         }
860         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
861             final String key = header.getKey().toLowerCase(Locale.ROOT);
862             if (isPreflightHeader(key, header.getValue())
863                     && !headersHeader.contains(key)) {
864                 return false;
865             }
866         }
867         return true;
868     }
869 
870     /**
871      * @param name header name (MUST be lower-case for performance reasons)
872      * @param value header value
873      */
874     private static boolean isPreflightHeader(final String name, final String value) {
875         if ("content-type".equals(name)) {
876             final String lcValue = value.toLowerCase(Locale.ROOT);
877             if (lcValue.startsWith(FormEncodingType.URL_ENCODED.getName())
878                 || lcValue.startsWith(FormEncodingType.MULTIPART.getName())
879                 || lcValue.startsWith("text/plain")) {
880                 return false;
881             }
882             return true;
883         }
884         if ("accept".equals(name) || "accept-language".equals(name) || "content-language".equals(name)
885                 || "referer".equals(name) || "accept-encoding".equals(name) || "origin".equals(name)) {
886             return false;
887         }
888         return true;
889     }
890 
891     /**
892      * Sets the specified header to the specified value. The <tt>open</tt> method must be
893      * called before this method, or an error will occur.
894      * @param name the name of the header being set
895      * @param value the value of the header being set
896      */
897     @JsxFunction
898     public void setRequestHeader(final String name, final String value) {
899         if (!isAuthorizedHeader(name)) {
900             LOG.warn("Ignoring XMLHttpRequest.setRequestHeader for " + name
901                 + ": it is a restricted header");
902             return;
903         }
904 
905         if (webRequest_ != null) {
906             webRequest_.setAdditionalHeader(name, value);
907         }
908         else {
909             throw Context.reportRuntimeError("The open() method must be called before setRequestHeader().");
910         }
911     }
912 
913     /**
914      * Not all request headers can be set from JavaScript.
915      * @see <a href="http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method">W3C doc</a>
916      * @param name the header name
917      * @return {@code true} if the header can be set from JavaScript
918      */
919     static boolean isAuthorizedHeader(final String name) {
920         final String nameLowerCase = name.toLowerCase(Locale.ROOT);
921         if (PROHIBITED_HEADERS_.contains(nameLowerCase)) {
922             return false;
923         }
924         else if (nameLowerCase.startsWith("proxy-") || nameLowerCase.startsWith("sec-")) {
925             return false;
926         }
927         return true;
928     }
929 
930     /**
931      * Override the mime type returned by the server (if any). This may be used, for example, to force a stream
932      * to be treated and parsed as text/xml, even if the server does not report it as such.
933      * This must be done before the send method is invoked.
934      * @param mimeType the type used to override that returned by the server (if any)
935      * @see <a href="http://xulplanet.com/references/objref/XMLHttpRequest.html#method_overrideMimeType">XUL Planet</a>
936      */
937     @JsxFunction
938     public void overrideMimeType(final String mimeType) {
939         if (getBrowserVersion().hasFeature(XHR_OVERRIDE_MIME_TYPE_BEFORE_SEND)
940                 && state_ != UNSENT && state_ != OPENED) {
941             throw Context.reportRuntimeError("Property 'overrideMimeType' not writable after sent.");
942         }
943         overriddenMimeType_ = mimeType;
944     }
945 
946     /**
947      * Returns the {@code withCredentials} property.
948      * @return the {@code withCredentials} property
949      */
950     @JsxGetter
951     public boolean isWithCredentials() {
952         return withCredentials_;
953     }
954 
955     /**
956      * Sets the {@code withCredentials} property.
957      * @param withCredentials the {@code withCredentials} property.
958      */
959     @JsxSetter
960     public void setWithCredentials(final boolean withCredentials) {
961         if (!async_ && state_ != UNSENT) {
962             if (getBrowserVersion().hasFeature(XHR_WITHCREDENTIALS_NOT_WRITEABLE_IN_SYNC_EXCEPTION)) {
963                 throw Context.reportRuntimeError("Property 'withCredentials' not writable in sync mode.");
964             }
965         }
966         withCredentials_ = withCredentials;
967     }
968 
969     /**
970      * {@inheritDoc}
971      */
972     @Override
973     public Object get(String name, final Scriptable start) {
974         if (!caseSensitiveProperties_) {
975             for (final String property : ALL_PROPERTIES_) {
976                 if (property.equalsIgnoreCase(name)) {
977                     name = property;
978                     break;
979                 }
980             }
981         }
982         return super.get(name, start);
983     }
984 
985     /**
986      * {@inheritDoc}
987      */
988     @Override
989     public void put(String name, final Scriptable start, final Object value) {
990         if (!caseSensitiveProperties_) {
991             for (final String property : ALL_PROPERTIES_) {
992                 if (property.equalsIgnoreCase(name)) {
993                     name = property;
994                     break;
995                 }
996             }
997         }
998         super.put(name, start, value);
999     }
1000 
1001     /**
1002      * Returns the {@code upload} property.
1003      * @return the {@code upload} property
1004      */
1005     @JsxGetter({CHROME, FF})
1006     public XMLHttpRequestUpload getUpload() {
1007         final XMLHttpRequestUpload upload = new XMLHttpRequestUpload();
1008         upload.setParentScope(getParentScope());
1009         upload.setPrototype(getPrototype(upload.getClass()));
1010         return upload;
1011     }
1012 
1013     /**
1014      * Returns the {@code upload} property - IE version.
1015      * @return the {@code upload} property
1016      */
1017     @JsxGetter(value = IE, propertyName = "upload")
1018     public XMLHttpRequestEventTarget getUploadIE() {
1019         final XMLHttpRequestEventTarget upload = new XMLHttpRequestEventTarget();
1020         upload.setParentScope(getParentScope());
1021         upload.setPrototype(getPrototype(upload.getClass()));
1022         return upload;
1023     }
1024 
1025     private static final class NetworkErrorWebResponse extends WebResponse {
1026         private final WebRequest request_;
1027         private final IOException error_;
1028 
1029         private NetworkErrorWebResponse(final WebRequest webRequest, final IOException error) {
1030             super(null, null, 0);
1031             request_ = webRequest;
1032             error_ = error;
1033         }
1034 
1035         @Override
1036         public int getStatusCode() {
1037             return 0;
1038         }
1039 
1040         @Override
1041         public String getStatusMessage() {
1042             return "";
1043         }
1044 
1045         @Override
1046         public String getContentType() {
1047             return "";
1048         }
1049 
1050         @Override
1051         public String getContentAsString() {
1052             return "";
1053         }
1054 
1055         @Override
1056         public InputStream getContentAsStream() {
1057             return null;
1058         }
1059 
1060         @Override
1061         public List<NameValuePair> getResponseHeaders() {
1062             return Collections.emptyList();
1063         }
1064 
1065         @Override
1066         public String getResponseHeaderValue(final String headerName) {
1067             return "";
1068         }
1069 
1070         @Override
1071         public long getLoadTime() {
1072             return 0;
1073         }
1074 
1075         @Override
1076         public Charset getContentCharset() {
1077             return null;
1078         }
1079 
1080         @Override
1081         public Charset getContentCharsetOrNull() {
1082             return null;
1083         }
1084 
1085         @Override
1086         public WebRequest getWebRequest() {
1087             return request_;
1088         }
1089 
1090         /**
1091          * @return the error
1092          */
1093         public IOException getError() {
1094             return error_;
1095         }
1096     }
1097 }