mirror of https://github.com/xwiki-labs/cryptpad
Add converter app
This commit is contained in:
parent
a4bd4e2784
commit
544e5bcbfe
|
@ -12,7 +12,7 @@ define(function() {
|
|||
* You should never remove the drive from this list.
|
||||
*/
|
||||
AppConfig.availablePadTypes = ['drive', 'teams', 'pad', 'sheet', 'code', 'slide', 'poll', 'kanban', 'whiteboard',
|
||||
/*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts', 'form'];
|
||||
/*'doc', 'presentation',*/ 'file', /*'todo',*/ 'contacts', 'form', 'convert'];
|
||||
/* The registered only types are apps restricted to registered users.
|
||||
* You should never remove apps from this list unless you know what you're doing. The apps
|
||||
* listed here by default can't work without a user account.
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
@import (reference) '../../customize/src/less2/include/framework.less';
|
||||
@import (reference) '../../customize/src/less2/include/sidebar-layout.less';
|
||||
|
||||
&.cp-app-convert {
|
||||
|
||||
.framework_min_main(
|
||||
@bg-color: @colortheme_apps[default],
|
||||
);
|
||||
.sidebar-layout_main();
|
||||
|
||||
// body
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
background-color: @cp_app-bg;
|
||||
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
define([
|
||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||
], function () {
|
||||
var Nacl = window.nacl;
|
||||
//var PARANOIA = true;
|
||||
|
||||
var plainChunkLength = 128 * 1024;
|
||||
var cypherChunkLength = 131088;
|
||||
|
||||
var computeEncryptedSize = function (bytes, meta) {
|
||||
var metasize = Nacl.util.decodeUTF8(JSON.stringify(meta)).length;
|
||||
var chunks = Math.ceil(bytes / plainChunkLength);
|
||||
return metasize + 18 + (chunks * 16) + bytes;
|
||||
};
|
||||
|
||||
var encodePrefix = function (p) {
|
||||
return [
|
||||
65280, // 255 << 8
|
||||
255,
|
||||
].map(function (n, i) {
|
||||
return (p & n) >> ((1 - i) * 8);
|
||||
});
|
||||
};
|
||||
var decodePrefix = function (A) {
|
||||
return (A[0] << 8) | A[1];
|
||||
};
|
||||
|
||||
var slice = function (A) {
|
||||
return Array.prototype.slice.call(A);
|
||||
};
|
||||
|
||||
var createNonce = function () {
|
||||
return new Uint8Array(new Array(24).fill(0));
|
||||
};
|
||||
|
||||
var increment = function (N) {
|
||||
var l = N.length;
|
||||
while (l-- > 1) {
|
||||
/* our linter suspects this is unsafe because we lack types
|
||||
but as long as this is only used on nonces, it should be safe */
|
||||
if (N[l] !== 255) { return void N[l]++; } // jshint ignore:line
|
||||
if (l === 0) { throw new Error('E_NONCE_TOO_LARGE'); }
|
||||
N[l] = 0;
|
||||
}
|
||||
};
|
||||
|
||||
var joinChunks = function (chunks) {
|
||||
return new Blob(chunks);
|
||||
};
|
||||
|
||||
var decrypt = function (u8, key, done, progress) {
|
||||
var MAX = u8.length;
|
||||
var _progress = function (offset) {
|
||||
if (typeof(progress) !== 'function') { return; }
|
||||
progress(Math.min(1, offset / MAX));
|
||||
};
|
||||
|
||||
var nonce = createNonce();
|
||||
var i = 0;
|
||||
|
||||
var prefix = u8.subarray(0, 2);
|
||||
var metadataLength = decodePrefix(prefix);
|
||||
|
||||
var res = {
|
||||
metadata: undefined,
|
||||
};
|
||||
|
||||
var cancelled = false;
|
||||
var cancel = function () {
|
||||
cancelled = true;
|
||||
};
|
||||
|
||||
var metaBox = new Uint8Array(u8.subarray(2, 2 + metadataLength));
|
||||
|
||||
var metaChunk = Nacl.secretbox.open(metaBox, nonce, key);
|
||||
increment(nonce);
|
||||
|
||||
try {
|
||||
res.metadata = JSON.parse(Nacl.util.encodeUTF8(metaChunk));
|
||||
} catch (e) {
|
||||
return window.setTimeout(function () {
|
||||
done('E_METADATA_DECRYPTION');
|
||||
});
|
||||
}
|
||||
|
||||
if (!res.metadata) {
|
||||
return void setTimeout(function () {
|
||||
done('NO_METADATA');
|
||||
});
|
||||
}
|
||||
|
||||
var takeChunk = function (cb) {
|
||||
setTimeout(function () {
|
||||
var start = i * cypherChunkLength + 2 + metadataLength;
|
||||
var end = start + cypherChunkLength;
|
||||
i++;
|
||||
var box = new Uint8Array(u8.subarray(start, end));
|
||||
|
||||
// decrypt the chunk
|
||||
var plaintext = Nacl.secretbox.open(box, nonce, key);
|
||||
increment(nonce);
|
||||
|
||||
if (!plaintext) { return cb('DECRYPTION_ERROR'); }
|
||||
|
||||
_progress(end);
|
||||
cb(void 0, plaintext);
|
||||
});
|
||||
};
|
||||
|
||||
var chunks = [];
|
||||
|
||||
var again = function () {
|
||||
if (cancelled) { return; }
|
||||
takeChunk(function (e, plaintext) {
|
||||
if (e) {
|
||||
return setTimeout(function () {
|
||||
done(e);
|
||||
});
|
||||
}
|
||||
if (plaintext) {
|
||||
if ((2 + metadataLength + i * cypherChunkLength) < u8.length) { // not done
|
||||
chunks.push(plaintext);
|
||||
return setTimeout(again);
|
||||
}
|
||||
chunks.push(plaintext);
|
||||
res.content = joinChunks(chunks);
|
||||
return done(void 0, res);
|
||||
}
|
||||
done('UNEXPECTED_ENDING');
|
||||
});
|
||||
};
|
||||
|
||||
again();
|
||||
|
||||
return {
|
||||
cancel: cancel
|
||||
};
|
||||
};
|
||||
|
||||
// metadata
|
||||
/* { filename: 'raccoon.jpg', type: 'image/jpeg' } */
|
||||
var encrypt = function (u8, metadata, key) {
|
||||
var nonce = createNonce();
|
||||
|
||||
// encode metadata
|
||||
var plaintext = Nacl.util.decodeUTF8(JSON.stringify(metadata));
|
||||
|
||||
// if metadata is too large, drop the thumbnail.
|
||||
if (plaintext.length > 65535) {
|
||||
var temp = JSON.parse(JSON.stringify(metadata));
|
||||
delete metadata.thumbnail;
|
||||
plaintext = Nacl.util.decodeUTF8(JSON.stringify(temp));
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
|
||||
var state = 0;
|
||||
var next = function (cb) {
|
||||
if (state === 2) { return void setTimeout(cb); }
|
||||
|
||||
var start;
|
||||
var end;
|
||||
var part;
|
||||
var box;
|
||||
|
||||
if (state === 0) { // metadata...
|
||||
part = new Uint8Array(plaintext);
|
||||
box = Nacl.secretbox(part, nonce, key);
|
||||
increment(nonce);
|
||||
|
||||
if (box.length > 65535) {
|
||||
return void cb('METADATA_TOO_LARGE');
|
||||
}
|
||||
var prefixed = new Uint8Array(encodePrefix(box.length)
|
||||
.concat(slice(box)));
|
||||
state++;
|
||||
|
||||
return void setTimeout(function () {
|
||||
cb(void 0, prefixed);
|
||||
});
|
||||
}
|
||||
|
||||
// encrypt the rest of the file...
|
||||
start = i * plainChunkLength;
|
||||
end = start + plainChunkLength;
|
||||
|
||||
part = u8.subarray(start, end);
|
||||
box = Nacl.secretbox(part, nonce, key);
|
||||
increment(nonce);
|
||||
i++;
|
||||
|
||||
// regular data is done
|
||||
if (i * plainChunkLength >= u8.length) { state = 2; }
|
||||
|
||||
setTimeout(function () {
|
||||
cb(void 0, box);
|
||||
});
|
||||
};
|
||||
|
||||
return next;
|
||||
};
|
||||
|
||||
return {
|
||||
decrypt: decrypt,
|
||||
encrypt: encrypt,
|
||||
joinChunks: joinChunks,
|
||||
computeEncryptedSize: computeEncryptedSize,
|
||||
};
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>CryptPad</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<script async data-bootload="main.js" data-main="/common/boot.js?ver=1.0" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<link href="/customize/src/outer.css?ver=1.3.2" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<iframe-placeholder>
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="cp-app-noscroll">
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<script async data-bootload="/convert/inner.js" data-main="/common/sframe-boot.js?ver=1.7" src="/bower_components/requirejs/require.js?ver=2.3.5"></script>
|
||||
<style>
|
||||
.loading-hidden { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="cp-app-convert">
|
||||
<div id="cp-toolbar" class="cp-toolbar-container"></div>
|
||||
<div id="cp-sidebarlayout-container"></div>
|
||||
<noscript>
|
||||
<p><strong>OOPS</strong> In order to do encryption in your browser, Javascript is really <strong>really</strong> required.</p>
|
||||
<p><strong>OUPS</strong> Afin de pouvoir réaliser le chiffrement dans votre navigateur, Javascript est <strong>vraiment</strong> nécessaire.</p>
|
||||
</noscript>
|
||||
</body>
|
||||
|
|
@ -0,0 +1,259 @@
|
|||
define([
|
||||
'jquery',
|
||||
'/api/config',
|
||||
'/bower_components/chainpad-crypto/crypto.js',
|
||||
'/common/toolbar.js',
|
||||
'/bower_components/nthen/index.js',
|
||||
'/common/sframe-common.js',
|
||||
'/common/hyperscript.js',
|
||||
'/customize/messages.js',
|
||||
'/common/common-interface.js',
|
||||
'/common/common-util.js',
|
||||
|
||||
'/bower_components/file-saver/FileSaver.min.js',
|
||||
'css!/bower_components/bootstrap/dist/css/bootstrap.min.css',
|
||||
'css!/bower_components/components-font-awesome/css/font-awesome.min.css',
|
||||
'less!/convert/app-convert.less',
|
||||
], function (
|
||||
$,
|
||||
ApiConfig,
|
||||
Crypto,
|
||||
Toolbar,
|
||||
nThen,
|
||||
SFCommon,
|
||||
h,
|
||||
Messages,
|
||||
UI,
|
||||
Util
|
||||
)
|
||||
{
|
||||
var APP = {};
|
||||
|
||||
var common;
|
||||
var sFrameChan;
|
||||
|
||||
var debug = console.debug;
|
||||
|
||||
var x2tReady = Util.mkEvent(true);
|
||||
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();
|
||||
//fetchFonts(x2t);
|
||||
debug("x2t mount done");
|
||||
};
|
||||
var getX2t = function (cb) {
|
||||
// XXX require http headers on firefox...
|
||||
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; }
|
||||
// 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 '<m_nFormatFrom>'+id+'</m_nFormatFrom>';
|
||||
};
|
||||
var getToId = function (ext) {
|
||||
var id = getFormatId(ext);
|
||||
if (!id) { return ''; }
|
||||
return '<m_nFormatTo>'+id+'</m_nFormatTo>';
|
||||
};
|
||||
var x2tConvertDataInternal = function(x2t, data, fileName, outputFormat) {
|
||||
debug("Converting Data for " + fileName + " to " + outputFormat);
|
||||
|
||||
var inputFormat = fileName.split('.').pop();
|
||||
|
||||
x2t.FS.writeFile('/working/' + fileName, data);
|
||||
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>"
|
||||
+ getFromId(inputFormat)
|
||||
+ getToId(outputFormat)
|
||||
+ "<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) {
|
||||
console.error(e, x2t.FS);
|
||||
debug("Failed reading converted file");
|
||||
UI.warn(Messages.error);
|
||||
return "";
|
||||
}
|
||||
return result;
|
||||
};
|
||||
var x2tConverter = function (typeSrc, typeTarget) {
|
||||
return function (data, name, cb) {
|
||||
getX2t(function (x2t) {
|
||||
if (typeSrc === 'ods') {
|
||||
data = x2tConvertDataInternal(x2t, data, name, 'xlsx');
|
||||
name += '.xlsx';
|
||||
}
|
||||
if (typeSrc === 'odt') {
|
||||
data = x2tConvertDataInternal(x2t, data, name, 'docx');
|
||||
name += '.docx';
|
||||
}
|
||||
if (typeSrc === 'odp') {
|
||||
data = x2tConvertDataInternal(x2t, data, name, 'pptx');
|
||||
name += '.pptx';
|
||||
}
|
||||
cb(x2tConvertDataInternal(x2t, data, name, typeTarget));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
var CONVERTERS = {
|
||||
xlsx: {
|
||||
//pdf: x2tConverter('xlsx', 'pdf'),
|
||||
ods: x2tConverter('xlsx', 'ods'),
|
||||
bin: x2tConverter('xlsx', 'bin'),
|
||||
},
|
||||
ods: {
|
||||
//pdf: x2tConverter('ods', 'pdf'),
|
||||
xlsx: x2tConverter('ods', 'xlsx'),
|
||||
bin: x2tConverter('ods', 'bin'),
|
||||
},
|
||||
odt: {
|
||||
docx: x2tConverter('odt', 'docx'),
|
||||
txt: x2tConverter('odt', 'txt'),
|
||||
bin: x2tConverter('odt', 'bin'),
|
||||
},
|
||||
docx: {
|
||||
odt: x2tConverter('docx', 'odt'),
|
||||
txt: x2tConverter('docx', 'txt'),
|
||||
bin: x2tConverter('docx', 'bin'),
|
||||
},
|
||||
txt: {
|
||||
odt: x2tConverter('txt', 'odt'),
|
||||
docx: x2tConverter('txt', 'docx'),
|
||||
bin: x2tConverter('txt', 'bin'),
|
||||
},
|
||||
odp: {
|
||||
pptx: x2tConverter('odp', 'pptx'),
|
||||
bin: x2tConverter('odp', 'bin'),
|
||||
},
|
||||
pptx: {
|
||||
odp: x2tConverter('pptx', 'odp'),
|
||||
bin: x2tConverter('pptx', 'bin'),
|
||||
},
|
||||
};
|
||||
|
||||
Messages.convertPage = "Convert"; // XXX
|
||||
Messages.convert_hint = "Pick the file you want to convert. The list of output format will be visible afterward.";
|
||||
|
||||
var createToolbar = function () {
|
||||
var displayed = ['useradmin', 'newpad', 'limit', 'pageTitle', 'notifications'];
|
||||
var configTb = {
|
||||
displayed: displayed,
|
||||
sfCommon: common,
|
||||
$container: APP.$toolbar,
|
||||
pageTitle: Messages.convertPage,
|
||||
metadataMgr: common.getMetadataMgr(),
|
||||
};
|
||||
APP.toolbar = Toolbar.create(configTb);
|
||||
APP.toolbar.$rightside.hide();
|
||||
};
|
||||
|
||||
nThen(function (waitFor) {
|
||||
$(waitFor(UI.addLoadingScreen));
|
||||
SFCommon.create(waitFor(function (c) { APP.common = common = c; }));
|
||||
}).nThen(function (waitFor) {
|
||||
APP.$container = $('#cp-sidebarlayout-container');
|
||||
APP.$toolbar = $('#cp-toolbar');
|
||||
APP.$leftside = $('<div>', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container);
|
||||
APP.$rightside = $('<div>', {id: 'cp-sidebarlayout-rightside'}).appendTo(APP.$container);
|
||||
sFrameChan = common.getSframeChannel();
|
||||
sFrameChan.onReady(waitFor());
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
createToolbar();
|
||||
|
||||
var hint = h('p.cp-convert-hint', Messages.convert_hint);
|
||||
|
||||
var picker = h('input', {
|
||||
type: 'file'
|
||||
});
|
||||
APP.$rightside.append([hint, picker]);
|
||||
|
||||
$(picker).on('change', function () {
|
||||
var file = picker.files[0];
|
||||
var name = file && file.name;
|
||||
var reader = new FileReader();
|
||||
var parsed = file && file.name && /.+\.([^.]+)$/.exec(file.name);
|
||||
var ext = parsed && parsed[1];
|
||||
reader.onload = function (e) {
|
||||
if (CONVERTERS[ext]) {
|
||||
Object.keys(CONVERTERS[ext]).forEach(function (to) {
|
||||
var button = h('button.btn', to);
|
||||
$(button).click(function () {
|
||||
CONVERTERS[ext][to](new Uint8Array(e.target.result), name, function (a) {
|
||||
var n = name.slice(0, -ext.length) + to;
|
||||
var blob = new Blob([a], {type: "application/bin;charset=utf-8"});
|
||||
window.saveAs(blob, n);
|
||||
});
|
||||
|
||||
}).appendTo(APP.$rightside);
|
||||
});
|
||||
}
|
||||
};
|
||||
reader.readAsArrayBuffer(file, 'application/octet-stream');
|
||||
});
|
||||
|
||||
UI.removeLoadingScreen();
|
||||
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
// 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',
|
||||
'/common/dom-ready.js',
|
||||
'/common/sframe-common-outer.js'
|
||||
], function (nThen, ApiConfig, DomReady, SFCommonO) {
|
||||
|
||||
// Loaded in load #2
|
||||
nThen(function (waitFor) {
|
||||
DomReady.onReady(waitFor());
|
||||
}).nThen(function (waitFor) {
|
||||
SFCommonO.initIframe(waitFor, true);
|
||||
}).nThen(function (/*waitFor*/) {
|
||||
var category;
|
||||
if (window.location.hash) {
|
||||
category = window.location.hash.slice(1);
|
||||
window.location.hash = '';
|
||||
}
|
||||
var addData = function (obj) {
|
||||
if (category) { obj.category = category; }
|
||||
};
|
||||
SFCommonO.start({
|
||||
noRealtime: true,
|
||||
addData: addData
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue