diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index cb70986a4..1fe900e52 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -1275,7 +1275,7 @@ define([ hide.push('preview'); } if ($element.is('.cp-border-color-sheet')) { - hide.push('download'); + //hide.push('download'); // XXX if we don't want to enable this feature yet } if ($element.is('.cp-app-drive-static')) { hide.push('access', 'hashtag', 'properties', 'download'); diff --git a/www/common/make-backup.js b/www/common/make-backup.js index 3f66c1310..bcabd9be6 100644 --- a/www/common/make-backup.js +++ b/www/common/make-backup.js @@ -28,7 +28,7 @@ define([ return n; }; - var transform = function (ctx, type, sjson, cb) { + var transform = function (ctx, type, sjson, cb, padData) { var result = { data: sjson, ext: '.json', @@ -45,7 +45,7 @@ define([ result.ext = Exporter.ext || ''; result.data = data; cb(result); - }); + }, null, ctx.sframeChan, padData); }, function () { cb(result); }); @@ -117,6 +117,10 @@ define([ var opts = { password: pData.password }; + var padData = { + hash: parsed.hash, + password: pData.password + }; var handler = ctx.sframeChan.on("EV_CRYPTGET_PROGRESS", function (data) { if (data.hash !== parsed.hash) { return; } updateProgress.progress(data.progress); @@ -136,14 +140,14 @@ define([ if (cancelled) { return; } if (!res.data) { return; } var dl = function () { - saveAs(res.data, Util.fixFileName(name)); + saveAs(res.data, Util.fixFileName(name)+(res.ext || '')); }; cb(null, { metadata: res.metadata, content: res.data, download: dl }); - }); + }, padData); }); return { cancel: cancel @@ -228,6 +232,9 @@ define([ zip.file(fileName, res.data, opts); console.log('DONE ---- ' + fileName); setTimeout(done, 500); + }, { + hash: parsed.hash, + password: fData.password }); }); }; @@ -292,7 +299,7 @@ define([ }; // Main function. Create the empty zip and fill it starting from drive.root - var create = function (data, getPad, fileHost, cb, progress, cache) { + var create = function (data, getPad, fileHost, cb, progress, cache, sframeChan) { if (!data || !data.uo || !data.uo.drive) { return void cb('EEMPTY'); } var sem = Saferphore.create(5); var ctx = { @@ -307,7 +314,8 @@ define([ updateProgress: progress, max: 0, done: 0, - cache: cache + cache: cache, + sframeChan: sframeChan }; var filesData = data.sharedFolderId && ctx.sf[data.sharedFolderId] ? ctx.sf[data.sharedFolderId].filesData : ctx.data.filesData; progress('reading', -1); // Msg.settings_export_reading @@ -358,7 +366,7 @@ define([ else if (state === "done") { updateProgress.folderProgress(3); } - }, ctx.cache); + }, ctx.cache, ctx.sframeChan); }; var createExportUI = function (origin) { diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index f43404505..26c5aa947 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -254,6 +254,10 @@ define([ 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': @@ -727,6 +731,7 @@ define([ 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'; @@ -735,6 +740,7 @@ define([ 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)) { @@ -786,6 +792,7 @@ define([ } 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); @@ -1345,6 +1352,35 @@ define([ }); }; + var x2tConvertData = function (data, fileName, format, cb) { + var sframeChan = common.getSframeChannel(); + var e = getEditor(); + var fonts = e.FontLoader.fontInfos; + var files = 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: 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 + }); + }; + startOO = function (blob, file, force) { if (APP.ooconfig && !force) { return void console.error('already started'); } var url = URL.createObjectURL(blob); @@ -1425,6 +1461,13 @@ define([ }); } }, + "onError": function () { + console.error(arguments); + if (APP.isDownload) { + var sframeChan = common.getSframeChannel(); + sframeChan.event('EV_OOIFRAME_DONE', ''); + } + }, "onDocumentReady": function () { evOnSync.fire(); var onMigrateRdy = Util.mkEvent(); @@ -1504,6 +1547,16 @@ define([ } } + if (APP.isDownload) { + var bin = getContent(); + x2tConvertData(bin, 'filename.bin', file.type, function (xlsData) { + var sframeChan = common.getSframeChannel(); + sframeChan.event('EV_OOIFRAME_DONE', xlsData, {raw: true}); + }); + return; + } + + if (isLockedModal.modal && force) { isLockedModal.modal.closeModal(); delete isLockedModal.modal; @@ -1701,35 +1754,6 @@ define([ makeChannel(); }; - var x2tConvertData = function (data, fileName, format, cb) { - var sframeChan = common.getSframeChannel(); - var e = getEditor(); - var fonts = e.FontLoader.fontInfos; - var files = 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: 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 - }); - }; - APP.printPdf = function (obj, cb) { var bin = getContent(); x2tConvertData({ @@ -2193,6 +2217,39 @@ define([ }); }; + 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; + 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/' : CURRENT_VERSION+'/'); + var s = h('script', { + type:'text/javascript', + src: '/common/onlyoffice/'+version+'web-apps/apps/api/documents/api.js' + }); + $('#cp-app-oo-editor').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) @@ -2506,6 +2563,7 @@ define([ readOnly = true; } } + // NOTE: don't forget to also update the version in 'EV_OOIFRAME_REFRESH' // If the sheet is locked by an offline user, remove it if (content && content.saveLock && !isUserOnline(content.saveLock)) { diff --git a/www/common/onlyoffice/ooiframe.js b/www/common/onlyoffice/ooiframe.js new file mode 100644 index 000000000..87d5adfed --- /dev/null +++ b/www/common/onlyoffice/ooiframe.js @@ -0,0 +1,175 @@ +// Load #1, load as little as possible because we are in a race to get the loading screen up. +define([ + '/bower_components/nthen/index.js', + '/api/config', + 'jquery', + '/common/requireconfig.js', + '/customize/messages.js', +], function (nThen, ApiConfig, $, RequireConfig, Messages) { + var requireConfig = RequireConfig(); + + var ready = false; + var currentCb; + var queue = []; + + var create = function (config) { + // Loaded in load #2 + var sframeChan; + var refresh = function (data, cb) { + if (currentCb) { + queue.push({data: data, cb: cb}); + return; + } + if (!ready) { + ready = function () { + refresh(data, cb); + }; + return; + } + currentCb = cb; + sframeChan.event('EV_OOIFRAME_REFRESH', data); + }; + nThen(function (waitFor) { + $(waitFor()); + }).nThen(function (waitFor) { + var lang = Messages._languageUsed; + var themeKey = 'CRYPTPAD_STORE|colortheme'; + var req = { + cfg: requireConfig, + req: [ '/common/loading.js' ], + pfx: window.location.origin, + theme: localStorage[themeKey], + themeOS: localStorage[themeKey+'_default'], + lang: lang + }; + window.rc = requireConfig; + window.apiconf = ApiConfig; + $('#sbox-oo-iframe').attr('src', + ApiConfig.httpSafeOrigin + '/sheet/inner.html?' + requireConfig.urlArgs + + '#' + encodeURIComponent(JSON.stringify(req))); + + // This is a cheap trick to avoid loading sframe-channel in parallel with the + // loading screen setup. + var done = waitFor(); + var onMsg = function (msg) { + var data = typeof(msg.data) === "object" ? msg.data : JSON.parse(msg.data); + if (data.q !== 'READY') { return; } + window.removeEventListener('message', onMsg); + var _done = done; + done = function () { }; + _done(); + }; + window.addEventListener('message', onMsg); + }).nThen(function (/*waitFor*/) { + var Cryptpad = config.modules.Cryptpad; + var Utils = config.modules.Utils; + + nThen(function (waitFor) { + // The inner iframe tries to get some data from us every ms (cache, store...). + // It will send a "READY" message and wait for our answer with the correct txid. + // First, we have to answer to this message, otherwise we're going to block + // sframe-boot.js. Then we can start the channel. + var msgEv = Utils.Util.mkEvent(); + var iframe = $('#sbox-oo-iframe')[0].contentWindow; + var postMsg = function (data) { + iframe.postMessage(data, '*'); + }; + var w = waitFor(); + var whenReady = function (msg) { + if (msg.source !== iframe) { return; } + var data = JSON.parse(msg.data); + if (!data.txid) { return; } + // Remove the listener once we've received the READY message + window.removeEventListener('message', whenReady); + // Answer with the requested data + postMsg(JSON.stringify({ txid: data.txid, language: Cryptpad.getLanguage(), localStore: window.localStore, cache: window.cpCache })); + + // Then start the channel + window.addEventListener('message', function (msg) { + if (msg.source !== iframe) { return; } + msgEv.fire(msg); + }); + config.modules.SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) { + sframeChan = sfc; + })); + w(); + }; + window.addEventListener('message', whenReady); + }).nThen(function () { + var updateMeta = function () { + //console.log('EV_METADATA_UPDATE'); + var metaObj; + nThen(function (waitFor) { + Cryptpad.getMetadata(waitFor(function (err, n) { + if (err) { + waitFor.abort(); + return void console.log(err); + } + metaObj = n; + })); + }).nThen(function (/*waitFor*/) { + metaObj.doc = {}; + var additionalPriv = { + fileHost: ApiConfig.fileHost, + loggedIn: Utils.LocalStore.isLoggedIn(), + origin: window.location.origin, + pathname: window.location.pathname, + feedbackAllowed: Utils.Feedback.state, + secureIframe: true, + }; + for (var k in additionalPriv) { metaObj.priv[k] = additionalPriv[k]; } + + sframeChan.event('EV_METADATA_UPDATE', metaObj); + }); + }; + Cryptpad.onMetadataChanged(updateMeta); + sframeChan.onReg('EV_METADATA_UPDATE', updateMeta); + + config.addCommonRpc(sframeChan, true); + + Cryptpad.padRpc.onMetadataEvent.reg(function (data) { + sframeChan.event('EV_RT_METADATA', data); + }); + + sframeChan.on('EV_OOIFRAME_DONE', function (data) { + if (queue.length) { + setTimeout(function () { + var first = queue.shift(); + refresh(first.data, first.cb); + }); + } + if (!currentCb) { return; } + currentCb(data); + currentCb = undefined; + }); + + // X2T + var x2t; + var onConvert = function (obj, cb) { + x2t.convert(obj, cb); + }; + sframeChan.on('Q_OO_CONVERT', function (obj, cb) { + if (x2t) { return void onConvert(obj, cb); } + require(['/common/outer/x2t.js'], function (X2T) { + x2t = X2T.start(); + onConvert(obj, cb); + }); + }); + + sframeChan.onReady(function () { + if (ready === true) { return; } + if (typeof ready === "function") { + ready(); + } + ready = true; + }); + }); + }); + return { + refresh: refresh + }; + }; + return { + create: create + }; +}); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 575971368..ca60c2243 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -72,6 +72,7 @@ define([ var SFrameChannel; var sframeChan; var SecureIframe; + var OOIframe; var Messaging; var Notifier; var Utils = { @@ -98,6 +99,7 @@ define([ '/common/cryptget.js', '/common/outer/worker-channel.js', '/secureiframe/main.js', + '/common/onlyoffice/ooiframe.js', '/common/common-messaging.js', '/common/common-notifier.js', '/common/common-hash.js', @@ -112,7 +114,7 @@ define([ '/common/test.js', '/common/userObject.js', ], waitFor(function (_CpNfOuter, _Cryptpad, _Crypto, _Cryptget, _SFrameChannel, - _SecureIframe, _Messaging, _Notifier, _Hash, _Util, _Realtime, _Notify, + _SecureIframe, _OOIframe, _Messaging, _Notifier, _Hash, _Util, _Realtime, _Notify, _Constants, _Feedback, _LocalStore, _Cache, _AppConfig, _Test, _UserObject) { CpNfOuter = _CpNfOuter; Cryptpad = _Cryptpad; @@ -120,6 +122,7 @@ define([ Cryptget = _Cryptget; SFrameChannel = _SFrameChannel; SecureIframe = _SecureIframe; + OOIframe = _OOIframe; Messaging = _Messaging; Notifier = _Notifier; Utils.Hash = _Hash; @@ -571,6 +574,7 @@ define([ var edPublic, curvePublic, notifications, isTemplate; var settings = {}; var isSafe = ['debug', 'profile', 'drive', 'teams', 'calendar', 'file'].indexOf(currentPad.app) !== -1; + var ooDownloadData = {}; var isDeleted = isNewFile && currentPad.hash.length > 0; if (isDeleted) { @@ -1031,6 +1035,51 @@ define([ } }, cb); }); + sframeChan.on('Q_GET_HISTORY_RANGE', function (data, cb) { + var nSecret = secret; + if (cfg.isDrive) { + // Shared folder or user hash or fs hash + var hash = Utils.LocalStore.getUserHash() || Utils.LocalStore.getFSHash(); + if (data.sharedFolder) { hash = data.sharedFolder.hash; } + if (hash) { + var password = (data.sharedFolder && data.sharedFolder.password) || undefined; + nSecret = Utils.Hash.getSecrets('drive', hash, password); + } + } + if (data.href) { + var _parsed = Utils.Hash.parsePadUrl(data.href); + nSecret = Utils.Hash.getSecrets(_parsed.type, _parsed.hash, data.password); + } + if (data.isDownload && ooDownloadData[data.isDownload]) { + var ooData = ooDownloadData[data.isDownload]; + delete ooDownloadData[data.isDownload]; + nSecret = Utils.Hash.getSecrets('sheet', ooData.hash, ooData.password); + } + var channel = nSecret.channel; + var validate = nSecret.keys.validateKey; + var crypto = Crypto.createEncryptor(nSecret.keys); + Cryptpad.getHistoryRange({ + channel: data.channel || channel, + validateKey: validate, + toHash: data.toHash, + lastKnownHash: data.lastKnownHash + }, function (data) { + cb({ + isFull: data.isFull, + messages: data.messages.map(function (obj) { + // The 3rd parameter "true" means we're going to skip signature validation. + // We don't need it since the message is already validated serverside by hk + return { + msg: crypto.decrypt(obj.msg, true, true), + serverHash: obj.serverHash, + author: obj.author, + time: obj.time + }; + }), + lastKnownHash: data.lastKnownHash + }); + }); + }); }; addCommonRpc(sframeChan, isSafe); @@ -1272,46 +1321,6 @@ define([ }); }); }); - sframeChan.on('Q_GET_HISTORY_RANGE', function (data, cb) { - var nSecret = secret; - if (cfg.isDrive) { - // Shared folder or user hash or fs hash - var hash = Utils.LocalStore.getUserHash() || Utils.LocalStore.getFSHash(); - if (data.sharedFolder) { hash = data.sharedFolder.hash; } - if (hash) { - var password = (data.sharedFolder && data.sharedFolder.password) || undefined; - nSecret = Utils.Hash.getSecrets('drive', hash, password); - } - } - if (data.href) { - var _parsed = Utils.Hash.parsePadUrl(data.href); - nSecret = Utils.Hash.getSecrets(_parsed.type, _parsed.hash, data.password); - } - var channel = nSecret.channel; - var validate = nSecret.keys.validateKey; - var crypto = Crypto.createEncryptor(nSecret.keys); - Cryptpad.getHistoryRange({ - channel: data.channel || channel, - validateKey: validate, - toHash: data.toHash, - lastKnownHash: data.lastKnownHash - }, function (data) { - cb({ - isFull: data.isFull, - messages: data.messages.map(function (obj) { - // The 3rd parameter "true" means we're going to skip signature validation. - // We don't need it since the message is already validated serverside by hk - return { - msg: crypto.decrypt(obj.msg, true, true), - serverHash: obj.serverHash, - author: obj.author, - time: obj.time - }; - }), - lastKnownHash: data.lastKnownHash - }); - }); - }); // Store sframeChan.on('Q_DRIVE_GETDELETED', function (data, cb) { @@ -1461,6 +1470,38 @@ define([ initSecureModal('share', data || {}); }); + // OO iframe + var OOIframeObject = {}; + var initOOIframe = function (cfg, cb) { + if (!OOIframeObject.$iframe) { + var config = {}; + config.addCommonRpc = addCommonRpc; + config.modules = { + Cryptpad: Cryptpad, + SFrameChannel: SFrameChannel, + Utils: Utils + }; + OOIframeObject.$iframe = $('