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