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.activex.javascript.msxml;
16  
17  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.IE;
18  import static java.nio.charset.StandardCharsets.UTF_8;
19  
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.net.MalformedURLException;
23  import java.net.URL;
24  import java.nio.charset.Charset;
25  import java.util.Arrays;
26  import java.util.Collection;
27  import java.util.Collections;
28  import java.util.List;
29  import java.util.Locale;
30  import java.util.Map.Entry;
31  import java.util.Stack;
32  
33  import org.apache.commons.lang3.ArrayUtils;
34  import org.apache.commons.lang3.StringUtils;
35  import org.apache.commons.logging.Log;
36  import org.apache.commons.logging.LogFactory;
37  import org.apache.http.auth.UsernamePasswordCredentials;
38  
39  import com.gargoylesoftware.htmlunit.AjaxController;
40  import com.gargoylesoftware.htmlunit.FormEncodingType;
41  import com.gargoylesoftware.htmlunit.HttpMethod;
42  import com.gargoylesoftware.htmlunit.WebClient;
43  import com.gargoylesoftware.htmlunit.WebRequest;
44  import com.gargoylesoftware.htmlunit.WebResponse;
45  import com.gargoylesoftware.htmlunit.html.HtmlPage;
46  import com.gargoylesoftware.htmlunit.javascript.JavaScriptEngine;
47  import com.gargoylesoftware.htmlunit.javascript.background.BackgroundJavaScriptFactory;
48  import com.gargoylesoftware.htmlunit.javascript.background.JavaScriptJob;
49  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
50  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
51  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
52  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxGetter;
53  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxSetter;
54  import com.gargoylesoftware.htmlunit.javascript.host.xml.FormData;
55  import com.gargoylesoftware.htmlunit.util.NameValuePair;
56  import com.gargoylesoftware.htmlunit.xml.XmlPage;
57  
58  import net.sourceforge.htmlunit.corejs.javascript.Context;
59  import net.sourceforge.htmlunit.corejs.javascript.ContextAction;
60  import net.sourceforge.htmlunit.corejs.javascript.ContextFactory;
61  import net.sourceforge.htmlunit.corejs.javascript.Function;
62  import net.sourceforge.htmlunit.corejs.javascript.ScriptRuntime;
63  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
64  import net.sourceforge.htmlunit.corejs.javascript.Undefined;
65  
66  /**
67   * A JavaScript object for MSXML's (ActiveX) XMLHTTPRequest.<br>
68   * Provides client-side protocol support for communication with HTTP servers.
69   * @see <a href="http://msdn.microsoft.com/en-us/library/ms759148.aspx">MSDN documentation</a>
70   *
71   * @author Daniel Gredler
72   * @author Marc Guillemot
73   * @author Ahmed Ashour
74   * @author Stuart Begg
75   * @author Ronald Brill
76   * @author Sebastian Cato
77   * @author Frank Danek
78   * @author Jake Cobb
79   */
80  @JsxClass(IE)
81  public class XMLHTTPRequest extends MSXMLScriptable {
82  
83      private static final Log LOG = LogFactory.getLog(XMLHTTPRequest.class);
84  
85      /** The object has been created, but not initialized (the open() method has not been called). */
86      public static final int STATE_UNSENT = 0;
87      /** The object has been created, but the send() method has not been called. */
88      public static final int STATE_OPENED = 1;
89      /** The send() method has been called, but the status and headers are not yet available. */
90      public static final int STATE_HEADERS_RECEIVED = 2;
91      /** Some data has been received. */
92      public static final int STATE_LOADING = 3;
93      /** All the data has been received; the complete data is available in responseBody and responseText. */
94      public static final int STATE_DONE = 4;
95  
96      private static final String HEADER_ORIGIN = "Origin";
97      private static final char REQUEST_HEADERS_SEPARATOR = ',';
98  
99      private static final String HEADER_ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method";
100     private static final String HEADER_ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers";
101     private static final String HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
102     private static final String HEADER_ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers";
103 
104     private static final String ALLOW_ORIGIN_ALL = "*";
105 
106     private static final String[] ALL_PROPERTIES_ = {"onreadystatechange", "readyState", "responseText", "responseXML",
107         "status", "statusText", "abort", "getAllResponseHeaders", "getResponseHeader", "open", "send",
108         "setRequestHeader"};
109 
110     private static Collection<String> PROHIBITED_HEADERS_ = Arrays.asList("accept-charset", "accept-encoding",
111         "connection", "content-length", "cookie", "cookie2", "content-transfer-encoding", "date", "expect",
112         "host", "keep-alive", "referer", "te", "trailer", "transfer-encoding", "upgrade", "user-agent", "via");
113 
114     private int state_ = STATE_UNSENT;
115     private Function stateChangeHandler_;
116     private WebRequest webRequest_;
117     private boolean async_;
118     private int jobID_;
119     private WebResponse webResponse_;
120     private HtmlPage containingPage_;
121     private boolean openedMultipleTimes_;
122     private boolean sent_;
123 
124     /**
125      * Creates an instance.
126      */
127     @JsxConstructor
128     public XMLHTTPRequest() {
129     }
130 
131     /**
132      * Returns the event handler to be called when the <code>readyState</code> property changes.
133      * @return the event handler to be called when the readyState property changes
134      */
135     @JsxGetter
136     public Object getOnreadystatechange() {
137         if (stateChangeHandler_ == null) {
138             return Undefined.instance;
139         }
140         return stateChangeHandler_;
141     }
142 
143     /**
144      * Sets the event handler to be called when the <code>readyState</code> property changes.
145      * @param stateChangeHandler the event handler to be called when the readyState property changes
146      */
147     @JsxSetter
148     public void setOnreadystatechange(final Function stateChangeHandler) {
149         stateChangeHandler_ = stateChangeHandler;
150         if (state_ == STATE_OPENED) {
151             setState(state_, null);
152         }
153     }
154 
155     /**
156      * Sets the state as specified and invokes the state change handler if one has been set.
157      * @param state the new state
158      * @param context the context within which the state change handler is to be invoked;
159      *     if {@code null}, the current thread's context is used
160      */
161     private void setState(final int state, Context context) {
162         state_ = state;
163 
164         if (stateChangeHandler_ != null && !openedMultipleTimes_) {
165             final Scriptable scope = stateChangeHandler_.getParentScope();
166             final JavaScriptEngine jsEngine = (JavaScriptEngine) containingPage_.getWebClient().getJavaScriptEngine();
167 
168             if (LOG.isDebugEnabled()) {
169                 LOG.debug("Calling onreadystatechange handler for state " + state);
170             }
171             final Object[] params = ArrayUtils.EMPTY_OBJECT_ARRAY;
172 
173             jsEngine.callFunction(containingPage_, stateChangeHandler_, scope, this, params);
174             if (LOG.isDebugEnabled()) {
175                 if (context == null) {
176                     context = Context.getCurrentContext();
177                 }
178                 LOG.debug("onreadystatechange handler: " + context.decompileFunction(stateChangeHandler_, 4));
179                 LOG.debug("Calling onreadystatechange handler for state " + state + ". Done.");
180             }
181         }
182     }
183 
184     /**
185      * Returns the state of the request. The possible values are:
186      * <ul>
187      *   <li>0 = unsent</li>
188      *   <li>1 = opened</li>
189      *   <li>2 = headers_received</li>
190      *   <li>3 = loading</li>
191      *   <li>4 = done</li>
192      * </ul>
193      * @return the state of the request
194      */
195     @JsxGetter
196     public int getReadyState() {
197         return state_;
198     }
199 
200     /**
201      * Returns the response entity body as a string.
202      * @return the response entity body as a string
203      */
204     @JsxGetter
205     public String getResponseText() {
206         if (state_ == STATE_UNSENT) {
207             throw Context.reportRuntimeError(
208                     "The data necessary to complete this operation is not yet available (request not opened).");
209         }
210         if (state_ != STATE_DONE) {
211             throw Context.reportRuntimeError("Unspecified error (request not sent).");
212         }
213         if (webResponse_ != null) {
214             final String content = webResponse_.getContentAsString();
215             if (content == null) {
216                 return "";
217             }
218             return content;
219         }
220         if (LOG.isDebugEnabled()) {
221             LOG.debug("XMLHTTPRequest.responseText was retrieved before the response was available.");
222         }
223         return "";
224     }
225 
226     /**
227      * Returns the parsed response entity body.
228      * @return the parsed response entity body
229      */
230     @JsxGetter
231     public Object getResponseXML() {
232         if (state_ == STATE_UNSENT) {
233             throw Context.reportRuntimeError("Unspecified error (request not opened).");
234         }
235         if (state_ == STATE_DONE && webResponse_ != null && !(webResponse_ instanceof NetworkErrorWebResponse)) {
236             final String contentType = webResponse_.getContentType();
237             if (contentType.contains("xml")) {
238                 try {
239                     final XmlPage page = new XmlPage(webResponse_, getWindow().getWebWindow(), true, false);
240                     final XMLDOMDocument doc = new XMLDOMDocument();
241                     doc.setDomNode(page);
242                     doc.setPrototype(getPrototype(doc.getClass()));
243                     doc.setEnvironment(getEnvironment());
244                     doc.setParentScope(getWindow());
245                     return doc;
246                 }
247                 catch (final IOException e) {
248                     LOG.warn("Failed parsing XML document " + webResponse_.getWebRequest().getUrl() + ": "
249                             + e.getMessage());
250                     return null;
251                 }
252             }
253         }
254         final XMLDOMDocument doc = new XMLDOMDocument(getWindow().getWebWindow());
255         doc.setPrototype(getPrototype(doc.getClass()));
256         doc.setEnvironment(getEnvironment());
257         return doc;
258     }
259 
260     /**
261      * Returns the HTTP status code returned by a request.
262      * @return the HTTP status code returned by a request
263      */
264     @JsxGetter
265     public int getStatus() {
266         if (state_ != STATE_DONE) {
267             throw Context.reportRuntimeError("Unspecified error (request not sent).");
268         }
269         if (webResponse_ != null) {
270             return webResponse_.getStatusCode();
271         }
272 
273         LOG.error("XMLHTTPRequest.status was retrieved without a response available (readyState: "
274             + state_ + ").");
275         return 0;
276     }
277 
278     /**
279      * Returns the HTTP response line status.
280      * @return the HTTP response line status
281      */
282     @JsxGetter
283     public String getStatusText() {
284         if (state_ != STATE_DONE) {
285             throw Context.reportRuntimeError("Unspecified error (request not sent).");
286         }
287         if (webResponse_ != null) {
288             return webResponse_.getStatusMessage();
289         }
290 
291         LOG.error("XMLHTTPRequest.statusText was retrieved without a response available (readyState: "
292             + state_ + ").");
293         return null;
294     }
295 
296     /**
297      * Cancels the current HTTP request.
298      */
299     @JsxFunction
300     public void abort() {
301         getWindow().getWebWindow().getJobManager().stopJob(jobID_);
302         setState(STATE_UNSENT, Context.getCurrentContext());
303     }
304 
305     /**
306      * Returns the values of all the HTTP headers.
307      * @return the resulting header information
308      */
309     @JsxFunction
310     public String getAllResponseHeaders() {
311         if (state_ == STATE_UNSENT || state_ == STATE_OPENED) {
312             throw Context.reportRuntimeError("Unspecified error (request not sent).");
313         }
314         if (webResponse_ != null) {
315             final StringBuilder builder = new StringBuilder();
316             for (final NameValuePair header : webResponse_.getResponseHeaders()) {
317                 builder.append(header.getName()).append(": ").append(header.getValue()).append("\r\n");
318             }
319             return builder.append("\r\n").toString();
320         }
321 
322         LOG.error("XMLHTTPRequest.getAllResponseHeaders() was called without a response available (readyState: "
323             + state_ + ").");
324         return null;
325     }
326 
327     /**
328      * Retrieves the value of an HTTP header from the response body.
329      * @param header the case-insensitive header name
330      * @return the resulting header information
331      */
332     @JsxFunction
333     public String getResponseHeader(final String header) {
334         if (state_ == STATE_UNSENT || state_ == STATE_OPENED) {
335             throw Context.reportRuntimeError("Unspecified error (request not sent).");
336         }
337         if (header == null || "null".equals(header)) {
338             throw Context.reportRuntimeError("Type mismatch (header is null).");
339         }
340         if ("".equals(header)) {
341             throw Context.reportRuntimeError("Invalid argument (header is empty).");
342         }
343         if (webResponse_ != null) {
344             final String value = webResponse_.getResponseHeaderValue(header);
345             if (value == null) {
346                 return "";
347             }
348             return value;
349         }
350 
351         LOG.error("XMLHTTPRequest.getResponseHeader(..) was called without a response available (readyState: "
352             + state_ + ").");
353         return null;
354     }
355 
356     /**
357      * Initializes the request and specifies the method, URL, and authentication information for the request.
358      * @param method the HTTP method used to open the connection, such as GET, POST, PUT, or PROPFIND;
359      *      for XMLHTTP, this parameter is not case-sensitive; the verbs TRACE and TRACK are not allowed.
360      * @param url the requested URL; this can be either an absolute URL or a relative URL
361      * @param asyncParam indicator of whether the call is asynchronous; the default is {@code true} (the call
362      *     returns immediately); if set to {@code true}, attach an <code>onreadystatechange</code> property
363      *     callback so that you can tell when the <code>send</code> call has completed
364      * @param user the name of the user for authentication
365      * @param password the password for authentication
366      */
367     @JsxFunction
368     public void open(final String method, final Object url, final Object asyncParam,
369         final Object user, final Object password) {
370         if (method == null || "null".equals(method)) {
371             throw Context.reportRuntimeError("Type mismatch (method is null).");
372         }
373         if (url == null || "null".equals(url)) {
374             throw Context.reportRuntimeError("Type mismatch (url is null).");
375         }
376         state_ = STATE_UNSENT;
377         openedMultipleTimes_ = webRequest_ != null;
378         sent_ = false;
379         webRequest_ = null;
380         webResponse_ = null;
381         if ("".equals(method) || "TRACE".equalsIgnoreCase(method)) {
382             throw Context.reportRuntimeError("Invalid procedure call or argument (method is invalid).");
383         }
384         if ("".equals(url)) {
385             throw Context.reportRuntimeError("Invalid procedure call or argument (url is empty).");
386         }
387 
388         // defaults to true if not specified
389         boolean async = true;
390         if (asyncParam != Undefined.instance) {
391             async = ScriptRuntime.toBoolean(asyncParam);
392         }
393 
394         final String urlAsString = Context.toString(url);
395 
396         // (URL + Method + User + Password) become a WebRequest instance.
397         containingPage_ = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
398 
399         try {
400             final URL fullUrl = containingPage_.getFullyQualifiedUrl(urlAsString);
401 
402             final WebRequest request = new WebRequest(fullUrl);
403             request.setCharset(UTF_8);
404             request.setAdditionalHeader("Referer", containingPage_.getUrl().toExternalForm());
405 
406             request.setHttpMethod(HttpMethod.valueOf(method.toUpperCase(Locale.ROOT)));
407 
408             // password is ignored if no user defined
409             if (user != null && user != Undefined.instance) {
410                 final String userCred = user.toString();
411 
412                 String passwordCred = "";
413                 if (password != null && password != Undefined.instance) {
414                     passwordCred = password.toString();
415                 }
416 
417                 request.setCredentials(new UsernamePasswordCredentials(userCred, passwordCred));
418             }
419             webRequest_ = request;
420         }
421         catch (final MalformedURLException e) {
422             LOG.error("Unable to initialize XMLHTTPRequest using malformed URL '" + urlAsString + "'.");
423             return;
424         }
425         catch (final IllegalArgumentException e) {
426             LOG.error("Unable to initialize XMLHTTPRequest using illegal argument '" + e.getMessage() + "'.");
427             webRequest_ = null;
428         }
429         // Async stays a boolean.
430         async_ = async;
431         // Change the state!
432         setState(STATE_OPENED, null);
433     }
434 
435     /**
436      * Sends an HTTP request to the server and receives a response.
437      * @param body the body of the message being sent with the request.
438      */
439     @JsxFunction
440     public void send(final Object body) {
441         if (webRequest_ == null) {
442             setState(STATE_DONE, Context.getCurrentContext());
443             return;
444         }
445         if (sent_) {
446             throw Context.reportRuntimeError("Unspecified error (request already sent).");
447         }
448         sent_ = true;
449 
450         prepareRequest(body);
451 
452         // quite strange but IE seems to fire state loading twice
453         setState(STATE_OPENED, Context.getCurrentContext());
454 
455         final WebClient client = getWindow().getWebWindow().getWebClient();
456         final AjaxController ajaxController = client.getAjaxController();
457         final HtmlPage page = (HtmlPage) getWindow().getWebWindow().getEnclosedPage();
458         final boolean synchron = ajaxController.processSynchron(page, webRequest_, async_);
459         if (synchron) {
460             doSend(Context.getCurrentContext());
461         }
462         else {
463             // Create and start a thread in which to execute the request.
464             final Scriptable startingScope = getWindow();
465             final ContextFactory cf = ((JavaScriptEngine) client.getJavaScriptEngine()).getContextFactory();
466             final ContextAction action = new ContextAction() {
467                 @Override
468                 public Object run(final Context cx) {
469                     // KEY_STARTING_SCOPE maintains a stack of scopes
470                     @SuppressWarnings("unchecked")
471                     Stack<Scriptable> stack =
472                             (Stack<Scriptable>) cx.getThreadLocal(JavaScriptEngine.KEY_STARTING_SCOPE);
473                     if (null == stack) {
474                         stack = new Stack<>();
475                         cx.putThreadLocal(JavaScriptEngine.KEY_STARTING_SCOPE, stack);
476                     }
477                     stack.push(startingScope);
478 
479                     try {
480                         doSend(cx);
481                     }
482                     finally {
483                         stack.pop();
484                     }
485                     return null;
486                 }
487             };
488             final JavaScriptJob job = BackgroundJavaScriptFactory.theFactory().
489                     createJavascriptXMLHttpRequestJob(cf, action);
490             if (LOG.isDebugEnabled()) {
491                 LOG.debug("Starting XMLHTTPRequest thread for asynchronous request");
492             }
493             jobID_ = getWindow().getWebWindow().getJobManager().addJob(job, page);
494         }
495     }
496 
497     /**
498      * Prepares the WebRequest that will be sent.
499      * @param content the content to send
500      */
501     private void prepareRequest(final Object content) {
502         if (content != null && !Undefined.instance.equals(content)) {
503             if (!"".equals(content) && HttpMethod.GET == webRequest_.getHttpMethod()) {
504                 webRequest_.setHttpMethod(HttpMethod.POST);
505             }
506             if (HttpMethod.POST == webRequest_.getHttpMethod()
507                     || HttpMethod.PUT == webRequest_.getHttpMethod()
508                     || HttpMethod.PATCH == webRequest_.getHttpMethod()) {
509                 if (content instanceof FormData) {
510                     ((FormData) content).fillRequest(webRequest_);
511                 }
512                 else {
513                     final String body = Context.toString(content);
514                     if (!body.isEmpty()) {
515                         if (LOG.isDebugEnabled()) {
516                             LOG.debug("Setting request body to: " + body);
517                         }
518                         webRequest_.setRequestBody(body);
519                     }
520                 }
521             }
522         }
523     }
524 
525     /**
526      * The real send job.
527      * @param context the current context
528      */
529     private void doSend(final Context context) {
530         final WebClient wc = getWindow().getWebWindow().getWebClient();
531         try {
532             final String originHeaderValue = webRequest_.getAdditionalHeaders().get(HEADER_ORIGIN);
533             if (originHeaderValue != null && isPreflight()) {
534                 final WebRequest preflightRequest = new WebRequest(webRequest_.getUrl(), HttpMethod.OPTIONS);
535 
536                 // header origin
537                 preflightRequest.setAdditionalHeader(HEADER_ORIGIN, originHeaderValue);
538 
539                 // header request-method
540                 preflightRequest.setAdditionalHeader(
541                         HEADER_ACCESS_CONTROL_REQUEST_METHOD,
542                         webRequest_.getHttpMethod().name());
543 
544                 // header request-headers
545                 final StringBuilder builder = new StringBuilder();
546                 for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
547                     final String name = header.getKey().toLowerCase(Locale.ROOT);
548                     if (isPreflightHeader(name, header.getValue())) {
549                         if (builder.length() != 0) {
550                             builder.append(REQUEST_HEADERS_SEPARATOR);
551                         }
552                         builder.append(name);
553                     }
554                 }
555                 preflightRequest.setAdditionalHeader(HEADER_ACCESS_CONTROL_REQUEST_HEADERS, builder.toString());
556 
557                 // do the preflight request
558                 final WebResponse preflightResponse = wc.loadWebResponse(preflightRequest);
559                 if (!isPreflightAuthorized(preflightResponse)) {
560                     setState(STATE_HEADERS_RECEIVED, context);
561                     setState(STATE_LOADING, context);
562                     setState(STATE_DONE, context);
563                     if (LOG.isDebugEnabled()) {
564                         LOG.debug("No permitted request for URL " + webRequest_.getUrl());
565                     }
566                     Context.throwAsScriptRuntimeEx(
567                             new RuntimeException("No permitted \"Access-Control-Allow-Origin\" header."));
568                     return;
569                 }
570             }
571             final WebResponse webResponse = wc.loadWebResponse(webRequest_);
572             if (LOG.isDebugEnabled()) {
573                 LOG.debug("Web response loaded successfully.");
574             }
575             boolean allowOriginResponse = true;
576             if (originHeaderValue != null) {
577                 final String value = webResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN);
578                 allowOriginResponse = originHeaderValue.equals(value);
579                 allowOriginResponse = allowOriginResponse || ALLOW_ORIGIN_ALL.equals(value);
580             }
581             if (allowOriginResponse) {
582                 webResponse_ = webResponse;
583             }
584             if (allowOriginResponse) {
585                 setState(STATE_HEADERS_RECEIVED, context);
586                 setState(STATE_LOADING, context);
587                 setState(STATE_DONE, context);
588             }
589             else {
590                 if (LOG.isDebugEnabled()) {
591                     LOG.debug("No permitted \"Access-Control-Allow-Origin\" header for URL " + webRequest_.getUrl());
592                 }
593                 throw new IOException("No permitted \"Access-Control-Allow-Origin\" header.");
594             }
595         }
596         catch (final IOException e) {
597             if (LOG.isDebugEnabled()) {
598                 LOG.debug("IOException: returning a network error response.", e);
599             }
600             webResponse_ = new NetworkErrorWebResponse(webRequest_);
601             setState(STATE_HEADERS_RECEIVED, context);
602             setState(STATE_DONE, context);
603             if (!async_) {
604                 throw Context.reportRuntimeError("Object not found.");
605             }
606         }
607     }
608 
609     private boolean isPreflight() {
610         final HttpMethod method = webRequest_.getHttpMethod();
611         if (method != HttpMethod.GET && method != HttpMethod.HEAD && method != HttpMethod.POST) {
612             return true;
613         }
614         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
615             if (isPreflightHeader(header.getKey().toLowerCase(Locale.ROOT),
616                     header.getValue())) {
617                 return true;
618             }
619         }
620         return false;
621     }
622 
623     private boolean isPreflightAuthorized(final WebResponse preflightResponse) {
624         final String originHeader = preflightResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN);
625         if (!ALLOW_ORIGIN_ALL.equals(originHeader)
626                 && !webRequest_.getAdditionalHeaders().get(HEADER_ORIGIN).equals(originHeader)) {
627             return false;
628         }
629         String headersHeader = preflightResponse.getResponseHeaderValue(HEADER_ACCESS_CONTROL_ALLOW_HEADERS);
630         if (headersHeader == null) {
631             headersHeader = "";
632         }
633         else {
634             headersHeader = headersHeader.toLowerCase(Locale.ROOT);
635         }
636         for (final Entry<String, String> header : webRequest_.getAdditionalHeaders().entrySet()) {
637             final String key = header.getKey().toLowerCase(Locale.ROOT);
638             if (isPreflightHeader(key, header.getValue())
639                     && !headersHeader.contains(key)) {
640                 return false;
641             }
642         }
643         return true;
644     }
645 
646     /**
647      * @param name header name (MUST be lower-case for performance reasons)
648      * @param value header value
649      */
650     private static boolean isPreflightHeader(final String name, final String value) {
651         if ("content-type".equals(name)) {
652             final String lcValue = value.toLowerCase(Locale.ROOT);
653             if (lcValue.startsWith(FormEncodingType.URL_ENCODED.getName())
654                 || lcValue.startsWith(FormEncodingType.MULTIPART.getName())
655                 || lcValue.startsWith("text/plain")) {
656                 return false;
657             }
658             return true;
659         }
660         if ("accept".equals(name) || "accept-language".equals(name) || "content-language".equals(name)
661                 || "referer".equals(name) || "accept-encoding".equals(name) || "origin".equals(name)) {
662             return false;
663         }
664         return true;
665     }
666 
667     /**
668      * Sets the specified header to the specified value.<br>
669      * The <code>open</code> method must be called before this method, or an error will occur.
670      * @param name the header name to set
671      * @param value the value of the header
672      */
673     @JsxFunction
674     public void setRequestHeader(final String name, final String value) {
675         if (name == null || "null".equals(name)) {
676             throw Context.reportRuntimeError("Type mismatch (name is null).");
677         }
678         if ("".equals(name)) {
679             throw Context.reportRuntimeError("Invalid argument (name is empty).");
680         }
681         if (value == null || "null".equals(value)) {
682             throw Context.reportRuntimeError("Type mismatch (value is null).");
683         }
684         if (StringUtils.isBlank(value)) {
685             return;
686         }
687         if (!isAuthorizedHeader(name)) {
688             LOG.warn("Ignoring XMLHTTPRequest.setRequestHeader for " + name
689                 + ": it is a restricted header");
690             return;
691         }
692 
693         if (webRequest_ == null) {
694             throw Context.reportRuntimeError("Unspecified error (request not opened).");
695         }
696         webRequest_.setAdditionalHeader(name, value);
697     }
698 
699     /**
700      * Not all request headers can be set from JavaScript.
701      * @see <a href="http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader-method">W3C doc</a>
702      * @param name the header name
703      * @return {@code true} if the header can be set from JavaScript
704      */
705     static boolean isAuthorizedHeader(final String name) {
706         final String nameLowerCase = name.toLowerCase(Locale.ROOT);
707         if (PROHIBITED_HEADERS_.contains(nameLowerCase)) {
708             return false;
709         }
710         else if (nameLowerCase.startsWith("proxy-") || nameLowerCase.startsWith("sec-")) {
711             return false;
712         }
713         return true;
714     }
715 
716     /**
717      * {@inheritDoc}
718      */
719     @Override
720     public Object get(String name, final Scriptable start) {
721         for (final String property : ALL_PROPERTIES_) {
722             if (property.equalsIgnoreCase(name)) {
723                 name = property;
724                 break;
725             }
726         }
727         return super.get(name, start);
728     }
729 
730     /**
731      * {@inheritDoc}
732      */
733     @Override
734     public void put(String name, final Scriptable start, final Object value) {
735         for (final String property : ALL_PROPERTIES_) {
736             if (property.equalsIgnoreCase(name)) {
737                 name = property;
738                 break;
739             }
740         }
741         super.put(name, start, value);
742     }
743 
744     private static final class NetworkErrorWebResponse extends WebResponse {
745         private final WebRequest request_;
746 
747         private NetworkErrorWebResponse(final WebRequest webRequest) {
748             super(null, null, 0);
749             request_ = webRequest;
750         }
751 
752         @Override
753         public int getStatusCode() {
754             return 0;
755         }
756 
757         @Override
758         public String getStatusMessage() {
759             return "";
760         }
761 
762         @Override
763         public String getContentType() {
764             return "";
765         }
766 
767         @Override
768         public String getContentAsString() {
769             return "";
770         }
771 
772         @Override
773         public InputStream getContentAsStream() {
774             return null;
775         }
776 
777         @Override
778         public List<NameValuePair> getResponseHeaders() {
779             return Collections.emptyList();
780         }
781 
782         @Override
783         public String getResponseHeaderValue(final String headerName) {
784             return "";
785         }
786 
787         @Override
788         public long getLoadTime() {
789             return 0;
790         }
791 
792         @Override
793         public Charset getContentCharset() {
794             return null;
795         }
796 
797         @Override
798         public Charset getContentCharsetOrNull() {
799             return null;
800         }
801 
802         @Override
803         public WebRequest getWebRequest() {
804             return request_;
805         }
806     }
807 }