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-2018 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 = LABKEY.defaultHeaders; 28 29 var callback = function(fn, scope, args) { 30 if (fn) { 31 fn.apply(scope, args); 32 } 33 }; 34 35 // Returns true iff obj contains case-insensitive key 36 var contains = function(obj, key) { 37 if (key) { 38 var lowerKey = key.toLowerCase(); 39 for (var k in obj) { 40 if (obj.hasOwnProperty(k) && k.toLowerCase() === lowerKey) { 41 return true; 42 } 43 } 44 } 45 return false; 46 }; 47 48 var configureOptions = function(config) { 49 var url, params, method = 'GET', data, isForm = false; 50 51 if (!config.hasOwnProperty('url') || config.url === null) { 52 throw new Error("a URL is required to make a request"); 53 } 54 55 url = config.url; 56 params = config.params; 57 58 // configure data 59 if (config.form) { 60 data = config.form instanceof FormData ? config.form : new FormData(config.form); 61 isForm = true; 62 } 63 else if (config.jsonData) { 64 data = JSON.stringify(config.jsonData); 65 } 66 else { 67 data = null; 68 } 69 70 // configure method 71 if (config.hasOwnProperty('method') && config.method !== null) { 72 method = config.method.toUpperCase(); 73 } 74 else if (data) { 75 method = 'POST'; 76 } 77 78 // configure params 79 if (params !== undefined && params !== null) { 80 81 var qs = LABKEY.ActionURL.queryString(params); 82 83 // 26617: backwards compatibility to append params to the body in the case of a POST without form/jsonData 84 if (method === 'POST' && (data === undefined || data === null)) { 85 data = qs; 86 } 87 else { 88 url += (url.indexOf('?') === -1 ? '?' : '&') + qs; 89 } 90 } 91 92 return { 93 url: url, 94 method: method, 95 data: data, 96 isForm: isForm 97 }; 98 }; 99 100 var configureHeaders = function(xhr, config, options) { 101 var headers = config.headers, 102 jsonData = config.jsonData; 103 104 if (headers === undefined || headers === null) { 105 headers = {}; 106 } 107 108 // only set Content-Type if this is not FormData and it has not been set explicitly 109 if (!options.isForm && !contains(headers, 'Content-Type')) { 110 if (jsonData !== undefined && jsonData !== null) { 111 headers['Content-Type'] = 'application/json'; 112 } 113 else { 114 headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; 115 } 116 } 117 118 if (!contains(headers, 'X-Requested-With')) { 119 headers['X-Requested-With'] = 'XMLHttpRequest'; 120 } 121 122 for (var k in DEFAULT_HEADERS) { 123 if (DEFAULT_HEADERS.hasOwnProperty(k)) { 124 xhr.setRequestHeader(k, DEFAULT_HEADERS[k]); 125 } 126 } 127 128 for (var k in headers) { 129 if (headers.hasOwnProperty(k)) { 130 xhr.setRequestHeader(k, headers[k]); 131 } 132 } 133 134 return headers; 135 }; 136 137 /** @scope LABKEY.Ajax */ 138 return { 139 DEFAULT_HEADERS : DEFAULT_HEADERS, 140 141 /** 142 * Make a XMLHttpRequest nominally to a LabKey instance. Includes success/failure callback mechanism, 143 * HTTP header configuration, support for FormData, and parameter encoding amongst other features. 144 * @param config An object which contains the following configuration properties. 145 * @param {String} config.url the url used for the XMLHttpRequest. If you are making a request to the 146 * LabKey Server instance see {@link LABKEY.ActionURL.buildURL} for helpful URL construction. 147 * @param {String} [config.method] the HTTP request method used for the XMLHttpRequest. Examples are "GET", "PUSH, "DELETE", etc. 148 * Defaults to "GET" unless jsonData is supplied then the default is changed to "POST". For more information, 149 * see this <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods">HTTP request method documentation</a>. 150 * @param {Object} [config.jsonData] data provided to the XMLHttpRequest.send(data) function. If the request is method "POST" this is the body of the request. 151 * @param {Object} [config.params] An object representing URL parameters that will be added to the URL. 152 * Note, that if the request is method "POST" and jsonData is not provided these params will be sent via the body of the request. 153 * @param {Object} [config.headers] Object specifying additional HTTP headers to add the request. 154 * @param {Mixed} [config.form] FormData or Object consumable by FormData that can be used to POST key/value pairs of form information. 155 * For more information, see <a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData">FormData documentation</a>. 156 * @param {Function} [config.success] A function called when a successful response is received (determined by XHR readyState and status). 157 * It will be passed the following arguments: 158 * <ul> 159 * <li><b>xhr:</b> The XMLHttpRequest where the text of the response can be found on xhr.responseText amongst other properties</li> 160 * <li><b>originalConfig:</b> The config originally supplied to LABKEY.Ajax.request</li> 161 * </ul> 162 * @param {Function} [config.failure] A function called when a failure response is received (determined by 163 * XHR readyState, status, or ontimeout if supplied). It will be passed the following arguments: 164 * <ul> 165 * <li><b>xhr:</b> The XMLHttpRequest where the text of the response can be found on xhr.responseText amongst other properties</li> 166 * <li><b>originalConfig:</b> The config originally supplied to LABKEY.Ajax.request</li> 167 * </ul> 168 * @param {Function} [config.callback] A function called after any success/failure response is received. It will 169 * be passed the following arguments: 170 * <ul> 171 * <li><b>originalConfig:</b> The config originally supplied to LABKEY.Ajax.request</li> 172 * <li><b>success:</b> boolean value that is true if the request was successful</li> 173 * <li><b>xhr:</b> The XMLHttpRequest where the text of the response can be found on xhr.responseText amongst other properties</li> 174 * </ul> 175 * @param {Mixed} [config.scope] A scope for the callback functions. Defaults to "this". 176 * @param {Mixed} [config.timeout] If a non-null value is supplied then XMLHttpRequest.ontimeout will be hooked to failure. 177 * @returns {XMLHttpRequest} 178 */ 179 request : function(config) { 180 var options = configureOptions(config), 181 scope = config.hasOwnProperty('scope') && config.scope !== null ? config.scope : this, 182 xhr = new XMLHttpRequest(); 183 184 xhr.onreadystatechange = function() { 185 if (xhr.readyState === 4) { 186 var success = (xhr.status >= 200 && xhr.status < 300) || xhr.status == 304; 187 188 callback(success ? config.success : config.failure, scope, [xhr, config]); 189 callback(config.callback, scope, [config, success, xhr]); 190 } 191 }; 192 193 xhr.open(options.method, options.url, true); 194 195 // configure headers after request is open 196 configureHeaders(xhr, config, options); 197 198 // configure timeout after request is open 199 if (config.hasOwnProperty('timeout') && config.timeout !== null) { 200 xhr.ontimeout = function() { 201 callback(config.failure, scope, [xhr, config]); 202 callback(config.callback, scope, [config, false /* success */, xhr]); 203 }; 204 } 205 206 xhr.send(options.data); 207 208 return xhr; 209 } 210 } 211 }; 212