diff --git a/www/common/common-util.js b/www/common/common-util.js index bec6cb125..e70f4747e 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -575,6 +575,26 @@ return false; }; + // Tell if a file is spreadsheet from its metadata={title, fileType} + Util.isSpreadsheet = function (type, name) { + return (type && + (type === 'application/vnd.oasis.opendocument.spreadsheet' || + type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')) + || (name && (name.endsWith('.xlsx') || name.endsWith('.ods'))); + }; + Util.isOfficeDoc = function (type, name) { + return (type && + (type === 'application/vnd.oasis.opendocument.text' || + type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document')) + || (name && (name.endsWith('.docx') || name.endsWith('.odt'))); + }; + Util.isPresentation = function (type, name) { + return (type && + (type === 'application/vnd.oasis.opendocument.presentation' || + type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation')) + || (name && (name.endsWith('.pptx') || name.endsWith('.odp'))); + }; + Util.isValidURL = function (str) { var pattern = new RegExp('^(https?:\\/\\/)'+ // protocol '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name @@ -618,6 +638,32 @@ getColor().toString(16); }; + /* Chrome 92 dropped support for SharedArrayBuffer in cross-origin contexts + where window.crossOriginIsolated is false. + + Their blog (https://blog.chromium.org/2021/02/restriction-on-sharedarraybuffers.html) + isn't clear about why they're doing this, but since it's related to site-isolation + it seems they're trying to do vague security things. + + In any case, there seems to be a workaround where you can still create them + by using `new WebAssembly.Memory({shared: true, ...})` instead of `new SharedArrayBuffer`. + + This seems unreliable, but it's better than not being able to export, since + we actively rely on postMessage between iframes and therefore can't afford + to opt for full isolation. + */ + var supportsSharedArrayBuffers = function () { + try { + return Object.prototype.toString.call(new window.WebAssembly.Memory({shared: true, initial: 0, maximum: 0}).buffer) === '[object SharedArrayBuffer]'; + } catch (err) { + console.error(err); + } + return false; + }; + Util.supportsWasm = function () { + return !(typeof(Atomics) === "undefined" || !supportsSharedArrayBuffers() || typeof(WebAssembly) === 'undefined'); + }; + if (typeof(module) !== 'undefined' && module.exports) { module.exports = Util; } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index d2f76ef8d..32a7e7111 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -89,7 +89,10 @@ define([ var faShared = 'fa-shhare-alt'; var faReadOnly = 'fa-eye'; var faPreview = 'fa-eye'; - var faOpenInCode = 'cptools-code'; + var faOpenInCode = AppConfig.applicationsIcon.code; + var faOpenInSheet = AppConfig.applicationsIcon.sheet; + var faOpenInDoc = AppConfig.applicationsIcon.doc; + var faOpenInPresentation = AppConfig.applicationsIcon.presentation; var faRename = 'fa-pencil'; var faColor = 'cptools-palette'; var faTrash = 'fa-trash'; @@ -332,6 +335,9 @@ define([ }; + Messages.fc_openInSheet = "Edit in Sheet"; // XXX + Messages.fc_openInDoc = "Edit in Document"; // XXX + Messages.fc_openInPresentation = "Edit in Presentation"; // XXX var createContextMenu = function () { var menu = h('div.cp-contextmenu.dropdown.cp-unselectable', [ h('ul.dropdown-menu', { @@ -356,6 +362,18 @@ define([ 'tabindex': '-1', 'data-icon': faOpenInCode, }, Messages.fc_openInCode)), + h('li', h('a.cp-app-drive-context-openinsheet.dropdown-item', { + 'tabindex': '-1', + 'data-icon': faOpenInSheet, + }, Messages.fc_openInSheet)), + h('li', h('a.cp-app-drive-context-openindoc.dropdown-item', { + 'tabindex': '-1', + 'data-icon': faOpenInDoc, + }, Messages.fc_openInDoc)), + h('li', h('a.cp-app-drive-context-openinpresentation.dropdown-item', { + 'tabindex': '-1', + 'data-icon': faOpenInPresentation, + }, Messages.fc_openInPresentation)), h('li', h('a.cp-app-drive-context-savelocal.dropdown-item', { 'tabindex': '-1', 'data-icon': 'fa-cloud-upload', @@ -1275,7 +1293,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'); @@ -1293,6 +1311,18 @@ define([ if (!metadata || !Util.isPlainTextFile(metadata.fileType, metadata.title)) { hide.push('openincode'); } + if (!metadata || !Util.isSpreadsheet(metadata.fileType, metadata.title) + || !priv.supportsWasm) { + hide.push('openinsheet'); + } + if (!metadata || !Util.isOfficeDoc(metadata.fileType, metadata.title) + || !priv.supportsWasm) { + hide.push('openindoc'); + } + if (!metadata || !Util.isPresentation(metadata.fileType, metadata.title) + || !priv.supportsWasm) { + hide.push('openinpresentation'); + } if (metadata.channel && metadata.channel.length < 48) { hide.push('preview'); } @@ -1309,6 +1339,9 @@ define([ containsFolder = true; hide.push('openro'); hide.push('openincode'); + hide.push('openinsheet'); + hide.push('openindoc'); + hide.push('openinpresentation'); hide.push('hashtag'); //hide.push('delete'); hide.push('makeacopy'); @@ -1324,6 +1357,9 @@ define([ hide.push('savelocal'); hide.push('openro'); hide.push('openincode'); + hide.push('openinsheet'); + hide.push('openindoc'); + hide.push('openinpresentation'); hide.push('properties', 'access'); hide.push('hashtag'); hide.push('makeacopy'); @@ -1355,7 +1391,8 @@ define([ hide.push('download'); hide.push('share'); hide.push('savelocal'); - hide.push('openincode'); // can't because of race condition + //hide.push('openincode'); // can't because of race condition + //hide.push('openinsheet'); // can't because of race condition hide.push('makeacopy'); hide.push('preview'); } @@ -1367,6 +1404,9 @@ define([ if (!APP.loggedIn) { hide.push('openparent'); hide.push('rename'); + hide.push('openinsheet'); + hide.push('openindoc'); + hide.push('openinpresentation'); } filter = function ($el, className) { @@ -1380,11 +1420,12 @@ define([ break; case 'tree': show = ['open', 'openro', 'preview', 'openincode', 'expandall', 'collapseall', - 'color', 'download', 'share', 'savelocal', 'rename', 'delete', 'makeacopy', + 'color', 'download', 'share', 'savelocal', 'rename', 'delete', + 'makeacopy', 'openinsheet', 'openindoc', 'openinpresentation', 'deleteowned', 'removesf', 'access', 'properties', 'hashtag']; break; case 'default': - show = ['open', 'openro', 'preview', 'openincode', 'share', 'download', 'openparent', 'delete', 'deleteowned', 'properties', 'access', 'hashtag', 'makeacopy', 'savelocal', 'rename']; + show = ['open', 'openro', 'preview', 'openincode', 'openinsheet', 'openindoc', 'openinpresentation', 'share', 'download', 'openparent', 'delete', 'deleteowned', 'properties', 'access', 'hashtag', 'makeacopy', 'savelocal', 'rename']; break; case 'trashtree': { show = ['empty']; @@ -4349,6 +4390,21 @@ define([ }); }; + var openInApp = function (paths, app) { + var p = paths[0]; + var el = manager.find(p.path); + var path = currentPath; + if (path[0] !== ROOT) { path = [ROOT]; } + var _metadata = manager.getFileData(el); + var _simpleData = { + title: _metadata.filename || _metadata.title, + href: _metadata.href || _metadata.roHref, + fileType: _metadata.fileType, + password: _metadata.password, + channel: _metadata.channel, + }; + openIn(app, path, APP.team, _simpleData); + }; $contextMenu.on("click", "a", function(e) { e.stopPropagation(); @@ -4436,20 +4492,19 @@ define([ } else if ($this.hasClass('cp-app-drive-context-openincode')) { if (paths.length !== 1) { return; } - var p = paths[0]; - el = manager.find(p.path); - (function () { - var path = currentPath; - if (path[0] !== ROOT) { path = [ROOT]; } - var _metadata = manager.getFileData(el); - var _simpleData = { - title: _metadata.filename || _metadata.title, - href: _metadata.href || _metadata.roHref, - password: _metadata.password, - channel: _metadata.channel, - }; - openIn('code', path, APP.team, _simpleData); - })(); + openInApp(paths, 'code'); + } + else if ($this.hasClass('cp-app-drive-context-openinsheet')) { + if (paths.length !== 1) { return; } + openInApp(paths, 'sheet'); + } + else if ($this.hasClass('cp-app-drive-context-openindoc')) { + if (paths.length !== 1) { return; } + openInApp(paths, 'doc'); + } + else if ($this.hasClass('cp-app-drive-context-openinpresentation')) { + if (paths.length !== 1) { return; } + openInApp(paths, 'presentation'); } else if ($this.hasClass('cp-app-drive-context-expandall') || $this.hasClass('cp-app-drive-context-collapseall')) { diff --git a/www/common/make-backup.js b/www/common/make-backup.js index 3f66c1310..a80663677 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', @@ -41,11 +41,11 @@ define([ } var path = '/' + type + '/export.js'; require([path], function (Exporter) { - Exporter.main(json, function (data) { - result.ext = Exporter.ext || ''; + Exporter.main(json, function (data, _ext) { + result.ext = _ext || Exporter.ext || ''; result.data = data; cb(result); - }); + }, null, ctx.sframeChan, padData); }, function () { cb(result); }); @@ -117,12 +117,16 @@ 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); if (data.progress === 1) { handler.stop(); - updateProgress.progress2(1); + updateProgress.progress2(2); } }); ctx.get({ @@ -136,14 +140,15 @@ 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 || '')); }; + updateProgress.progress2(1); cb(null, { metadata: res.metadata, content: res.data, download: dl }); - }); + }, padData); }); return { cancel: cancel @@ -195,9 +200,16 @@ define([ }); }; + var timeout = 60000; + // OO pads can only be converted one at a time so we have to give them a + // bigger timeout value in case there are 5 of them in the current queue + if (['sheet', 'doc', 'presentation'].indexOf(parsed.type) !== -1) { + timeout = 180000; + } + to = setTimeout(function () { error('TIMEOUT'); - }, 60000); + }, timeout); setTimeout(function () { if (ctx.stop) { return; } @@ -228,6 +240,9 @@ define([ zip.file(fileName, res.data, opts); console.log('DONE ---- ' + fileName); setTimeout(done, 500); + }, { + hash: parsed.hash, + password: fData.password }); }); }; @@ -292,7 +307,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 +322,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 +374,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/history.js b/www/common/onlyoffice/history.js index b8ae4b77d..c0126023c 100644 --- a/www/common/onlyoffice/history.js +++ b/www/common/onlyoffice/history.js @@ -119,7 +119,7 @@ define([ // The first "cp" in history is the empty doc. It doesn't include the first patch // of the history - var initialCp = cpIndex === sortedCp.length; + var initialCp = cpIndex === sortedCp.length || !cp.hash; var messages = (data.messages || []).slice(initialCp ? 0 : 1); diff --git a/www/common/onlyoffice/inner.js b/www/common/onlyoffice/inner.js index 0b1ddbde8..7bbfc7d0d 100644 --- a/www/common/onlyoffice/inner.js +++ b/www/common/onlyoffice/inner.js @@ -20,6 +20,7 @@ define([ '/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', @@ -47,7 +48,8 @@ define([ EmptyCell, EmptyDoc, EmptySlide, - Channel) + Channel, + X2T) { var saveAs = window.saveAs; var Nacl = window.nacl; @@ -60,7 +62,7 @@ define([ var DISPLAY_RESTORE_BUTTON = false; var NEW_VERSION = 4; var PENDING_TIMEOUT = 30000; - var CURRENT_VERSION = 'v4'; + var CURRENT_VERSION = X2T.CURRENT_VERSION; //var READONLY_REFRESH_TO = 15000; var debug = function (x, type) { @@ -72,34 +74,6 @@ define([ return JSONSortify(obj); }; - /* Chrome 92 dropped support for SharedArrayBuffer in cross-origin contexts - where window.crossOriginIsolated is false. - - Their blog (https://blog.chromium.org/2021/02/restriction-on-sharedarraybuffers.html) - isn't clear about why they're doing this, but since it's related to site-isolation - it seems they're trying to do vague security things. - - In any case, there seems to be a workaround where you can still create them - by using `new WebAssembly.Memory({shared: true, ...})` instead of `new SharedArrayBuffer`. - - This seems unreliable, but it's better than not being able to export, since - we actively rely on postMessage between iframes and therefore can't afford - to opt for full isolation. - */ - var supportsSharedArrayBuffers = function () { - try { - return Object.prototype.toString.call(new window.WebAssembly.Memory({shared: true, initial: 0, maximum: 0}).buffer) === '[object SharedArrayBuffer]'; - } catch (err) { - console.error(err); - } - return false; - }; - - var supportsXLSX = function () { - return !(typeof(Atomics) === "undefined" || !supportsSharedArrayBuffers() /* || typeof (SharedArrayBuffer) === "undefined" */ || typeof(WebAssembly) === 'undefined'); - }; - - var toolbar; var cursor; @@ -136,6 +110,10 @@ define([ var startOO = function () {}; + var supportsXLSX = function () { + return privateData.supportsWasm; + }; + var getMediasSources = APP.getMediasSources = function() { content.mediasSources = content.mediasSources || {}; return content.mediasSources; @@ -256,6 +234,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': @@ -499,10 +481,10 @@ define([ startOO(blob, type, true); }; - var saveToServer = function () { + var saveToServer = function (blob, title) { if (APP.cantCheckpoint) { return; } // TOO_LARGE var text = getContent(); - if (!text) { + if (!text && !blob) { setEditable(false, true); sframeChan.query('Q_CLEAR_CACHE_CHANNELS', [ 'chainpad', @@ -513,9 +495,9 @@ define([ }); return; } - var blob = new Blob([text], {type: 'plain/text'}); + blob = blob || new Blob([text], {type: 'plain/text'}); var file = getFileType(); - blob.name = (metadataMgr.getMetadataLazy().title || file.doc) + '.' + file.type; + 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 @@ -729,6 +711,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'; @@ -737,6 +720,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)) { @@ -746,7 +730,7 @@ define([ // The first "cp" in history is the empty doc. It doesn't include the first patch // of the history - var initialCp = major === 0; + var initialCp = major === 0 || !cp.hash; var messages = (data.messages || []).slice(initialCp ? 0 : 1, minor); messages.forEach(function (obj) { @@ -788,6 +772,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); @@ -1422,6 +1407,39 @@ define([ }); }; + 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 = {}; + startOO = function (blob, file, force) { if (APP.ooconfig && !force) { return void console.error('already started'); } var url = URL.createObjectURL(blob); @@ -1502,6 +1520,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(); @@ -1581,6 +1606,25 @@ define([ } } + if (APP.isDownload) { + var bin = getContent(); + if (!supportsXLSX()) { + return void sframeChan.event('EV_OOIFRAME_DONE', bin, {raw: true}); + } + nThen(function (waitFor) { + // wait for all the images to be loaded before converting + Object.keys(downloadImages).forEach(function (name) { + downloadImages[name].reg(waitFor()); + }); + }).nThen(function () { + x2tConvertData(bin, 'filename.bin', file.type, function (xlsData) { + sframeChan.event('EV_OOIFRAME_DONE', xlsData, {raw: true}); + }); + }); + return; + } + + if (isLockedModal.modal && force) { isLockedModal.modal.closeModal(); delete isLockedModal.modal; @@ -1720,6 +1764,7 @@ define([ var mediasSources = getMediasSources(); var data = mediasSources[name]; + downloadImages[name] = Util.mkEvent(true); if (typeof data === 'undefined') { debug("CryptPad - could not find matching media for " + name); @@ -1757,6 +1802,7 @@ define([ reader.onloadend = function () { debug("MediaData set"); mediaData.content = reader.result; + downloadImages[name].fire(); }; reader.readAsArrayBuffer(res.content); debug("Adding CryptPad Image " + data.name + ": " + blobUrl); @@ -1778,239 +1824,52 @@ define([ makeChannel(); }; - var x2tReady = Util.mkEvent(true); - var fetchFonts = function (x2t) { - var path = '/common/onlyoffice/'+CURRENT_VERSION+'/fonts/'; - var e = getEditor(); - var fonts = e.FontLoader.fontInfos; - var files = e.FontLoader.fontFiles; - var suffixes = { - indexR: '', - indexB: '_Bold', - indexBI: '_Bold_Italic', - indexI: '_Italic', - }; - nThen(function (waitFor) { - fonts.forEach(function (font) { - // Check if the font is already loaded - if (!font.NeedStyles) { return; } - // Pick the variants we need (regular, bold, italic) - ['indexR', 'indexB', 'indexI', 'indexBI'].forEach(function (k) { - if (typeof(font[k]) !== "number" || font[k] === -1) { return; } // No matching file - var file = files[font[k]]; - - var name = font.Name + suffixes[k] + '.ttf'; - Util.fetch(path + file.Id, waitFor(function (err, buffer) { - if (buffer) { - x2t.FS.writeFile('/working/fonts/' + name, buffer); - } - })); - }); - }); - }).nThen(function () { - x2tReady.fire(); - }); - }; - - var x2tInitialized = false; - var x2tInit = function(x2t) { - debug("x2t mount"); - // x2t.FS.mount(x2t.MEMFS, {} , '/'); - x2t.FS.mkdir('/working'); - x2t.FS.mkdir('/working/media'); - x2t.FS.mkdir('/working/fonts'); - x2tInitialized = true; - fetchFonts(x2t); - debug("x2t mount done"); - }; - var getX2T = function (cb) { - // Perform the x2t conversion - require(['/common/onlyoffice/x2t/x2t.js'], function() { // FIXME why does this fail without an access-control-allow-origin header? - var x2t = window.Module; - x2t.run(); - if (x2tInitialized) { - debug("x2t runtime already initialized"); - return void x2tReady.reg(function () { - cb(x2t); - }); - } - - x2t.onRuntimeInitialized = function() { - debug("x2t in runtime initialized"); - // Init x2t js module - x2tInit(x2t); - x2tReady.reg(function () { - cb(x2t); - }); - }; - }); - }; - - - /* - 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 getFormatId = function (ext) { - // Sheets - if (ext === 'xlsx') { return 257; } - if (ext === 'xls') { return 258; } - if (ext === 'ods') { return 259; } - if (ext === 'csv') { return 260; } - if (ext === 'pdf') { return 513; } - return; - }; - var getFromId = function (ext) { - var id = getFormatId(ext); - if (!id) { return ''; } - return ''+id+''; - }; - var getToId = function (ext) { - var id = getFormatId(ext); - if (!id) { return ''; } - return ''+id+''; - }; - var x2tConvertDataInternal = function(x2t, data, fileName, outputFormat) { - debug("Converting Data for " + fileName + " to " + outputFormat); - - // PDF - var pdfData = ''; - if (outputFormat === "pdf" && typeof(data) === "object" && data.bin && data.buffer) { - // Add conversion rules - pdfData = "false" + - "/working/fonts/"; - // writing file to mounted working disk (in memory) - x2t.FS.writeFile('/working/' + fileName, data.bin); - x2t.FS.writeFile('/working/pdf.bin', data.buffer); - } else { - // 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 inputFormat = fileName.split('.').pop(); - - var params = "" - + "" - + "/working/" + fileName + "" - + "/working/" + fileName + "." + outputFormat + "" - + pdfData - + getFromId(inputFormat) - + getToId(outputFormat) - + "false" - + ""; - // 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; - }; - APP.printPdf = function (obj, cb) { - getX2T(function (x2t) { - //var e = getEditor(); - //var d = e.asc_nativePrint(undefined, undefined, 0x100 + opts.printType).ImData; - var bin = getContent(); - var xlsData = x2tConvertDataInternal(x2t, { - buffer: obj.data, - bin: bin - }, 'output.bin', 'pdf'); - if (xlsData) { - var md = common.getMetadataMgr().getMetadataLazy(); - var type = common.getMetadataMgr().getPrivateData().ooType; - var title = md.title || md.defaultTitle || type; - var blob = new Blob([xlsData], {type: "application/pdf"}); - //var url = URL.createObjectURL(blob, { type: "application/pdf" }); - saveAs(blob, title+'.pdf'); - //window.open(url); - cb({ - "type":"save", - "status":"ok", - //"data":url + "?disposition=inline&ooname=output.pdf" - }); - /* - ooChannel.send({ - "type":"documentOpen", - "data": { - "type":"save", - "status":"ok", - "data":url + "?disposition=inline&ooname=output.pdf" - } - }); - */ - } + var bin = getContent(); + x2tConvertData({ + buffer: obj.data, + bin: bin + }, 'output.bin', 'pdf', function (xlsData) { + if (!xlsData) { return; } + var md = common.getMetadataMgr().getMetadataLazy(); + var type = common.getMetadataMgr().getPrivateData().ooType; + var title = md.title || md.defaultTitle || type; + var blob = new Blob([xlsData], {type: "application/pdf"}); + saveAs(blob, title+'.pdf'); + cb({ + "type":"save", + "status":"ok", + }); }); }; - var x2tSaveAndConvertDataInternal = function(x2t, data, filename, extension, finalFilename) { + var x2tSaveAndConvertData = function(data, filename, extension, finalFilename) { var type = common.getMetadataMgr().getPrivateData().ooType; - var xlsData; // PDF if (type === "sheet" && extension === "pdf") { var e = getEditor(); var d = e.asc_nativePrint(undefined, undefined, 0x101).ImData; - xlsData = x2tConvertDataInternal(x2t, { + x2tConvertData({ buffer: d.data, bin: data - }, filename, extension); - if (xlsData) { - var _blob = new Blob([xlsData], {type: "application/bin;charset=utf-8"}); - UI.removeModals(); - saveAs(_blob, finalFilename); - } + }, filename, extension, function (res) { + if (res) { + var _blob = new Blob([res], {type: "application/bin;charset=utf-8"}); + UI.removeModals(); + saveAs(_blob, finalFilename); + } + }); return; } - if (type === "sheet" && extension !== 'xlsx') { - xlsData = x2tConvertDataInternal(x2t, data, filename, 'xlsx'); - filename += '.xlsx'; - } else if (type === "presentation" && extension !== "pptx") { - xlsData = x2tConvertDataInternal(x2t, data, filename, 'pptx'); - filename += '.pptx'; - } else if (type === "doc" && 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, finalName) { - getX2T(function (x2t) { - x2tSaveAndConvertDataInternal(x2t, data, filename, extension, finalName); + x2tConvertData(data, filename, extension, function (xlsData) { + if (xlsData) { + var blob = new Blob([xlsData], {type: "application/bin;charset=utf-8"}); + UI.removeModals(); + saveAs(blob, finalFilename); + return; + } + UI.warn(Messages.error); }); }; @@ -2083,32 +1942,29 @@ define([ $select.find('button').addClass('btn'); }; - var x2tImportImagesInternal = function(x2t, images, i, callback) { + var x2tImportImagesInternal = function(images, i, callback) { if (i >= images.length) { callback(); } else { debug("Import image " + i); var handleFileData = { - name: images[i], + name: images[i].name, mediasSources: getMediasSources(), callback: function() { debug("next image"); - x2tImportImagesInternal(x2t, images, i+1, callback); + x2tImportImagesInternal(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"); + var fileData = images[i].data; debug("Buffer"); debug(fileData.buffer); var blob = new Blob([fileData.buffer], {type: 'image/png'}); - blob.name = images[i]; + blob.name = images[i].name; APP.FMImages.handleFile(blob, handleFileData); } }; - var x2tImportImages = function (x2t, callback) { + var x2tImportImages = function (images, callback) { if (!APP.FMImages) { var fmConfigImages = { noHandlers: true, @@ -2134,14 +1990,8 @@ define([ // 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(images); + x2tImportImagesInternal(images, 0, function() { debug("Sync media sources elements"); debug(getMediasSources()); APP.onLocal(); @@ -2151,26 +2001,12 @@ define([ }; - 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")) { - console.log(x2t, data, filename, extension); - convertedContent = x2tConvertDataInternal(x2t, new Uint8Array(data), filename, "xlsx"); - console.log(convertedContent); - 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 x2tImportData = function (data, filename, extension, callback) { + x2tConvertData(new Uint8Array(data), filename, extension, function (binData, images) { + if (!binData) { return void UI.warn(Messages.error); } + x2tImportImages(images, function() { + callback(binData); + }); }); }; @@ -2224,10 +2060,8 @@ define([ ]); UI.openCustomModal(UI.dialog.customModal(div, {buttons: []})); setTimeout(function () { - getX2T(function (x2t) { - x2tConvertData(x2t, new Uint8Array(content), filename.name, "bin", function(c) { - importFile(c); - }); + x2tImportData(new Uint8Array(content), filename.name, "bin", function(c) { + importFile(c); }); }, 100); }; @@ -2453,6 +2287,40 @@ 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; + downloadImages = {}; + var json = data && data.json; + if (!json || !json.content) { + return void sframeChan.event('EV_OOIFRAME_DONE', ''); + } + content = json.content; + readOnly = true; + var version = (!content.version || content.version === 1) ? 'v1/' : + (content.version <= 3 ? 'v2b/' : 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) @@ -2766,6 +2634,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)) { @@ -2853,9 +2722,99 @@ define([ return; } - loadDocument(newDoc, useNewDefault); - setEditable(!readOnly); - UI.removeLoadingScreen(); + var next = function () { + loadDocument(newDoc, useNewDefault); + setEditable(!readOnly); + UI.removeLoadingScreen(); + }; + + if (privateData.isNewFile && privateData.fromFileData) { + try { + (function () { + var data = privateData.fromFileData; + + var type = data.fileType; + var title = data.title; + // Fix extension if the file was renamed + if (Util.isSpreadsheet(type) && !Util.isSpreadsheet(data.title)) { + if (type === 'application/vnd.oasis.opendocument.spreadsheet') { + data.title += '.ods'; + } + if (type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') { + data.title += '.xlsx'; + } + } + if (Util.isOfficeDoc(type) && !Util.isOfficeDoc(data.title)) { + if (type === 'application/vnd.oasis.opendocument.text') { + data.title += '.odt'; + } + if (type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') { + data.title += '.docx'; + } + } + if (Util.isPresentation(type) && !Util.isPresentation(data.title)) { + if (type === 'application/vnd.oasis.opendocument.presentation') { + data.title += '.odp'; + } + if (type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation') { + data.title += '.pptx'; + } + } + + var href = data.href; + var password = data.password; + var parsed = Hash.parsePadUrl(href); + var secret = Hash.getSecrets('file', parsed.hash, password); + var hexFileName = secret.channel; + var fileHost = privateData.fileHost || privateData.origin; + var src = fileHost + Hash.getBlobPathFromHex(hexFileName); + var key = secret.keys && secret.keys.cryptKey; + var xhr = new XMLHttpRequest(); + xhr.open('GET', src, true); + xhr.responseType = 'arraybuffer'; + xhr.onload = function () { + if (/^4/.test('' + this.status)) { + // fallback to empty sheet + console.error(this.status); + return void next(); + } + var arrayBuffer = xhr.response; + if (arrayBuffer) { + var u8 = new Uint8Array(arrayBuffer); + FileCrypto.decrypt(u8, key, function (err, decrypted) { + if (err) { + // fallback to empty sheet + console.error(err); + return void next(); + } + var blobXlsx = decrypted.content; + new Response(blobXlsx).arrayBuffer().then(function (buffer) { + var u8Xlsx = new Uint8Array(buffer); + x2tImportData(u8Xlsx, data.title, 'bin', function (bin) { + var blob = new Blob([bin], {type: 'text/plain'}); + saveToServer(blob, data.title); + Title.updateTitle(title); + UI.removeLoadingScreen(); + }); + }); + }); + } + }; + xhr.onerror = function (err) { + // fallback to empty sheet + console.error(err); + next(); + }; + xhr.send(null); + })(); + } catch (e) { + console.error(e); + next(); + } + return; + } + + next(); }); }; diff --git a/www/common/onlyoffice/main.js b/www/common/onlyoffice/main.js index b7626a589..8b79474ae 100644 --- a/www/common/onlyoffice/main.js +++ b/www/common/onlyoffice/main.js @@ -143,6 +143,22 @@ define([ } sframeChan.event('EV_OO_EVENT', obj); }); + + // 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); + }); + }); + + + }; SFCommonO.start({ hash: hash, diff --git a/www/common/onlyoffice/ooiframe.js b/www/common/onlyoffice/ooiframe.js new file mode 100644 index 000000000..097d21e5e --- /dev/null +++ b/www/common/onlyoffice/ooiframe.js @@ -0,0 +1,176 @@ +// 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, + supportsWasm: Utils.Util.supportsWasm() + }; + 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/outer/x2t.js b/www/common/outer/x2t.js new file mode 100644 index 000000000..55ce85f2c --- /dev/null +++ b/www/common/outer/x2t.js @@ -0,0 +1,245 @@ +define([ + '/api/config', + '/bower_components/nthen/index.js', + '/common/common-util.js', +], function (ApiConfig, nThen, Util) { + var X2T = {}; + + var CURRENT_VERSION = X2T.CURRENT_VERSION = 'v4'; + var debug = function (str) { + if (localStorage.CryptPad_dev !== "1") { return; } + console.debug(str); + }; + + X2T.start = function () { + var x2tReady = Util.mkEvent(true); + var fetchFonts = function (x2t, obj, cb) { + if (!obj.fonts) { return void cb(); } + var path = ApiConfig.httpSafeOrigin + '/common/onlyoffice/'+CURRENT_VERSION+'/fonts/'; + var ver = '?' + ApiConfig.requireConf.urlArgs; + var fonts = obj.fonts; + var files = obj.fonts_files; + var suffixes = { + indexR: '', + indexB: '_Bold', + indexBI: '_Bold_Italic', + indexI: '_Italic', + }; + nThen(function (waitFor) { + fonts.forEach(function (font) { + // Check if the font is already loaded + if (!font.NeedStyles) { return; } + // Pick the variants we need (regular, bold, italic) + ['indexR', 'indexB', 'indexI', 'indexBI'].forEach(function (k) { + if (typeof(font[k]) !== "number" || font[k] === -1) { return; } // No matching file + var file = files[font[k]]; + + var name = font.Name + suffixes[k] + '.ttf'; + Util.fetch(path + file.Id + ver, waitFor(function (err, buffer) { + if (buffer) { + x2t.FS.writeFile('/working/fonts/' + name, buffer); + } + })); + }); + }); + }).nThen(function () { + cb(); + }); + }; + var x2tInitialized = false; + var x2tInit = function(x2t) { + debug("x2t mount"); + // x2t.FS.mount(x2t.MEMFS, {} , '/'); + x2t.FS.mkdir('/working'); + x2t.FS.mkdir('/working/media'); + x2t.FS.mkdir('/working/fonts'); + x2tInitialized = true; + x2tReady.fire(); + debug("x2t mount done"); + }; + var getX2T = function (cb) { + // Perform the x2t conversion + require(['/common/onlyoffice/x2t/x2t.js'], function() { // FIXME why does this fail without an access-control-allow-origin header? + var x2t = window.Module; + x2t.run(); + if (x2tInitialized) { + debug("x2t runtime already initialized"); + return void x2tReady.reg(function () { + cb(x2t); + }); + } + + x2t.onRuntimeInitialized = function() { + debug("x2t in runtime initialized"); + // Init x2t js module + x2tInit(x2t); + x2tReady.reg(function () { + cb(x2t); + }); + }; + }); + }; + + var getFormatId = function (ext) { + // Sheets + if (ext === 'xlsx') { return 257; } + if (ext === 'xls') { return 258; } + if (ext === 'ods') { return 259; } + if (ext === 'csv') { return 260; } + if (ext === 'pdf') { return 513; } + // Docs + if (ext === 'docx') { return 65; } + if (ext === 'doc') { return 66; } + if (ext === 'odt') { return 67; } + if (ext === 'txt') { return 69; } + if (ext === 'html') { return 70; } + + // Slides + if (ext === 'pptx') { return 129; } + if (ext === 'ppt') { return 130; } + if (ext === 'odp') { return 131; } + + return; + }; + var getFromId = function (ext) { + var id = getFormatId(ext); + if (!id) { return ''; } + return ''+id+''; + }; + var getToId = function (ext) { + var id = getFormatId(ext); + if (!id) { return ''; } + return ''+id+''; + }; + + var x2tConvertDataInternal = function(x2t, obj) { + var data = obj.data; + var fileName = obj.fileName; + var outputFormat = obj.outputFormat; + var images = obj.images; + debug("Converting Data for " + fileName + " to " + outputFormat); + + // PDF + var pdfData = ''; + if (outputFormat === "pdf" && typeof(data) === "object" && data.bin && data.buffer) { + // Add conversion rules + pdfData = "false" + + "/working/fonts/"; + // writing file to mounted working disk (in memory) + x2t.FS.writeFile('/working/' + fileName, data.bin); + x2t.FS.writeFile('/working/pdf.bin', data.buffer); + } else { + // writing file to mounted working disk (in memory) + x2t.FS.writeFile('/working/' + fileName, data); + } + + // Adding images + Object.keys(images || {}).forEach(function (_mediaFileName) { + var mediaFileName = _mediaFileName.substring(6); + var mediasSources = obj.mediasSources || {}; + var mediasData = obj.mediasData || {}; + 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 inputFormat = fileName.split('.').pop(); + + var params = "" + + "" + + "/working/" + fileName + "" + + "/working/" + fileName + "." + outputFormat + "" + + pdfData + + getFromId(inputFormat) + + getToId(outputFormat) + + "false" + + ""; + // 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"); + return ""; + } + return result; + }; + + var convert = function (obj, cb) { + getX2T(function (x2t) { + // Fonts + fetchFonts(x2t, obj, function () { + var o = obj.outputFormat; + + if (o !== 'pdf') { + // Add intermediary conversion to Microsoft Office format if needed + // (bin to pdf is allowed) + [ + // Import from Open Document + {source: '.ods', format: 'xlsx'}, + {source: '.odt', format: 'docx'}, + {source: '.odp', format: 'pptx'}, + // Export to non Microsoft Office + {source: '.bin', type: 'sheet', format: 'xlsx'}, + {source: '.bin', type: 'doc', format: 'docx'}, + {source: '.bin', type: 'presentation', format: 'pptx'}, + ].forEach(function (_step) { + if (obj.fileName.endsWith(_step.source) && obj.outputFormat !== _step.format && + (!_step.type || _step.type === obj.type)) { + obj.outputFormat = _step.format; + obj.data = x2tConvertDataInternal(x2t, obj); + obj.fileName += '.'+_step.format; + } + }); + obj.outputFormat = o; + } + + var data = x2tConvertDataInternal(x2t, obj); + + // Convert to bin -- Import + // We need to extract the images + var images; + if (o === 'bin') { + images = []; + var files = x2t.FS.readdir("/working/media/"); + files.forEach(function (file) { + if (file !== "." && file !== "..") { + var fileData = x2t.FS.readFile("/working/media/" + file, { + encoding : "binary" + }); + images.push({ + name: file, + data: fileData + }); + } + }); + + } + + cb({ + data: data, + images: images + }); + }); + }); + }; + + return { + convert: convert + }; + }; + + return X2T; +}); diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index 33d46caf6..e4f6c9260 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -660,6 +660,11 @@ define([ var updateDecryptProgress = function (progressValue) { var text = Math.round(progressValue * 100) + '%'; text += progressValue === 1 ? '' : ' (' + Messages.download_step2 + '...)'; + if (progressValue === 2) { + Messages.download_step3 = "Converting..."; // XXX + text = Messages.download_step3; + progressValue = 1; + } $pv.text(text); $pb.css({ width: (progressValue * 100) + '%' diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 5223898d1..21344cd3f 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,8 @@ define([ var edPublic, curvePublic, notifications, isTemplate; var settings = {}; var isSafe = ['debug', 'profile', 'drive', 'teams', 'calendar', 'file'].indexOf(currentPad.app) !== -1; + var isOO = ['sheet', 'doc', 'presentation'].indexOf(parsed.type) !== -1; + var ooDownloadData = {}; var isDeleted = isNewFile && currentPad.hash.length > 0; if (isDeleted) { @@ -631,11 +636,12 @@ define([ channel: secret.channel, enableSF: localStorage.CryptPad_SF === "1", // TODO to remove when enabled by default devMode: localStorage.CryptPad_dev === "1", - fromFileData: Cryptpad.fromFileData ? { + fromFileData: Cryptpad.fromFileData ? (isOO ? Cryptpad.fromFileData : { title: Cryptpad.fromFileData.title - } : undefined, + }) : undefined, burnAfterReading: burnAfterReading, - storeInTeam: Cryptpad.initialTeam || (Cryptpad.initialPath ? -1 : undefined) + storeInTeam: Cryptpad.initialTeam || (Cryptpad.initialPath ? -1 : undefined), + supportsWasm: Utils.Util.supportsWasm() }; if (window.CryptPad_newSharedFolder) { additionalPriv.newSharedFolder = window.CryptPad_newSharedFolder; @@ -1032,6 +1038,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); @@ -1273,46 +1324,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) { @@ -1462,6 +1473,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 = $('