From c7e08fedfbda97ad331e566c149c1235bb294a18 Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Fri, 25 May 2018 18:00:10 +0200 Subject: [PATCH 01/12] Password-protected files --- www/code/inner.js | 11 ++--- www/common/common-hash.js | 57 ++++++++++++++++++++++---- www/common/common-thumbnail.js | 10 ++--- www/common/common-ui-elements.js | 4 +- www/common/cryptpad-common.js | 1 - www/common/diffMarked.js | 12 ++++-- www/common/migrate-user-object.js | 9 +--- www/common/outer/async-store.js | 2 +- www/common/outer/upload.js | 23 +++++------ www/common/outer/userObject.js | 7 +--- www/common/sframe-common-codemirror.js | 12 ++---- www/common/sframe-common.js | 12 +++--- www/common/userObject.js | 1 - www/drive/inner.js | 2 +- www/file/inner.js | 16 +++----- www/filepicker/inner.js | 18 ++++---- www/pad/inner.js | 8 ++-- www/slide/inner.js | 8 ++-- 18 files changed, 117 insertions(+), 96 deletions(-) diff --git a/www/code/inner.js b/www/code/inner.js index 0a6b11abc..afb4cdcd0 100644 --- a/www/code/inner.js +++ b/www/code/inner.js @@ -331,14 +331,11 @@ define([ dropArea: $('.CodeMirror'), body: $('body'), onUploaded: function (ev, data) { - //var cursor = editor.getCursor(); - //var cleanName = data.name.replace(/[\[\]]/g, ''); - //var text = ''; - // PASSWORD_FILES var parsed = Hash.parsePadUrl(data.url); - var hexFileName = Util.base64ToHex(parsed.hashData.channel); - var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName; - var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + parsed.hashData.key + '"></media-tag>'; + var secret = Hash.getSecrets('file', parsed.hash, data.password); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = Hash.encodeBase64(secret.keys.cryptKey); + var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>'; editor.replaceSelection(mt); } }; diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 3aaa7719b..2b75b0e8e 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -11,6 +11,7 @@ define([ var uint8ArrayToHex = Util.uint8ArrayToHex; var hexToBase64 = Util.hexToBase64; var base64ToHex = Util.base64ToHex; + Hash.encodeBase64 = Nacl.util.encodeBase64; // This implementation must match that on the server // it's used for a checksum @@ -59,6 +60,11 @@ define([ return '/1/' + hexToBase64(secret.channel) + '/' + Crypto.b64RemoveSlashes(data.fileKeyStr) + '/'; } + if (version === 2) { + if (!data.fileKeyStr) { return; } + var pass = secret.password ? 'p/' : ''; + return '/2/' + secret.type + '/' + Crypto.b64RemoveSlashes(data.fileKeyStr) + '/' + pass; + } }; // V1 @@ -95,12 +101,22 @@ define([ }; Hash.createRandomHash = function (type, password) { - var cryptor = Crypto.createEditCryptor2(void 0, void 0, password); + var cryptor; + if (type === 'file') { + cryptor = Crypto.createFileCryptor2(void 0, password); + return getFileHashFromKeys({ + password: Boolean(password), + version: 2, + type: type, + keys: cryptor.fileKeyStr + }); + } + cryptor = Crypto.createEditCryptor2(void 0, void 0, password); return getEditHashFromKeys({ password: Boolean(password), version: 2, type: type, - keys: { editKeyStr: cryptor.editKeyStr } + keys: cryptor.editKeyStr }); }; @@ -113,6 +129,7 @@ Version 1 var parseTypeHash = Hash.parseTypeHash = function (type, hash) { if (!hash) { return; } + var options; var parsed = {}; var hashArr = fixDuplicateSlashes(hash).split('/'); if (['media', 'file', 'user', 'invite'].indexOf(type) === -1) { @@ -125,7 +142,6 @@ Version 1 parsed.version = 0; return parsed; } - var options; if (hashArr[1] && hashArr[1] === '1') { // Version 1 parsed.version = 1; parsed.mode = hashArr[2]; @@ -175,6 +191,25 @@ Version 1 parsed.key = hashArr[3].replace(/-/g, '/'); return parsed; } + if (hashArr[1] && hashArr[1] === '2') { // Version 2 + parsed.version = 2; + parsed.app = hashArr[2]; + parsed.key = hashArr[3]; + + options = hashArr.slice(4); + parsed.password = options.indexOf('p') !== -1; + parsed.present = options.indexOf('present') !== -1; + parsed.embed = options.indexOf('embed') !== -1; + + parsed.getHash = function (opts) { + var hash = hashArr.slice(0, 4).join('/') + '/'; + if (parsed.password) { hash += 'p/'; } + if (opts.embed) { hash += 'embed/'; } + if (opts.present) { hash += 'present/'; } + return hash; + }; + return parsed; + } return parsed; } if (['user'].indexOf(type) !== -1) { @@ -309,11 +344,12 @@ Version 1 } } } else if (parsed.type === "file") { - // version 2 hashes are to be used for encrypted blobs - secret.channel = parsed.channel; - secret.keys = { fileKeyStr: parsed.key }; + secret.channel = base64ToHex(parsed.channel); + secret.keys = { + fileKeyStr: parsed.key, + cryptKey: Nacl.util.decodeBase64(parsed.key) + }; } else if (parsed.type === "user") { - // version 2 hashes are to be used for encrypted blobs throw new Error("User hashes can't be opened (yet)"); } } else if (parsed.version === 2) { @@ -338,7 +374,12 @@ Version 1 } } } else if (parsed.type === "file") { - throw new Error("File hashes should be version 1"); + secret.channel = base64ToHex(secret.keys.chanId); + secret.keys = Crypto.createFileCryptor2(parsed.key, password); + secret.key = secret.keys.fileKeyStr; + if (secret.channel.length !== 48 || secret.key.length !== 24) { + throw new Error("The channel key and/or the encryption key is invalid"); + } } else if (parsed.type === "user") { throw new Error("User hashes can't be opened (yet)"); } diff --git a/www/common/common-thumbnail.js b/www/common/common-thumbnail.js index ab6d3e6d4..bc8145d46 100644 --- a/www/common/common-thumbnail.js +++ b/www/common/common-thumbnail.js @@ -250,17 +250,15 @@ define([ var k = getKey(parsed.type, channel); common.setThumbnail(k, b64, cb); }; - Thumb.displayThumbnail = function (common, href, channel, $container, cb) { + Thumb.displayThumbnail = function (common, href, channel, password, $container, cb) { cb = cb || function () {}; var parsed = Hash.parsePadUrl(href); var k = getKey(parsed.type, channel); var whenNewThumb = function () { - // PASSWORD_FILES - var secret = Hash.getSecrets('file', parsed.hash); - var hexFileName = Util.base64ToHex(secret.channel); + var secret = Hash.getSecrets('file', parsed.hash, password); + var hexFileName = channel; var src = Hash.getBlobPathFromHex(hexFileName); - var cryptKey = secret.keys && secret.keys.fileKeyStr; - var key = Nacl.util.decodeBase64(cryptKey); + var key = secret.keys && secret.keys.cryptKey; FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) { if (e) { if (e === 'XHR_ERROR') { return; } diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 3ba800b49..2169e1640 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -1169,8 +1169,8 @@ define([ // No password for avatars var secret = Hash.getSecrets('file', parsed.hash); if (secret.keys && secret.channel) { - var cryptKey = secret.keys && secret.keys.fileKeyStr; - var hexFileName = Util.base64ToHex(secret.channel); + var hexFileName = secret.channel; + var cryptKey = Hash.encodeBase64(secret.keys && secret.keys.cryptKey); var src = Hash.getBlobPathFromHex(hexFileName); Common.getFileSize(hexFileName, function (e, data) { if (e || !data) { diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 5faffc5c9..68debf694 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -578,7 +578,6 @@ define([ } var parsed = Hash.parsePadUrl(window.location.href); if (!parsed.type || !parsed.hashData) { return void cb('E_INVALID_HREF'); } - if (parsed.type === 'file' && typeof(parsed.channel) === 'string') { secret.channel = Util.base64ToHex(secret.channel); } hashes = Hash.getHashes(secret); if (secret.version === 0) { diff --git a/www/common/diffMarked.js b/www/common/diffMarked.js index 80c9c4b79..fca3023b4 100644 --- a/www/common/diffMarked.js +++ b/www/common/diffMarked.js @@ -41,11 +41,15 @@ define([ }; renderer.image = function (href, title, text) { if (href.slice(0,6) === '/file/') { - // PASSWORD_FILES + // DEPRECATED + // Mediatag using markdown syntax should not be used anymore so they don't support + // password-protected files + console.log('DEPRECATED: mediatag using markdown syntax!'); var parsed = Hash.parsePadUrl(href); - var hexFileName = Util.base64ToHex(parsed.hashData.channel); - var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName; - var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + parsed.hashData.key + '">'; + var secret = Hash.getSecrets('file', parsed.hash); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = Hash.encodeBase64(secret.keys.cryptKey); + var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>'; if (mediaMap[src]) { mt += mediaMap[src]; } diff --git a/www/common/migrate-user-object.js b/www/common/migrate-user-object.js index 1d6dd1210..fb69fb20b 100644 --- a/www/common/migrate-user-object.js +++ b/www/common/migrate-user-object.js @@ -115,13 +115,8 @@ define([ parsed = Hash.parsePadUrl(el.href); if (!el.href) { return; } if (!el.channel) { - if (parsed.hashData && parsed.hashData.type === "file") { - // PASSWORD_FILES - el.channel = Util.base64ToHex(parsed.hashData.channel); - } else { - var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password); - el.channel = secret.channel; - } + var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password); + el.channel = secret.channel; progress(6, Math.round(100*i/padsLength)); console.log('Adding missing channel in filesData ', el.channel); } diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index e02a527ae..20cea6cd0 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -92,7 +92,7 @@ define([ var profileChan = profile.edit ? Hash.hrefToHexChannelId('/profile/#' + profile.edit, null) : null; if (profileChan) { list.push(profileChan); } var avatarChan = profile.avatar ? Hash.hrefToHexChannelId(profile.avatar, null) : null; - if (avatarChan) { list.push(Util.base64ToHex(avatarChan)); } + if (avatarChan) { list.push(avatarChan); } } if (store.proxy.friends) { diff --git a/www/common/outer/upload.js b/www/common/outer/upload.js index 7f3874511..9ccc32500 100644 --- a/www/common/outer/upload.js +++ b/www/common/outer/upload.js @@ -13,7 +13,16 @@ define([ // if it exists, path contains the new pad location in the drive var path = file.path; - var key = Nacl.randomBytes(32); + // XXX + // PASSWORD_FILES + var password; + var hash = Hash.createRandomHash('file', password); + var secret = Hash.getSecrets('file', hash, password); + var key = secret.keys.cryptKey; + var id = secret.channel; + //var key = Nacl.randomBytes(32); + + // XXX provide channel id to "next" var next = FileCrypto.encrypt(u8, metadata, key); var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata); @@ -44,21 +53,11 @@ define([ } // if not box then done - common.uploadComplete(function (e, id) { + common.uploadComplete(function (e/*, id*/) { // XXX id is given, not asked if (e) { return void console.error(e); } var uri = ['', 'blob', id.slice(0,2), id].join('/'); console.log("encrypted blob is now available as %s", uri); - var b64Key = Nacl.util.encodeBase64(key); - - var secret = { - version: 1, - channel: id, - keys: { - fileKeyStr: b64Key - } - }; - var hash = Hash.getFileHashFromKeys(secret); var href = '/file/#' + hash; var title = metadata.name; diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js index b24092673..8b2270797 100644 --- a/www/common/outer/userObject.js +++ b/www/common/outer/userObject.js @@ -589,14 +589,9 @@ define([ // Fix channel if (!el.channel) { try { - if (parsed.hashData && parsed.hashData.type === "file") { - // PASSWORD_FILES - el.channel = Util.base64ToHex(parsed.hashData.channel); - } else { var secret = Hash.getSecrets(parsed.type, parsed.hash, el.password); el.channel = secret.channel; - } - console.log('Adding missing channel in filesData ', el.channel); + console.log('Adding missing channel in filesData ', el.channel); } catch (e) { console.error(e); } diff --git a/www/common/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js index 19b4c7ae0..0c1a6a7a5 100644 --- a/www/common/sframe-common-codemirror.js +++ b/www/common/sframe-common-codemirror.js @@ -329,15 +329,11 @@ define([ dropArea: $('.CodeMirror'), body: $('body'), onUploaded: function (ev, data) { - //var cursor = editor.getCursor(); - //var cleanName = data.name.replace(/[\[\]]/g, ''); - //var text = ''; - // PASSWORD_FILES var parsed = Hash.parsePadUrl(data.url); - var hexFileName = Util.base64ToHex(parsed.hashData.channel); - var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName; - var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + - parsed.hashData.key + '"></media-tag>'; + var secret = Hash.getSecrets('file', parsed.hash, data.password); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = Hash.encodeBase64(secret.keys.cryptKey); + var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>'; editor.replaceSelection(mt); } }; diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js index 4fc5a6ac6..572fe5f4a 100644 --- a/www/common/sframe-common.js +++ b/www/common/sframe-common.js @@ -113,16 +113,16 @@ define([ return '<script src="' + origin + '/common/media-tag-nacl.min.js"></script>'; }; funcs.getMediatagFromHref = function (href) { - // PASSWORD_FILES - var parsed = Hash.parsePadUrl(href); - var secret = Hash.getSecrets('file', parsed.hash); + // XXX: Should only be used with the current href var data = ctx.metadataMgr.getPrivateData(); + var parsed = Hash.parsePadUrl(href); + var secret = Hash.getSecrets('file', parsed.hash, data.password); if (secret.keys && secret.channel) { - var cryptKey = secret.keys && secret.keys.fileKeyStr; - var hexFileName = Util.base64ToHex(secret.channel); + var key = Hash.encodeBase64(secret.keys && secret.keys.cryptKey); + var hexFileName = secret.channel; var origin = data.fileHost || data.origin; var src = origin + Hash.getBlobPathFromHex(hexFileName); - return '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + cryptKey + '">' + + return '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '">' + '</media-tag>'; } return; diff --git a/www/common/userObject.js b/www/common/userObject.js index b05798946..fb39eb484 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -590,7 +590,6 @@ define([ } }, cb); } - console.log(path, newName); if (path.length <= 1) { logError('Renaming `root` is forbidden'); return; diff --git a/www/drive/inner.js b/www/drive/inner.js index b668429cd..d072992d6 100644 --- a/www/drive/inner.js +++ b/www/drive/inner.js @@ -1305,7 +1305,7 @@ define([ $span.attr('title', name); var type = Messages.type[hrefData.type] || hrefData.type; - common.displayThumbnail(data.href, data.channel, $span, function ($thumb) { + common.displayThumbnail(data.href, data.channel, data.password, $span, function ($thumb) { // Called only if the thumbnail exists // Remove the .hide() added by displayThumnail() because it hides the icon in // list mode too diff --git a/www/file/inner.js b/www/file/inner.js index 3d752d95e..d472cb95d 100644 --- a/www/file/inner.js +++ b/www/file/inner.js @@ -54,17 +54,14 @@ define([ var uploadMode = false; var secret; - var hexFileName; var metadataMgr = common.getMetadataMgr(); var priv = metadataMgr.getPrivateData(); if (!priv.filehash) { uploadMode = true; } else { - // PASSWORD_FILES - secret = Hash.getSecrets('file', priv.filehash); + secret = Hash.getSecrets('file', priv.filehash, priv.password); if (!secret.keys) { throw new Error("You need a hash"); } - hexFileName = Util.base64ToHex(secret.channel); } var Title = common.createTitle({}); @@ -87,9 +84,10 @@ define([ toolbar.$rightside.html(''); if (!uploadMode) { + var hexFileName = secret.channel; var src = Hash.getBlobPathFromHex(hexFileName); - var cryptKey = secret.keys && secret.keys.fileKeyStr; - var key = Nacl.util.decodeBase64(cryptKey); + var key = secret.keys && secret.keys.cryptKey; + var cryptKey = Nacl.util.encodeBase64(key); FileCrypto.fetchDecryptedMetadata(src, key, function (e, metadata) { if (e) { @@ -118,9 +116,7 @@ define([ }; var $mt = $dlview.find('media-tag'); - var cryptKey = secret.keys && secret.keys.fileKeyStr; - var hexFileName = Util.base64ToHex(secret.channel); - $mt.attr('src', '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName); + $mt.attr('src', src); $mt.attr('data-crypto-key', 'cryptpad:'+cryptKey); var rightsideDisplayed = false; @@ -263,7 +259,7 @@ define([ dropArea: $form, hoverArea: $label, body: $body, - keepTable: true // Don't fadeOut the tbale with the uploaded files + keepTable: true // Don't fadeOut the table with the uploaded files }; var FM = common.createFileManager(fmConfig); diff --git a/www/filepicker/inner.js b/www/filepicker/inner.js index 9dd05076a..c30ada772 100644 --- a/www/filepicker/inner.js +++ b/www/filepicker/inner.js @@ -40,14 +40,14 @@ define([ var parsed = Hash.parsePadUrl(data.url); hideFileDialog(); if (parsed.type === 'file') { - // PASSWORD_FILES - var hexFileName = Util.base64ToHex(parsed.hashData.channel); - var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName; + var secret = Hash.getSecrets('file', parsed.hash, data.password); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = Hash.encodeBase64(secret.keys.cryptKey); sframeChan.event("EV_FILE_PICKED", { type: parsed.type, src: src, name: data.name, - key: parsed.hashData.key + key: key }); return; } @@ -69,8 +69,8 @@ define([ APP.FM = common.createFileManager(fmConfig); // Create file picker - var onSelect = function (url, name) { - onFilePicked({url: url, name: name}); + var onSelect = function (url, name, password) { + onFilePicked({url: url, name: name, password: password}); }; var data = { FM: APP.FM @@ -135,11 +135,13 @@ define([ $('<span>', {'class': 'cp-filepicker-content-element-name'}).text(name) .appendTo($span); $span.click(function () { - if (typeof onSelect === "function") { onSelect(data.href, name); } + if (typeof onSelect === "function") { + onSelect(data.href, name, data.password); + } }); // Add thumbnail if it exists - common.displayThumbnail(data.href, data.channel, $span); + common.displayThumbnail(data.href, data.channel, data.password, $span); }); $input.focus(); }; diff --git a/www/pad/inner.js b/www/pad/inner.js index 5b150c419..b8199bc61 100644 --- a/www/pad/inner.js +++ b/www/pad/inner.js @@ -552,11 +552,11 @@ define([ ckeditor: editor, body: $('body'), onUploaded: function (ev, data) { - // PASSWORD_FILES var parsed = Hash.parsePadUrl(data.url); - var hexFileName = Util.base64ToHex(parsed.hashData.channel); - var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName; - var mt = '<media-tag contenteditable="false" src="' + src + '" data-crypto-key="cryptpad:' + parsed.hashData.key + '" tabindex="1"></media-tag>'; + var secret = Hash.getSecrets('file', parsed.hash, data.password); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = Hash.encodeBase64(secret.keys.cryptKey); + var mt = '<media-tag contenteditable="false" src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>'; // MEDIATAG var element = window.CKEDITOR.dom.element.createFromHtml(mt); editor.insertElement(element); diff --git a/www/slide/inner.js b/www/slide/inner.js index 18b7da4e3..497b0b8ca 100644 --- a/www/slide/inner.js +++ b/www/slide/inner.js @@ -500,11 +500,11 @@ define([ dropArea: $('.CodeMirror'), body: $('body'), onUploaded: function (ev, data) { - // PASSWORD_FILES var parsed = Hash.parsePadUrl(data.url); - var hexFileName = Util.base64ToHex(parsed.hashData.channel); - var src = '/blob/' + hexFileName.slice(0,2) + '/' + hexFileName; - var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + parsed.hashData.key + '"></media-tag>'; + var secret = Hash.getSecrets('file', parsed.hash, data.password); + var src = Hash.getBlobPathFromHex(secret.channel); + var key = Hash.encodeBase64(secret.keys.cryptKey); + var mt = '<media-tag src="' + src + '" data-crypto-key="cryptpad:' + key + '"></media-tag>'; editor.replaceSelection(mt); } }; From 586193d6a157ec5548915c2a98b1bb10ad002b15 Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Mon, 28 May 2018 15:30:39 +0200 Subject: [PATCH 02/12] Password-protected files: upload files with hashes V2 --- rpc.js | 65 ++++++++++++++++----------------- www/common/common-hash.js | 23 ++---------- www/common/cryptpad-common.js | 8 ++-- www/common/outer/async-store.js | 6 +-- www/common/outer/upload.js | 15 +++----- www/common/pinpad.js | 8 ++-- 6 files changed, 52 insertions(+), 73 deletions(-) diff --git a/rpc.js b/rpc.js index 88228fdac..486c01e42 100644 --- a/rpc.js +++ b/rpc.js @@ -53,9 +53,15 @@ var uint8ArrayToHex = function (a) { }).join(''); }; +var testFileId = function (id) { + if (id.length !== 48 || /[^a-f0-9]/.test(id)) { + return false; + } + return true; +}; var createFileId = function () { var id = uint8ArrayToHex(Nacl.randomBytes(24)); - if (id.length !== 48 || /[^a-f0-9]/.test(id)) { + if (!testFileId(id)) { throw new Error('file ids must consist of 48 hex characters'); } return id; @@ -876,7 +882,7 @@ var upload = function (Env, publicKey, content, cb) { var session = getSession(Env.Sessions, publicKey); if (typeof(session.currentUploadSize) !== 'number' || - typeof(session.currentUploadSize) !== 'number') { + typeof(session.pendingUploadSize) !== 'number') { // improperly initialized... maybe they didn't check before uploading? // reject it, just in case return cb('NOT_READY'); @@ -902,12 +908,12 @@ var upload = function (Env, publicKey, content, cb) { } }; -var upload_cancel = function (Env, publicKey, cb) { +var upload_cancel = function (Env, publicKey, fileSize, cb) { var paths = Env.paths; var session = getSession(Env.Sessions, publicKey); - delete session.currentUploadSize; - delete session.pendingUploadSize; + session.pendingUploadSize = fileSize; + session.currentUploadSize = 0; if (session.blobstage) { session.blobstage.close(); } var path = makeFilePath(paths.staging, publicKey); @@ -933,13 +939,14 @@ var isFile = function (filePath, cb) { }); }; -var upload_complete = function (Env, publicKey, cb) { +var upload_complete = function (Env, publicKey, id, cb) { var paths = Env.paths; var session = getSession(Env.Sessions, publicKey); - if (session.blobstage && session.blobstage.close) { - session.blobstage.close(); - delete session.blobstage; + if (!testFileId(id)) { + console.log(id); + WARN('uploadComplete', "id is invalid"); + return void cb('RENAME_ERR'); } var oldPath = makeFilePath(paths.staging, publicKey); @@ -948,8 +955,7 @@ var upload_complete = function (Env, publicKey, cb) { return void cb('RENAME_ERR'); } - var tryRandomLocation = function (cb) { - var id = createFileId(); + var tryLocation = function (cb) { var prefix = id.slice(0, 2); var newPath = makeFilePath(paths.blob, id); if (typeof(newPath) !== 'string') { @@ -968,7 +974,8 @@ var upload_complete = function (Env, publicKey, cb) { return void cb(e); } if (yes) { - return void tryRandomLocation(cb); + WARN('isFile', 'FILE EXISTS!'); + return void cb('RENAME_ERR'); } cb(void 0, newPath, id); @@ -976,38 +983,27 @@ var upload_complete = function (Env, publicKey, cb) { }); }; - var retries = 3; - var handleMove = function (e, newPath, id) { if (e || !oldPath || !newPath) { - if (retries--) { - setTimeout(function () { - return tryRandomLocation(handleMove); - }, 750); - } else { - cb(e); - } - return; + return void cb(e || 'PATH_ERR'); + } + + if (session.blobstage && session.blobstage.close) { + session.blobstage.close(); + delete session.blobstage; } // lol wut handle ur errors Fs.rename(oldPath, newPath, function (e) { if (e) { WARN('rename', e); - - if (retries--) { - return void setTimeout(function () { - tryRandomLocation(handleMove); - }, 750); - } - return void cb('RENAME_ERR'); } cb(void 0, id); }); }; - tryRandomLocation(handleMove); + tryLocation(handleMove); }; var owned_upload_complete = function (Env, safeKey, cb) { @@ -1504,7 +1500,7 @@ RPC.create = function ( }); case 'UPLOAD_COMPLETE': if (!privileged) { return deny(); } - return void upload_complete(Env, safeKey, function (e, hash) { + return void upload_complete(Env, safeKey, msg[1], function (e, hash) { WARN(e, hash); Respond(e, hash); }); @@ -1516,8 +1512,11 @@ RPC.create = function ( }); case 'UPLOAD_CANCEL': if (!privileged) { return deny(); } - return void upload_cancel(Env, safeKey, function (e) { - WARN(e); + // msg[1] is fileSize + // if we pass it here, we can start an upload right away without calling + // UPLOAD_STATUS again + return void upload_cancel(Env, safeKey, msg[1], function (e) { + WARN(e, 'UPLOAD_CANCEL'); Respond(e); }); default: diff --git a/www/common/common-hash.js b/www/common/common-hash.js index 2b75b0e8e..4b0c2c607 100644 --- a/www/common/common-hash.js +++ b/www/common/common-hash.js @@ -67,23 +67,6 @@ define([ } }; - // V1 - /*var getEditHashFromKeys = Hash.getEditHashFromKeys = function (chanKey, keys) { - if (typeof keys === 'string') { - return chanKey + keys; - } - if (!keys.editKeyStr) { return; } - 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)+'/'; - }; - var getFileHashFromKeys = Hash.getFileHashFromKeys = function (fileKey, cryptKey) { - return '/1/' + hexToBase64(fileKey) + '/' + Crypto.b64RemoveSlashes(cryptKey) + '/'; - };*/ Hash.getUserHrefFromKeys = function (origin, username, pubkey) { return origin + '/user/#/1/' + username + '/' + pubkey.replace(/\//g, '-'); }; @@ -108,7 +91,7 @@ define([ password: Boolean(password), version: 2, type: type, - keys: cryptor.fileKeyStr + keys: cryptor }); } cryptor = Crypto.createEditCryptor2(void 0, void 0, password); @@ -116,7 +99,7 @@ define([ password: Boolean(password), version: 2, type: type, - keys: cryptor.editKeyStr + keys: cryptor }); }; @@ -374,8 +357,8 @@ Version 1 } } } else if (parsed.type === "file") { - secret.channel = base64ToHex(secret.keys.chanId); secret.keys = Crypto.createFileCryptor2(parsed.key, password); + secret.channel = base64ToHex(secret.keys.chanId); secret.key = secret.keys.fileKeyStr; if (secret.channel.length !== 48 || secret.key.length !== 24) { throw new Error("The channel key and/or the encryption key is invalid"); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 68debf694..0d934d20b 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -204,8 +204,8 @@ define([ }); }; - common.uploadComplete = function (cb) { - postMessage("UPLOAD_COMPLETE", null, function (obj) { + common.uploadComplete = function (id, cb) { + postMessage("UPLOAD_COMPLETE", id, function (obj) { if (obj && obj.error) { return void cb(obj.error); } cb(null, obj); }); @@ -218,8 +218,8 @@ define([ }); }; - common.uploadCancel = function (cb) { - postMessage("UPLOAD_CANCEL", null, function (obj) { + common.uploadCancel = function (size, cb) { + postMessage("UPLOAD_CANCEL", {size: size}, function (obj) { if (obj && obj.error) { return void cb(obj.error); } cb(null, obj); }); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 20cea6cd0..f64f9efbe 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -230,9 +230,9 @@ define([ }); }; - Store.uploadComplete = function (data, cb) { + Store.uploadComplete = function (id, cb) { if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - store.rpc.uploadComplete(function (err, res) { + store.rpc.uploadComplete(id, function (err, res) { if (err) { return void cb({error:err}); } cb(res); }); @@ -248,7 +248,7 @@ define([ Store.uploadCancel = function (data, cb) { if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - store.rpc.uploadCancel(function (err, res) { + store.rpc.uploadCancel(data.size, function (err, res) { if (err) { return void cb({error:err}); } cb(res); }); diff --git a/www/common/outer/upload.js b/www/common/outer/upload.js index 9ccc32500..f911aba28 100644 --- a/www/common/outer/upload.js +++ b/www/common/outer/upload.js @@ -13,16 +13,14 @@ define([ // if it exists, path contains the new pad location in the drive var path = file.path; - // XXX - // PASSWORD_FILES - var password; + var password = file.password; var hash = Hash.createRandomHash('file', password); var secret = Hash.getSecrets('file', hash, password); var key = secret.keys.cryptKey; var id = secret.channel; - //var key = Nacl.randomBytes(32); - // XXX provide channel id to "next" + // XXX check id here (getFileSize) + var next = FileCrypto.encrypt(u8, metadata, key); var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata); @@ -53,7 +51,7 @@ define([ } // if not box then done - common.uploadComplete(function (e/*, id*/) { // XXX id is given, not asked + common.uploadComplete(id, function (e) { if (e) { return void console.error(e); } var uri = ['', 'blob', id.slice(0,2), id].join('/'); console.log("encrypted blob is now available as %s", uri); @@ -64,11 +62,11 @@ define([ if (noStore) { return void onComplete(href); } - // PASSWORD_FILES var data = { title: title || "", href: href, path: path, + password: password, channel: id }; common.setPadTitle(data, function (err) { @@ -89,11 +87,10 @@ define([ if (pending) { return void onPending(function () { // if the user wants to cancel the pending upload to execute that one - common.uploadCancel(function (e, res) { + common.uploadCancel(estimate, function (e) { if (e) { return void console.error(e); } - console.log(res); next(again); }); }); diff --git a/www/common/pinpad.js b/www/common/pinpad.js index b200f5493..1f31a884a 100644 --- a/www/common/pinpad.js +++ b/www/common/pinpad.js @@ -176,8 +176,8 @@ define([ }); }; - exp.uploadComplete = function (cb) { - rpc.send('UPLOAD_COMPLETE', null, function (e, res) { + exp.uploadComplete = function (id, cb) { + rpc.send('UPLOAD_COMPLETE', id, function (e, res) { if (e) { return void cb(e); } var id = res[0]; if (typeof(id) !== 'string') { @@ -203,8 +203,8 @@ define([ }); }; - exp.uploadCancel = function (cb) { - rpc.send('UPLOAD_CANCEL', void 0, function (e) { + exp.uploadCancel = function (size, cb) { + rpc.send('UPLOAD_CANCEL', size, function (e) { if (e) { return void cb(e); } cb(); }); From 8aac7bad454b18aa71dc02c367b62b0d924c2107 Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Mon, 28 May 2018 16:57:20 +0200 Subject: [PATCH 03/12] Make sure the ID is not used when uploading a file --- www/common/outer/upload.js | 160 +++++++++++++++++++++---------------- 1 file changed, 89 insertions(+), 71 deletions(-) diff --git a/www/common/outer/upload.js b/www/common/outer/upload.js index f911aba28..6ca3478fb 100644 --- a/www/common/outer/upload.js +++ b/www/common/outer/upload.js @@ -1,8 +1,9 @@ define([ '/file/file-crypto.js', '/common/common-hash.js', + '/bower_components/nthen/index.js', '/bower_components/tweetnacl/nacl-fast.min.js', -], function (FileCrypto, Hash) { +], function (FileCrypto, Hash, nThen) { var Nacl = window.nacl; var module = {}; @@ -14,89 +15,106 @@ define([ var path = file.path; var password = file.password; - var hash = Hash.createRandomHash('file', password); - var secret = Hash.getSecrets('file', hash, password); - var key = secret.keys.cryptKey; - var id = secret.channel; + var hash, secret, key, id, href; - // XXX check id here (getFileSize) + var getNewHash = function () { + hash = Hash.createRandomHash('file', password); + secret = Hash.getSecrets('file', hash, password); + key = secret.keys.cryptKey; + id = secret.channel; + href = '/file/#' + hash; + }; - var next = FileCrypto.encrypt(u8, metadata, key); - - var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata); - - var sendChunk = function (box, cb) { - var enc = Nacl.util.encodeBase64(box); - common.uploadChunk(enc, function (e, msg) { - cb(e, msg); + var getValidHash = function (cb) { + getNewHash(); + common.getFileSize(href, password, function (err, size) { + if (err || typeof(size) !== "number") { throw new Error(err || "Invalid size!"); } + if (size === 0) { return void cb(); } + getValidHash(); }); }; - var actual = 0; - var again = function (err, box) { - if (err) { throw new Error(err); } - if (box) { - actual += box.length; - var progressValue = (actual / estimate * 100); - updateProgress(progressValue); + nThen(function (waitFor) { + // Generate a hash and check if the resulting id is valid (not already used) + getValidHash(waitFor()); + }).nThen(function () { + var next = FileCrypto.encrypt(u8, metadata, key); - return void sendChunk(box, function (e) { - if (e) { return console.error(e); } - next(again); + var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata); + + var sendChunk = function (box, cb) { + var enc = Nacl.util.encodeBase64(box); + common.uploadChunk(enc, function (e, msg) { + cb(e, msg); }); - } + }; - if (actual !== estimate) { - console.error('Estimated size does not match actual size'); - } + var actual = 0; + var again = function (err, box) { + if (err) { throw new Error(err); } + if (box) { + actual += box.length; + var progressValue = (actual / estimate * 100); + updateProgress(progressValue); - // if not box then done - common.uploadComplete(id, function (e) { - if (e) { return void console.error(e); } - var uri = ['', 'blob', id.slice(0,2), id].join('/'); - console.log("encrypted blob is now available as %s", uri); - - var href = '/file/#' + hash; - - var title = metadata.name; - - if (noStore) { return void onComplete(href); } - - var data = { - title: title || "", - href: href, - path: path, - password: password, - channel: id - }; - common.setPadTitle(data, function (err) { - if (err) { return void console.error(err); } - onComplete(href); - common.setPadAttribute('fileType', metadata.type, null, href); - }); - }); - }; - - common.uploadStatus(estimate, function (e, pending) { - if (e) { - console.error(e); - onError(e); - return; - } - - if (pending) { - return void onPending(function () { - // if the user wants to cancel the pending upload to execute that one - common.uploadCancel(estimate, function (e) { - if (e) { - return void console.error(e); - } + return void sendChunk(box, function (e) { + if (e) { return console.error(e); } next(again); }); + } + + if (actual !== estimate) { + console.error('Estimated size does not match actual size'); + } + + // if not box then done + common.uploadComplete(id, function (e) { + if (e) { return void console.error(e); } + var uri = ['', 'blob', id.slice(0,2), id].join('/'); + console.log("encrypted blob is now available as %s", uri); + + + var title = metadata.name; + + if (noStore) { return void onComplete(href); } + + var data = { + title: title || "", + href: href, + path: path, + password: password, + channel: id + }; + common.setPadTitle(data, function (err) { + if (err) { return void console.error(err); } + onComplete(href); + common.setPadAttribute('fileType', metadata.type, null, href); + }); }); - } - next(again); + }; + + common.uploadStatus(estimate, function (e, pending) { + if (e) { + console.error(e); + onError(e); + return; + } + + if (pending) { + return void onPending(function () { + // if the user wants to cancel the pending upload to execute that one + common.uploadCancel(estimate, function (e) { + if (e) { + return void console.error(e); + } + next(again); + }); + }); + } + next(again); + }); }); + }; return module; }); From 66655b4a23687e8fe4c78e4b3277d0e2f87f86a8 Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Mon, 28 May 2018 17:50:28 +0200 Subject: [PATCH 04/12] Upload a file with a password --- customize.dist/translations/messages.fr.js | 2 ++ customize.dist/translations/messages.js | 2 ++ www/common/common-interface.js | 9 +++++-- www/common/sframe-common-file.js | 31 +++++++++++++++++++--- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index fd406f7aa..970fc815e 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -600,6 +600,8 @@ define(function () { out.upload_rename = "Souhaitez-vous renommer <b>{0}</b> avant son stockage en ligne ?<br>" + "<em>L'extension du fichier ({1}) sera ajoutée automatiquement. "+ "Ce nom sera permanent et visible par les autres utilisateurs</em>."; + out.upload_password = "Souhaitez-vous protéger ce fichier avec un mot de passe ?<br>" + + "Choisir <em>Ne plus demander</em> implique qu'aucun fichier importé durant cette session n'aura de mot de passe."; out.upload_serverError = "Erreur interne: impossible d'importer le fichier pour l'instant."; out.upload_uploadPending = "Vous avez déjà un fichier en cours d'importation. Souhaitez-vous l'annuler et importer ce nouveau fichier ?"; out.upload_success = "Votre fichier ({0}) a été importé avec succès et ajouté à votre CryptDrive."; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index eb29b9395..21e1b1817 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -604,6 +604,8 @@ define(function () { out.upload_rename = "Do you want to rename <b>{0}</b> before uploading it to the server?<br>" + "<em>The file extension ({1}) will be added automatically. "+ "This name will be permanent and visible to other users.</em>"; + out.upload_password = "Do you want to protect your file with a password?<br>" + + "If you choose <em>Do not ask me again</em>, uploaded files won't have a password."; out.upload_serverError = "Server Error: unable to upload your file at this time."; out.upload_uploadPending = "You already have an upload in progress. Cancel it and upload your new file?"; out.upload_success = "Your file ({0}) has been successfully uploaded and added to your drive."; diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 0592d859a..2653c71b3 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -425,9 +425,14 @@ define([ cb = cb || function () {}; opt = opt || {}; - var input = dialog.textInput(); + var inputBlock = opt.password ? UI.passwordInput() : dialog.textInput(); + var input = opt.password ? $(inputBlock).find('input')[0] : inputBlock; input.value = typeof(def) === 'string'? def: ''; + if (opt.password) { + $(inputBlock).find('.cp-checkmark').css('margin-bottom', '15px'); + } + var message; if (typeof(msg) === 'string') { if (!force) { msg = Util.fixHTML(msg); } @@ -441,7 +446,7 @@ define([ var cancel = dialog.cancelButton(opt.cancel); var frame = dialog.frame([ message, - input, + inputBlock, dialog.nav([ cancel, ok, ]), ]); diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index 2c212701e..54ad2096c 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -212,8 +212,10 @@ define([ queue.next(); }; - // Don't show the rename prompt if we don't want to store the file in the drive (avatar) + // Don't show the rename and password prompts if we don't store the file in the drive + // e.g. avatar var showNamePrompt = !config.noStore; + var showPasswordPrompt = !config.noStore; var promptName = function (file, cb) { var extIdx = file.name.lastIndexOf('.'); @@ -225,6 +227,7 @@ define([ ]); UI.prompt(msg, name, function (newName) { if (newName === null) { + // "Don't ask me again" showNamePrompt = false; return void cb (file.name); } @@ -235,6 +238,19 @@ define([ cb(newName); }, {cancel: Messages.doNotAskAgain}, true); }; + + var promptPassword = function (file, cb) { + var msg = Messages.upload_password; + UI.prompt(msg, '', function (password) { + if (password === null) { + // "Don't ask me again" + showPasswordPrompt = false; + return void cb (); + } + cb(password); + }, {cancel: Messages.doNotAskAgain, password: true}, true); + }; + var handleFileState = { queue: [], inProgress: false @@ -246,6 +262,7 @@ define([ var thumb; var file_arraybuffer; var name = file.name; + var password; var finish = function () { var metadata = { name: name, @@ -255,6 +272,7 @@ define([ queue.push({ blob: file_arraybuffer, metadata: metadata, + password: password, dropEvent: e }); handleFileState.inProgress = false; @@ -263,11 +281,18 @@ define([ handleFile(next[0], next[1]); } }; + var getPassword = function () { + if (!showPasswordPrompt) { return void finish(); } + promptPassword(file, function (pw) { + password = pw; + finish(); + }); + }; var getName = function () { - if (!showNamePrompt) { return void finish(); } + if (!showNamePrompt) { return void getPassword(); } promptName(file, function (newName) { name = newName; - finish(); + getPassword(); }); }; From 05bd41f256b6565cd420d6a8d49d18c124d7325d Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Mon, 28 May 2018 18:03:35 +0200 Subject: [PATCH 05/12] Upload a password-protected file from apps --- www/common/sframe-common-file.js | 1 + 1 file changed, 1 insertion(+) diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index 54ad2096c..ff604b189 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -53,6 +53,7 @@ define([ data.name = file.metadata.name; data.url = href; + data.password = file.password; if (file.metadata.type.slice(0,6) === 'image/') { data.mediatag = true; } From a6bac8ae413d42bdb8d649d49581832c3063b318 Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Mon, 28 May 2018 18:18:25 +0200 Subject: [PATCH 06/12] Fix password prompt issue when the password was wrong in the file app --- www/common/sframe-common-outer.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index fcebc354f..c78bf53f9 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -150,12 +150,12 @@ define([ todo(); } else { // Ask for the password and check if the pad exists - // If the pad doesn't exist, it means the password is oncorrect + // If the pad doesn't exist, it means the password isn't correct // or the pad has been deleted var correctPassword = waitFor(); sframeChan.on('Q_PAD_PASSWORD_VALUE', function (data, cb) { password = data; - Cryptpad.isNewChannel(window.location.href, password, function (e, isNew) { + var next = function (e, isNew) { if (Boolean(isNew)) { // Ask again in the inner iframe // We should receive a new Q_PAD_PASSWORD_VALUE @@ -165,7 +165,17 @@ define([ correctPassword(); cb(true); } - }); + }; + if (parsed.type === "file") { + // `isNewChannel` doesn't work for files (not a channel) + // `getFileSize` is not adapted to channels because of metadata + Cryptpad.getFileSize(window.location.href, password, function (e, size) { + next(e, size === 0); + }); + return; + } + // Not a file, so we can use `isNewChannel` + Cryptpad.isNewChannel(window.location.href, password, next); }); sframeChan.event("EV_PAD_PASSWORD"); } From 93ecc06964d03956f0e5ec80a326700edc5c1f9d Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Tue, 29 May 2018 19:42:20 +0200 Subject: [PATCH 07/12] Upload owned files --- rpc.js | 127 ++++++++++++++++++++++++++++++-- www/common/cryptpad-common.js | 4 +- www/common/outer/async-store.js | 20 ++++- www/common/outer/upload.js | 14 +++- www/common/pinpad.js | 11 +++ www/file/inner.js | 4 + 6 files changed, 166 insertions(+), 14 deletions(-) diff --git a/rpc.js b/rpc.js index 486c01e42..a69bfc4ef 100644 --- a/rpc.js +++ b/rpc.js @@ -943,10 +943,14 @@ var upload_complete = function (Env, publicKey, id, cb) { var paths = Env.paths; var session = getSession(Env.Sessions, publicKey); + if (session.blobstage && session.blobstage.close) { + session.blobstage.close(); + delete session.blobstage; + } + if (!testFileId(id)) { - console.log(id); WARN('uploadComplete', "id is invalid"); - return void cb('RENAME_ERR'); + return void cb('EINVAL_ID'); } var oldPath = makeFilePath(paths.staging, publicKey); @@ -988,11 +992,6 @@ var upload_complete = function (Env, publicKey, id, cb) { return void cb(e || 'PATH_ERR'); } - if (session.blobstage && session.blobstage.close) { - session.blobstage.close(); - delete session.blobstage; - } - // lol wut handle ur errors Fs.rename(oldPath, newPath, function (e) { if (e) { @@ -1099,6 +1098,118 @@ var owned_upload_complete = function (Env, safeKey, cb) { }); }; +var owned_upload_complete_2 = function (Env, safeKey, id, cb) { + var session = getSession(Env.Sessions, safeKey); + + // the file has already been uploaded to the staging area + // close the pending writestream + if (session.blobstage && session.blobstage.close) { + session.blobstage.close(); + delete session.blobstage; + } + + if (!testFileId(id)) { + WARN('ownedUploadComplete', "id is invalid"); + return void cb('EINVAL_ID'); + } + + var oldPath = makeFilePath(Env.paths.staging, safeKey); + if (typeof(oldPath) !== 'string') { + return void cb('EINVAL_CONFIG'); + } + + // construct relevant paths + var root = Env.paths.blob; + + //var safeKey = escapeKeyCharacters(safeKey); + var safeKeyPrefix = safeKey.slice(0, 3); + + //var blobId = createFileId(); + var blobIdPrefix = id.slice(0, 2); + + var ownPath = Path.join(root, safeKeyPrefix, safeKey, blobIdPrefix); + var filePath = Path.join(root, blobIdPrefix); + + var tryId = function (path, cb) { + Fs.access(path, Fs.constants.R_OK | Fs.constants.W_OK, function (e) { + if (!e) { + // generate a new id (with the same prefix) and recurse + WARN('ownedUploadComplete', 'id is already used '+ id); + return void cb('EEXISTS'); + } else if (e.code === 'ENOENT') { + // no entry, so it's safe for us to proceed + return void cb(); + } else { + // it failed in an unexpected way. log it + WARN(e, 'ownedUploadComplete'); + return void cb(e.code); + } + }); + }; + + // the user wants to move it into blob and create a empty file with the same id + // in their own space: + // /blob/safeKeyPrefix/safeKey/blobPrefix/blobID + + var finalPath; + var finalOwnPath; + nThen(function (w) { + // make the requisite directory structure using Mkdirp + Mkdirp(filePath, w(function (e /*, path */) { + if (e) { // does not throw error if the directory already existed + w.abort(); + return void cb(e); + } + })); + Mkdirp(ownPath, w(function (e /*, path */) { + if (e) { // does not throw error if the directory already existed + w.abort(); + return void cb(e); + } + })); + }).nThen(function (w) { + // make sure the id does not collide with another + finalPath = Path.join(filePath, id); + finalOwnPath = Path.join(ownPath, id); + tryId(finalPath, w(function (e) { + if (e) { + w.abort(); + return void cb(e); + } + })); + }).nThen(function (w) { + // Create the empty file proving ownership + Fs.writeFile(finalOwnPath, '', w(function (e) { + if (e) { + w.abort(); + return void cb(e.code); + } + // otherwise it worked... + })); + }).nThen(function (w) { + // move the existing file to its new path + + // flow is dumb and I need to guard against this which will never happen + /*:: if (typeof(oldPath) === 'object') { throw new Error('should never happen'); } */ + Fs.rename(oldPath /* XXX */, finalPath, w(function (e) { + if (e) { + // Remove the ownership file + // XXX not needed if we have a cleanup script? + Fs.unlink(finalOwnPath, function (e) { + WARN(e, 'Removing ownership file ownedUploadComplete'); + }); + w.abort(); + return void cb(e.code); + } + // otherwise it worked... + })); + }).nThen(function () { + // clean up their session when you're done + // call back with the blob id... + cb(void 0, id); + }); +}; + var upload_status = function (Env, publicKey, filesize, cb) { var paths = Env.paths; @@ -1506,7 +1617,7 @@ RPC.create = function ( }); case 'OWNED_UPLOAD_COMPLETE': if (!privileged) { return deny(); } - return void owned_upload_complete(Env, safeKey, function (e, blobId) { + return void owned_upload_complete_2(Env, safeKey, msg[1], function (e, blobId) { WARN(e, blobId); Respond(e, blobId); }); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 0d934d20b..cb8d94bf8 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -204,8 +204,8 @@ define([ }); }; - common.uploadComplete = function (id, cb) { - postMessage("UPLOAD_COMPLETE", id, function (obj) { + common.uploadComplete = function (id, owned, cb) { + postMessage("UPLOAD_COMPLETE", {id: id, owned, owned}, function (obj) { if (obj && obj.error) { return void cb(obj.error); } cb(null, obj); }); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index f64f9efbe..680712f17 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -230,9 +230,18 @@ define([ }); }; - Store.uploadComplete = function (id, cb) { + Store.uploadComplete = function (data, cb) { if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); } - store.rpc.uploadComplete(id, function (err, res) { + if (data.owned) { + // Owned file + store.rpc.ownedUploadComplete(data.id, function (err, res) { + if (err) { return void cb({error:err}); } + cb(res); + }); + return; + } + // Normal upload + store.rpc.uploadComplete(data.id, function (err, res) { if (err) { return void cb({error:err}); } cb(res); }); @@ -678,6 +687,7 @@ define([ if (Store.channel && Store.channel.wc && channel === Store.channel.wc.id) { owners = Store.channel.data.owners || undefined; } + var expire; if (Store.channel && Store.channel.wc && channel === Store.channel.wc.id) { expire = +Store.channel.data.expire || undefined; @@ -726,7 +736,11 @@ define([ contains = true; pad.atime = +new Date(); pad.title = title; - pad.owners = owners; + if (owners || h.type !== "file") { + // OWNED_FILES + // Never remove owner for files + pad.owners = owners; + } pad.expire = expire; // If the href is different, it means we have a stronger one diff --git a/www/common/outer/upload.js b/www/common/outer/upload.js index 6ca3478fb..65f674517 100644 --- a/www/common/outer/upload.js +++ b/www/common/outer/upload.js @@ -11,6 +11,10 @@ define([ var u8 = file.blob; // This is not a blob but a uint8array var metadata = file.metadata; + var owned = file.isOwned; +// XXX +owned = true; + // if it exists, path contains the new pad location in the drive var path = file.path; @@ -34,9 +38,16 @@ define([ }); }; + var edPublic; nThen(function (waitFor) { // Generate a hash and check if the resulting id is valid (not already used) getValidHash(waitFor()); + }).nThen(function (waitFor) { + if (!owned) { return; } + common.getMetadata(waitFor(function (err, m) { + edPublic = m.priv.edPublic; + metadata.owners = [edPublic]; + })); }).nThen(function () { var next = FileCrypto.encrypt(u8, metadata, key); @@ -68,7 +79,7 @@ define([ } // if not box then done - common.uploadComplete(id, function (e) { + common.uploadComplete(id, owned, function (e) { if (e) { return void console.error(e); } var uri = ['', 'blob', id.slice(0,2), id].join('/'); console.log("encrypted blob is now available as %s", uri); @@ -89,6 +100,7 @@ define([ if (err) { return void console.error(err); } onComplete(href); common.setPadAttribute('fileType', metadata.type, null, href); + common.setPadAttribute('owners', metadata.owners, null, href); }); }); }; diff --git a/www/common/pinpad.js b/www/common/pinpad.js index 1f31a884a..6d6295989 100644 --- a/www/common/pinpad.js +++ b/www/common/pinpad.js @@ -187,6 +187,17 @@ define([ }); }; + exp.ownedUploadComplete = function (id, cb) { + rpc.send('OWNED_UPLOAD_COMPLETE', id, function (e, res) { + if (e) { return void cb(e); } + var id = res[0]; + if (typeof(id) !== 'string') { + return void cb('INVALID_ID'); + } + cb(void 0, id); + }); + }; + exp.uploadStatus = function (size, cb) { if (typeof(size) !== 'number') { return void setTimeout(function () { diff --git a/www/file/inner.js b/www/file/inner.js index d472cb95d..f9b2a462a 100644 --- a/www/file/inner.js +++ b/www/file/inner.js @@ -104,6 +104,10 @@ define([ toolbar.$rightside.append(common.createButton('hashtag', true)); } + var owners = metadata.owners; + if (owners) { + common.setPadAttribute('owners', owners); + } common.setPadAttribute('fileType', metadata.type); From 85e5c495d9dca72557d3a551d1d6967e7be6a8ac Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Wed, 30 May 2018 14:36:29 +0200 Subject: [PATCH 08/12] Remove owned file --- rpc.js | 80 +++++++++++++++++++++++++++++------ www/common/cryptpad-common.js | 2 +- www/common/pinpad.js | 2 +- www/file/inner.js | 19 ++++++--- 4 files changed, 81 insertions(+), 22 deletions(-) diff --git a/rpc.js b/rpc.js index a69bfc4ef..f2ba8e93e 100644 --- a/rpc.js +++ b/rpc.js @@ -817,6 +817,16 @@ var makeFileStream = function (root, id, cb) { }); }; +var isFile = function (filePath /*:string*/, cb) { + Fs.stat(filePath, function (e, stats) { + if (e) { + if (e.code === 'ENOENT') { return void cb(void 0, false); } + return void cb(e.message); + } + return void cb(void 0, stats.isFile()); + }); +}; + var clearOwnedChannel = function (Env, channelId, unsafeKey, cb) { if (typeof(channelId) !== 'string' || channelId.length !== 32) { return cb('INVALID_ARGUMENTS'); @@ -840,11 +850,65 @@ var clearOwnedChannel = function (Env, channelId, unsafeKey, cb) { }); }; +var removeOwnedBlob = function (Env, blobId, unsafeKey, cb) { + var safeKey = escapeKeyCharacters(unsafeKey); + var safeKeyPrefix = safeKey.slice(0,3); + var blobPrefix = blobId.slice(0,2); + + var blobPath = makeFilePath(Env.paths.blob, blobId); + var ownPath = Path.join(Env.paths.blob, safeKeyPrefix, safeKey, blobPrefix, blobId); + + nThen(function (w) { + // Check if the blob exists + isFile(blobPath, w(function (e, isFile) { + if (e) { + w.abort(); + return void cb(e); + } + if (!isFile) { + WARN('removeOwnedBlob', 'The provided blob ID is not a file!'); + w.abort(); + return void cb('EINVAL_BLOBID'); + } + })); + }).nThen(function (w) { + // Check if you're the owner + isFile(ownPath, w(function (e, isFile) { + if (e) { + w.abort(); + return void cb(e); + } + if (!isFile) { + WARN('removeOwnedBlob', 'Incorrect owner'); + w.abort(); + return void cb('INSUFFICIENT_PERMISSIONS'); + } + })); + }).nThen(function (w) { + // Delete the blob + Fs.unlink(blobPath, w(function (e) { + if (e) { + w.abort(); + return void cb(e); + } + })); + }).nThen(function () { + // Delete the proof of ownership + Fs.unlink(ownPath, function (e) { + cb(e); + }); + }); +}; + var removeOwnedChannel = function (Env, channelId, unsafeKey, cb) { - if (typeof(channelId) !== 'string' || channelId.length !== 32) { + if (typeof(channelId) !== 'string' || !isValidId(channelId)) { return cb('INVALID_ARGUMENTS'); } + if (testFileId(channelId)) { + return void removeOwnedBlob(Env, channelId, unsafeKey, cb); + } + if (!(Env.msgStore && Env.msgStore.removeChannel && Env.msgStore.getChannelMetadata)) { return cb("E_NOT_IMPLEMENTED"); } @@ -929,16 +993,6 @@ var upload_cancel = function (Env, publicKey, fileSize, cb) { }); }; -var isFile = function (filePath, cb) { - Fs.stat(filePath, function (e, stats) { - if (e) { - if (e.code === 'ENOENT') { return void cb(void 0, false); } - return void cb(e.message); - } - return void cb(void 0, stats.isFile()); - }); -}; - var upload_complete = function (Env, publicKey, id, cb) { var paths = Env.paths; var session = getSession(Env.Sessions, publicKey); @@ -1098,7 +1152,7 @@ var owned_upload_complete = function (Env, safeKey, cb) { }); }; -var owned_upload_complete_2 = function (Env, safeKey, id, cb) { +owned_upload_complete = function (Env, safeKey, id, cb) { var session = getSession(Env.Sessions, safeKey); // the file has already been uploaded to the staging area @@ -1617,7 +1671,7 @@ RPC.create = function ( }); case 'OWNED_UPLOAD_COMPLETE': if (!privileged) { return deny(); } - return void owned_upload_complete_2(Env, safeKey, msg[1], function (e, blobId) { + return void owned_upload_complete(Env, safeKey, msg[1], function (e, blobId) { WARN(e, blobId); Respond(e, blobId); }); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index cb8d94bf8..f91f050e8 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -205,7 +205,7 @@ define([ }; common.uploadComplete = function (id, owned, cb) { - postMessage("UPLOAD_COMPLETE", {id: id, owned, owned}, function (obj) { + postMessage("UPLOAD_COMPLETE", {id: id, owned: owned}, function (obj) { if (obj && obj.error) { return void cb(obj.error); } cb(null, obj); }); diff --git a/www/common/pinpad.js b/www/common/pinpad.js index 6d6295989..957f5d1d6 100644 --- a/www/common/pinpad.js +++ b/www/common/pinpad.js @@ -151,7 +151,7 @@ define([ }; exp.removeOwnedChannel = function (channel, cb) { - if (typeof(channel) !== 'string' || channel.length !== 32) { + if (typeof(channel) !== 'string' || [32,48].indexOf(channel.length) === -1) { // can't use this on files because files can't be owned... return void cb('INVALID_ARGUMENTS'); } diff --git a/www/file/inner.js b/www/file/inner.js index f9b2a462a..41517a036 100644 --- a/www/file/inner.js +++ b/www/file/inner.js @@ -96,21 +96,26 @@ define([ } return void console.error(e); } + + // Add pad attributes when the file is saved in the drive + Title.onTitleChange(function () { + var owners = metadata.owners; + if (owners) { + common.setPadAttribute('owners', owners); + } + common.setPadAttribute('fileType', metadata.type); + }); + + // Save to the drive or update the acces time var title = document.title = metadata.name; Title.updateTitle(title || Title.defaultTitle); + toolbar.addElement(['pageTitle'], {pageTitle: title}); toolbar.$rightside.append(common.createButton('forget', true)); if (common.isLoggedIn()) { toolbar.$rightside.append(common.createButton('hashtag', true)); } - var owners = metadata.owners; - if (owners) { - common.setPadAttribute('owners', owners); - } - - common.setPadAttribute('fileType', metadata.type); - var displayFile = function (ev, sizeMb, CB) { var called_back; var cb = function (e) { From 68fb534d2567554ff1a023ee5029a309f95f3320 Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Wed, 30 May 2018 15:27:36 +0200 Subject: [PATCH 09/12] Flow... --- rpc.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/rpc.js b/rpc.js index f2ba8e93e..89ed726b8 100644 --- a/rpc.js +++ b/rpc.js @@ -817,7 +817,8 @@ var makeFileStream = function (root, id, cb) { }); }; -var isFile = function (filePath /*:string*/, cb) { +var isFile = function (filePath, cb) { + /*:: if (typeof(filePath) !== 'string') { throw new Error('should never happen'); } */ Fs.stat(filePath, function (e, stats) { if (e) { if (e.code === 'ENOENT') { return void cb(void 0, false); } @@ -886,6 +887,7 @@ var removeOwnedBlob = function (Env, blobId, unsafeKey, cb) { })); }).nThen(function (w) { // Delete the blob + /*:: if (typeof(blobPath) !== 'string') { throw new Error('should never happen'); } */ Fs.unlink(blobPath, w(function (e) { if (e) { w.abort(); @@ -1059,6 +1061,7 @@ var upload_complete = function (Env, publicKey, id, cb) { tryLocation(handleMove); }; +/* var owned_upload_complete = function (Env, safeKey, cb) { var session = getSession(Env.Sessions, safeKey); @@ -1118,7 +1121,7 @@ var owned_upload_complete = function (Env, safeKey, cb) { var finalPath; nThen(function (w) { // make the requisite directory structure using Mkdirp - Mkdirp(plannedPath, w(function (e /*, path */) { + Mkdirp(plannedPath, w(function (e) { if (e) { // does not throw error if the directory already existed w.abort(); return void cb(e); @@ -1137,8 +1140,8 @@ var owned_upload_complete = function (Env, safeKey, cb) { // move the existing file to its new path // flow is dumb and I need to guard against this which will never happen - /*:: if (typeof(oldPath) === 'object') { throw new Error('should never happen'); } */ - Fs.rename(oldPath /* XXX */, finalPath, w(function (e) { + // / *:: if (typeof(oldPath) === 'object') { throw new Error('should never happen'); } * / + Fs.rename(oldPath, finalPath, w(function (e) { if (e) { w.abort(); return void cb(e.code); @@ -1151,8 +1154,9 @@ var owned_upload_complete = function (Env, safeKey, cb) { cb(void 0, blobId); }); }; +*/ -owned_upload_complete = function (Env, safeKey, id, cb) { +var owned_upload_complete = function (Env, safeKey, id, cb) { var session = getSession(Env.Sessions, safeKey); // the file has already been uploaded to the staging area From 06818b798f8353ee6950472f9dd7c15d5cfaa2b9 Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Wed, 30 May 2018 18:48:29 +0200 Subject: [PATCH 10/12] Add properties button to the file app to see owners and password --- www/file/inner.js | 1 + 1 file changed, 1 insertion(+) diff --git a/www/file/inner.js b/www/file/inner.js index 41517a036..78d162f5e 100644 --- a/www/file/inner.js +++ b/www/file/inner.js @@ -112,6 +112,7 @@ define([ toolbar.addElement(['pageTitle'], {pageTitle: title}); toolbar.$rightside.append(common.createButton('forget', true)); + toolbar.$rightside.append(common.createButton('properties', true)); if (common.isLoggedIn()) { toolbar.$rightside.append(common.createButton('hashtag', true)); } From 8ce539a89e866da7915eadafb2c8244e48e37bce Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Wed, 30 May 2018 18:49:48 +0200 Subject: [PATCH 11/12] lint compliance --- rpc.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rpc.js b/rpc.js index 89ed726b8..c21bee11f 100644 --- a/rpc.js +++ b/rpc.js @@ -36,6 +36,7 @@ var isValidId = function (chan) { [32, 48].indexOf(chan.length) > -1; }; +/* var uint8ArrayToHex = function (a) { // call slice so Uint8Arrays work as expected return Array.prototype.slice.call(a).map(function (e) { @@ -52,6 +53,7 @@ var uint8ArrayToHex = function (a) { } }).join(''); }; +*/ var testFileId = function (id) { if (id.length !== 48 || /[^a-f0-9]/.test(id)) { @@ -59,6 +61,8 @@ var testFileId = function (id) { } return true; }; + +/* var createFileId = function () { var id = uint8ArrayToHex(Nacl.randomBytes(24)); if (!testFileId(id)) { @@ -66,6 +70,7 @@ var createFileId = function () { } return id; }; +*/ var makeToken = function () { return Number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)) From ba0e000808e237f1add75035f341e7b9c05ccd60 Mon Sep 17 00:00:00 2001 From: yflory <yann.flory@xwiki.com> Date: Thu, 31 May 2018 13:26:06 +0200 Subject: [PATCH 12/12] Add a file upload modal --- .../src/less2/include/alertify.less | 4 + .../src/less2/include/password-input.less | 1 + customize.dist/translations/messages.fr.js | 8 +- customize.dist/translations/messages.js | 8 +- www/common/common-interface.js | 4 - www/common/common-ui-elements.js | 3 +- www/common/sframe-common-file.js | 129 +++++++++++------- 7 files changed, 88 insertions(+), 69 deletions(-) diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less index 24c63e570..ebc0b1383 100644 --- a/customize.dist/src/less2/include/alertify.less +++ b/customize.dist/src/less2/include/alertify.less @@ -203,6 +203,10 @@ padding: @alertify_padding-base; } + span.cp-password-container { + margin-bottom: 15px; + } + input[type="checkbox"], input[type="radio"] { width: auto; padding: 0; diff --git a/customize.dist/src/less2/include/password-input.less b/customize.dist/src/less2/include/password-input.less index 8836476fd..015f364aa 100644 --- a/customize.dist/src/less2/include/password-input.less +++ b/customize.dist/src/less2/include/password-input.less @@ -5,6 +5,7 @@ input { flex: 1; min-width: 0; + margin-bottom: 0 !important; // Override margin from alertify } label, .fa { margin-left: 10px; diff --git a/customize.dist/translations/messages.fr.js b/customize.dist/translations/messages.fr.js index 970fc815e..059ab4314 100644 --- a/customize.dist/translations/messages.fr.js +++ b/customize.dist/translations/messages.fr.js @@ -597,11 +597,9 @@ define(function () { out.settings_templateSkipHint = "Quand vous créez un nouveau pad, et si vous possédez des modèles pour ce type de pad, une fenêtre peut apparaître pour demander si vous souhaitez importer un modèle. Ici vous pouvez choisir de ne jamais montrer cette fenêtre et donc de ne jamais utiliser de modèle."; out.upload_title = "Hébergement de fichiers"; - out.upload_rename = "Souhaitez-vous renommer <b>{0}</b> avant son stockage en ligne ?<br>" + - "<em>L'extension du fichier ({1}) sera ajoutée automatiquement. "+ - "Ce nom sera permanent et visible par les autres utilisateurs</em>."; - out.upload_password = "Souhaitez-vous protéger ce fichier avec un mot de passe ?<br>" + - "Choisir <em>Ne plus demander</em> implique qu'aucun fichier importé durant cette session n'aura de mot de passe."; + out.upload_modal_title = "Options d'importation du fichier"; + out.upload_modal_filename = "Nom (extension <em>{0}</em> ajoutée automatiquement)"; + out.upload_modal_owner = "Être propriétaire du fichier"; out.upload_serverError = "Erreur interne: impossible d'importer le fichier pour l'instant."; out.upload_uploadPending = "Vous avez déjà un fichier en cours d'importation. Souhaitez-vous l'annuler et importer ce nouveau fichier ?"; out.upload_success = "Votre fichier ({0}) a été importé avec succès et ajouté à votre CryptDrive."; diff --git a/customize.dist/translations/messages.js b/customize.dist/translations/messages.js index 21e1b1817..3b955847a 100644 --- a/customize.dist/translations/messages.js +++ b/customize.dist/translations/messages.js @@ -601,11 +601,9 @@ define(function () { out.settings_templateSkipHint = "When you create a new empty pad, if you have stored templates for this type of pad, a modal appears to ask if you want to use a template. Here you can choose to never show this modal and so to never use a template."; out.upload_title = "File upload"; - out.upload_rename = "Do you want to rename <b>{0}</b> before uploading it to the server?<br>" + - "<em>The file extension ({1}) will be added automatically. "+ - "This name will be permanent and visible to other users.</em>"; - out.upload_password = "Do you want to protect your file with a password?<br>" + - "If you choose <em>Do not ask me again</em>, uploaded files won't have a password."; + out.upload_modal_title = "File upload options"; + out.upload_modal_filename = "File name (extension <em>{0}</em> added automatically)"; + out.upload_modal_owner = "Owned file"; out.upload_serverError = "Server Error: unable to upload your file at this time."; out.upload_uploadPending = "You already have an upload in progress. Cancel it and upload your new file?"; out.upload_success = "Your file ({0}) has been successfully uploaded and added to your drive."; diff --git a/www/common/common-interface.js b/www/common/common-interface.js index 2653c71b3..e65742d0f 100644 --- a/www/common/common-interface.js +++ b/www/common/common-interface.js @@ -429,10 +429,6 @@ define([ var input = opt.password ? $(inputBlock).find('input')[0] : inputBlock; input.value = typeof(def) === 'string'? def: ''; - if (opt.password) { - $(inputBlock).find('.cp-checkmark').css('margin-bottom', '15px'); - } - var message; if (typeof(msg) === 'string') { if (!force) { msg = Util.fixHTML(msg); } diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 2169e1640..c16fd7e15 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -162,7 +162,6 @@ define([ $pwInput.val(data.password).click(function () { $pwInput[0].select(); }); - $(password).find('.cp-checkmark').css('margin-bottom', '15px'); $d.append(password); } @@ -957,7 +956,7 @@ define([ }; }; - var setHTML = function (e, html) { + var setHTML = UIElements.setHTML = function (e, html) { e.innerHTML = html; return e; }; diff --git a/www/common/sframe-common-file.js b/www/common/sframe-common-file.js index ff604b189..83127d886 100644 --- a/www/common/sframe-common-file.js +++ b/www/common/sframe-common-file.js @@ -3,11 +3,13 @@ define([ '/file/file-crypto.js', '/common/common-thumbnail.js', '/common/common-interface.js', + '/common/common-ui-elements.js', '/common/common-util.js', + '/common/hyperscript.js', '/customize/messages.js', '/bower_components/tweetnacl/nacl-fast.min.js', -], function ($, FileCrypto, Thumb, UI, Util, Messages) { +], function ($, FileCrypto, Thumb, UI, UIElements, Util, h, Messages) { var Nacl = window.nacl; var module = {}; @@ -26,6 +28,7 @@ define([ module.create = function (common, config) { var File = {}; + var origin = common.getMetadataMgr().getPrivateData().origin; var queue = File.queue = { queue: [], @@ -213,43 +216,59 @@ define([ queue.next(); }; - // Don't show the rename and password prompts if we don't store the file in the drive - // e.g. avatar - var showNamePrompt = !config.noStore; - var showPasswordPrompt = !config.noStore; - - var promptName = function (file, cb) { + // Get the upload options + var fileUploadModal = function (file, cb) { var extIdx = file.name.lastIndexOf('.'); var name = extIdx !== -1 ? file.name.slice(0,extIdx) : file.name; var ext = extIdx !== -1 ? file.name.slice(extIdx) : ""; - var msg = Messages._getKey('upload_rename', [ - Util.fixHTML(file.name), - Util.fixHTML(ext) + + var createHelper = function (href, text) { + var q = h('a.fa.fa-question-circle', { + style: 'text-decoration: none !important;', + title: text, + href: origin + href, + target: "_blank", + 'data-tippy-placement': "right" + }); + return q; + }; + + // Ask for name, password and owner + var content = h('div', [ + h('h4', Messages.upload_modal_title), + UIElements.setHTML(h('label', {for: 'cp-upload-name'}), + Messages._getKey('upload_modal_filename', [ext])), + h('input#cp-upload-name', {type: 'text', placeholder: name}), + h('label', {for: 'cp-upload-password'}, Messages.creation_passwordValue), + UI.passwordInput({id: 'cp-upload-password'}), + h('span', { + style: 'display:flex;align-items:center;justify-content:space-between' + }, [ + UI.createCheckbox('cp-upload-owned', Messages.upload_modal_owner, true), + createHelper('/faq.html#keywords-owned', Messages.creation_owned1) + ]), ]); - UI.prompt(msg, name, function (newName) { - if (newName === null) { - // "Don't ask me again" - showNamePrompt = false; - return void cb (file.name); - } - if (!newName || !newName.trim()) { return void cb (file.name); } + + UI.confirm(content, function (yes) { + if (!yes) { return void cb(); } + + // Get the values + var newName = $(content).find('#cp-upload-name').val(); + var password = $(content).find('#cp-upload-password').val() || undefined; + var owned = $(content).find('#cp-upload-owned').is(':checked'); + + // Add extension to the name if needed + if (!newName || !newName.trim()) { newName = file.name; } var newExtIdx = newName.lastIndexOf('.'); var newExt = newExtIdx !== -1 ? newName.slice(newExtIdx) : ""; if (newExt !== ext) { newName += ext; } - cb(newName); - }, {cancel: Messages.doNotAskAgain}, true); - }; - var promptPassword = function (file, cb) { - var msg = Messages.upload_password; - UI.prompt(msg, '', function (password) { - if (password === null) { - // "Don't ask me again" - showPasswordPrompt = false; - return void cb (); - } - cb(password); - }, {cancel: Messages.doNotAskAgain, password: true}, true); + cb({ + name: newName, + password: password, + owned: owned + }); + }); }; var handleFileState = { @@ -264,36 +283,40 @@ define([ var file_arraybuffer; var name = file.name; var password; - var finish = function () { - var metadata = { - name: name, - type: file.type, - }; - if (thumb) { metadata.thumbnail = thumb; } - queue.push({ - blob: file_arraybuffer, - metadata: metadata, - password: password, - dropEvent: e - }); + var owned = true; + var finish = function (abort) { + if (!abort) { + var metadata = { + name: name, + type: file.type, + }; + if (thumb) { metadata.thumbnail = thumb; } + queue.push({ + blob: file_arraybuffer, + metadata: metadata, + password: password, + owned: owned, + dropEvent: e + }); + } handleFileState.inProgress = false; if (handleFileState.queue.length) { var next = handleFileState.queue.shift(); handleFile(next[0], next[1]); } }; - var getPassword = function () { - if (!showPasswordPrompt) { return void finish(); } - promptPassword(file, function (pw) { - password = pw; - finish(); - }); - }; var getName = function () { - if (!showNamePrompt) { return void getPassword(); } - promptName(file, function (newName) { - name = newName; - getPassword(); + // If "noStore", it means we don't want to store this file in our drive (avatar) + // In this case, we don't want a password or a filename, and we own the file + if (config.noStore) { return void finish(); } + + // Otherwise, ask for password, name and ownership + fileUploadModal(file, function (obj) { + if (!obj) { return void finish(true); } + name = obj.name; + password = obj.password; + owned = obj.owned; + finish(); }); };