diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js
index ad5f726f8..68ba4ed06 100644
--- a/www/common/application_config_internal.js
+++ b/www/common/application_config_internal.js
@@ -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.
diff --git a/www/convert/app-convert.less b/www/convert/app-convert.less
new file mode 100644
index 000000000..c5813641a
--- /dev/null
+++ b/www/convert/app-convert.less
@@ -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;
+
+}
diff --git a/www/convert/file-crypto.js b/www/convert/file-crypto.js
new file mode 100644
index 000000000..6a0c08816
--- /dev/null
+++ b/www/convert/file-crypto.js
@@ -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,
+ };
+});
diff --git a/www/convert/index.html b/www/convert/index.html
new file mode 100644
index 000000000..96a3cce86
--- /dev/null
+++ b/www/convert/index.html
@@ -0,0 +1,12 @@
+
+
+
+ CryptPad
+
+
+
+
+
+
+
+
diff --git a/www/convert/inner.html b/www/convert/inner.html
new file mode 100644
index 000000000..206b85722
--- /dev/null
+++ b/www/convert/inner.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/www/convert/inner.js b/www/convert/inner.js
new file mode 100644
index 000000000..d8eca9af2
--- /dev/null
+++ b/www/convert/inner.js
@@ -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 ''+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);
+
+ var inputFormat = fileName.split('.').pop();
+
+ x2t.FS.writeFile('/working/' + fileName, data);
+ var params = ""
+ + ""
+ + "/working/" + fileName + ""
+ + "/working/" + fileName + "." + outputFormat + ""
+ + 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) {
+ 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 = $('', {id: 'cp-sidebarlayout-leftside'}).appendTo(APP.$container);
+ APP.$rightside = $('
', {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();
+
+ });
+});
diff --git a/www/convert/main.js b/www/convert/main.js
new file mode 100644
index 000000000..236290b13
--- /dev/null
+++ b/www/convert/main.js
@@ -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
+ });
+ });
+});