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.HttpHeader;
56  import com.gargoylesoftware.htmlunit.HttpMethod;
57  import com.gargoylesoftware.htmlunit.WebClient;
58  import com.gargoylesoftware.htmlunit.WebRequest;
59  import com.gargoylesoftware.htmlunit.WebResponse;
60  import com.gargoylesoftware.htmlunit.WebWindow;
61  import com.gargoylesoftware.htmlunit.html.HtmlPage;
62  import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
63  import com.gargoylesoftware.htmlunit.javascript.background.BackgroundJavaScriptFactory;
64  import com.gargoylesoftware.htmlunit.javascript.background.JavaScriptJob;
65  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
66  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstant;
67  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
68  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
69  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
70  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxSetter;
71  import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
72  import com.gargoylesoftware.htmlunit.javascript.host.event.ProgressEvent;
73  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDocument;
74  import com.gargoylesoftware.htmlunit.util.EncodingSniffer;
75  import com.gargoylesoftware.htmlunit.util.NameValuePair;
76  import com.gargoylesoftware.htmlunit.util.WebResponseWrapper;
77  import com.gargoylesoftware.htmlunit.xml.XmlPage;
78  
79  import net.sourceforge.htmlunit.corejs.javascript.Context;
80  import net.sourceforge.htmlunit.corejs.javascript.ContextAction;
81  import net.sourceforge.htmlunit.corejs.javascript.ContextFactory;
82  import net.sourceforge.htmlunit.corejs.javascript.Function;
83  import net.sourceforge.htmlunit.corejs.javascript.ScriptRuntime;
84  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
85  import net.sourceforge.htmlunit.corejs.javascript.Undefined;
86  
87  /**
88   * A JavaScript object for an {@code XMLHttpRequest}.
89   *
90   * @author Daniel Gredler
91   * @author Marc Guillemot
92   * @author Ahmed Ashour
93   * @author Stuart Begg
94   * @author Ronald Brill
95   * @author Sebastian Cato
96   * @author Frank Danek
97   * @author Jake Cobb
98   *
99   * @see <a href="http://www.w3.org/TR/XMLHttpRequest/">W3C XMLHttpRequest</a>
100  * @see <a href="http://developer.apple.com/internet/webcontent/xmlhttpreq.html">Safari documentation</a>
101  */
102 @JsxClass
103 public class XMLHttpRequest extends XMLHttpRequestEventTarget {
104 
105     private static final Log LOG = LogFactory.getLog(XMLHttpRequest.class);
106 
107     /** The object has been created, but not initialized (the open() method has not been called). */
108     @JsxConstant
109     public static final int UNSENT = 0;
110     /** The object has been created, but the send() method has not been called. */
111     @JsxConstant
112     public static final int OPENED = 1;
113     /** The send() method has been called, but the status and headers are not yet available. */
114     @JsxConstant
115     public static final int HEADERS_RECEIVED = 2;
116     /** Some data has been received. */
117     @JsxConstant
118     public static final int LOADING = 3;
119     /** All the data has been received; the complete data is available in responseBody and responseText. */
120     @JsxConstant
121     public static final int DONE = 4;
122 
123     private static final String ALLOW_ORIGIN_ALL = "*";
124 
125     private static final String[] ALL_PROPERTIES_ = {"onreadystatechange", "readyState", "responseText", "responseXML",
126         "status", "statusText", "abort", "getAllResponseHeaders", "getResponseHeader", "open", "send",
127         "setRequestHeader"};
128 
129     private static Collection<String> PROHIBITED_HEADERS_ = Arrays.asList(
130         "accept-charset", HttpHeader.ACCEPT_ENCODING_LC,
131         HttpHeader.CONNECTION_LC, HttpHeader.CONTENT_LENGTH_LC, HttpHeader.COOKIE_LC, "cookie2",
132         "content-transfer-encoding", "date", "expect",
133         HttpHeader.HOST_LC, "keep-alive", HttpHeader.REFERER_LC, "te", "trailer", "transfer-encoding",
134         "upgrade", HttpHeader.USER_AGENT_LC, "via");
135 
136     private int state_;
137     private Function stateChangeHandler_;
138     private Function loadHandler_;
139     private Function errorHandler_;
140     private WebRequest webRequest_;
141     private boolean async_;
142     private int jobID_;
143     private WebResponse webResponse_;
144     private String overriddenMimeType_;
145     private HtmlPage containingPage_;
146     private final boolean caseSensitiveProperties_;
147     private boolean withCredentials_;
148 
149     /**
150      * Creates a new instance.
151      */
152     @JsxConstructor
153     public XMLHttpRequest() {
154         this(true);
155     }
156 
157     /**
158      * Creates a new instance.
159      * @param caseSensitiveProperties if properties and methods are case sensitive
160      */
161     public XMLHttpRequest(final boolean caseSensitiveProperties) {
162         caseSensitiveProperties_ = caseSensitiveProperties;
163         state_ = UNSENT;
164     }
165 
166     /**
167      * Returns the event handler that fires on every state change.
168      * @return the event handler that fires on every state change
169      */
170     @JsxGetter
171     public Function getOnreadystatechange() {
172         return stateChangeHandler_;
173     }
174 
175     /**
176      * Sets the event handler that fires on every state change.
177      * @param stateChangeHandler the event handler that fires on every state change
178      */
179     @JsxSetter
180     public void setOnreadystatechange(final Function stateChangeHandler) {
181         stateChangeHandler_ = stateChangeHandler;
182         if (state_ == OPENED) {
183             setState(state_, null);
184         }
185     }
186 
187     /**
188      * Sets the state as specified and invokes the state change handler if one has been set.
189      * @param state the new state
190      * @param context the context within which the state change handler is to be invoked;
191      *                if {@code null}, the current thread's context is used.
192      */
193     private void setState(final int state, Context context) {
194         state_ = state;
195 
196         final BrowserVersion browser = getBrowserVersion();
197         if (stateChangeHandler_ != null && (async_ || state == DONE)) {
198             final Scriptable scope = stateChangeHandler_.getParentScope();
199             final JavaScriptEngine jsEngine = (JavaScriptEngine) containingPage_.getWebClient().getJavaScriptEngine();
200 
201             if (LOG.isDebugEnabled()) {
202                 LOG.debug("Calling onreadystatechange handler for state " + state);
203             }
204             final Object[] params = new Event[] {new Event(this, Event.TYPE_READY_STATE_CHANGE)};
205             jsEngine.callFunction(containingPage_, stateChangeHandler_, scope, this, params);
206             if (LOG.isDebugEnabled()) {
207                 if (context == null) {
208                     context = Context.getCurrentContext();
209                 }
210                 LOG.debug("onreadystatechange handler: " + context.decompileFunction(stateChangeHandler_, 4));
211                 LOG.debug("Calling onreadystatechange handler for state " + state + ". Done.");
212             }
213         }
214 
215         if (state == DONE) {
216             final JavaScriptEngine jsEngine = (JavaScriptEngine) containingPage_.getWebClient().getJavaScriptEngine();
217 
218             final ProgressEvent event = new ProgressEvent(this, Event.TYPE_LOAD);
219             final Object[] params = new Event[] {event};
220             final boolean lengthComputable = browser.hasFeature(XHR_LENGTH_COMPUTABLE);
221             if (lengthComputable) {
222                 event.setLengthComputable(true);
223             }
224 
225             if (webResponse_ != null) {
226                 final long contentLength = webResponse_.getContentLength();
227                 event.setLoaded(contentLength);
228                 if (lengthComputable) {
229                     event.setTotal(contentLength);
230                 }
231             }
232 
233             if (loadHandler_ != null) {
234                 jsEngine.callFunction(containingPage_, loadHandler_, loadHandler_.getParentScope(), this, params);
235             }
236 
237             List<Scriptable> handlers = getEventListenersContainer().getListeners(Event.TYPE_LOAD, false);
238             if (handlers != null) {
239                 for (final Scriptable scriptable : handlers) {
240                     if (scriptable instanceof Function) {
241                         final Function function = (Function) scriptable;
242                         jsEngine.callFunction(containingPage_, function, function.getParentScope(), this, params);
243                     }
244                 }
245             }
246 
247             handlers = getEventListenersContainer().getListeners(Event.TYPE_LOAD, true);
248             if (handlers != null) {
249                 for (final Scriptable scriptable : handlers) {
250                     if (scriptable instanceof Function) {
251                         final Function function = (Function) scriptable;
252                         jsEngine.callFunction(containingPage_, function, function.getParentScope(), this, params);
253                     }
254                 }
255             }
256         }
257     }
258 
259     /**
260      * Returns the event handler that fires on load.
261      * @return the event handler that fires on load
262      */
263     @JsxGetter
264     public Function getOnload() {
265         return loadHandler_;
266     }
267 
268     /**
269      * Sets the event handler that fires on load.
270      * @param loadHandler the event handler that fires on load
271      */
272     @JsxSetter
273     public void setOnload(final Function loadHandler) {
274         loadHandler_ = loadHandler;
275     }
276 
277     /**
278      * Returns the event handler that fires on error.
279      * @return the event handler that fires on error
280      */
281     @JsxGetter
282     public Function getOnerror() {
283         return errorHandler_;
284     }
285 
286     /**
287      * Sets the event handler that fires on error.
288      * @param errorHandler the event handler that fires on error
289      */
290     @JsxSetter
291     public void setOnerror(final Function errorHandler) {
292         errorHandler_ = errorHandler;
293     }
294 
295     /**
296      * Invokes the onerror handler if one has been set.
297      * @param context the context within which the onerror handler is to be invoked;
298      *                if {@code null}, the current thread's context is used.
299      */
300     private void processError(Context context) {
301         if (errorHandler_ != null) {
302             final Scriptable scope = errorHandler_.getParentScope();
303             final JavaScriptEngine jsEngine = (JavaScriptEngine) containingPage_.getWebClient().getJavaScriptEngine();
304 
305             final Object[] params = new Event[] {new ProgressEvent(this, Event.TYPE_ERROR)};
306 
307             if (LOG.isDebugEnabled()) {
308                 LOG.debug("Calling onerror handler");
309             }
310             jsEngine.callFunction(containingPage_, errorHandler_, this, scope, params);
311             if (LOG.isDebugEnabled()) {
312                 if (context == null) {
313                     context = Context.getCurrentContext();
314                 }
315                 LOG.debug("onerror handler: " + context.decompileFunction(errorHandler_, 4));
316                 LOG.debug("Calling onerror handler done.");
317             }
318         }
319     }
320 
321     /**
322      * Returns the current state of the HTTP request. The possible values are:
323      * <ul>
324      *   <li>0 = unsent</li>
325      *   <li>1 = opened</li>
326      *   <li>2 = headers_received</li>
327      *   <li>3 = loading</li>
328      *   <li>4 = done</li>
329      * </ul>
330      * @return the current state of the HTTP request
331      */
332     @JsxGetter
333     public int getReadyState() {
334         return state_;
335     }
336 
337     /**
338      * Returns a string version of the data retrieved from the server.
339      * @return a string version of the data retrieved from the server
340      */
341     @JsxGetter
342     public String getResponseText() {
343         if (state_ == UNSENT || state_ == OPENED) {
344             return "";
345         }
346         if (webResponse_ != null) {
347             final Charset encoding = webResponse_.getContentCharset();
348             if (encoding == null) {
349                 return "";
350             }
351             final String content = webResponse_.getContentAsString(encoding);
352             if (content == null) {
353                 return "";
354             }
355             return content;
356         }
357         if (LOG.isDebugEnabled()) {
358             LOG.debug("XMLHttpRequest.responseText was retrieved before the response was available.");
359         }
360         return "";
361     }
362 
363     /**
364      * Returns a DOM-compatible document object version of the data retrieved from the server.
365      * @return a DOM-compatible document object version of the data retrieved from the server
366      */
367     @JsxGetter
368     public Object getResponseXML() {
369         if (webResponse_ == null) {
370             if (LOG.isDebugEnabled()) {
371                 LOG.debug("XMLHttpRequest.responseXML returns null because there "
372                         + "in no web resonse so far (has send() been called?)");
373             }
374             return null;
375         }
376         if (webResponse_ instanceof NetworkErrorWebResponse) {
377             if (LOG.isDebugEnabled()) {
378                 LOG.debug("XMLHttpRequest.responseXML returns of a network error ("
379                         + ((NetworkErrorWebResponse) webResponse_).getError() + ")");
380             }
381             return null;
382         }
383         final String contentType = webResponse_.getContentType();
384         if (contentType.isEmpty() || contentType.contains("xml")) {
385             final WebWindow webWindow = getWindow().getWebWindow();
386             try {
387                 final XmlPage page = new XmlPage(webResponse_, webWindow);
388                 final XMLDocument document = new XMLDocument();
389                 document.setPrototype(getPrototype(document.getClass()));
390                 document.setParentScope(getWindow());
391                 document.setDomNode(page);
392                 return document;
393             }
394             catch (final IOException e) {
395                 LOG.warn("Failed parsing XML document " + webResponse_.getWebRequest().getUrl() + ": "
396                         + e.getMessage());
397                 return null;
398             }
399         }
400         if (LOG.isDebugEnabled()) {
401             LOG.debug("XMLHttpRequest.responseXML was called but the response is "
402                 + webResponse_.getContentType());
403         }
404         return null;
405     }
406 
407     /**
408      * Returns the numeric status returned by the server, such as 404 for "Not Found"
409      * or 200 for "OK".
410      * @return the numeric status returned by the server
411      */
412     @JsxGetter
413     public int getStatus() {
414         if (state_ == UNSENT || state_ == OPENED) {
415             return 0;
416         }
417         if (webResponse_ != null) {
418             return webResponse_.getStatusCode();
419         }
420 
421         LOG.error("XMLHttpRequest.status was retrieved without a response available (readyState: "
422             + state_ + ").");
423         return 0;
424     }
425 
426     /**
427      * Returns the string message accompanying the status code, such as "Not Found" or "OK".
428      * @return the string message accompanying the status code
429      */
430     @JsxGetter
431     public String getStatusText() {
432         if (state_ == UNSENT || state_ == OPENED) {
433             return "";
434         }
435         if (webResponse_ != null) {
436             return webResponse_.getStatusMessage();
437         }
438 
439         LOG.error("XMLHttpRequest.statusText was retrieved without a response available (readyState: "
440             + state_ + ").");
441         return null;
442     }
443 
444     /**
445      * Cancels the current HTTP request.
446      */
447     @JsxFunction
448     public void abort() {
449         getWindow().getWebWindow().getJobManager().stopJob(jobID_);
450     }
451 
452     /**
453      * Returns the labels and values of all the HTTP headers.
454      * @return the labels and values of all the HTTP headers
455      */
456     @JsxFunction
457     public String getAllResponseHeaders() {
458         if (state_ == UNSENT || state_ == OPENED) {
459             return "";
460         }
461         if (webResponse_ != null) {
462             final StringBuilder builder = new StringBuilder();
463             for (final NameValuePair header : webResponse_.getResponseHeaders()) {
464                 builder.append(header.getName()).append(": ").append(header.getValue()).append("\r\n");
465             }
466             if (getBrowserVersion().hasFeature(XHR_ALL_RESPONSE_HEADERS_APPEND_CRLF)) {
467                 builder.append("\r\n");
468             }
469             return builder.toString();
470         }
471 
472         LOG.error("XMLHttpRequest.getAllResponseHeaders() was called without a response available (readyState: "
473             + state_ + ").");
474         return null;
475     }
476 
477     /**
478      * Retrieves the value of an HTTP header from the response body.
479      * @param headerName the (case-insensitive) name of the header to retrieve
480      * @return the value of the specified HTTP header
481      */
482     @JsxFunction
483     public String getResponseHeader(final String headerName) {
484         if (state_ == UNSENT || state_ == OPENED) {
485             return null;
486         }
487         if (webResponse_ != null) {
488             return webResponse_.getResponseHeaderValue(headerName);
489         }
490 
491         LOG.error("XMLHttpRequest.getAllResponseHeaders(..) was called without a response available (readyState: "
492             + state_ + ").");
493         return null;
494     }
495 
496     /**
497      * Assigns the destination URL, method and other optional attributes of a pending request.
498      * @param method the method to use to send the request to the server (GET, POST, etc)
499      * @param urlParam the URL to send the request to
500      * @param asyncParam Whether or not to send the request to the server asynchronously, defaults to {@code true}
501      * @param user If authentication is needed for the specified URL, the username to use to authenticate
502      * @param password If authentication is needed for the specified URL, the password to use to authenticate
503      */
504     @JsxFunction
505     public void open(final String method, final Object urlParam, final Object asyncParam,
506         final Object user, final Object password) {
507         if ((urlParam == null || "".equals(urlParam)) && !getBrowserVersion().hasFeature(XHR_OPEN_ALLOW_EMTPY_URL)) {
508             throw Context.reportRuntimeError("URL for XHR.open can't be empty!");
509         }
510 
511         // async defaults to true if not specified
512         boolean async = true;
513         if (asyncParam != Undefined.instance) {
514             async = ScriptRuntime.toBoolean(asyncParam);
515         }
516 
517         if (!async
518                 && isWithCredentials()
519                 && getBrowserVersion().hasFeature(XHR_OPEN_WITHCREDENTIALS_TRUE_IN_SYNC_EXCEPTION)) {
520             throw Context.reportRuntimeError(
521                             "open() in sync mode is not possible because 'withCredentials' is set to true");
522         }
523 
524         final String url = Context.toString(urlParam);
525 
526         // (URL + Method + User + Password) become a WebRequest instance.
527         containingPage_ = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
528 
529         try {
530             final URL fullUrl = containingPage_.getFullyQualifiedUrl(url);
531             final URL originUrl = containingPage_.getFullyQualifiedUrl("");
532             if (!isAllowCrossDomainsFor(fullUrl)) {
533                 throw Context.reportRuntimeError("Access to restricted URI denied");
534             }
535 
536             final WebRequest request = new WebRequest(fullUrl, getBrowserVersion().getXmlHttpRequestAcceptHeader());
537             request.setCharset(UTF_8);
538             request.setAdditionalHeader(HttpHeader.REFERER, containingPage_.getUrl().toExternalForm());
539 
540             if (!isSameOrigin(originUrl, fullUrl)) {
541                 final StringBuilder origin = new StringBuilder().append(originUrl.getProtocol()).append("://")
542                         .append(originUrl.getHost());
543                 if (originUrl.getPort() != -1) {
544                     origin.append(':').append(originUrl.getPort());
545                 }
546                 request.setAdditionalHeader(HttpHeader.ORIGIN, origin.toString());
547             }
548 
549             try {
550                 request.setHttpMethod(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)));
551             }
552             catch (final IllegalArgumentException e) {
553                 LOG.info("Incorrect HTTP Method '" + method + "'");
554                 return;
555             }
556 
557             // password is ignored if no user defined
558             if (user != null && user != Undefined.instance) {
559                 final String userCred = user.toString();
560 
561                 String passwordCred = "";
562                 if (password != null && password != Undefined.instance) {
563                     passwordCred = password.toString();
564                 }
565 
566                 request.setCredentials(new UsernamePasswordCredentials(userCred, passwordCred));
567             }
568             webRequest_ = request;
569         }
570         catch (final MalformedURLException e) {
571             LOG.error("Unable to initialize XMLHttpRequest using malformed URL '" + url + "'.");
572             return;
573         }
574         // Async stays a boolean.
575         async_ = async;
576         // Change the state!
577         setState(OPENED, null);
578     }
579 
580     private boolean isAllowCrossDomainsFor(final URL newUrl) {
581         final BrowserVersion browser = getBrowserVersion();
582         if (browser.hasFeature(XHR_NO_CROSS_ORIGIN_TO_ABOUT)
583                 && "about".equals(newUrl.getProtocol())) {
584             return false;
585         }
586 
587         return true;
588     }
589 
590     private boolean isSameOrigin(final URL originUrl, final URL newUrl) {
591         if (!originUrl.getHost().equals(newUrl.getHost())) {
592             return false;
593         }
594 
595         if (getBrowserVersion().hasFeature(XHR_IGNORE_PORT_FOR_SAME_ORIGIN)) {
596             return true;
597         }
598 
599         int originPort = originUrl.getPort();
600         if (originPort == -1) {
601             originPort = originUrl.getDefaultPort();
602         }
603         int newPort = newUrl.getPort();
604         if (newPort == -1) {
605             newPort = newUrl.getDefaultPort();
606         }
607         return originPort == newPort;
608     }
609 
610     /**
611      * Sends the specified content to the server in an HTTP request and receives the response.
612      * @param content the body of the message being sent with the request
613      */
614     @JsxFunction
615     public void send(final Object content) {
616         if (webRequest_ == null) {
617             return;
618         }
619         prepareRequest(content);
620 
621         final WebClient client = getWindow().getWebWindow().getWebClient();
622         final AjaxController ajaxController = client.getAjaxController();
623         final HtmlPage page = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
624         final boolean synchron = ajaxController.processSynchron(page, webRequest_, async_);
625         if (synchron) {
626             doSend(Context.getCurrentContext());
627         }
628         else {
629             if (getBrowserVersion().hasFeature(XHR_FIRE_STATE_OPENED_AGAIN_IN_ASYNC_MODE)) {
630                 // quite strange but IE seems to fire state loading twice
631                 // in async mode (at least with HTML of the unit tests)
632                 setState(OPENED, Context.getCurrentContext());
633             }
634 
635             // Create and start a thread in which to execute the request.
636             final Scriptable startingScope = getWindow();
637             final ContextFactory cf = ((JavaScriptEngine) client.getJavaScriptEngine()).getContextFactory();
638             final ContextAction action = new ContextAction() {
639                 @Override
640                 public Object run(final Context cx) {
641                     // KEY_STARTING_SCOPE maintains a stack of scopes
642                     @SuppressWarnings("unchecked")
643                     Stack<Scriptable> stack =
644                             (Stack<Scriptable>) cx.getThreadLocal(JavaScriptEngine.KEY_STARTING_SCOPE);
645                     if (null == stack) {
646                         stack = new Stack<>();
647                         cx.putThreadLocal(JavaScriptEngine.KEY_STARTING_SCOPE, stack);
648                     }
649                     stack.push(startingScope);
650 
651                     try {
652                         doSend(cx);
653                     }
654                     finally {
655                         stack.pop();
656                     }
657                     return null;
658                 }
659 
660                 @Override
661                 public String toString() {
662                     return "XMLHttpRequest " + webRequest_.getHttpMethod() + " '" + webRequest_.getUrl() + "'";
663                 }
664             };
665             final JavaScriptJob job = BackgroundJavaScriptFactory.theFactory().
666                     createJavascriptXMLHttpRequestJob(cf, action);
667             if (LOG.isDebugEnabled()) {
668                 LOG.debug("Starting XMLHttpRequest thread for asynchronous request");
669             }
670             jobID_ = getWindow().getWebWindow().getJobManager().addJob(job, page);
671         }
672     }
673 
674     /**
675      * Prepares the WebRequest that will be sent.
676      * @param content the content to send
677      */
678     private void prepareRequest(final Object content) {
679         if (content != null
680             && (HttpMethod.POST == webRequest_.getHttpMethod()
681                     || HttpMethod.PUT == webRequest_.getHttpMethod()
682                     || HttpMethod.PATCH == webRequest_.getHttpMethod())
683             && content != Undefined.instance) {
684             if (content instanceof FormData) {
685                 ((FormData) content).fillRequest(webRequest_);
686             }
687             else {
688                 final String body = Context.toString(content);
689                 if (!body.isEmpty()) {
690                     if (LOG.isDebugEnabled()) {
691                         LOG.debug("Setting request body to: " + body);
692                     }
693                     webRequest_.setRequestBody(body);
694                 }
695             }
696         }
697     }
698 
699     /**
700      * The real send job.
701      * @param context the current context
702      */
703     private void doSend(final Context context) {
704         final WebClient wc = getWindow().getWebWindow().getWebClient();
705         try {
706             final String originHeaderValue = webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN);
707             if (originHeaderValue != null && isPreflight()) {
708                 final WebRequest preflightRequest = new WebRequest(webRequest_.getUrl(), HttpMethod.OPTIONS);
709 
710                 // header origin
711                 preflightRequest.setAdditionalHeader(HttpHeader.ORIGIN, originHeaderValue);
712 
713                 // header request-method
714                 preflightRequest.setAdditionalHeader(
715                         HttpHeader.ACCESS_CONTROL_REQUEST_METHOD,
716                         webRequest_.getHttpMethod().name());
717 
718                 // header request-headers
719                 final StringBuilder builder = new StringBuilder();
720                 for (final Entry<String, String> header
721                         : new TreeMap<>(webRequest_.getAdditionalHeaders()).entrySet()) {
722                     final String name = header.getKey().toLowerCase(Locale.ROOT);
723                     if (isPreflightHeader(name, header.getValue())) {
724                         if (builder.length() != 0) {
725                             builder.append(',');
726                         }
727                         builder.append(name);
728                     }
729                 }
730                 preflightRequest.setAdditionalHeader(HttpHeader.ACCESS_CONTROL_REQUEST_HEADERS, builder.toString());
731 
732                 // do the preflight request
733                 final WebResponse preflightResponse = wc.loadWebResponse(preflightRequest);
734                 if (!isPreflightAuthorized(preflightResponse)) {
735                     setState(HEADERS_RECEIVED, context);
736                     setState(LOADING, context);
737                     setState(DONE, context);
738                     if (LOG.isDebugEnabled()) {
739                         LOG.debug("No permitted request for URL " + webRequest_.getUrl());
740                     }
741                     Context.throwAsScriptRuntimeEx(
742                             new RuntimeException("No permitted \"Access-Control-Allow-Origin\" header."));
743                     return;
744                 }
745             }
746             final WebResponse webResponse = wc.loadWebResponse(webRequest_);
747             if (LOG.isDebugEnabled()) {
748                 LOG.debug("Web response loaded successfully.");
749             }
750             boolean allowOriginResponse = true;
751             if (originHeaderValue != null) {
752                 String value = webResponse.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN);
753                 allowOriginResponse = originHeaderValue.equals(value);
754                 if (isWithCredentials()) {
755                     allowOriginResponse = allowOriginResponse
756                             || (getBrowserVersion().hasFeature(XHR_WITHCREDENTIALS_ALLOW_ORIGIN_ALL)
757                             && ALLOW_ORIGIN_ALL.equals(value));
758 
759                     // second step: check the allow-credentials header for true
760                     value = webResponse.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_CREDENTIALS);
761                     allowOriginResponse = allowOriginResponse && Boolean.parseBoolean(value);
762                 }
763                 else {
764                     allowOriginResponse = allowOriginResponse || ALLOW_ORIGIN_ALL.equals(value);
765                 }
766             }
767             if (allowOriginResponse) {
768                 if (overriddenMimeType_ == null) {
769                     webResponse_ = webResponse;
770                 }
771                 else {
772                     final int index = overriddenMimeType_.toLowerCase(Locale.ROOT).indexOf("charset=");
773                     String charsetName = "";
774                     if (index != -1) {
775                         charsetName = overriddenMimeType_.substring(index + "charset=".length());
776                     }
777                     Charset charset = EncodingSniffer.toCharset(charsetName);
778                     if (charset == null
779                             && getBrowserVersion().hasFeature(XHR_USE_DEFAULT_CHARSET_FROM_PAGE)) {
780                         final HTMLDocument doc = (HTMLDocument) containingPage_.getScriptableObject();
781                         charset = Charset.forName(doc.getDefaultCharset());
782                     }
783                     final String charsetNameFinal = charsetName;
784                     final Charset charsetFinal = charset;
785                     webResponse_ = new WebResponseWrapper(webResponse) {
786                         @Override
787                         public String getContentType() {
788                             return overriddenMimeType_;
789                         }
790                         @Override
791                         public Charset getContentCharset() {
792                             if (charsetNameFinal.isEmpty()
793                                     || (charsetFinal == null && getBrowserVersion()
794                                                 .hasFeature(XHR_USE_CONTENT_CHARSET))) {
795                                 return super.getContentCharset();
796                             }
797                             return charsetFinal;
798                         }
799                     };
800                 }
801             }
802             if (allowOriginResponse) {
803                 setState(HEADERS_RECEIVED, context);
804                 setState(LOADING, context);
805                 setState(DONE, context);
806             }
807             else {
808                 if (LOG.isDebugEnabled()) {
809                     LOG.debug("No permitted \"Access-Control-Allow-Origin\" header for URL " + webRequest_.getUrl());
810                 }
811                 throw new IOException("No permitted \"Access-Control-Allow-Origin\" header.");
812             }
813         }
814         catch (final IOException e) {
815             if (LOG.isDebugEnabled()) {
816                 LOG.debug("IOException: returning a network error response.", e);
817             }
818             webResponse_ = new NetworkErrorWebResponse(webRequest_, e);
819             setState(HEADERS_RECEIVED, context);
820             setState(DONE, context);
821             if (async_) {
822                 processError(context);
823             }
824             else {
825                 Context.throwAsScriptRuntimeEx(e);
826             }
827         }
828     }
829 
830     private boolean isPreflight() {
831         final HttpMethod method = webRequest_.getHttpMethod();
832         if (method != HttpMethod.GET && method != HttpMethod.HEAD && method != HttpMethod.POST) {
833             return true;
834         }
835         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
836             if (isPreflightHeader(header.getKey().toLowerCase(Locale.ROOT), header.getValue())) {
837                 return true;
838             }
839         }
840         return false;
841     }
842 
843     private boolean isPreflightAuthorized(final WebResponse preflightResponse) {
844         final String originHeader = preflightResponse.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_ORIGIN);
845         if (!ALLOW_ORIGIN_ALL.equals(originHeader)
846                 && !webRequest_.getAdditionalHeaders().get(HttpHeader.ORIGIN).equals(originHeader)) {
847             return false;
848         }
849         String headersHeader = preflightResponse.getResponseHeaderValue(HttpHeader.ACCESS_CONTROL_ALLOW_HEADERS);
850         if (headersHeader == null) {
851             headersHeader = "";
852         }
853         else {
854             headersHeader = headersHeader.toLowerCase(Locale.ROOT);
855         }
856         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
857             final String key = header.getKey().toLowerCase(Locale.ROOT);
858             if (isPreflightHeader(key, header.getValue())
859                     && !headersHeader.contains(key)) {
860                 return false;
861             }
862         }
863         return true;
864     }
865 
866     /**
867      * @param name header name (MUST be lower-case for performance reasons)
868      * @param value header value
869      */
870     private static boolean isPreflightHeader(final String name, final String value) {
871         if (HttpHeader.CONTENT_TYPE_LC.equals(name)) {
872             final String lcValue = value.toLowerCase(Locale.ROOT);
873             if (lcValue.startsWith(FormEncodingType.URL_ENCODED.getName())
874                 || lcValue.startsWith(FormEncodingType.MULTIPART.getName())
875                 || lcValue.startsWith("text/plain")) {
876                 return false;
877             }
878             return true;
879         }
880         if (HttpHeader.ACCEPT_LC.equals(name)
881                 || HttpHeader.ACCEPT_LANGUAGE_LC.equals(name)
882                 || HttpHeader.CONTENT_LANGUAGE_LC.equals(name)
883                 || HttpHeader.REFERER_LC.equals(name)
884                 || "accept-encoding".equals(name)
885                 || HttpHeader.ORIGIN_LC.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 }