PMA.UI Documentation by Pathomation

components/js/context.js

import { checkBrowserCompatibility } from '../../view/helpers';
import { Events, ApiMethods, callApiMethod, parseJson, _sessionList } from './components';
import { PromptLogin } from './promptlogin';
import { Resources } from '../../resources/resources';

let PmaStartUrl = "http://127.0.0.1:54001/";

export
    /**
     * The Context class glues component instances together. It provides API method implementations and simplifies the interaction with PMA.core by automatically managing authentication and sessionID handling, via the authentication provider classes.
     * @memberof PMA.UI.Components
     * @alias Context
     * @class
     * @param  {Object} options
     * @param  {string} options.caller
     * @tutorial 03-gallery
     * @tutorial 04-tree
     * @tutorial 05-annotations
     */
    class Context {
    constructor(options) {
        if (!checkBrowserCompatibility()) {
            return;
        }

        if (!options || typeof options.caller !== "string") {
            throw "Caller parameter not supplied";
        }

        this.options = options;

        // create event listeners object and add one array for each event type
        this.listeners = {};
        for (var ev in Events) {
            // if (Events.hasOwnProperty(ev)) {
            if (Object.prototype.hasOwnProperty.call(Events, ev)) {
                this.listeners[Events[ev]] = [];
            }
        }

        // list of components that can authenticate against a PMA.core server
        this.authenticationProviders = [];
    }
    /**
     * Gets the caller value
     * @return {string}
     */
    getCaller() {
        return this.options.caller;
    }
    /**
     * Adds an authentication provider to the list of available authentication methods
     * @param  {AutoLogin|PromptLogin|SessionLogin} provider
     */
    registerAuthenticationProvider(provider) {
        if (typeof provider.authenticate !== "function") {
            console.error("Invalid authentication provider");
        }
        else {
            this.authenticationProviders.push(provider);
        }
    }
    /**
     * Removes an authentication provider from the list of available authentication methods
     * @param  {AutoLogin|PromptLogin|SessionLogin} provider - The provider to remove
     * @param {bool} clearCache - Clears the session ids cache
     */
    removeAuthenticationProvider(provider, clearCache) {
        if (typeof provider.authenticate !== "function") {
            console.error("Invalid authentication provider");
        }
        else {
            for (var i = 0; i < this.authenticationProviders.length; i++) {
                if (this.authenticationProviders[i] === provider) {
                    this.authenticationProviders.splice(i, 1);

                    // Clear all cache
                    if (clearCache !== false) {
                        _sessionList.clear();
                    }
                    break;
                }
            }
        }
    }
    /**
     * Gets the list of available authentication methods
     * @returns [{PMA.UI.Authentication.AutoLogin|PMA.UI.Authentication.PromptLogin|PMA.UI.Authentication.SessionLogin}] providers
     */
    getAuthenticationProviders() {
        return this.authenticationProviders;
    }

    /**
    * PMA.core authentication response
    * @typedef {Object} Context~authenticationResponse
    * @property {string} SessionId - The session ID
    * @property {string} Username - The username
    * @property {string} Email - The user's email
    * @property {string} FirstName - The user's first name
    * @property {string} LastName - The user's last name
    */
    /**
     * A function called after successfully pinging a list of servers
     * @callback Context~pingServersDoneCallback
     * @param {String[]} servers - A sorted list of server url's from fastest to slowest
     * @param {Object[]} detailInfo - An array of detailed information from pinging the servers (not sorted)
     * @param {string} detailInfo.serverUrl - The server url
     * @param {bool} detailInfo.success - Whether the server responded to any pinging
     * @param {Number[]} detailInfo.times - An array of all the times the server took to respond (in miliseconds)
     * @param {Number} detailInfo.avgTime - The average time the server took to respond (in miliseconds)
     * @param {Number} detailInfo.attempts - The number of attempted pings to the server
     */
    /**
     * Pings a list of servers to find the fastests
     * @param {string[]} servers - An array of server url to ping
     * @param {Context~pingServersDoneCallback} done - The done callback to run
     * @param {Number} [maxAttempts=5] - The number of attempts for each server
     * */
    pingServers(servers, done, maxAttempts) {
        if (maxAttempts <= 1 || !maxAttempts || maxAttempts === undefined) {
            maxAttempts = 6;
        }

        var instances = servers.map(function (s) { return { serverUrl: s, success: false, times: [], avgTime: null, attempts: 0 }; });

        var cb = function (startTime, instanceIndex, success) {
            instances[instanceIndex].attempts++;
            if (success && instances[instanceIndex].attempts > 1) {
                // We ignore first attempt for warm up
                var time = performance.now() - startTime;
                instances[instanceIndex].times.push(time);
                instances[instanceIndex].avgTime = instances[instanceIndex].avgTime != null ?
                    ((instances[instanceIndex].avgTime * instances[instanceIndex].times.length) + time) / (instances[instanceIndex].times.length + 1) :
                    time;
                instances[instanceIndex].success = true;
            }

            if (instances[instanceIndex].attempts >= maxAttempts) {
                instanceIndex++;
            }

            if (instanceIndex >= instances.length) {
                // done testing
                if (typeof done === "function") {
                    done(instances.sort(function (a, b) {
                        if (a.avgTime != null && b.avgTime != null) {
                            return a.avgTime - b.avgTime;
                        }
                        else if (a.avgTime != null) {
                            return -10000000;
                        }
                        else {
                            return 10000000;
                        }
                    }).map(function (s) { return s.serverUrl; }), instances);
                }
            }
            else {
                // Run next test
                var nowTime = performance.now();
                callApiMethod({
                    serverUrl: instances[instanceIndex].serverUrl,
                    method: ApiMethods.GetVersionInfo,
                    success: cb.bind(this, nowTime, instanceIndex, true),
                    failure: cb.bind(this, nowTime, instanceIndex, false)
                });
            }
        };

        //Start first test
        var nowTime = performance.now();
        callApiMethod({
            serverUrl: instances[0].serverUrl,
            method: ApiMethods.GetVersionInfo,
            success: cb.bind(this, nowTime, 0, true),
            failure: cb.bind(this, nowTime, 0, false)
        });
    }

    /**
     * Connection information
     * @typedef {Object} Context~connection
     * @property {Number} roundTrip - The ping time in milliseconds
     * @property {Number} downloadSpeed - The download speed in bytes/second
     */
    /**
     * Calculates the ping time and download speed between the client and a PMA.core instance
     * @param {string} serverUrl - The server url to connect to
     * @returns {Context~connection}
     **/
    async evaluateConnection(serverUrl) {
        if (!serverUrl.endsWith('/')) {
            serverUrl += '/';
        }

        const maxAttempts = 10;
        const dataSize = 50 * 1024 * 1024;
        let pingTime = 0;

        let i = 0;
        do {
            let start = performance.now();
            try {
                await fetch(`${serverUrl}api/json/GetVersionInfo`);
            }
            catch {
                console.error("connection failed");
                return {
                    roundTrip: null,
                    downloadSpeed: null
                };
            }

            let end = performance.now();
            pingTime += end - start;
            i++;
        } while (i < maxAttempts)

        pingTime /= maxAttempts;

        let start = performance.now();
        try {
            const resp = await fetch(`${serverUrl}speedtest`, {
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
                "body": `size=${dataSize}`,
                "method": "POST",
                // "mode": 'no-cors'
            });

            const reader = resp.body.getReader();
            var finalDone = false;

            while (!finalDone) {
                const { done, value } = await reader.read();
                finalDone = done;
            }
        }
        catch {
            console.error("download failed");
            return {
                roundTrip: pingTime,
                downloadSpeed: null
            };
        }
        let end = performance.now();

        const downloadTime = end - start;
        const transferSpeed = dataSize / (downloadTime / 1000);

        return {
            roundTrip: pingTime,
            downloadSpeed: transferSpeed
        };
    }

    /**
     * Gets the user information associated with a server
     * @param {string} serverUrl - The URL of the PMA.core for which to fetch user information
     * @returns {Context~authenticationResponse} If no authentication has taken place for the particular server, null is returned, otherwise an object.
     */
    getUserInfo(serverUrl) {
        for (var url in _sessionList.get()) {
            // if (_sessionList.get().hasOwnProperty(url) && url === serverUrl && _sessionList.get()[url]) {
            if (Object.prototype.hasOwnProperty.call(_sessionList.get(), url) && url === serverUrl && _sessionList.get()[url]) {
                return _sessionList.get()[url];
            }
        }

        return null;
    }
    /**
     * Called when a session ID was successfully obtained
     * @callback Context~getSessionCallback
     * @param {string} sessionID
     */
    /**
     * Finds a session ID for the requested server, either by scanning the already cached session ids or by invoking one by one the available authentication providers, until a valid session ID is found
     * @param  {string} serverUrl - The URL of the PMA.core for which to fetch a session ID
     * @param  {Context~getSessionCallback} success
     * @param  {function} [failure]
     */
    getSession(serverUrl, success, failure) {
        // scan cached session IDs
        for (var url in _sessionList.get()) {
            // if (_sessionList.get().hasOwnProperty(url) && url === serverUrl && _sessionList.get()[url]) {
            if (Object.prototype.hasOwnProperty.call(_sessionList.get(), url) && url === serverUrl && _sessionList.get()[url]) {
                success(_sessionList.get()[url].SessionId);
                return;
            }
        }

        // no session ID found in cached list
        // fire all providers until one succeeds or all failed
        authenticateWithProvider.call(this, serverUrl, success, failure);
    }

    getImageInfo(serverUrl, pathOrUid, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.GetImageInfo,
            data: {
                pathOrUid: pathOrUid
            },
            httpMethod: "GET",
            success: success,
            failure: failure
        });
    }

    getFiles(serverUrl, path, success, failure) {
        console.warn("Context.getFiles is deprecated please use Context.getSlides instead");
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.GetFiles,
            data: {
                path: path
            },
            httpMethod: "GET",
            success: success,
            failure: failure
        });
    }
    /**
    * Gets all slides in a specified path
    * @param {Object} options - Parameters to pass to the GetSlides request
    * @param {string} options.serverUrl - The server url to get slides from
    * @param {string} options.path - The path to get slides from
    * @param {PMA.UI.Components.GetSlidesScope} options.scope - The search scope to use
    * @param {function} [options.success] - Called upon success
    * @param {function} [options.failure] - Called upon failure
    **/
    getSlides(options) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: options.serverUrl,
            method: ApiMethods.GetFiles,
            data: {
                path: options.path,
                scope: options.scope ? options.scope : 0
            },
            httpMethod: "GET",
            success: options.success,
            failure: options.failure
        });
    }

    getDirectories(serverUrl, path, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.GetDirectories,
            data: {
                path: path
            },
            httpMethod: "GET",
            success: success,
            failure: failure
        });
    }

    createDirectory(serverUrl, path, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.CreateDirectory,
            data: {
                path: path
            },
            httpMethod: "POST",
            contentType: "application/json",
            success: success,
            failure: failure
        });
    }

    deleteDirectory(serverUrl, path, deleteContents, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.DeleteDirectory,
            data: {
                path: path,
                deleteContents: deleteContents
            },
            httpMethod: "POST",
            contentType: "application/json",
            success: success,
            failure: failure
        });
    }

    renameDirectory(serverUrl, path, newName, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.RenameDirectory,
            data: {
                path: path,
                newName: newName
            },
            httpMethod: "POST",
            contentType: "application/json",
            success: success,
            failure: failure
        });
    }

    deleteSlide(serverUrl, path, deleteAsFile, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.DeleteSlide,
            data: {
                path: path,
                deleteAsFile: deleteAsFile
            },
            httpMethod: "POST",
            contentType: "application/json",
            success: success,
            failure: failure
        });
    }

    getAnnotations(serverUrl, path, currentUserOnly, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.GetAnnotations,
            data: {
                pathOrUid: path,
                currentUserOnly: currentUserOnly
            },
            httpMethod: "GET",
            success: success,
            failure: failure
        });
    }

    addAnnotation(serverUrl, path, classification, layerID, notes, geometry, color, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.AddAnnotation,
            data: {
                pathOrUid: path,
                classification: classification,
                layerID: layerID,
                notes: notes,
                geometry: geometry,
                color: color
            },
            httpMethod: "POST",
            contentType: "application/json",
            success: success,
            failure: failure
        });
    }

    updateAnnotation(serverUrl, path, layerID, annotationID, notes, geometry, color, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.UpdateAnnotation,
            data: {
                pathOrUid: path,
                layerID: layerID,
                annotationID: annotationID,
                notes: notes,
                geometry: geometry,
                color: color
            },
            httpMethod: "POST",
            contentType: "application/json",
            success: success,
            failure: failure
        });
    }

    saveAnnotations(serverUrl, path, added, updated, deleted, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.SaveAnnotations,
            data: {
                pathOrUid: path,
                added: added,
                updated: updated,
                deleted: deleted
            },
            httpMethod: "POST",
            contentType: "application/json",
            success: success,
            failure: failure
        });
    }

    deleteAnnotation(serverUrl, path, layerID, annotationID, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.DeleteAnnotation,
            data: {
                pathOrUid: path,
                layerID: layerID,
                annotationID: annotationID
            },
            httpMethod: "POST",
            contentType: "application/json",
            success: success,
            failure: failure
        });
    }

    getFormDefinitions(serverUrl, formIDs, rootDirectoryAlias, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.GetFormDefinitions,
            data: {
                formIDs: formIDs instanceof Array ? formIDs.join(",") : formIDs,
                rootDirectoryAlias: rootDirectoryAlias
            },
            httpMethod: "GET",
            success: success,
            failure: failure
        });
    }

    getFormSubmissions(serverUrl, pathOrUids, formIDs, currentUserOnly, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.GetFormSubmissions,
            data: {
                pathOrUids: pathOrUids instanceof Array ? pathOrUids : [],
                formIDs: formIDs instanceof Array ? formIDs : [],
                currentUserOnly: currentUserOnly
            },
            httpMethod: "POST",
            contentType: "application/json",
            success: success,
            failure: failure
        });
    }

    saveFormDefinition(serverUrl, definition, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.SaveFormDefinition,
            apiPath: "admin",
            contentType: "application/json",
            data: {
                definition: definition
            },
            httpMethod: "POST",
            success: success,
            failure: failure
        });
    }
    deleteFormDefinition(serverUrl, formID, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.DeleteFormDefinition,
            apiPath: "admin",
            data: {
                formID: formID
            },
            httpMethod: "GET",
            success: success,
            failure: failure
        });
    }

    getVersionInfo(serverUrl, success, failure) {
        callApiMethod({
            serverUrl: serverUrl,
            method: ApiMethods.GetVersionInfo,
            success: function (http) {
                if (typeof success === "function") {
                    var response = parseJson(http.responseText);
                    success(response);
                }
            },
            failure: function (http) {
                if (typeof failure === "function") {
                    if (http.responseText && http.responseText.length !== 0) {
                        var response = parseJson(http.responseText);
                        failure(response);
                    }
                    else {
                        failure({ Message: Resources.translate("Get Version Info failed") });
                    }
                }
            }
        });
    }

    /**
    * Gets the events log from the server
    * @param {Object} options - Parameters to pass to the GetEvents request
    * @param {string} options.serverUrl - The server url to get events log
    * @param {number} options.page - The page to fetch
    * @param {number} options.pageSize - The page size to fetch
    * @param {function} [options.success] - Called upon success
    * @param {function} [options.failure] - Called upon failure
    **/
    getEvents(options, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: options.serverUrl,
            method: ApiMethods.GetEvents,
            apiPath: "admin",
            data: {
                page: options.page,
                pageSize: options.pageSize
            },
            httpMethod: "GET",
            success: success,
            failure: failure
        });
    }

    deAuthenticate(serverUrl, success, failure) {
        var sessionId = null;
        // if (_sessionList.get().hasOwnProperty(serverUrl) && _sessionList.get()[serverUrl]) {
        if (Object.prototype.hasOwnProperty.call(_sessionList.get(), serverUrl) && _sessionList.get()[serverUrl]) {
            sessionId = _sessionList.get()[serverUrl].SessionId;
            if (!sessionId) {
                // nothing to do, no session ID cached, call success
                if (typeof success === "function") {
                    success();
                    return;
                }
            }
        }
        else {
            var cbFn = function (sId) {
                sessionId = sId;
            };

            // there is no cached sessionId for this server check for a sessionLogin provider
            for (var i = 0; i < this.authenticationProviders.length; i++) {
                if (this.authenticationProviders[i] instanceof PMA.UI.Authentication.SessionLogin) { // eslint-disable-line no-undef
                    // check that this sessionLogin provider can handle the requested serverUrl
                    if (this.authenticationProviders[i].authenticate(serverUrl, cbFn, null)) {
                        break;
                    }

                    continue;
                }
            }
        }

        if (sessionId == null) {
            // cannot handle this serverUrl just return
            return;
        }

        _sessionList.set(serverUrl, null);

        callApiMethod({
            serverUrl: serverUrl,
            method: ApiMethods.DeAuthenticate,
            data: { sessionID: sessionId },
            success: function () {
                if (typeof success === "function") {
                    success();
                }
            },
            failure: function (http) {
                if (typeof failure === "function") {
                    if (http.responseText && http.responseText.length !== 0) {
                        var response = parseJson(http.responseText);
                        failure(response);
                    }
                    else {
                        failure();
                    }
                }
            }
        });
    }

    queryFilename(serverUrl, path, pattern, success, failure) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: serverUrl,
            method: ApiMethods.QueryFilename,
            apiPath: "query",
            data: {
                path: path,
                pattern: pattern
            },
            httpMethod: "GET",
            success: success,
            failure: failure
        });
    }
    /**
    * Gets all distinct values for a field in a form
    * @param {Object} options - Parameters to pass to the "distinct values" request
    * @param {string} options.serverUrl - The server url to use
    * @param {string} options.formId - The Form Id to use
    * @param {string} options.fieldId - The Field Id to get distinct values for
    * @param {function} [options.success] - Called upon success
    * @param {function} [options.failure] - Called upon failure
    **/
    distinctValues(options) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: options.serverUrl,
            method: ApiMethods.DistinctValues,
            apiPath: "query",
            data: {
                formID: options.formId,
                fieldID: options.fieldId
            },
            httpMethod: "GET",
            success: options.success,
            failure: options.failure
        });
    }

    /**
    * Gets information for all slides specified
    * @param {Object} options - Parameters to pass to the GetSlides request
    * @param {string} options.serverUrl - The server url to get slides from
    * @param {string[]} options.images - An array of image paths or uids to fetch information for
    * @param {function} [options.success] - Called upon success
    * @param {function} [options.failure] - Called upon failure
    **/
    getImagesInfo(options) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: options.serverUrl,
            method: ApiMethods.GetImagesInfo,
            apiPath: "api",
            data: {
                pathOrUids: options.images
            },
            contentType: "application/json",
            httpMethod: "POST",
            success: options.success,
            failure: options.failure
        });
    }

    /**
    * Gets slides that satisfy the specified expressions
    * @param {Object} options - Parameters to pass to the "distinct values" request
    * @param {string} options.serverUrl - The server url to use
    * @param {Object[]} options.expressions - The Expressions to use
    * @param {Number} options.expressions.FormID - The form Id for this expression
    * @param {Number} options.expressions.FieldID - The field Id for this expression
    * @param {Number} options.expressions.Operator - The Operator for this expression ( Equals = 0, LessThan = 1, LessThanOrEquals = 2, GreaterThan = 3, GreaterThanOrEquals = 4)
    * @param {Number} options.expressions.Value - The value to compare for this expression
    * @param {function} [options.success] - Called upon success
    * @param {function} [options.failure] - Called upon failure
    **/
    metadata(options) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: options.serverUrl,
            method: ApiMethods.Metadata,
            apiPath: "query",
            data: {
                expressions: options.expressions
            },
            httpMethod: "POST",
            contentType: "application/json",
            success: options.success,
            failure: options.failure
        });
    }

    /**
    * Checks if a PMA.start server is available
    * @param {function} success - The function to call when check succeded
    * @param {function} failure - The function to call when check failed
    **/
    checkPmaStartServer(success, failure) {
        callApiMethod({
            method: ApiMethods.GetVersionInfo,
            httpMethod: "GET",
            serverUrl: PmaStartUrl,
            success: function () {
                if (typeof callback === "function") {
                    success.call(this, true);
                }
            },
            failure: function () {
                if (typeof callback === "function") {
                    failure.call(this, false);
                }
            }
        });
    }

    /**
    * Returns the url to PMA.start CORS allow page
    * @return {string}
    **/
    getPmaStartCorsUrl() {
        var url = PmaStartUrl + "home/allowdomain/?host=" + document.location.origin;
        return url;
    }

    /**
    * Gets slides that satisfy the specified expressions
    * @param {Object} options - Parameters to pass to the "distinct values" request
    * @param {string} options.serverUrl - The server url to use
    * @param {string} options.name - The script name to run
    * @param {object} [options.params] - Optional script parameters to pass
    * @param {function} [options.success] - Called upon success
    * @param {function} [options.failure] - Called upon failure
    **/
    runScript(options) {
        callApiMethodWithAuthentication.call(
            this, {
            attemptCount: 0,
            serverUrl: options.serverUrl,
            method: ApiMethods.RunScripts,
            apiPath: "scripts",
            webapi: true,
            data: Object.assign({
                name: options.name,
            }, options.params),
            httpMethod: "GET",
            success: options.success,
            failure: options.failure
        });
    }

    // registers an event listener
    listen(eventName, callback) {
        // if (!this.listeners.hasOwnProperty(eventName)) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
            console.error(eventName + " is not a valid event");
        }

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

    fireEvent(eventName, eventArgs) {
        // if (!this.listeners.hasOwnProperty(eventName)) {
        if (!Object.prototype.hasOwnProperty.call(this.listeners, eventName)) {
            console.error(eventName + " does not exist");
            return;
        }

        for (var i = 0, max = this.listeners[eventName].length; i < max; i++) {
            this.listeners[eventName][i](eventArgs);
        }
    }

    /**
    * A function called for progress on an upload
    * @callback Context~uploadProgressCallback
    * @param {String} name - The file name uploaded
    * @param {Number} progress - The progress percentage
    */

    /**
     * Uploads a file to the pma.core server
     * @param {Object} options 
     * @param {string} options.serverUrl - The server url to use
     * @param {string} options.targetPath - The virtual path to upload to
     * @param {File} options.file - The file object to upload
     * @param {Context~uploadProgressCallback} options.progress - A progress callback
     * @returns A promise to get the result of the upload
     */
    uploadFile(options) {
        let that = this;
        return new Promise(function (resolve, reject) {
            callApiMethodWithAuthentication.call(
                that, {
                attemptCount: 0,
                serverUrl: options.serverUrl,
                method: ApiMethods.Upload,
                apiPath: "transfer",
                webapi: true,
                data: {
                    "path": options.targetPath,
                    "files": [{
                        "isMain": true,
                        "length": options.file.size,
                        "path": options.file.name
                    }]
                },
                httpMethod: "POST",
                contentType: "application/json",
                success: function (sessionId, uploadContent) {
                    let method = "PUT";
                    if (!uploadContent.Urls) {
                        uploadContent.Urls = [];
                    }

                    if (uploadContent.Urls.length === 0) {
                        method = "POST";
                        uploadContent.Urls.push(`${options.serverUrl}transfer/Upload/${uploadContent.Id}?sessionId=${sessionId}&path=${encodeURIComponent(options.file.name)}`);
                    }

                    var fd = new FormData();
                    fd.append("file", options.file);
                    var xhr = new XMLHttpRequest();
                    xhr.open(method, uploadContent.Urls[0], true);

                    xhr.upload.onprogress = function (e) {
                        let progress = Math.ceil((e.loaded / e.total) * 100);
                        if (typeof options.progress === "function") {
                            options.progress(options.file.name, progress);
                        }
                    };

                    xhr.onload = function () {
                        resolve(true);
                    };

                    xhr.onerror = function () {
                        reject();
                    }

                    if (method === "POST") {
                        xhr.send(fd);
                    }
                    else {
                        xhr.send(options.file);
                    }
                },
                failure: function (f) { reject(f); }
            });
        });
    }


    /**
    * A function called for progress on an upload
    * @callback Context~slideUploadProgressCallback
    * @param {ImageUpload~Slide} slide - The slide uploaded
    * @param {Number} progress - The progress percentage
    */

    /**
    * A function called for result of an upload
    * @callback Context~slideUploadResultCallback
    * @param {ImageUpload~Slide} slide - The slide uploaded
    */

    /**
     * Uploads a file to the pma.core server
     * @param {Object} options 
     * @param {string} options.serverUrl - The server url to use
     * @param {string} options.targetPath - The virtual path to upload to
     * @param {ImageUpload~Slide} options.slide - The file object to upload
     * @param {Context~slideUploadProgressCallback} options.progress - A callback called on progress
     * @param {Context~slideUploadResultCallback} options.success - A callback called on success
     * @param {Context~slideUploadResultCallback} options.error - A callback called on error
     * @returns A promise to get the result of the upload
     */
    uploadSlide(options) {
        let that = this;
        return new Promise(function (resolve) {
            var completed = 0;
            var totalSize = options.slide.files.map(f => (f && f.size) ? f.size : 0).reduce((accumulator, current) => accumulator + current, 0);
            var uploadedSize = [];
            callApiMethodWithAuthentication.call(
                that, {
                attemptCount: 0,
                serverUrl: options.serverUrl,
                method: ApiMethods.Upload,
                apiPath: "transfer",
                webapi: true,
                data: {
                    "path": options.targetPath,
                    "files": options.slide.files.map((x, i) => {
                        return {
                            "isMain": i === 0,
                            "length": x.size,
                            "path": (i === 0 ? "" : (!options.targetChildPath ? "" : (Array.isArray(options.targetChildPath) ? options.targetChildPath[i] : options.targetChildPath) + "/")) + x.name
                        }
                    })
                },
                httpMethod: "POST",
                contentType: "application/json",
                success: function (sessionId, uploadContent) {
                    let method = "PUT";
                    let isUploadingAzure = false;

                    if (uploadContent.UploadType === 2) {
                        isUploadingAzure = true;
                    }

                    if (!uploadContent.Urls) {
                        uploadContent.Urls = [];
                    }

                    if (uploadContent.Urls.length === 0) {
                        method = "POST";
                        options.slide.files.map((x, i) => uploadContent.Urls.push(`${options.serverUrl}transfer/Upload/${uploadContent.Id}?sessionId=${sessionId}&path=${encodeURIComponent((i === 0 ? "" : (!options.targetChildPath ? "" : (Array.isArray(options.targetChildPath) ? options.targetChildPath[i] : options.targetChildPath) + "/")) + x.name)}`))
                    }

                    for (var i = 0; i < options.slide.files.length; i++) {
                        (function (index) {
                            var fd = new FormData();
                            var f = options.slide.files[index];
                            fd.append("file", f, f.name);
                            var xhr = new XMLHttpRequest();
                            xhr.open(method, uploadContent.Urls[index], true);
                            uploadedSize[index] = 0;

                            if (isUploadingAzure) {
                                xhr.setRequestHeader("x-ms-blob-type", "BlockBlob");
                            }

                            xhr.upload.onprogress = function (e) {
                                uploadedSize[index] = e.loaded;
                                let totalUploadedSize = uploadedSize.reduce((accumulator, current) => accumulator + current, 0);
                                let progress = Math.floor((totalUploadedSize / totalSize) * 100);
                                if (progress !== options.slide.uploadProgress && typeof options.progress === "function") {
                                    options.progress(options.slide, progress);
                                }
                            };

                            xhr.onload = function () {
                                uploadedSize[index] = f.size;
                                completed++;
                                if (completed >= options.slide.files.length) {
                                    fetch(`${options.serverUrl}transfer/Upload/${uploadContent.Id}?sessionId=${sessionId}`);
                                    if (typeof options.success === "function") {
                                        options.success(options.slide);
                                    }
                                    resolve(true);
                                }
                            };

                            xhr.onerror = function (evt) {
                                if (typeof options.error === "function") {
                                    options.error(options.slide, evt.Message ? evt.Message : "");
                                }
                                resolve();
                            }

                            if (method === "POST") {
                                xhr.send(fd);
                            }
                            else {
                                xhr.send(f);
                            }
                        })(i);
                    }
                },
                failure: function (f) {
                    if (typeof options.error === "function") {
                        options.error(options.slide, f.Message ? f.Message : "");
                    }
                    resolve(f);
                }
            });
        });
    }
}

