Source: widget/companionCore/SEN.js

(function() {
    /**
     * @typedef {Object} Help4.widget.companionCore.SEN.CreatePubUrlParam
     * @property {string} [serverBaseUrl] - base URL of the content server
     * @property {string} [waId] - ID of workspace on the sever
     * @property {string} [serverUrl] - URL to the content on the server
     * @property {string} tag - to be used tag
     */

    /**
     * @typedef {Object} Help4.widget.companionCore.SEN.MultifileRequest
     * @property {string} url - URL on the server; relative to serverBaseUrl
     * @property {string} [method = GET] - GET, POST, PUT, DELETE
     * @property {string} [dataType] - e.g. text, xml, json, multipart
     */

    /**
     * @typedef {Object} Help4.widget.companionCore.SEN.MultifileParams
     * @property {string} serverBaseUrl - base URL of the content server
     * @property {Help4.widget.companionCore.SEN.MultifileRequest[]} request - the request information
     */

    /**
     * @typedef {Object} Help4.widget.companionCore.SEN.CatalogueResponse
     * @property {*} [status] - response status on error
     * @property {Help4.widget.help.catalogues.SEN.CatalogueProject[]} [response] - server response
     */

    /**
     * @typedef {Object} Help4.widget.companionCore.SEN.ContextResponse
     * @property {*} [status] - response status on error
     * @property {Object[]} [response]
     */

    /**
     * @typedef {Document|Object} Help4.widget.companionCore.SEN.XmlResponse
     * @property {*} [status] - response status on error
     */

    /**
     * @typedef {Object} Help4.widget.companionCore.SEN.ProjectDataResponse
     * @property {*} [status] - response status on error
     * @property {Help4.widget.help.project.SEN.Project} [response]
     */

    /**
     * @typedef {Object} Help4.widget.companionCore.SEN.TagResponse
     * @property {*} [status] - response status on error
     * @property {Object} [response]
     * @property {Help4.widget.help.project.SEN.TagInfoList} response.resource
     */

    /**
     * @typedef {Help4.widget.companionCore.SEN.ContextResponse|Help4.widget.companionCore.SEN.XmlResponse|Help4.widget.companionCore.SEN.ProjectDataResponse|Help4.widget.companionCore.SEN.TagResponse|null} Help4.widget.companionCore.SEN.ProjectResponses
     */

    /**
     * @typedef {Object} Help4.widget.companionCore.SEN.PubProjectResponse
     * @property {Help4.widget.help.ContextTypes} contextType
     * @property {Help4.widget.help.project.SEN.EntityTxt} entityTxt
     * @property {Help4.widget.help.project.SEN.LessonJs} lessonJs
     */

    /**
     * @typedef {Object} Help4.widget.companionCore.SEN.ProjectResponsePub
     * @property {Help4.widget.companionCore.SEN.PubProjectResponse} context
     * @property {Help4.widget.help.project.SEN.Project} project
     */

    /**
     * @typedef {Object} Help4.widget.companionCore.SEN.ProjectResponseHead
     * @property {Document} entityXml
     * @property {Document} projectDpr
     * @property {Help4.widget.help.project.SEN.Project} project
     */

    /**
     * @typedef {Object} Help4.widget.companionCore.SEN.ProjectResponse
     * @property {?Help4.widget.companionCore.SEN.ProjectResponsePub} pub
     * @property {?Help4.widget.companionCore.SEN.ProjectResponseHead} head
     * @property {?Help4.widget.help.project.SEN.TagInfoList} tagInfo
     */

    /**
     * helper functionality for SEN backends
     */
    Help4.widget.companionCore.SEN = class {
        /**
         * Header information for XHR to prohibit caching.
         * @memberof Help4.widget.companionCore.SEN
         */
        static NO_CACHE_HEADER =  {
            'Cache-Control': 'no-cache, no-store, must-revalidate, max-age=0',
            Pragma: 'no-cache'
        }

        /**
         * extracts the server base url from a full server url
         * @param {string} serverUrl
         * @returns {string}
         */
        static getServerBaseUrl(serverUrl) {
            return serverUrl.replace(/\/(wa|pub)\/.*$/, '');
        }

        /**
         * creates a public url from a non-public one
         * @param {Help4.widget.companionCore.SEN.CreatePubUrlParam} params
         * @returns {string}
         */
        static createPubUrl({serverBaseUrl, waId, serverUrl, tag}) {
            if (waId) {
                return tag === 'published'
                    ? `${serverBaseUrl}/pub/${waId}/`
                    : `${serverBaseUrl}/wa/${waId}/~tag/${tag}/`;
            } else {
                if (tag === 'published') {  // XRAY-1486
                    // use wormhole
                    // - replace '/wa/<waid>'
                    // - with '/pub/<waid'
                    return serverUrl.replace(/\/wa\//, '/pub/');
                } else {  // XRAY-2483
                    // use long version
                    // - replace '/wa/<waid>'
                    // - with 'wa/<waid>/~tag/<tag>'
                    return serverUrl.replace(/(\/wa\/[^/]+)/, `$1/~tag/${tag}`);
                }
            }
        }

        /**
         * @param {Help4.typedef.SystemConfiguration} config - the system configuration
         * @param {Help4.widget.companionCore.SEN.MultifileParams} params - the request information
         * @returns {Promise<*>}
         */
        static doMultifileRequest({core: {multifile}}, {serverBaseUrl, request}) {
            if (!multifile) return this.doMultifileWorkaround({serverBaseUrl, request});

            return new Help4.Promise(resolve => {
                /**
                 * @param {*} data
                 * @param {Help4.typedef.XmlHttpResponse} xhr
                 * @param {*} error
                 * @returns {Promise<void>}
                 */
                const afterRequest = async (data, xhr, error) => {
                    Help4.widget.companionCore.Core.broadcastXhrStatus(xhr);

                    if (error === 'error' && xhr && xhr.status !== 200) {
                        // multifile not supported here
                        data = await this.doMultifileWorkaround({serverBaseUrl, request});
                    }

                    resolve(data);
                }

                Help4.ajax.EnableNow({
                    headers: this.NO_CACHE_HEADER,
                    url: serverBaseUrl + '/multifile',
                    saml: true,
                    xcsrf: this.getXCSRF(serverBaseUrl),
                    method: 'POST',
                    dataType: 'multipart',
                    data: {request},
                    success: afterRequest,
                    error: afterRequest
                });
            });
        }

        /**
         * @param {Help4.widget.companionCore.SEN.MultifileParams} params - the request information
         * @returns {Promise<*>}
         */
        static async doMultifileWorkaround({serverBaseUrl, request}) {
            function* generateWorkaroundRequest() {
                const {Core, SEN} = Help4.widget.companionCore;

                for (const {url, method = 'GET', dataType} of request) {
                    const promise = new Help4.Promise(resolve => {
                        /**
                         * @param {*} data
                         * @param {Help4.typedef.XmlHttpResponse} xhr
                         */
                        const afterRequest = (data, xhr) => {
                            Core.broadcastXhrStatus(xhr);
                            resolve(data);
                        };

                        Help4.ajax.Ajax({
                            headers: SEN.NO_CACHE_HEADER,
                            method,
                            url: serverBaseUrl + url,
                            saml: true,
                            dataType,
                            success: afterRequest,
                            error: afterRequest
                        });
                    });

                    yield {promise};
                }
            }

            const {/** @type {Array<*>} */ array} = await Help4.awaitPromises(generateWorkaroundRequest);
            return array;
        }

        /**
         * @param {string} serverBaseUrl - the server base URL
         * @returns {string}
         */
        static getXCSRF(serverBaseUrl) {
            const {Ajax} = Help4.ajax;

            const url = serverBaseUrl + '/self';
            let id = Ajax.getXCSRF(url);
            if (id) return id;

            id = Help4.createId();
            Ajax.setXCSRF({id, url});
            return id;
        }

        /**
         * @param {string} url
         * @returns {Promise<*>}
         */
        static doGetRequest(url) {
            return new Help4.Promise(resolve => {
                /**
                 * @param {*} data
                 * @param {Help4.typedef.XmlHttpResponse} xhr
                 */
                const afterRequest = (data, xhr) => {
                    Help4.widget.companionCore.Core.broadcastXhrStatus(xhr);
                    resolve(data);
                };

                // XRAY-1490: do not use XCSRF ID here
                Help4.ajax.Ajax({
                    url: url,
                    saml: true,
                    success: afterRequest,
                    error: afterRequest
                });
            });
        }

        /**
         * @param {Help4.typedef.SystemConfiguration} config - the system configuration
         * @param {boolean} [allowDuplicates = false] - whether to allow duplicate language codes
         * @returns {string[]}
         */
        static getLanguages({core: {language, defaultLanguage}}, allowDuplicates = false) {
            const languagesList = [language.wpb].concat(defaultLanguage.wpb);
            return allowDuplicates ? languagesList : Help4.uniqueArray(languagesList);
        }

        /**
         * loads an SEN project
         * @param {Help4.typedef.SystemConfiguration} config
         * @param {string} serviceUrl
         * @param {Help4.widget.help.catalogues.SEN.ProjectUrlConfig} request
         * @returns {Promise<Help4.widget.companionCore.SEN.ProjectResponse|Help4.widget.companionCore.SEN.PubProjectResponse|null>}
         */
        static async doProjectRequest(config, serviceUrl, {serverBaseUrl, pubUrl, headUrl = null}) {
            /**
             * @param {Help4.widget.companionCore.SEN.ContextResponse|null} serverResponse
             * @returns {Help4.widget.companionCore.SEN.PubProjectResponse|null}
             */
            const extractPubContextInfo = serverResponse => {
                serverResponse = !serverResponse?.status && serverResponse?.response?.[0] || {};
                /** @type {'HELP'|'TOUR'|null} */
                let contextType = null;
                /** @type {Help4.widget.help.project.SEN.EntityTxt|null} */
                let entityTxt = null;
                /** @type {Help4.widget.help.project.SEN.LessonJs|null} */
                let lessonJs = null;

                for (const [key, value] of Object.entries(serverResponse)) {
                    if (key.includes('entity.txt')) {
                        entityTxt = value;
                    } else if (key.includes('lesson.js')) {
                        lessonJs = value;
                    } else {
                        contextType = value;
                    }
                }

                return contextType && entityTxt && lessonJs
                    ? {contextType, entityTxt, lessonJs}
                    : null;
            }


            if (!headUrl) {
                // non-editor mode; download PUB only
                const response = await this.doGetRequest(pubUrl);
                /** @type {Help4.widget.companionCore.SEN.PubProjectResponse|null} */
                return extractPubContextInfo(response);
            } else {
                // editor mode; download PUB and head
                const len = serverBaseUrl.length;
                serviceUrl = serviceUrl.substring(len);
                pubUrl = pubUrl.substring(len);
                headUrl = headUrl.substring(len);

                const {playbackTag: tag} = config.core;
                const pubUrl2 = this.createPubUrl({serverUrl: headUrl, tag});  // XRAY-3305

                /** @type {Array<{url: string, dataType: ?string}>} */
                const request = [
                    {url: pubUrl},                                     // "/pub/<waId>/.context?..."
                    {url: `${headUrl}/entity.xml`, dataType: 'xml'},   // "/wa/<waId>/project/<projectId>/entity.xml"
                    {url: `${headUrl}/project.dpr`, dataType: 'xml'},  // "/wa/<waId>/project/<projectId>/project.dpr"
                    {url: headUrl},                                    // "/wa/<waId>/project/<projectId>"
                    {url: pubUrl2},                                    // "/pub/<waId>/project/<projectId>"
                    {url: `${serviceUrl}/~tag`}                        // "/wa/<waId>/~tag"
                ];

                /** @type {Array<Help4.widget.companionCore.SEN.ProjectResponses>} */
                const response = await this.doMultifileRequest(config, {serverBaseUrl, request});
                if (!response) return null;

                /** @type {Help4.widget.companionCore.SEN.ContextResponse} */
                let pubContext = response[0];
                /** @type {Help4.widget.companionCore.SEN.XmlResponse} */
                let headEntityXml = response[1];
                /** @type {Help4.widget.companionCore.SEN.XmlResponse} */
                let headProjectDpr = response[2];
                /** @type {Help4.widget.companionCore.SEN.ProjectDataResponse} */
                let headProject = response[3];
                /** @type {Help4.widget.companionCore.SEN.ProjectDataResponse} */
                let pubProject = response[4];
                /** @type {Help4.widget.companionCore.SEN.TagResponse} */
                let tagInfo = response[5];

                /** @type {Help4.widget.companionCore.SEN.PubProjectResponse|null} */
                pubContext = extractPubContextInfo(pubContext);
                /** @type {Document|null} */
                headEntityXml = headEntityXml?.status ? null : headEntityXml;
                /** @type {Document|null} */
                headProjectDpr = headProjectDpr?.status ? null : headProjectDpr;
                /** @type {Help4.widget.help.project.SEN.Project|null} */
                headProject = !headProject?.status && headProject?.response || null;
                /** @type {Help4.widget.help.project.SEN.Project|null} */
                pubProject = !pubProject?.status && pubProject?.response || null;
                /** @type {Help4.widget.help.project.SEN.TagInfoList|null} */
                tagInfo = !tagInfo?.status && tagInfo?.response?.resource || null;

                /** @type {Help4.widget.companionCore.SEN.ProjectResponsePub|null} */
                const pub = pubContext && pubProject
                    ? {context: pubContext, project: pubProject}
                    : null;

                /** @type {Help4.widget.companionCore.SEN.ProjectResponseHead|null} */
                const head = headEntityXml && headProjectDpr && headProject
                    ? {entityXml: headEntityXml, projectDpr: headProjectDpr, project: headProject}
                    : null;

                /** @type {Help4.widget.companionCore.SEN.ProjectResponse} */
                return {pub, head, tagInfo};
            }
        }
    }
})();