// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
//
// SPDX-License-Identifier: AGPL-3.0-or-later

define([
    'jquery',
    '/common/toolbar.js',
    'json.sortify',
    '/components/nthen/index.js',
    '/common/sframe-common.js',
    '/common/common-interface.js',
    '/common/common-hash.js',
    '/common/common-util.js',
    '/common/common-ui-elements.js',
    '/common/common-feedback.js',
    '/common/hyperscript.js',
    '/api/config',
    '/customize/messages.js',
    '/components/chainpad/chainpad.dist.js',
    '/file/file-crypto.js',
    '/common/onlyoffice/history.js',
    '/common/onlyoffice/oocell_base.js',
    '/common/onlyoffice/oodoc_base.js',
    '/common/onlyoffice/ooslide_base.js',
    '/common/outer/worker-channel.js',

    '/common/onlyoffice/current-version.js',
    '/common/onlyoffice/broken-formats.js',
    '/components/file-saver/FileSaver.min.js',

    'css!/components/bootstrap/dist/css/bootstrap.min.css',
    'less!/components/components-font-awesome/css/font-awesome.min.css',
    'less!/common/onlyoffice/app-oo.less',
], function (
    $,
    Toolbar,
    JSONSortify,
    nThen,
    SFCommon,
    UI,
    Hash,
    Util,
    UIElements,
    Feedback,
    h,
    ApiConfig,
    Messages,
    ChainPad,
    FileCrypto,
    History,
    EmptyCell,
    EmptyDoc,
    EmptySlide,
    Channel,
    OOCurrentVersion,
    BrokenFormats)
{
    var saveAs = window.saveAs;
    var Nacl = window.nacl;
    var APP = window.APP = {
        $: $,
        urlArgs: Util.find(ApiConfig, ['requireConf', 'urlArgs'])
    };

    var CHECKPOINT_INTERVAL = 100;
    var FORCE_CHECKPOINT_INTERVAL = 10000;
    var DISPLAY_RESTORE_BUTTON = false;
    var PENDING_TIMEOUT = 30000;
    const HISTORY_KEEPER_INDEX_USER = 1;
    const READ_ONLY_INDEX_USER = 2;
    //var READONLY_REFRESH_TO = 15000;

    var debug = function (x, type) {
        if (!window.CP_DEV_MODE) { return; }
        console.debug(x, type);
    };

    var stringify = function (obj) {
        return JSONSortify(obj);
    };

    var toolbar;
    var cursor;


    var andThen = function (common) {
        var Title;
        var sframeChan = common.getSframeChannel();
        var metadataMgr = common.getMetadataMgr();
        var privateData = metadataMgr.getPrivateData();
        var readOnly = false;
        var offline = false;
        var ooLoaded = false;
        var pendingChanges = {};
        var config = {};
        var content = {
            hashes: {},
            ids: {},
            mediasSources: {},
            version: privateData.ooForceVersion ? Number(privateData.ooForceVersion) : OOCurrentVersion.currentVersionNumber
        };
        var oldHashes = {};
        var oldIds = {};
        var oldLocks = {};
        var myUniqueOOId;
        var myOOId;
        var cpNfInner;
        let integrationChannel;

        var evOnPatch = Util.mkEvent();
        var evOnSync = Util.mkEvent();
        const evIntegrationSave = Util.mkEvent();

        // This structure is used for caching media data and blob urls for each media cryptpad url
        var mediasData = {};

        let startOO = function () {};

        var supportsXLSX = function () {
            return privateData.supportsWasm;
        };

        var getMediasSources = APP.getMediasSources =  function() {
            content.mediasSources = content.mediasSources || {};
            return content.mediasSources;
        };

        var getId = function () {
            return metadataMgr.getNetfluxId() + '-' + privateData.clientId;
        };

        var getWindow = function () {
            return window.frames && window.frames[0];
        };
        var getEditor = function () {
            var w = getWindow();
            if (!w) { return; }
            return w.editor || w.editorCell;
        };

        var setEditable = function (state, force) {
            $('#cp-app-oo-editor').find('#cp-app-oo-offline').remove();
            /*
            try {
                getEditor().asc_setViewMode(!state);
                //window.frames[0].editor.setViewModeDisconnect(true);
            } catch (e) {}
            */
            if (!state && (!readOnly || force)) {
                $('#cp-app-oo-editor').append(h('div#cp-app-oo-offline'));
            }
        };

        var deleteOffline = function () {
            var ids = content.ids;
            var users = Object.keys(metadataMgr.getMetadata().users);
            Object.keys(ids).forEach(function (id) {
                var nId = id.slice(0,32);
                if (users.indexOf(nId) === -1) {
                    delete ids[id];
                }
            });
            APP.onLocal();
        };

        var isRegisteredUserOnline = function () {
            var users = metadataMgr.getMetadata().users || {};
            return Object.keys(users).some(function (id) {
                return users[id] && users[id].curvePublic;
            });
        };
        var isUserOnline = function (ooid) {
            // Remove ids for users that have left the channel
            deleteOffline();
            var ids = content.ids;
            // Check if the provided id is in the ID list
            return Object.keys(ids).some(function (id) {
                return ooid === ids[id].ooid;
            });
        };

        const getNextUserIndex = function () {
            let nextUserIndex;
            do {
                nextUserIndex = Util.createRandomInteger();
            } while (nextUserIndex === HISTORY_KEEPER_INDEX_USER || nextUserIndex === READ_ONLY_INDEX_USER);
            return nextUserIndex;
        };

        var setMyId = function () {
            deleteOffline(); // Remove ids for users that have left the channel
            const ids = content.ids;
            if (!myOOId) {
                myOOId = Util.createRandomInteger();
                // f: function used in .some(f) but defined outside of the while
                var f = function (id) {
                    return ids[id].ooid === myOOId;
                };
                while (Object.keys(ids).some(f)) {
                    myOOId = Util.createRandomInteger();
                }
            }

            const myId = getId();
            const myIndex = getNextUserIndex();

            ids[myId] = {
                ooid: myOOId,
                index: myIndex,
                netflux: metadataMgr.getNetfluxId()
            };

            if (!myUniqueOOId) {
                myUniqueOOId = String(myOOId) + myIndex;
            }

            oldIds = structuredClone(ids);
            APP.onLocal();
        };

        // Another tab from our worker has left: remove its id from the list
        var removeClient = function (obj) {
            var tabId = metadataMgr.getNetfluxId() + '-' + obj.id;
            if (content.ids[tabId]) {
                delete content.ids[tabId];
                if (content.locks) { delete content.locks[tabId]; }
                APP.onLocal();
            }
        };

        // Make sure a former tab on the same worker doesn't have remaining locks
        var checkClients = function (clients) {
            if (!clients) { return; }
            Object.keys(content.ids).forEach(function (id) {
                var tabId = Number(id.slice(33)); // remove the netflux ID and the "-"
                if (clients.indexOf(tabId) === -1) {
                    removeClient({
                        id: tabId
                    });
                }
            });
        };


        var getFileType = function () {
            var priv = common.getMetadataMgr().getPrivateData();
            var type = priv.ooType;
            var title = common.getMetadataMgr().getMetadataLazy().title;
            if (APP.downloadType) {
                type = APP.downloadType;
                title = "download";
            }
            if(title === "" && APP.startWithTemplate && priv.fromFileData) {
                var metadata = APP.startWithTemplate.content.metadata;
                var copyTitle = Messages._getKey('copy_title', [metadata.title || metadata.defaultTitle]);
                common.getMetadataMgr().updateTitle(copyTitle);
                title = copyTitle;
            }
            var file = {};
            switch(type) {
                case 'doc':
                    file.type = 'docx';
                    file.title = title + '.docx' || 'document.docx';
                    file.doc = 'text';
                    break;
                case 'sheet':
                    file.type = 'xlsx';
                    file.title = title + '.xlsx' || 'spreadsheet.xlsx';
                    file.doc = 'spreadsheet';
                    break;
                case 'presentation':
                    file.type = 'pptx';
                    file.title = title + '.pptx' || 'presentation.pptx';
                    file.doc = 'presentation';
                    break;
            }
            return file;
        };

        var now = function () { return +new Date(); };

        var sortCpIndex = function (hashes) {
            return Object.keys(hashes).map(Number).sort(function (a, b) {
                return a-b;
            });
        };
        var getLastCp = function (old, i) {
            var hashes = old ? oldHashes : content.hashes;
            if (!hashes || !Object.keys(hashes).length) { return {}; }
            i = i || 0;
            var idx = sortCpIndex(hashes);
            var lastIndex = idx[idx.length - 1 - i];
            if (typeof(lastIndex) === "undefined" || !hashes[lastIndex]) {
                return {};
            }
            var last = JSON.parse(JSON.stringify(hashes[lastIndex]));
            return last;
        };
        var deleteLastCp = function (i) {
            var hashes = content.hashes;
            if (!hashes || !Object.keys(hashes).length) { return {}; }
            i = i || 0;
            var idx = sortCpIndex(hashes);
            var lastIndex = idx[idx.length - 1 - i];
            if (typeof(lastIndex) === "undefined" || !hashes[lastIndex]) {
                return;
            }
            delete hashes[lastIndex];
            APP.onLocal();
            APP.realtime.onSettle(function () {
                UI.log(Messages.saved);
            });
        };

        var rtChannel = {
            ready: false,
            readyCb: undefined,
            sendCmd: function (data, cb) {
                if (APP.history) { return; }
                sframeChan.query('Q_OO_COMMAND', data, cb);
            },
            getHistory: function (cb) {
                rtChannel.sendCmd({
                    cmd: 'GET_HISTORY',
                    data: {}
                }, function () {
                    APP.onHistorySynced = cb;
                });
            },
            sendMsg: function (msg, cp, cb) {
                evOnPatch.fire();
                rtChannel.sendCmd({
                    cmd: 'SEND_MESSAGE',
                    data: {
                        msg: msg,
                        isCp: cp
                    }
                }, function (err, h) {
                    if (!err) {
                        evOnSync.fire();
                        evIntegrationSave.fire();
                    }
                    cb(err, h);
                });
            },
        };

        var ooChannel = {
            ready: false,
            queue: [],
            send: function () {},
            cpIndex: 0
        };

        var getContent = function () {
            try {
                return getEditor().asc_nativeGetFile();
            } catch (e) {
                console.error(e);
                return;
            }
        };

        /*
        var checkDrawings = function () {
            var editor = getEditor();
            if (!editor || !editor.GetSheets) { return false; }
            var s = editor.GetSheets();
            return s.some(function (obj) {
                return obj.worksheet.Drawings.length;
            });
        };
        */

        // DEPRECATED from version 3
        // Loading a checkpoint reorder the sheet starting from ID "5".
        // We have to reorder it manually when a checkpoint is created
        // so that the messages we send to the realtime channel are
        // loadable by users joining after the checkpoint
        var fixSheets = function () {
            // Starting from version 3, we don't need to fix the sheet IDs anymore
            // because we reload onlyoffice whenever we receive a checkpoint
            if (!APP.migrate || (content && content.version > 2)) { return; }

            try {
                var editor = getEditor();
                // if we are not in the sheet app
                // we should not call this code
                if (typeof editor.GetSheets === 'undefined') { return; }
                var s = editor.GetSheets();
                if (s.length === 0) { return; }
                var wb = s[0].worksheet.workbook;
                s.forEach(function (obj, i) {
                    var id = String(i + 5);
                    obj.worksheet.Id = id;
                    wb.aWorksheetsById[id] = obj.worksheet;
                });
            } catch (e) {
                console.error(e);
            }
        };

        // Add a lock
        var isLockedModal = {
            content: UI.dialog.customModal(h('div.cp-oo-x2tXls', [
                h('span.fa.fa-spin.fa-spinner'),
                h('span', Messages.oo_isLocked)
            ]))
        };

        var onUploaded = function (ev, data, err) {
            if (!ev && err) {
                console.error(err);
                return void UI.warn(Messages.error);
            }
            if (ev.newTemplate) {
                if (err) {
                    console.error(err);
                    return void UI.warn(Messages.error);
                }
                var _content = ev.newTemplate;
                _content.hashes = {};
                _content.hashes[1] = {
                    file: data.url,
                    index: 0,
                    version: OOCurrentVersion.currentVersionNumber
                };
                _content.version = OOCurrentVersion.currentVersionNumber;
                _content.channel = Hash.createChannelId();
                _content.ids = {};
                sframeChan.query('Q_SAVE_AS_TEMPLATE', {
                    toSave: JSON.stringify({
                        content: _content,
                        metadata: {
                            title: '',
                            defaultTitle: ev.title
                        }
                    }),
                    title: ev.title
                }, function () {
                    UI.alert(Messages.templateSaved);
                    Feedback.send('OO_TEMPLATE_CREATED');
                });
                return;
            }

            content.saveLock = undefined;
            if (err) {
                console.error(err);
                if (content.saveLock === myOOId) { delete content.saveLock; } // Unlock checkpoints
                if (APP.migrateModal) {
                    try { getEditor().asc_setRestriction(true); } catch (e) {}
                    setEditable(true);
                    delete content.migration;
                    APP.migrateModal.closeModal();
                    APP.onLocal();
                }
                if (isLockedModal.modal && err === "TOO_LARGE") {
                    if (APP.migrate) {
                        UI.warn(Messages.oo_cantMigrate);
                    }
                    APP.cantCheckpoint = true;
                    isLockedModal.modal.closeModal();
                    delete isLockedModal.modal;
                    if (content.saveLock === myOOId) {
                        delete content.saveLock;
                    }
                    APP.onLocal();
                    return;
                }
                return void UI.alert(Messages.oo_saveError);
            }
            // Get the last cp idx
            var all = sortCpIndex(content.hashes || {});
            var current = all[all.length - 1] || 0;

            var i = current + 1;
            content.hashes[i] = {
                file: data.url,
                hash: ev.hash,
                index: ev.index,
                version: OOCurrentVersion.currentVersionNumber
            };
            oldHashes = JSON.parse(JSON.stringify(content.hashes));
            content.locks = {};
            content.ids = {};
            // If this is a migration, set the new version
            if (APP.migrate) {
                delete content.migration;
                content.version = OOCurrentVersion.currentVersionNumber;
            }
            APP.onLocal();
            APP.realtime.onSettle(function () {
                UI.log(Messages.saved);
                APP.realtime.onSettle(function () {
                    if (APP.migrate) {
                        UI.removeModals();
                        UI.alert(Messages.oo_sheetMigration_complete, function () {
                            common.gotoURL();
                        });
                        return;
                    }
                    if (ev.callback) {
                        return void ev.callback();
                    }
                });
            });
            sframeChan.query('Q_OO_COMMAND', {
                cmd: 'UPDATE_HASH',
                data: ev.hash
            }, function (err, obj) {
                if (err || (obj && obj.error)) { console.error(err || obj.error); }
            });
        };

        var fmConfig = {
            noHandlers: true,
            noStore: true,
            body: $('body'),
            onUploaded: function (ev, data) {
                if (!data || !data.url) { return; }
                data.hash = ev.hash;
                sframeChan.query('Q_OO_SAVE', data, function (err) {
                    onUploaded(ev, data, err);
                });
            },
            onError: function (err) {
                onUploaded(null, null, err);
            }
        };
        APP.FM = common.createFileManager(fmConfig);

        var resetData = function (blob, type) {
            // If a read-only refresh popup was planned, abort it
            delete APP.refreshPopup;
            clearTimeout(APP.refreshRoTo);

            // Don't create the initial checkpoint indefinitely in a loop
            delete APP.initCheckpoint;

            if (!isLockedModal.modal) {
                isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
            }
            myUniqueOOId = undefined;
            setMyId();
            var editor = getEditor();
            if (editor) {
                var app = common.getMetadataMgr().getPrivateData().ooType;
                var d;
                if (app === 'doc') {
                    d = editor.GetDocument().Document;
                } else if (app === 'presentation') {
                    d = editor.GetPresentation().Presentation;
                }
                if (d) {
                    APP.oldCursor = d.GetSelectionState();
                }
            }
            if (APP.docEditor) { APP.docEditor.destroyEditor(); } // Kill the old editor
            $('iframe[name="frameEditor"]').after(h('div#cp-app-oo-placeholder-a')).remove();
            ooLoaded = false;
            oldLocks = {};
            Object.keys(pendingChanges).forEach(function (key) {
                clearTimeout(pendingChanges[key]);
                delete pendingChanges[key];
            });
            if (APP.stopHistory || APP.template) { APP.history = false; }
            startOO(blob, type, true);
        };

        var saveToServer = function (blob, title) {
            if (APP.cantCheckpoint) { return; } // TOO_LARGE
            var text = !blob && getContent();
            if (!text && !blob) {
                setEditable(false, true);
                sframeChan.query('Q_CLEAR_CACHE_CHANNELS', [
                    'chainpad',
                    content.channel,
                ], function () {});
                UI.alert(Messages.realtime_unrecoverableError, function () {
                    common.gotoURL();
                });
                return;
            }
            blob = blob || new Blob([text], {type: 'plain/text'});
            var file = getFileType();
            blob.name = title || (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type;
            var data = {
                hash: (APP.history || APP.template) ? ooChannel.historyLastHash : ooChannel.lastHash,
                index: (APP.history || APP.template) ? ooChannel.currentIndex : ooChannel.cpIndex
            };
            fixSheets();

            if (!isLockedModal.modal) {
                isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
            }
            ooChannel.ready = false;
            ooChannel.queue = [];
            data.callback = function () {
                if (APP.template) { APP.template = false; }
                resetData(blob, file);
            };

            APP.FM.handleFile(blob, data);
        };

        var noLogin = false;

        var makeCheckpoint = function (force) {
            if (APP.cantCheckpoint) { return; } // TOO_LARGE

            var locked = content.saveLock;
            var lastCp = getLastCp();

            var currentIdx = ooChannel.cpIndex;
            var needCp = force || (currentIdx - (lastCp.index || 0)) > FORCE_CHECKPOINT_INTERVAL;

            if (!needCp) { return; }

            if (!locked || !isUserOnline(locked) || force) {
                if (!common.isLoggedIn() && !isRegisteredUserOnline() && !noLogin) {
                    var login = h('button.cp-corner-primary', Messages.login_login);
                    var register = h('button.cp-corner-primary', Messages.login_register);
                    var cancel = h('button.cp-corner-cancel', Messages.cancel);
                    var actions = h('div', [cancel, register, login]);
                    var modal = UI.cornerPopup(Messages.oo_login, actions, '', {alt: true});
                    $(register).click(function () {
                        common.setLoginRedirect('register');
                        modal.delete();
                    });
                    $(login).click(function () {
                        common.setLoginRedirect('login');
                        modal.delete();
                    });
                    $(cancel).click(function () {
                        modal.delete();
                        noLogin = true;
                    });
                    return;
                }
                if (!common.isLoggedIn()) { return; }
                content.saveLock = myOOId;
                APP.onLocal();
                APP.realtime.onSettle(function () {
                    saveToServer();
                });
            }
        };
        var restoreLastCp = function () {
            content.saveLock = myOOId;
            APP.onLocal();
            APP.realtime.onSettle(function () {
                onUploaded({
                    hash: ooChannel.lastHash,
                    index: ooChannel.cpIndex
                }, {
                    url: getLastCp().file,
                });
            });
        };
        // Add a timeout to check if a checkpoint was correctly saved by the locking user
        // and "unlock the sheet" or "make a checkpoint" if needed
        var cpTo;
        var checkCheckpoint = function () {
            clearTimeout(cpTo);
            var saved = stringify(content.hashes);
            var locked = content.saveLock;
            var to = 20000 + (Math.random() * 20000);
            cpTo = setTimeout(function () {
                // If no checkpoint was added and the same user still has the lock
                // then make a checkpoint if needed (cp interval)
                if (stringify(content.hashes) === saved && locked === content.saveLock) {
                    content.saveLock = undefined;
                    makeCheckpoint();
                }
            }, to);
        };

        var loadInitDocument = function (type, useNewDefault) {
            var newText;
            switch (type) {
                case 'sheet' :
                    newText = EmptyCell(useNewDefault);
                    break;
                case 'doc':
                    newText = EmptyDoc();
                    break;
                case 'presentation':
                    newText = EmptySlide();
                    break;
                default:
                    newText = '';
            }
            return new Blob([newText], {type: 'text/plain'});
        };

        const loadLastDocument = function (lastCp) {
            return new Promise((resolve, reject) => {
                if (!lastCp || !lastCp.file) {
                    return void reject('EEMPTY');
                }
                ooChannel.cpIndex = lastCp.index || 0;
                ooChannel.lastHash = lastCp.hash;
                var parsed = Hash.parsePadUrl(lastCp.file);
                var secret = Hash.getSecrets('file', parsed.hash);
                if (!secret || !secret.channel) { return; }
                var hexFileName = secret.channel;
                var fileHost = privateData.fileHost || privateData.origin;
                var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
                var key = secret.keys && secret.keys.cryptKey;
                var xhr = new XMLHttpRequest();
                xhr.open('GET', src, true);
                if (window.sendCredentials) { xhr.withCredentials = true; }
                xhr.responseType = 'arraybuffer';
                xhr.onload = function () {
                    if (/^4/.test('' + this.status)) {
                        reject(this.status);
                        return void console.error('XHR error', this.status);
                    }
                    var arrayBuffer = xhr.response;
                    if (arrayBuffer) {
                        var u8 = new Uint8Array(arrayBuffer);
                        FileCrypto.decrypt(u8, key, function (err, decrypted) {
                            if (err) {
                                if (err === "DECRYPTION_ERROR" ||
                                err === "E_METADATA_DECRYPTION") {
                                    console.warn(err);
                                    return void reject(err);
                                }
                                return void console.error(err);
                            }
                            var blob = new Blob([decrypted.content], {type: 'plain/text'});
                            resolve({blob, fileType: getFileType()});
                        });
                    }
                };
                xhr.onerror = function (err) {
                    reject(err);
                };
                xhr.send(null);
            });
        };

        /*
        var refreshReadOnly = function () {
            var cancel = h('button.cp-corner-cancel', Messages.cancel);
            var reload = h('button.cp-corner-primary', [
                h('i.fa.fa-refresh'),
                Messages.oo_refresh
            ]);

            var actions = h('div', [cancel, reload]);
            var m = UI.cornerPopup(Messages.oo_refreshText, actions, '');
            $(reload).click(function () {
                ooChannel.ready = false;
                var lastCp = getLastCp();
                loadLastDocument(lastCp, function () {
                    var file = getFileType();
                    var type = common.getMetadataMgr().getPrivateData().ooType;
                    var blob = loadInitDocument(type, true);
                    resetData(blob, file);
                }, function (blob, file) {
                    resetData(blob, file);
                });
                delete APP.refreshPopup;
                m.delete();
            });
            $(cancel).click(function () {
                delete APP.refreshPopup;
                m.delete();
            });
        };
        */

        var openVersionHash = function (version) {
            readOnly = true;
            var hashes = content.hashes || {};
            var sortedCp = Object.keys(hashes).map(Number).sort(function (a, b) {
                return hashes[a].index - hashes[b].index;
            });
            var s = version.split('.');
            if (s.length !== 2) { return UI.errorLoadingScreen(Messages.error); }

            var major = Number(s[0]);
            var cpId = sortedCp[major - 1];
            var nextCpId = sortedCp[major];
            var cp = hashes[cpId] || {};

            var minor = Number(s[1]) + 1;
            if (APP.isDownload) { minor = undefined; }

            var toHash = cp.hash || 'NONE';
            var fromHash = nextCpId ? hashes[nextCpId].hash : 'NONE';

            sframeChan.query('Q_GET_HISTORY_RANGE', {
                channel: content.channel,
                lastKnownHash: fromHash,
                toHash: toHash,
                isDownload: APP.isDownload
            }, function (err, data) {
                if (err) { console.error(err); return void UI.errorLoadingScreen(Messages.error); }
                if (!Array.isArray(data.messages)) {
                    console.error('Not an array');
                    return void UI.errorLoadingScreen(Messages.error);
                }

                // The first "cp" in history is the empty doc. It doesn't include the first patch
                // of the history
                var initialCp = major === 0 || !cp.hash;
                var messages = (data.messages || []).slice(initialCp ? 0 : 1, minor);

                messages.forEach(function (obj) {
                    try { obj.msg = JSON.parse(obj.msg); } catch (e) { console.error(e); }
                });

                // The version exists if we have results in the "messages" array
                // or if we requested a x.0 version
                var exists = !Number(s[1]) || messages.length;
                var vHashEl;

                if (!privateData.embed) {
                    var vTime = (messages[messages.length - 1] || {}).time;
                    var vTimeStr = vTime ? new Date(vTime).toLocaleString()
                                         : 'v' + privateData.ooVersionHash;
                    var vTxt = Messages._getKey('infobar_versionHash',  [vTimeStr]);

                    // If we expected patched and we don't have any, it means this part
                    // of the history has been deleted
                    var vType = "warning";
                    if (!exists) {
                        vTxt = Messages.oo_deletedVersion;
                        vType = "danger";
                    }

                    vHashEl = h('div.alert.alert-'+vType+'.cp-burn-after-reading', vTxt);
                    $('#cp-app-oo-editor').prepend(vHashEl);
                }

                if (!exists) { return void UI.removeLoadingScreen(); }

                loadLastDocument(cp)
                    .then(({blob, fileType}) => {
                        ooChannel.queue = messages;
                        resetData(blob, fileType);
                        UI.removeLoadingScreen();
                    })
                    .catch(() => {
                        if (cp.hash && vHashEl) {
                            // We requested a checkpoint but we can't find it...
                            UI.removeLoadingScreen();
                            vHashEl.innerText = Messages.oo_deletedVersion;
                            $(vHashEl).removeClass('alert-warning').addClass('alert-danger');
                            return;
                        }
                        var file = getFileType();
                        var type = common.getMetadataMgr().getPrivateData().ooType;
                        if (APP.downloadType) { type = APP.downloadType; }
                        var blob = loadInitDocument(type, true);
                        ooChannel.queue = messages;
                        resetData(blob, file);
                        UI.removeLoadingScreen();
                    });
            });
        };

        var openRtChannel = function (cb) {
            if (rtChannel.ready) { return void cb(); }
            var chan = content.channel || Hash.createChannelId();
            if (!content.channel) {
                content.channel = chan;
                APP.onLocal();
            }
            sframeChan.query('Q_OO_OPENCHANNEL', {
                channel: content.channel,
                lastCpHash: getLastCp().hash
            }, function (err, obj) {
                if (err || (obj && obj.error)) { console.error(err || (obj && obj.error)); }
                // XXX an error loading a checkpoint was ignored, causing a sheet
                // to load incorrectly. There's a risk of a new checkpoint being created
                // with the resulting (incorrect) state. Errors like this should be reported
                // to the user so they realize something is wrong.
            });
            sframeChan.on('EV_OO_EVENT', function (obj) {
                switch (obj.ev) {
                    case 'READY':
                        checkClients(obj.data);
                        cb();
                        break;
                    case 'LEAVE':
                        removeClient(obj.data);
                        break;
                    case 'MESSAGE':
                        if (APP.history) {
                            ooChannel.historyLastHash = obj.data.hash;
                            ooChannel.currentIndex++;
                            return;
                        }
                        if (ooChannel.ready) {
                            // In read-only mode, push the message to the queue and prompt
                            // the user to refresh OO (without reloading the page)
                            /*if (readOnly) {
                                ooChannel.queue.push(obj.data);
                                if (APP.refreshPopup) { return; }
                                APP.refreshPopup = true;

                                // Don't "spam" the user instantly and no more than
                                // 1 popup every 15s
                                APP.refreshRoTo = setTimeout(refreshReadOnly, READONLY_REFRESH_TO);
                                return;
                            }*/
                            ooChannel.send(obj.data.msg);
                            ooChannel.lastHash = obj.data.hash;
                            ooChannel.cpIndex++;
                        } else {
                            ooChannel.queue.push(obj.data);
                        }
                        break;
                    case 'HISTORY_SYNCED':
                        if (typeof(APP.onHistorySynced) !== "function") { return; }
                        APP.onHistorySynced();
                        delete APP.onHistorySynced;
                        break;

                }
            });
        };

        const findUserByOOId = function(ooId) {
            return Object.values(content.ids)
                  .find((user) => user.ooid === ooId);
        };

        const getMyOOIndex = function() {
            const user = findUserByOOId(myOOId);
            return user ? user.index : READ_ONLY_INDEX_USER;
        };

        const getParticipants = function () {
            const users = metadataMgr.getMetadata().users;
            // Add an history keeper user to show that we're never alone
            var hkId = Util.createRandomInteger();
            const historyKeeper = [{
                id: String(hkId),
                idOriginal: String(hkId),
                username: "History",
                indexUser: HISTORY_KEEPER_INDEX_USER,
                connectionId: Hash.createChannelId(),
                isCloseCoAuthoring:false,
                view: false
            }];

            const realParticipants = Object.entries(content.ids).map(([id, user]) => {
                const nId = id.slice(0,32);
                const username = Util.find(privateData, ['integrationConfig', 'user', 'name']) ||
                       (users[nId] || {}).name || Messages.anonymous;

                return {
                    id: String(user.ooid) + user.index,
                    idOriginal: String(user.ooid),
                    username,
                    indexUser: user.index,
                    connectionId: user.netflux || Hash.createChannelId(),
                    isCloseCoAuthoring: false,
                    view: false
                };
            });

            const participants = historyKeeper.concat(realParticipants);

            return {
                index: getMyOOIndex(),
                list: participants,
            };
        };

        // Get all existing locks
        var getUserLock = function (id, forceArray) {
            var type = common.getMetadataMgr().getPrivateData().ooType;
            content.locks = content.locks || {};
            var l = content.locks[id] || {};
            if (type === "sheet" || forceArray) {
                return Object.keys(l).map(function (uid) { return l[uid]; });
            }
            var res = {};
            Object.keys(l).forEach(function (uid) {
                res[uid] = l[uid];
            });
            return res;
        };
        var getLock = function () {
            var type = common.getMetadataMgr().getPrivateData().ooType;
            var locks = [];
            if (type === "sheet") {
                Object.keys(content.locks || {}).forEach(function (id) {
                    Array.prototype.push.apply(locks, getUserLock(id));
                });
                return locks;
            }
            locks = {};
            Object.keys(content.locks || {}).forEach(function (id) {
                Util.extend(locks, getUserLock(id));
            });
            return locks;
        };

        // Update the userlist in onlyoffice
        var handleNewIds = function (o, n) {
            if (stringify(o) === stringify(n)) { return; }
            var p = getParticipants();
            ooChannel.send({
                type: "connectState",
                participantsTimestamp: +new Date(),
                participants: p.list,
                waitAuth: false
            });
        };
        // Update the locks status in onlyoffice
        var handleNewLocks = function (o, n) {
            var hasNew = false;
            // Check if we have at least one new lock
            Object.keys(n || {}).some(function (id) {
                if (typeof(n[id]) !== "object") { return; } // Ignore old format
                // n[id] = { uid: lock, uid2: lock2 };
                return Object.keys(n[id]).some(function (uid) {
                    // New lock
                    if (!o[id] || !o[id][uid]) {
                        hasNew = true;
                        return true;
                    }
                });
            });
            // Remove old locks
            Object.keys(o || {}).forEach(function (id) {
                if (typeof(o[id]) !== "object") { return; } // Ignore old format
                Object.keys(o[id]).forEach(function (uid) {
                    // Removed lock
                    if (!n[id] || !n[id][uid]) {
                        ooChannel.send({
                            type: "releaseLock",
                            locks: [o[id][uid]]
                        });
                    }
                });
            });
            if (hasNew) {
                ooChannel.send({
                    type: "getLock",
                    locks: getLock()
                });
            }
        };

        // Remove locks from offline users
        var deleteOfflineLocks = function () {
            var locks = content.locks || {};
            var users = Object.keys(metadataMgr.getMetadata().users);
            Object.keys(locks).forEach(function (id) {
                var nId = id.slice(0,32);
                if (users.indexOf(nId) === -1) {
                    // Offline locks: support old format
                    var l = (locks[id] && !locks[id].block) ? getUserLock(id) : [locks[id]];
                    ooChannel.send({
                        type: "releaseLock",
                        locks: l
                    });
                    delete content.locks[id];
                }
            });
            if (content.saveLock && !isUserOnline(content.saveLock)) {
                delete content.saveLock;
            }
        };

        const getInitialChanges = function() {
            const changes = [];
            if (content.version > 2) {
                ooChannel.queue.forEach(function (data) {
                    Array.prototype.push.apply(changes, data.msg.changes);
                });
                ooChannel.ready = true;

                ooChannel.cpIndex += ooChannel.queue.length;
                var last = ooChannel.queue.pop();
                if (last) { ooChannel.lastHash = last.hash; }
            }
            return changes;
        };

        const onAuth = function () {
            if (content.version <= 2) {
                setEditable(false, true);
            }
            console.log("OO loading");
        };


        var handleAuth = function (obj, send) {
            //setEditable(false);

            var changes = [];
            if (content.version > 2) {
                ooChannel.queue.forEach(function (data) {
                    Array.prototype.push.apply(changes, data.msg.changes);
                });
                ooChannel.ready = true;

                ooChannel.cpIndex += ooChannel.queue.length;
                var last = ooChannel.queue.pop();
                if (last) { ooChannel.lastHash = last.hash; }
            } else {
                setEditable(false, true);
            }
            send({
                type: "authChanges",
                changes: changes
            });

            // Answer to the auth command
            var p = getParticipants();
            send({
                type: "auth",
                result: 1,
                sessionId: "sessionId",
                participants: p.list,
                locks: [],
                changes: [],
                changesIndex: 0,
                indexUser: p.index,
                buildVersion: "5.2.6",
                buildNumber: 2,
                licenseType: 3,
                //"g_cAscSpellCheckUrl": "/spellchecker",
                //"settings":{"spellcheckerUrl":"/spellchecker","reconnection":{"attempts":50,"delay":2000}}
            });
            // Open the document
            send({
                type: "documentOpen",
                data: {"type":"open","status":"ok","data":{"Editor.bin":obj.openCmd.url}}
            });

            /*
            // TODO: make sure we don't have new popups that can break our integration
            var observer = new MutationObserver(function(mutations) {
                mutations.forEach(function(mutation) {
                    if (mutation.type === "childList") {
                        for (var i = 0; i < mutation.addedNodes.length; i++) {
                            if (mutation.addedNodes[i].classList.contains('asc-window') &&
                                mutation.addedNodes[i].classList.contains('alert')) {
                                $(mutation.addedNodes[i]).find('button').not('.custom').click();
                            }
                        }
                    }
                });
            });
            observer.observe(window.frames[0].document.body, {
                childList: true,
            });
            */
        };
        
        var handleLock = function (obj, send) {
            if (APP.history) { return; }

            if (content.saveLock) {
                if (!isLockedModal.modal) {
                    isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
                }
                setTimeout(function () {
                    handleLock(obj, send);
                }, 50);
                return;
            }
            var type = common.getMetadataMgr().getPrivateData().ooType;
            var b = obj.block && obj.block[0];
            var msg = {
                time: now(),
                user: myUniqueOOId,
                block: b
            };

            var editor = getEditor();
            if (type === "presentation" && (APP.themeChanged || APP.themeRemote) && b &&
                b.guid === editor.GetPresentation().Presentation.themeLock.Id) {
                APP.themeLocked = APP.themeChanged;
                APP.themeChanged = undefined;
                var fakeLocks = getLock();
                fakeLocks[Util.uid()] = msg;
                send({
                    type: "getLock",
                    locks: fakeLocks
                });
                return;
            }

            content.locks = content.locks || {};
            // Send the lock to other users
            var myId = getId();
            content.locks[myId] = content.locks[myId] || {};
            if (type === "sheet" || typeof(b) !== "string") {
                var uid = Util.uid();
                content.locks[myId][uid] = msg;
            } else {
                if (typeof(b) === "string") { content.locks[myId][b] = msg; }
            }
            oldLocks = JSON.parse(JSON.stringify(content.locks));
            // Remove old locks
            deleteOfflineLocks();

            // Prepare callback
            if (cpNfInner) {
                var waitLock = APP.waitLock = Util.mkEvent(true);
                setTimeout(function () {
                    // Make sure the waitLock is never stuck
                    waitLock.fire();
                    if (waitLock === APP.waitLock) { delete APP.waitLock; }
                }, 5000);
                var onPatchSent = function (again) {
                    if (!again) { cpNfInner.offPatchSent(onPatchSent); }
                    // Answer to our onlyoffice
                    if (!content.saveLock) {
                        if (isLockedModal.modal) {
                            isLockedModal.modal.closeModal();
                            delete isLockedModal.modal;
                            if (!APP.history) {
                                $('#cp-app-oo-editor > iframe')[0].contentWindow.focus();
                            }
                        }
                        send({
                            type: "getLock",
                            locks: getLock()
                        });
                        waitLock.fire();
                        if (waitLock === APP.waitLock) { delete APP.waitLock; }
                    } else {
                        if (!isLockedModal.modal) {
                            isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
                        }
                        setTimeout(function () {
                            onPatchSent(true);
                        }, 50);
                    }
                };
                cpNfInner.onPatchSent(onPatchSent);
            }
            // Commit
            APP.onLocal();
            APP.realtime.sync();
        };

        var parseChanges = function (changes, isObj) {
            try {
                changes = JSON.parse(changes);
            } catch (e) {
                return [];
            }
            return changes.map(function (change) {
                return {
                    docid: "fresh",
                    change: isObj ? change : '"' + change + '"',
                    time: now(),
                    user: myUniqueOOId,
                    useridoriginal: String(myOOId)
                };
            });
        };

        var handleChanges = function (obj, send) {
            if (APP.history) {
                send({
                    type: "unSaveLock",
                    index: ooChannel.cpIndex,
                    time: +new Date()
                });
                return;
            }
            // Add a new entry to the pendingChanges object.
            // If we can't send the patch within 30s, force a page reload
            var uid = Util.uid();
            pendingChanges[uid] = setTimeout(function () {
                // If we're offline, force a reload on reconnect
                if (offline) {
                    pendingChanges.force = true;
                    return;
                }

                // We're online: force a reload now
                setEditable(false);
                UI.alert(Messages.realtime_unrecoverableError, function () {
                    common.gotoURL();
                });

            }, PENDING_TIMEOUT);
            if (offline) {
                pendingChanges.force = true;
                return;
            }

            var changes = obj.changes;
            if (obj.type === "cp_theme") {
                changes = JSON.stringify([JSON.stringify(obj)]);
            }

            // Send the changes
            content.locks = content.locks || {};
            rtChannel.sendMsg({
                type: "saveChanges",
                changes: parseChanges(changes, obj.type === "cp_theme"),
                changesIndex: ooChannel.cpIndex || 0,
                locks: getUserLock(getId(), true),
                excelAdditionalInfo: obj.excelAdditionalInfo,
                startSaveChanges: obj.startSaveChanges,
                endSaveChanges: obj.endSaveChanges
            }, null, function (err, hash) {
                if (err) {
                    return void console.error(err);
                }

                if (pendingChanges[uid]) {
                    clearTimeout(pendingChanges[uid]);
                    delete pendingChanges[uid];
                }

                // If endSaveChanges is false, it means the patch is split into
                // several set of changes and this is only a part of it. The last
                // part will have this value to "true"
                if (!obj.endSaveChanges) {
                    send({
                        type: "savePartChanges",
                        changesIndex: -1,
                        time: +new Date()
                    });
                } else {
                    // Call unSaveLock to tell onlyoffice that the patch was sent.
                    // It will allow you to make changes to another cell.
                    // If there is an error and unSaveLock is not called, onlyoffice
                    // will try to send the patch again
                    send({
                        type: "unSaveLock",
                        index: ooChannel.cpIndex,
                        time: +new Date()
                    });
                }
                // Increment index and update latest hash
                ooChannel.cpIndex++;
                ooChannel.lastHash = hash;
                // Check if a checkpoint is needed
                makeCheckpoint();
                // Remove my locks
                delete content.locks[getId()];
                oldLocks = JSON.parse(JSON.stringify(content.locks));
                APP.onLocal();
            });
        };

        APP.testArr = [
            ['a','b',1,'d'],
            ['e',undefined,'g','h']
        ];
        var makePatch = APP.makePatch = function (arr) {
            var w = getWindow();
            if (!w) { return; }
            // Define OO classes
            var AscCommonExcel = w.AscCommonExcel;
            var CellValueData = AscCommonExcel.UndoRedoData_CellValueData;
            var CCellValue = AscCommonExcel.CCellValue;
            //var History = w.AscCommon.History;
            var AscCH = w.AscCH;
            var Asc = w.Asc;
            var UndoRedoData_CellSimpleData = AscCommonExcel.UndoRedoData_CellSimpleData;
            var editor = getEditor();

            var Id = editor.GetSheet(0).worksheet.Id;
            //History.Create_NewPoint();
            var patches = [];
            arr.forEach(function (arr2, i) {
                arr2.forEach(function (v, j) {
                    var obj = {};
                    if (typeof(v) === "string") { obj.text = v; obj.type = 1; }
                    else if (typeof(v) === "number") { obj.number = v; obj.type = 0; }
                    else { return; }
                    var newValue = new CellValueData(undefined, new CCellValue(obj));
                    var nCol = j;
                    var nRow = i;
                    var patch = new AscCommonExcel.UndoRedoItemSerializable(AscCommonExcel.g_oUndoRedoCell, AscCH.historyitem_Cell_ChangeValue, Id,
                        new Asc.Range(nCol, nRow, nCol, nRow),
                        new UndoRedoData_CellSimpleData(nRow, nCol, undefined, newValue), undefined);
                    patches.push(patch);
                    /*
                    History.Add(AscCommonExcel.g_oUndoRedoCell, AscCH.historyitem_Cell_ChangeValue, Id,
                        new Asc.Range(nCol, nRow, nCol, nRow),
                        new UndoRedoData_CellSimpleData(nRow, nCol, undefined, newValue), undefined, true);
                    */
                });
            });
            var oMemory = new w.AscCommon.CMemory();
            var aRes = [];
            patches.forEach(function (item) {
                editor.GetSheet(0).worksheet.workbook._SerializeHistory(oMemory, item, aRes);
            });

            // Make the patch
            var msg = {
                type: "saveChanges",
                changes: parseChanges(JSON.stringify(aRes)),
                changesIndex: ooChannel.cpIndex || 0,
                startSaveChanges: true,
                endSaveChanges: true,
                isExcel: true,
                deleteIndex: null,
                unlock: false,
                releaseLocks: true,
                reSave: true,
                isCoAuthoring: true,
                locks: getUserLock(getId(), true),
                excelAdditionalInfo: null
            };

            // Send the patch
            rtChannel.sendMsg(msg, null, function (err, hash) {
                if (err) {
                    return void console.error(err);
                }
                // Apply it on our side
                ooChannel.send(msg);
                ooChannel.lastHash = hash;
                ooChannel.cpIndex++;
            });
        };

        const send = ooChannel.send = function (obj, force) {
            // can't push to OO before reloading cp
            if (APP.onStrictSaveChanges && !force) { return; }
            // We only need to release locks for sheets
            const type = common.getMetadataMgr().getPrivateData().ooType;
            if (type !== "sheet" && obj.type === "releaseLock") { return; }
            if (type === "presentation" && obj.type === "cp_theme") {
                console.error(obj);
                return;
            }

            debug(obj, 'toOOClient');
            APP.docEditor.sendMessageToOO(obj);
            if (obj && obj.type === "saveChanges") {
                evIntegrationSave.fire();
            }
        };

        const fromOOHandler = function (obj) {
            debug(obj, 'fromOOClient');
            switch (obj.type) {
                case "auth":
                    // Handled by onlyoffice-editor now
                    break;
                case "isSaveLock":
                    // TODO ping the server to check if we're online first?
                    if (!offline) {
                        if (APP.waitLock) {
                            APP.waitLock.reg(function () {
                                send({
                                    type: "saveLock",
                                    saveLock: false
                                }, true);
                            });
                        } else {
                            send({
                                type: "saveLock",
                                saveLock: false
                            }, true);
                        }
                    }
                    break;
                case "cursor":
                    if (cursor && cursor.updateCursor) {
                        cursor.updateCursor({
                            type: "cursor",
                            messages: [{
                                cursor: obj.cursor,
                                time: +new Date(),
                                user: myUniqueOOId,
                                useridoriginal: myOOId
                            }]
                        });
                    }
                    break;
                case "forceSaveStart":
                    if (APP.integrationSave) {
                        APP.integrationSave(obj => {
                            if (obj?.error) {
                                console.error(obj.error);
                                return void UI.warn(Messages.error);
                            }
                            content.integrationSave = `${myUniqueOOId}-${+new Date()}`;
                            APP.integrationSaved = content.integrationSave;
                            APP.onLocal();
                            UI.log(Messages.saved);
                        });
                    }
                    break;
                case "getLock":
                    handleLock(obj, send);
                    break;
                case "getMessages":
                    // OO chat messages?
                    send({ type: "message" });
                    break;
                case "saveChanges":
                    // If we have unsaved data before reloading for a checkpoint...
                    if (APP.onStrictSaveChanges) {
                        const type = common.getMetadataMgr().getPrivateData().ooType;
                        delete APP.unsavedLocks;
                        APP.unsavedChanges = {
                            type: "saveChanges",
                            changes: parseChanges(obj.changes),
                            changesIndex: ooChannel.cpIndex || 0,
                            locks: type === "sheet" ? [] : APP.unsavedLocks,
                            excelAdditionalInfo: null,
                            recover: true
                        };
                        APP.onStrictSaveChanges();
                        return;
                    }
                    var AscCommon = window.frames[0] && window.frames[0].AscCommon;
                    if (Util.find(AscCommon, ['CollaborativeEditing','m_bFast'])
                                && APP.themeLocked) {
                        obj = APP.themeLocked;
                        APP.themeLocked = undefined;
                        obj.type = "cp_theme";
                        console.error(obj);
                    }
                    if (APP.themeRemote) {
                        delete APP.themeRemote;
                        send({
                            type: "unSaveLock",
                            index: ooChannel.cpIndex,
                            time: +new Date()
                        });
                        return;
                    }

                    // We're sending our changes to netflux
                    handleChanges(obj, send);
                    // If we're alone, clean up the medias
                    var m = metadataMgr.getChannelMembers().slice().filter(function (nId) {
                        return nId.length === 32;
                    });
                    if (m.length === 1 && APP.loadingImage <= 0) {
                        try {
                            // "docs" contains the correct images that we've just uploaded
                            // "docs2" contains the correct images from the .bin checkpoint
                            // both of them are not reliable in the other case
                            var docs = getWindow().AscCommon.g_oDocumentUrls.urls;
                            var docs2 = getEditor().ImageLoader.map_image_index;
                            var mediasSources = getMediasSources();
                            Object.keys(mediasSources).forEach(function (name) {
                                if (!docs && !docs2) { return; }
                                if (!docs['media/'+name] && !docs2[name]) {
                                    delete mediasSources[name];
                                }
                            });
                            APP.onLocal();
                        } catch (e) {}
                    }
                    break;
                case "unLockDocument":
                    if (obj.releaseLocks && content.locks && content.locks[getId()]) {
                        send({
                            type: "releaseLock",
                            locks: getUserLock(getId())
                        });
                        delete content.locks[getId()];
                        APP.onLocal();
                    }
                    if (obj.isSave) {
                        send({
                            type: "unSaveLock",
                            time: -1,
                            index: -1
                        });
                    }
                    if (APP.onDocumentUnlock) {
                        APP.onDocumentUnlock();
                        APP.onDocumentUnlock = undefined;
                    }
                    break;
                case 'openDocument':
                    // When duplicating a slide, OO may ask the URLs of the images
                    // in that slide
                    var _obj = obj.message;
                    if (_obj.c === "imgurls") {
                        var _mediasSources = getMediasSources();
                        var images = _obj.data || [];
                        if (!Array.isArray(images)) { return; }
                        var urls = [];
                        nThen(function (waitFor) {
                            images.forEach(function (name) {
                                if (/^data\:image/.test(name)) {
                                    Util.fetch(name, waitFor(function (err, u8) {
                                        if (err) { return; }
                                        var b = new Blob([u8]);
                                        urls.push(URL.createObjectURL(b));
                                    }));
                                    return;
                                }
                                var data = _mediasSources[name];
                                if (!data) { return; }
                                var media = mediasData[data.src];
                                if (!media) { return; }
                                urls.push({
                                    path: name,
                                    url: media.blobUrl,
                                });
                            });
                        }).nThen(function () {
                            send({
                                type: "documentOpen",
                                data: {
                                    type: "imgurls",
                                    status: "ok",
                                    data: {
                                        urls: urls,
                                        error: 0
                                    }
                                }
                            });
                        });
                    }
                    break;
            }
        };


        var makeChannel = function () {
            var msgEv = Util.mkEvent();
            var iframe = $('#cp-app-oo-editor > iframe')[0].contentWindow;
            var type = common.getMetadataMgr().getPrivateData().ooType;
            window.addEventListener('message', function (msg) {
                if (msg.source !== iframe) { return; }
                msgEv.fire(msg);
            });
            var postMsg = function (data) {
                iframe.postMessage(data, ApiConfig.httpSafeOrigin);
            };
            Channel.create(msgEv, postMsg, function (chan) {
                APP.chan = chan;

                var send = ooChannel.send = function (obj, force) {
                    // can't push to OO before reloading cp
                    if (APP.onStrictSaveChanges && !force) { return; }
                    // We only need to release locks for sheets
                    if (type !== "sheet" && obj.type === "releaseLock") { return; }
                    if (type === "presentation" && obj.type === "cp_theme") {
                        console.error(obj);
                        return;
                    }

                    debug(obj, 'toOO');
                    chan.event('CMD', obj);
                    if (obj && obj.type === "saveChanges") {
                        evIntegrationSave.fire();
                    }
                };

                chan.on('CMD', function (obj) {
                    debug(obj, 'fromOO');
                    switch (obj.type) {
                        case "auth":
                            handleAuth(obj, send);
                            break;
                        case "isSaveLock":
                            // TODO ping the server to check if we're online first?
                            if (!offline) {
                                if (APP.waitLock) {
                                    APP.waitLock.reg(function () {
                                        send({
                                            type: "saveLock",
                                            saveLock: false
                                        }, true);
                                    });
                                } else {
                                    send({
                                        type: "saveLock",
                                        saveLock: false
                                    }, true);
                                }
                            }
                            break;
                        case "cursor":
                            if (cursor && cursor.updateCursor) {
                                cursor.updateCursor({
                                    type: "cursor",
                                    messages: [{
                                        cursor: obj.cursor,
                                        time: +new Date(),
                                        user: myUniqueOOId,
                                        useridoriginal: myOOId
                                    }]
                                });
                            }
                            break;
                        case "forceSaveStart":
                            if (APP.integrationSave) {
                                APP.integrationSave(obj => {
                                    if (obj?.error) {
                                        console.error(obj.error);
                                        return void UI.warn(Messages.error);
                                    }
                                    content.integrationSave = `${myUniqueOOId}-${+new Date()}`;
                                    APP.integrationSaved = content.integrationSave;
                                    APP.onLocal();
                                    UI.log(Messages.saved);
                                });
                            }
                            break;
                        case "getLock":
                            handleLock(obj, send);
                            break;
                        case "getMessages":
                            // OO chat messages?
                            send({ type: "message" });
                            break;
                        case "saveChanges":
                            if (readOnly) {
                                return;
                            }

                            // If we have unsaved data before reloading for a checkpoint...
                            if (APP.onStrictSaveChanges) {
                                delete APP.unsavedLocks;
                                APP.unsavedChanges = {
                                    type: "saveChanges",
                                    changes: parseChanges(obj.changes),
                                    changesIndex: ooChannel.cpIndex || 0,
                                    locks: type === "sheet" ? [] : APP.unsavedLocks,
                                    excelAdditionalInfo: null,
                                    recover: true
                                };
                                APP.onStrictSaveChanges();
                                return;
                            }
                            var AscCommon = window.frames[0] && window.frames[0].AscCommon;
                            if (Util.find(AscCommon, ['CollaborativeEditing','m_bFast'])
                                        && APP.themeLocked) {
                                obj = APP.themeLocked;
                                APP.themeLocked = undefined;
                                obj.type = "cp_theme";
                                console.error(obj);
                            }
                            if (APP.themeRemote) {
                                delete APP.themeRemote;
                                send({
                                    type: "unSaveLock",
                                    index: ooChannel.cpIndex,
                                    time: +new Date()
                                });
                                return;
                            }

                            // We're sending our changes to netflux
                            handleChanges(obj, send);
                            // If we're alone, clean up the medias
                            var m = metadataMgr.getChannelMembers().slice().filter(function (nId) {
                                return nId.length === 32;
                            });
                            if (m.length === 1 && APP.loadingImage <= 0) {
                                try {
                                    // "docs" contains the correct images that we've just uploaded
                                    // "docs2" contains the correct images from the .bin checkpoint
                                    // both of them are not reliable in the other case
                                    var docs = getWindow().AscCommon.g_oDocumentUrls.urls;
                                    var docs2 = getEditor().ImageLoader.map_image_index;
                                    var mediasSources = getMediasSources();
                                    Object.keys(mediasSources).forEach(function (name) {
                                        if (!docs && !docs2) { return; }
                                        if (!docs['media/'+name] && !docs2[name]) {
                                            delete mediasSources[name];
                                        }
                                    });
                                    APP.onLocal();
                                } catch (e) {}
                            }
                            break;
                        case "unLockDocument":
                            if (obj.releaseLocks && content.locks && content.locks[getId()]) {
                                send({
                                    type: "releaseLock",
                                    locks: getUserLock(getId())
                                });
                                delete content.locks[getId()];
                                APP.onLocal();
                            }
                            if (obj.isSave) {
                                send({
                                    type: "unSaveLock",
                                    time: -1,
                                    index: -1
                                });
                            }
                            if (APP.onDocumentUnlock) {
                                APP.onDocumentUnlock();
                                APP.onDocumentUnlock = undefined;
                            }
                            break;
                        case 'openDocument':
                            // When duplicating a slide, OO may ask the URLs of the images
                            // in that slide
                            var _obj = obj.message;
                            if (_obj.c === "imgurls") {
                                var _mediasSources = getMediasSources();
                                var images = _obj.data || [];
                                if (!Array.isArray(images)) { return; }
                                var urls = [];
                                nThen(function (waitFor) {
                                    images.forEach(function (name) {
                                        if (/^data\:image/.test(name)) {
                                            Util.fetch(name, waitFor(function (err, u8) {
                                                if (err) { return; }
                                                var b = new Blob([u8]);
                                                urls.push(URL.createObjectURL(b));
                                            }));
                                            return;
                                        }
                                        var data = _mediasSources[name];
                                        if (!data) { return; }
                                        var media = mediasData[data.src];
                                        if (!media) { return; }
                                        urls.push({
                                            path: name,
                                            url: media.blobUrl,
                                        });
                                    });
                                }).nThen(function () {
                                    send({
                                        type: "documentOpen",
                                        data: {
                                            type: "imgurls",
                                            status: "ok",
                                            data: {
                                                urls: urls,
                                                error: 0
                                            }
                                        }
                                    });
                                });
                            }
                            break;
                    }
                });
            });
        };

        
        var x2tConvertData = function (data, fileName, format, cb) {
            var sframeChan = common.getSframeChannel();
            var editor = getEditor();
            var fonts = editor && editor.FontLoader.fontInfos;
            var files = editor && editor.FontLoader.fontFiles.map(function (f) {
                return { 'Id': f.Id, };
            });
            var type = common.getMetadataMgr().getPrivateData().ooType;
            const images = editor
                ? structuredClone(window.frames[0].AscCommon.g_oDocumentUrls.getUrls())
                : {};

            // Fix race condition which could drop images sometimes
            // ==> make sure each image has a 'media/image_name.ext' entry as well
            Object.keys(images).forEach(function (img) {
                if (/^media\//.test(img)) { return; }
                if (images['media/'+img]) { return; }
                images['media/'+img] = images[img];
            });

            // Add theme images
            var theme = editor && window.frames[0].AscCommon.g_image_loader.map_image_index;
            if (theme) {
                Object.keys(theme).forEach(function (url) {
                    if (!/^(\/|blob:|data:)/.test(url)) {
                        images[url] = url;
                    }
                });
            }

            sframeChan.query('Q_OO_CONVERT', {
                data: data,
                type: type,
                fileName: fileName,
                outputFormat: format,
                images: (editor && window.frames[0].AscCommon.g_oDocumentUrls.urls) || {},
                fonts: fonts,
                fonts_files: files,
                mediasSources: getMediasSources(),
                mediasData: mediasData
            }, function (err, obj) {
                if (err || !obj || !obj.data) {
                    UI.alert(Messages.oo_couldNotConvertDocument, cb);
                    return;
                }
                cb(obj.data, obj.images);
            }, {
                raw: true
            });
        };

        // When download a sheet from the drive, we must wait for all the images
        // to be downloaded and decrypted before converting to xlsx
        var downloadImages = {};

        const onAppReady = function() {
            APP.docEditor.getIframe().setAttribute('tabindex', '-1');
            var css = // Old OO
                        //'#id-toolbar-full .toolbar-group:nth-child(2), #id-toolbar-full .separator:nth-child(3) { display: none; }' +
                        //'#fm-btn-save { display: none !important; }' +
                        //'#panel-settings-general tr.autosave { display: none !important; }' +
                        //'#panel-settings-general tr.coauth { display: none !important; }' +
                        //'#header { display: none !important; }' +
                        '#title-doc-name { display: none !important; }' +
                        '#title-user-name { display: none !important; }' +
    (supportsXLSX() ? '' : '#slot-btn-dt-print { display: none !important; }') +
                        // New OO:
                        'section[data-tab="ins"] .separator:nth-last-child(2) { display: none !important; }' + // separator
                        '#slot-btn-insequation { display: none !important; }' + // Insert equation
                        //'#asc-gen125 { display: none !important; }' + // Disable presenter mode
                        //'.toolbar .tabs .ribtab:not(.canedit) { display: none !important; }' + // Switch collaborative mode
                        '#fm-btn-info { display: none !important; }' + // Author name, doc title, etc. in "File" (menu entry)
                        '#panel-info { display: none !important; }' + // Same but content
                        '#image-button-from-url { display: none !important; }' + // Inline image settings: replace with url
                        '.cp-from-url, #textart-button-from-url { display: none !important; }' + // Spellcheck language
                        '.statusbar .cnt-lang { display: none !important; }' + // Spellcheck language
                        '.statusbar #btn-doc-spell { display: none !important; }' + // Spellcheck button
                        '#file-menu-panel .devider { display: none !important; }' + // separator in the "File" menu
                        '#left-btn-spellcheck, #left-btn-about { display: none !important; }'+
                        'div.btn-users.dropdown-toggle { display: none; !important }';
            if (readOnly) {
                css += '#toolbar { display: none !important; }';
                //css += '#app-title { display: none !important; }'; // OnlyOffice logo + doc title
                //css += '#file-menu-panel { top: 28px !important; }'; // Position of the "File" menu
            }
            APP.docEditor.injectCSS(css);
            setTimeout(function () {
                $(window).trigger('resize');
            });
            if (UI.findOKButton().length) {
                UI.findOKButton().on('focusout', function () {
                    window.setTimeout(function () { UI.findOKButton().focus(); });
                });
            }
        };

        const onError = function() {
            console.error(arguments);
            if (APP.isDownload) {
                var sframeChan = common.getSframeChannel();
                sframeChan.event('EV_OOIFRAME_DONE', '');
            }
        };

        const onDocumentReady = function(lock, lang, fromContent, file, force) {
            evOnSync.fire();
            var onMigrateRdy = Util.mkEvent();
            onMigrateRdy.reg(function () {
                var div = h('div.cp-oo-x2tXls', [
                    h('span.fa.fa-spin.fa-spinner'),
                    h('span', Messages.oo_sheetMigration_loading)
                ]);
                APP.migrateModal = UI.openCustomModal(UI.dialog.customModal(div, {buttons: []}));
                makeCheckpoint(true);
            });
            // DEPRECATED: from version 3, the queue is sent again during init
            if (APP.migrate && ((content.version || 1) <= 2)) {
                // The doc is ready, fix the worksheets IDs and push the queue
                fixSheets();
                // Push changes since last cp
                ooChannel.ready = true;
                var changes = [];
                var changesIndex;
                ooChannel.queue.forEach(function (data) {
                    Array.prototype.push.apply(changes, data.msg.changes);
                    changesIndex = data.msg.changesIndex;
                    //ooChannel.send(data.msg);
                });
                ooChannel.cpIndex += ooChannel.queue.length;
                var last = ooChannel.queue.pop();
                if (last) { ooChannel.lastHash = last.hash; }

                var onDocUnlock = function () {
                    // Migration required but read-only: continue...
                    if (readOnly) {
                        setEditable(true);
                        try { getEditor().asc_setRestriction(true); } catch (e) {}
                    } else {
                        // No changes after the cp: migrate now
                        onMigrateRdy.fire();
                    }
                };

                // Send the changes all at once
                if (changes.length) {
                    setTimeout(function () {
                        ooChannel.send({
                            type: 'saveChanges',
                            changesIndex: changesIndex,
                            changes: changes,
                            locks: []
                        });
                        APP.onDocumentUnlock = onDocUnlock;
                    }, 5000);
                    return;
                }
                onDocUnlock();
                return;
            }

            if (lock || readOnly) {
                try { getEditor().asc_setRestriction(true); } catch (e) {}
                //getEditor().setViewModeDisconnect(); // can't be used anymore, display an OO error popup
            } else {
                setEditable(true);
                deleteOfflineLocks();
                handleNewLocks({}, content.locks);
                if (APP.unsavedChanges) {
                    var unsaved = APP.unsavedChanges;
                    delete APP.unsavedChanges;
                    rtChannel.sendMsg(unsaved, null, function (err, hash) {
                        if (err) { return void UI.alert(Messages.oo_lostEdits); }
                        // This is supposed to be a "send" function to tell our OO
                        // to unlock the cell. We use this to know that the patch was
                        // correctly sent so that we can apply it to our OO too.
                        ooChannel.send(unsaved);
                        ooChannel.cpIndex++;
                        ooChannel.lastHash = hash;
                    });
                }

                if (APP.startNew) {
                    var w = getWindow();
                    if (lang === "fr") { lang = 'fr-fr'; }
                    var l = w.Common.util.LanguageInfo.getLocalLanguageCode(lang);
                    getEditor().asc_setDefaultLanguage(l);
                }

                if (APP.oldCursor) {
                    var app = common.getMetadataMgr().getPrivateData().ooType;
                    var d;
                    if (app === 'doc') {
                        d = getEditor().GetDocument().Document;
                    } else if (app === 'presentation') {
                        d = getEditor().GetPresentation().Presentation;
                    }
                    if (d) {
                        d.SetSelectionState(APP.oldCursor);
                        d.UpdateSelection();
                    }
                    delete APP.oldCursor;
                }
                if (integrationChannel) {
                    APP.onDocumentUnlock = () => {
                        integrationChannel.event('EV_INTEGRATION_READY');
                    };
                }
            }
            delete APP.startNew;

            if (fromContent && !lock && Array.isArray(fromContent.content)) {
                makePatch(fromContent.content);
            }

            if (APP.isDownload) {
                delete APP.isDownload;
                var bin = getContent();
                if (!supportsXLSX()) {
                    return void sframeChan.event('EV_OOIFRAME_DONE', bin, {raw: true});
                }
                nThen(function (waitFor) {
                    // wait for all the images to be loaded before converting
                    Object.keys(downloadImages).forEach(function (name) {
                        downloadImages[name].reg(waitFor());
                    });
                }).nThen(function () {
                    x2tConvertData(bin, 'filename.bin', file.type, function (xlsData) {
                        sframeChan.event('EV_OOIFRAME_DONE', xlsData, {raw: true});
                    });
                });
                return;
            }


            if (isLockedModal.modal && force) {
                isLockedModal.modal.closeModal();
                delete isLockedModal.modal;
                if (!APP.history) {
                    $('#cp-app-oo-editor > iframe')[0].contentWindow.focus();
                }
            }

            if (APP.template) {
                try { getEditor().asc_setRestriction(true); } catch (e) {}
                //getEditor().setViewModeDisconnect();
                UI.removeLoadingScreen();
                makeCheckpoint(true);
                return;
            }

            APP.onLocal(); // Add our data to the userlist

            if (APP.history) {
                try {
                    getEditor().asc_setRestriction(true);
                } catch (e) {}
            }

            if (lock && !readOnly) { // Lock = !history && migrate
                onMigrateRdy.fire();
            }

            if (APP.initCheckpoint) {
                getEditor().asc_setRestriction(true);
                makeCheckpoint(true);
            }

            // Check if history can/should be trimmed
            var cp = getLastCp();
            if (cp && cp.file && cp.hash) {
                var channels = [{
                    channel: content.channel,
                    lastKnownHash: cp.hash
                }];
                common.checkTrimHistory(channels);
            }
            console.log("OO ready");
        };

        const copy = (a, b) => {
            Object.keys(b).forEach(k => {
                if (k === "user") { return; } // Don't change user values
                if (a[k]) {
                    if (typeof(a[k]) === "object" && typeof(b[k]) === "object") {
                        copy(a[k], b[k]);
                    }
                    return;
                }
                a[k] = b[k];
            });
        };

        const createOOConfig = function(blob, file, lock, fromContent, lang, force) {
            const url = URL.createObjectURL(blob);
            let username = Util.find(privateData, ['integrationConfig', 'user', 'name'])
                            || metadataMgr.getUserData().name
                            || Messages.anonymous;

            let integrationConfig = privateData?.integrationConfig?._;
            //let ec = integrationConfig?.editorConfig;
            let dc = integrationConfig?.document;

            const ooconfig = {
                document: {
                    fileType: file.type,
                    key: "fresh",
                    title: dc?.title || file.title,
                    url: url,
                    permissions: {
                        download: dc?.permissions?.download || false,
                        print: dc?.permissions?.print !== false,
                        protect: file.type === 'xlsx',
                    }
                },
                "documentType": file.doc,
                "editorConfig": {
                    customization: {
                        compactHeader: true,
                        chat: false,
                        logo: {
                            url: "/bounce/#" + encodeURIComponent('https://www.onlyoffice.com')
                        },
                        comments: !lock && !readOnly,
                        hideRightMenu: true,
                        uiTheme: window.CryptPad_theme === "dark" ? "theme-dark" : "theme-classic-light"
                    },
                    "user": {
                        "id": String(myOOId), //"c0c3bf82-20d7-4663-bf6d-7fa39c598b1d",
                        "firstname": username,
                        "name": username
                    },
                    "mode": "edit",
                    "lang": lang
                },
                "events": {
                    "onAppReady": onAppReady,
                    "onError": onError,
                    "onDocumentReady": () => onDocumentReady(lock, lang, fromContent, file, force),
                }
            };

            if (integrationConfig) {
                let ec = integrationConfig.editorConfig;
                let c = ooconfig.editorConfig.customization;
                copy(ooconfig.editorConfig, ec);
                // Open "goback" in new tabs because of csp and
                // iframes
                if (ec.editorConfig?.customization?.goback) {
                    c.goback.blank = true;
                }
                c.forcesave = true;
            }

            console.error('updated config', ooconfig);
            return ooconfig;
        };

        var firstOO = true;
        startOO = function (blob, file, force) {
            if (APP.ooconfig && !force) { return void console.error('already started'); }
            const lock = !APP.history && (APP.migrate);

            let fromContent = metadataMgr.getPrivateData().fromContent;
            if (!firstOO) { fromContent = undefined; }
            firstOO = false;

            // Starting from version 3, we can use the view mode again
            // defined but never used
            //var mode = (content && content.version > 2 && lock) ? "view" : "edit";

            const lang = (window.cryptpadLanguage || navigator.language || navigator.userLanguage || '').slice(0,2);

            // Config
            APP.ooconfig = createOOConfig(blob, file, lock, fromContent, lang, force);
            /*
            // NOTE: Make sure it won't break anaything new (Firefox setTimeout bug)
            window.onbeforeunload = function () {
                var ifr = document.getElementsByTagName('iframe')[0];
                if (ifr) { ifr.remove(); }
            };
            */

            APP.getUserColor = function (userId) {
                var hex;
                Object.keys(content.ids || {}).some(function (k) {
                    var u = content.ids[k];
                    if (Number(u.ooid) === Number(userId)) {
                        var md = common.getMetadataMgr().getMetadataLazy();
                        if (md && md.users && md.users[u.netflux]) {
                            hex = md.users[u.netflux].color;
                        }
                        return true;
                    }
                });
                if (hex) {
                    var rgb = Util.hexToRGB(hex);
                    return {
                        r: rgb[0],
                        g: rgb[1],
                        b: rgb[2],
                        a: 255
                    };
                }
            };

            APP.UploadImageFiles = function (files, type, id, jwt, cb) {
                return void cb();
            };

            const getImageURL = function(name) {
                return new Promise((resolve) => {
                    if (name && /^data:image/.test(name)) {
                        return void resolve('');
                    }

                    var mediasSources = getMediasSources();
                    var data = mediasSources[name];
                    downloadImages[name] = Util.mkEvent(true);

                    if (typeof data === 'undefined') {
                        if (/^http/.test(name) && /slide\/themes\/theme/.test(name)) {
                            Util.fetch(name, function (err, u8) {
                                if (err) { return; }
                                mediasData[name] = {
                                    blobUrl: name,
                                    content: u8,
                                    name: name
                                };
                                var b = new Blob([u8], {type: "image/jpeg"});
                                var blobUrl = URL.createObjectURL(b);
                                return void resolve(blobUrl);
                            });
                            return;
                        }
                        debug("CryptPad - could not find matching media for " + name);
                        return void resolve("");
                    }

                    var blobUrl = (typeof mediasData[data.src] === 'undefined') ? "" : mediasData[data.src].blobUrl;
                    if (blobUrl) {
                        delete downloadImages[name];
                        debug("CryptPad Image already loaded " + blobUrl);

                        // Fix: https://github.com/cryptpad/cryptpad/issues/1500
                        // Maybe OO was reloaded, but the CryptPad cache is still intact?
                        // -> Add the image to OnlyOffice again.
                        const documentUrls = window.frames[0].AscCommon.g_oDocumentUrls;
                        if (!(data.name in documentUrls.getUrls())) {
                            documentUrls.addImageUrl(data.name, blobUrl);
                        }

                        return void resolve(blobUrl);
                    }

                    APP.loadingImage++;
                    Util.fetch(data.src, function (err, u8) {
                        if (err) {
                            APP.loadingImage--;
                            console.error(err);
                            return void resolve("");
                        }
                        try {
                            debug("Decrypt with key " + data.key);
                            FileCrypto.decrypt(u8, Util.decodeBase64(data.key), function (err, res) {
                                APP.loadingImage--;
                                if (err || !res.content) {
                                    debug("Decrypting failed");
                                    return void resolve("");
                                }

                                try {
                                    var blobUrl = URL.createObjectURL(res.content);
                                    // store media blobUrl and content for cache and export
                                    var mediaData = {
                                        blobUrl : blobUrl,
                                        content : "",
                                        name: name
                                    };
                                    mediasData[data.src] = mediaData;
                                    var reader = new FileReader();
                                    reader.onloadend = function () {
                                        debug("MediaData set");
                                        mediaData.content = reader.result;
                                        downloadImages[name].fire();
                                    };
                                    reader.readAsArrayBuffer(res.content);
                                    debug("Adding CryptPad Image " + data.name + ": " +  blobUrl);
                                    window.frames[0].AscCommon.g_oDocumentUrls.addImageUrl(data.name, blobUrl);
                                    resolve(blobUrl);
                                } catch (e) {}
                            });
                        } catch (e) {
                            APP.loadingImage--;
                            debug("Exception decrypting image " + data.name);
                            console.error(e);
                            resolve("");
                        }
                    }, void 0, common.getCache());
                });
            };

            let integrationConfig = privateData?.integrationConfig?._;
            if (integrationConfig?.editorConfig) {
                let ec = integrationConfig.editorConfig;
                let c = APP.ooconfig.editorConfig.customization;
                copy(APP.ooconfig.editorConfig, ec);
                // Open "goback" in new tabs because of csp and
                // iframes
                if (ec.editorConfig?.customization?.goback) {
                    c.goback.blank = true;
                }
                if (!privateData?.integrationConfig?.autosave) {
                    c.forcesave = true;
                }
            }

            APP.AddImage = function(cb1, cb2) {
                APP.AddImageSuccessCallback = cb1;
                APP.AddImageErrorCallback = cb2;
                common.openFilePicker({
                    types: ['file'],
                    where: ['root'],
                    filter: {
                        fileType: ['image/']
                    }
                }, function (data) {
                    if (data.type !== 'file') {
                        debug("Unexpected data type picked " + data.type);
                        return;
                    }
                    var name = data.name;

                    // Add image to the list
                    var mediasSources = getMediasSources();

                    // Check if name already exists
                    var getUniqueName = function (name, mediasSources) {
                        var get = function () {
                            var s = name.split('.');
                            if (s.length > 1) {
                                s[s.length - 2] = s[s.length - 2] + '-' + Util.uid();
                                name = s.join('.');
                            } else {
                                name += '-'+ Util.uid();
                            }
                        };
                        while (mediasSources[name]) { get(); }
                        return name;
                    };
                    if (mediasSources[name]) {
                        name = getUniqueName(name, mediasSources);
                        data.name = name;
                    }
                    mediasSources[name] = data;
                    APP.onLocal();

                    APP.realtime.onSettle(function () {
                        getImageURL(name).then(function(url) {
                            debug("CRYPTPAD success add " + name);
                            common.setPadAttribute('atime', +new Date(), null, data.href);
                            APP.AddImageSuccessCallback({
                                name: name,
                                url: url
                            });
                        });
                    });
                });
            };

            APP.remoteTheme = function () {
                /*
                    APP.themeRemote = true;
                */
            };
            APP.changeTheme = function (/*id*/) {
                /*
                // disabled:
Uncaught TypeError: Cannot read property 'calculatedType' of null
    at CPresentation.changeTheme (sdk-all.js?ver=4.11.0-1633612942653-1633619288217:15927)
                */

                /*
                APP.themeChanged = {
                    id: id
                };
                */
            };
            APP.openURL = function (url) {
                common.openUnsafeURL(url);
            };

            APP.loadingImage = 0;

            APP.getImageURL = function(name, callback) {
                if (name && /^data:image/.test(name)) {
                    return void callback('');
                }

                var mediasSources = getMediasSources();
                var data = mediasSources[name];
                downloadImages[name] = Util.mkEvent(true);

                if (typeof data === 'undefined') {
                    if (/^http/.test(name) && /slide\/themes\/theme/.test(name)) {
                        Util.fetch(name, function (err, u8) {
                            if (err) { return; }
                            mediasData[name] = {
                                blobUrl: name,
                                content: u8,
                                name: name
                            };
                            var b = new Blob([u8], {type: "image/jpeg"});
                            var blobUrl = URL.createObjectURL(b);
                            return void callback(blobUrl);
                        });
                        return;
                    }
                    debug("CryptPad - could not find matching media for " + name);
                    return void callback("");
                }

                var blobUrl = (typeof mediasData[data.src] === 'undefined') ? "" : mediasData[data.src].blobUrl;
                if (blobUrl) {
                    delete downloadImages[name];
                    debug("CryptPad Image already loaded " + blobUrl);

                    // Fix: https://github.com/cryptpad/cryptpad/issues/1500
                    // Maybe OO was reloaded, but the CryptPad cache is still intact?
                    // -> Add the image to OnlyOffice again.
                    const documentUrls = window.frames[0].AscCommon.g_oDocumentUrls;
                    if (!(data.name in documentUrls.getUrls())) {
                        documentUrls.addImageUrl(data.name, blobUrl);
                    }

                    return void callback(blobUrl);
                }

                APP.loadingImage++;
                Util.fetch(data.src, function (err, u8) {
                    if (err) {
                        APP.loadingImage--;
                        console.error(err);
                        return void callback("");
                    }
                    try {
                        debug("Decrypt with key " + data.key);
                        FileCrypto.decrypt(u8, Nacl.util.decodeBase64(data.key), function (err, res) {
                            APP.loadingImage--;
                            if (err || !res.content) {
                                debug("Decrypting failed");
                                return void callback("");
                            }

                            try {
                                var blobUrl = URL.createObjectURL(res.content);
                                // store media blobUrl and content for cache and export
                                var mediaData = {
                                    blobUrl : blobUrl,
                                    content : "",
                                    name: name
                                };
                                mediasData[data.src] = mediaData;
                                var reader = new FileReader();
                                reader.onloadend = function () {
                                    debug("MediaData set");
                                    mediaData.content = reader.result;
                                    downloadImages[name].fire();
                                };
                                reader.readAsArrayBuffer(res.content);
                                debug("Adding CryptPad Image " + data.name + ": " +  blobUrl);
                                window.frames[0].AscCommon.g_oDocumentUrls.addImageUrl(data.name, blobUrl);
                                callback(blobUrl);
                            } catch (e) {}
                        });
                    } catch (e) {
                        APP.loadingImage--;
                        debug("Exception decrypting image " + data.name);
                        console.error(e);
                        callback("");
                    }
                }, void 0, common.getCache());
            };

            
            // Always hide right menu
            try {
                localStorage?.original?.removeItem('sse-hide-right-settings');
                localStorage?.original?.removeItem('de-hide-right-settings');
                localStorage?.original?.removeItem('pe-hide-right-settings');
            } catch (e) {}

            APP.docEditor = new window.DocsAPI.DocEditor("cp-app-oo-placeholder-a", APP.ooconfig);

            ooLoaded = true;
            if (content.version < 7) {
                makeChannel();
                return;
            }
            APP.docEditor.connectMockServer({
                onMessage: fromOOHandler,
                getParticipants: getParticipants,
                onAuth: onAuth,
                getImageURL: getImageURL,
                getInitialChanges: getInitialChanges,
            });
        };

        APP.printPdf = function (obj, cb) {
            var bin = getContent();
            x2tConvertData({
                buffer: obj.data,
                bin: bin
            }, 'output.bin', 'pdf', function (xlsData) {
                if (!xlsData) { return; }
                var md = common.getMetadataMgr().getMetadataLazy();
                var type = common.getMetadataMgr().getPrivateData().ooType;
                var title = md.title || md.defaultTitle || type;
                var blob = new Blob([xlsData], {type: "application/pdf"});
                UI.removeModals();
                cb();
                saveAs(blob, APP.exportPdfName || title+'.pdf');
                delete APP.exportPdfName;
            });
        };

        var x2tSaveAndConvertData = function(data, filename, extension, finalFilename) {
            var e = getEditor();

            if (extension === "pdf") {
                APP.exportPdfName = finalFilename;
                return void e.asc_Print({});
            }
            x2tConvertData(data, filename, extension, function (xlsData) {
                UI.removeModals();
                if (xlsData) {
                    var blob = new Blob([xlsData], {type: "application/bin;charset=utf-8"});
                    saveAs(blob, finalFilename);
                    return;
                }
                UI.alert(Messages.oo_couldNotConvertDocument);
            });
        };

        var exportXLSXFile = function() {
            var text = getContent();
            var suggestion = Title.suggestTitle(Title.defaultTitle);
            var ext = ['.xlsx', '.ods', '.bin', '.pdf'];
            var type = common.getMetadataMgr().getPrivateData().ooType;
            var warning = '';
            if (type==="presentation") {
                ext = ['.pptx', '.odp', '.bin', '.pdf'];
            } else if (type==="doc") {
                ext = ['.docx', '.odt', '.bin', '.pdf'];
            }

            if (!supportsXLSX()) {
                ext = ['.bin'];
                warning = h('div.alert.alert-info.cp-alert-top', Messages.oo_conversionSupport);
            }

            var types = ext.map(function (val) {
                return {
                    tag: 'a',
                    attributes: {
                        'data-value': val,
                    },
                    content: val
                };
            });
            var dropdownConfig = {
                text: ext[0], // Button initial text
                caretDown: true,
                options: types, // Entries displayed in the menu
                isSelect: true,
                initialValue: ext[0],
                common: common
            };
            var $select = UIElements.createDropdown(dropdownConfig);

            var promptMessage = h('span', [
                Messages.exportPrompt,
                warning
            ]);

            UI.prompt(promptMessage, Util.fixFileName(suggestion), async function (filename) {
                // $select.getValue()
                if (!(typeof(filename) === 'string' && filename)) { return; }
                var ext = ($select.getValue() || '').slice(1);
                if (ext === 'bin') {
                    var blob = new Blob([text], {type: "application/bin;charset=utf-8"});
                    saveAs(blob, filename+'.bin');
                    return;
                }

                if (BrokenFormats.brokenExportFormats.includes(ext)) {
                    await UI.alertPromise(Messages.oo_unstableMigrationWarning);
                }

                var content = h('div.cp-oo-x2tXls', [
                    h('span.fa.fa-spin.fa-spinner'),
                    h('span', Messages.oo_exportInProgress)
                ]);
                UI.openCustomModal(UI.dialog.customModal(content, {buttons: []}));

                setTimeout(function () {
                    x2tSaveAndConvertData(text, "filename.bin", ext, filename+'.'+ext);
                }, 100);
            }, {
                typeInput: $select[0]
            }, true);
            $select.find('button').addClass('btn');
        };

        var x2tImportImagesInternal = function(images, i, callback) {
            if (i >= images.length) {
                callback();
            } else {
                debug("Import image " + i);
                var handleFileData = {
                    name: images[i].name,
                    mediasSources: getMediasSources(),
                    callback: function() {
                        debug("next image");
                        x2tImportImagesInternal(images, i+1, callback);
                    },
                };
                var fileData = images[i].data;
                debug("Buffer");
                debug(fileData.buffer);
                var blob = new Blob([fileData.buffer], {type: 'image/png'});
                blob.name = images[i].name;
                APP.FMImages.handleFile(blob, handleFileData);
            }
        };

        var x2tImportImages = function (images, callback) {
            if (!APP.FMImages) {
                var fmConfigImages = {
                    noHandlers: true,
                    noStore: true,
                    body: $('body'),
                    onUploaded: function (ev, data) {
                        if (!ev.callback) { return; }
                        debug("Image uploaded at " + data.url);
                        var parsed = Hash.parsePadUrl(data.url);
                        if (parsed.type === 'file') {
                            var secret = Hash.getSecrets('file', parsed.hash, data.password);
                            var fileHost = privateData.fileHost || privateData.origin;
                            var src = fileHost + Hash.getBlobPathFromHex(secret.channel);
                            var key = Hash.encodeBase64(secret.keys.cryptKey);
                            debug("Final src: " + src);
                            ev.mediasSources[ev.name] = { name : ev.name, src : src, key : key };
                        }
                        ev.callback();
                    }
                };
                APP.FMImages = common.createFileManager(fmConfigImages);
            }

            // Import Images
            debug("Import Images");
            debug(images);
            x2tImportImagesInternal(images, 0, function() {
                debug("Sync media sources elements");
                debug(getMediasSources());
                APP.onLocal();
                debug("Import Images finalized");
                callback();
            });
        };


        var x2tImportData = function (data, filename, extension, callback) {
            x2tConvertData(new Uint8Array(data), filename, extension, function (binData, images) {
                if (!binData) { return void callback(); }
                x2tImportImages(images, function() {
                    callback(binData);
                });
            });
        };

        var importFile = function(content) {
            // Abort if there is another real user in the channel (history keeper excluded)
            var m = metadataMgr.getChannelMembers().slice().filter(function (nId) {
                return nId.length === 32;
            });
            if (m.length > 1) {
                UI.removeModals();
                return void UI.alert(Messages.oo_cantUpload);
            }
            if (!content) {
                UI.removeModals();
                return void UI.alert(Messages.oo_invalidFormat);
            }
            var blob = new Blob([content], {type: 'plain/text'});
            var file = getFileType();
            blob.name = (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type;
            var uploadedCallback = function() {
                UI.removeModals();
                UI.confirm(Messages.oo_uploaded, function (yes) {
                    try {
                        getEditor().asc_setRestriction(true);
                    } catch (e) {}
                    if (!yes) { return; }
                    common.gotoURL();
                });
            };
            var data = {
                hash: ooChannel.lastHash,
                index: ooChannel.cpIndex,
                callback: uploadedCallback
            };
            APP.FM.handleFile(blob, data);
        };

        var importXLSXFile = async function(content, filename, ext) {
            // Perform the x2t conversion
            debug("Filename");
            debug(filename);

            if (BrokenFormats.brokenImportFormats.includes(ext)) {
                await UI.alertPromise(Messages.oo_unstableMigrationWarning);
            }

            if (ext === "bin") {
                return void importFile(content);
            }
            if (!supportsXLSX()) {
                return void UI.alert(Messages.oo_invalidFormat);
            }
            var div = h('div.cp-oo-x2tXls', [
                h('span.fa.fa-spin.fa-spinner'),
                h('span', Messages.oo_importInProgress)
            ]);
            UI.openCustomModal(UI.dialog.customModal(div, {buttons: []}));
            setTimeout(function () {
                x2tImportData(new Uint8Array(content), filename.name, "bin", function(c) {
                    if (!c) {
                        UI.removeModals();
                        return void UI.warn(Messages.error);
                    }
                    importFile(c);
                });
            }, 100);
        };

        var loadDocument = function (noCp, useNewDefault, i, cb) {
            if (ooLoaded) { return; }
            var type = common.getMetadataMgr().getPrivateData().ooType;
            var file = getFileType();
            if (!noCp) {
                var lastCp = getLastCp(false, i);
                // If the last checkpoint is empty, load the "initial" doc instead
                if (!lastCp || !lastCp.file) { return void loadDocument(true, useNewDefault, undefined, cb); }
                // Load latest checkpoint
                return void loadLastDocument(lastCp)
                    .then(({blob, fileType}) => {
                        cb({
                            blob,
                            file: fileType
                        });
                    })
                    .catch(() => {
                        // Checkpoint error: load the previous one
                        deleteLastCp(i);
                        i = i || 0;
                        loadDocument(noCp, useNewDefault, ++i, cb);
                    });
            }
            var blob = loadInitDocument(type, useNewDefault);
            cb({
                blob, file
            });
        };

        var initializing = true;
        var $bar = $('#cp-toolbar');

        config = {
            patchTransformer: ChainPad.SmartJSONTransformer,
            // cryptpad debug logging (default is 1)
            // logLevel: 0,
            validateContent: function (content) {
                try {
                    JSON.parse(content);
                    return true;
                } catch (e) {
                    debug("Failed to parse, rejecting patch");
                    return false;
                }
            }
        };

        var stringifyInner = function () {
            var obj = {
                content: content,
                metadata: metadataMgr.getMetadataLazy()
            };
            // stringify the json and send it into chainpad
            return stringify(obj);
        };

        var pinImages = function () {
            if (content.mediasSources) {
                var toPin = Object.keys(content.mediasSources || {}).map(function (id) {
                    var data = content.mediasSources[id] || {};
                    var src = data.src;
                    if (!src) { return; }
                    // Remove trailing slash
                    if (src.slice(-1) === '/') {
                        src = src.slice(0, -1);
                    }
                    // Extract the channel id from the source href
                    return src.slice(src.lastIndexOf('/') + 1);
                }).filter(Boolean);
                sframeChan.query('EV_OO_PIN_IMAGES', toPin);
            }
        };

        var setStrictEditing = function () {
            if (APP.isFast) { return; }
            var editor = getEditor();
            var editing = editor.asc_isDocumentModified ? editor.asc_isDocumentModified() : editor.isDocumentModified();
            if (editing) {
                evOnPatch.fire();
            } else {
                evOnSync.fire();
            }
        };
        APP.onFastChange = function (isFast) {
            APP.isFast = isFast;
            if (isFast) {
                if (APP.hasChangedInterval) {
                    window.clearInterval(APP.hasChangedInterval);
                }
                return;
            }
            setStrictEditing();
            APP.hasChangedInterval = window.setInterval(setStrictEditing, 500);
        };

        APP.getContent = function () { return content; };

        APP.onLocal = config.onLocal = function () {
            if (initializing) { return; }
            if (readOnly) { return; }

            // Update metadata
            var content = stringifyInner();
            APP.realtime.contentUpdate(content);
            pinImages();
        };

        const loadCp = async function (cp, keepQueue) {
            if (!isLockedModal.modal) {
                isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
            }
            try {
                const {blob, fileType} = await loadLastDocument(cp);
                if (!keepQueue) { ooChannel.queue = []; }
                resetData(blob, fileType);
            } catch (e) {
                var file = getFileType();
                var type = common.getMetadataMgr().getPrivateData().ooType;
                var blob = loadInitDocument(type, true);
                if (!keepQueue) { ooChannel.queue = []; }
                resetData(blob, file);
            }
        };

        var loadTemplate = function (href, pw, parsed) {
            APP.history = true;
            APP.template = true;
            var editor = getEditor();
            if (editor) {
                try { getEditor().asc_setRestriction(true); } catch (e) {}
            }
            var _content = parsed.content;

            // Get checkpoint
            var hashes = _content.hashes || {};
            var medias = _content.mediasSources;
            var idx = sortCpIndex(hashes);
            var lastIndex = idx[idx.length - 1];
            var lastCp = hashes[lastIndex] || {};

            // Current cp or initial hash (invalid hash ==> initial hash)
            var toHash = lastCp.hash || 'NONE';
            // Last hash
            var fromHash = 'NONE';

            content.mediasSources = medias;

            sframeChan.query('Q_GET_HISTORY_RANGE', {
                href: href,
                password: pw,
                channel: _content.channel,
                lastKnownHash: fromHash,
                toHash: toHash,
            }, function (err, data) {
                if (err) { return void console.error(err); }
                if (!Array.isArray(data.messages)) { return void console.error('Not an array!'); }

                // The first "cp" in history is the empty doc. It doesn't include the first patch
                // of the history
                var initialCp = !lastCp.hash;

                var messages = (data.messages || []).slice(initialCp ? 0 : 1);

                ooChannel.queue = messages.map(function (obj) {
                    return {
                        hash: obj.serverHash,
                        msg: JSON.parse(obj.msg)
                    };
                });
                ooChannel.historyLastHash = ooChannel.lastHash;
                ooChannel.currentIndex = ooChannel.cpIndex;
                loadCp(lastCp, true);
            });
        };

        var openTemplatePicker = function () {
            var metadataMgr = common.getMetadataMgr();
            var type = metadataMgr.getPrivateData().app;
            var sframeChan = common.getSframeChannel();
            var pickerCfgInit = {
                types: [type],
                where: ['template'],
                hidden: true
            };
            var pickerCfg = {
                types: [type],
                where: ['template'],
            };
            var onConfirm = function () {
                common.openFilePicker(pickerCfg, function (data) {
                    if (data.type !== type) { return; }
                    UI.addLoadingScreen({hideTips: true});
                    sframeChan.query('Q_OO_TEMPLATE_USE', {
                        href: data.href,
                    }, function (err, val) {
                        var parsed;
                        try {
                            parsed = JSON.parse(val);
                        } catch (e) {
                            console.error(e, val);
                            UI.removeLoadingScreen();
                            return void UI.warn(Messages.error);
                        }
                        console.error(data);
                        loadTemplate(data.href, data.password, parsed);
                    });
                });
            };
            sframeChan.query("Q_TEMPLATE_EXIST", type, function (err, data) {
                if (data) {
                    common.openFilePicker(pickerCfgInit);
                    onConfirm();
                } else {
                    UI.alert(Messages.template_empty);
                }
            });
        };

        sframeChan.on('EV_INTEGRATION_DOWNLOADAS', function (format) {
            console.error('DOWNLOAD AS RECEIVED');
            var data = getContent();
            x2tConvertData(data, "document.bin", format, function (xlsData) {
                UI.removeModals();
                if (xlsData) {
                    var blob = new Blob([xlsData], {type: "application/bin;charset=utf-8"});
                    if (integrationChannel) {
                        integrationChannel.event('EV_INTEGRATION_ON_DOWNLOADAS',
                                                blob, { raw: true });
                    }
                    return;
                }
                UI.warn(Messages.error);
            });
        });

        sframeChan.on('EV_OOIFRAME_REFRESH', function (data) {
            // We want to get the "bin" content of a sheet from its json in order to download
            // something useful from a non-onlyoffice app (download from drive or settings).
            // We don't want to initialize a full pad in async-store because we only need a
            // static version, so we can use "openVersionHash" which is based on GET_HISTORY_RANGE
            APP.isDownload = data.downloadId;
            APP.downloadType = data.type;
            downloadImages = {};
            var json = data && data.json;
            if (!json || !json.content) {
                return void sframeChan.event('EV_OOIFRAME_DONE', '');
            }
            content = json.content;
            readOnly = true;
            var version = (!content.version || content.version === 1) ? 'v1/' :
                          (content.version <= 3 ? 'v2b/' : OOCurrentVersion.currentVersion + '/');
            var s = h('script', {
                type:'text/javascript',
                src: ApiConfig.httpSafeOrigin + '/common/onlyoffice/dist/'+version+'web-apps/apps/api/documents/api.js?' + APP.urlArgs
            });
            $('#cp-app-oo-editor').empty().append(h('div#cp-app-oo-placeholder-a')).append(s);

            var hashes = content.hashes || {};
            var idx = sortCpIndex(hashes);
            var lastIndex = idx[idx.length - 1];

            // We're going to open using "openVersionHash" to avoid reimplementing existing code.
            // To do so, we're using a version corresponding to the latest checkpoint with a
            // minor version of 0. "openVersionHash" knows that it needs to give us the latest
            // version when "APP.isDownload" is true.
            var sheetVersion = lastIndex + '.0';
            openVersionHash(sheetVersion);
        });

        config.onInit = function (info) {
            var privateData = metadataMgr.getPrivateData();
            metadataMgr.setDegraded(false); // FIXME degraded moded unsupported (no cursor channel)

            readOnly = privateData.readOnly;

            Title = common.createTitle({});

            var configTb = {
                displayed: ['pad'],
                title: Title.getTitleConfig(),
                metadataMgr: metadataMgr,
                readOnly: readOnly,
                realtime: info.realtime,
                spinner: {
                    onPatch: evOnPatch,
                    onSync: evOnSync
                },
                sfCommon: common,
                $container: $bar,
                $contentContainer: $('#cp-app-oo-container'),
                skipLink: 'iframe[name="frameEditor"]|#editor_sdk'
            };
            toolbar = APP.toolbar = Toolbar.create(configTb);
            toolbar.showColors();
            Title.setToolbar(toolbar);

            if (window.CP_DEV_MODE) {

                var $save = common.createButton('save', true, {}, function () {
                    makeCheckpoint(true);
                });
                $save.appendTo(toolbar.$bottomM);

                var $dlMedias = common.createButton('', true, {
                    name: 'dlmedias',
                    icon: 'fa-download',
                }, function () {
                    require(['/components/jszip/dist/jszip.min.js'], function (JsZip) {
                        var zip = new JsZip();
                        Object.keys(mediasData || {}).forEach(function (url) {
                            var obj = mediasData[url];
                            var b = new Blob([obj.content]);
                            zip.file(obj.name, b, {binary: true});
                        });
                        setTimeout(function () {
                            zip.generateAsync({type: 'blob'}).then(function (content) {
                                saveAs(content, 'media.zip');
                            });
                        }, 100);
                    });
                }).attr('title', "Download medias");
                $dlMedias.appendTo(toolbar.$bottomM);
            }

            if (!privateData.ooVersionHash) {
            (function () {
                /* add a history button */
                var commit = function () {
                    // Wait for the checkpoint to be uploaded before leaving history mode
                    // (race condition). We use "stopHistory" to remove the history
                    // flag only when the checkpoint is ready.
                    APP.stopHistory = true;
                    makeCheckpoint(true);
                };
                var onPatch = function (patch) {
                    // Patch on the current cp
                    ooChannel.send(JSON.parse(patch.msg));
                };
                var onCheckpoint = function (cp) {
                    // We want to load a checkpoint:
                    loadCp(cp);
                };
                var setHistoryMode = function (bool) {
                    if (bool) {
                        APP.history = true;
                        try { getEditor().asc_setRestriction(true); } catch (e) {}
                        return;
                    }
                    // Cancel button: redraw from lastCp
                    APP.history = false;
                    ooChannel.queue = [];
                    ooChannel.ready = false;
                    // Fill the queue and then load the last CP
                    rtChannel.getHistory(function () {
                        var lastCp = getLastCp();
                        loadCp(lastCp, true);
                    });
                };

                var deleteSnapshot = function (hash) {
                    var md = Util.clone(cpNfInner.metadataMgr.getMetadata());
                    var snapshots = md.snapshots = md.snapshots || {};
                    delete snapshots[hash];
                    metadataMgr.updateMetadata(md);
                    APP.onLocal();
                };
                var makeSnapshot = function (title, cb, obj) {
                    var hash, time;
                    if (obj && obj.hash && obj.time) {
                        hash = obj.hash;
                        time = obj.time;
                    } else {
                        var major = Object.keys(content.hashes).length;
                        var cpIndex = getLastCp().index || 0;
                        var minor = ooChannel.cpIndex - cpIndex;
                        hash = major+'.'+minor;
                        time = +new Date();
                    }
                    var md = Util.clone(metadataMgr.getMetadata());
                    var snapshots = md.snapshots = md.snapshots || {};
                    if (snapshots[hash]) { cb('EEXISTS'); return void UI.warn(Messages.snapshot_error_exists); }
                    snapshots[hash] = {
                        title: title,
                        time: time
                    };
                    metadataMgr.updateMetadata(md);
                    APP.onLocal();
                    APP.realtime.onSettle(cb);
                };
                var loadSnapshot = function (hash) {
                    sframeChan.event('EV_OO_OPENVERSION', {
                        hash: hash
                    });
                };

                var $historyButton = common.createButton('', true, {
                    name: 'history',
                    icon: 'fa-history',
                    text: Messages.historyText,
                    tippy: Messages.historyButton
                });

                $historyButton.click(function () {
                    ooChannel.historyLastHash = ooChannel.lastHash;
                    ooChannel.currentIndex = ooChannel.cpIndex;
                    Feedback.send('OO_HISTORY');
                    var histConfig = {
                        onPatch: onPatch,
                        onCheckpoint: onCheckpoint,
                        onRevert: commit,
                        setHistory: setHistoryMode,
                        makeSnapshot: makeSnapshot,
                        onlyoffice: {
                            hashes: content.hashes || {},
                            channel: content.channel,
                            lastHash: ooChannel.lastHash
                        },
                        $toolbar: $('.cp-toolbar-container')
                    };
                    History.create(common, histConfig);
                });

                var $historyDropdown = UIElements.getEntryFromButton($historyButton);
                $historyDropdown.appendTo(toolbar.$drawer);

                // Snapshots
                var $snapshotButton = common.createButton('snapshots', true, {
                    remove: deleteSnapshot,
                    make: makeSnapshot,
                    load: loadSnapshot
                });
                var $snapshot = UIElements.getEntryFromButton($snapshotButton);
                toolbar.$drawer.append($snapshot);

                // Import template
                var $importTemplateButton = common.createButton('importtemplate', true, {}, openTemplatePicker);
                if ($importTemplateButton && $importTemplateButton.length) {
                    let $template = UIElements.getEntryFromButton($importTemplateButton);
                    $template.appendTo(toolbar.$drawer);
                }

                // Save as template
                if (!metadataMgr.getPrivateData().isTemplate) {
                    var templateObj = {
                        //rt: cpNfInner.chainpad,
                        getTitle: function () { return cpNfInner.metadataMgr.getMetadata().title; },
                        callback: function (title) {
                            var newContent = {};
                            newContent.mediasSources = content.mediasSources;
                            var text = getContent();
                            var blob = new Blob([text], {type: 'plain/text'});
                            var file = getFileType();
                            blob.name = title || (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type;
                            var data = {
                                newTemplate: newContent,
                                title: title
                            };
                            APP.FM.handleFile(blob, data);
                        }
                    };
                    let $templateButton = common.createButton('template', true, templateObj);
                    let $template = UIElements.getEntryFromButton($templateButton);
                    toolbar.$drawer.append($template);
                }
            })();
            }

            if (window.CP_DEV_MODE || DISPLAY_RESTORE_BUTTON) {
                common.createButton('', true, {
                    name: 'delete',
                    icon: 'fa-trash',
                    hiddenReadOnly: true
                }).click(function () {
                    if (initializing) { return void console.error('initializing'); }
                    deleteLastCp();
                }).attr('title', 'Delete last checkpoint').appendTo(toolbar.$bottomM);
                common.createButton('', true, {
                    name: 'restore',
                    icon: 'fa-history',
                    hiddenReadOnly: true
                }).click(function () {
                    if (initializing) { return void console.error('initializing'); }
                    restoreLastCp();
                }).attr('title', 'Restore last checkpoint').appendTo(toolbar.$bottomM);
            }

            var $exportXLSXButton = common.createButton('export', true, {}, exportXLSXFile);
            var $exportXLSX = UIElements.getEntryFromButton($exportXLSXButton);
            $exportXLSX.appendTo(toolbar.$drawer);

            var type = privateData.ooType;
            var accept = [".bin", ".ods", ".xlsx"];
            if (type === "presentation") {
                accept = ['.bin', '.odp', '.pptx'];
            } else if (type === "doc") {
                accept = ['.bin', '.odt', '.docx'];
            }
            var first;
            if (!supportsXLSX()) {
                accept = ['.bin'];
                first = function (cb) {
                    var msg = h('span', [
                        Messages.oo_conversionSupport,
                        ' ', h('span', Messages.oo_importBin),
                    ]);
                    UI.confirm(msg, function (yes) {
                        if (yes) {
                            cb();
                        }
                    });
                };
            }

            if (common.isLoggedIn()) {
                window.CryptPad_deleteLastCp = deleteLastCp;
                var $importXLSXButton = common.createButton('import', true, {
                    accept: accept,
                    binary: ["ods", "xlsx", "odt", "docx", "odp", "pptx"],
                    first: first,
                }, importXLSXFile);
                var $importXLSX = UIElements.getEntryFromButton($importXLSXButton);
                // tag button
                var $hashtagButton = common.createButton('hashtag', true);
                var $hashtag = UIElements.getEntryFromButton($hashtagButton);
                $importXLSX.appendTo(toolbar.$drawer);
                $hashtag.appendTo(toolbar.$drawer);
            }

            var $storeButton = common.createButton('storeindrive', true);
            var $store = UIElements.getEntryFromButton($storeButton);
            toolbar.$drawer.append($store);

            // Move to trash button
            var $forgetButton = common.createButton('forget', true, {}, function (err) {
                if (err) { return; }
                setEditable(false);
            });
            var $forget = UIElements.getEntryFromButton($forgetButton);
            toolbar.$drawer.append($forget);

            if (!privateData.isEmbed) {
                var helpMenu = APP.helpMenu = common.createHelpMenu(['beta', 'oo']);
                $('#cp-app-oo-editor').prepend(common.getBurnAfterReadingWarning());
                $('#cp-app-oo-editor').prepend(helpMenu.menu);
                var $helpMenuButton = UIElements.getEntryFromButton(helpMenu.button);
                toolbar.$drawer.append($helpMenuButton);
            }

            var $propertiesButton = common.createButton('properties', true);
            var $properties = UIElements.getEntryFromButton($propertiesButton);
            toolbar.$drawer.append($properties);

            var $copyButton = common.createButton('copy', true);
            var $copy = UIElements.getEntryFromButton($copyButton);
            toolbar.$drawer.append($copy);
        };

        var noCache = false; // Prevent reload loops
        var onCorruptedCache = function () {
            if (noCache) {
                UI.errorLoadingScreen(Messages.unableToDisplay, false, function () {
                    common.gotoURL('');
                });
            }
            noCache = true;
            var sframeChan = common.getSframeChannel();
            sframeChan.event("EV_CORRUPTED_CACHE");
        };

        var firstReady = true;
        config.onReady = function (info) {
            if (APP.realtime !== info.realtime) {
                APP.realtime = info.realtime;
            }

            var userDoc = APP.realtime.getUserDoc();
            var isNew = false;
            var newDoc = true;
            if (userDoc === "" || userDoc === "{}") { isNew = true; }

            if (userDoc !== "") {
                var hjson = JSON.parse(userDoc);

                if (hjson && hjson.metadata) {
                    metadataMgr.updateMetadata(hjson.metadata);
                }
                if (typeof (hjson) !== 'object' || Array.isArray(hjson) ||
                    (hjson.metadata && typeof(hjson.metadata.type) !== 'undefined' &&
                     hjson.metadata.type !== 'oo')) {
                    var errorText = Messages.typeError;
                    UI.errorLoadingScreen(errorText);
                    throw new Error(errorText);
                }
                content = hjson.content || content;
                var newLatest = getLastCp();
                sframeChan.query('Q_OO_SAVE', {
                    hash: newLatest.hash,
                    url: newLatest.file
                }, function () { });
                newDoc = !content.hashes || Object.keys(content.hashes).length === 0;
            } else if (!privateData.isNewFile) {
                // This is an empty doc but not a new file: error
                onCorruptedCache();
                return void console.error("Empty chainpad for a non-empty doc");
            } else {
                Title.updateTitle(Title.defaultTitle);
            }

            APP.startNew = isNew;

            var version = OOCurrentVersion.currentVersion + '/';
            var msg;
            // Old version detected: use the old OO and start the migration if we can
            if (privateData.ooForceVersion) {
                if (privateData.ooForceVersion === "1") {
                    version = "v1/";
                }
            } else if (content && (!content.version || content.version === 1)) {
                version = 'v1/';
                APP.migrate = true;
                // Registedred ~~users~~ editors can start the migration
                if (common.isLoggedIn() && !readOnly) {
                    content.migration = true;
                    APP.onLocal();
                } else {
                    msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor);
                    if (APP.helpMenu) {
                        $(APP.helpMenu.menu).after(msg);
                    } else {
                        $('#cp-app-oo-editor').prepend(msg);
                    }
                    readOnly = true;
                }
            } else if (content && content.version <= 4) { // V2 or V3
                version = content.version <= 3 ? 'v2b/' : 'v4/';
                APP.migrate = true;
                // Registedred ~~users~~ editors can start the migration
                if (common.isLoggedIn() && !readOnly) {
                    content.migration = true;
                    APP.onLocal();
                } else {
                    msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor);
                    if (APP.helpMenu) {
                        $(APP.helpMenu.menu).after(msg);
                    } else {
                        $('#cp-app-oo-editor').prepend(msg);
                    }
                    readOnly = true;
                }
            } else if (content && content.version <= 5) {
                version = 'v5/';
                APP.migrate = true;
                // Registedred ~~users~~ editors can start the migration
                if (common.isLoggedIn() && !readOnly) {
                    content.migration = true;
                    APP.onLocal();
                } else {
                    msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor);
                    if (APP.helpMenu) {
                        $(APP.helpMenu.menu).after(msg);
                    } else {
                        $('#cp-app-oo-editor').prepend(msg);
                    }
                    readOnly = true;
                }
            } else if (content && content.version <= 6) {
                version = 'v6/';
                APP.migrate = true;
                // Registedred ~~users~~ editors can start the migration
                if (common.isLoggedIn() && !readOnly) {
                    content.migration = true;
                    APP.onLocal();
                } else {
                    msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor);
                    if (APP.helpMenu) {
                        $(APP.helpMenu.menu).after(msg);
                    } else {
                        $('#cp-app-oo-editor').prepend(msg);
                    }
                    readOnly = true;
                }
            } else if (content && content.version <= 7) {
                version = 'v7/';
                APP.migrate = true;
                // Registedred ~~users~~ editors can start the migration
                if (common.isLoggedIn() && !readOnly) {
                    content.migration = true;
                    APP.onLocal();
                } else {
                    msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor);
                    if (APP.helpMenu) {
                        $(APP.helpMenu.menu).after(msg);
                    } else {
                        $('#cp-app-oo-editor').prepend(msg);
                    }
                    readOnly = true;
                }
            }

            // If the sheet is locked by an offline user, remove it
            if (content && content.saveLock && !isUserOnline(content.saveLock)) {
                content.saveLock = undefined;
                APP.onLocal();
            } else if (content && content.saveLock) {
                // If someone is currently creating a checkpoint (and locking the sheet),
                // make sure it will end (maybe you'll have to make the checkpoint yourself)
                checkCheckpoint();
            }

            var s = h('script', {
                type:'text/javascript',
                src: ApiConfig.httpSafeOrigin + '/common/onlyoffice/dist/'+version+'web-apps/apps/api/documents/api.js?' + APP.urlArgs
            });
            $('#cp-app-oo-editor').append(s);

            if (metadataMgr.getPrivateData().burnAfterReading && content && content.channel) {
                sframeChan.event('EV_BURN_PAD', content.channel);
            }

            if (privateData.ooVersionHash) {
                return void openVersionHash(privateData.ooVersionHash);
            }


            // Only execute the following code the first time we call onReady
            if (!firstReady) {
                setMyId();
                oldHashes = JSON.parse(JSON.stringify(content.hashes));
                initializing = false;
                return void setEditable(!readOnly);
            }
            firstReady = false;


            var useNewDefault = content.version && content.version >= 2;

            loadDocument(newDoc, useNewDefault,void 0, cpObj => {
            openRtChannel(Util.once(function () {
                setMyId();
                oldHashes = JSON.parse(JSON.stringify(content.hashes));
                initializing = false;

                // If we have more than CHECKPOINT_INTERVAL patches in the initial history
                // and we're the only editor in the pad, make a checkpoint
                var v = metadataMgr.getViewers();
                var m = metadataMgr.getChannelMembers().filter(function (str) {
                    return str.length === 32;
                }).length;
                if ((m - v) === 1 && !readOnly && common.isLoggedIn()) {
                    var needCp = ooChannel.queue.length > CHECKPOINT_INTERVAL;
                    APP.initCheckpoint = needCp;
                }


                common.openPadChat(APP.onLocal);

                if (!readOnly) {
                    var cursors = {};
                    common.openCursorChannel(APP.onLocal);
                    cursor = common.createCursor(APP.onLocal);
                    cursor.onCursorUpdate(function (data) {
                        // Leaving user
                        if (data && data.leave && data.id) {
                            // When a netflux user leaves, remove all their cursors
                            Object.keys(cursors).forEach(function (ooid) {
                                var d = cursors[ooid];
                                if (d !== data.id) { return; } // Only continue for the leaving user
                                // Remove from OO UI
                                ooChannel.send({
                                    type: "cursor",
                                    messages: [{
                                        cursor: "10;AgAAADIAAAAAAA==",
                                        time: +new Date(),
                                        user: ooid,
                                        useridoriginal: String(ooid).slice(0,-1),
                                    }]
                                });
                                // Remove from memory
                                delete cursors[ooid];
                            });
                            handleNewIds({}, content.ids);
                        }

                        // Cursor update
                        if (!data || !data.cursor) { return; }
                        // Store the new cursor in memory for this user, with their netflux ID
                        var ooid = Util.find(data.cursor, ['messages', 0, 'user']);
                        if (ooid) { cursors[ooid] = data.id.slice(0,32); }
                        // Update cursor in the UI
                        ooChannel.send(data.cursor);
                    });
                }

                if (APP.startWithTemplate) {
                    var template = APP.startWithTemplate;
                    loadTemplate(template.href, template.password, template.content);
                    return;
                }

                var next = function () {
                    const { blob, file } = cpObj;
                    startOO(blob, file);
                    setEditable(!readOnly);
                    UI.removeLoadingScreen();
                };

                let convertImportBlob = (blob, title) => {
                    new Response(blob).arrayBuffer().then(function (buffer) {
                        var u8Xlsx = new Uint8Array(buffer);
                        x2tImportData(u8Xlsx, title, 'bin', function (bin) {
                            if (!bin) {
                                return void UI.errorLoadingScreen(Messages.error);
                            }
                            var blob = new Blob([bin], {type: 'text/plain'});
                            //var file = getFileType();
                            //resetData(blob, file);
                            saveToServer(blob, title);
                            Title.updateTitle(title);
                            UI.removeLoadingScreen();
                        });
                    });
                };

                if (privateData.integration) {
                    let cfg = privateData.integrationConfig || {};
                    common.openIntegrationChannel(APP.onLocal);
                    integrationChannel = common.getSframeChannel();
                    let hasUnsavedChanges = false;
                    var integrationSave = function (cb) {
                        var ext = cfg.fileType;

                        var upload = Util.once(function (_blob) {
                            integrationChannel.query('Q_INTEGRATION_SAVE', {
                                blob: _blob
                            }, obj => {
                                if (!obj?.error) { hasUnsavedChanges = false; }
                                cb(obj);
                            }, {
                                raw: true
                            });
                        });

                        var data = getContent();
                        x2tConvertData(data, "document.bin", ext, function (xlsData) {
                            UI.removeModals();
                            if (xlsData) {
                                var blob = new Blob([xlsData], {type: "application/bin;charset=utf-8"});
                                upload(blob);
                                return;
                            }
                            UI.warn(Messages.error);
                        });
                    };
                    const integrationHasUnsavedChanges = function(unsavedChanges, cb) {
                        integrationChannel.query('Q_INTEGRATION_HAS_UNSAVED_CHANGES', unsavedChanges, cb);
                    };
                    var inte = common.createIntegration(integrationSave,
                                                integrationHasUnsavedChanges);
                    if (inte && cfg.autosave) {
                        evIntegrationSave.reg(function () {
                            inte.changed();
                        });
                    } else {
                        APP.integrationSave = integrationSave;
                        APP.integrationSetSaved = () => {
                            hasUnsavedChanges = false;
                        };
                        evIntegrationSave.reg(function () {
                            hasUnsavedChanges = true;
                        });
                    }
                    $(window).on('beforeunload', function (ev) {
                        if (hasUnsavedChanges) { return false; }
                        ev.returnValue = '';
                    });
                    integrationChannel.on('Q_INTEGRATION_NEEDSAVE', function (data, cb) {
                        if (!cfg.autosave) { return; }
                        integrationSave(function (obj) {
                            if (obj && obj.error) { console.error(obj.error); }
                            cb();
                        });
                    });

                    /* test button
                    if (!cfg.autosave) {
                        let $save = common.createButton('save', true, {}, function () {
                            $save.attr('disabled', 'disabled');
                            integrationSave(() => {
                                $save.removeAttr('disabled');
                            });
                        });
                        $('body').prepend($save);
                    }
                    */

                    if (privateData.initialState && (!content || !content.hashes || !Object.keys(content.hashes).length)) {
                        var blob = privateData.initialState;
                        let title = `document.${cfg.fileType}`;
                        return convertImportBlob(blob, title);
                    }
                }

                if (privateData.isNewFile && privateData.fromFileData) {
                    try {
                    (function () {
                        var data = privateData.fromFileData;

                        var type = data.fileType;
                        var title = data.title;
                        // Fix extension if the file was renamed
                        if (Util.isSpreadsheet(type) && !Util.isSpreadsheet(data.title)) {
                            if (type === 'application/vnd.oasis.opendocument.spreadsheet') {
                                data.title += '.ods';
                            }
                            if (type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
                                data.title += '.xlsx';
                            }
                        }
                        if (Util.isOfficeDoc(type) && !Util.isOfficeDoc(data.title)) {
                            if (type === 'application/vnd.oasis.opendocument.text') {
                                data.title += '.odt';
                            }
                            if (type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
                                data.title += '.docx';
                            }
                        }
                        if (Util.isPresentation(type) && !Util.isPresentation(data.title)) {
                            if (type === 'application/vnd.oasis.opendocument.presentation') {
                                data.title += '.odp';
                            }
                            if (type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation') {
                                data.title += '.pptx';
                            }
                        }

                        var href = data.href;
                        var password = data.password;
                        var parsed = Hash.parsePadUrl(href);
                        var secret = Hash.getSecrets('file', parsed.hash, password);
                        var hexFileName = secret.channel;
                        var fileHost = privateData.fileHost || privateData.origin;
                        var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
                        var key = secret.keys && secret.keys.cryptKey;
                        var xhr = new XMLHttpRequest();
                        xhr.open('GET', src, true);
                        xhr.responseType = 'arraybuffer';
                        xhr.onload = function () {
                            if (/^4/.test('' + this.status)) {
                                // fallback to empty sheet
                                console.error(this.status);
                                return void next();
                            }
                            var arrayBuffer = xhr.response;
                            if (arrayBuffer) {
                                var u8 = new Uint8Array(arrayBuffer);
                                FileCrypto.decrypt(u8, key, function (err, decrypted) {
                                    if (err) {
                                        // fallback to empty sheet
                                        console.error(err);
                                        return void next();
                                    }
                                    var blobXlsx = decrypted.content;
                                    new Response(blobXlsx).arrayBuffer().then(function (buffer) {
                                        var u8Xlsx = new Uint8Array(buffer);
                                        x2tImportData(u8Xlsx, data.title, 'bin', function (bin) {
                                            if (!bin) {
                                                return void UI.errorLoadingScreen(Messages.error);
                                            }
                                            var blob = new Blob([bin], {type: 'text/plain'});
                                            saveToServer(blob, data.title);
                                            Title.updateTitle(title);
                                            UI.removeLoadingScreen();
                                        });
                                    });
                                });
                            }
                        };
                        xhr.onerror = function (err) {
                            // fallback to empty sheet
                            console.error(err);
                            next();
                        };
                        xhr.send(null);
                    })();
                    } catch (e) {
                        console.error(e);
                        next();
                    }
                    return;
                }

                next();
            }));
            });
        };

        config.onError = function (err) {
            common.onServerError(err, toolbar, function () {
                setEditable(false);
            });
        };

        var reloadPopup = false;

        var checkNewCheckpoint = function () {
            if (!isLockedModal.modal) {
                isLockedModal.modal = UI.openCustomModal(isLockedModal.content);
            }
            var lastCp = getLastCp();
            loadLastDocument(lastCp)
                .then(({blob, fileType}) => {
                    resetData(blob, fileType);
                })
                .catch((err) => {
                    console.error(err);
                    // On error, do nothing
                    // FIXME lock the document or ask for a page reload?
                });
        };

        config.onRemote = function () {
            if (initializing) { return; }
            var userDoc = APP.realtime.getUserDoc();
            var json = JSON.parse(userDoc);
            if (json.metadata) {
                metadataMgr.updateMetadata(json.metadata);
            }

            var wasLocked = content.saveLock;

            var wasMigrating = content.migration;

            var myLocks = getUserLock(getId(), true);
            //var integrationSave = content.integrationSave;

            content = json.content;

            if (content.saveLock && wasLocked !== content.saveLock) {
                // Someone new is creating a checkpoint: fix the sheets ids
                fixSheets();
                // If the checkpoint is not saved in 20s to 40s, do it ourselves
                checkCheckpoint();
            }

            // Integration: mark the current content as saved
            // if manually saved by someone else
            if (content.integrationSave !== APP.integrationSaved) {
                APP.integrationSaved = content.integrationSave;
                if (APP.integrationSetSaved) {
                    APP.integrationSetSaved();
                }
            }

            var editor = getEditor();
            if (content.hashes) {
                var latest = getLastCp(true);
                var newLatest = getLastCp();
                if (newLatest.index > latest.index || (newLatest.index && !latest.index)) {
                    ooChannel.queue = [];
                    ooChannel.ready = false;
                    var reload = function () {
                        // New checkpoint
                        sframeChan.query('Q_OO_SAVE', {
                            hash: newLatest.hash,
                            url: newLatest.file
                        }, function () {
                            checkNewCheckpoint();
                        });
                    };
                    var editing = editor.asc_isDocumentModified ? editor.asc_isDocumentModified() : editor.isDocumentModify;
                    if (editing) {
                        setEditable(false);
                        APP.unsavedLocks = myLocks;
                        APP.onStrictSaveChanges = function () {
                            reload();
                            delete APP.onStrictSaveChanges;
                        };
                        editor.asc_Save();
                    } else {
                        reload();
                    }
                }
                oldHashes = JSON.parse(JSON.stringify(content.hashes));
            }

            if (content.ids) {
                handleNewIds(oldIds, content.ids);
                oldIds = structuredClone(content.ids);
            }
            if (content.locks) {
                handleNewLocks(oldLocks, content.locks);
                oldLocks = JSON.parse(JSON.stringify(content.locks));
            }
            if (content.migration) {
                setEditable(false);
            }
            if (wasMigrating && !content.migration && !reloadPopup) {
                reloadPopup = true;
                UI.alert(Messages.oo_sheetMigration_complete, function () {
                    common.gotoURL();
                });
            }
            pinImages();
        };

        config.onConnectionChange = function (info) {
            if (info.state) {
                // If we tried to send changes while we were offline, force a page reload
                UIElements.reconnectAlert();
                if (Object.keys(pendingChanges).length) {
                    return void UI.confirm(Messages.oo_reconnect, function (yes) {
                        if (!yes) { return; }
                        common.gotoURL();
                    });
                }
                //setEditable(true);
                try { getEditor().asc_setViewMode(false); } catch (e) {}
                offline = false;
            } else {
                try { getEditor().asc_setViewMode(true); } catch (e) {}
                //setEditable(false);
                offline = true;
                UI.findOKButton().click();
                UIElements.disconnectAlert();
            }
        };

        cpNfInner = common.startRealtime(config);

        cpNfInner.onInfiniteSpinner(function () {
            if (offline) { return; }
            setEditable(false);
            UI.confirm(Messages.realtime_unrecoverableError, function (yes) {
                if (!yes) { return; }
                common.gotoURL();
            });
        });

        common.onLogout(function () { setEditable(false); });
    };

    var main = function () {
        var common;

        nThen(function (waitFor) {
            $(waitFor(function () {
                UI.addLoadingScreen();
            }));
            SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
        }).nThen(function (waitFor) {
            common.getSframeChannel().on('EV_OO_TEMPLATE', function (data) {
                APP.startWithTemplate = data;
            });
            common.handleNewFile(waitFor, {
                //noTemplates: true
            });
        }).nThen(function (/*waitFor*/) {
            andThen(common);
        });
    };
    main();
});
