Commit | Line | Data |
---|---|---|
8737e1cc AE |
1 | /** |
2 | * Versatile AJAX request handling. | |
3 | * | |
4 | * In case you want to issue JSONP requests, please use `AjaxJsonp` instead. | |
5 | * | |
6 | * @author Alexander Ebert | |
50d96bd8 | 7 | * @copyright 2001-2017 WoltLab GmbH |
8737e1cc | 8 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> |
58d7e8f8 | 9 | * @module WoltLabSuite/Core/Ajax/Request |
8737e1cc | 10 | */ |
58d7e8f8 | 11 | define(['Core', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Ui/Dialog', 'WoltLabSuite/Core/Ajax/Status'], function(Core, Language, DomChangeListener, DomUtil, UiDialog, AjaxStatus) { |
28dfae01 AE |
12 | "use strict"; |
13 | ||
14 | var _didInit = false; | |
15 | var _ignoreAllErrors = false; | |
16 | ||
8737e1cc AE |
17 | /** |
18 | * @constructor | |
19 | */ | |
28dfae01 AE |
20 | function AjaxRequest(options) { |
21 | this._data = null; | |
22 | this._options = {}; | |
23 | this._previousXhr = null; | |
24 | this._xhr = null; | |
25 | ||
26 | this._init(options); | |
f27e5de2 | 27 | } |
28dfae01 AE |
28 | AjaxRequest.prototype = { |
29 | /** | |
30 | * Initializes the request options. | |
31 | * | |
f27e5de2 | 32 | * @param {Object} options request options |
28dfae01 AE |
33 | */ |
34 | _init: function(options) { | |
35 | this._options = Core.extend({ | |
36 | // request data | |
37 | data: {}, | |
2fb800af | 38 | contentType: 'application/x-www-form-urlencoded; charset=UTF-8', |
4a179dfa | 39 | responseType: 'application/json', |
28dfae01 AE |
40 | type: 'POST', |
41 | url: '', | |
bb282d8b | 42 | withCredentials: false, |
28dfae01 AE |
43 | |
44 | // behavior | |
45 | autoAbort: false, | |
46 | ignoreError: false, | |
47 | pinData: false, | |
48 | silent: false, | |
49 | ||
50 | // callbacks | |
51 | failure: null, | |
52 | finalize: null, | |
53 | success: null, | |
500102c2 MS |
54 | progress: null, |
55 | uploadProgress: null, | |
28dfae01 AE |
56 | |
57 | callbackObject: null | |
58 | }, options); | |
59 | ||
15a48bb3 AE |
60 | if (typeof options.callbackObject === 'object') { |
61 | this._options.callbackObject = options.callbackObject; | |
62 | } | |
63 | ||
28dfae01 | 64 | this._options.url = Core.convertLegacyUrl(this._options.url); |
ca5140b5 | 65 | if (this._options.url.indexOf('index.php') === 0) { |
8186015c | 66 | this._options.url = WSC_API_URL + this._options.url; |
ca5140b5 | 67 | } |
28dfae01 | 68 | |
5a4c5344 AE |
69 | if (this._options.url.indexOf(WSC_API_URL) === 0) { |
70 | // allows allow credentials when querying the very own server | |
71 | this._options.withCredentials = true; | |
72 | } | |
73 | ||
28dfae01 AE |
74 | if (this._options.pinData) { |
75 | this._data = Core.extend({}, this._options.data); | |
76 | } | |
77 | ||
78 | if (this._options.callbackObject !== null) { | |
b5a32d79 AE |
79 | if (typeof this._options.callbackObject._ajaxFailure === 'function') this._options.failure = this._options.callbackObject._ajaxFailure.bind(this._options.callbackObject); |
80 | if (typeof this._options.callbackObject._ajaxFinalize === 'function') this._options.finalize = this._options.callbackObject._ajaxFinalize.bind(this._options.callbackObject); | |
81 | if (typeof this._options.callbackObject._ajaxSuccess === 'function') this._options.success = this._options.callbackObject._ajaxSuccess.bind(this._options.callbackObject); | |
500102c2 MS |
82 | if (typeof this._options.callbackObject._ajaxProgress === 'function') this._options.progress = this._options.callbackObject._ajaxProgress.bind(this._options.callbackObject); |
83 | if (typeof this._options.callbackObject._ajaxUploadProgress === 'function') this._options.uploadProgress = this._options.callbackObject._ajaxUploadProgress.bind(this._options.callbackObject); | |
28dfae01 AE |
84 | } |
85 | ||
86 | if (_didInit === false) { | |
87 | _didInit = true; | |
88 | ||
89 | window.addEventListener('beforeunload', function() { _ignoreAllErrors = true; }); | |
90 | } | |
91 | }, | |
92 | ||
93 | /** | |
94 | * Dispatches a request, optionally aborting a currently active request. | |
95 | * | |
96 | * @param {boolean} abortPrevious abort currently active request | |
97 | */ | |
98 | sendRequest: function(abortPrevious) { | |
99 | if (abortPrevious === true || this._options.autoAbort) { | |
100 | this.abortPrevious(); | |
101 | } | |
102 | ||
103 | if (!this._options.silent) { | |
104 | AjaxStatus.show(); | |
105 | } | |
106 | ||
107 | if (this._xhr instanceof XMLHttpRequest) { | |
108 | this._previousXhr = this._xhr; | |
109 | } | |
110 | ||
111 | this._xhr = new XMLHttpRequest(); | |
112 | this._xhr.open(this._options.type, this._options.url, true); | |
2fb800af MS |
113 | if (this._options.contentType) { |
114 | this._xhr.setRequestHeader('Content-Type', this._options.contentType); | |
115 | } | |
28dfae01 | 116 | this._xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); |
bb282d8b TD |
117 | if (this._options.withCredentials) { |
118 | this._xhr.withCredentials = true; | |
119 | } | |
28dfae01 AE |
120 | |
121 | var self = this; | |
b5a32d79 | 122 | var options = Core.clone(this._options); |
28dfae01 AE |
123 | this._xhr.onload = function() { |
124 | if (this.readyState === XMLHttpRequest.DONE) { | |
125 | if (this.status >= 200 && this.status < 300 || this.status === 304) { | |
0e5544d3 | 126 | if (options.responseType && this.getResponseHeader('Content-Type').indexOf(options.responseType) !== 0) { |
4a179dfa AE |
127 | // request succeeded but invalid response type |
128 | self._failure(this, options); | |
129 | } | |
130 | else { | |
131 | self._success(this, options); | |
132 | } | |
28dfae01 AE |
133 | } |
134 | else { | |
b5a32d79 | 135 | self._failure(this, options); |
28dfae01 AE |
136 | } |
137 | } | |
138 | }; | |
139 | this._xhr.onerror = function() { | |
b5a32d79 | 140 | self._failure(this, options); |
28dfae01 AE |
141 | }; |
142 | ||
500102c2 MS |
143 | if (this._options.progress) { |
144 | this._xhr.onprogress = this._options.progress; | |
145 | } | |
146 | if (this._options.uploadProgress) { | |
147 | this._xhr.upload.onprogress = this._options.uploadProgress; | |
148 | } | |
149 | ||
28dfae01 AE |
150 | if (this._options.type === 'POST') { |
151 | var data = this._options.data; | |
f8270d7c | 152 | if (typeof data === 'object' && Core.getType(data) !== 'FormData') { |
28dfae01 AE |
153 | data = Core.serialize(data); |
154 | } | |
155 | ||
156 | this._xhr.send(data); | |
157 | } | |
158 | else { | |
159 | this._xhr.send(); | |
160 | } | |
161 | }, | |
162 | ||
163 | /** | |
164 | * Aborts a previous request. | |
165 | */ | |
166 | abortPrevious: function() { | |
167 | if (this._previousXhr === null) { | |
168 | return; | |
169 | } | |
170 | ||
171 | this._previousXhr.abort(); | |
172 | this._previousXhr = null; | |
173 | ||
174 | if (!this._options.silent) { | |
175 | AjaxStatus.hide(); | |
176 | } | |
177 | }, | |
178 | ||
179 | /** | |
180 | * Sets a specific option. | |
181 | * | |
28dfae01 | 182 | * @param {string} key option name |
f27e5de2 | 183 | * @param {?} value option value |
28dfae01 AE |
184 | */ |
185 | setOption: function(key, value) { | |
186 | this._options[key] = value; | |
187 | }, | |
188 | ||
b99935c8 AE |
189 | /** |
190 | * Returns an option by key or undefined. | |
191 | * | |
192 | * @param {string} key option name | |
193 | * @return {(*|null)} option value or null | |
194 | */ | |
195 | getOption: function(key) { | |
f5336f4f | 196 | if (objOwns(this._options, key)) { |
b99935c8 AE |
197 | return this._options[key]; |
198 | } | |
199 | ||
200 | return null; | |
201 | }, | |
202 | ||
28dfae01 AE |
203 | /** |
204 | * Sets request data while honoring pinned data from setup callback. | |
205 | * | |
f27e5de2 | 206 | * @param {Object} data request data |
28dfae01 AE |
207 | */ |
208 | setData: function(data) { | |
f8270d7c | 209 | if (this._data !== null && Core.getType(data) !== 'FormData') { |
28dfae01 AE |
210 | data = Core.extend(this._data, data); |
211 | } | |
212 | ||
213 | this._options.data = data; | |
214 | }, | |
215 | ||
216 | /** | |
217 | * Handles a successful request. | |
218 | * | |
b5a32d79 | 219 | * @param {XMLHttpRequest} xhr request object |
f27e5de2 | 220 | * @param {Object} options request options |
28dfae01 | 221 | */ |
b5a32d79 AE |
222 | _success: function(xhr, options) { |
223 | if (!options.silent) { | |
28dfae01 AE |
224 | AjaxStatus.hide(); |
225 | } | |
226 | ||
b5a32d79 | 227 | if (typeof options.success === 'function') { |
28dfae01 AE |
228 | var data = null; |
229 | if (xhr.getResponseHeader('Content-Type') === 'application/json') { | |
230 | try { | |
231 | data = JSON.parse(xhr.responseText); | |
232 | } | |
233 | catch (e) { | |
234 | // invalid JSON | |
b99935c8 | 235 | this._failure(xhr, options); |
28dfae01 AE |
236 | |
237 | return; | |
238 | } | |
239 | ||
240 | // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring | |
2e97a69c | 241 | if (data && data.returnValues && data.returnValues.template !== undefined) { |
28dfae01 AE |
242 | data.returnValues.template = data.returnValues.template.trim(); |
243 | } | |
244 | } | |
245 | ||
b99935c8 | 246 | options.success(data, xhr.responseText, xhr, options.data); |
28dfae01 AE |
247 | } |
248 | ||
b5a32d79 | 249 | this._finalize(options); |
28dfae01 AE |
250 | }, |
251 | ||
252 | /** | |
253 | * Handles failed requests, this can be both a successful request with | |
254 | * a non-success status code or an entirely failed request. | |
255 | * | |
b5a32d79 | 256 | * @param {XMLHttpRequest} xhr request object |
f27e5de2 | 257 | * @param {Object} options request options |
28dfae01 | 258 | */ |
b5a32d79 | 259 | _failure: function (xhr, options) { |
28dfae01 AE |
260 | if (_ignoreAllErrors) { |
261 | return; | |
262 | } | |
263 | ||
b5a32d79 | 264 | if (!options.silent) { |
28dfae01 AE |
265 | AjaxStatus.hide(); |
266 | } | |
267 | ||
268 | var data = null; | |
269 | try { | |
270 | data = JSON.parse(xhr.responseText); | |
271 | } | |
272 | catch (e) {} | |
273 | ||
274 | var showError = true; | |
f27e5de2 | 275 | if (data !== null && typeof options.failure === 'function') { |
b99935c8 | 276 | showError = options.failure(data, xhr.responseText, xhr, options.data); |
28dfae01 AE |
277 | } |
278 | ||
b5a32d79 | 279 | if (options.ignoreError !== true && showError !== false) { |
28dfae01 AE |
280 | var details = ''; |
281 | var message = ''; | |
282 | ||
283 | if (data !== null) { | |
227cda57 AE |
284 | if (data.stacktrace) details = '<br><p>Stacktrace:</p><p>' + data.stacktrace + '</p>'; |
285 | else if (data.exceptionID) details = '<br><p>Exception ID: <code>' + data.exceptionID + '</code></p>'; | |
28dfae01 AE |
286 | |
287 | message = data.message; | |
227cda57 AE |
288 | |
289 | data.previous.forEach(function(previous) { | |
290 | details += '<hr><p>' + previous.message + '</p>'; | |
291 | details += '<br><p>Stacktrace</p><p>' + previous.stacktrace + '</p>'; | |
292 | }); | |
28dfae01 AE |
293 | } |
294 | else { | |
295 | message = xhr.responseText; | |
296 | } | |
297 | ||
298 | if (!message || message === 'undefined') { | |
299 | return; | |
300 | } | |
301 | ||
302 | var html = '<div class="ajaxDebugMessage"><p>' + message + '</p>' + details + '</div>'; | |
303 | ||
eae3eb79 | 304 | if (UiDialog === undefined) UiDialog = require('Ui/Dialog'); |
9a421cc7 | 305 | UiDialog.openStatic(DomUtil.getUniqueId(), html, { |
28dfae01 AE |
306 | title: Language.get('wcf.global.error.title') |
307 | }); | |
308 | } | |
309 | ||
b5a32d79 | 310 | this._finalize(options); |
28dfae01 AE |
311 | }, |
312 | ||
313 | /** | |
314 | * Finalizes a request. | |
b5a32d79 | 315 | * |
f27e5de2 | 316 | * @param {Object} options request options |
28dfae01 | 317 | */ |
b5a32d79 AE |
318 | _finalize: function(options) { |
319 | if (typeof options.finalize === 'function') { | |
320 | options.finalize(this._xhr); | |
28dfae01 AE |
321 | } |
322 | ||
323 | this._previousXhr = null; | |
324 | ||
9a421cc7 | 325 | DomChangeListener.trigger(); |
28dfae01 AE |
326 | |
327 | // fix anchor tags generated through WCF::getAnchor() | |
d0023381 | 328 | var links = elBySelAll('a[href*="#"]'); |
28dfae01 AE |
329 | for (var i = 0, length = links.length; i < length; i++) { |
330 | var link = links[i]; | |
d0023381 | 331 | var href = elAttr(link, 'href'); |
28dfae01 AE |
332 | if (href.indexOf('AJAXProxy') !== -1 || href.indexOf('ajax-proxy') !== -1) { |
333 | href = href.substr(href.indexOf('#')); | |
d0023381 | 334 | elAttr(link, 'href', document.location.toString().replace(/#.*/, '') + href); |
28dfae01 AE |
335 | } |
336 | } | |
337 | } | |
338 | }; | |
339 | ||
340 | return AjaxRequest; | |
341 | }); |