Satisfy ESLint
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WoltLabSuite / Core / Ajax / Request.js
CommitLineData
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 12define(["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});