1 /**
  2  * @fileOverview
  3  * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>)
  4  * @license Copyright (c) 2011-2016 LabKey Corporation
  5  * <p/>
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  * <p/>
 10  * http://www.apache.org/licenses/LICENSE-2.0
 11  * <p/>
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing permissions and
 16  * limitations under the License.
 17  * <p/>
 18  */
 19 
 20 /**
 21  * @namespace
 22  * Utility for making XHR.
 23  */
 24 LABKEY.Ajax = new function () {
 25     'use strict';
 26 
 27     var DEFAULT_HEADERS = {'X-LABKEY-CSRF': LABKEY.CSRF};
 28 
 29     var callback = function(fn, scope, args) {
 30         if (fn) {
 31             fn.apply(scope, args);
 32         }
 33     };
 34 
 35     /**
 36      * Returns true iff obj contains case-insensitive key
 37      * @param {object} obj
 38      * @param {string} key
 39      */
 40     var contains = function(obj, key) {
 41         if (key) {
 42             var lowerKey = key.toLowerCase();
 43             for (var k in obj) {
 44                 if (obj.hasOwnProperty(k) && k.toLowerCase() === lowerKey) {
 45                     return true;
 46                 }
 47             }
 48         }
 49         return false;
 50     };
 51 
 52     var configureOptions = function(config) {
 53         var url, params, method = 'GET', data, isForm = false;
 54 
 55         if (!config.hasOwnProperty('url') || config.url === null) {
 56             throw new Error("a URL is required to make a request");
 57         }
 58 
 59         url = config.url;
 60         params = config.params;
 61 
 62         // configure data
 63         if (config.form) {
 64             data = config.form instanceof FormData ? config.form : new FormData(config.form);
 65             isForm = true;
 66         }
 67         else if (config.jsonData) {
 68             data = JSON.stringify(config.jsonData);
 69         }
 70         else {
 71             data = null;
 72         }
 73 
 74         // configure method
 75         if (config.hasOwnProperty('method') && config.method !== null) {
 76             method = config.method.toUpperCase();
 77         }
 78         else if (data) {
 79             method = 'POST';
 80         }
 81 
 82         // configure params
 83         if (params !== undefined && params !== null) {
 84 
 85             var qs = LABKEY.ActionURL.queryString(params);
 86 
 87             // 26617: backwards compatibility to append params to the body in the case of a POST without form/jsonData
 88             if (method === 'POST' && (data === undefined || data === null)) {
 89                 data = qs;
 90             }
 91             else {
 92                 url += (url.indexOf('?') === -1 ? '?' : '&') + qs;
 93             }
 94         }
 95 
 96         return {
 97             url: url,
 98             method: method,
 99             data: data,
100             isForm: isForm
101         };
102     };
103 
104     var configureHeaders = function(xhr, config, options) {
105         var headers = config.headers,
106             jsonData = config.jsonData;
107 
108         if (headers === undefined || headers === null) {
109             headers = {};
110         }
111 
112         // only set Content-Type if this is not FormData and it has not been set explicitly
113         if (!options.isForm && !contains(headers, 'Content-Type')) {
114             if (jsonData !== undefined && jsonData !== null) {
115                 headers['Content-Type'] = 'application/json';
116             }
117             else {
118                 headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
119             }
120         }
121 
122         if (!contains(headers, 'X-Requested-With')) {
123             headers['X-Requested-With'] = 'XMLHttpRequest';
124         }
125 
126         for (var k in DEFAULT_HEADERS) {
127             if (DEFAULT_HEADERS.hasOwnProperty(k)) {
128                 xhr.setRequestHeader(k, DEFAULT_HEADERS[k]);
129             }
130         }
131 
132         for (var k in headers) {
133             if (headers.hasOwnProperty(k)) {
134                 xhr.setRequestHeader(k, headers[k]);
135             }
136         }
137 
138         return headers;
139     };
140 
141     /** @scope LABKEY.Ajax */
142     return {
143         DEFAULT_HEADERS : DEFAULT_HEADERS,
144 
145         request : function(config) {
146             var options = configureOptions(config),
147                 scope = config.hasOwnProperty('scope') && config.scope !== null ? config.scope : this,
148                 xhr = new XMLHttpRequest();
149 
150             xhr.onreadystatechange = function() {
151                 if (xhr.readyState === 4) {
152                     var success = (xhr.status >= 200 && xhr.status < 300) || xhr.status == 304;
153 
154                     callback(success ? config.success : config.failure, scope, [xhr, config]);
155                     callback(config.callback, scope, [config, success, xhr]);
156                 }
157             };
158 
159             xhr.open(options.method, options.url, true);
160 
161             // configure headers after request is open
162             configureHeaders(xhr, config, options);
163 
164             // configure timeout after request is open
165             if (config.hasOwnProperty('timeout') && config.timeout !== null) {
166                 xhr.ontimeout = function() {
167                     callback(config.failure, scope, [xhr, config]);
168                     callback(config.callback, scope, [config, false /* success */, xhr]);
169                 };
170             }
171 
172             xhr.send(options.data);
173 
174             return xhr;
175         }
176     }
177 };
178