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