From: Alexander Ebert Date: Fri, 16 Oct 2020 16:32:56 +0000 (+0200) Subject: Convert `Ajax/Request` to TypeScript X-Git-Tag: 5.4.0_Alpha_1~704^2~49 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=8883ca84e2b5c9a499ab0face23e028d24b330cd;p=GitHub%2FWoltLab%2FWCF.git Convert `Ajax/Request` to TypeScript There are two missing components that will later be added. --- diff --git a/global.d.ts b/global.d.ts index bd55ce7231..0410faf60a 100644 --- a/global.d.ts +++ b/global.d.ts @@ -5,7 +5,9 @@ import * as ColorUtil from './wcfsetup/install/files/ts/WoltLabSuite/Core/ColorU declare global { interface Window { Devtools?: typeof Devtools; + ENABLE_DEBUG_MODE: boolean; WCF_PATH: string; + WSC_API_URL: string; bc_wcfDomUtil: typeof DomUtil; __wcf_bc_colorUtil: typeof ColorUtil; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Request.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Request.js index e9fad9cb17..91159492e8 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Request.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Request.js @@ -1,376 +1,326 @@ /** * Versatile AJAX request handling. - * + * * In case you want to issue JSONP requests, please use `AjaxJsonp` instead. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module AjaxRequest (alias) - * @module WoltLabSuite/Core/Ajax/Request + * + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module AjaxRequest (alias) + * @module WoltLabSuite/Core/Ajax/Request */ -define(['Core', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Ui/Dialog', 'WoltLabSuite/Core/Ajax/Status'], function(Core, Language, DomChangeListener, DomUtil, UiDialog, AjaxStatus) { - "use strict"; - - var _didInit = false; - var _ignoreAllErrors = false; - - /** - * @constructor - */ - function AjaxRequest(options) { - this._data = null; - this._options = {}; - this._previousXhr = null; - this._xhr = null; - - this._init(options); - } - AjaxRequest.prototype = { - /** - * Initializes the request options. - * - * @param {Object} options request options - */ - _init: function(options) { - this._options = Core.extend({ - // request data - data: {}, - contentType: 'application/x-www-form-urlencoded; charset=UTF-8', - responseType: 'application/json', - type: 'POST', - url: '', - withCredentials: false, - - // behavior - autoAbort: false, - ignoreError: false, - pinData: false, - silent: false, - includeRequestedWith: true, - - // callbacks - failure: null, - finalize: null, - success: null, - progress: null, - uploadProgress: null, - - callbackObject: null - }, options); - - if (typeof options.callbackObject === 'object') { - this._options.callbackObject = options.callbackObject; - } - - this._options.url = Core.convertLegacyUrl(this._options.url); - if (this._options.url.indexOf('index.php') === 0) { - this._options.url = WSC_API_URL + this._options.url; - } - - if (this._options.url.indexOf(WSC_API_URL) === 0) { - this._options.includeRequestedWith = true; - // always include credentials when querying the very own server - this._options.withCredentials = true; - } - - if (this._options.pinData) { - this._data = Core.extend({}, this._options.data); - } - - if (this._options.callbackObject !== null) { - if (typeof this._options.callbackObject._ajaxFailure === 'function') this._options.failure = this._options.callbackObject._ajaxFailure.bind(this._options.callbackObject); - if (typeof this._options.callbackObject._ajaxFinalize === 'function') this._options.finalize = this._options.callbackObject._ajaxFinalize.bind(this._options.callbackObject); - if (typeof this._options.callbackObject._ajaxSuccess === 'function') this._options.success = this._options.callbackObject._ajaxSuccess.bind(this._options.callbackObject); - if (typeof this._options.callbackObject._ajaxProgress === 'function') this._options.progress = this._options.callbackObject._ajaxProgress.bind(this._options.callbackObject); - if (typeof this._options.callbackObject._ajaxUploadProgress === 'function') this._options.uploadProgress = this._options.callbackObject._ajaxUploadProgress.bind(this._options.callbackObject); - } - - if (_didInit === false) { - _didInit = true; - - window.addEventListener('beforeunload', function() { _ignoreAllErrors = true; }); - } - }, - - /** - * Dispatches a request, optionally aborting a currently active request. - * - * @param {boolean} abortPrevious abort currently active request - */ - sendRequest: function(abortPrevious) { - if (abortPrevious === true || this._options.autoAbort) { - this.abortPrevious(); - } - - if (!this._options.silent) { - AjaxStatus.show(); - } - - if (this._xhr instanceof XMLHttpRequest) { - this._previousXhr = this._xhr; - } - - this._xhr = new XMLHttpRequest(); - this._xhr.open(this._options.type, this._options.url, true); - if (this._options.contentType) { - this._xhr.setRequestHeader('Content-Type', this._options.contentType); - } - if (this._options.withCredentials || this._options.includeRequestedWith) { - this._xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - } - if (this._options.withCredentials) { - this._xhr.withCredentials = true; - } - - var self = this; - var options = Core.clone(this._options); - this._xhr.onload = function() { - if (this.readyState === XMLHttpRequest.DONE) { - if (this.status >= 200 && this.status < 300 || this.status === 304) { - if (options.responseType && this.getResponseHeader('Content-Type').indexOf(options.responseType) !== 0) { - // request succeeded but invalid response type - self._failure(this, options); - } - else { - self._success(this, options); - } - } - else { - self._failure(this, options); - } - } - }; - this._xhr.onerror = function() { - self._failure(this, options); - }; - - if (this._options.progress) { - this._xhr.onprogress = this._options.progress; - } - if (this._options.uploadProgress) { - this._xhr.upload.onprogress = this._options.uploadProgress; - } - - if (this._options.type === 'POST') { - var data = this._options.data; - if (typeof data === 'object' && Core.getType(data) !== 'FormData') { - data = Core.serialize(data); - } - - this._xhr.send(data); - } - else { - this._xhr.send(); - } - }, - - /** - * Aborts a previous request. - */ - abortPrevious: function() { - if (this._previousXhr === null) { - return; - } - - this._previousXhr.abort(); - this._previousXhr = null; - - if (!this._options.silent) { - AjaxStatus.hide(); - } - }, - - /** - * Sets a specific option. - * - * @param {string} key option name - * @param {?} value option value - */ - setOption: function(key, value) { - this._options[key] = value; - }, - - /** - * Returns an option by key or undefined. - * - * @param {string} key option name - * @return {(*|null)} option value or null - */ - getOption: function(key) { - if (objOwns(this._options, key)) { - return this._options[key]; - } - - return null; - }, - - /** - * Sets request data while honoring pinned data from setup callback. - * - * @param {Object} data request data - */ - setData: function(data) { - if (this._data !== null && Core.getType(data) !== 'FormData') { - data = Core.extend(this._data, data); - } - - this._options.data = data; - }, - - /** - * Handles a successful request. - * - * @param {XMLHttpRequest} xhr request object - * @param {Object} options request options - */ - _success: function(xhr, options) { - if (!options.silent) { - AjaxStatus.hide(); - } - - if (typeof options.success === 'function') { - var data = null; - if (xhr.getResponseHeader('Content-Type').split(';', 1)[0].trim() === 'application/json') { - try { - data = JSON.parse(xhr.responseText); - } - catch (e) { - // invalid JSON - this._failure(xhr, options); - - return; - } - - // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring - if (data && data.returnValues && data.returnValues.template !== undefined) { - data.returnValues.template = data.returnValues.template.trim(); - } - - // force-invoke the background queue - if (data && data.forceBackgroundQueuePerform) { - require(['WoltLabSuite/Core/BackgroundQueue'], function(BackgroundQueue) { - BackgroundQueue.invoke(); - }); - } - } - - options.success(data, xhr.responseText, xhr, options.data); - } - - this._finalize(options); - }, - - /** - * Handles failed requests, this can be both a successful request with - * a non-success status code or an entirely failed request. - * - * @param {XMLHttpRequest} xhr request object - * @param {Object} options request options - */ - _failure: function (xhr, options) { - if (_ignoreAllErrors) { - return; - } - - if (!options.silent) { - AjaxStatus.hide(); - } - - var data = null; - try { - data = JSON.parse(xhr.responseText); - } - catch (e) {} - - var showError = true; - if (typeof options.failure === 'function') { - showError = options.failure((data || {}), (xhr.responseText || ''), xhr, options.data); - } - - if (options.ignoreError !== true && showError !== false) { - var html = this.getErrorHtml(data, xhr); - - if (html) { - if (UiDialog === undefined) UiDialog = require('Ui/Dialog'); - UiDialog.openStatic(DomUtil.getUniqueId(), html, { - title: Language.get('wcf.global.error.title') - }); - } - } - - this._finalize(options); - }, - - /** - * Returns the inner HTML for an error/exception display. - * - * @param {Object} data - * @param {XMLHttpRequest} xhr - * @return {string} - */ - getErrorHtml: function(data, xhr) { - var details = ''; - var message = ''; - - if (data !== null) { - if (data.returnValues && data.returnValues.description) { - details += '

Description:

' + data.returnValues.description + '

'; - } - - if (data.file && data.line) { - details += '

File:

' + data.file + ' in line ' + data.line + '

'; - } - - if (data.stacktrace) details += '

Stacktrace:

' + data.stacktrace + '

'; - else if (data.exceptionID) details += '

Exception ID: ' + data.exceptionID + '

'; - - message = data.message; - - data.previous.forEach(function(previous) { - details += '

' + previous.message + '

'; - details += '

Stacktrace

' + previous.stacktrace + '

'; - }); - } - else { - message = xhr.responseText; - } - - if (!message || message === 'undefined') { - if (!ENABLE_DEBUG_MODE) return null; - - message = 'XMLHttpRequest failed without a responseText. Check your browser console.' - } - - return '

' + message + '

' + details + '
'; - }, - - /** - * Finalizes a request. - * - * @param {Object} options request options - */ - _finalize: function(options) { - if (typeof options.finalize === 'function') { - options.finalize(this._xhr); - } - - this._previousXhr = null; - - DomChangeListener.trigger(); - - // fix anchor tags generated through WCF::getAnchor() - var links = elBySelAll('a[href*="#"]'); - for (var i = 0, length = links.length; i < length; i++) { - var link = links[i]; - var href = elAttr(link, 'href'); - if (href.indexOf('AJAXProxy') !== -1 || href.indexOf('ajax-proxy') !== -1) { - href = href.substr(href.indexOf('#')); - elAttr(link, 'href', document.location.toString().replace(/#.*/, '') + href); - } - } - } - }; - - return AjaxRequest; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +define(["require", "exports", "./Status", "../Core", "../Dom/Change/Listener"], function (require, exports, AjaxStatus, Core, Listener_1) { + "use strict"; + AjaxStatus = __importStar(AjaxStatus); + Core = __importStar(Core); + Listener_1 = __importDefault(Listener_1); + let _didInit = false; + let _ignoreAllErrors = false; + /** + * @constructor + */ + class AjaxRequest { + constructor(options) { + this._options = Core.extend({ + data: {}, + contentType: 'application/x-www-form-urlencoded; charset=UTF-8', + responseType: 'application/json', + type: 'POST', + url: '', + withCredentials: false, + // behavior + autoAbort: false, + ignoreError: false, + pinData: false, + silent: false, + includeRequestedWith: true, + // callbacks + failure: null, + finalize: null, + success: null, + progress: null, + uploadProgress: null, + callbackObject: null, + }, options); + if (typeof options.callbackObject === 'object') { + this._options.callbackObject = options.callbackObject; + } + this._options.url = Core.convertLegacyUrl(this._options.url); + if (this._options.url.indexOf('index.php') === 0) { + this._options.url = window.WSC_API_URL + this._options.url; + } + if (this._options.url.indexOf(window.WSC_API_URL) === 0) { + this._options.includeRequestedWith = true; + // always include credentials when querying the very own server + this._options.withCredentials = true; + } + if (this._options.pinData) { + this._data = this._options.data; + } + if (this._options.callbackObject) { + if (typeof this._options.callbackObject._ajaxFailure === 'function') + this._options.failure = this._options.callbackObject._ajaxFailure.bind(this._options.callbackObject); + if (typeof this._options.callbackObject._ajaxFinalize === 'function') + this._options.finalize = this._options.callbackObject._ajaxFinalize.bind(this._options.callbackObject); + if (typeof this._options.callbackObject._ajaxSuccess === 'function') + this._options.success = this._options.callbackObject._ajaxSuccess.bind(this._options.callbackObject); + if (typeof this._options.callbackObject._ajaxProgress === 'function') + this._options.progress = this._options.callbackObject._ajaxProgress.bind(this._options.callbackObject); + if (typeof this._options.callbackObject._ajaxUploadProgress === 'function') + this._options.uploadProgress = this._options.callbackObject._ajaxUploadProgress.bind(this._options.callbackObject); + } + if (!_didInit) { + _didInit = true; + window.addEventListener('beforeunload', () => _ignoreAllErrors = true); + } + } + /** + * Dispatches a request, optionally aborting a currently active request. + */ + sendRequest(abortPrevious) { + if (abortPrevious || this._options.autoAbort) { + this.abortPrevious(); + } + if (!this._options.silent) { + AjaxStatus.show(); + } + if (this._xhr instanceof XMLHttpRequest) { + this._previousXhr = this._xhr; + } + this._xhr = new XMLHttpRequest(); + this._xhr.open(this._options.type, this._options.url, true); + if (this._options.contentType) { + this._xhr.setRequestHeader('Content-Type', this._options.contentType); + } + if (this._options.withCredentials || this._options.includeRequestedWith) { + this._xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + } + if (this._options.withCredentials) { + this._xhr.withCredentials = true; + } + const self = this; + const options = Core.clone(this._options); + this._xhr.onload = function () { + if (this.readyState === XMLHttpRequest.DONE) { + if (this.status >= 200 && this.status < 300 || this.status === 304) { + if (options.responseType && this.getResponseHeader('Content-Type').indexOf(options.responseType) !== 0) { + // request succeeded but invalid response type + self._failure(this, options); + } + else { + self._success(this, options); + } + } + else { + self._failure(this, options); + } + } + }; + this._xhr.onerror = function () { + self._failure(this, options); + }; + if (this._options.progress) { + this._xhr.onprogress = this._options.progress; + } + if (this._options.uploadProgress) { + this._xhr.upload.onprogress = this._options.uploadProgress; + } + if (this._options.type === 'POST') { + let data = this._options.data; + if (typeof data === 'object' && Core.getType(data) !== 'FormData') { + data = Core.serialize(data); + } + this._xhr.send(data); + } + else { + this._xhr.send(); + } + } + /** + * Aborts a previous request. + */ + abortPrevious() { + if (!this._previousXhr) { + return; + } + this._previousXhr.abort(); + this._previousXhr = undefined; + if (!this._options.silent) { + AjaxStatus.hide(); + } + } + /** + * Sets a specific option. + */ + setOption(key, value) { + this._options[key] = value; + } + /** + * Returns an option by key or undefined. + */ + getOption(key) { + if (this._options.hasOwnProperty(key)) { + return this._options[key]; + } + return null; + } + /** + * Sets request data while honoring pinned data from setup callback. + */ + setData(data) { + if (this._data !== null && Core.getType(data) !== 'FormData') { + data = Core.extend(this._data, data); + } + this._options.data = data; + } + /** + * Handles a successful request. + */ + _success(xhr, options) { + if (!options.silent) { + AjaxStatus.hide(); + } + if (typeof options.success === 'function') { + let data = null; + if (xhr.getResponseHeader('Content-Type').split(';', 1)[0].trim() === 'application/json') { + try { + data = JSON.parse(xhr.responseText); + } + catch (e) { + // invalid JSON + this._failure(xhr, options); + return; + } + // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring + if (data && data.returnValues && data.returnValues.template !== undefined) { + data.returnValues.template = data.returnValues.template.trim(); + } + // force-invoke the background queue + if (data && data.forceBackgroundQueuePerform) { + // TODO + throw new Error('TODO: Invoking the BackgroundQueue is not yet supported.'); + /* + require(['WoltLabSuite/Core/BackgroundQueue'], function (BackgroundQueue) { + BackgroundQueue.invoke(); + }); + */ + } + } + options.success(data, xhr.responseText, xhr, options.data); + } + this._finalize(options); + } + /** + * Handles failed requests, this can be both a successful request with + * a non-success status code or an entirely failed request. + */ + _failure(xhr, options) { + if (_ignoreAllErrors) { + return; + } + if (!options.silent) { + AjaxStatus.hide(); + } + let data = null; + try { + data = JSON.parse(xhr.responseText); + } + catch (e) { + } + let showError = true; + if (typeof options.failure === 'function') { + showError = options.failure((data || {}), (xhr.responseText || ''), xhr, options.data); + } + if (options.ignoreError !== true && showError) { + const html = this.getErrorHtml(data, xhr); + if (html) { + // TODO + throw new Error('TODO: Yielding dialogs is not yet supported.'); + /* + if (UiDialog === undefined) UiDialog = require('Ui/Dialog'); + UiDialog.openStatic(DomUtil.getUniqueId(), html, { + title: Language.get('wcf.global.error.title'), + }); + */ + } + } + this._finalize(options); + } + /** + * Returns the inner HTML for an error/exception display. + */ + getErrorHtml(data, xhr) { + let details = ''; + let message; + if (data !== null) { + if (data.returnValues && data.returnValues.description) { + details += '

Description:

' + data.returnValues.description + '

'; + } + if (data.file && data.line) { + details += '

File:

' + data.file + ' in line ' + data.line + '

'; + } + if (data.stacktrace) + details += '

Stacktrace:

' + data.stacktrace + '

'; + else if (data.exceptionID) + details += '

Exception ID: ' + data.exceptionID + '

'; + message = data.message; + data.previous.forEach(function (previous) { + details += '

' + previous.message + '

'; + details += '

Stacktrace

' + previous.stacktrace + '

'; + }); + } + else { + message = xhr.responseText; + } + if (!message || message === 'undefined') { + if (!window.ENABLE_DEBUG_MODE) + return null; + message = 'XMLHttpRequest failed without a responseText. Check your browser console.'; + } + return '

' + message + '

' + details + '
'; + } + /** + * Finalizes a request. + * + * @param {Object} options request options + */ + _finalize(options) { + if (typeof options.finalize === 'function') { + options.finalize(this._xhr); + } + this._previousXhr = undefined; + Listener_1.default.trigger(); + // fix anchor tags generated through WCF::getAnchor() + document.querySelectorAll('a[href*="#"]').forEach(link => { + let href = link.href; + if (href.indexOf('AJAXProxy') !== -1 || href.indexOf('ajax-proxy') !== -1) { + href = href.substr(href.indexOf('#')); + link.href = document.location.toString().replace(/#.*/, '') + href; + } + }); + } + } + return AjaxRequest; }); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ajax/Request.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ajax/Request.ts new file mode 100644 index 0000000000..b8fb545a5c --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ajax/Request.ts @@ -0,0 +1,399 @@ +/** + * Versatile AJAX request handling. + * + * In case you want to issue JSONP requests, please use `AjaxJsonp` instead. + * + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module AjaxRequest (alias) + * @module WoltLabSuite/Core/Ajax/Request + */ + +import * as AjaxStatus from './Status'; +import * as Core from '../Core'; +import DomChangeListener from '../Dom/Change/Listener'; + +let _didInit = false; +let _ignoreAllErrors = false; + +/** + * @constructor + */ +class AjaxRequest { + private readonly _options: RequestOptions; + private readonly _data: RequestData; + private _previousXhr?: XMLHttpRequest; + private _xhr?: XMLHttpRequest; + + constructor(options: RequestOptions) { + this._options = Core.extend({ + data: {}, + contentType: 'application/x-www-form-urlencoded; charset=UTF-8', + responseType: 'application/json', + type: 'POST', + url: '', + withCredentials: false, + + // behavior + autoAbort: false, + ignoreError: false, + pinData: false, + silent: false, + includeRequestedWith: true, + + // callbacks + failure: null, + finalize: null, + success: null, + progress: null, + uploadProgress: null, + + callbackObject: null, + }, options); + + if (typeof options.callbackObject === 'object') { + this._options.callbackObject = options.callbackObject; + } + + this._options.url = Core.convertLegacyUrl(this._options.url!); + if (this._options.url.indexOf('index.php') === 0) { + this._options.url = window.WSC_API_URL + this._options.url; + } + + if (this._options.url.indexOf(window.WSC_API_URL) === 0) { + this._options.includeRequestedWith = true; + // always include credentials when querying the very own server + this._options.withCredentials = true; + } + + if (this._options.pinData) { + this._data = this._options.data!; + } + + if (this._options.callbackObject) { + if (typeof this._options.callbackObject._ajaxFailure === 'function') this._options.failure = this._options.callbackObject._ajaxFailure.bind(this._options.callbackObject); + if (typeof this._options.callbackObject._ajaxFinalize === 'function') this._options.finalize = this._options.callbackObject._ajaxFinalize.bind(this._options.callbackObject); + if (typeof this._options.callbackObject._ajaxSuccess === 'function') this._options.success = this._options.callbackObject._ajaxSuccess.bind(this._options.callbackObject); + if (typeof this._options.callbackObject._ajaxProgress === 'function') this._options.progress = this._options.callbackObject._ajaxProgress.bind(this._options.callbackObject); + if (typeof this._options.callbackObject._ajaxUploadProgress === 'function') this._options.uploadProgress = this._options.callbackObject._ajaxUploadProgress.bind(this._options.callbackObject); + } + + if (!_didInit) { + _didInit = true; + + window.addEventListener('beforeunload', () => _ignoreAllErrors = true); + } + } + + /** + * Dispatches a request, optionally aborting a currently active request. + */ + sendRequest(abortPrevious: boolean): void { + if (abortPrevious || this._options.autoAbort) { + this.abortPrevious(); + } + + if (!this._options.silent) { + AjaxStatus.show(); + } + + if (this._xhr instanceof XMLHttpRequest) { + this._previousXhr = this._xhr; + } + + this._xhr = new XMLHttpRequest(); + this._xhr.open(this._options.type!, this._options.url!, true); + if (this._options.contentType) { + this._xhr.setRequestHeader('Content-Type', this._options.contentType); + } + if (this._options.withCredentials || this._options.includeRequestedWith) { + this._xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + } + if (this._options.withCredentials) { + this._xhr.withCredentials = true; + } + + const self = this; + const options = Core.clone(this._options) as RequestOptions; + this._xhr.onload = function () { + if (this.readyState === XMLHttpRequest.DONE) { + if (this.status >= 200 && this.status < 300 || this.status === 304) { + if (options.responseType && this.getResponseHeader('Content-Type')!.indexOf(options.responseType) !== 0) { + // request succeeded but invalid response type + self._failure(this, options); + } else { + self._success(this, options); + } + } else { + self._failure(this, options); + } + } + }; + this._xhr.onerror = function () { + self._failure(this, options); + }; + + if (this._options.progress) { + this._xhr.onprogress = this._options.progress; + } + if (this._options.uploadProgress) { + this._xhr.upload.onprogress = this._options.uploadProgress; + } + + if (this._options.type === 'POST') { + let data: string | RequestData = this._options.data!; + if (typeof data === 'object' && Core.getType(data) !== 'FormData') { + data = Core.serialize(data); + } + + this._xhr.send(data as any); + } else { + this._xhr.send(); + } + } + + /** + * Aborts a previous request. + */ + abortPrevious(): void { + if (!this._previousXhr) { + return; + } + + this._previousXhr.abort(); + this._previousXhr = undefined; + + if (!this._options.silent) { + AjaxStatus.hide(); + } + } + + /** + * Sets a specific option. + */ + setOption(key: string, value: any): void { + this._options[key] = value; + } + + /** + * Returns an option by key or undefined. + */ + getOption(key: string): unknown | null { + if (this._options.hasOwnProperty(key)) { + return this._options[key]; + } + + return null; + } + + /** + * Sets request data while honoring pinned data from setup callback. + */ + setData(data: RequestData): void { + if (this._data !== null && Core.getType(data) !== 'FormData') { + data = Core.extend(this._data, data); + } + + this._options.data = data; + } + + /** + * Handles a successful request. + */ + _success(xhr: XMLHttpRequest, options: RequestOptions): void { + if (!options.silent) { + AjaxStatus.hide(); + } + + if (typeof options.success === 'function') { + let data: ResponseData | null = null; + if (xhr.getResponseHeader('Content-Type')!.split(';', 1)[0].trim() === 'application/json') { + try { + data = JSON.parse(xhr.responseText); + } catch (e) { + // invalid JSON + this._failure(xhr, options); + + return; + } + + // trim HTML before processing, see http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring + if (data && data.returnValues && data.returnValues.template !== undefined) { + data.returnValues.template = data.returnValues.template.trim(); + } + + // force-invoke the background queue + if (data && data.forceBackgroundQueuePerform) { + // TODO + throw new Error('TODO: Invoking the BackgroundQueue is not yet supported.'); + /* + require(['WoltLabSuite/Core/BackgroundQueue'], function (BackgroundQueue) { + BackgroundQueue.invoke(); + }); + */ + } + } + + options.success(data!, xhr.responseText, xhr, options.data!); + } + + this._finalize(options); + } + + /** + * Handles failed requests, this can be both a successful request with + * a non-success status code or an entirely failed request. + */ + _failure(xhr: XMLHttpRequest, options: RequestOptions): void { + if (_ignoreAllErrors) { + return; + } + + if (!options.silent) { + AjaxStatus.hide(); + } + + let data: ResponseData | null = null; + try { + data = JSON.parse(xhr.responseText); + } catch (e) { + } + + let showError = true; + if (typeof options.failure === 'function') { + showError = options.failure((data || {}), (xhr.responseText || ''), xhr, options.data!); + } + + if (options.ignoreError !== true && showError) { + const html = this.getErrorHtml(data, xhr); + + if (html) { + // TODO + throw new Error('TODO: Yielding dialogs is not yet supported.'); + /* + if (UiDialog === undefined) UiDialog = require('Ui/Dialog'); + UiDialog.openStatic(DomUtil.getUniqueId(), html, { + title: Language.get('wcf.global.error.title'), + }); + */ + } + } + + this._finalize(options); + } + + /** + * Returns the inner HTML for an error/exception display. + */ + getErrorHtml(data: ResponseData | null, xhr: XMLHttpRequest): string | null { + let details = ''; + let message: string; + + if (data !== null) { + if (data.returnValues && data.returnValues.description) { + details += '

Description:

' + data.returnValues.description + '

'; + } + + if (data.file && data.line) { + details += '

File:

' + data.file + ' in line ' + data.line + '

'; + } + + if (data.stacktrace) details += '

Stacktrace:

' + data.stacktrace + '

'; + else if (data.exceptionID) details += '

Exception ID: ' + data.exceptionID + '

'; + + message = data.message; + + data.previous.forEach(function (previous) { + details += '

' + previous.message + '

'; + details += '

Stacktrace

' + previous.stacktrace + '

'; + }); + } else { + message = xhr.responseText; + } + + if (!message || message === 'undefined') { + if (!window.ENABLE_DEBUG_MODE) return null; + + message = 'XMLHttpRequest failed without a responseText. Check your browser console.'; + } + + return '

' + message + '

' + details + '
'; + } + + /** + * Finalizes a request. + * + * @param {Object} options request options + */ + _finalize(options: RequestOptions): void { + if (typeof options.finalize === 'function') { + options.finalize(this._xhr!); + } + + this._previousXhr = undefined; + + DomChangeListener.trigger(); + + // fix anchor tags generated through WCF::getAnchor() + document.querySelectorAll('a[href*="#"]').forEach(link => { + let href = link.href; + if (href.indexOf('AJAXProxy') !== -1 || href.indexOf('ajax-proxy') !== -1) { + href = href.substr(href.indexOf('#')); + link.href = document.location.toString().replace(/#.*/, '') + href; + } + }); + } +} + +interface RequestPayload { + [key: string]: any; +} + +type RequestData = FormData | RequestPayload; + +interface ResponseData { + [key: string]: any; +} + +type CallbackFailure = (data: ResponseData, responseText: string, xhr: XMLHttpRequest, requestData: RequestData) => boolean; +type CallbackFinalize = (xhr: XMLHttpRequest) => void; +type CallbackProgress = (event: ProgressEvent) => void; +type CallbackSuccess = (data: ResponseData, responseText: string, xhr: XMLHttpRequest, requestData: RequestData) => void; +type CallbackUploadProgress = (event: ProgressEvent) => void; + +interface CallbackObject { + _ajaxFailure?: CallbackFailure; + _ajaxFinalize?: CallbackFinalize; + _ajaxProgress?: CallbackProgress; + _ajaxSuccess: CallbackSuccess; + _ajaxUploadProgress?: CallbackUploadProgress; +} + +interface RequestOptions { + // request data + data?: RequestData, + contentType?: string, + responseType?: string, + type?: string, + url?: string, + withCredentials?: boolean, + + // behavior + autoAbort?: boolean, + ignoreError?: boolean, + pinData?: boolean, + silent?: boolean, + includeRequestedWith?: boolean, + + // callbacks + failure?: CallbackFailure, + finalize?: CallbackFinalize, + success?: CallbackSuccess, + progress?: CallbackProgress, + uploadProgress?: CallbackUploadProgress, + + callbackObject?: CallbackObject | null, +} + +export = AjaxRequest;