diff --git a/www/code/export.js b/www/code/export.js new file mode 100644 index 000000000..23d689361 --- /dev/null +++ b/www/code/export.js @@ -0,0 +1,17 @@ +// This file is used when a user tries to export the entire CryptDrive. +// Pads from the code app will be exported using this format instead of plain text. +define([ + '/common/sframe-common-codemirror.js', +], function (SFCodeMirror) { + var module = {}; + + module.main = function (userDoc, cb) { + var mode = userDoc.highlightMode || 'gfm'; + var content = userDoc.content; + module.type = SFCodeMirror.getContentExtension(mode); + cb(SFCodeMirror.fileExporter(content)); + }; + + return module; +}); + diff --git a/www/common/cryptget.js b/www/common/cryptget.js index b3a3c7511..5279a17c9 100644 --- a/www/common/cryptget.js +++ b/www/common/cryptget.js @@ -12,11 +12,18 @@ define([ S.cb(err, doc); S.done = true; - var disconnect = Util.find(S, ['network', 'disconnect']); - if (typeof(disconnect) === 'function') { disconnect(); } - var abort = Util.find(S, ['realtime', 'realtime', 'abort']); + if (!S.hasNetwork) { + var disconnect = Util.find(S, ['network', 'disconnect']); + if (typeof(disconnect) === 'function') { disconnect(); } + } + if (S.leave) { + try { + S.leave(); + } catch (e) { console.log(e); } + } + var abort = Util.find(S, ['session', 'realtime', 'abort']); if (typeof(abort) === 'function') { - S.realtime.realtime.sync(); + S.session.realtime.sync(); abort(); } }; @@ -51,11 +58,12 @@ define([ opt = opt || {}; var config = makeConfig(hash, opt.password); - var Session = { cb: cb, }; + var Session = { cb: cb, hasNetwork: Boolean(opt.network) }; config.onReady = function (info) { var rt = Session.session = info.realtime; Session.network = info.network; + Session.leave = info.leave; finish(Session, void 0, rt.getUserDoc()); }; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index cabab6ca4..030c6e7f2 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -59,6 +59,19 @@ define([ cb(); }; + common.makeNetwork = function (cb) { + require([ + '/bower_components/netflux-websocket/netflux-client.js', + '/common/outer/network-config.js' + ], function (Netflux, NetConfig) { + var wsUrl = NetConfig.getWebsocketURL(); + Netflux.connect(wsUrl).then(function (network) { + cb(null, network); + }, function (err) { + cb(err); + }); + }); + }; // RESTRICTED // Settings only diff --git a/www/common/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js index 0c1a6a7a5..58d84a616 100644 --- a/www/common/sframe-common-codemirror.js +++ b/www/common/sframe-common-codemirror.js @@ -38,6 +38,12 @@ define([ return cursor; }; + module.getContentExtension = function (mode) { + return (Modes.extensionOf(mode) || '.txt').slice(1); + }; + module.fileExporter = function (content) { + return new Blob([ content ], { type: 'text/plain;charset=utf-8' }); + }; module.setValueAndCursor = function (editor, oldDoc, remoteDoc) { var scroll = editor.getScrollInfo(); //get old cursor here @@ -271,10 +277,10 @@ define([ }; exp.getContentExtension = function () { - return (Modes.extensionOf(exp.highlightMode) || '.txt').slice(1); + return module.getContentExtension(exp.highlightMode); }; exp.fileExporter = function () { - return new Blob([ editor.getValue() ], { type: 'text/plain;charset=utf-8' }); + return module.fileExporter(editor.getValue()); }; exp.fileImporter = function (content, file) { var $toolbarContainer = $('#cme_toolbox'); diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index f40bd6514..aff66dddc 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -21,7 +21,9 @@ define([ var FilePicker; var Messaging; var Notifier; - var Utils = {}; + var Utils = { + nThen: nThen + }; var AppConfig; var Test; var password; @@ -744,13 +746,46 @@ define([ Cryptpad.removeLoginBlock(data, cb); }); + var cgNetwork; + var whenCGReady = function (cb) { + if (cgNetwork && cgNetwork !== true) { console.log(cgNetwork); return void cb(); } + setTimeout(function () { + whenCGReady(cb); + }, 500); + }; + var i = 0; sframeChan.on('Q_CRYPTGET', function (data, cb) { - Cryptget.get(data.hash, function (err, val) { - cb({ - error: err, - data: val + var todo = function () { + data.opts.network = cgNetwork; + Cryptget.get(data.hash, function (err, val) { + cb({ + error: err, + data: val + }); + }, data.opts); + }; + //return void todo(); + if (i > 30) { + i = 0; + cgNetwork = undefined; + } + i++ + if (!cgNetwork) { + cgNetwork = true; + return void Cryptpad.makeNetwork(function (err, nw) { + console.log(nw); + cgNetwork = nw; + todo(); }); - }, data.opts); + } else if (cgNetwork === true) { + return void whenCGReady(todo); + } + todo(); + }); + sframeChan.on('EV_CRYPTGET_DISCONNECT', function () { + if (!cgNetwork) { return; } + cgNetwork.disconnect(); + cgNetwork = undefined; }); if (cfg.addRpc) { diff --git a/www/common/sframe-protocol.js b/www/common/sframe-protocol.js index f349ef8da..c222fc485 100644 --- a/www/common/sframe-protocol.js +++ b/www/common/sframe-protocol.js @@ -274,5 +274,6 @@ define({ // Ability to get a pad's content from its hash 'Q_CRYPTGET': true, + 'EV_CRYPTGET_DISCONNECT': true, }); diff --git a/www/kanban/export.js b/www/kanban/export.js new file mode 100644 index 000000000..2240031fe --- /dev/null +++ b/www/kanban/export.js @@ -0,0 +1,16 @@ +// This file is used when a user tries to export the entire CryptDrive. +// Pads from the code app will be exported using this format instead of plain text. +define([ +], function () { + var module = {}; + + module.main = function (userDoc, cb) { + var content = userDoc.content; + cb(new Blob([JSON.stringify(content, 0, 2)], { + type: 'application/json', + })); + }; + + return module; +}); + diff --git a/www/kanban/inner.js b/www/kanban/inner.js index ed34b837e..48e4e76e0 100644 --- a/www/kanban/inner.js +++ b/www/kanban/inner.js @@ -368,7 +368,7 @@ define([ } framework.setFileExporter('json', function () { - return new Blob([JSON.stringify(kanban.getBoardsJSON())], { + return new Blob([JSON.stringify(kanban.getBoardsJSON(), 0, 2)], { type: 'application/json', }); }); diff --git a/www/pad/export.js b/www/pad/export.js new file mode 100644 index 000000000..2bc54a430 --- /dev/null +++ b/www/pad/export.js @@ -0,0 +1,64 @@ +define([ + 'jquery', + '/common/common-util.js', + '/bower_components/hyperjson/hyperjson.js', + '/bower_components/nthen/index.js', +], function ($, Util, Hyperjson, nThen) { + var module = { + type: 'html' + }; + + var exportMediaTags = function (inner, cb) { + var $clone = $(inner).clone(); + nThen(function (waitFor) { + $(inner).find('media-tag').each(function (i, el) { + if (!$(el).data('blob') || !el.blob) { return; } + Util.blobToImage(el.blob || $(el).data('blob'), waitFor(function (imgSrc) { + $clone.find('media-tag[src="' + $(el).attr('src') + '"] img') + .attr('src', imgSrc); + $clone.find('media-tag').parent() + .find('.cke_widget_drag_handler_container').remove(); + })); + }); + }).nThen(function () { + cb($clone[0]); + }); + }; + + module.getHTML = function (inner) { + return ('\n' + '\n' + + ' \n ' + + inner.innerHTML.replace(/]*class="cke_anchor"[^>]*data-cke-realelement="([^"]*)"[^>]*>/g, + function(match,realElt){ + //console.log("returning realElt \"" + unescape(realElt)+ "\"."); + return decodeURIComponent(realElt); }) + + ' \n' + ); + }; + + module.main = function (userDoc, cb) { + var inner; + if (userDoc instanceof Element || userDoc instanceof HTMLElement) { + inner = userDoc; + } else { + try { + if (Array.isArray(userDoc)) { + inner = Hyperjson.toDOM(userDoc); + } else { + console.error('This Pad is not an array!', userDoc); + return void cb(''); + } + } catch (e) { + console.log(JSON.stringify(userDoc)); + console.error(userDoc); + console.error(e); + return void cb(''); + } + } + exportMediaTags(inner, function (toExport) { + cb(new Blob([ module.getHTML(toExport) ], { type: "text/html;charset=utf-8" })); + }); + }; + + return module; +}); diff --git a/www/pad/inner.js b/www/pad/inner.js index a97406d70..1700132b5 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -25,6 +25,7 @@ define([ '/common/TypingTests.js', '/customize/messages.js', '/pad/links.js', + '/pad/export.js', '/bower_components/nthen/index.js', '/common/media-tag.js', '/api/config', @@ -49,6 +50,7 @@ define([ TypingTest, Messages, Links, + Exporter, nThen, MediaTag, ApiConfig, @@ -166,17 +168,6 @@ define([ //'AUDIO' ]; - var getHTML = function (inner) { - return ('\n' + '\n' + - ' \n ' + - inner.innerHTML.replace(/]*class="cke_anchor"[^>]*data-cke-realelement="([^"]*)"[^>]*>/g, - function(match,realElt){ - //console.log("returning realElt \"" + unescape(realElt)+ "\"."); - return decodeURIComponent(realElt); }) + - ' \n' - ); - }; - var CKEDITOR_CHECK_INTERVAL = 100; var ckEditorAvailable = function (cb) { var intr; @@ -647,26 +638,8 @@ define([ }); }, true); - var exportMediaTags = function (inner, cb) { - var $clone = $(inner).clone(); - nThen(function (waitFor) { - $(inner).find('media-tag').each(function (i, el) { - if (!$(el).data('blob') || !el.blob) { return; } - Util.blobToImage(el.blob || $(el).data('blob'), waitFor(function (imgSrc) { - $clone.find('media-tag[src="' + $(el).attr('src') + '"] img') - .attr('src', imgSrc); - $clone.find('media-tag').parent() - .find('.cke_widget_drag_handler_container').remove(); - })); - }); - }).nThen(function () { - cb($clone[0]); - }); - }; - framework.setFileExporter('html', function (cb) { - exportMediaTags(inner, function (toExport) { - cb(new Blob([ getHTML(toExport) ], { type: "text/html;charset=utf-8" })); - }); + framework.setFileExporter(Exporter.type, function (cb) { + Exporter.main(inner, cb); }, true); framework.setNormalizer(function (hjson) { @@ -837,7 +810,7 @@ define([ test.fail("No anchors found. Please adjust document"); } else { console.log(anchors.length + " anchors found."); - var exported = getHTML(window.inner); + var exported = Exporter.getHTML(window.inner); console.log("Obtained exported: " + exported); var allFound = true; for(var i=0; i]/gi, '_')/*.toLowerCase()*/; }; var getUnique = function (name, ext, existing) { - var n = name; + var n = name + ext; var i = 1; - while (existing.indexOf(n) !== -1) { - n = name + ' ('+ i++ + ')'; + while (existing.indexOf(n.toLowerCase()) !== -1) { + n = name + ' ('+ i++ + ')' + ext; } return n; }; + var transform = function (ctx, type, sjson, cb) { + var result = { + data: sjson, + ext: '.json', + }; + var json; + try { + json = JSON.parse(sjson); + } catch (e) { + return void cb(result); + } + var path = '/' + type + '/export.js'; + require([path], function (Exporter) { + Exporter.main(json, function (data) { + result.ext = '.' + Exporter.type; + result.data = data; + cb(result); + }); + }, function () { + cb(result); + }); + }; + + // Add a file to the zip. We have to cryptget&transform it if it's a pad + // or fetch&decrypt it if it's a file. var addFile = function (ctx, zip, fData, existingNames) { if (!fData.href && !fData.roHref) { return void ctx.errors.push({ @@ -28,70 +55,121 @@ define([ } var parsed = Hash.parsePadUrl(fData.href || fData.roHref); - // TODO deal with files here - if (parsed.hashData.type !== 'pad') { return; } + if (['pad', 'file'].indexOf(parsed.hashData.type) === -1) { return; } + // waitFor is used to make sure all the pads and files are process before downloading the zip. var w = ctx.waitFor(); + + // Work with only 10 pad/files at a time ctx.sem.take(function (give) { var opts = { password: fData.password }; - var rawName = fData.fileName || fData.title || 'File'; + var rawName = fData.filename || fData.title || 'File'; console.log(rawName); - ctx.get({ - hash: parsed.hash, - opts: opts - }, give(function (err, val) { + var g = give(); + + var done = function () { + //setTimeout(g, 2000); + g(); w(); - if (err) { - return void ctx.errors.push({ - error: err, - data: fData + }; + var error = function (err) { + done(); + return void ctx.errors.push({ + error: err, + data: fData + }); + }; + + // Pads (pad,code,slide,kanban,poll,...) + var todoPad = function () { + ctx.get({ + hash: parsed.hash, + opts: opts + }, function (err, val) { + if (err) { return void error(err); } + if (!val) { return void error('EEMPTY'); } + + var opts = { + binary: true, + }; + transform(ctx, parsed.type, val, function (res) { + if (!res.data) { return void error('EEMPTY'); } + var fileName = getUnique(sanitize(rawName), res.ext, existingNames); + existingNames.push(fileName.toLowerCase()); + zip.file(fileName, res.data, opts); + console.log('DONE ---- ' + fileName); + setTimeout(done, 1000); }); - } - // TODO transform file here - // var blob = transform(val, type); - var opts = {}; - var fileName = getUnique(sanitize(rawName), '.txt', existingNames); - existingNames.push(fileName); - zip.file(fileName, val, opts); - console.log('DONE ---- ' + rawName); - })); + }); + }; + + // Files (mediatags...) + var todoFile = function () { + var secret = Hash.getSecrets('file', parsed.hash, fData.password); + var hexFileName = secret.channel; + var src = Hash.getBlobPathFromHex(hexFileName); + var key = secret.keys && secret.keys.cryptKey; + Util.fetch(src, function (err, u8) { + if (err) { return void error('E404'); } + FileCrypto.decrypt(u8, key, function (err, res) { + if (err) { return void error(err); } + var opts = { + binary: true, + }; + var extIdx = rawName.lastIndexOf('.'); + var name = extIdx !== -1 ? rawName.slice(0,extIdx) : rawName; + var ext = extIdx !== -1 ? rawName.slice(extIdx) : ""; + var fileName = getUnique(sanitize(name), ext, existingNames); + existingNames.push(fileName.toLowerCase()); + zip.file(fileName, res.content, opts); + console.log('DONE ---- ' + fileName); + setTimeout(done, 1000); + }); + }); + }; + if (parsed.hashData.type === 'file') { + return void todoFile(); + } + todoPad(); }); // cb(err, blob); - // wiht blob.name not undefined }; - var makeFolder = function (ctx, root, zip) { + // Add folders and their content recursively in the zip + var makeFolder = function (ctx, root, zip, fd) { if (typeof (root) !== "object") { return; } var existingNames = []; Object.keys(root).forEach(function (k) { var el = root[k]; if (typeof el === "object") { var fName = getUnique(sanitize(k), '', existingNames); - existingNames.push(fName); - return void makeFolder(ctx, el, zip.folder(fName)); + existingNames.push(fName.toLowerCase()); + return void makeFolder(ctx, el, zip.folder(fName), fd); } if (ctx.data.sharedFolders[el]) { - // TODO later... - return; + var sfData = ctx.sf[el].metadata; + var sfName = getUnique(sanitize(sfData.title || 'Folder'), '', existingNames); + existingNames.push(sfName.toLowerCase()); + return void makeFolder(ctx, ctx.sf[el].root, zip.folder(sfName), ctx.sf[el].filesData); } - var fData = ctx.data.filesData[el]; + var fData = fd[el]; if (fData) { addFile(ctx, zip, fData, existingNames); return; } - // What is this element? - console.error(el); }); }; + // Main function. Create the empty zip and fill it starting from drive.root var create = function (data, getPad, cb) { - if (!data || !data.drive) { return void cb('EEMPTY'); } - var sem = Saferphore.create(10); + if (!data || !data.uo || !data.uo.drive) { return void cb('EEMPTY'); } + var sem = Saferphore.create(5); var ctx = { get: getPad, - data: data.drive, + data: data.uo.drive, + sf: data.sf, zip: new JsZip(), errors: [], sem: sem, @@ -99,9 +177,8 @@ define([ nThen(function (waitFor) { ctx.waitFor = waitFor; var zipRoot = ctx.zip.folder('Root'); - makeFolder(ctx, data.drive.root, zipRoot); + makeFolder(ctx, ctx.data.root, zipRoot, ctx.data.filesData); }).nThen(function () { - // TODO call cb with ctx.zip here console.log(ctx.zip); console.log(ctx.errors); ctx.zip.generateAsync({type: 'blob'}).then(function (content) { diff --git a/www/slide/export.js b/www/slide/export.js new file mode 100644 index 000000000..814950306 --- /dev/null +++ b/www/slide/export.js @@ -0,0 +1,18 @@ +// This file is used when a user tries to export the entire CryptDrive. +// Pads from the slide app will be exported using this format instead of plain text. +define([ + '/common/sframe-common-codemirror.js', +], function (SFCodeMirror) { + var module = { + type: 'md' + }; + + module.main = function (userDoc, cb) { + var content = userDoc.content; + cb(SFCodeMirror.fileExporter(content)); + }; + + return module; +}); + + diff --git a/www/whiteboard/export.js b/www/whiteboard/export.js new file mode 100644 index 000000000..e9f68e311 --- /dev/null +++ b/www/whiteboard/export.js @@ -0,0 +1,24 @@ +// This file is used when a user tries to export the entire CryptDrive. +// Pads from the code app will be exported using this format instead of plain text. +define([ + '/bower_components/secure-fabric.js/dist/fabric.min.js', +], function () { + var module = {}; + + var Fabric = window.fabric; + module.main = function (userDoc, cb) { + var canvas_node = document.createElement('canvas'); + canvas_node.setAttribute('style', 'width:600px;height:600px;'); + canvas_node.setAttribute('width', '600'); + canvas_node.setAttribute('height', '600'); + var canvas = new Fabric.Canvas(canvas_node); + var content = userDoc.content; + canvas.loadFromJSON(content, function () { + module.type = 'svg'; + cb(canvas.toSVG()); + }); + }; + + return module; +}); + diff --git a/www/whiteboard/inner.js b/www/whiteboard/inner.js index 3e088b231..df392d7a7 100644 --- a/www/whiteboard/inner.js +++ b/www/whiteboard/inner.js @@ -277,6 +277,7 @@ define([ // Start of the main loop var andThen2 = function (framework) { + APP.framework = framework; var canvas = APP.canvas = new Fabric.Canvas('cp-app-whiteboard-canvas', { containerClass: 'cp-app-whiteboard-canvas-container' });