Commit | Line | Data |
---|---|---|
8737e1cc AE |
1 | /** |
2 | * Versatile AJAX request handling. | |
8883ca84 | 3 | * |
8737e1cc | 4 | * In case you want to issue JSONP requests, please use `AjaxJsonp` instead. |
8883ca84 AE |
5 | * |
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 | |
8737e1cc | 11 | */ |
a132a670 | 12 | define(["require", "exports", "tslib", "./Status", "../Core", "../Dom/Change/Listener", "../Dom/Util", "../Language"], function (require, exports, tslib_1, AjaxStatus, Core, Listener_1, Util_1, Language) { |
8883ca84 | 13 | "use strict"; |
716617cf TD |
14 | AjaxStatus = tslib_1.__importStar(AjaxStatus); |
15 | Core = tslib_1.__importStar(Core); | |
16 | Listener_1 = tslib_1.__importDefault(Listener_1); | |
17 | Util_1 = tslib_1.__importDefault(Util_1); | |
18 | Language = tslib_1.__importStar(Language); | |
8883ca84 AE |
19 | let _didInit = false; |
20 | let _ignoreAllErrors = false; | |
21 | /** | |
22 | * @constructor | |
23 | */ | |
24 | class AjaxRequest { | |
25 | constructor(options) { | |
26 | this._options = Core.extend({ | |
27 | data: {}, | |
6ab9b916 TD |
28 | contentType: "application/x-www-form-urlencoded; charset=UTF-8", |
29 | responseType: "application/json", | |
30 | type: "POST", | |
31 | url: "", | |
8883ca84 AE |
32 | withCredentials: false, |
33 | // behavior | |
34 | autoAbort: false, | |
35 | ignoreError: false, | |
36 | pinData: false, | |
37 | silent: false, | |
38 | includeRequestedWith: true, | |
39 | // callbacks | |
40 | failure: null, | |
41 | finalize: null, | |
42 | success: null, | |
43 | progress: null, | |
44 | uploadProgress: null, | |
45 | callbackObject: null, | |
46 | }, options); | |
6ab9b916 | 47 | if (typeof options.callbackObject === "object") { |
8883ca84 AE |
48 | this._options.callbackObject = options.callbackObject; |
49 | } | |
50 | this._options.url = Core.convertLegacyUrl(this._options.url); | |
6ab9b916 | 51 | if (this._options.url.indexOf("index.php") === 0) { |
8883ca84 AE |
52 | this._options.url = window.WSC_API_URL + this._options.url; |
53 | } | |
54 | if (this._options.url.indexOf(window.WSC_API_URL) === 0) { | |
55 | this._options.includeRequestedWith = true; | |
56 | // always include credentials when querying the very own server | |
57 | this._options.withCredentials = true; | |
58 | } | |
59 | if (this._options.pinData) { | |
60 | this._data = this._options.data; | |
61 | } | |
62 | if (this._options.callbackObject) { | |
665fa171 | 63 | if (typeof this._options.callbackObject._ajaxFailure === "function") { |
8883ca84 | 64 | this._options.failure = this._options.callbackObject._ajaxFailure.bind(this._options.callbackObject); |
665fa171 AE |
65 | } |
66 | if (typeof this._options.callbackObject._ajaxFinalize === "function") { | |
8883ca84 | 67 | this._options.finalize = this._options.callbackObject._ajaxFinalize.bind(this._options.callbackObject); |
665fa171 AE |
68 | } |
69 | if (typeof this._options.callbackObject._ajaxSuccess === "function") { | |
8883ca84 | 70 | this._options.success = this._options.callbackObject._ajaxSuccess.bind(this._options.callbackObject); |
665fa171 AE |
71 | } |
72 | if (typeof this._options.callbackObject._ajaxProgress === "function") { | |
8883ca84 | 73 | this._options.progress = this._options.callbackObject._ajaxProgress.bind(this._options.callbackObject); |
665fa171 AE |
74 | } |
75 | if (typeof this._options.callbackObject._ajaxUploadProgress === "function") { | |
8883ca84 | 76 | this._options.uploadProgress = this._options.callbackObject._ajaxUploadProgress.bind(this._options.callbackObject); |
665fa171 | 77 | } |
8883ca84 AE |
78 | } |
79 | if (!_didInit) { | |
80 | _didInit = true; | |
6ab9b916 | 81 | window.addEventListener("beforeunload", () => (_ignoreAllErrors = true)); |
8883ca84 AE |
82 | } |
83 | } | |
84 | /** | |
85 | * Dispatches a request, optionally aborting a currently active request. | |
86 | */ | |
87 | sendRequest(abortPrevious) { | |
88 | if (abortPrevious || this._options.autoAbort) { | |
89 | this.abortPrevious(); | |
90 | } | |
91 | if (!this._options.silent) { | |
92 | AjaxStatus.show(); | |
93 | } | |
94 | if (this._xhr instanceof XMLHttpRequest) { | |
95 | this._previousXhr = this._xhr; | |
96 | } | |
97 | this._xhr = new XMLHttpRequest(); | |
98 | this._xhr.open(this._options.type, this._options.url, true); | |
99 | if (this._options.contentType) { | |
6ab9b916 | 100 | this._xhr.setRequestHeader("Content-Type", this._options.contentType); |
8883ca84 AE |
101 | } |
102 | if (this._options.withCredentials || this._options.includeRequestedWith) { | |
6ab9b916 | 103 | this._xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); |
8883ca84 AE |
104 | } |
105 | if (this._options.withCredentials) { | |
106 | this._xhr.withCredentials = true; | |
107 | } | |
8883ca84 | 108 | const options = Core.clone(this._options); |
84ac9bab AE |
109 | // Use a local variable in all callbacks, because `this._xhr` can be overwritten by |
110 | // subsequent requests while a request is still in-flight. | |
111 | const xhr = this._xhr; | |
112 | xhr.onload = () => { | |
665fa171 AE |
113 | if (xhr.readyState === XMLHttpRequest.DONE) { |
114 | if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { | |
0d9e1a53 AE |
115 | if (xhr.status === 204) { |
116 | // HTTP 204 does not contain a body, the `content-type` is undefined. | |
117 | this._success(xhr, options); | |
8883ca84 AE |
118 | } |
119 | else { | |
87d366bf | 120 | if (options.responseType && this.getContentType(xhr) !== options.responseType) { |
0d9e1a53 AE |
121 | // request succeeded but invalid response type |
122 | this._failure(xhr, options); | |
123 | } | |
124 | else { | |
125 | this._success(xhr, options); | |
126 | } | |
8883ca84 AE |
127 | } |
128 | } | |
129 | else { | |
665fa171 | 130 | this._failure(xhr, options); |
8883ca84 AE |
131 | } |
132 | } | |
133 | }; | |
84ac9bab AE |
134 | xhr.onerror = () => { |
135 | this._failure(xhr, options); | |
8883ca84 AE |
136 | }; |
137 | if (this._options.progress) { | |
84ac9bab | 138 | xhr.onprogress = this._options.progress; |
8883ca84 AE |
139 | } |
140 | if (this._options.uploadProgress) { | |
84ac9bab | 141 | xhr.upload.onprogress = this._options.uploadProgress; |
8883ca84 | 142 | } |
6ab9b916 | 143 | if (this._options.type === "POST") { |
8883ca84 | 144 | let data = this._options.data; |
6ab9b916 | 145 | if (typeof data === "object" && Core.getType(data) !== "FormData") { |
8883ca84 AE |
146 | data = Core.serialize(data); |
147 | } | |
84ac9bab | 148 | xhr.send(data); |
8883ca84 AE |
149 | } |
150 | else { | |
84ac9bab | 151 | xhr.send(); |
8883ca84 AE |
152 | } |
153 | } | |
154 | /** | |
155 | * Aborts a previous request. | |
156 | */ | |
157 | abortPrevious() { | |
158 | if (!this._previousXhr) { | |
159 | return; | |
160 | } | |
161 | this._previousXhr.abort(); | |
162 | this._previousXhr = undefined; | |
163 | if (!this._options.silent) { | |
164 | AjaxStatus.hide(); | |
165 | } | |
166 | } | |
167 | /** | |
168 | * Sets a specific option. | |
169 | */ | |
170 | setOption(key, value) { | |
171 | this._options[key] = value; | |
172 | } | |
173 | /** | |
174 | * Returns an option by key or undefined. | |
175 | */ | |
6b64df9d | 176 | // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents |
8883ca84 | 177 | getOption(key) { |
665fa171 | 178 | if (Object.prototype.hasOwnProperty.call(this._options, key)) { |
8883ca84 AE |
179 | return this._options[key]; |
180 | } | |
181 | return null; | |
182 | } | |
183 | /** | |
184 | * Sets request data while honoring pinned data from setup callback. | |
185 | */ | |
186 | setData(data) { | |
6ab9b916 | 187 | if (this._data !== null && Core.getType(data) !== "FormData") { |
8883ca84 AE |
188 | data = Core.extend(this._data, data); |
189 | } | |
190 | this._options.data = data; | |
191 | } | |
192 | /** | |
193 | * Handles a successful request. | |
194 | */ | |
195 | _success(xhr, options) { | |
196 | if (!options.silent) { | |
197 | AjaxStatus.hide(); | |
198 | } | |
6ab9b916 | 199 | if (typeof options.success === "function") { |
8883ca84 | 200 | let data = null; |
87d366bf | 201 | if (this.getContentType(xhr) === "application/json") { |
8883ca84 AE |
202 | try { |
203 | data = JSON.parse(xhr.responseText); | |
204 | } | |
205 | catch (e) { | |
206 | // invalid JSON | |
207 | this._failure(xhr, options); | |
208 | return; | |
209 | } | |
210 | // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring | |
211 | if (data && data.returnValues && data.returnValues.template !== undefined) { | |
212 | data.returnValues.template = data.returnValues.template.trim(); | |
213 | } | |
214 | // force-invoke the background queue | |
215 | if (data && data.forceBackgroundQueuePerform) { | |
665fa171 | 216 | void new Promise((resolve_1, reject_1) => { require(["../BackgroundQueue"], resolve_1, reject_1); }).then(tslib_1.__importStar).then((backgroundQueue) => backgroundQueue.invoke()); |
8883ca84 AE |
217 | } |
218 | } | |
0d9e1a53 | 219 | options.success(data || {}, xhr.responseText, xhr, options.data); |
8883ca84 AE |
220 | } |
221 | this._finalize(options); | |
222 | } | |
223 | /** | |
224 | * Handles failed requests, this can be both a successful request with | |
225 | * a non-success status code or an entirely failed request. | |
226 | */ | |
227 | _failure(xhr, options) { | |
228 | if (_ignoreAllErrors) { | |
229 | return; | |
230 | } | |
231 | if (!options.silent) { | |
232 | AjaxStatus.hide(); | |
233 | } | |
234 | let data = null; | |
235 | try { | |
236 | data = JSON.parse(xhr.responseText); | |
237 | } | |
57b36106 TD |
238 | catch (e) { |
239 | // Ignore JSON parsing failure. | |
240 | } | |
8883ca84 | 241 | let showError = true; |
6ab9b916 | 242 | if (typeof options.failure === "function") { |
df42d11e TD |
243 | // undefined might be returned by legacy callbacks and must be treated as 'true'. |
244 | const result = options.failure(data || {}, xhr.responseText || "", xhr, options.data); | |
245 | showError = result !== false; | |
8883ca84 AE |
246 | } |
247 | if (options.ignoreError !== true && showError) { | |
248 | const html = this.getErrorHtml(data, xhr); | |
249 | if (html) { | |
665fa171 | 250 | void new Promise((resolve_2, reject_2) => { require(["../Ui/Dialog"], resolve_2, reject_2); }).then(tslib_1.__importStar).then((UiDialog) => { |
e7906854 | 251 | UiDialog.openStatic(Util_1.default.getUniqueId(), html, { |
6ab9b916 | 252 | title: Language.get("wcf.global.error.title"), |
e7906854 | 253 | }); |
8883ca84 | 254 | }); |
8883ca84 AE |
255 | } |
256 | } | |
257 | this._finalize(options); | |
258 | } | |
259 | /** | |
260 | * Returns the inner HTML for an error/exception display. | |
261 | */ | |
262 | getErrorHtml(data, xhr) { | |
6ab9b916 | 263 | let details = ""; |
8883ca84 | 264 | let message; |
b49c9ead | 265 | if (data !== null && Object.keys(data).length > 0) { |
8883ca84 | 266 | if (data.returnValues && data.returnValues.description) { |
665fa171 | 267 | details += `<br><p>Description:</p><p>${data.returnValues.description}</p>`; |
8883ca84 AE |
268 | } |
269 | if (data.file && data.line) { | |
665fa171 AE |
270 | details += `<br><p>File:</p><p>${data.file} in line ${data.line}</p>`; |
271 | } | |
272 | if (data.stacktrace) { | |
273 | details += `<br><p>Stacktrace:</p><p>${data.stacktrace}</p>`; | |
274 | } | |
275 | else if (data.exceptionID) { | |
276 | details += `<br><p>Exception ID: <code>${data.exceptionID}</code></p>`; | |
8883ca84 | 277 | } |
8883ca84 | 278 | message = data.message; |
665fa171 AE |
279 | data.previous.forEach((previous) => { |
280 | details += `<hr><p>${previous.message}</p>`; | |
281 | details += `<br><p>Stacktrace</p><p>${previous.stacktrace}</p>`; | |
8883ca84 AE |
282 | }); |
283 | } | |
284 | else { | |
285 | message = xhr.responseText; | |
286 | } | |
6ab9b916 | 287 | if (!message || message === "undefined") { |
665fa171 | 288 | if (!window.ENABLE_DEBUG_MODE) { |
8883ca84 | 289 | return null; |
665fa171 | 290 | } |
6ab9b916 | 291 | message = "XMLHttpRequest failed without a responseText. Check your browser console."; |
8883ca84 | 292 | } |
665fa171 | 293 | return `<div class="ajaxDebugMessage"><p>${message}</p>${details}</div>`; |
8883ca84 AE |
294 | } |
295 | /** | |
296 | * Finalizes a request. | |
297 | * | |
298 | * @param {Object} options request options | |
299 | */ | |
300 | _finalize(options) { | |
6ab9b916 | 301 | if (typeof options.finalize === "function") { |
8883ca84 AE |
302 | options.finalize(this._xhr); |
303 | } | |
304 | this._previousXhr = undefined; | |
305 | Listener_1.default.trigger(); | |
306 | // fix anchor tags generated through WCF::getAnchor() | |
9a0c1b60 | 307 | document.querySelectorAll('a[href*="#"]').forEach((link) => { |
8883ca84 | 308 | let href = link.href; |
6ab9b916 TD |
309 | if (href.indexOf("AJAXProxy") !== -1 || href.indexOf("ajax-proxy") !== -1) { |
310 | href = href.substr(href.indexOf("#")); | |
311 | link.href = document.location.toString().replace(/#.*/, "") + href; | |
8883ca84 AE |
312 | } |
313 | }); | |
314 | } | |
87d366bf AE |
315 | getContentType(xhr) { |
316 | const contentType = xhr.getResponseHeader("content-type"); | |
317 | if (contentType === null) { | |
318 | return null; | |
319 | } | |
320 | return contentType.split(";", 1)[0].trim(); | |
321 | } | |
8883ca84 | 322 | } |
564f1742 | 323 | Core.enableLegacyInheritance(AjaxRequest); |
8883ca84 | 324 | return AjaxRequest; |
28dfae01 | 325 | }); |