Merge branch '5.2' into 5.3
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WoltLabSuite / Core / Ajax / Request.js
CommitLineData
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 12define(['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});