From 48973bccd617f530a26842b3c74407555d2bbc5a Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 25 Apr 2017 16:11:19 +0200 Subject: [PATCH 001/114] add tests to assert for hash parsing --- www/assert/main.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/www/assert/main.js b/www/assert/main.js index eb9bb9157..3306c3bab 100644 --- a/www/assert/main.js +++ b/www/assert/main.js @@ -139,6 +139,49 @@ define([ strungJSON(orig); }); + // check that old hashes parse correctly + assert(function () { + var secret = Cryptpad.parseHash('67b8385b07352be53e40746d2be6ccd7XAYSuJYYqa9NfmInyHci7LNy'); + return secret.channel === "67b8385b07352be53e40746d2be6ccd7" && + secret.key === "XAYSuJYYqa9NfmInyHci7LNy" && + secret.version === 0; + }, "Old hash failed to parse"); + + // make sure version 1 hashes parse correctly + assert(function () { + var secret = Cryptpad.parseHash('/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI'); + return secret.version === 1 && + secret.mode === "edit" && + secret.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" && + secret.key === "usn4+9CqVja8Q7RZOGTfRgqI" && + !secret.present; + }, "version 1 hash failed to parse"); + + // test support for present mode in hashes + assert(function () { + var secret = Cryptpad.parseHash('/1/edit/CmN5+YJkrHFS3NSBg-P7Sg/DNZ2wcG683GscU4fyOyqA87G/present'); + return secret.version === 1 + && secret.mode === "edit" + && secret.channel === "CmN5+YJkrHFS3NSBg-P7Sg" + && secret.key === "DNZ2wcG683GscU4fyOyqA87G" + && secret.present; + }, "version 1 hash failed to parse"); + + // test support for trailing slash + assert(function () { + var secret = Cryptpad.parseHash('/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI/'); + return secret.version === 1 && + secret.mode === "edit" && + secret.channel === "3Ujt4F2Sjnjbis6CoYWpoQ" && + secret.key === "usn4+9CqVja8Q7RZOGTfRgqI" && + !secret.present; + }, "test support for trailing slashes in version 1 hash failed to parse"); + + assert(function () { + // TODO + return true; + }, "version 2 hash failed to parse correctly"); + var swap = function (str, dict) { return str.replace(/\{\{(.*?)\}\}/g, function (all, key) { return typeof dict[key] !== 'undefined'? dict[key] : all; From f8f97036be76599ee5b812ca5aedd0c9ac32f574 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 25 Apr 2017 16:12:28 +0200 Subject: [PATCH 002/114] ignore media-tag.js --- .jshintignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.jshintignore b/.jshintignore index ae60d4ba0..51636ed77 100644 --- a/.jshintignore +++ b/.jshintignore @@ -9,4 +9,4 @@ server.js NetFluxWebsocketSrv.js NetFluxWebsocketServer.js WebRTCSrv.js - +www/common/media-tag.js From f196b836db801cab6908ee4a641e8de3b6647635 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 25 Apr 2017 16:17:52 +0200 Subject: [PATCH 003/114] fix undefined reference --- www/examples/file/main.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/www/examples/file/main.js b/www/examples/file/main.js index 9a0d142fc..35bf4b191 100644 --- a/www/examples/file/main.js +++ b/www/examples/file/main.js @@ -16,6 +16,7 @@ define([ var $iframe = $('#pad-iframe').contents(); Cryptpad.addLoadingScreen(); + var toolbar; var andThen = function () { var $bar = $iframe.find('.toolbar-container'); @@ -38,9 +39,7 @@ define([ common: Cryptpad }; toolbar = Toolbar.create($bar, null, null, null, null, configTb); - }); - }; Cryptpad.ready(function (err, anv) { From 33090872778d3228e9d2bc7f42dc14b2a420a908 Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 25 Apr 2017 17:09:39 +0200 Subject: [PATCH 004/114] send feedback if isArray is not supported --- www/common/cryptpad-common.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 1d5015497..c2559bde2 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -1189,6 +1189,11 @@ define([ if (typeof(window.Proxy) === 'undefined') { feedback("NO_PROXIES"); } + + if (typeof(Array.isArray) === 'function') { + feedback("NO_ISARRAY"); + } + $(function() { // Race condition : if document.body is undefined when alertify.js is loaded, Alertify // won't work. We have to reset it now to make sure it uses a correct "body" From d985b144cc1ecfe4c90b79f10751ace2fe6c8ccb Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 25 Apr 2017 17:19:13 +0200 Subject: [PATCH 005/114] Add a new hash version for the file viewer --- .gitignore | 1 + server.js | 2 ++ www/common/common-hash.js | 26 +++++++++++++++++++------- www/common/cryptpad-common.js | 2 +- www/examples/file/main.js | 33 +++++++++++++++++++-------------- 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 996e55b97..76bc0ea38 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ www/scratch data npm-debug.log pins/ +blob/ diff --git a/server.js b/server.js index f12b90229..d7f5b90fc 100644 --- a/server.js +++ b/server.js @@ -82,6 +82,8 @@ var mainPages = config.mainPages || ['index', 'privacy', 'terms', 'about', 'cont var mainPagePattern = new RegExp('^\/(' + mainPages.join('|') + ').html$'); app.get(mainPagePattern, Express.static(__dirname + '/customize.dist')); +app.use("/blob", Express.static(__dirname + '/blob')); + app.use("/customize", Express.static(__dirname + '/customize')); app.use("/customize", Express.static(__dirname + '/customize.dist')); app.use(/^\/[^\/]*$/, Express.static('customize')); diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 40fe6bc7b..12f23c0a7 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -23,13 +23,16 @@ define([ return chanKey + keys; } if (!keys.editKeyStr) { return; } - return '/1/edit/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.editKeyStr); + return '/1/edit/' + hexToBase64(chanKey) + '/'+Crypto.b64RemoveSlashes(keys.editKeyStr)+'/'; }; var getViewHashFromKeys = Hash.getViewHashFromKeys = function (chanKey, keys) { if (typeof keys === 'string') { return; } - return '/1/view/' + hexToBase64(chanKey) + '/' + Crypto.b64RemoveSlashes(keys.viewKeyStr); + return '/1/view/' + hexToBase64(chanKey) + '/'+Crypto.b64RemoveSlashes(keys.viewKeyStr)+'/'; + }; + var getFileHashFromKey = Hash.getFileHashFromKey = function (fileKey, cryptKey, type) { + return '/2/' + hexToBase64(fileKey) + '/' + Crypto.b64RemoveSlashes(cryptKey) + '/' + Crypto.base64RemoveSlashes(type); }; var parsePadUrl = Hash.parsePadUrl = function (href) { @@ -119,7 +122,9 @@ define([ } } else if (version === "2") { // version 2 hashes are to be used for encrypted blobs - // TODO + var fileId = secret.file = hashArray[2].replace(/-/g, '/'); + var key = secret.key = hashArray[3].replace(/-/g, '/'); + var type = secret.type = hashArray[4].replace(/-/g, '/'); } } } @@ -150,7 +155,7 @@ define([ var channelId = Util.hexToBase64(createChannelId()); // 18 byte encryption key var key = Crypto.b64RemoveSlashes(Crypto.rand64(18)); - return '/1/edit/' + [channelId, key].join('/'); + return '/1/edit/' + [channelId, key].join('/') + '/'; }; /* @@ -159,8 +164,8 @@ Version 0 Version 1 /code/#/1/edit/3Ujt4F2Sjnjbis6CoYWpoQ/usn4+9CqVja8Q7RZOGTfRgqI Version 2 - /file//#/2// - /file//#/2/ajExFODrFH4lVBwxxsrOKw/pdf + /file/#/2/// + /file/#/2/K6xWU-LT9BJHCQcDCT-DcQ/ajExFODrFH4lVBwxxsrOKw/image-png */ var parseHash = Hash.parseHash = function (hash) { var parsed = {}; @@ -177,7 +182,14 @@ Version 2 parsed.mode = hashArr[2]; parsed.channel = hashArr[3]; parsed.key = hashArr[4]; - parsed.present = hashArr[5] && hashArr[5] === 'present'; + parsed.present = typeof(hashArr[5]) === "string" && hashArr[5] === 'present'; + return parsed; + } + if (hashArr[1] && hashArr[1] === '2') { + parsed.version = 2; + parsed.file = hashArr[2].replace(/-/g, '/'); + parsed.key = hashArr[3].replace(/-/g, '/'); + parsed.type = hashArr[4].replace(/-/g, '/'); return parsed; } return; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 1d5015497..757b04e15 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -514,7 +514,7 @@ define([ if (p.type !== parsed.type) { return pad; } - var shouldUpdate = p.hash === parsed.hash; + var shouldUpdate = p.hash.replace(/\/$/, '') === parsed.hash.replace(/\/$/, ''); // Version 1 : we have up to 4 differents hash for 1 pad, keep the strongest : // Edit > Edit (present) > View > View (present) diff --git a/www/examples/file/main.js b/www/examples/file/main.js index 9a0d142fc..4db3eb8ba 100644 --- a/www/examples/file/main.js +++ b/www/examples/file/main.js @@ -20,25 +20,30 @@ define([ var andThen = function () { var $bar = $iframe.find('.toolbar-container'); var secret = Cryptpad.getSecrets(); - var readOnly = secret.keys && !secret.keys.editKeyStr; - if (!secret.keys) { - secret.keys = secret.key; - } + + if (secret.keys) { throw new Error("You need a hash"); } // TODO + + var cryptKey = secret.key; + var fileId = secret.file; + var hexFileName = Cryptpad.base64ToHex(fileId); + var type = secret.type; + +// Test hash: +// #/2/K6xWU-LT9BJHCQcDCT-DcQ/TBo77200c0e-FdldQFcnQx4Y/image-png var $mt = $iframe.find('#encryptedFile'); - $mt.attr('src', './assets/image.png-encrypted'); - $mt.attr('data-crypto-key', 'TBo77200c0e/FdldQFcnQx4Y'); - $mt.attr('data-type', 'image/png'); + $mt.attr('src', '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName); + $mt.attr('data-crypto-key', cryptKey); + $mt.attr('data-type', type); require(['/common/media-tag.js'], function (MediaTag) { MediaTag($mt[0]); Cryptpad.removeLoadingScreen(); - var configTb = { - displayed: ['useradmin', 'newpad'], - ifrw: ifrw, - common: Cryptpad - }; - toolbar = Toolbar.create($bar, null, null, null, null, configTb); - + var configTb = { + displayed: ['useradmin', 'newpad'], + ifrw: ifrw, + common: Cryptpad + }; + Toolbar.create($bar, null, null, null, null, configTb); }); }; From 74bcec8b3116a5ebb89883bab34f3640a8dbd15d Mon Sep 17 00:00:00 2001 From: ansuz Date: Tue, 25 Apr 2017 18:28:39 +0200 Subject: [PATCH 006/114] programming is hard okay --- www/common/cryptpad-common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index c2559bde2..f0c710460 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -1190,7 +1190,7 @@ define([ feedback("NO_PROXIES"); } - if (typeof(Array.isArray) === 'function') { + if (typeof(Array.isArray) !== 'function') { feedback("NO_ISARRAY"); } From 9882a3a923a6a8cd51f65dfa37e2042b600af11d Mon Sep 17 00:00:00 2001 From: yflory Date: Tue, 25 Apr 2017 18:42:21 +0200 Subject: [PATCH 007/114] Add support for the file applicaiton in the drive --- customize.dist/translations/messages.fr.js | 1 + customize.dist/translations/messages.js | 1 + www/common/common-hash.js | 2 +- www/common/toolbar.js | 2 +- .../file/assets/image.png-encrypted | Bin www/{examples => }/file/index.html | 0 www/{examples => }/file/inner.html | 0 www/{examples => }/file/main.js | 40 +++++++++++++++++- 8 files changed, 43 insertions(+), 3 deletions(-) rename www/{examples => }/file/assets/image.png-encrypted (100%) rename www/{examples => }/file/index.html (100%) rename www/{examples => }/file/inner.html (100%) rename www/{examples => }/file/main.js (55%) diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 97aec74ab..429e2f928 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -11,6 +11,7 @@ define(function () { out.type.slide = 'Présentation'; out.type.drive = 'Drive'; out.type.whiteboard = "Tableau Blanc"; + out.type.file = "Fichier"; out.button_newpad = 'Nouveau document texte'; out.button_newcode = 'Nouvelle page de code'; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index e27a074b5..678d3c4fb 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -11,6 +11,7 @@ define(function () { out.type.slide = 'Presentation'; out.type.drive = 'Drive'; out.type.whiteboard = 'Whiteboard'; + out.type.file = 'File'; out.button_newpad = 'New Rich Text pad'; out.button_newcode = 'New Code pad'; diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 12f23c0a7..23f5f6a24 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -252,7 +252,7 @@ Version 2 if (parsed.version === 0) { return parsed.channel; - } else if (parsed.version !== 1) { + } else if (parsed.version !== 1 && parsed.version !== 2) { console.error("parsed href had no version"); console.error(parsed); return; diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 9302441f0..4162e3dbf 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -791,7 +791,7 @@ define([ if (!connected) { return; } checkLag(getLag, lagElement); }, 3000); - } + } else { connected = true; } var failed = function () { connected = false; diff --git a/www/examples/file/assets/image.png-encrypted b/www/file/assets/image.png-encrypted similarity index 100% rename from www/examples/file/assets/image.png-encrypted rename to www/file/assets/image.png-encrypted diff --git a/www/examples/file/index.html b/www/file/index.html similarity index 100% rename from www/examples/file/index.html rename to www/file/index.html diff --git a/www/examples/file/inner.html b/www/file/inner.html similarity index 100% rename from www/examples/file/inner.html rename to www/file/inner.html diff --git a/www/examples/file/main.js b/www/file/main.js similarity index 55% rename from www/examples/file/main.js rename to www/file/main.js index 298d88904..b628edee9 100644 --- a/www/examples/file/main.js +++ b/www/file/main.js @@ -31,6 +31,37 @@ define([ // Test hash: // #/2/K6xWU-LT9BJHCQcDCT-DcQ/TBo77200c0e-FdldQFcnQx4Y/image-png + var parsed = Cryptpad.parsePadUrl(window.location.href); + var defaultName = Cryptpad.getDefaultName(parsed); + + var getTitle = function () { + var pad = Cryptpad.getRelativeHref(window.location.href); + var fo = Cryptpad.getStore().getProxy().fo; + var data = fo.getFileData(pad); + return data ? data.title : undefined; + }; + + var updateTitle = function (newTitle) { + Cryptpad.renamePad(newTitle, function (err, data) { + if (err) { + console.log("Couldn't set pad title"); + console.error(err); + return; + } + document.title = newTitle; + $bar.find('.' + Toolbar.constants.title).find('span.title').text(data); + $bar.find('.' + Toolbar.constants.title).find('input').val(data); + }); + }; + + var suggestName = function () { + return document.title || getTitle() || ''; + }; + + var renameCb = function (err, title) { + document.title = title; + }; + var $mt = $iframe.find('#encryptedFile'); $mt.attr('src', '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName); $mt.attr('data-crypto-key', cryptKey); @@ -41,9 +72,16 @@ define([ var configTb = { displayed: ['useradmin', 'newpad'], ifrw: ifrw, - common: Cryptpad + common: Cryptpad, + title: { + onRename: renameCb, + defaultName: defaultName, + suggestName: suggestName + } }; Toolbar.create($bar, null, null, null, null, configTb); + + updateTitle(Cryptpad.initialName || getTitle() || defaultName); }); }; From f50aa5c29b959331da921bb9e8be757f4e5804fc Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 26 Apr 2017 14:55:06 +0200 Subject: [PATCH 008/114] Move the mediatag viewer into a media app --- customize.dist/translations/messages.fr.js | 1 + customize.dist/translations/messages.js | 1 + www/common/common-hash.js | 15 +-- www/common/toolbar.js | 16 +++- www/file/inner.html | 6 +- www/file/main.js | 43 +++++++-- www/media/assets/image.png-encrypted | Bin 0 -> 27455 bytes www/media/index.html | 47 ++++++++++ www/media/inner.html | 27 ++++++ www/media/main.js | 103 +++++++++++++++++++++ 10 files changed, 239 insertions(+), 20 deletions(-) create mode 100644 www/media/assets/image.png-encrypted create mode 100644 www/media/index.html create mode 100644 www/media/inner.html create mode 100644 www/media/main.js diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 429e2f928..2b50a088d 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -12,6 +12,7 @@ define(function () { out.type.drive = 'Drive'; out.type.whiteboard = "Tableau Blanc"; out.type.file = "Fichier"; + out.type.media = "Média"; out.button_newpad = 'Nouveau document texte'; out.button_newcode = 'Nouvelle page de code'; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 678d3c4fb..6c3ec7c4d 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -12,6 +12,7 @@ define(function () { out.type.drive = 'Drive'; out.type.whiteboard = 'Whiteboard'; out.type.file = 'File'; + out.type.media = 'Media'; out.button_newpad = 'New Rich Text pad'; out.button_newcode = 'New Code pad'; diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 23f5f6a24..620acd319 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -31,8 +31,8 @@ define([ } return '/1/view/' + hexToBase64(chanKey) + '/'+Crypto.b64RemoveSlashes(keys.viewKeyStr)+'/'; }; - var getFileHashFromKey = Hash.getFileHashFromKey = function (fileKey, cryptKey, type) { - return '/2/' + hexToBase64(fileKey) + '/' + Crypto.b64RemoveSlashes(cryptKey) + '/' + Crypto.base64RemoveSlashes(type); + var getFileHashFromKeys = Hash.getFileHashFromKeys = function (fileKey, cryptKey) { + return '/2/' + hexToBase64(fileKey) + '/' + Crypto.b64RemoveSlashes(cryptKey) + '/'; }; var parsePadUrl = Hash.parsePadUrl = function (href) { @@ -122,9 +122,8 @@ define([ } } else if (version === "2") { // version 2 hashes are to be used for encrypted blobs - var fileId = secret.file = hashArray[2].replace(/-/g, '/'); - var key = secret.key = hashArray[3].replace(/-/g, '/'); - var type = secret.type = hashArray[4].replace(/-/g, '/'); + secret.channel = hashArray[2].replace(/-/g, '/'); + secret.keys = { fileKeyStr: hashArray[3].replace(/-/g, '/') }; } } } @@ -139,6 +138,9 @@ define([ if (secret.keys.viewKeyStr) { hashes.viewHash = getViewHashFromKeys(channel, secret.keys); } + if (secret.keys.fileKeyStr) { + hashes.fileHash = getFileHashFromKeys(channel, secret.keys.fileKeyStr); + } return hashes; }; @@ -187,9 +189,8 @@ Version 2 } if (hashArr[1] && hashArr[1] === '2') { parsed.version = 2; - parsed.file = hashArr[2].replace(/-/g, '/'); + parsed.channel = hashArr[2].replace(/-/g, '/'); parsed.key = hashArr[3].replace(/-/g, '/'); - parsed.type = hashArr[4].replace(/-/g, '/'); return parsed; } return; diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 4162e3dbf..7ff4f2b51 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -205,6 +205,13 @@ define([ }); } } + if (hashes.fileHash) { + options.push({ + tag: 'a', + attributes: {title: Messages.viewShareTitle, 'class': 'fileShare'}, + content: ' ' + Messages.viewShare + }); + } var dropdownConfigShare = { text: $('
').append($shareIcon).append($span).html(), options: options @@ -223,7 +230,14 @@ define([ } if (hashes.viewHash) { $shareBlock.find('a.viewShare').click(function () { - var url = window.location.origin + window.location.pathname + '#' + hashes.viewHash; + var url = window.location.origin + window.location.pathname + '#' + hashes.viewHash ; + var success = Cryptpad.Clipboard.copy(url); + if (success) { Cryptpad.log(Messages.shareSuccess); } + }); + } + if (hashes.fileHash) { + $shareBlock.find('a.fileShare').click(function () { + var url = window.location.origin + window.location.pathname + '#' + hashes.fileHash ; var success = Cryptpad.Clipboard.copy(url); if (success) { Cryptpad.log(Messages.shareSuccess); } }); diff --git a/www/file/inner.html b/www/file/inner.html index bba73059e..7f315cef2 100644 --- a/www/file/inner.html +++ b/www/file/inner.html @@ -5,7 +5,6 @@ -
- + diff --git a/www/file/main.js b/www/file/main.js index b628edee9..bf7cb9e00 100644 --- a/www/file/main.js +++ b/www/file/main.js @@ -7,9 +7,11 @@ define([ '/common/visible.js', '/common/notify.js', '/bower_components/tweetnacl/nacl-fast.min.js', + '/bower_components/file-saver/FileSaver.min.js', ], function ($, Crypto, realtimeInput, Toolbar, Cryptpad, Visible, Notify) { var Messages = Cryptpad.Messages; - window.Nacl = window.nacl; + var saveAs = window.saveAs; + //window.Nacl = window.nacl; $(function () { var ifrw = $('#pad-iframe')[0].contentWindow; @@ -21,15 +23,14 @@ define([ var $bar = $iframe.find('.toolbar-container'); var secret = Cryptpad.getSecrets(); - if (secret.keys) { throw new Error("You need a hash"); } // TODO + if (!secret.keys) { throw new Error("You need a hash"); } // TODO - var cryptKey = secret.key; - var fileId = secret.file; + var cryptKey = secret.keys && secret.keys.fileKeyStr; + var fileId = secret.channel; var hexFileName = Cryptpad.base64ToHex(fileId); - var type = secret.type; - + var type = "image/png"; // Test hash: -// #/2/K6xWU-LT9BJHCQcDCT-DcQ/TBo77200c0e-FdldQFcnQx4Y/image-png +// #/2/K6xWU-LT9BJHCQcDCT-DcQ/TBo77200c0e-FdldQFcnQx4Y/ var parsed = Cryptpad.parsePadUrl(window.location.href); var defaultName = Cryptpad.getDefaultName(parsed); @@ -62,26 +63,48 @@ define([ document.title = title; }; + var blob; + var exportFile = function () { + var suggestion = document.title; + Cryptpad.prompt(Messages.exportPrompt, + Cryptpad.fixFileName(suggestion) + '.html', function (filename) { + if (!(typeof(filename) === 'string' && filename)) { return; } + //var blob = new Blob([html], {type: "text/html;charset=utf-8"}); + saveAs(blob, filename); + }); + }; + var $mt = $iframe.find('#encryptedFile'); $mt.attr('src', '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName); $mt.attr('data-crypto-key', cryptKey); $mt.attr('data-type', type); + require(['/common/media-tag.js'], function (MediaTag) { - MediaTag($mt[0]); - Cryptpad.removeLoadingScreen(); var configTb = { - displayed: ['useradmin', 'newpad'], + displayed: ['useradmin', 'share', 'newpad'], ifrw: ifrw, common: Cryptpad, title: { onRename: renameCb, defaultName: defaultName, suggestName: suggestName + }, + share: { + secret: secret, + channel: hexFileName } }; Toolbar.create($bar, null, null, null, null, configTb); + var $rightside = $bar.find('.' + Toolbar.constants.rightside); + + var $export = Cryptpad.createButton('export', true, {}, exportFile); + $rightside.append($export); updateTitle(Cryptpad.initialName || getTitle() || defaultName); + + var mt = MediaTag($mt[0]); + + Cryptpad.removeLoadingScreen(); }); }; diff --git a/www/media/assets/image.png-encrypted b/www/media/assets/image.png-encrypted new file mode 100644 index 0000000000000000000000000000000000000000..634bb90f22b5c88fa8295ea79c50e80feb480db0 GIT binary patch literal 27455 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 + + + CryptPad + + + + + + + + + + +
+
+ +
+ +
+

+
+
+ + diff --git a/www/media/inner.html b/www/media/inner.html new file mode 100644 index 000000000..bc5b96ae0 --- /dev/null +++ b/www/media/inner.html @@ -0,0 +1,27 @@ + + + + + + + + + + +
+ + + + diff --git a/www/media/main.js b/www/media/main.js new file mode 100644 index 000000000..e176701c8 --- /dev/null +++ b/www/media/main.js @@ -0,0 +1,103 @@ +define([ + 'jquery', + '/bower_components/chainpad-crypto/crypto.js', + '/bower_components/chainpad-netflux/chainpad-netflux.js', + '/common/toolbar.js', + '/common/cryptpad-common.js', + '/common/visible.js', + '/common/notify.js', + '/bower_components/tweetnacl/nacl-fast.min.js', + '/bower_components/file-saver/FileSaver.min.js', +], function ($, Crypto, realtimeInput, Toolbar, Cryptpad, Visible, Notify) { + var Messages = Cryptpad.Messages; + var saveAs = window.saveAs; + //window.Nacl = window.nacl; + $(function () { + + var ifrw = $('#pad-iframe')[0].contentWindow; + var $iframe = $('#pad-iframe').contents(); + + Cryptpad.addLoadingScreen(); + + var andThen = function () { + var $bar = $iframe.find('.toolbar-container'); + var secret = Cryptpad.getSecrets(); + + if (!secret.keys) { throw new Error("You need a hash"); } // TODO + + var cryptKey = secret.keys && secret.keys.fileKeyStr; + var fileId = secret.channel; + var hexFileName = Cryptpad.base64ToHex(fileId); + var type = "image/png"; +// Test hash: +// #/2/K6xWU-LT9BJHCQcDCT-DcQ/TBo77200c0e-FdldQFcnQx4Y/ + + var parsed = Cryptpad.parsePadUrl(window.location.href); + var defaultName = Cryptpad.getDefaultName(parsed); + + var getTitle = function () { + var pad = Cryptpad.getRelativeHref(window.location.href); + var fo = Cryptpad.getStore().getProxy().fo; + var data = fo.getFileData(pad); + return data ? data.title : undefined; + }; + + var updateTitle = function (newTitle) { + Cryptpad.renamePad(newTitle, function (err, data) { + if (err) { + console.log("Couldn't set pad title"); + console.error(err); + return; + } + document.title = newTitle; + $bar.find('.' + Toolbar.constants.title).find('span.title').text(data); + $bar.find('.' + Toolbar.constants.title).find('input').val(data); + }); + }; + + var suggestName = function () { + return document.title || getTitle() || ''; + }; + + var renameCb = function (err, title) { + document.title = title; + }; + + var $mt = $iframe.find('#encryptedFile'); + $mt.attr('src', '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName); + $mt.attr('data-crypto-key', cryptKey); + $mt.attr('data-type', type); + + require(['/common/media-tag.js'], function (MediaTag) { + var configTb = { + displayed: ['useradmin', 'share', 'newpad'], + ifrw: ifrw, + common: Cryptpad, + title: { + onRename: renameCb, + defaultName: defaultName, + suggestName: suggestName + }, + share: { + secret: secret, + channel: hexFileName + } + }; + Toolbar.create($bar, null, null, null, null, configTb); + var $rightside = $bar.find('.' + Toolbar.constants.rightside); + + updateTitle(Cryptpad.initialName || getTitle() || defaultName); + + var mt = MediaTag($mt[0]); + + Cryptpad.removeLoadingScreen(); + }); + }; + + Cryptpad.ready(function (err, anv) { + andThen(); + Cryptpad.reportAppUsage(); + }); + + }); +}); From 520dabe09426a9453be8045b7c6d91d489af8edf Mon Sep 17 00:00:00 2001 From: yflory Date: Wed, 26 Apr 2017 18:46:40 +0200 Subject: [PATCH 009/114] Remove the unsorted files category --- www/common/cryptpad-common.js | 16 +++++++++- www/common/fsStore.js | 4 +-- www/common/userObject.js | 59 +++++++++++++++++++++-------------- www/drive/main.js | 42 ++++++------------------- 4 files changed, 62 insertions(+), 59 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 5d8ab7804..4c119654e 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -495,6 +495,18 @@ define([ } }; + var updateFileName = function (href, oldName, newName) { + var fo = getStore().getProxy().fo; + var paths = fo.findFileInRoot(href); + paths.forEach(function (path) { + if (path.length !== 2) { return; } + var name = path[1].split('_')[0]; + var parsed = parsePadUrl(href); + if (path.length === 2 && name === oldName && isDefaultName(parsed, name)) { + fo.rename(path, newName); + } + }); + }; var setPadTitle = common.setPadTitle = function (name, cb) { var href = window.location.href; var parsed = parsePadUrl(href); @@ -540,6 +552,7 @@ define([ pad.atime = +new Date(); // set the name + var old = pad.title; pad.title = name; // If we now have a stronger version of a stored href, replace the weaker one by the strong one @@ -550,6 +563,7 @@ define([ }); } pad.href = href; + updateFileName(href, old, name); } return pad; }); @@ -557,7 +571,7 @@ define([ if (!contains) { var data = makePad(href, name); getStore().pushData(data); - getStore().addPad(href, common.initialPath, common.initialName || name); + getStore().addPad(data, common.initialPath); } if (updateWeaker.length > 0) { updateWeaker.forEach(function (obj) { diff --git a/www/common/fsStore.js b/www/common/fsStore.js index 74ccd710a..c5e30558c 100644 --- a/www/common/fsStore.js +++ b/www/common/fsStore.js @@ -88,8 +88,8 @@ define([ ret.removeData = filesOp.removeData; ret.pushData = filesOp.pushData; - ret.addPad = function (href, path, name) { - filesOp.add(href, path, name); + ret.addPad = function (data, path) { + filesOp.add(data, path); }; ret.forgetPad = function (href, cb) { diff --git a/www/common/userObject.js b/www/common/userObject.js index ca2bae258..9ea1af726 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -41,14 +41,13 @@ define([ var getStructure = exp.getStructure = function () { var a = {}; a[ROOT] = {}; - a[UNSORTED] = []; a[TRASH] = {}; a[FILES_DATA] = []; a[TEMPLATE] = []; return a; }; var getHrefArray = function () { - return [UNSORTED, TEMPLATE]; + return [TEMPLATE]; }; @@ -297,6 +296,9 @@ define([ return paths; }; + var findFileInRoot = exp.findFileInRoot = function (href) { + return _findFileInRoot([ROOT], href); + }; var _findFileInHrefArray = function (rootName, href) { var unsorted = files[rootName].slice(); var ret = []; @@ -345,10 +347,9 @@ define([ }; var findFile = exp.findFile = function (href) { var rootpaths = _findFileInRoot([ROOT], href); - var unsortedpaths = _findFileInHrefArray(UNSORTED, href); var templatepaths = _findFileInHrefArray(TEMPLATE, href); var trashpaths = _findFileInTrash([TRASH], href); - return rootpaths.concat(unsortedpaths, templatepaths, trashpaths); + return rootpaths.concat(templatepaths, trashpaths); }; var search = exp.search = function (value) { if (typeof(value) !== "string") { return []; } @@ -534,8 +535,10 @@ define([ // ADD - var add = exp.add = function (href, path, name, cb) { - if (!href) { return; } + var add = exp.add = function (data, path) { + if (!data || typeof(data) !== "object") { return; } + var href = data.href; + var name = data.title; var newPath = path, parentEl; if (path && !Array.isArray(path)) { newPath = decodeURIComponent(path).split(','); @@ -546,20 +549,16 @@ define([ parentEl.push(href); return; } - // Add to root - if (path && isPathIn(newPath, [ROOT]) && name) { - parentEl = find(newPath); + // Add to root if path is ROOT or if no path + var filesList = getFiles([ROOT, TRASH, 'hrefArray']); + if ((path && isPathIn(newPath, [ROOT]) || filesList.indexOf(href) === -1) && name) { + parentEl = find(newPath || [ROOT]); if (parentEl) { var newName = getAvailableName(parentEl, name); parentEl[newName] = href; return; } } - // No path: push to unsorted - var filesList = getFiles([ROOT, TRASH, 'hrefArray']); - if (filesList.indexOf(href) === -1) { files[UNSORTED].push(href); } - - if (typeof cb === "function") { cb(); } }; var addFile = exp.addFile = function (filePath, name, type, cb) { var parentEl = findElement(files, filePath); @@ -780,7 +779,7 @@ define([ // * FILES_DATA: - Data (title, cdate, adte) are stored in filesData. filesData contains only href keys linking to object with title, cdate, adate. // - Dates (adate, cdate) can be parsed/formatted // - All files in filesData should be either in 'root', 'trash' or 'unsorted'. If that's not the case, copy the fily to 'unsorted' - // * UNSORTED: Contains only files (href), and does not contains files that are in ROOT + // * TEMPLATE: Contains only files (href), and does not contains files that are in ROOT debug("Cleaning file system..."); var before = JSON.stringify(files); @@ -821,26 +820,37 @@ define([ } } }; + // Make sure unsorted doesn't exist anymore var fixUnsorted = function () { - if (!Array.isArray(files[UNSORTED])) { debug("UNSORTED was not an array"); files[UNSORTED] = []; } - files[UNSORTED] = Cryptpad.deduplicateString(files[UNSORTED].slice()); + if (!files[UNSORTED]) { return; } + debug("UNSORTED still exists in the object, removing it..."); var us = files[UNSORTED]; + if (us.length === 0) { + delete files[UNSORTED]; + return; + } var rootFiles = getFiles([ROOT, TEMPLATE]).slice(); var toClean = []; + var root = find([ROOT]); us.forEach(function (el, idx) { if (!isFile(el) || rootFiles.indexOf(el) !== -1) { - toClean.push(idx); + return; + //toClean.push(idx); } + var name = getFileData(el).title || NEW_FILE_NAME; + var newName = getAvailableName(root, name); + root[newName] = el; }); - toClean.forEach(function (idx) { + delete files[UNSORTED]; + /*toClean.forEach(function (idx) { us.splice(idx, 1); - }); + });*/ }; var fixTemplate = function () { if (!Array.isArray(files[TEMPLATE])) { debug("TEMPLATE was not an array"); files[TEMPLATE] = []; } files[TEMPLATE] = Cryptpad.deduplicateString(files[TEMPLATE].slice()); var us = files[TEMPLATE]; - var rootFiles = getFiles([ROOT, UNSORTED]).slice(); + var rootFiles = getFiles([ROOT]).slice(); var toClean = []; us.forEach(function (el, idx) { if (!isFile(el) || rootFiles.indexOf(el) !== -1) { @@ -855,6 +865,7 @@ define([ if (!$.isArray(files[FILES_DATA])) { debug("FILES_DATA was not an array"); files[FILES_DATA] = []; } var fd = files[FILES_DATA]; var rootFiles = getFiles([ROOT, TRASH, 'hrefArray']); + var root = find([ROOT]); var toClean = []; fd.forEach(function (el, idx) { if (!el || typeof(el) !== "object") { @@ -863,8 +874,10 @@ define([ return; } if (rootFiles.indexOf(el.href) === -1) { - debug("An element in filesData was not in ROOT, UNSORTED or TRASH.", el); - files[UNSORTED].push(el.href); + debug("An element in filesData was not in ROOT, TEMPLATE or TRASH.", el); + var name = el.title || NEW_FILE_NAME; + var newName = getAvailableName(root, name); + root[newName] = el.href; return; } }); diff --git a/www/drive/main.js b/www/drive/main.js index d7d90a9d8..038f9402e 100644 --- a/www/drive/main.js +++ b/www/drive/main.js @@ -41,8 +41,6 @@ define([ var SEARCH_NAME = Messages.fm_searchName; var ROOT = "root"; var ROOT_NAME = Messages.fm_rootName; - var UNSORTED = "unsorted"; - var UNSORTED_NAME = Messages.fm_unsortedName; var FILES_DATA = Cryptpad.storageKey; var FILES_DATA_NAME = Messages.fm_filesDataName; var TEMPLATE = "template"; @@ -68,9 +66,9 @@ define([ var getLastOpenedFolder = function () { var path; try { - path = localStorage[LOCALSTORAGE_LAST] ? JSON.parse(localStorage[LOCALSTORAGE_LAST]) : [UNSORTED]; + path = localStorage[LOCALSTORAGE_LAST] ? JSON.parse(localStorage[LOCALSTORAGE_LAST]) : [ROOT]; } catch (e) { - path = [UNSORTED]; + path = [ROOT]; } return path; }; @@ -218,7 +216,7 @@ define([ // Categories dislayed in the menu // _WORKGROUP_ : do not display unsorted - var displayedCategories = [ROOT, UNSORTED, TRASH, SEARCH]; + var displayedCategories = [ROOT, TRASH, SEARCH]; if (AppConfig.enableTemplates) { displayedCategories.push(TEMPLATE); } if (isWorkgroup()) { displayedCategories = [ROOT, TRASH, SEARCH]; } @@ -680,7 +678,7 @@ define([ var msg = Messages._getKey('fm_removeSeveralDialog', [paths.length]); if (paths.length === 1) { var path = paths[0]; - var name = path[0] === UNSORTED ? filesOp.getTitle(filesOp.find(path)) : path[path.length - 1]; + var name = path[0] === TEMPLATE ? filesOp.getTitle(filesOp.find(path)) : path[path.length - 1]; msg = Messages._getKey('fm_removeDialog', [name]); } Cryptpad.confirm(msg, function (res) { @@ -948,7 +946,6 @@ define([ switch (name) { case ROOT: pName = ROOT_NAME; break; case TRASH: pName = TRASH_NAME; break; - case UNSORTED: pName = UNSORTED_NAME; break; case TEMPLATE: pName = TEMPLATE_NAME; break; case FILES_DATA: pName = FILES_DATA_NAME; break; case SEARCH: pName = SEARCH_NAME; break; @@ -997,9 +994,6 @@ define([ case ROOT: msg = Messages.fm_info_root; break; - case UNSORTED: - msg = Messages.fm_info_unsorted; - break; case TEMPLATE: msg = Messages.fm_info_template; break; @@ -1243,10 +1237,6 @@ define([ //return $fileHeader; }; - var allFilesSorted = function () { - return filesOp.getFiles([UNSORTED]).length === 0; - }; - var sortElements = function (folder, path, oldkeys, prop, asc, useHref, useData) { var root = filesOp.find(path); var test = folder ? filesOp.isFolder : filesOp.isFile; @@ -1343,7 +1333,6 @@ define([ // and they don't hav a hierarchical structure (folder/subfolders) var displayHrefArray = function ($container, rootName, draggable) { var unsorted = files[rootName]; - if (rootName === UNSORTED && allFilesSorted()) { return; } var $fileHeader = getFileListHeader(false); $container.append($fileHeader); var keys = unsorted; @@ -1517,7 +1506,6 @@ define([ } var isInRoot = filesOp.isPathIn(path, [ROOT]); var isTrashRoot = filesOp.comparePath(path, [TRASH]); - var isUnsorted = filesOp.comparePath(path, [UNSORTED]); var isTemplate = filesOp.comparePath(path, [TEMPLATE]); var isAllFiles = filesOp.comparePath(path, [FILES_DATA]); var isSearch = path[0] === SEARCH; @@ -1596,7 +1584,7 @@ define([ var $folderHeader = getFolderListHeader(); var $fileHeader = getFileListHeader(true); - if (isUnsorted || isTemplate) { + if (isTemplate) { displayHrefArray($list, path[0], true); } else if (isAllFiles) { displayAllFiles($list); @@ -1733,15 +1721,6 @@ define([ }); }; - var createUnsorted = function ($container, path) { - var $icon = $unsortedIcon.clone(); - var isOpened = filesOp.comparePath(path, currentPath); - var $unsortedElement = createTreeElement(UNSORTED_NAME, $icon, [UNSORTED], false, true, false, isOpened); - $unsortedElement.addClass('root'); - var $unsortedList = $('
    ', { id: 'unsortedTree', 'class': 'category2' }).append($unsortedElement); - $container.append($unsortedList); - }; - var createTemplate = function ($container, path) { var $icon = $templateIcon.clone(); var isOpened = filesOp.comparePath(path, currentPath); @@ -1808,7 +1787,6 @@ define([ $tree.html(''); if (displayedCategories.indexOf(SEARCH) !== -1) { createSearch($tree); } if (displayedCategories.indexOf(ROOT) !== -1) { createTree($tree, [ROOT]); } - if (displayedCategories.indexOf(UNSORTED) !== -1) { createUnsorted($tree, [UNSORTED]); } if (displayedCategories.indexOf(TEMPLATE) !== -1) { createTemplate($tree, [TEMPLATE]); } if (displayedCategories.indexOf(FILES_DATA) !== -1) { createAllFiles($tree, [FILES_DATA]); } if (displayedCategories.indexOf(TRASH) !== -1) { createTrash($tree, [TRASH]); } @@ -1830,9 +1808,6 @@ define([ case ROOT: prettyName = ROOT_NAME; break; - case UNSORTED: - prettyName = UNSORTED_NAME; - break; case FILES_DATA: prettyName = FILES_DATA_NAME; break; @@ -2210,13 +2185,14 @@ define([ Get.put(hash, Messages.driveReadme, function (e) { if (e) { logError(e); } var href = '/pad/#' + hash; - proxy.drive[UNSORTED].push(href); - proxy.drive[FILES_DATA].push({ + var data = { href: href, title: Messages.driveReadmeTitle, atime: new Date().toISOString(), ctime: new Date().toISOString() - }); + }; + filesOp.pushData(data); + filesOp.add(data); if (typeof(cb) === "function") { cb(); } }); delete sessionStorage.createReadme; From a97e7223f1ac52b0589edc2ca127bd6fc1341175 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 27 Apr 2017 12:47:21 +0200 Subject: [PATCH 010/114] implement getBlobPathFromHex --- www/common/common-hash.js | 4 ++++ www/common/cryptpad-common.js | 1 + 2 files changed, 5 insertions(+) diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 620acd319..b282a1b94 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -266,5 +266,9 @@ Version 2 return hex; }; + var getBlobPath = Hash.getBlobPathFromHex = function (id) { + return '/blob/' + id.slice(0,2) + '/' + id; + }; + return Hash; }); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 5d8ab7804..f4834b285 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -73,6 +73,7 @@ define([ var hrefToHexChannelId = common.hrefToHexChannelId = Hash.hrefToHexChannelId; var parseHash = common.parseHash = Hash.parseHash; var getRelativeHref = common.getRelativeHref = Hash.getRelativeHref; + common.getBlobPathFromHex = Hash.getBlobPathFromHex; common.getEditHashFromKeys = Hash.getEditHashFromKeys; common.getViewHashFromKeys = Hash.getViewHashFromKeys; From e2942f959b549d72faeffbe6cf833e9cf4a93779 Mon Sep 17 00:00:00 2001 From: ansuz Date: Thu, 27 Apr 2017 12:56:42 +0200 Subject: [PATCH 011/114] add crypto for decrypting a chunked file --- www/file/file-crypto.js | 88 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 www/file/file-crypto.js diff --git a/www/file/file-crypto.js b/www/file/file-crypto.js new file mode 100644 index 000000000..8c520dd59 --- /dev/null +++ b/www/file/file-crypto.js @@ -0,0 +1,88 @@ +define([ + '/bower_components/tweetnacl/nacl-fast.min.js', +], function () { + var Nacl = window.nacl; + + var chunkLength = 131088; + + var slice = function (A) { + return Array.prototype.slice.call(A); + }; + + var increment = function (N) { + var l = N.length; + while (l-- > 1) { + if (N[l] !== 255) { return void N[l]++; } + N[l] = 0; + if (l === 0) { return true; } + } + }; + + var joinChunks = function (B) { + return new Uint8Array(chunks.reduce(function (A, B) { + return slice(A).concat(slice(B)); + }, [])); + }; + + var decrypt = function (u8, key, cb) { + var nonce = new Uint8Array(new Array(24).fill(0)); + var i = 0; + var takeChunk = function () { + let start = i * chunkLength; + let end = start + chunkLength; + i++; + let box = new Uint8Array(u8.subarray(start, end)); + + // decrypt the chunk + let plaintext = Nacl.secretbox.open(box, nonce, key); + increment(nonce); + return plaintext; + }; + + var buffer = ''; + + var res = { + metadata: undefined, + }; + + // decrypt metadata + for (; !res.metadata && i * chunkLength < u8.length;) { + var chunk = takeChunk(); + buffer += Nacl.util.encodeUTF8(chunk); + try { + res.metadata = JSON.parse(buffer); + //console.log(res.metadata); + } catch (e) { + console.log('buffering another chunk for metadata'); + } + } + + if (!res.metadata) { + return void setTimeout(function () { + cb('NO_METADATA'); + }); + } + + var chunks = []; + // decrypt file contents + for (;i * chunkLength < u8.length;) { + let chunk = takeChunk(); + if (!chunk) { + return void window.setTimeout(function () { + cb('DECRYPTION_ERROR'); + }); + //throw new Error('failed to parse'); + } + chunks.push(chunk); + } + + // send chunks + res.content = joinChunks(chunks); + + cb(void 0, res); + }; + + return { + decrypt: decrypt, + }; +}); From 197b366712db2e8a6667160c192124c3d76d7de4 Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 27 Apr 2017 17:01:56 +0200 Subject: [PATCH 012/114] Ability to drag&select in the drive --- www/drive/file.css | 25 +++++++++-- www/drive/file.less | 23 ++++++++-- www/drive/main.js | 103 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 6 deletions(-) diff --git a/www/drive/file.css b/www/drive/file.css index a68b8e7bc..38b314d56 100644 --- a/www/drive/file.css +++ b/www/drive/file.css @@ -89,6 +89,16 @@ li { .selected .fa-plus-square-o { color: #000; } +.selectedTmp { + border: 1px dotted #bbb; + background: #AAA; + color: #ddd; + margin: -1px; +} +.selectedTmp .fa-minus-square-o, +.selectedTmp .fa-plus-square-o { + color: #000; +} span.fa-folder, span.fa-folder-open { color: #FEDE8B; @@ -215,6 +225,12 @@ span.fa-folder-open { flex: 1; display: flex; flex-flow: column; + position: relative; +} +#content .selectBox { + display: none; + background-color: rgba(100, 100, 100, 0.7); + position: absolute; } #content.readonly { background: #e6e6e6; @@ -242,7 +258,7 @@ span.fa-folder-open { #content li:not(.header) *:not(input) { /*pointer-events: none;*/ } -#content li:not(.header):hover:not(.selected) { +#content li:not(.header):hover:not(.selected, .selectedTmp) { background-color: #eee; } #content li:not(.header):hover .name { @@ -304,8 +320,8 @@ span.fa-folder-open { padding-bottom: 5px; max-height: 145px; } -#content div.grid li:not(.selected) { - border: transparent 1px; +#content div.grid li:not(.selected):not(.selectedTmp) { + border: 1px solid transparent; } #content div.grid li .name { width: 100%; @@ -326,6 +342,9 @@ span.fa-folder-open { #content div.grid .listElement { display: none; } +#content .list { + padding-left: 20px; +} #content .list ul { display: table; width: 100%; diff --git a/www/drive/file.less b/www/drive/file.less index 82c6a8657..03bc4750f 100644 --- a/www/drive/file.less +++ b/www/drive/file.less @@ -121,6 +121,16 @@ li { } } +.selectedTmp { + border: 1px dotted #bbb; + background: #AAA; + color: #ddd; + margin: -1px; + .fa-minus-square-o, .fa-plus-square-o { + color: @tree-fg; + } +} + span { &.fa-folder, &.fa-folder-open { color: #FEDE8B; @@ -260,6 +270,12 @@ span { flex: 1; display: flex; flex-flow: column; + position: relative; + .selectBox { + display: none; + background-color: rgba(100, 100, 100, 0.7); + position: absolute; + } &.readonly { background: @content-bg-ro; } @@ -287,7 +303,7 @@ span { /*pointer-events: none;*/ } &:hover { - &:not(.selected) { + &:not(.selected, .selectedTmp) { background-color: @drive-hover; } .name { @@ -357,8 +373,8 @@ span { padding-bottom: 5px; max-height: 145px; - &:not(.selected) { - border: transparent 1px; + &:not(.selected):not(.selectedTmp) { + border: 1px solid transparent; } .name { width: 100%; @@ -384,6 +400,7 @@ span { .list { // Make it act as a table! + padding-left: 20px; ul { display: table; width: 100%; diff --git a/www/drive/main.js b/www/drive/main.js index 038f9402e..057a0a8ee 100644 --- a/www/drive/main.js +++ b/www/drive/main.js @@ -199,6 +199,7 @@ define([ var $trashTreeContextMenu = $iframe.find("#trashTreeContextMenu"); var $trashContextMenu = $iframe.find("#trashContextMenu"); + // TOOLBAR /* add a "change username" button */ @@ -252,12 +253,113 @@ define([ return $el.is('.element-row') ? $el : $el.closest('.element-row'); }; + + // Selection var removeSelected = function () { $iframe.find('.selected').removeClass("selected"); var $container = $driveToolbar.find('#contextButtonsContainer'); if (!$container.length) { return; } $container.html(''); }; + + var sel = {}; + sel.refresh = 200; + sel.$selectBox = $('
    ', {'class': 'selectBox'}).appendTo($content); + var checkSelected = function () { + if (!sel.down) { return; } + var pos = sel.pos; + var l = $content[0].querySelectorAll('.element:not(.selected):not(.header)'); + var p, el; + for (var i = 0; i < l.length; i++) { + el = l[i]; + p = $(el).position(); + p.top += 10 + $content.scrollTop(); + p.left += 10; + p.bottom = p.top + $(el).outerHeight(); + p.right = p.left + $(el).outerWidth(); + if (p.right < pos.left || p.left > pos.right + || p.top > pos.bottom || p.bottom < pos.top) { + $(el).removeClass('selectedTmp'); + } else { + $(el).addClass('selectedTmp'); + } + } + }; + $content.on('mousedown', function (e) { + console.log('down'); + sel.down = true; + if (!e.ctrlKey) { removeSelected(); } + var rect = e.currentTarget.getBoundingClientRect(); + sel.startX = e.clientX - rect.left, + sel.startY = e.clientY - rect.top + $content.scrollTop(); + sel.$selectBox.show().css({ + left: sel.startX + 'px', + top: sel.startY + 'px', + width: '0px', + height: '0px' + }); + if (sel.move) { console.log('ret'); return; } + sel.move = function (ev) { + var rectMove = ev.currentTarget.getBoundingClientRect(), + offX = ev.clientX - rectMove.left, + offY = ev.clientY - rectMove.top + $content.scrollTop(); + + + var left = sel.startX, + top = sel.startY; + var width = offX - sel.startX; + if (width < 0) { + left = Math.max(0, offX); + var diffX = left-offX; + width = Math.abs(width) - diffX; + } + var height = offY - sel.startY; + if (height < 0) { + top = Math.max(0, offY); + var diffY = top-offY; + height = Math.abs(height) - diffY; + } + sel.$selectBox.css({ + width: width + 'px', + left: left + 'px', + height: height + 'px', + top: top + 'px' + }); + + + sel.pos = { + top: top, + left: left, + bottom: top + height, + right: left + width + }; + var diffT = sel.update ? +new Date() - sel.update : sel.refresh; + if (diffT < sel.refresh) { + if (!sel.to) { + sel.to = window.setTimeout(function () { + sel.update = +new Date(); + checkSelected(); + sel.to = undefined; + }, (sel.refresh - diffT)); + } + console.log('cancelled'); + return; + } + sel.update = +new Date(); + checkSelected(); + }; + $content.mousemove(sel.move); + }); + $content.on('mouseup', function (e) { + console.log(sel.pos); + sel.down = false; + sel.$selectBox.hide(); + $content.off('mousemove', sel.move); + delete sel.move; + $content.find('.selectedTmp').removeClass('selectedTmp').addClass('selected'); + }); + + var removeInput = function (cancel) { if (!cancel && $iframe.find('.element-row > input').length === 1) { var $input = $iframe.find('.element-row > input'); @@ -1501,6 +1603,7 @@ define([ currentPath = path; var s = $content.scrollTop() || 0; $content.html(""); + sel.$selectBox = $('
    ', {'class': 'selectBox'}).appendTo($content); if (!path || path.length === 0) { path = [ROOT]; } From bf7c7c45d0d62edb226597a17ea8d84c214b34fd Mon Sep 17 00:00:00 2001 From: yflory Date: Thu, 27 Apr 2017 18:46:46 +0200 Subject: [PATCH 013/114] Add the storage limit warning in the toolbar --- customize.dist/src/less/toolbar.less | 16 +++++++++++++++ customize.dist/toolbar.css | 14 +++++++++++++ customize.dist/translations/messages.fr.js | 4 ++++ customize.dist/translations/messages.js | 4 ++++ www/code/main.js | 2 +- www/common/cryptpad-common.js | 4 ++++ www/common/toolbar.js | 24 ++++++++++++++++++++++ 7 files changed, 67 insertions(+), 1 deletion(-) diff --git a/customize.dist/src/less/toolbar.less b/customize.dist/src/less/toolbar.less index 5e4f74dcf..f67e2ee96 100644 --- a/customize.dist/src/less/toolbar.less +++ b/customize.dist/src/less/toolbar.less @@ -79,6 +79,22 @@ } } + .cryptpad-limit { + color: red; + box-sizing: content-box; + height: 16px; + width: 16px; + display: inline-block; + padding: 3px; + margin: 3px; + margin-right: 6px; + font-size: 20px; + span { + cursor: pointer; + margin: auto; + } + } + .cryptpad-lag { box-sizing: content-box; height: 16px; diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css index 1165c6df4..db8ba7776 100644 --- a/customize.dist/toolbar.css +++ b/customize.dist/toolbar.css @@ -150,6 +150,20 @@ .cryptpad-toolbar button.hidden { display: none; } +.cryptpad-toolbar .cryptpad-limit { + color: red; + box-sizing: content-box; + height: 16px; + width: 16px; + display: inline-block; + padding: 3px; + margin: 3px; + margin-right: 6px; + font-size: 20px; +} +.cryptpad-toolbar .cryptpad-limit span { + margin: auto; +} .cryptpad-toolbar .cryptpad-lag { box-sizing: content-box; height: 16px; diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 2b50a088d..a826659ca 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -55,6 +55,10 @@ define(function () { out.orangeLight = "Votre connexion est lente, ce qui réduit la qualité de l'éditeur"; out.redLight = "Vous êtes déconnectés de la session"; + out.pinLimitReached = "Vous avez atteint votre limite de stockage"; + out.pinLimitReachedAlert = "Vous avez atteint votre limite de stockage. Ce pad ne sera pas enregistré dans votre CrypDrive.
    " + + "Pour résoudre ce problème, vous pouvez soit supprimer des pads de votre CryptDrive (y compris la corbeille), soit vous abonner à une offre premium pour augmenter la limite maximale."; + out.importButtonTitle = 'Importer un pad depuis un fichier local'; out.exportButtonTitle = 'Exporter ce pad vers un fichier local'; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 6c3ec7c4d..5e37ffdc6 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -57,6 +57,10 @@ define(function () { out.orangeLight = "Your slow connection may impact your experience"; out.redLight = "You are disconnected from the session"; + out.pinLimitReached = "You've reached your storage limit"; + out.pinLimitReachedAlert = "You've reached your storage limit. This pad won't be stored in your CryptDrive.
    " + + "To fix this problem, you can either remove pads from your CryptDrive (including the trash) or subscribe to a premium offer to increase your limit."; + out.importButtonTitle = 'Import a pad from a local file'; out.exportButtonTitle = 'Export this pad to a local file'; diff --git a/www/code/main.js b/www/code/main.js index 4f12d0926..35bcd90d4 100644 --- a/www/code/main.js +++ b/www/code/main.js @@ -382,7 +382,7 @@ define([ userList = info.userList; var configTb = { - displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'], + displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'], userData: userData, readOnly: readOnly, ifrw: ifrw, diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 4c119654e..66030927f 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -708,6 +708,10 @@ define([ }); }; + var getPinLimit = common.getPinLimit = function (cb) { + cb(void 0, 10); + }; + var createButton = common.createButton = function (type, rightside, data, callback) { var button; var size = "17px"; diff --git a/www/common/toolbar.js b/www/common/toolbar.js index 7ff4f2b51..ed7b88459 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -16,6 +16,8 @@ define([ /** Id of the div containing the lag info. */ var LAG_ELEM_CLS = Bar.constants.lag = 'cryptpad-lag'; + var LIMIT_ELEM_CLS = Bar.constants.lag = 'cryptpad-limit'; + /** The toolbar class which contains the user list, debug link and lag. */ var TOOLBAR_CLS = Bar.constants.toolbar = 'cryptpad-toolbar'; @@ -488,6 +490,28 @@ define([ $userContainer.append($lag); } + if (config.displayed.indexOf('limit') !== -1 && Config.enablePinning) { + var usage; + var $limitIcon = $('', {'class': 'fa fa-exclamation-triangle'}); + var $limit = $('', { + 'class': LIMIT_ELEM_CLS, + 'title': Messages.pinLimitReached + }).append($limitIcon).hide().appendTo($userContainer); + var andThen = function (e, limit) { + if (usage > limit) { + $limit.show().click(function () { + Cryptpad.alert(Messages.pinLimitReachedAlert, null, true); + }); + } + }; + var todo = function (e, used) { + usage = Cryptpad.bytesToMegabytes(used); + if (e) { console.error("Unable tog et the pinned usage"); return; } + Cryptpad.getPinLimit(andThen); + }; + Cryptpad.getPinnedUsage(todo); + } + if (config.displayed.indexOf('newpad') !== -1) { var pads_options = []; Config.availablePadTypes.forEach(function (p) { From e132ccf94ac7d381c0a30289855cba67e62a9dfd Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 28 Apr 2017 11:45:53 +0200 Subject: [PATCH 014/114] prepare for upload --- .jshintignore | 1 + www/file/file-crypto.js | 126 ++++++++++++++++++++++++++++++++----- www/file/inner.html | 36 ++++++++++- www/file/main.js | 135 ++++++++++++++++++++++++++++------------ 4 files changed, 239 insertions(+), 59 deletions(-) diff --git a/.jshintignore b/.jshintignore index 51636ed77..93e467aef 100644 --- a/.jshintignore +++ b/.jshintignore @@ -10,3 +10,4 @@ NetFluxWebsocketSrv.js NetFluxWebsocketServer.js WebRTCSrv.js www/common/media-tag.js +www/scratch diff --git a/www/file/file-crypto.js b/www/file/file-crypto.js index 8c520dd59..924cbe334 100644 --- a/www/file/file-crypto.js +++ b/www/file/file-crypto.js @@ -2,39 +2,75 @@ define([ '/bower_components/tweetnacl/nacl-fast.min.js', ], function () { var Nacl = window.nacl; + var PARANOIA = true; - var chunkLength = 131088; + var plainChunkLength = 128 * 1024; + var cypherChunkLength = 131088; 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) { - if (N[l] !== 255) { return void N[l]++; } + if (PARANOIA) { + if (typeof(N[l]) !== 'number') { + throw new Error('E_UNSAFE_TYPE'); + } + if (N[l] > 255) { + throw new Error('E_OUT_OF_BOUNDS'); + } + } + /* jshint probably 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 N[l] = 0; + + // you don't need to worry about this running out. + // you'd need a REAAAALLY big file if (l === 0) { return true; } } }; - var joinChunks = function (B) { + var joinChunks = function (chunks) { return new Uint8Array(chunks.reduce(function (A, B) { return slice(A).concat(slice(B)); }, [])); }; + var padChunk = function (A) { + var padding; + if (A.length === plainChunkLength) { return A; } + if (A.length < plainChunkLength) { + padding = new Array(plainChunkLength - A.length).fill(32); + return A.concat(padding); + } + if (A.length > plainChunkLength) { + // how many times larger is it? + var chunks = Math.ceil(A.length / plainChunkLength); + padding = new Array((plainChunkLength * chunks) - A.length).fill(32); + return A.concat(padding); + } + }; + var decrypt = function (u8, key, cb) { - var nonce = new Uint8Array(new Array(24).fill(0)); + var nonce = createNonce(); var i = 0; + var takeChunk = function () { - let start = i * chunkLength; - let end = start + chunkLength; + var start = i * cypherChunkLength; + var end = start + cypherChunkLength; i++; - let box = new Uint8Array(u8.subarray(start, end)); + var box = new Uint8Array(u8.subarray(start, end)); // decrypt the chunk - let plaintext = Nacl.secretbox.open(box, nonce, key); + var plaintext = Nacl.secretbox.open(box, nonce, key); + // TODO handle nonce-too-large-error increment(nonce); return plaintext; }; @@ -46,8 +82,9 @@ define([ }; // decrypt metadata - for (; !res.metadata && i * chunkLength < u8.length;) { - var chunk = takeChunk(); + var chunk; + for (; !res.metadata && i * cypherChunkLength < u8.length;) { + chunk = takeChunk(); buffer += Nacl.util.encodeUTF8(chunk); try { res.metadata = JSON.parse(buffer); @@ -63,15 +100,16 @@ define([ }); } + var fail = function () { + cb("DECRYPTION_ERROR"); + }; + var chunks = []; // decrypt file contents - for (;i * chunkLength < u8.length;) { - let chunk = takeChunk(); + for (;i * cypherChunkLength < u8.length;) { + chunk = takeChunk(); if (!chunk) { - return void window.setTimeout(function () { - cb('DECRYPTION_ERROR'); - }); - //throw new Error('failed to parse'); + return window.setTimeout(fail); } chunks.push(chunk); } @@ -82,7 +120,63 @@ define([ cb(void 0, res); }; + // metadata + /* { filename: 'raccoon.jpg', type: 'image/jpeg' } */ + + + /* TODO + in your callback, return an object which you can iterate... + + + */ + + var encrypt = function (u8, metadata, key, cb) { + var nonce = createNonce(); + + // encode metadata + var metaBuffer = Array.prototype.slice + .call(Nacl.util.decodeUTF8(JSON.stringify(metadata))); + + var plaintext = new Uint8Array(padChunk(metaBuffer)); + + var chunks = []; + var j = 0; + + var start; + var end; + + var part; + var box; + + // prepend some metadata + for (;j * plainChunkLength < plaintext.length; j++) { + start = j * plainChunkLength; + end = start + plainChunkLength; + + part = plaintext.subarray(start, end); + box = Nacl.secretbox(part, nonce, key); + chunks.push(box); + increment(nonce); + } + + // append the encrypted file chunks + var i = 0; + for (;i * plainChunkLength < u8.length; i++) { + start = i * plainChunkLength; + end = start + plainChunkLength; + + part = new Uint8Array(u8.subarray(start, end)); + box = Nacl.secretbox(part, nonce, key); + chunks.push(box); + increment(nonce); + } + + + // TODO do something with the chunks... + }; + return { decrypt: decrypt, + encrypt: encrypt, }; }); diff --git a/www/file/inner.html b/www/file/inner.html index 7f315cef2..09f627842 100644 --- a/www/file/inner.html +++ b/www/file/inner.html @@ -14,14 +14,44 @@ padding: 0px; display: inline-block; } - media-tag * { - max-width: 100%; + #file { + display: block; + height: 300px; + width: 300px; + border: 2px solid black; + margin: 50px; } + + .inputfile { + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; + } + .inputfile + label { + border: 2px solid black; + display: block; + height: 500px; + width: 500px; + background-color: rgba(50, 50, 50, .10); + margin: 50px; + } + + .inputfile:focus + label, + .inputfile + label:hover { + background-color: rgba(50, 50, 50, 0.30); + } +
    - + diff --git a/www/file/main.js b/www/file/main.js index bf7cb9e00..e237d7a63 100644 --- a/www/file/main.js +++ b/www/file/main.js @@ -6,12 +6,14 @@ define([ '/common/cryptpad-common.js', '/common/visible.js', '/common/notify.js', + '/file/file-crypto.js', '/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/file-saver/FileSaver.min.js', -], function ($, Crypto, realtimeInput, Toolbar, Cryptpad, Visible, Notify) { +], function ($, Crypto, realtimeInput, Toolbar, Cryptpad, Visible, Notify, FileCrypto) { var Messages = Cryptpad.Messages; var saveAs = window.saveAs; - //window.Nacl = window.nacl; + var Nacl = window.nacl; + $(function () { var ifrw = $('#pad-iframe')[0].contentWindow; @@ -19,18 +21,40 @@ define([ Cryptpad.addLoadingScreen(); + var fetch = function (src, cb) { + var xhr = new XMLHttpRequest(); + xhr.open("GET", src, true); + xhr.responseType = "arraybuffer"; + xhr.onload = function (e) { + return void cb(void 0, new Uint8Array(xhr.response)); + }; + xhr.send(null); + }; + + var upload = function (blob, id, key) { + Cryptpad.alert("UPLOAD IS NOT IMPLEMENTED YET"); + }; + + var myFile; + var myDataType; + var uploadMode = false; + var andThen = function () { var $bar = $iframe.find('.toolbar-container'); - var secret = Cryptpad.getSecrets(); - if (!secret.keys) { throw new Error("You need a hash"); } // TODO - - var cryptKey = secret.keys && secret.keys.fileKeyStr; - var fileId = secret.channel; - var hexFileName = Cryptpad.base64ToHex(fileId); - var type = "image/png"; // Test hash: // #/2/K6xWU-LT9BJHCQcDCT-DcQ/TBo77200c0e-FdldQFcnQx4Y/ + var secret; + var hexFileName; + if (window.location.hash) { + secret = Cryptpad.getSecrets(); + if (!secret.keys) { throw new Error("You need a hash"); } // TODO + hexFileName = Cryptpad.base64ToHex(secret.channel); + } else { + uploadMode = true; + } + + //window.location.hash = '/2/K6xWU-LT9BJHCQcDCT-DcQ/VLIgpQOgmSaW3AQcUCCoJnYvCbMSO0MKBqaICSly9fo='; var parsed = Cryptpad.parsePadUrl(window.location.href); var defaultName = Cryptpad.getDefaultName(parsed); @@ -67,45 +91,76 @@ define([ var exportFile = function () { var suggestion = document.title; Cryptpad.prompt(Messages.exportPrompt, - Cryptpad.fixFileName(suggestion) + '.html', function (filename) { + Cryptpad.fixFileName(suggestion), function (filename) { if (!(typeof(filename) === 'string' && filename)) { return; } - //var blob = new Blob([html], {type: "text/html;charset=utf-8"}); + var blob = new Blob([myFile], {type: myDataType}); saveAs(blob, filename); }); }; - var $mt = $iframe.find('#encryptedFile'); - $mt.attr('src', '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName); - $mt.attr('data-crypto-key', cryptKey); - $mt.attr('data-type', type); - - require(['/common/media-tag.js'], function (MediaTag) { - var configTb = { - displayed: ['useradmin', 'share', 'newpad'], - ifrw: ifrw, - common: Cryptpad, - title: { - onRename: renameCb, - defaultName: defaultName, - suggestName: suggestName - }, - share: { - secret: secret, - channel: hexFileName - } - }; - Toolbar.create($bar, null, null, null, null, configTb); - var $rightside = $bar.find('.' + Toolbar.constants.rightside); - - var $export = Cryptpad.createButton('export', true, {}, exportFile); - $rightside.append($export); - - updateTitle(Cryptpad.initialName || getTitle() || defaultName); + var displayed = ['useradmin', 'newpad', 'limit']; + if (secret && hexFileName) { + displayed.push('share'); + } + + var configTb = { + displayed: displayed, + ifrw: ifrw, + common: Cryptpad, + title: { + onRename: renameCb, + defaultName: defaultName, + suggestName: suggestName + }, + share: { + secret: secret, + channel: hexFileName + } + }; + Toolbar.create($bar, null, null, null, null, configTb); + var $rightside = $bar.find('.' + Toolbar.constants.rightside); + + var $export = Cryptpad.createButton('export', true, {}, exportFile); + $rightside.append($export); + + updateTitle(Cryptpad.initialName || getTitle() || defaultName); + + if (!uploadMode) { + var src = Cryptpad.getBlobPathFromHex(hexFileName); + return fetch(src, function (e, u8) { + // now decrypt the u8 + if (e) { return window.alert('error'); } + var cryptKey = secret.keys && secret.keys.fileKeyStr; + var key = Nacl.util.decodeBase64(cryptKey); + + FileCrypto.decrypt(u8, key, function (e, data) { + console.log(data); + var title = document.title = data.metadata.filename; + myFile = data.content; + myDataType = data.metadata.type; + updateTitle(title || defaultName); + + Cryptpad.removeLoadingScreen(); + }); + }); + } - var mt = MediaTag($mt[0]); + var $form = $iframe.find('#upload-form'); + $form.css({ + display: 'block', + }); - Cryptpad.removeLoadingScreen(); + var $file = $form.find("#file").on('change', function (e) { + var file = e.target.files[0]; + var reader = new FileReader(); + reader.onload = function (e) { + upload(e.target.result); + }; + reader.readAsText(file); }); + + // we're in upload mode + Cryptpad.removeLoadingScreen(); }; Cryptpad.ready(function (err, anv) { From fe93da8817901ecb495dda23b1702f6af2099dc0 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 28 Apr 2017 11:46:13 +0200 Subject: [PATCH 015/114] get ready to implement blob storage --- rpc.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/rpc.js b/rpc.js index 808815d84..5670f1c7a 100644 --- a/rpc.js +++ b/rpc.js @@ -376,6 +376,23 @@ var resetUserPins = function (store, Sessions, publicKey, channelList, cb) { }); }; +var getLimit = function (cb) { + +}; + +var createBlobStaging = function (cb) { + +}; + +var createBlobStore = function (cb) { +}; + +var upload = function (store, Sessions, publicKey, cb) { + +}; + + + /*::const ConfigType = require('./config.example.js');*/ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)=>void*/) { // load pin-store... @@ -428,7 +445,6 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) return void respond('INVALID_MESSAGE_OR_PUBLIC_KEY'); } - if (checkSignature(serialized, signature, publicKey) !== true) { return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY"); } @@ -459,7 +475,8 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) return resetUserPins(store, Sessions, safeKey, msg[1], function (e, hash) { return void Respond(e, hash); }); - case 'PIN': + case 'PIN': // TODO don't pin if over the limit + // if over, send error E_OVER_LIMIT return pinChannel(store, Sessions, safeKey, msg[1], function (e, hash) { Respond(e, hash); }); @@ -471,13 +488,17 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) return void getHash(store, Sessions, safeKey, function (e, hash) { Respond(e, hash); }); - case 'GET_TOTAL_SIZE': + case 'GET_TOTAL_SIZE': // TODO cache this, since it will get called quite a bit return getTotalSize(store, ctx.store, Sessions, safeKey, function (e, size) { if (e) { return void Respond(e); } Respond(e, size); }); case 'GET_FILE_SIZE': return void getFileSize(ctx.store, msg[1], Respond); + case 'GET_LIMIT': // TODO implement this and cache it per-user + return void getLimit(function (e, limit) { + Respond('NOT_IMPLEMENTED'); + }); case 'GET_MULTIPLE_FILE_SIZE': return void getMultipleFileSize(ctx.store, msg[1], function (e, dict) { if (e) { return void Respond(e); } From a165332c15293639959f047633c04c35ccafeb15 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 Apr 2017 12:01:47 +0200 Subject: [PATCH 016/114] Don't store a pad in the drive if the limit has been reached --- customize.dist/toolbar.css | 1 + customize.dist/translations/messages.fr.js | 4 ++- customize.dist/translations/messages.js | 4 ++- www/common/cryptpad-common.js | 30 +++++++++++++++++++--- www/common/toolbar.js | 12 +++------ www/common/userObject.js | 23 +++++++++++------ www/drive/main.js | 2 +- www/pad/main.js | 2 +- www/poll/main.js | 2 +- www/slide/main.js | 2 +- www/whiteboard/main.js | 2 +- 11 files changed, 58 insertions(+), 26 deletions(-) diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css index db8ba7776..4ef7a4567 100644 --- a/customize.dist/toolbar.css +++ b/customize.dist/toolbar.css @@ -162,6 +162,7 @@ font-size: 20px; } .cryptpad-toolbar .cryptpad-limit span { + cursor: pointer; margin: auto; } .cryptpad-toolbar .cryptpad-lag { diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index a826659ca..50a2bf667 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -56,8 +56,10 @@ define(function () { out.redLight = "Vous êtes déconnectés de la session"; out.pinLimitReached = "Vous avez atteint votre limite de stockage"; - out.pinLimitReachedAlert = "Vous avez atteint votre limite de stockage. Ce pad ne sera pas enregistré dans votre CrypDrive.
    " + + out.pinLimitReachedAlert = "Vous avez atteint votre limite de stockage. Les nouveaux pads ne seront pas enregistrés dans votre CrypDrive.
    " + "Pour résoudre ce problème, vous pouvez soit supprimer des pads de votre CryptDrive (y compris la corbeille), soit vous abonner à une offre premium pour augmenter la limite maximale."; + out.pinLimitNotPinned = "Vous avez atteint votre limite de stockage.
    "+ + "Ce pad n'est pas enregistré dans votre CryptDrive."; out.importButtonTitle = 'Importer un pad depuis un fichier local'; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 5e37ffdc6..7c5fd8df5 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -58,8 +58,10 @@ define(function () { out.redLight = "You are disconnected from the session"; out.pinLimitReached = "You've reached your storage limit"; - out.pinLimitReachedAlert = "You've reached your storage limit. This pad won't be stored in your CryptDrive.
    " + + out.pinLimitReachedAlert = "You've reached your storage limit. New pads won't be stored in your CryptDrive.
    " + "To fix this problem, you can either remove pads from your CryptDrive (including the trash) or subscribe to a premium offer to increase your limit."; + out.pinLimitNotPinned = "You've reached your storage limit.
    "+ + "This pad is not stored in your CryptDrive."; out.importButtonTitle = 'Import a pad from a local file'; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 66030927f..0fb7d7ad8 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -570,8 +570,16 @@ define([ if (!contains) { var data = makePad(href, name); - getStore().pushData(data); - getStore().addPad(data, common.initialPath); + getStore().pushData(data, function (e, state) { + if (e) { + if (e === 'E_OVER_LIMIT') { + Cryptpad.alert(Messages.pinLimitNotPinned, null, true); + return; + } + else { throw new Error("Cannot push this pad to CryptDrive", e); } + } + getStore().addPad(data, common.initialPath); + }); } if (updateWeaker.length > 0) { updateWeaker.forEach(function (obj) { @@ -709,7 +717,23 @@ define([ }; var getPinLimit = common.getPinLimit = function (cb) { - cb(void 0, 10); + cb(void 0, 1000); + }; + + var isOverPinLimit = common.isOverPinLimit = function (cb) { + var andThen = function (e, limit) { + if (e) { return void cb(e); } + if (usage > limit) { + return void cb (null, true); + } + return void cb (null, false); + }; + var todo = function (e, used) { + usage = common.bytesToMegabytes(used); + if (e) { return void cb(e); } + common.getPinLimit(andThen); + }; + common.getPinnedUsage(todo); }; var createButton = common.createButton = function (type, rightside, data, callback) { diff --git a/www/common/toolbar.js b/www/common/toolbar.js index ed7b88459..23de3262d 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -497,19 +497,15 @@ define([ 'class': LIMIT_ELEM_CLS, 'title': Messages.pinLimitReached }).append($limitIcon).hide().appendTo($userContainer); - var andThen = function (e, limit) { - if (usage > limit) { + var todo = function (e, overLimit) { + if (e) { return void console.error("Unable tog et the pinned usage"); } + if (overLimit) { $limit.show().click(function () { Cryptpad.alert(Messages.pinLimitReachedAlert, null, true); }); } }; - var todo = function (e, used) { - usage = Cryptpad.bytesToMegabytes(used); - if (e) { console.error("Unable tog et the pinned usage"); return; } - Cryptpad.getPinLimit(andThen); - }; - Cryptpad.getPinnedUsage(todo); + Cryptpad.isOverPinLimit(todo); } if (config.displayed.indexOf('newpad') !== -1) { diff --git a/www/common/userObject.js b/www/common/userObject.js index 9ea1af726..6757e7cd7 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -1,6 +1,7 @@ define([ 'jquery', -], function ($) { + '/customize/application_config.js' +], function ($, AppConfig) { var module = {}; var ROOT = module.ROOT = "root"; @@ -427,19 +428,25 @@ define([ }; // FILES DATA - var pushFileData = exp.pushData = function (data) { + var pushFileData = exp.pushData = function (data, cb) { + if (typeof cb !== "function") { cb = function () {}; } + var todo = function () { + files[FILES_DATA].push(data); + cb(); + }; + if (!Cryptpad.isLoggedIn() || !AppConfig.enablePinning) { todo(); } Cryptpad.pinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e, hash) { - if (e) { console.log(e); return; } - console.log(hash); + if (e) { return void cb(e); } + cb('E_OVER_LIMIT'); return; //TODO + todo(); }); - files[FILES_DATA].push(data); }; var spliceFileData = exp.removeData = function (idx) { var data = files[FILES_DATA][idx]; - if (typeof data === "object") { + if (typeof data === "object" && Cryptpad.isLoggedIn() && AppConfig.enablePinning) { Cryptpad.unpinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e, hash) { - if (e) { console.log(e); return; } - console.log(hash); + if (e) { return void logError(e); } + debug('UNPIN', hash); }); } files[FILES_DATA].splice(idx, 1); diff --git a/www/drive/main.js b/www/drive/main.js index 057a0a8ee..e05772676 100644 --- a/www/drive/main.js +++ b/www/drive/main.js @@ -2392,7 +2392,7 @@ define([ var userList = APP.userList = info.userList; var config = { - displayed: ['useradmin', 'spinner', 'lag', 'state'], + displayed: ['useradmin', 'spinner', 'lag', 'state', 'limit'], readOnly: readOnly, ifrw: window, common: Cryptpad, diff --git a/www/pad/main.js b/www/pad/main.js index c2bbdc5d2..e1b552a48 100644 --- a/www/pad/main.js +++ b/www/pad/main.js @@ -578,7 +578,7 @@ define([ userList = info.userList; var configTb = { - displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'], + displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'], userData: userData, readOnly: readOnly, ifrw: ifrw, diff --git a/www/poll/main.js b/www/poll/main.js index 5aa3e4ee2..1d6a151e5 100644 --- a/www/poll/main.js +++ b/www/poll/main.js @@ -733,7 +733,7 @@ define([ userList = APP.userList = info.userList; var config = { - displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'], + displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'], userData: userData, readOnly: readOnly, share: { diff --git a/www/slide/main.js b/www/slide/main.js index bef2a4e52..eb9919abc 100644 --- a/www/slide/main.js +++ b/www/slide/main.js @@ -513,7 +513,7 @@ define([ userList = info.userList; var configTb = { - displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'], + displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'], userData: userData, readOnly: readOnly, ifrw: ifrw, diff --git a/www/whiteboard/main.js b/www/whiteboard/main.js index 6cadfc487..b35ae705d 100644 --- a/www/whiteboard/main.js +++ b/www/whiteboard/main.js @@ -334,7 +334,7 @@ window.canvas = canvas; var onInit = config.onInit = function (info) { userList = info.userList; var config = { - displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad'], + displayed: ['useradmin', 'spinner', 'lag', 'state', 'share', 'userlist', 'newpad', 'limit'], userData: userData, readOnly: readOnly, share: { From 0ef1c14d7fd0c4982c6604f675989a992ee3b0c9 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 Apr 2017 12:12:17 +0200 Subject: [PATCH 017/114] Fix lint errors --- www/common/cryptpad-common.js | 3 ++- www/common/userObject.js | 1 - www/drive/main.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 3462affdb..20f68f3be 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -574,7 +574,7 @@ define([ getStore().pushData(data, function (e, state) { if (e) { if (e === 'E_OVER_LIMIT') { - Cryptpad.alert(Messages.pinLimitNotPinned, null, true); + common.alert(Messages.pinLimitNotPinned, null, true); return; } else { throw new Error("Cannot push this pad to CryptDrive", e); } @@ -722,6 +722,7 @@ define([ }; var isOverPinLimit = common.isOverPinLimit = function (cb) { + var usage; var andThen = function (e, limit) { if (e) { return void cb(e); } if (usage > limit) { diff --git a/www/common/userObject.js b/www/common/userObject.js index 6757e7cd7..66d507a27 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -437,7 +437,6 @@ define([ if (!Cryptpad.isLoggedIn() || !AppConfig.enablePinning) { todo(); } Cryptpad.pinPads([Cryptpad.hrefToHexChannelId(data.href)], function (e, hash) { if (e) { return void cb(e); } - cb('E_OVER_LIMIT'); return; //TODO todo(); }); }; diff --git a/www/drive/main.js b/www/drive/main.js index e05772676..37a699a21 100644 --- a/www/drive/main.js +++ b/www/drive/main.js @@ -290,7 +290,7 @@ define([ sel.down = true; if (!e.ctrlKey) { removeSelected(); } var rect = e.currentTarget.getBoundingClientRect(); - sel.startX = e.clientX - rect.left, + sel.startX = e.clientX - rect.left; sel.startY = e.clientY - rect.top + $content.scrollTop(); sel.$selectBox.show().css({ left: sel.startX + 'px', From c820b3485c2c3c4cedbbbcca35d424fcd044ccf3 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 Apr 2017 12:16:05 +0200 Subject: [PATCH 018/114] Fix function undefined when loading a template --- www/common/userObject.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/common/userObject.js b/www/common/userObject.js index 66d507a27..0acb44c68 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -727,7 +727,7 @@ define([ parentEl[newName] = element; parentEl[oldName] = undefined; delete parentEl[oldName]; - cb(); + if (typeof cb === "function") { cb(); } }; // REPLACE From ab1dc48b6dede9fb881dbc67448798299eb5e6e7 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 Apr 2017 12:32:49 +0200 Subject: [PATCH 019/114] Fix the 'limit reached' icon in the pad app --- customize.dist/src/less/toolbar.less | 4 ++-- customize.dist/toolbar.css | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/customize.dist/src/less/toolbar.less b/customize.dist/src/less/toolbar.less index f67e2ee96..2248c8e63 100644 --- a/customize.dist/src/less/toolbar.less +++ b/customize.dist/src/less/toolbar.less @@ -80,7 +80,6 @@ } .cryptpad-limit { - color: red; box-sizing: content-box; height: 16px; width: 16px; @@ -88,10 +87,11 @@ padding: 3px; margin: 3px; margin-right: 6px; - font-size: 20px; span { + color: red; cursor: pointer; margin: auto; + font-size: 20px; } } diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css index 4ef7a4567..58fa58033 100644 --- a/customize.dist/toolbar.css +++ b/customize.dist/toolbar.css @@ -151,7 +151,6 @@ display: none; } .cryptpad-toolbar .cryptpad-limit { - color: red; box-sizing: content-box; height: 16px; width: 16px; @@ -159,11 +158,12 @@ padding: 3px; margin: 3px; margin-right: 6px; - font-size: 20px; } .cryptpad-toolbar .cryptpad-limit span { + color: red; cursor: pointer; margin: auto; + font-size: 20px; } .cryptpad-toolbar .cryptpad-lag { box-sizing: content-box; From 0ee228666b3e3d37b6bd2fea4af2ad289a51a81f Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 Apr 2017 12:40:39 +0200 Subject: [PATCH 020/114] Replace the IRC link by the Matrix one --- customize.dist/about.html | 2 +- customize.dist/contact.html | 2 +- customize.dist/index.html | 2 +- customize.dist/privacy.html | 2 +- customize.dist/src/fragments/footer.html | 2 +- customize.dist/terms.html | 2 +- www/settings/index.html | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/customize.dist/about.html b/customize.dist/about.html index c751125a7..2b13aca95 100644 --- a/customize.dist/about.html +++ b/customize.dist/about.html @@ -106,7 +106,7 @@
    • -
    • IRC
    • +
    • Chat
    • Twitter
    • GitHub
    • Email
    • diff --git a/customize.dist/contact.html b/customize.dist/contact.html index 619f98df4..eff9c1869 100644 --- a/customize.dist/contact.html +++ b/customize.dist/contact.html @@ -103,7 +103,7 @@
      • -
      • IRC
      • +
      • Chat
      • Twitter
      • GitHub
      • Email
      • diff --git a/customize.dist/index.html b/customize.dist/index.html index 7aa29d8e9..f6ab10fa6 100644 --- a/customize.dist/index.html +++ b/customize.dist/index.html @@ -225,7 +225,7 @@
        • -
        • IRC
        • +
        • Chat
        • Twitter
        • GitHub
        • Email
        • diff --git a/customize.dist/privacy.html b/customize.dist/privacy.html index 8d2e09f5f..a7751db84 100644 --- a/customize.dist/privacy.html +++ b/customize.dist/privacy.html @@ -124,7 +124,7 @@
          • -
          • IRC
          • +
          • Chat
          • Twitter
          • GitHub
          • Email
          • diff --git a/customize.dist/src/fragments/footer.html b/customize.dist/src/fragments/footer.html index ec80ed27d..b15a2b5da 100644 --- a/customize.dist/src/fragments/footer.html +++ b/customize.dist/src/fragments/footer.html @@ -31,7 +31,7 @@
            • -
            • IRC
            • +
            • Chat
            • Twitter
            • GitHub
            • Email
            • diff --git a/customize.dist/terms.html b/customize.dist/terms.html index 73a13cea7..edfd77f86 100644 --- a/customize.dist/terms.html +++ b/customize.dist/terms.html @@ -107,7 +107,7 @@
              • -
              • IRC
              • +
              • Chat
              • Twitter
              • GitHub
              • Email
              • diff --git a/www/settings/index.html b/www/settings/index.html index a84a60963..c24748490 100644 --- a/www/settings/index.html +++ b/www/settings/index.html @@ -97,7 +97,7 @@
                • -
                • IRC
                • +
                • Chat
                • Twitter
                • GitHub
                • Email
                • From 69368def48a4981195317067dfdca3a50b6dfa21 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 28 Apr 2017 12:48:36 +0200 Subject: [PATCH 021/114] make pin, blob, and blobstage path configurable --- config.example.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/config.example.js b/config.example.js index 692a91c59..859c4b872 100644 --- a/config.example.js +++ b/config.example.js @@ -141,6 +141,23 @@ module.exports = { */ filePath: './datastore/', + /* CryptPad allows logged in users to request that particular documents be + * stored by the server indefinitely. This is called 'pinning'. + * Pin requests are stored in a pin-store. The location of this store is + * defined here. + */ + pinPath: './pins', + + /* CryptPad allows logged in users to upload encrypted files. Files/blobs + * are stored in a 'blob-store'. Set its location here. + */ + blobPath: './blob', + + /* CryptPad stores incomplete blobs in a 'staging' area until they are + * fully uploaded. Set its location here. + */ + blobStagingPath: './blobstage', + /* Cryptpad's file storage adaptor closes unused files after a configurale * number of milliseconds (default 30000 (30 seconds)) */ From 4d48c88807b49a88afc73222b9d47415852951c3 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 28 Apr 2017 12:49:08 +0200 Subject: [PATCH 022/114] create blob and blobstage stores if not exists --- rpc.js | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/rpc.js b/rpc.js index 5670f1c7a..4d1e0005d 100644 --- a/rpc.js +++ b/rpc.js @@ -2,6 +2,8 @@ /* Use Nacl for checking signatures of messages */ var Nacl = require("tweetnacl"); +var Fs = require("fs"); + var RPC = module.exports; var Store = require("./storage/file"); @@ -380,19 +382,17 @@ var getLimit = function (cb) { }; -var createBlobStaging = function (cb) { - -}; - -var createBlobStore = function (cb) { +var safeMkdir = function (path, cb) { + Fs.mkdir(path, function (e) { + if (!e || e.code === 'EEXIST') { return void cb(); } + cb(e); + }); }; var upload = function (store, Sessions, publicKey, cb) { }; - - /*::const ConfigType = require('./config.example.js');*/ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function)=>void*/) { // load pin-store... @@ -509,15 +509,29 @@ RPC.create = function (config /*:typeof(ConfigType)*/, cb /*:(?Error, ?Function) } }; + var keyOrDefaultString = function (key, def) { + return typeof(config[key]) === 'string'? config[key]: def; + }; + + var pinPath = keyOrDefaultString('pinPath', './pins'); + var blobPath = keyOrDefaultString('blobPath', './blob'); + var blobStagingPath = keyOrDefaultString('blobStagingPath', './blobstage'); + Store.create({ - filePath: './pins' + filePath: pinPath, }, function (s) { store = s; - cb(void 0, rpc); - // expire old sessions once per minute - setInterval(function () { - expireSessions(Sessions); - }, 60000); + safeMkdir(blobPath, function (e) { + if (e) { throw e; } + safeMkdir(blobStagingPath, function (e) { + if (e) { throw e; } + cb(void 0, rpc); + // expire old sessions once per minute + setInterval(function () { + expireSessions(Sessions); + }, 60000); + }) + }); }); }; From 16f6ab813c8df795a9468601239f636cba41ca16 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 28 Apr 2017 13:06:55 +0200 Subject: [PATCH 023/114] Make it clear that the number is the history represents a version --- customize.dist/src/less/toolbar.less | 37 ++++++++------ customize.dist/toolbar.css | 57 ++++++++++++++-------- customize.dist/translations/messages.fr.js | 1 + customize.dist/translations/messages.js | 1 + www/common/common-history.js | 28 +++++------ 5 files changed, 72 insertions(+), 52 deletions(-) diff --git a/customize.dist/src/less/toolbar.less b/customize.dist/src/less/toolbar.less index 2248c8e63..40f895c24 100644 --- a/customize.dist/src/less/toolbar.less +++ b/customize.dist/src/less/toolbar.less @@ -42,7 +42,7 @@ } button { - &#shareButton { + &#shareButton, &.btn.btn-success { // Bootstrap 4 colors color: #fff; background: @toolbar-green; @@ -58,7 +58,7 @@ margin-left: 5px; } } - &#newdoc { + &#newdoc, &.btn.btn-primary { // Bootstrap 4 colors color: #fff; background: #0275d8; @@ -77,6 +77,18 @@ &.hidden { display: none; } + + // Bootstrap 4 colors (btn-secondary) + border: 1px solid transparent; + border-radius: .25rem; + color: #292b2c; + background-color: #fff; + border-color: #ccc; + &:hover { + color: #292b2c; + background-color: #e6e6e6; + border-color: #adadad; + } } .cryptpad-limit { @@ -195,17 +207,6 @@ margin-right: 2px; } - button { - color: #000; - background-color: inherit; - background-image: linear-gradient(to bottom,#fff,#e4e4e4); - border: 1px solid #A6A6A6; - border-bottom-color: #979797; - border-radius: 3px; - &:hover { - background-image:linear-gradient(to bottom,#f2f2f2,#ccc); - } - } .cryptpad-state { line-height: 32px; /* equivalent to 26px + 2*2px margin used for buttons */ } @@ -429,13 +430,19 @@ display: none; text-align: center; .next { - float: right; + display: inline-block; + vertical-align: middle; + margin: 20px; } .previous { - float: left; + display: inline-block; + vertical-align: middle; + margin: 20px; } .goto { display: inline-block; + vertical-align: middle; + text-align: center; input { width: 50px; } } .gotoInput { diff --git a/customize.dist/toolbar.css b/customize.dist/toolbar.css index 58fa58033..76eeb5b4d 100644 --- a/customize.dist/toolbar.css +++ b/customize.dist/toolbar.css @@ -117,39 +117,59 @@ .cryptpad-toolbar a { float: right; } -.cryptpad-toolbar button#shareButton { +.cryptpad-toolbar button { + border: 1px solid transparent; + border-radius: .25rem; + color: #292b2c; + background-color: #fff; + border-color: #ccc; +} +.cryptpad-toolbar button#shareButton, +.cryptpad-toolbar button.btn.btn-success { color: #fff; background: #5cb85c; border-color: #5cb85c; } -.cryptpad-toolbar button#shareButton:hover { +.cryptpad-toolbar button#shareButton:hover, +.cryptpad-toolbar button.btn.btn-success:hover { background: #449d44; border: 1px solid #419641; } -.cryptpad-toolbar button#shareButton span { +.cryptpad-toolbar button#shareButton span, +.cryptpad-toolbar button.btn.btn-success span { color: #fff; } -.cryptpad-toolbar button#shareButton .large { +.cryptpad-toolbar button#shareButton .large, +.cryptpad-toolbar button.btn.btn-success .large { margin-left: 5px; } -.cryptpad-toolbar button#newdoc { +.cryptpad-toolbar button#newdoc, +.cryptpad-toolbar button.btn.btn-primary { color: #fff; background: #0275d8; border-color: #0275d8; } -.cryptpad-toolbar button#newdoc:hover { +.cryptpad-toolbar button#newdoc:hover, +.cryptpad-toolbar button.btn.btn-primary:hover { background: #025aa5; border: 1px solid #01549b; } -.cryptpad-toolbar button#newdoc span { +.cryptpad-toolbar button#newdoc span, +.cryptpad-toolbar button.btn.btn-primary span { color: #fff; } -.cryptpad-toolbar button#newdoc .large { +.cryptpad-toolbar button#newdoc .large, +.cryptpad-toolbar button.btn.btn-primary .large { margin-left: 5px; } .cryptpad-toolbar button.hidden { display: none; } +.cryptpad-toolbar button:hover { + color: #292b2c; + background-color: #e6e6e6; + border-color: #adadad; +} .cryptpad-toolbar .cryptpad-limit { box-sizing: content-box; height: 16px; @@ -265,17 +285,6 @@ margin-top: -3px; margin-right: 2px; } -.cryptpad-toolbar button { - color: #000; - background-color: inherit; - background-image: linear-gradient(to bottom, #fff, #e4e4e4); - border: 1px solid #A6A6A6; - border-bottom-color: #979797; - border-radius: 3px; -} -.cryptpad-toolbar button:hover { - background-image: linear-gradient(to bottom, #f2f2f2, #ccc); -} .cryptpad-toolbar .cryptpad-state { line-height: 32px; /* equivalent to 26px + 2*2px margin used for buttons */ @@ -492,13 +501,19 @@ text-align: center; } .cryptpad-toolbar-history .next { - float: right; + display: inline-block; + vertical-align: middle; + margin: 20px; } .cryptpad-toolbar-history .previous { - float: left; + display: inline-block; + vertical-align: middle; + margin: 20px; } .cryptpad-toolbar-history .goto { display: inline-block; + vertical-align: middle; + text-align: center; } .cryptpad-toolbar-history .goto input { width: 50px; diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 50a2bf667..030817836 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -133,6 +133,7 @@ define(function () { out.history_restoreTitle = "Restaurer la version du document sélectionnée"; out.history_restorePrompt = "Êtes-vous sûr de vouloir remplacer la version actuelle du document par la version affichée ?"; out.history_restoreDone = "Document restauré"; + out.history_version = "Version :"; // Polls diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 7c5fd8df5..0c4c74a95 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -135,6 +135,7 @@ define(function () { out.history_restoreTitle = "Restore the selected version of the document"; out.history_restorePrompt = "Are you sure you want to replace the current version of the document by the displayed one?"; out.history_restoreDone = "Document restored"; + out.history_version = "Version:"; // Polls diff --git a/www/common/common-history.js b/www/common/common-history.js index e006210a6..58fd54900 100644 --- a/www/common/common-history.js +++ b/www/common/common-history.js @@ -112,9 +112,9 @@ define([ var val = states[i].getContent().doc; c = i; if (typeof onUpdate === "function") { onUpdate(); } - $hist.find('.next, .previous').show(); - if (c === states.length - 1) { $hist.find('.next').hide(); } - if (c === 0) { $hist.find('.previous').hide(); } + $hist.find('.next, .previous').css('visibility', ''); + if (c === states.length - 1) { $hist.find('.next').css('visibility', 'hidden'); } + if (c === 0) { $hist.find('.previous').css('visibility', 'hidden'); } return val || ''; }; @@ -132,15 +132,16 @@ define([ $right.hide(); $cke.hide(); var $prev =$('
            - + diff --git a/customize.dist/contact.html b/customize.dist/contact.html index eff9c1869..aa81fac60 100644 --- a/customize.dist/contact.html +++ b/customize.dist/contact.html @@ -111,7 +111,7 @@
      - + diff --git a/customize.dist/index.html b/customize.dist/index.html index f6ab10fa6..d246d84d0 100644 --- a/customize.dist/index.html +++ b/customize.dist/index.html @@ -233,7 +233,7 @@
    - + diff --git a/customize.dist/privacy.html b/customize.dist/privacy.html index a7751db84..00a266236 100644 --- a/customize.dist/privacy.html +++ b/customize.dist/privacy.html @@ -132,7 +132,7 @@
- + diff --git a/customize.dist/src/fragments/footer.html b/customize.dist/src/fragments/footer.html index b15a2b5da..4cf4b101d 100644 --- a/customize.dist/src/fragments/footer.html +++ b/customize.dist/src/fragments/footer.html @@ -39,5 +39,5 @@ - + diff --git a/customize.dist/terms.html b/customize.dist/terms.html index edfd77f86..68eb51599 100644 --- a/customize.dist/terms.html +++ b/customize.dist/terms.html @@ -115,7 +115,7 @@ - + diff --git a/package.json b/package.json index ed76592b6..696091528 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "cryptpad", "description": "realtime collaborative visual editor with zero knowlege server", - "version": "1.5.0", + "version": "1.6.0", "dependencies": { "express": "~4.10.1", "ws": "^1.0.1", diff --git a/www/settings/index.html b/www/settings/index.html index c24748490..752d04468 100644 --- a/www/settings/index.html +++ b/www/settings/index.html @@ -105,7 +105,7 @@ - + From 5a5b02b82b9abe3db45ae7ded35fe4df9adaa798 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 5 May 2017 11:55:19 +0200 Subject: [PATCH 068/114] Don't store in the drive pads without a hash --- www/common/cryptpad-common.js | 10 +++++++--- www/common/userObject.js | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 104b67a9b..8096e3044 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -299,10 +299,14 @@ define([ pads.forEach(function (pad, i) { if (pad && typeof(pad) === 'object') { var hash = checkObjectData(pad); - if (!hash || !common.parseHash(hash)) { return; } + if (!hash || !common.parseHash(hash)) { + console.error("[Cryptpad.checkRecentPads] pad had unexpected value", pad); + getStore().removeData(i); + return; + } return pad; } - console.error("[Cryptpad.migrateRecentPads] pad had unexpected value"); + console.error("[Cryptpad.checkRecentPads] pad had unexpected value", pad); getStore().removeData(i); }); }; @@ -571,7 +575,7 @@ define([ return pad; }); - if (!contains) { + if (!contains && href) { var data = makePad(href, name); getStore().pushData(data, function (e) { if (e) { diff --git a/www/common/userObject.js b/www/common/userObject.js index aa61c8573..a7c21f3f3 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -883,6 +883,11 @@ define([ toClean.push(el); return; } + if (!el.href) { + debug("Rmoving an element in filesData with a missing href.", el); + toClean.push(el); + return; + } if (rootFiles.indexOf(el.href) === -1) { debug("An element in filesData was not in ROOT, TEMPLATE or TRASH.", el); var name = el.title || NEW_FILE_NAME; From a173e4c7a033ec985be76e6b11d9a04357f71737 Mon Sep 17 00:00:00 2001 From: ansuz Date: Fri, 5 May 2017 15:09:07 +0200 Subject: [PATCH 069/114] add 'log out everywhere' functionality in settings --- customize.dist/translations/messages.js | 4 +++ www/common/cryptpad-common.js | 1 + www/common/fsStore.js | 47 +++++++++++++++++++++---- www/settings/main.js | 40 +++++++++++++++++++++ 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index b0b7a9304..2eaf65345 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -333,6 +333,10 @@ define(function () { out.settings_pinningError = "Something went wrong"; out.settings_usageAmount = "Your pinned pads occupy {0}MB"; + out.settings_logoutEverywhere = "Expire remote sessions"; + out.settings_logoutEverywhereTitle = "Log out of all other sessions"; + out.settings_logoutEverywhereConfirm = "Are you sure? You will need to log in with all your devices."; + // index.html diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 104b67a9b..b3a660432 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -192,6 +192,7 @@ define([ [ userNameKey, userHashKey, + 'loginToken', ].forEach(function (k) { sessionStorage.removeItem(k); localStorage.removeItem(k); diff --git a/www/common/fsStore.js b/www/common/fsStore.js index a929e84b3..8d6aa31bf 100644 --- a/www/common/fsStore.js +++ b/www/common/fsStore.js @@ -134,6 +134,23 @@ define([ return ret; }; + var requestLogin = function () { + // log out so that you don't go into an endless loop... + Cryptpad.logout(); + + // redirect them to log in, and come back when they're done. + sessionStorage.redirectTo = window.location.href; + window.location.href = '/login/'; + }; + + var tryParsing = function (x) { + try { return JSON.parse(x); } + catch (e) { + console.error(e); + return null; + } + }; + var onReady = function (f, proxy, Cryptpad, exp) { var fo = exp.fo = FO.init(proxy.drive, { Cryptpad: Cryptpad @@ -145,6 +162,28 @@ define([ f(void 0, store); } + if (Cryptpad.isLoggedIn()) { +/* This isn't truly secure, since anyone who can read the user's object can + set their local loginToken to match that in the object. However, it exposes + a UI that will work most of the time. */ + var tokenKey = 'loginToken'; + + // every user object should have a persistent, random number + if (typeof(proxy.loginToken) !== 'number') { + proxy[tokenKey] = Math.floor(Math.random()*Number.MAX_SAFE_INTEGER); + } + + var localToken = tryParsing(localStorage.getItem(tokenKey)); + if (localToken === null) { + // if that number hasn't been set to localStorage, do so. + localStorage.setItem(tokenKey, proxy.loginToken); + } else if (localToken !== proxy[tokenKey]) { + // if it has been, and the local number doesn't match that in + // the user object, request that they reauthenticate. + return void requestLogin(); + } + } + if (typeof(proxy.allowUserFeedback) !== 'boolean') { proxy.allowUserFeedback = true; } @@ -157,13 +196,7 @@ define([ // if the user is logged in, but does not have signing keys... if (Cryptpad.isLoggedIn() && !Cryptpad.hasSigningKeys(proxy)) { - // log out so that you don't go into an endless loop... - Cryptpad.logout(); - - // redirect them to log in, and come back when they're done. - sessionStorage.redirectTo = window.location.href; - window.location.href = '/login/'; - return; + return void requestLogin(); } proxy.on('change', [Cryptpad.displayNameKey], function (o, n) { diff --git a/www/settings/main.js b/www/settings/main.js index 2a2a7cd3d..1e79371fd 100644 --- a/www/settings/main.js +++ b/www/settings/main.js @@ -252,6 +252,42 @@ define([ return $div; }; + var createLogoutEverywhere = function (obj) { + var proxy = obj.proxy; + var $div = $('
', { 'class': 'logoutEverywhere', }); + $('