2 * Versatile AJAX request handling.
4 * In case you want to issue JSONP requests, please use `AjaxJsonp` instead.
6 * @author Alexander Ebert
7 * @copyright 2001-2019 WoltLab GmbH
8 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
9 * @module AjaxRequest (alias)
10 * @module WoltLabSuite/Core/Ajax/Request
12 define(['Core', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Ui/Dialog', 'WoltLabSuite/Core/Ajax/Status'], function(Core
, Language
, DomChangeListener
, DomUtil
, UiDialog
, AjaxStatus
) {
16 var _ignoreAllErrors
= false;
21 function AjaxRequest(options
) {
24 this._previousXhr
= null;
29 AjaxRequest
.prototype = {
31 * Initializes the request options.
33 * @param {Object} options request options
35 _init: function(options
) {
36 this._options
= Core
.extend({
39 contentType
: 'application/x-www-form-urlencoded; charset=UTF-8',
40 responseType
: 'application/json',
43 withCredentials
: false,
50 includeRequestedWith
: true,
62 if (typeof options
.callbackObject
=== 'object') {
63 this._options
.callbackObject
= options
.callbackObject
;
66 this._options
.url
= Core
.convertLegacyUrl(this._options
.url
);
67 if (this._options
.url
.indexOf('index.php') === 0) {
68 this._options
.url
= WSC_API_URL
+ this._options
.url
;
71 if (this._options
.url
.indexOf(WSC_API_URL
) === 0) {
72 this._options
.includeRequestedWith
= true;
73 // always include credentials when querying the very own server
74 this._options
.withCredentials
= true;
77 if (this._options
.pinData
) {
78 this._data
= Core
.extend({}, this._options
.data
);
81 if (this._options
.callbackObject
!== null) {
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
);
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
);
89 if (_didInit
=== false) {
92 window
.addEventListener('beforeunload', function() { _ignoreAllErrors
= true; });
97 * Dispatches a request, optionally aborting a currently active request.
99 * @param {boolean} abortPrevious abort currently active request
101 sendRequest: function(abortPrevious
) {
102 if (abortPrevious
=== true || this._options
.autoAbort
) {
103 this.abortPrevious();
106 if (!this._options
.silent
) {
110 if (this._xhr
instanceof XMLHttpRequest
) {
111 this._previousXhr
= this._xhr
;
114 this._xhr
= new XMLHttpRequest();
115 this._xhr
.open(this._options
.type
, this._options
.url
, true);
116 if (this._options
.contentType
) {
117 this._xhr
.setRequestHeader('Content-Type', this._options
.contentType
);
119 if (this._options
.withCredentials
|| this._options
.includeRequestedWith
) {
120 this._xhr
.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
122 if (this._options
.withCredentials
) {
123 this._xhr
.withCredentials
= true;
127 var options
= Core
.clone(this._options
);
128 this._xhr
.onload = function() {
129 if (this.readyState
=== XMLHttpRequest
.DONE
) {
130 if (this.status
>= 200 && this.status
< 300 || this.status
=== 304) {
131 if (options
.responseType
&& this.getResponseHeader('Content-Type').indexOf(options
.responseType
) !== 0) {
132 // request succeeded but invalid response type
133 self
._failure(this, options
);
136 self
._success(this, options
);
140 self
._failure(this, options
);
144 this._xhr
.onerror = function() {
145 self
._failure(this, options
);
148 if (this._options
.progress
) {
149 this._xhr
.onprogress
= this._options
.progress
;
151 if (this._options
.uploadProgress
) {
152 this._xhr
.upload
.onprogress
= this._options
.uploadProgress
;
155 if (this._options
.type
=== 'POST') {
156 var data
= this._options
.data
;
157 if (typeof data
=== 'object' && Core
.getType(data
) !== 'FormData') {
158 data
= Core
.serialize(data
);
161 this._xhr
.send(data
);
169 * Aborts a previous request.
171 abortPrevious: function() {
172 if (this._previousXhr
=== null) {
176 this._previousXhr
.abort();
177 this._previousXhr
= null;
179 if (!this._options
.silent
) {
185 * Sets a specific option.
187 * @param {string} key option name
188 * @param {?} value option value
190 setOption: function(key
, value
) {
191 this._options
[key
] = value
;
195 * Returns an option by key or undefined.
197 * @param {string} key option name
198 * @return {(*|null)} option value or null
200 getOption: function(key
) {
201 if (objOwns(this._options
, key
)) {
202 return this._options
[key
];
209 * Sets request data while honoring pinned data from setup callback.
211 * @param {Object} data request data
213 setData: function(data
) {
214 if (this._data
!== null && Core
.getType(data
) !== 'FormData') {
215 data
= Core
.extend(this._data
, data
);
218 this._options
.data
= data
;
222 * Handles a successful request.
224 * @param {XMLHttpRequest} xhr request object
225 * @param {Object} options request options
227 _success: function(xhr
, options
) {
228 if (!options
.silent
) {
232 if (typeof options
.success
=== 'function') {
234 if (xhr
.getResponseHeader('Content-Type').split(';', 1)[0].trim() === 'application/json') {
236 data
= JSON
.parse(xhr
.responseText
);
240 this._failure(xhr
, options
);
245 // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring
246 if (data
&& data
.returnValues
&& data
.returnValues
.template
!== undefined) {
247 data
.returnValues
.template
= data
.returnValues
.template
.trim();
250 // force-invoke the background queue
251 if (data
&& data
.forceBackgroundQueuePerform
) {
252 require(['WoltLabSuite/Core/BackgroundQueue'], function(BackgroundQueue
) {
253 BackgroundQueue
.invoke();
258 options
.success(data
, xhr
.responseText
, xhr
, options
.data
);
261 this._finalize(options
);
265 * Handles failed requests, this can be both a successful request with
266 * a non-success status code or an entirely failed request.
268 * @param {XMLHttpRequest} xhr request object
269 * @param {Object} options request options
271 _failure: function (xhr
, options
) {
272 if (_ignoreAllErrors
) {
276 if (!options
.silent
) {
282 data
= JSON
.parse(xhr
.responseText
);
286 var showError
= true;
287 if (typeof options
.failure
=== 'function') {
288 showError
= options
.failure((data
|| {}), (xhr
.responseText
|| ''), xhr
, options
.data
);
291 if (options
.ignoreError
!== true && showError
!== false) {
292 var html
= this.getErrorHtml(data
, xhr
);
295 if (UiDialog
=== undefined) UiDialog
= require('Ui/Dialog');
296 UiDialog
.openStatic(DomUtil
.getUniqueId(), html
, {
297 title
: Language
.get('wcf.global.error.title')
302 this._finalize(options
);
306 * Returns the inner HTML for an error/exception display.
308 * @param {Object} data
309 * @param {XMLHttpRequest} xhr
312 getErrorHtml: function(data
, xhr
) {
317 if (data
.file
&& data
.line
) {
318 details
+= '<br><p>File:</p><p>' + data
.file
+ ' in line ' + data
.line
+ '</p>';
321 if (data
.stacktrace
) details
+= '<br><p>Stacktrace:</p><p>' + data
.stacktrace
+ '</p>';
322 else if (data
.exceptionID
) details
+= '<br><p>Exception ID: <code>' + data
.exceptionID
+ '</code></p>';
324 message
= data
.message
;
326 data
.previous
.forEach(function(previous
) {
327 details
+= '<hr><p>' + previous
.message
+ '</p>';
328 details
+= '<br><p>Stacktrace</p><p>' + previous
.stacktrace
+ '</p>';
332 message
= xhr
.responseText
;
335 if (!message
|| message
=== 'undefined') {
336 if (!ENABLE_DEBUG_MODE
) return null;
338 message
= 'XMLHttpRequest failed without a responseText. Check your browser console.'
341 return '<div class="ajaxDebugMessage"><p>' + message
+ '</p>' + details
+ '</div>';
345 * Finalizes a request.
347 * @param {Object} options request options
349 _finalize: function(options
) {
350 if (typeof options
.finalize
=== 'function') {
351 options
.finalize(this._xhr
);
354 this._previousXhr
= null;
356 DomChangeListener
.trigger();
358 // fix anchor tags generated through WCF::getAnchor()
359 var links
= elBySelAll('a[href*="#"]');
360 for (var i
= 0, length
= links
.length
; i
< length
; i
++) {
362 var href
= elAttr(link
, 'href');
363 if (href
.indexOf('AJAXProxy') !== -1 || href
.indexOf('ajax-proxy') !== -1) {
364 href
= href
.substr(href
.indexOf('#'));
365 elAttr(link
, 'href', document
.location
.toString().replace(/#.*/, '') + href
);