Source: cordova-plugin-bbd-xmlhttprequest/assets/www/android/GDXmlHttpRequest.js

/*
 * (c) 2021 BlackBerry Limited. All rights reserved.
 */

;
(function() {
    var cordovaExec = require('cordova/exec'),
        ProgressEvent = require('cordova-plugin-bbd-file.ProgressEvent');

    // =====================================================================================
    //      EventTarget
    //  Implementation of EventTarget interface for the GDXMLHttpRequestEventTarget prototype.
    //  Implements the "addEventListener" and "removeEventListener" functions. "fireEvent"
    //  function is used as a callback trigger.
    // =====================================================================================
    var EventTarget = function() {
        var eventListeners = {};

        Object.defineProperty(this, 'listeners', {
            get: function() {
                return eventListeners;
            },
            enumerable: false
        });
    }

    Object.defineProperty(EventTarget, 'toString', {
        value: function() {
            return 'function EventTarget() { [native code] }';
        },
        enumerable: false
    })

    EventTarget.prototype.constructor = EventTarget;

    EventTarget.prototype.addEventListener = function(type, callback) {
        if (arguments < 2) {
            throw new Error("TypeError: Failed to execute 'addEventListener' on 'EventTarget': 2 arguments required, but only" + arguments.length + "present.");
        }
        if (typeof type === 'string' && typeof callback === 'function') {
            if (typeof this.listeners[type] == 'undefined') {
                this.listeners[type] = [];
            }

            this.listeners[type].push(callback);
        }
    };

    EventTarget.prototype.fireEvent = function(event) {
        if (typeof event == "string") {
            event = { type: event };
        }
        if (!event.target) {
            event.target = this;
        }

        if (!event.type) {
            event.type = event.target.toString();
        }

        if (this.listeners[event.type] instanceof Array) {
            var listeners = this.listeners[event.type];
            for (var i = 0, len = listeners.length; i < len; i++) {
                if (typeof listeners[i] === 'function') {
                    listeners[i].call(this, event);
                }
            }
        }
    };

    EventTarget.prototype.removeEventListener = function(type, listener) {
        if (this.eventListeners[type] instanceof Array) {
            var listeners = this.eventListeners[type];
            for (var i = 0, len = listeners.length; i < len; i++) {
                if (listeners[i] === listener) {
                    listeners.splice(i, 1);
                    break;
                }
            }
        }
    }

    hideOwnPropertiesImplementation(EventTarget.prototype);

    // =====================================================================================
    //      GDXMLHttpRequestEventTarget
    //  Implementation of GDXMLHttpRequestEventTarget prototype for the GDXMLHttpRequest and
    //  GDXMLHttpRequestUpload. Used as a place holder for the callbacks and event handlers
    //  of GDXMLHttpRequest and GDXMLHttpRequestUpload. Inherits the prototype functions of
    //  EventTarget interface implementation and own properties of EventTarget constructor
    // =====================================================================================
    var GDXMLHttpRequestEventTarget = function() {
        EventTarget.call(this);
        this.listeners = {};
        this.listeners.onabort = null;
        this.listeners.onerror = null;
        this.listeners.onload = null;
        this.listeners.onloadend = null;
        this.listeners.onloadstart = null;
        this.listeners.onprogress = null;
        this.listeners.ontimeout = null;
        this.listeners.onabort = null;
    };

    Object.defineProperty(GDXMLHttpRequestEventTarget, 'toString', {
        value: function() {
            return 'function GDXMLHttpRequestEventTarget() { [native code] }';
        },
        writable: false
    });

    GDXMLHttpRequestEventTarget.prototype = Object.create(EventTarget.prototype);
    GDXMLHttpRequestEventTarget.prototype.constructor = GDXMLHttpRequestEventTarget;

    Object.defineProperties(GDXMLHttpRequestEventTarget.prototype, {
        'onabort': {
            get: function() {
                return this.listeners.onabort;
            },
            set: function(callback) {
                this.listeners.onabort = typeof callback === 'function' ? callback : null;

                return callback;
            },
            enumerable: true,
            configurable: true
        },
        'onerror': {
            get: function() {
                return this.listeners.onerror;
            },
            set: function(callback) {
                this.listeners.onerror = typeof callback === 'function' ? callback : null;

                return callback;
            },
            enumerable: true,
            configurable: true
        },
        'onload': {
            get: function() {
                return this.listeners.onload;
            },
            set: function(callback) {
                this.listeners.onload = typeof callback === 'function' ? callback : null;

                return callback;
            },
            enumerable: true,
            configurable: true
        },
        'onloadend': {
            get: function() {
                return this.listeners.onloadend;
            },
            set: function(callback) {
                this.listeners.onloadend = typeof callback === 'function' ? callback : null;

                return callback;
            },
            enumerable: true,
            configurable: true
        },
        'onloadstart': {
            get: function() {
                return this.listeners.onloadstart;
            },
            set: function(callback) {
                this.listeners.onloadstart = typeof callback === 'function' ? callback : null;

                return callback;
            },
            enumerable: true,
            configurable: true
        },
        'onprogress': {
            get: function() {
                return this.listeners.onprogress;
            },
            set: function(callback) {
                this.listeners.onprogress = typeof callback === 'function' ? callback : null;

                return callback;
            },
            enumerable: true,
            configurable: true
        },
        'ontimeout': {
            get: function() {
                return this.listeners.ontimeout;
            },
            set: function(callback) {
                this.listeners.ontimeout = typeof callback === 'function' ? callback : null;

                return callback;
            },
            enumerable: true,
            configurable: true
        },
    });

    // =====================================================================================
    //      GDXMLHttpRequestUpload
    //  Implementation of GDXMLHttpRequestUpload prototype for the 'upload' property of
    //  GDXMLHttpRequest instance. Used as a place holder for the callbacks and event handlers
    //  of GDXMLHttpRequest and GDXMLHttpRequestUpload. Inherits the prototype functions of
    //  EventTarget interface implementation and own properties of EventTarget constructor
    // =====================================================================================

    var GDXMLHttpRequestUpload = function() {
        GDXMLHttpRequestEventTarget.call(this);
        this.listeners = {};
        this.listeners.onabort = null;
        this.listeners.onerror = null;
        this.listeners.onload = null;
        this.listeners.onloadend = null;
        this.listeners.onloadstart = null;
        this.listeners.onprogress = null;
        this.listeners.ontimeout = null;
        this.listeners.onabort = null;

        Object.defineProperty(this, 'toString', {
            value: function() {
                return '[object GDXMLHttpRequestUpload]';
            }
        });
    }

    GDXMLHttpRequestUpload.prototype = Object.create(GDXMLHttpRequestEventTarget.prototype);
    GDXMLHttpRequestUpload.prototype.constructor = GDXMLHttpRequestUpload;

    Object.defineProperty(GDXMLHttpRequestUpload, 'toString', {
        value: function() {
            return 'function GDXMLHttpRequestUpload() { [native code] }';
        },
        writable: false
    });
    // =====================================================================================
    //      GDXMLHttpRequest
    //  Inherits the own properties and prototype own properties of EventTarget,
    //  GDXMLHttpRequestEventTarget
    // =====================================================================================

    /**
     * @class XMLHttpRequest
     * @classdesc XMLHttpRequest is a JavaScript object that provides an easy way
     * to retrieve data from a URL without having to do a full page refresh.
     * A Web page can update just a part of the page without disrupting what the user is doing.
     * XMLHttpRequest is used heavily in AJAX programming.
     *
     * @deprecated since version 9.0, where XMLHttpRequest is secured within cordova-plugin-bbd-base.
     * It will be removed in future versions.
     *
     * @property {string} onreadystatechange A JavaScript function object that is called whenever the readyState attribute changes.
     * The callback is called from the user interface thread.
     *
     * @property {number} readyState The state of the request. Possible values are:
     * 0 - when open() method has not been called yet
     * 1 - when send() methos has not been called yet
     * 2 - when send() method has been called, and headers and status are available
     * 3 - downloading; responseText holds partial data
     * 4 - operation is complete
     *
     * @property {string} response The response entity body according to responseType,
     * as an ArrayBuffer, Blob, Document, JavaScript object (for "json"), or string.
     * This is null if the request is not complete or was not successful.
     *
     * @property {string} responseText The response to the request as text, or null if the request was unsuccessful or has not yet been sent.
     *
     * @property {string} responseType Can be set to change the response type. Possible values are:
     * "" (empty string) - String (this is the default)
     * "document" - Document
     * "json" - JavaScript object, parsed from a JSON string returned by the server
     * "text" - String
     * "blob" - JavaScript object, parsed from readable stream of binary data responded by the server
     *
     * @property {string} responseXML The response to the request as a DOM Document object, or null if the request was unsuccessful, has not yet been sent, or cannot be parsed as XML or HTML.
     * The response is parsed as if it were a text/xml stream.
     * When the responseType is set to "document" and the request has been made asynchronously, the response is parsed as a text/html stream.
     *
     * @property {number} status The status of the response to the request. This is the HTTP result code (for example, status is 200 for a successful request).
     *
     * @property {string} statusText The response string returned by the HTTP server. Unlike status, this includes the entire text of the response message ("200 OK", for example).
     *
     * @property {string} timeout The number of milliseconds a request can take before automatically being terminated. A value of 0 (which is the default) means there is no timeout.
     *
     * @property {string} ontimeout A JavaScript function object that is called whenever the request times out.
     *
     * @property {string} upload The upload process can be tracked by adding an event listener to upload.
     *
     * @property {string} withCredentials Indicates whether or not cross-site Access-Control requests should be made using credentials such as cookies or authorization headers.
     * The default is false.
     */
    var _realXMLHttpRequest = XMLHttpRequest,
        originOpenMethod = _realXMLHttpRequest.prototype.open;

    // prevent calling the open method of XMLHttpRequest for non local resources
    _realXMLHttpRequest.prototype.open = function() {
        if (arguments.length < 2) {
            throw new Error("TypeError: Failed to execute 'open' on 'XMLHttpRequest': 2 arguments required, but only " + arguments.length + " present.");
        }
        var parsedURL = parseURL(arguments[1]);

        if (parsedURL.protocol === 'file:') {
            originOpenMethod.apply(this, arguments);
        }

        return false;
    }

    Object.defineProperty(_realXMLHttpRequest.prototype.open, 'toString', {
        value: function() {
            return 'function open() { [native code] }';
        },
        writable: false
    });

    var events = [
        'onabort', 'onerror', 'onload', 'onloadstart', 'onloadend', 'onprogress', 'ontimeout', 'onreadystatechange'
    ];

    var GDXMLHttpRequest = function() {
        console.warn('cordova-plugin-bbd-xmlhttprequest is deprecated since version 9.0, where XMLHttpRequest is secured ' +
            'within cordova-plugin-bbd-base. It will be removed in future versions.');

        GDXMLHttpRequestEventTarget.call(this);

        this.listeners.onreadystatechange = null;

        var realXMLHttpRequest = new _realXMLHttpRequest(),
            options = {
                sendOptions: { "RequestHeaders": null, "PostParameters": null, "HttpBody": null },
                readyState: 0,
                response: '',
                responseText: '',
                responseType: '',
                responseURL: '',
                responseXML: '',
                status: 0,
                statusText: '',
                timeout: 0,
                withCredentials: false,
                isRealUsed: false
            };

        Object.defineProperties(this, {
            '_realXMLHttpRequest': {
                get: function() {
                    return realXMLHttpRequest;
                },
                enumerable: false
            },
            'options': {
                get: function() {
                    return options;
                },
                enumerable: false
            },
            'upload': {
                value: new GDXMLHttpRequestUpload(),
                enumerable: true
            },
            'toString': {
                value: function() {
                    return '[object GDXMLHttpRequest]';
                }
            }
        });
    };

    Object.defineProperties(GDXMLHttpRequest, {
        'DONE': {
            value: 4,
            enumerable: true
        },
        'HEADERS_RECEIVED': {
            value: 2,
            enumerable: true
        },
        'LOADING': {
            value: 3,
            enumerable: true
        },
        'OPENED': {
            value: 1,
            enumerable: true
        },
        'UNSENT': {
            value: 0,
            enumerable: true
        },
        'toString': {
            value: function() {
                return 'function GDXMLHttpRequest() { [native code] }';
            },
            enumerable: false
        }
    });

    GDXMLHttpRequest.prototype = Object.create(GDXMLHttpRequestEventTarget.prototype);

    GDXMLHttpRequest.prototype.constructor = GDXMLHttpRequest;

    Object.defineProperties(GDXMLHttpRequest.prototype, {
        'DONE': {
            value: 4,
            enumerable: true
        },
        'HEADERS_RECEIVED': {
            value: 2,
            enumerable: true
        },
        'LOADING': {
            value: 3,
            enumerable: true
        },
        'OPENED': {
            value: 1,
            enumerable: true
        },
        'UNSENT': {
            value: 0,
            enumerable: true
        },
        'onreadystatechange': {
            get: function() {
                if (this.options.isRealUsed) {
                    onRequestUpdated(this);
                }
                return this.listeners.onreadystatechange;
            },
            set: function(callback) {
                this.listeners.onreadystatechange = callback;
            },
            enumerable: true
        },
        'readyState': {
            get: function() {
                if (this.options.isRealUsed) {
                    onRequestUpdated(this);
                }
                return this.options.readyState;
            },
            enumerable: true
        },
        'response': {
            get: function() {
                if (this.options.isRealUsed) {
                    onRequestUpdated(this);
                }
                return this.options.response;
            },
            enumerable: true
        },
        'responseText': {
            get: function() {
                if (this.options.isRealUsed) {
                    onRequestUpdated(this);
                }
                return this.options.responseText;
            },
            enumerable: true
        },
        'responseType': {
            get: function() {
                if (this.options.isRealUsed) {
                    return this._realXMLHttpRequest.responseType;
                }
                return this.options.responseType;
            },
            set: function(type) {
                if (this.options.isRealUsed && this._realXMLHttpRequest.readyState < this.HEADERS_RECEIVED) {
                    this._realXMLHttpRequest.responseType = type;
                }
                this.options.responseType = type;
            },
            enumerable: true
        },
        'responseURL': {
            get: function() {
                if (this.options.isRealUsed) {
                    onRequestUpdated(this);
                }
                return this.options.responseURL;
            },
            enumerable: true
        },
        'responseXML': {
            get: function() {
                if (this.options.isRealUsed) {
                    onRequestUpdated(this);
                }
                return this.options.responseXML;
            },
            enumerable: true
        },
        'status': {
            get: function() {
                if (this.options.isRealUsed) {
                    onRequestUpdated(this);
                }
                return this.options.status;
            },
            enumerable: true
        },
        'statusText': {
            get: function() {
                if (this.options.isRealUsed) {
                    onRequestUpdated(this);
                }
                return this.options.statusText;
            },
            enumerable: true
        },
        'timeout': {
            get: function() {
                if (this.options.isRealUsed) {
                    onRequestUpdated(this);
                }
                return this.options.timeout;
            },
            set: function(delay) {
                if (this.options.isRealUsed && this._realXMLHttpRequest.readyState < this.HEADERS_RECEIVED) {
                    this._realXMLHttpRequest.timeout = delay;
                }
                this.options.timeout = delay;
            },
            enumerable: true
        },
        'withCredentials': {
            get: function() {
                return this.options.withCredentials;
            },
            set: function(value) {
                if (this.options.isRealUsed && this._realXMLHttpRequest.readyState < this.HEADERS_RECEIVED) {
                    this._realXMLHttpRequest.withCredentials = value;
                }
                this.options.withCredentials = value;
            },
            enumerable: true
        }
    });

    // ***** BEGIN: MODULE METHOD DEFINITIONS - XMLHttpRequest *****

    /**
     * @function XMLHttpRequest#open
     *
     * @description Initializes a request.
     * This method is to be used from JavaScript code; to initialize a request from native code.
     *
     * @param {string} method The HTTP method to use, such as "GET", "POST", "PUT", "DELETE".
     *
     * @param {string} url The URL to send the request to.
     *
     * @param {boolean} async An optional boolean parameter, defaulting to true, indicating whether or not to perform the operation asynchronously.
     * If this value is false, the send() method does not return until the response is received.
     * If true, notification of a completed transaction is provided using event listeners.
     * This must be true if the multipart attribute is true, or an exception will be thrown.
     *
     * @param {string} user The optional user name to use for authentication purposes; by default, this is an empty string.
     *
     * @param {string} password The optional password to use for authentication purposes; by default, this is an empty string.
     */

    GDXMLHttpRequest.prototype.open = function(method, url, optAsync, optUser, optPassword) {

        if (arguments.length < 2) {
            throw new Error("TypeError: Failed to execute 'open' on 'XMLHttpRequest': 2 arguments required, but only " + arguments.length + " present.");
        }

        var parsedURL = parseURL(url);

        this.options.method = method;
        this.options.url = parsedURL.href;

        if (parsedURL.protocol === 'file:') {
            //call native OPEN function
            this._realXMLHttpRequest.open.apply(this._realXMLHttpRequest, arguments);

            this.options.isRealUsed = true;

            onRequestUpdated(this);
        }

        this.options.readyState = this.OPENED;

        var isAsyncPassed = arguments.length >= 3,
            isAsync = true;

        if (isAsyncPassed) {
            isAsync = new Boolean(optAsync);
        }
        this.options.async = isAsync.valueOf();

        this.options.user = optUser ? optUser.toString() : '';
        this.options.password = optPassword ? optPassword.toString() : '';
    };

    /**
     * @function XMLHttpRequest#abort
     *
     * @description Aborts the request if it has already been sent.
     */

    GDXMLHttpRequest.prototype.abort = function() {

        var async = (this.options.async === false) ? "false" : "true";

        if (this.options.isRealUsed) {

            //call native ABORT function
            this._realXMLHttpRequest.abort.apply(this._realXMLHttpRequest);

            this.options.isAborted = true;

            onRequestUpdated(this);
        }

        if (this.readyState == this.UNSENT ||
            this.readyState == this.OPENED ||
            this.readyState == this.DONE) {
            return;
        }

        this.options.readyState = this.DONE;
        this.options.status = 0;

        cordovaExec(function() {}, function() {}, "GDXMLHttpRequest", "abort", [async, this.options.requestTag]);
        if (typeof this.onabort == 'function') {
            this.onabort();
        }
        this.fireEvent('abort');
    };

    /**
     * @function XMLHttpRequest#send
     *
     * @description Sends the request. If the request is asynchronous (which is the default), this method returns as soon as the request is sent.
     * If the request is synchronous, this method doesn't return until the response has arrived.
     * It takes optional parameter data.
     * Optional parameter data can contain following types of data:
     * ArrayBuffer
     * Document
     * DOMString
     * FormData - object with key/value pairs that can be passed to send method as parameter. User can append file from GDFileSystem by passing valid fullPath to this file as key/value
     */

    GDXMLHttpRequest.prototype.send = function(optData) {

        if (this.options.isRealUsed) {

            // update properties on native object
            updateNativeXHR(this);

            //call native SEND function
            this._realXMLHttpRequest.send.apply(this._realXMLHttpRequest, arguments);
            this.options.isRealUsed = true;

            onRequestUpdated(this);

            return;
        }

        if (this.readyState != this.OPENED) {
            throw new Error("DOMException: Failed to execute 'send' on 'XMLHttpRequest': The object's state must be OPENED.");
        }

        if (optData) {
            if (typeof this.options.sendOptions.HttpBody === 'undefined' ||
                this.options.sendOptions.HttpBody === null) {

                this.options.HttpBody = {};
            }

            if (typeof optData === 'string') {
                this.options.sendOptions.HttpBody = optData;
            } else if (optData instanceof ArrayBuffer) {
                this.options.sendOptions.HttpBody = fromArrayBuffer(optData);
            } else if (optData instanceof GDFormData) {
                var boundary = "---------------------------" + uniqueStr(),
                    body = '--' + boundary + '\r\n';
                // set request header Content-Type: multipart/form-data
                this.setRequestHeader("Content-Type", "multipart/form-data");
                this.setRequestHeader("Content-Type", "boundary=" + boundary);

                // prepare HTTP Body
                for (var key in optData) {
                    if (optData.hasOwnProperty(key)) {
                        // if user appended file from GDFileSystem by passing valid fullPath to this file as parameter
                        if (optData[key].indexOf("file:///data/") > -1) {
                            body = body + 'Content-Disposition: form-data; name="' + key + '"; ' + 'filename="' + optData[key] + '"' + '\n' + 'Content-Type: text/plain' + '\n\n\n--' + boundary + '\r\n';
                        } else {
                            body = body + 'Content-Disposition: form-data; name="' + key + '"' + '\n\n' + optData[key] + '\n--' + boundary + '\r\n';
                        }
                    }
                }
                this.options.sendOptions.HttpBody = body;
            }
        }

        this.options.requestTag = guid();

        var parms = [
            this.options.method,
            this.options.url,
            this.options.async,
            this.options.user,
            this.options.password,
            this.options.timeout,
            this.withCredentials,
            this.responseType,
            this.options.requestTag,
            this.options.sendOptions
        ];

        var that = this;

        var uploadCallsCount = 0;

        function successCallback(data) {
            var obj = JSON.parse(data);
            var escapedJson = {};

            for (var i in obj) {
                escapedJson[i] = unescape(obj[i]);
            }

            // handling upload events
            if (escapedJson.lengthComputable && escapedJson.total && escapedJson.loaded) {
                uploadCallsCount++;
                if (uploadCallsCount === 1) {
                    if (typeof that.upload.onloadstart === "function") {
                        that.upload.onloadstart.call(that, new ProgressEvent("loadstart", {
                            target: that.upload,
                            lengthComputable: escapedJson.lengthComputable,
                            loaded: escapedJson.loaded,
                            total: escapedJson.total
                        }));
                    }
                    that.upload.fireEvent(new ProgressEvent("loadstart", {
                        lengthComputable: escapedJson.lengthComputable,
                        loaded: escapedJson.loaded,
                        total: escapedJson.total
                    }));
                }

                // skip first call. "loadstart" callback triggered
                if (uploadCallsCount !== 1) {
                    if (typeof that.upload.onprogress === "function") {
                        that.upload.onprogress.call(that, new ProgressEvent("progress", {
                            target: that.upload,
                            lengthComputable: escapedJson.lengthComputable,
                            loaded: escapedJson.loaded,
                            total: escapedJson.total
                        }));
                    }

                    that.upload.fireEvent(new ProgressEvent("progress", {
                        lengthComputable: escapedJson.lengthComputable,
                        loaded: escapedJson.loaded,
                        total: escapedJson.total
                    }));
                }

                if (escapedJson.loaded === escapedJson.total) {
                    if (typeof that.upload.onload === "function") {
                        that.upload.onload.call(that, new ProgressEvent("load", {
                            target: that.upload,
                            lengthComputable: escapedJson.lengthComputable,
                            loaded: escapedJson.loaded,
                            total: escapedJson.total
                        }));
                    }

                    that.upload.fireEvent(new ProgressEvent("load", {
                        lengthComputable: escapedJson.lengthComputable,
                        loaded: escapedJson.loaded,
                        total: escapedJson.total
                    }));

                    if (typeof that.upload.onloadend === "function") {
                        that.upload.onloadend.call(that, new ProgressEvent("loadend", {
                            target: that.upload,
                            lengthComputable: escapedJson.lengthComputable,
                            loaded: escapedJson.loaded,
                            total: escapedJson.total
                        }));
                    }

                    that.upload.fireEvent(new ProgressEvent("loadend", {
                        lengthComputable: escapedJson.lengthComputable,
                        loaded: escapedJson.loaded,
                        total: escapedJson.total
                    }));
                }

                // this code should be reenabled after implementation of GD-29876
                // // If ontimeout callback
                // if (typeof that.upload.ontimeout === "function") {
                //     that.upload.ontimeout.call(that, new ProgressEvent("timeout", { target: escapedJson }));
                // }
                // // If onerror callback
                // if (typeof that.upload.onerror === "function") {
                //     that.upload.onerror.call(that, new ProgressEvent("error", { target: escapedJson }));
                // }
                // // If onabort callback
                // if (typeof that.upload.onabort === "function") {
                //     that.upload.onabort.call(that, new ProgressEvent("abort", { target: escapedJson }));
                // }
                return;
            }

            // handling response... if request was aborted or failed on timeout
            if (escapedJson.isAborted == "true" || escapedJson.isTimeout == "true") {
                that.options.statusText = "";
                that.options.status = 0;
                that.options.response = "";
                that.responseType = "";
                that.options.responseXML = null;
                that.options.responseText = "";
                that.options.readyState = that.DONE;
            } else {
                that.options.headers = escapedJson.headers;

                that.options.readyState = that.HEADERS_RECEIVED;
                if (typeof that.onreadystatechange === "function") {
                    that.onreadystatechange.call(that);
                }
                that.fireEvent('readystatechange');

                that.options.readyState = that.LOADING;
                if (typeof that.onreadystatechange === "function") {
                    that.onreadystatechange.call(that);
                }
                that.fireEvent('readystatechange');

                if (typeof that.onprogress === "function") {
                    that.onprogress.call(that);
                }
                that.fireEvent('progress');

                that.options.responseText = escapedJson.responseText.trim();
                that.options.responseURL = escapedJson.responseURL;
                that.options.status = parseInt(escapedJson.status, 10);
                that.options.statusText = escapedJson.statusText;
                that.options.readyState = that.DONE;

                // handling response property
                var contentType = that.getResponseHeader("Content-Type");

                if (that.responseType === "" || that.responseType === "text") {
                    that.options.response = escapedJson.responseText.toString();
                } else if (that.responseType === "json") {
                    that.options.response = JSON.parse(escapedJson.responseText);
                } else if (that.responseType === "document") {
                    if (window.DOMParser) {
                        var parser = new DOMParser();
                        var xmlDoc = parser.parseFromString(escapedJson.responseText, "text/xml");
                        var htmlDoc = parser.parseFromString(escapedJson.responseText, "text/html");

                        if (xmlDoc && xmlDoc instanceof Document) {
                            that.options.response = xmlDoc;
                            that.options.responseXML = escapedJson.responseText;
                        } else if (htmlDoc && htmlDoc instanceof HTMLDocument) {
                            that.options.response = htmlDoc;
                            that.options.responseXML = escapedJson.responseText;
                        } else {
                            that.options.responseXML = null;
                        }
                    }
                } else if (that.responseType === "blob") {
                    var contentType = that.getResponseHeader("Content-Type");

                    that.options.response = b64toBlob(escapedJson.responseBase64, contentType);
                }

                // handling responseXML property
                if (contentType != null && (contentType == "text/xml" || (contentType.indexOf('application/xml') > -1) || contentType == "text/html")) {
                    if (window.DOMParser) {
                        var parser = new DOMParser(),
                            xmlDoc = parser.parseFromString(escapedJson.responseText, "text/xml"),
                            htmlDoc = parser.parseFromString(escapedJson.responseText, "text/html");

                        if (xmlDoc || htmlDoc) {
                            that.options.responseXML = that.options.responseXML = (xmlDoc) ? xmlDoc : htmlDoc;
                        } else {
                            that.options.responseXML = null;
                        }
                    }
                }
            }

            if (escapedJson.isTimeout === "true") {
                if (typeof that.ontimeout === "function") {
                    that.ontimeout.call(that);
                }
                that.fireEvent('timeout');
            } else {
                if (typeof that.onreadystatechange === "function") {
                    that.onreadystatechange.call(that);
                }
                if (typeof that.onload === "function") {
                    that.onload.call(that);
                }
                that.fireEvent('load');
            }

            if (typeof that.onloadend == 'function') {
                that.onloadend();
            }

            that.fireEvent('loadend');
        };

        function errorCallback(error) {
            var obj = JSON.parse(error);
            var escapedJson = {};

            for (var i in obj) {
                escapedJson[i] = unescape(obj[i]);
            }

            that.options.readyState = that.DONE;
            that.options.status = 0;

            if (escapedJson.isTimeout === "true") {
                if (typeof that.ontimeout === "function") {
                    that.ontimeout.call(that);
                }
                that.fireEvent('timeout');
            }

            if (typeof that.onerror == 'function') {
                that.onerror();
            }

            that.fireEvent('error');
        }

        this.options.readyState = this.LOADING;

        cordovaExec(successCallback, errorCallback, "GDXMLHttpRequest", "send", parms);

        if (typeof this.onloadstart == 'function') {
            this.onloadstart();
        }

        this.fireEvent('loadstart');
    };

    /**
     * @function XMLHttpRequest#setRequestHeader
     *
     * @description Sets the value of an HTTP request header. You must call setRequestHeader() after open(), but before send().
     * If this method is called several times with the same header, the values are merged into one single request header.
     */

    GDXMLHttpRequest.prototype.setRequestHeader = function(header, value) {

        if (this.options.isRealUsed) {
            //call native setRequestHeader function
            this._realXMLHttpRequest.setRequestHeader.apply(this._realXMLHttpRequest, arguments);

            return;
        }

        if (arguments.length < 2) {
            throw new Error("TypeError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': 2 arguments required, but only " + arguments.length + " present.");
        }

        if (typeof this.options.sendOptions.RequestHeaders === 'undefined' ||
            this.options.sendOptions.RequestHeaders === null) {

            this.options.sendOptions.RequestHeaders = {};
        }

        if (this.options.sendOptions.RequestHeaders[header]) {
            this.options.sendOptions.RequestHeaders[header] += '; ' + value;
        } else {
            this.options.sendOptions.RequestHeaders[header] = value;
        }
    };

    /**
     * @function XMLHttpRequest#getResponseHeader
     *
     * @description Returns the string containing the text of the specified header,
     * or null if either the response has not yet been received or the header doesn't exist in the response.
     */

    GDXMLHttpRequest.prototype.getResponseHeader = function(header) {

        if (this.options.isRealUsed) {
            //call native getResponseHeader function
            this._realXMLHttpRequest.getResponseHeader.apply(this._realXMLHttpRequest, arguments);

            return;
        }

        if (arguments.length == 0) {
            throw new Error("TypeError: Failed to execute 'getResponseHeader' on 'XMLHttpRequest': 1 argument required, but only 0 present.");
        }

        if (this.readyState == this.DONE) {
            try {
                var headersArr = this.options.headers.split('\n'),
                    headersObj = {};

                for (var i = 0; i < headersArr.length; i++) {
                    var key = headersArr[i].slice(0, headersArr[i].indexOf(':')),
                        value = headersArr[i].slice(headersArr[i].indexOf(':') + 1, headersArr[i].length);

                    headersObj[key] = value;
                }

                var headers = Object.keys(headersObj);

                return headersObj[getMatchHeader(headers, header)];

            } catch (e) {
                return null;
            }
        } else {
            return null;
        }
    };

    /**
     * @function XMLHttpRequest#getAllResponseHeaders
     *
     * @description Returns all the response headers as a string, or null if no response has been received.
     * Note: For multipart requests, this returns the headers from the current part of the request, not from the original channel.
     */

    GDXMLHttpRequest.prototype.getAllResponseHeaders = function() {

        if (this.options.isRealUsed) {

            //call native getResponseHeader function
            this._realXMLHttpRequest.getAllResponseHeaders.apply(this._realXMLHttpRequest, arguments);

            return;
        }

        return (this.readyState == this.DONE) ? this.options.headers : null;
    };

    /**
     * @function XMLHttpRequest#overrideMimeType
     *
     * @description Overrides the MIME type returned by the server.
     * This may be used, for example, to force a stream to be treated and parsed as text/xml, even if the server does not report it as such. This method must be called before send().
     */

    GDXMLHttpRequest.prototype.overrideMimeType = function(mimeType) {

        if (this.options.isRealUsed) {

            //call native getResponseHeader function
            this._realXMLHttpRequest.overrideMimeType.apply(this._realXMLHttpRequest, arguments);

            return;
        }

        if (arguments.length == 0) {
            throw new Error("TypeError: Failed to execute 'overrideMimeType' on 'XMLHttpRequest': 1 argument required, but only 0 present.");
        }

        if (this.readyState == this.OPENED) {
            this.setRequestHeader("Content-Type", mimeType);
        }
    };

    hideOwnPropertiesImplementation(GDXMLHttpRequest.prototype);

    // helper functions
    function getMatchHeader(headers, header) {
        for (var i = 0; i < headers.length; i++) {
            if (isMatch(headers[i], header))
                return headers[i];
        }
    }

    function isMatch(currentValue, value) {
        return currentValue.match(new RegExp(value, 'i'));
    }

    function fromArrayBuffer(arrayBuffer) {
        var array = new Uint8Array(arrayBuffer);

        return uint8ToBase64(array);
    };

    var b64_6bit = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
        b64_12bit;

    function uint8ToBase64(rawData) {
        var numBytes = rawData.byteLength,
            output = "",
            segment,
            table = b64_12bitTable();

        for (var i = 0; i < numBytes - 2; i += 3) {
            segment = (rawData[i] << 16) + (rawData[i + 1] << 8) + rawData[i + 2];
            output += table[segment >> 12];
            output += table[segment & 0xfff];
        }
        if (numBytes - i == 2) {
            segment = (rawData[i] << 16) + (rawData[i + 1] << 8);
            output += table[segment >> 12];
            output += b64_6bit[(segment & 0xfff) >> 6];
            output += '=';
        } else if (numBytes - i == 1) {
            segment = (rawData[i] << 16);
            output += table[segment >> 12];
            output += '==';
        }

        return output;
    }

    function b64_12bitTable() {
        b64_12bit = [];

        for (var i = 0; i < 64; i++) {
            for (var j = 0; j < 64; j++) {
                b64_12bit[i * 64 + j] = b64_6bit[i] + b64_6bit[j];
            }
        }
        b64_12bitTable = function() {
            return b64_12bit;
        };

        return b64_12bit;
    };

    function guid() {
        function s4() {
            return Math.floor((1 + Math.random()) * 0x10000)
                .toString(16)
                .substring(1);
        }
        return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
            s4() + '-' + s4() + s4() + s4();
    };

    function uniqueStr() {
        function s4() {
            return Math.floor((1 + Math.random()) * 0x10000)
                .toString(16)
                .substring(1);
        }
        return s4() + s4() + s4() + s4() + s4() + s4() + s4() + s4();
    };

    function fixedEncodeURIComponent(str) {
        return encodeURIComponent(str).replace(/[!'()*]/g, function(c) {
            return '%' + c.charCodeAt(0).toString(16);
        });
    };

    function b64toBlob(b64Data, contentType, sliceSize) {
        contentType = contentType || '';
        sliceSize = sliceSize || 512;

        var byteCharacters = atob(b64Data),
            byteArrays = [];

        for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
            var slice = byteCharacters.slice(offset, offset + sliceSize),
                byteNumbers = new Array(slice.length);

            for (var i = 0; i < slice.length; i++) {
                byteNumbers[i] = slice.charCodeAt(i);
            }

            var byteArray = new Uint8Array(byteNumbers);

            byteArrays.push(byteArray);
        }

        var blob = new Blob(byteArrays, { type: contentType });

        return blob;
    };

    function onRequestUpdated(context) {
        context.options.readyState = context._realXMLHttpRequest.readyState;
        context.options.response = context._realXMLHttpRequest.response;
        context.options.responseURL = context._realXMLHttpRequest.responseURL;
        if (context._realXMLHttpRequest.responseType == "" || context._realXMLHttpRequest.responseType == 'text') {
            context.options.responseText = context._realXMLHttpRequest.responseText;
        }
        if (context._realXMLHttpRequest.responseType == "" || context._realXMLHttpRequest.responseType == 'document') {
            context.options.responseXML = context._realXMLHttpRequest.responseXML;
        }
        context.options.status = context._realXMLHttpRequest.status;
        context.options.statusText = context._realXMLHttpRequest.statusText;
    }

    function updateNativeXHR(context) {
        events.forEach(function(eventHandler) {
            if (eventHandler === 'onreadystatechange') { return; }
            context._realXMLHttpRequest[eventHandler] = context[eventHandler];
            context._realXMLHttpRequest.upload[eventHandler] = context.upload[eventHandler];
        });
        context._realXMLHttpRequest.onreadystatechange = context.onreadystatechange;
    }

    function parseURL(url) {
        try {
            return new URL(url);
        } catch (err) {
            return {
                protocol: "file:",
                href: url
            }
        }
    }

    // hide functions implementation in web inspector
    function hideOwnPropertiesImplementation(prototypeObject) {
        var propertiesToSkip = [
                'readyState', 'response', 'responseText', 'responseType', 'responseURL', 'responseXML',
                'status', 'statusText', 'timeout', 'withCredentials'
            ]
            .concat(events);
        for (ownProperty in prototypeObject) {
            if (ownProperty === 'constructor' || propertiesToSkip.indexOf(ownProperty) > -1) {
                continue;
            }

            if (prototypeObject.hasOwnProperty(ownProperty) &&
                typeof prototypeObject[ownProperty] == 'function') {

                // Checking, if function property 'name' is configurable
                // (for old browser, which has pre-ES2015 implementation(Android 5.0) function name property isn't configurable)
                var objProtoProperty = prototypeObject[ownProperty],
                    isFuncNamePropConfigurable = Object.getOwnPropertyDescriptor(objProtoProperty, 'name').configurable;

                if (isFuncNamePropConfigurable) {
                    Object.defineProperty(prototypeObject[ownProperty],
                        'name', {
                            value: ownProperty,
                            configurable: false
                        }
                    );
                }

                Object.defineProperty(prototypeObject[ownProperty],
                    'toString', {
                        value: function() {
                            var funcName = this.name || ownProperty;
                            return 'function ' + funcName + '() { [native code] }';
                        },
                        writable: false,
                        configurable: false
                    });

            }
        }
    }

    XMLHttpRequest = GDXMLHttpRequest;

    module.exports = XMLHttpRequest;
}());
// End the Module Definition.
//************************************************************************************************