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