// private methods

// finds the first authentication provider that handles the requested server
// and calls it's authenticate method 
function authenticateWithProvider(serverUrl, success, failure) {
    // search all authentication providers apart from PromptLogin
    var i = 0;
    for (i = 0; i < this.authenticationProviders.length; i++) {
        if (this.authenticationProviders[i] instanceof PromptLogin) {
            continue;
        }

        if (this.authenticationProviders[i].authenticate(serverUrl, success, failure)) {
            return;
        }
    }

    // if none of the previous providers were able to provide a session, search for a PromptLogin and attempt to authenticate with it
    for (i = 0; i < this.authenticationProviders.length; i++) {
        if (this.authenticationProviders[i] instanceof PromptLogin) {
            if (this.authenticationProviders[i].authenticate(serverUrl, success, failure)) {
                return;
            }

            break;
        }
    }

    // no provider found, call failure
    if (typeof failure === "function") {
        failure({ Message: Resources.translate("Authentication failed at server {serverUrl}.", { serverUrl: serverUrl }) });
    }
}

/**
 * calls an API method that requires a session ID. This method will first attempt to acquire 
 * a session ID (possibly a cached one) and then call the requested method. 
 * If the method call fails because the call was unauthorized (so possible the session ID was not good),
 * the method will be called again for a second time, this time forcing an authentication before the
 * actual call.
 * @param  {object} options - The parameters to pass to the ajax request
 * @param  {number} options.attemptCount - Current attempt count
 * @param  {string} options.serverUrl - The URL of the server to send the request to
 * @param  {string} options.method - The API method to call
 * @param  {object} options.data - The data to send
 * @param  {string} options.httpMethod - The HTTP method to use
 * @param  {string} options.contentType - The content type of the request
 * @param  {function} options.success - Called upon success
 * @param  {function} options.failure - Called upon failure
 * @param {string} [options.apiPath="api"] - The API path to append to the server URL
 * @param {boolean} [options.webapi=false] - Whether the api call is a webapi call
 * @fires PMA.UI.Components.Events.SessionIdLoginFailed
 * @ignore
 */
