define([ 'jquery', '/common/toolbar3.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/hyperscript.js', '/api/config', '/customize/messages.js', '/customize/application_config.js', '/bower_components/chainpad/chainpad.dist.js', '/file/file-crypto.js', '/common/onlyoffice/oocell_base.js', '/common/onlyoffice/oodoc_base.js', '/common/onlyoffice/ooslide_base.js', '/common/outer/worker-channel.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, h, ApiConfig, Messages, AppConfig, ChainPad, FileCrypto, EmptyCell, EmptyDoc, EmptySlide, Channel) { var saveAs = window.saveAs; var Nacl = window.nacl; var APP = window.APP = { $: $ }; var CHECKPOINT_INTERVAL = 50; var DISPLAY_RESTORE_BUTTON = false; var NEW_VERSION = 2; var debug = function (x) { if (!window.CP_DEV_MODE) { return; } console.debug(x); }; var stringify = function (obj) { return JSONSortify(obj); }; var toolbar; 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 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(); // This structure is used for caching media data and blob urls for each media cryptpad url var mediasData = {}; var getMediasSources = APP.getMediasSources = function() { content.mediasSources = content.mediasSources || {}; return content.mediasSources; }; var getId = function () { return metadataMgr.getNetfluxId() + '-' + privateData.clientId; }; 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 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(); } }; var getFileType = function () { var type = common.getMetadataMgr().getPrivateData().ooType; var title = common.getMetadataMgr().getMetadataLazy().title; var file = {}; switch(type) { case 'oodoc': 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 'ooslide': file.type = 'pptx'; file.title = title + '.pptx' || 'presentation.pptx'; file.doc = 'presentation'; break; } return file; }; var now = function () { return +new Date(); }; var getLastCp = function (old) { var hashes = old ? oldHashes : content.hashes; if (!hashes || !Object.keys(hashes).length) { return {}; } var lastIndex = Math.max.apply(null, Object.keys(hashes).map(Number)); var last = JSON.parse(JSON.stringify(hashes[lastIndex])); return last; }; var rtChannel = { ready: false, readyCb: undefined, sendCmd: function (data, cb) { sframeChan.query('Q_OO_COMMAND', data, cb); }, sendMsg: function (msg, cp, cb) { rtChannel.sendCmd({ cmd: 'SEND_MESSAGE', data: { msg: msg, isCp: cp } }, cb); }, }; var ooChannel = { ready: false, queue: [], send: function () {}, cpIndex: 0 }; var getContent = function () { try { return window.frames[0].editor.asc_nativeGetFile(); } catch (e) { console.error(e); return; } }; // 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 () { try { var editor = window.frames[0].editor; // 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); } }; var onUploaded = function (ev, data, err) { if (err) { console.error(err); return void UI.alert(Messages.oo_saveError); } var i = Math.floor(ev.index / CHECKPOINT_INTERVAL); content.hashes[i] = { file: data.url, hash: ev.hash, index: ev.index }; oldHashes = JSON.parse(JSON.stringify(content.hashes)); content.saveLock = undefined; // 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 () { fixSheets(); 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; } sframeChan.query('Q_OO_SAVE', data, function (err) { onUploaded(ev, data, err); }); } }; APP.FM = common.createFileManager(fmConfig); var saveToServer = function () { var text = getContent(); var blob = new Blob([text], {type: 'plain/text'}); var file = getFileType(); blob.name = (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type; var data = { hash: ooChannel.lastHash, index: ooChannel.cpIndex }; APP.FM.handleFile(blob, data); }; var makeCheckpoint = function (force) { if (!common.isLoggedIn()) { return; } var locked = content.saveLock; if (!locked || !isUserOnline(locked) || force) { content.saveLock = myOOId; APP.onLocal(); APP.realtime.onSettle(function () { saveToServer(); }); return; } // The save is locked by someone else. If no new checkpoint is created // in the next 20 to 40 secondes and the lock is kept by the same user, // force the lock and make a checkpoint. var saved = stringify(content.hashes); var to = 20000 + (Math.random() * 20000); setTimeout(function () { if (stringify(content.hashes) === saved && locked === content.saveLock) { makeCheckpoint(force); } }, to); }; var restoreLastCp = function () { content.saveLock = myOOId; APP.onLocal(); APP.realtime.onSettle(function () { onUploaded({ hash: ooChannel.lastHash, index: ooChannel.cpIndex }, { url: getLastCp().file, }); }); }; 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': break; case 'LEAVE': removeClient(obj.data); break; case 'MESSAGE': if (ooChannel.ready) { ooChannel.send(obj.data.msg); ooChannel.lastHash = obj.data.hash; ooChannel.cpIndex++; } else { ooChannel.queue.push(obj.data); } break; } }); cb(); }; 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); 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 }; }); 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 getLock = function () { return Object.keys(content.locks).map(function (id) { return content.locks[id]; }); }; // 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) { Object.keys(n).forEach(function (id) { // New lock if (!o[id]) { ooChannel.send({ type: "getLock", locks: getLock() }); return; } // Updated lock if (stringify(n[id]) !== stringify(o[id])) { ooChannel.send({ type: "releaseLock", locks: [o[id]] }); ooChannel.send({ type: "getLock", locks: getLock() }); } }); Object.keys(o).forEach(function (id) { // Removed lock if (!n[id]) { ooChannel.send({ type: "releaseLock", locks: [o[id]] }); return; } }); }; // 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) { ooChannel.send({ type: "releaseLock", locks: [locks[id]] }); delete content.locks[id]; } }); }; var handleAuth = function (obj, send) { // OO is ready ooChannel.ready = true; // Get the content pushed after the latest checkpoint var changes = []; ooChannel.queue.forEach(function (data) { Array.prototype.push.apply(changes, data.msg.changes); }); ooChannel.lastHash = getLastCp().hash; 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}} }); // Update current index var last = ooChannel.queue.pop(); if (last) { ooChannel.lastHash = last.hash; } ooChannel.cpIndex += ooChannel.queue.length; // Apply existing locks deleteOfflineLocks(); APP.onLocal(); handleNewLocks(oldLocks, content.locks || {}); 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, }); }; // Add a lock var handleLock = function (obj, send) { content.locks = content.locks || {}; // Send the lock to other users var msg = { time: now(), user: myUniqueOOId, block: obj.block && obj.block[0], }; var myId = getId(); content.locks[myId] = msg; oldLocks = JSON.parse(JSON.stringify(content.locks)); // Answer to our onlyoffice send({ type: "getLock", locks: getLock() }); // Remove old locks deleteOfflineLocks(); // Commit APP.onLocal(); }; var parseChanges = function (changes) { try { changes = JSON.parse(changes); } catch (e) { return []; } return changes.map(function (change) { return { docid: "fresh", change: '"' + change + '"', time: now(), user: myUniqueOOId, useridoriginal: String(myOOId) }; }); }; var handleChanges = function (obj, send) { // Allow the changes send({ type: "unSaveLock", index: ooChannel.cpIndex, time: +new Date() }); // Send the changes rtChannel.sendMsg({ type: "saveChanges", changes: parseChanges(obj.changes), changesIndex: ooChannel.cpIndex || 0, locks: [content.locks[getId()]], excelAdditionalInfo: null }, null, function (err, hash) { if (err) { return void console.error(err); } // Increment index and update latest hash ooChannel.cpIndex++; ooChannel.lastHash = hash; // Check if a checkpoint is needed var lastCp = getLastCp(); if (common.isLoggedIn() && (ooChannel.cpIndex % CHECKPOINT_INTERVAL === 0 || (ooChannel.cpIndex - lastCp.index) > CHECKPOINT_INTERVAL)) { makeCheckpoint(); } // Remove my lock delete content.locks[getId()]; oldLocks = JSON.parse(JSON.stringify(content.locks)); APP.onLocal(); }); }; var makeChannel = function () { var msgEv = Util.mkEvent(); var iframe = $('#cp-app-oo-editor > iframe')[0].contentWindow; 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) { debug(obj); chan.event('CMD', obj); }; chan.on('CMD', function (obj) { debug(obj); switch (obj.type) { case "auth": handleAuth(obj, send); break; case "isSaveLock": // TODO ping the server to check if we're online first? send({ type: "saveLock", saveLock: false }); break; case "getLock": handleLock(obj, send); break; case "getMessages": // OO chat messages? send({ type: "message" }); break; case "saveChanges": // 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: [content.locks[getId()]] }); delete content.locks[getId()]; APP.onLocal(); } if (obj.isSave) { send({ type: "unSaveLock", time: -1, index: -1 }); } break; } }); }); }; var ooLoaded = false; var startOO = function (blob, file) { if (APP.ooconfig) { return void console.error('already started'); } var url = URL.createObjectURL(blob); var lock = readOnly || APP.migrate; // Config APP.ooconfig = { "document": { "fileType": file.type, "key": "fresh", "title": file.title, "url": url, "permissions": { "download": false, "print": false, } }, "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, }, "mode": readOnly || lock ? "view" : "edit", "lang": (navigator.language || navigator.userLanguage || '').slice(0,2) }, "events": { "onAppReady": function(/*evt*/) { var $tb = $('iframe[name="frameEditor"]').contents().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; }' + // New OO: '#asc-gen566 { display: none !important; }' + // Insert image from url 'section[data-tab="ins"] .separator:nth-last-child(2) { display: none !important; }' + // separator '#slot-btn-insequation { display: none !important; }' + // Insert equation '.toolbar .tabs .ribtab:not(.canedit) { display: none !important; }' + // Switch collaborative mode '#app-title { display: none !important; }' + // OnlyOffice logo + doc title '#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 '#file-menu-panel .devider { display: none !important; }' + // separator in the "File" menu '#file-menu-panel { top: 28px !important; }' + // Position of 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; }'; } $('<style>').text(css).appendTo($tb); setTimeout(function () { $(window).trigger('resize'); }); if (UI.findOKButton().length) { UI.findOKButton().on('focusout', function () { window.setTimeout(function () { UI.findOKButton().focus(); }); }); } }, "onDocumentReady": function () { if (APP.migrate && !readOnly) { var div = h('div.cp-oo-x2tXls', [ h('span.fa.fa-spin.fa-spinner'), h('span', Messages.oo_sheetMigration_loading) ]); UI.openCustomModal(UI.dialog.customModal(div, {buttons: []})); setTimeout(function () { makeCheckpoint(true); }, 1000); } } } }; window.onbeforeunload = function () { var ifr = document.getElementsByTagName('iframe')[0]; if (ifr) { ifr.remove(); } }; common.initFilePicker({ onSelect: 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(); mediasSources[name] = data; APP.onLocal(); APP.realtime.onSettle(function () { APP.getImageURL(name, function(url) { debug("CRYPTPAD success add " + name); APP.AddImageSuccessCallback({ name: name, url: url }); }); }); } }); APP.AddImage = function(cb1, cb2) { APP.AddImageSuccessCallback = cb1; APP.AddImageErrorCallback = cb2; common.openFilePicker({ types: ['file'], where: ['root'], filter: { fileType: ['image/'] } }); }; APP.loadingImage = 0; APP.getImageURL = function(name, callback) { var mediasSources = getMediasSources(); var data = mediasSources[name]; if (typeof data === 'undefined') { debug("CryptPad - could not find matching media for " + name); return void callback(""); } var blobUrl = (typeof mediasData[data.src] === 'undefined') ? "" : mediasData[data.src].src; if (blobUrl) { debug("CryptPad Image already loaded " + 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 : "" }; mediasData[data.src] = mediaData; var reader = new FileReader(); reader.onloadend = function () { debug("MediaData set"); mediaData.content = reader.result; }; 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(""); } }); }; APP.docEditor = new window.DocsAPI.DocEditor("cp-app-oo-placeholder", APP.ooconfig); ooLoaded = true; makeChannel(); }; var x2tInitialized = false; var x2tInit = function(x2t) { debug("x2t mount"); // x2t.FS.mount(x2t.MEMFS, {} , '/'); x2t.FS.mkdir('/working'); x2t.FS.mkdir('/working/media'); x2tInitialized = true; debug("x2t mount done"); }; /* Converting Data This function converts a data in a specific format to the outputformat The filename extension needs to represent the input format Example: fileName=cryptpad.bin outputFormat=xlsx */ var x2tConvertDataInternal = function(x2t, data, fileName, outputFormat) { debug("Converting Data for " + fileName + " to " + outputFormat); // writing file to mounted working disk (in memory) x2t.FS.writeFile('/working/' + fileName, data); // Adding images Object.keys(window.frames[0].AscCommon.g_oDocumentUrls.urls || {}).forEach(function (_mediaFileName) { var mediaFileName = _mediaFileName.substring(6); var mediasSources = getMediasSources(); var mediaSource = mediasSources[mediaFileName]; var mediaData = mediaSource ? mediasData[mediaSource.src] : undefined; if (mediaData) { debug("Writing media data " + mediaFileName); debug("Data"); var fileData = mediaData.content; x2t.FS.writeFile('/working/media/' + mediaFileName, new Uint8Array(fileData)); } else { debug("Could not find media content for " + mediaFileName); } }); var params = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" + "<TaskQueueDataConvert xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" + "<m_sFileFrom>/working/" + fileName + "</m_sFileFrom>" + "<m_sFileTo>/working/" + fileName + "." + outputFormat + "</m_sFileTo>" + "<m_bIsNoBase64>false</m_bIsNoBase64>" + "</TaskQueueDataConvert>"; // writing params file to mounted working disk (in memory) x2t.FS.writeFile('/working/params.xml', params); // running conversion x2t.ccall("runX2T", ["number"], ["string"], ["/working/params.xml"]); // reading output file from working disk (in memory) var result; try { result = x2t.FS.readFile('/working/' + fileName + "." + outputFormat); } catch (e) { debug("Failed reading converted file"); UI.removeModals(); UI.warn(Messages.error); return ""; } return result; }; var x2tSaveAndConvertDataInternal = function(x2t, data, filename, extension, finalFilename) { var type = common.getMetadataMgr().getPrivateData().ooType; var xlsData; if (type === "sheet" && extension !== 'xlsx') { xlsData = x2tConvertDataInternal(x2t, data, filename, 'xlsx'); filename += '.xlsx'; } else if (type === "ooslide" && extension !== "pptx") { xlsData = x2tConvertDataInternal(x2t, data, filename, 'pptx'); filename += '.pptx'; } else if (type === "oodoc" && extension !== "docx") { xlsData = x2tConvertDataInternal(x2t, data, filename, 'docx'); filename += '.docx'; } xlsData = x2tConvertDataInternal(x2t, data, filename, extension); if (xlsData) { var blob = new Blob([xlsData], {type: "application/bin;charset=utf-8"}); UI.removeModals(); saveAs(blob, finalFilename); } }; var x2tSaveAndConvertData = function(data, filename, extension, finalFilename) { // Perform the x2t conversion require(['/common/onlyoffice/x2t/x2t.js'], function() { var x2t = window.Module; x2t.run(); if (x2tInitialized) { debug("x2t runtime already initialized"); x2tSaveAndConvertDataInternal(x2t, data, filename, extension, finalFilename); } x2t.onRuntimeInitialized = function() { debug("x2t in runtime initialized"); // Init x2t js module x2tInit(x2t); x2tSaveAndConvertDataInternal(x2t, data, filename, extension, finalFilename); }; }); }; var exportXLSXFile = function() { var text = getContent(); var suggestion = Title.suggestTitle(Title.defaultTitle); var ext = ['.xlsx', /*'.ods',*/ '.bin']; var type = common.getMetadataMgr().getPrivateData().ooType; var warning = ''; if (type==="ooslide") { ext = ['.pptx', /*'.odp',*/ '.bin']; } else if (type==="oodoc") { ext = ['.docx', /*'.odt',*/ '.bin']; } if (typeof(Atomics) === "undefined") { ext = ['.bin']; warning = '<div class="alert alert-info cp-alert-top">'+Messages.oo_exportChrome+'</div>'; } var types = ext.map(function (val) { return { tag: 'a', attributes: { 'data-value': val, href: '#' }, 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); UI.prompt(Messages.exportPrompt+warning, Util.fixFileName(suggestion), 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; } var content = h('div.cp-oo-x2tXls', [ h('span.fa.fa-spin.fa-spinner'), h('span', Messages.oo_exportInProgress) ]); UI.openCustomModal(UI.dialog.customModal(content, {buttons: []})); setTimeout(function () { x2tSaveAndConvertData(text, "filename.bin", ext, filename+'.'+ext); }, 100); }, { typeInput: $select[0] }, true); }; var x2tImportImagesInternal = function(x2t, images, i, callback) { if (i >= images.length) { callback(); } else { debug("Import image " + i); var handleFileData = { name: images[i], mediasSources: getMediasSources(), callback: function() { debug("next image"); x2tImportImagesInternal(x2t, images, i+1, callback); }, }; var filePath = "/working/media/" + images[i]; debug("Import filename " + filePath); var fileData = x2t.FS.readFile("/working/media/" + images[i], { encoding : "binary" }); debug("Importing data"); debug("Buffer"); debug(fileData.buffer); var blob = new Blob([fileData.buffer], {type: 'image/png'}); blob.name = images[i]; APP.FMImages.handleFile(blob, handleFileData); } }; var x2tImportImages = function (x2t, 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"); var files = x2t.FS.readdir("/working/media/"); var images = []; files.forEach(function (file) { if (file !== "." && file !== "..") { images.push(file); } }); x2tImportImagesInternal(x2t, images, 0, function() { debug("Sync media sources elements"); debug(getMediasSources()); APP.onLocal(); debug("Import Images finalized"); callback(); }); }; var x2tConvertData = function (x2t, data, filename, extension, callback) { var convertedContent; // Convert from ODF format: // first convert to Office format then to the selected extension if (filename.endsWith(".ods")) { convertedContent = x2tConvertDataInternal(x2t, new Uint8Array(data), filename, "xlsx"); convertedContent = x2tConvertDataInternal(x2t, convertedContent, filename + ".xlsx", extension); } else if (filename.endsWith(".odt")) { convertedContent = x2tConvertDataInternal(x2t, new Uint8Array(data), filename, "docx"); convertedContent = x2tConvertDataInternal(x2t, convertedContent, filename + ".docx", extension); } else if (filename.endsWith(".odp")) { convertedContent = x2tConvertDataInternal(x2t, new Uint8Array(data), filename, "pptx"); convertedContent = x2tConvertDataInternal(x2t, convertedContent, filename + ".pptx", extension); } else { convertedContent = x2tConvertDataInternal(x2t, new Uint8Array(data), filename, extension); } x2tImportImages(x2t, function() { callback(convertedContent); }); }; var importFile = function(content) { // Abort if there is another real user in the channel (history keeper excluded) var m = metadataMgr.getChannelMembers().slice().filter(function (nId) { return nId.length === 32; }); if (m.length > 1) { UI.removeModals(); return void UI.alert(Messages.oo_cantUpload); } if (!content) { UI.removeModals(); return void UI.alert(Messages.oo_invalidFormat); } var blob = new Blob([content], {type: 'plain/text'}); var file = getFileType(); blob.name = (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type; var uploadedCallback = function() { UI.removeModals(); UI.confirm(Messages.oo_uploaded, function (yes) { try { window.frames[0].editor.setViewModeDisconnect(); } catch (e) {} if (!yes) { return; } common.gotoURL(); }); }; var data = { hash: ooChannel.lastHash, index: ooChannel.cpIndex, callback: uploadedCallback }; APP.FM.handleFile(blob, data); }; var importXLSXFile = function(content, filename, ext) { // Perform the x2t conversion debug("Filename"); debug(filename); if (ext === "bin") { return void importFile(content); } if (typeof(Atomics) === "undefined") { return void UI.alert(Messages.oo_invalidFormat); } var div = h('div.cp-oo-x2tXls', [ h('span.fa.fa-spin.fa-spinner'), h('span', Messages.oo_importInProgress) ]); UI.openCustomModal(UI.dialog.customModal(div, {buttons: []})); setTimeout(function () { require(['/common/onlyoffice/x2t/x2t.js'], function() { var x2t = window.Module; x2t.run(); if (x2tInitialized) { debug("x2t runtime already initialized"); x2tConvertData(x2t, new Uint8Array(content), filename.name, "bin", function(convertedContent) { importFile(convertedContent); }); } x2t.onRuntimeInitialized = function() { debug("x2t in runtime initialized"); // Init x2t js module x2tInit(x2t); x2tConvertData(x2t, new Uint8Array(content), filename.name, "bin", function(convertedContent) { importFile(convertedContent); }); }; }); }, 100); }; var loadLastDocument = function () { var lastCp = getLastCp(); if (!lastCp) { return; } ooChannel.cpIndex = lastCp.index || 0; 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)) { 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) { return void console.error(err); } var blob = new Blob([decrypted.content], {type: 'plain/text'}); startOO(blob, getFileType()); }); } }; xhr.send(null); }; var loadDocument = function (noCp, useNewDefault) { if (ooLoaded) { return; } var type = common.getMetadataMgr().getPrivateData().ooType; var file = getFileType(); if (!noCp) { // Load latest checkpoint return void loadLastDocument(); } var newText; switch (type) { case 'sheet' : newText = EmptyCell(useNewDefault); break; case 'oodoc': newText = EmptyDoc(); break; case 'ooslide': newText = EmptySlide(); break; default: newText = ''; } var blob = new Blob([newText], {type: 'text/plain'}); startOO(blob, file); }; var initializing = true; var $bar = $('#cp-toolbar'); var cpNfInner; 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 setEditable = function (state) { if (!state) { try { window.frames[0].editor.setViewModeDisconnect(true); } catch (e) {} } debug(state); }; 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); } }; 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(); }; config.onInit = function (info) { readOnly = metadataMgr.getPrivateData().readOnly; Title = common.createTitle({}); var configTb = { displayed: [ 'chat', 'userlist', 'title', 'useradmin', 'spinner', 'newpad', 'share', 'limit', 'unpinnedWarning', 'notifications' ], title: Title.getTitleConfig(), metadataMgr: metadataMgr, readOnly: readOnly, realtime: info.realtime, sfCommon: common, $container: $bar, $contentContainer: $('#cp-app-oo-container') }; toolbar = APP.toolbar = Toolbar.create(configTb); Title.setToolbar(toolbar); var $rightside = toolbar.$rightside; if (window.CP_DEV_MODE) { var $save = common.createButton('save', true, {}, function () { saveToServer(); }); $save.appendTo($rightside); } if (window.CP_DEV_MODE || DISPLAY_RESTORE_BUTTON) { common.createButton('', true, { name: 'restore', icon: 'fa-history', hiddenReadOnly: true }).click(function () { if (initializing) { return void console.error('initializing'); } restoreLastCp(); }).attr('title', 'Restore last checkpoint').appendTo($rightside); } var $exportXLSX = common.createButton('export', true, {}, exportXLSXFile); $exportXLSX.appendTo($rightside); var accept = [".bin", ".ods", ".xlsx"]; if (typeof(Atomics) === "undefined") { accept = ['.bin']; } var $importXLSX = common.createButton('import', true, { accept: accept, binary : ["ods", "xlsx"] }, importXLSXFile); $importXLSX.appendTo($rightside); if (common.isLoggedIn()) { common.createButton('hashtag', true).appendTo($rightside); } var $forget = common.createButton('forget', true, {}, function (err) { if (err) { return; } setEditable(false); }); $rightside.append($forget); var helpMenu = APP.helpMenu = common.createHelpMenu(['beta', 'oo']); $('#cp-app-oo-editor').prepend(common.getBurnAfterReadingWarning()); $('#cp-app-oo-editor').prepend(helpMenu.menu); toolbar.$drawer.append(helpMenu.button); var $properties = common.createButton('properties', true); toolbar.$drawer.append($properties); }; 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', { url: newLatest.file }, function () { }); newDoc = !content.hashes || Object.keys(content.hashes).length === 0; } else { Title.updateTitle(Title.defaultTitle); } var version = 'v2/'; // 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 can start the migration if (common.isLoggedIn()) { content.migration = true; APP.onLocal(); } else { var msg = h('div.alert.alert-warning.cp-burn-after-reading', Messages.oo_sheetMigration_anonymousEditor); $(APP.helpMenu.menu).after(msg); readOnly = true; } } var s = h('script', { type:'text/javascript', src: '/common/onlyoffice/'+version+'web-apps/apps/api/documents/api.js' }); $('#cp-app-oo-editor').append(s); if (metadataMgr.getPrivateData().burnAfterReading && content && content.channel) { sframeChan.event('EV_BURN_PAD', content.channel); } var useNewDefault = content.version && content.version >= 2; openRtChannel(function () { setMyId(); oldHashes = JSON.parse(JSON.stringify(content.hashes)); loadDocument(newDoc, useNewDefault); initializing = false; setEditable(!readOnly); UI.removeLoadingScreen(); common.openPadChat(APP.onLocal); }); }; config.onError = function (err) { common.onServerError(err, toolbar, function () { setEditable(false); }); }; var reloadPopup = false; config.onRemote = function () { if (initializing) { return; } var userDoc = APP.realtime.getUserDoc(); var json = JSON.parse(userDoc); if (json.metadata) { metadataMgr.updateMetadata(json.metadata); } var wasMigrating = content.migration; content = json.content; if (content.hashes) { var latest = getLastCp(true); var newLatest = getLastCp(); if (newLatest.index > latest.index) { fixSheets(); sframeChan.query('Q_OO_SAVE', { url: newLatest.file }, function () { }); } oldHashes = JSON.parse(JSON.stringify(content.hashes)); } if (content.ids) { handleNewIds(oldIds, content.ids); oldIds = JSON.parse(JSON.stringify(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.onAbort = function () { // inform of network disconnect setEditable(false); toolbar.failed(); UI.alert(Messages.common_connectionLost, undefined, true); }; config.onConnectionChange = function (info) { setEditable(info.state); if (info.state) { UI.findOKButton().click(); UI.confirm(Messages.oo_reconnect, function (yes) { if (!yes) { return; } common.gotoURL(); }); } else { offline = true; UI.findOKButton().click(); UI.alert(Messages.common_connectionLost, undefined, true); } }; 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.handleNewFile(waitFor, { noTemplates: true }); }).nThen(function (/*waitFor*/) { andThen(common); }); }; main(); });