define([ 'jquery', '/common/toolbar.js', 'json.sortify', '/bower_components/nthen/index.js', '/common/sframe-common.js', '/common/common-interface.js', '/common/common-hash.js', '/common/common-util.js', '/common/common-ui-elements.js', '/common/common-feedback.js', '/common/hyperscript.js', '/api/config', '/customize/messages.js', '/customize/application_config.js', '/bower_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/outer/x2t.js', '/bower_components/file-saver/FileSaver.min.js', 'css!/bower_components/bootstrap/dist/css/bootstrap.min.css', 'less!/bower_components/components-font-awesome/css/font-awesome.min.css', 'less!/common/onlyoffice/app-oo.less', ], function ( $, Toolbar, JSONSortify, nThen, SFCommon, UI, Hash, Util, UIElements, Feedback, h, ApiConfig, Messages, AppConfig, ChainPad, FileCrypto, History, EmptyCell, EmptyDoc, EmptySlide, Channel, X2T) { var saveAs = window.saveAs; var Nacl = window.nacl; var APP = window.APP = { $: $, urlArgs: Util.find(ApiConfig, ['requireConf', 'urlArgs']) }; var CHECKPOINT_INTERVAL = 20; // XXX var FORCE_CHECKPOINT_INTERVAL = 50; // XXX var DISPLAY_RESTORE_BUTTON = false; var NEW_VERSION = 4; // version of the .bin, patches and ChainPad formats var PENDING_TIMEOUT = 30000; var CURRENT_VERSION = X2T.CURRENT_VERSION; //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) : NEW_VERSION }; var oldHashes = {}; var oldIds = {}; var oldLocks = {}; var myUniqueOOId; var myOOId; var sessionId = Hash.createChannelId(); var cpNfInner; var evOnPatch = Util.mkEvent(); var evOnSync = Util.mkEvent(); // This structure is used for caching media data and blob urls for each media cryptpad url var mediasData = {}; var 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; }); }; var getUserIndex = function () { var i = 1; var ids = content.ids || {}; Object.keys(ids).forEach(function (k) { if (ids[k] && ids[k].index && ids[k].index >= i) { i = ids[k].index + 1; } }); return i; }; var setMyId = function () { // Remove ids for users that have left the channel deleteOffline(); var 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] === myOOId; }; while (Object.keys(ids).some(f)) { myOOId = Util.createRandomInteger(); } } var myId = getId(); ids[myId] = { ooid: myOOId, index: getUserIndex(), netflux: metadataMgr.getNetfluxId() }; oldIds = JSON.parse(JSON.stringify(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]; delete content.locks[tabId]; APP.onLocal(); } }; // Make sure a former tab on the same worker doesn't have remaining locks var checkClients = function (clients) { 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 type = common.getMetadataMgr().getPrivateData().ooType; var title = common.getMetadataMgr().getMetadataLazy().title; if (APP.downloadType) { type = APP.downloadType; title = "download"; } 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 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(); } cb(err, h); }); }, }; var ooChannel = { ready: false, queue: [], send: function () {}, cpIndex: 0 }; var getContent = function () { try { return getEditor().asc_nativeGetFile(); } catch (e) { console.error(e); return; } }; /* var checkDrawings = function () { var editor = getEditor(); if (!editor || !editor.GetSheets) { return false; } var s = editor.GetSheets(); return s.some(function (obj) { return obj.worksheet.Drawings.length; }); }; */ // DEPRECATED from version 3 // Loading a checkpoint reorder the sheet starting from ID "5". // We have to reorder it manually when a checkpoint is created // so that the messages we send to the realtime channel are // loadable by users joining after the checkpoint var fixSheets = function () { // Starting from version 3, we don't need to fix the sheet IDs anymore // because we reload onlyoffice whenever we receive a checkpoint if (!APP.migrate || (content && content.version > 2)) { return; } try { var editor = getEditor(); // if we are not in the sheet app // we should not call this code if (typeof editor.GetSheets === 'undefined') { return; } var s = editor.GetSheets(); if (s.length === 0) { return; } var wb = s[0].worksheet.workbook; s.forEach(function (obj, i) { var id = String(i + 5); obj.worksheet.Id = id; wb.aWorksheetsById[id] = obj.worksheet; }); } catch (e) { console.error(e); } }; // Add a lock var isLockedModal = { content: UI.dialog.customModal(h('div.cp-oo-x2tXls', [ h('span.fa.fa-spin.fa-spinner'), h('span', Messages.oo_isLocked) ])) }; var onUploaded = function (ev, data, err) { 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: NEW_VERSION }; 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 = NEW_VERSION; } APP.onLocal(); APP.realtime.onSettle(function () { UI.log(Messages.saved); APP.realtime.onSettle(function () { if (APP.migrate) { UI.removeModals(); UI.alert(Messages.oo_sheetMigration_complete, function () { common.gotoURL(); }); return; } if (ev.callback) { return void ev.callback(); } }); }); sframeChan.query('Q_OO_COMMAND', { cmd: 'UPDATE_HASH', data: ev.hash }, function (err, obj) { if (err || (obj && obj.error)) { console.error(err || obj.error); } }); }; var fmConfig = { noHandlers: true, noStore: true, body: $('body'), onUploaded: function (ev, data) { if (!data || !data.url) { return; } data.hash = ev.hash; sframeChan.query('Q_OO_SAVE', data, function (err) { onUploaded(ev, data, err); }); }, onError: function (err) { onUploaded(null, null, err); } }; APP.FM = common.createFileManager(fmConfig); var resetData = function (blob, type) { // If a read-only refresh popup was planned, abort it delete APP.refreshPopup; clearTimeout(APP.refreshRoTo); // Don't create the initial checkpoint indefinitely in a loop delete APP.initCheckpoint; if (!isLockedModal.modal) { isLockedModal.modal = UI.openCustomModal(isLockedModal.content); } myUniqueOOId = undefined; setMyId(); 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 = getContent(); if (!text && !blob) { setEditable(false, true); sframeChan.query('Q_CLEAR_CACHE_CHANNELS', [ 'chainpad', content.channel, ], function () {}); UI.alert(Messages.realtime_unrecoverableError, function () { common.gotoURL(); }); return; } blob = blob || new Blob([text], {type: 'plain/text'}); var file = getFileType(); blob.name = title || (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type; var data = { hash: (APP.history || APP.template) ? ooChannel.historyLastHash : ooChannel.lastHash, index: (APP.history || APP.template) ? ooChannel.currentIndex : ooChannel.cpIndex }; fixSheets(); if (!isLockedModal.modal) { isLockedModal.modal = UI.openCustomModal(isLockedModal.content); } ooChannel.ready = false; ooChannel.queue = []; data.callback = function () { if (APP.template) { APP.template = false; } resetData(blob, file); }; APP.FM.handleFile(blob, data); }; var noLogin = false; var makeCheckpoint = function (force) { if (APP.cantCheckpoint) { return; } // TOO_LARGE var locked = content.saveLock; var lastCp = getLastCp(); var currentIdx = ooChannel.cpIndex; var needCp = force || (currentIdx - (lastCp.index || 0)) > FORCE_CHECKPOINT_INTERVAL; if (!needCp) { return; } if (!locked || !isUserOnline(locked) || force) { if (!common.isLoggedIn() && !isRegisteredUserOnline() && !noLogin) { var login = h('button.cp-corner-primary', Messages.login_login); var register = h('button.cp-corner-primary', Messages.login_register); var cancel = h('button.cp-corner-cancel', Messages.cancel); var actions = h('div', [cancel, register, login]); var modal = UI.cornerPopup(Messages.oo_login, actions, '', {alt: true}); $(register).click(function () { common.setLoginRedirect('register'); modal.delete(); }); $(login).click(function () { common.setLoginRedirect('login'); modal.delete(); }); $(cancel).click(function () { modal.delete(); noLogin = true; }); return; } if (!common.isLoggedIn()) { return; } content.saveLock = myOOId; APP.onLocal(); APP.realtime.onSettle(function () { saveToServer(); }); } }; var deleteLastCp = function () { var hashes = content.hashes; if (!hashes || !Object.keys(hashes).length) { return; } var i = 0; var idx = Object.keys(hashes).map(Number).sort(function (a, b) { return a-b; }); var lastIndex = idx[idx.length - 1 - i]; delete content.hashes[lastIndex]; APP.onLocal(); APP.realtime.onSettle(function () { UI.log(Messages.saved); }); }; 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'}); }; var loadLastDocument = function (lastCp, onCpError, cb) { if (!lastCp || !lastCp.file) { return void onCpError('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); xhr.responseType = 'arraybuffer'; xhr.onload = function () { if (/^4/.test('' + this.status)) { onCpError(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") { console.warn(err); return void onCpError(err); } return void console.error(err); } var blob = new Blob([decrypted.content], {type: 'plain/text'}); if (cb) { return cb(blob, getFileType()); } startOO(blob, getFileType()); }); } }; xhr.onerror = function (err) { onCpError(err); }; xhr.send(null); }; /* var refreshReadOnly = function () { var cancel = h('button.cp-corner-cancel', Messages.cancel); var reload = h('button.cp-corner-primary', [ h('i.fa.fa-refresh'), Messages.oo_refresh ]); var actions = h('div', [cancel, reload]); var m = UI.cornerPopup(Messages.oo_refreshText, actions, ''); $(reload).click(function () { ooChannel.ready = false; var lastCp = getLastCp(); loadLastDocument(lastCp, function () { var file = getFileType(); var type = common.getMetadataMgr().getPrivateData().ooType; var blob = loadInitDocument(type, true); resetData(blob, file); }, function (blob, file) { resetData(blob, file); }); delete APP.refreshPopup; m.delete(); }); $(cancel).click(function () { delete APP.refreshPopup; m.delete(); }); }; */ var openVersionHash = function (version) { readOnly = true; var hashes = content.hashes || {}; var sortedCp = Object.keys(hashes).map(Number).sort(function (a, b) { return hashes[a].index - hashes[b].index; }); var s = version.split('.'); if (s.length !== 2) { return UI.errorLoadingScreen(Messages.error); } var major = Number(s[0]); var cpId = sortedCp[major - 1]; var nextCpId = sortedCp[major]; var cp = hashes[cpId] || {}; var minor = Number(s[1]) + 1; if (APP.isDownload) { minor = undefined; } var toHash = cp.hash || 'NONE'; var fromHash = nextCpId ? hashes[nextCpId].hash : 'NONE'; sframeChan.query('Q_GET_HISTORY_RANGE', { channel: content.channel, lastKnownHash: fromHash, toHash: toHash, isDownload: APP.isDownload }, function (err, data) { if (err) { console.error(err); return void UI.errorLoadingScreen(Messages.error); } if (!Array.isArray(data.messages)) { console.error('Not an array'); return void UI.errorLoadingScreen(Messages.error); } // The first "cp" in history is the empty doc. It doesn't include the first patch // of the history var initialCp = major === 0 || !cp.hash; var messages = (data.messages || []).slice(initialCp ? 0 : 1, minor); messages.forEach(function (obj) { try { obj.msg = JSON.parse(obj.msg); } catch (e) { console.error(e); } }); // The version exists if we have results in the "messages" array // or if we requested a x.0 version var exists = !Number(s[1]) || messages.length; var vHashEl; if (!privateData.embed) { var vTime = (messages[messages.length - 1] || {}).time; var vTimeStr = vTime ? new Date(vTime).toLocaleString() : 'v' + privateData.ooVersionHash; var vTxt = Messages._getKey('infobar_versionHash',  [vTimeStr]); // If we expected patched and we don't have any, it means this part // of the history has been deleted var vType = "warning"; if (!exists) { vTxt = Messages.oo_deletedVersion; vType = "danger"; } vHashEl = h('div.alert.alert-'+vType+'.cp-burn-after-reading', vTxt); $('#cp-app-oo-editor').prepend(vHashEl); } if (!exists) { return void UI.removeLoadingScreen(); } loadLastDocument(cp, function () { if (cp.hash && vHashEl) { // We requested a checkpoint but we can't find it... UI.removeLoadingScreen(); vHashEl.innerText = Messages.oo_deletedVersion; $(vHashEl).removeClass('alert-warning').addClass('alert-danger'); return; } var file = getFileType(); var type = common.getMetadataMgr().getPrivateData().ooType; if (APP.downloadType) { type = APP.downloadType; } var blob = loadInitDocument(type, true); ooChannel.queue = messages; resetData(blob, file); UI.removeLoadingScreen(); }, function (blob, file) { ooChannel.queue = messages; resetData(blob, file); UI.removeLoadingScreen(); }); }); }; var openRtChannel = function (cb) { if (rtChannel.ready) { return void cb(); } var chan = content.channel || Hash.createChannelId(); if (!content.channel) { content.channel = chan; APP.onLocal(); } sframeChan.query('Q_OO_OPENCHANNEL', { channel: content.channel, lastCpHash: getLastCp().hash }, function (err, obj) { if (err || (obj && obj.error)) { console.error(err || (obj && obj.error)); } }); sframeChan.on('EV_OO_EVENT', function (obj) { switch (obj.ev) { case 'READY': checkClients(obj.data); cb(); break; case 'LEAVE': removeClient(obj.data); break; case 'MESSAGE': if (APP.history) { ooChannel.historyLastHash = obj.data.hash; ooChannel.currentIndex++; return; } if (ooChannel.ready) { // In read-only mode, push the message to the queue and prompt // the user to refresh OO (without reloading the page) /*if (readOnly) { ooChannel.queue.push(obj.data); if (APP.refreshPopup) { return; } APP.refreshPopup = true; // Don't "spam" the user instantly and no more than // 1 popup every 15s APP.refreshRoTo = setTimeout(refreshReadOnly, READONLY_REFRESH_TO); return; }*/ ooChannel.send(obj.data.msg); ooChannel.lastHash = obj.data.hash; ooChannel.cpIndex++; } else { ooChannel.queue.push(obj.data); } break; case 'HISTORY_SYNCED': if (typeof(APP.onHistorySynced) !== "function") { return; } APP.onHistorySynced(); delete APP.onHistorySynced; break; } }); }; var getParticipants = function () { var users = metadataMgr.getMetadata().users; var i = 1; var p = Object.keys(content.ids || {}).map(function (id) { var nId = id.slice(0,32); if (!users[nId]) { return; } var ooId = content.ids[id].ooid; var idx = content.ids[id].index; if (!ooId || ooId === myOOId) { return; } if (idx >= i) { i = idx + 1; } return { id: String(ooId) + idx, idOriginal: String(ooId), username: (users[nId] || {}).name || Messages.anonymous, indexUser: idx, connectionId: content.ids[id].netflux || Hash.createChannelId(), isCloseCoAuthoring:false, view: false }; }); // Add an history keeper user to show that we're never alone var hkId = Util.createRandomInteger(); p.push({ id: hkId, idOriginal: String(hkId), username: "History", indexUser: i, connectionId: Hash.createChannelId(), isCloseCoAuthoring:false, view: false }); i++; if (!myUniqueOOId) { myUniqueOOId = String(myOOId) + i; } p.push({ id: myUniqueOOId, idOriginal: String(myOOId), username: metadataMgr.getUserData().name || Messages.anonymous, indexUser: i, connectionId: metadataMgr.getNetfluxId() || Hash.createChannelId(), isCloseCoAuthoring:false, view: false }); return { index: i, list: p.filter(Boolean) }; }; // 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; } }; 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 }, null, function (err, hash) { if (err) { return void console.error(err); } if (pendingChanges[uid]) { clearTimeout(pendingChanges[uid]); delete pendingChanges[uid]; } // 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._SerializeHistoryBase64(oMemory, item, aRes); }); // Make the patch var msg = { type: "saveChanges", changes: parseChanges(JSON.stringify(aRes)), changesIndex: ooChannel.cpIndex || 0, 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++; }); }; 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, '*'); }; 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") { // XXX console.error(obj); return; } debug(obj, 'toOO'); chan.event('CMD', obj); }; 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": cursor.updateCursor({ type: "cursor", messages: [{ cursor: obj.cursor, time: +new Date(), user: myUniqueOOId, useridoriginal: myOOId }] }); 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) { 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; } console.error(APP.isFast, APP.themeLocked); 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 { var docs = window.frames[0].AscCommon.g_oDocumentUrls.urls || {}; var mediasSources = getMediasSources(); Object.keys(mediasSources).forEach(function (name) { if (!docs['media/'+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; } }); }); }; var x2tConvertData = function (data, fileName, format, cb) { var sframeChan = common.getSframeChannel(); var e = getEditor(); var fonts = e && e.FontLoader.fontInfos; var files = e && e.FontLoader.fontFiles.map(function (f) { return { 'Id': f.Id, }; }); var type = common.getMetadataMgr().getPrivateData().ooType; sframeChan.query('Q_OO_CONVERT', { data: data, type: type, fileName: fileName, outputFormat: format, images: (e && window.frames[0].AscCommon.g_oDocumentUrls.urls) || {}, fonts: fonts, fonts_files: files, mediasSources: getMediasSources(), mediasData: mediasData }, function (err, obj) { if (err || !obj || !obj.data) { UI.warn(Messages.error); return void cb(); } 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 = {}; var firstOO = true; startOO = function (blob, file, force) { if (APP.ooconfig && !force) { return void console.error('already started'); } var url = URL.createObjectURL(blob); var lock = !APP.history && (APP.migrate); var 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"; // Config APP.ooconfig = { "document": { "fileType": file.type, "key": "fresh", "title": file.title, "url": url, "permissions": { "download": false, "print": true, } }, "documentType": file.doc, "editorConfig": { customization: { chat: false, logo: { url: "/bounce/#" + encodeURIComponent('https://www.onlyoffice.com') } }, "user": { "id": String(myOOId), //"c0c3bf82-20d7-4663-bf6d-7fa39c598b1d", "firstname": metadataMgr.getUserData().name || Messages.anonymous, "name": metadataMgr.getUserData().name || Messages.anonymous, }, "mode": "edit", "lang": (window.cryptpadLanguage || navigator.language || navigator.userLanguage || '').slice(0,2) }, "events": { "onAppReady": function(/*evt*/) { var $iframe = $('iframe[name="frameEditor"]').contents(); $iframe.prop('tabindex', '-1'); var $tb = $iframe.find('head'); 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 } $('