function callApiMethodWithAuthentication(options) {
    var _this = this;

    if (!options.apiPath) {
        options.apiPath = "api";
    }

    _this.getSession(options.serverUrl,
        function (sessionId) {
            // we have a session ID, try to call the actual method
            options.data.sessionID = sessionId;

            callApiMethod({
                serverUrl: options.serverUrl,
                method: options.method,
                data: options.data,
                contentType: options.contentType,
                httpMethod: options.httpMethod,
                apiPath: options.apiPath,
                webapi: options.webapi,
                success: function (http) {
                    // the call succeeded, parse data and call success
                    if (typeof options.success === "function") {
                        var response = parseJson(http.responseText);
                        options.success(options.data.sessionID, response);
                    }
                },
                failure: function (http) {
                    // failed, clean up the session ID that was possibly cached for this server
                    _sessionList.set(options.serverUrl, null);

                    // if it's the first attempt, try again, otherwise fail for good
                    if (http.status == 0 && options.attemptCount === 0) {
                        options.attemptCount = 1;
                        callApiMethodWithAuthentication.call(_this, options);
                    }
                    else {
                        if (typeof options.failure === "function") {
                            if (http.responseText && http.responseText.length !== 0) {
                                try {
                                    var response = parseJson(http.responseText);
                                    options.failure(response);
                                    _this.fireEvent(Events.SessionIdLoginFailed, { serverUrl: options.serverUrl });
                                }
                                catch (ex) {
                                    options.failure(http.responseText);
                                    _this.fireEvent(Events.SessionIdLoginFailed, { serverUrl: options.serverUrl });
                                }
                            }
                            else {
                                options.failure({ Message: Resources.translate("Authentication failed") });
                                _this.fireEvent(Events.SessionIdLoginFailed, { serverUrl: options.serverUrl });
                            }
                        }
                    }
                }
            });
        },
        function (error) {
            if (!error.Message && error.Reason) {
                error.Message = error.Reason;
            }

            // session acquisition failed in the first place, so fail
            if (typeof options.failure === "function") {
                options.failure(error);
            }

            _this.fireEvent(Events.SessionIdLoginFailed, { serverUrl: options.serverUrl });
        });
}

// legacy alias because of typo
Context.prototype.GetImagesInfo = Context.prototype.getImagesInfo;