Convert `Ajax/Request` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Fri, 16 Oct 2020 16:32:56 +0000 (18:32 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Wed, 28 Oct 2020 11:29:44 +0000 (12:29 +0100)
There are two missing components that will later be added.

global.d.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Ajax/Request.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Ajax/Request.ts [new file with mode: 0644]

index bd55ce72315701ecab87a50507e0ab8cec57c8f6..0410faf60a49c44f47eac4af15a4d16aefc2684d 100644 (file)
@@ -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;
index e9fad9cb17cf875b429c21b20548c980c1f45dcc..91159492e861279edacc295a703e0a9d2b6c4443 100644 (file)
 /**
  * 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 <http://opensource.org/licenses/lgpl-license.php>
- * @module     AjaxRequest (alias)
- * @module     WoltLabSuite/Core/Ajax/Request
+ *
+ * @author  Alexander Ebert
+ * @copyright  2001-2019 WoltLab GmbH
+ * @license  GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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 += '<br><p>Description:</p><p>' + data.returnValues.description + '</p>';
-                               }
-                               
-                               if (data.file && data.line) {
-                                       details += '<br><p>File:</p><p>' + data.file + ' in line ' + data.line + '</p>';
-                               }
-                               
-                               if (data.stacktrace) details += '<br><p>Stacktrace:</p><p>' + data.stacktrace + '</p>';
-                               else if (data.exceptionID) details += '<br><p>Exception ID: <code>' + data.exceptionID + '</code></p>';
-                               
-                               message = data.message;
-                               
-                               data.previous.forEach(function(previous) {
-                                       details += '<hr><p>' + previous.message + '</p>';
-                                       details += '<br><p>Stacktrace</p><p>' + previous.stacktrace + '</p>';
-                               });
-                       }
-                       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 '<div class="ajaxDebugMessage"><p>' + message + '</p>' + details + '</div>';
-               },
-               
-               /**
-                * 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 += '<br><p>Description:</p><p>' + data.returnValues.description + '</p>';
+                }
+                if (data.file && data.line) {
+                    details += '<br><p>File:</p><p>' + data.file + ' in line ' + data.line + '</p>';
+                }
+                if (data.stacktrace)
+                    details += '<br><p>Stacktrace:</p><p>' + data.stacktrace + '</p>';
+                else if (data.exceptionID)
+                    details += '<br><p>Exception ID: <code>' + data.exceptionID + '</code></p>';
+                message = data.message;
+                data.previous.forEach(function (previous) {
+                    details += '<hr><p>' + previous.message + '</p>';
+                    details += '<br><p>Stacktrace</p><p>' + previous.stacktrace + '</p>';
+                });
+            }
+            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 '<div class="ajaxDebugMessage"><p>' + message + '</p>' + details + '</div>';
+        }
+        /**
+         * 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 (file)
index 0000000..b8fb545
--- /dev/null
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
+ * @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 += '<br><p>Description:</p><p>' + data.returnValues.description + '</p>';
+      }
+
+      if (data.file && data.line) {
+        details += '<br><p>File:</p><p>' + data.file + ' in line ' + data.line + '</p>';
+      }
+
+      if (data.stacktrace) details += '<br><p>Stacktrace:</p><p>' + data.stacktrace + '</p>';
+      else if (data.exceptionID) details += '<br><p>Exception ID: <code>' + data.exceptionID + '</code></p>';
+
+      message = data.message;
+
+      data.previous.forEach(function (previous) {
+        details += '<hr><p>' + previous.message + '</p>';
+        details += '<br><p>Stacktrace</p><p>' + previous.stacktrace + '</p>';
+      });
+    } 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 '<div class="ajaxDebugMessage"><p>' + message + '</p>' + details + '</div>';
+  }
+
+  /**
+   * 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<HTMLAnchorElement>('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;