// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team 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/application_config.js', '/customize/messages.js', '/support/ui.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', '/common/common-icons.js', '/components/file-saver/FileSaver.min.js', 'css!/components/bootstrap/dist/css/bootstrap.min.css', 'less!/common/onlyoffice/app-oo.less', ], function ( $, Toolbar, JSONSortify, nThen, SFCommon, UI, Hash, Util, UIElements, Feedback, h, ApiConfig, AppConfig, Messages, Support, ChainPad, FileCrypto, History, EmptyCell, EmptyDoc, EmptySlide, Channel, OOCurrentVersion, BrokenFormats, Icons) { 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; let myIndex; 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(); if (!myIndex) { 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 }; const fixProps = (title) => { try { const props = getEditor().asc_getCoreProps(); if (!props) { return; } props.title = title; if (!content.hashes || !Object.keys(content.hashes).length) { // No CP: document is using our templates // --> fix the "creator" field props.creator = ""; } getEditor().asc_setCoreProps(props); } catch {} }; var getContent = function (title) { try { // Update document metadata with latest title fixProps(title); 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', [ Icons.get('loading'), 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; myIndex = undefined; setMyId(); 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, time: new Date() }; 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 (/^[45]/.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', [ Icons.get('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('.'); var v = parseInt(s[1]); 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 messages = data.messages; 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.slice(1, minor+1); 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 = file.doc === 'spreadsheet' ? messages.slice(0, v) : messages.slice(0, v+1); resetData(blob, file); UI.removeLoadingScreen(); }); }); }; const sendDebugSupportTicket = (message) => { const title = "[Automatic] Office document locked"; APP.supportModule.execCommand('MAKE_TICKET', { channel: Hash.createChannelId(), title, ticket: APP.support.getDebuggingData({ title, message }) }, () => {}); }; const onRtChannelError = (err) => { const wasReadOnly = readOnly; readOnly = true; offline = true; const message = JSON.stringify({ error: err?.error, reason: err?.reason, channel: privateData.channel, rtChannel: content.channel }, 0, 2); let txt = Messages.oo_rtChannelMissing; let value; let f = UI.confirm; let cb = (yes) => { if (!yes) { return; } // Set flag if support has already been contacted content.missingRtChannel = +new Date(); readOnly = wasReadOnly; APP.onLocal(); readOnly = true; sendDebugSupportTicket(message); }; let btnText = content.missingRtChannel ? Messages.sent : Messages.support_formButton; let opts = { ok: [ Icons.get('send'), h('span', btnText) ], cancel: Messages.filePicker_close }; if (content.missingRtChannel) { value = h('strong', Messages._getKey('oo_rtChannelMissingDate', [ new Date(content.missingRtChannel).toLocaleDateString() ])); setTimeout(() => { const $b = UI.findOKButton(); $b.attr('disabled', 'disabled'); }); } if (!ApiConfig.supportMailboxKey) { txt = Messages.oo_rtChannelMissingNoSupport; value = UI.getPreCopy(message); f = UI.alert; opts = undefined; cb = undefined; } let div = h('div', [ h('p', txt), value ]); f(div, cb, opts); }; 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 'ERROR': onRtChannelError(obj.data); cb(); break; 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++; common.notify(); } 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) { if (integrationChannel) { integrationChannel.event('EV_INTEGRATION_ERROR', 'X2T_ERROR'); } 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 (integrationChannel) { integrationChannel.event('EV_INTEGRATION_ERROR', 'DOCUMENT_ERROR'); } 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', [ Icons.get('loading'), 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); delete content.missingRtChannel; APP.onLocal(); 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 (integrationChannel) { 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 (typeof(privateData?.integrationConfig?.autosave) !== "number") { 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 type = common.getMetadataMgr().getPrivateData().ooType; var md = common.getMetadataMgr().getMetadataLazy(); var title = md.title || md.defaultTitle || type; var text = getContent(title); var suggestion = Title.suggestTitle(Title.defaultTitle); var ext = ['.xlsx', '.ods', '.bin', '.pdf']; 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', [ Icons.get('loading'), 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); }); }); }; // Abort if there is another real user in the channel (history keeper excluded) var checkChannelUsers = function () { var m = metadataMgr.getChannelMembers().slice().filter(function (nId) { return nId.length === 32; }); if (m.length > 1) { UI.removeModals(); UI.alert(Messages.oo_cantUpload); return true; } }; var importFile = function(content) { if (checkChannelUsers()) { return; } 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); } if (checkChannelUsers()) { return; } var div = h('div.cp-oo-x2tXls', [ Icons.get('loading'), 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((err) => { // Checkpoint error: load the previous one if (err === "DECRYPTION_ERROR" || err === "E_METADATA_DECRYPTION") { 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 loadHistoryCp = function (cp, keepQueue) { APP.history = true; APP.stopHistory = false; loadCp(cp, keepQueue); }; 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: '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); toolbar.setHistory(false); }; 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, true); }; var onPatchBack = function (cp, msgs) { APP.history = true; APP.stopHistory = false; if (msgs) { var msgsFormatted = []; msgs.forEach(function(msg) { var parsedMsg = JSON.parse(msg.msg); var formattedMsg = { msg: parsedMsg, hash: msg.serverHash, author: msg.author, time: msg.time }; msgsFormatted.push(formattedMsg); }); ooChannel.queue = msgsFormatted; setTimeout(function () { loadCp(cp, true); }, 200); } else { loadCp(cp); } }; var docType = function() { return APP.ooconfig.documentType; }; var setHistoryMode = function (bool) { if (bool) { APP.history = true; toolbar.setHistory(true); try { getEditor().asc_setRestriction(true); } catch (e) {} return; } toolbar.setHistory(false); // 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: 'history', text: Messages.historyText, tippy: Messages.historyButton }); if (!AppConfig.enableHistory) { $historyButton.css('display', 'none'); } $historyButton.click(function () { ooChannel.historyLastHash = ooChannel.lastHash; ooChannel.currentIndex = ooChannel.cpIndex; Feedback.send('OO_HISTORY'); var histConfig = { onPatch: onPatch, onPatchBack: onPatchBack, docType: docType, loadCp: loadCp, loadHistoryCp: loadHistoryCp, 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: 'trash-full', 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: '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); toolbar.forgotten(); }); 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; } } else if (content && content.version <= 8) { version = 'v8/'; 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); }; const onUserlistChange = (list) => { integrationChannel.event('Q_INTEGRATION_USERLIST_CHANGE', list); }; var inte = common.createIntegration(integrationSave, integrationHasUnsavedChanges, onUserlistChange); 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(); }); }); integrationChannel.on('EV_INTEGRATION_MANUAL_SAVE', function () { integrationSave(function () { console.log('Integration manual save'); }); }); /* 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*/) { APP.supportModule = common.makeUniversal('support'); APP.support = Support.create(common, false); andThen(common); }); }; main(); });