Download spreadsheets as xlsx from the drive

This commit is contained in:
yflory 2021-08-17 16:05:49 +02:00
parent 99572cbb6e
commit 777de599c3
8 changed files with 385 additions and 80 deletions

View File

@ -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');

View File

@ -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) {

View File

@ -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)) {

View File

@ -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
};
});

View File

@ -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 = $('<iframe>', {id: 'sbox-oo-iframe'}).appendTo($('body')).hide();
OOIframeObject.modal = OOIframe.create(config);
}
OOIframeObject.modal.refresh(cfg, function (data) {
cb(data);
});
};
sframeChan.on('Q_OOIFRAME_OPEN', function (data, cb) {
if (!data) { return void cb(); }
// Extract unsafe data (href and password) before sending it to onlyoffice
var padData = data.padData;
delete data.padData;
var uid = Utils.Util.uid();
ooDownloadData[uid] = padData;
data.downloadId = uid;
initOOIframe(data || {}, cb);
});
sframeChan.on('Q_TEMPLATE_USE', function (data, cb) {
Cryptpad.useTemplate(data, Cryptget, cb);
});

View File

@ -1016,7 +1016,7 @@ define([
Feedback.send('FULL_DRIVE_EXPORT_COMPLETE');
saveAs(blob, filename);
}, errors);
}, ui.update, common.getCache());
}, ui.update, common.getCache(), common.getSframeChannel());
ui.onCancel(function() {
ui.close();
bu.stop();

23
www/sheet/export.js Normal file
View File

@ -0,0 +1,23 @@
define([], function () {
var module = {
ext: '.xlsx', // default
exts: ['.xlsx']
};
module.main = function (userDoc, cb, ext, sframeChan, padData) {
sframeChan.query('Q_OOIFRAME_OPEN', {
json: userDoc,
type: 'sheet',
padData: padData
}, function (err, u8) {
if (!u8) { return void cb(''); }
var blob = new Blob([u8], {type: "application/bin;charset=utf-8"});
cb(blob);
}, {
raw: true
});
};
return module;
});

View File

@ -1131,7 +1131,7 @@ define([
Feedback.send('FULL_TEAMDRIVE_EXPORT_COMPLETE');
saveAs(blob, filename);
}, errors);
}, ui.update, common.getCache);
}, ui.update, common.getCache, common.getSframeChannel());
ui.onCancel(function() {
ui.close();
bu.stop();