diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 08c84d1e6..a02accadf 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -2540,6 +2540,7 @@ define([ UIElements.onServerError = function (common, err, toolbar, cb) { //if (["EDELETED", "EEXPIRED", "ERESTRICTED"].indexOf(err.type) === -1) { return; } var priv = common.getMetadataMgr().getPrivateData(); + var sframeChan = common.getSframeChannel(); var msg = err.type; if (err.type === 'EEXPIRED') { msg = Messages.expiredError; @@ -2549,10 +2550,29 @@ define([ if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); } } else if (err.type === 'EDELETED') { if (priv.burnAfterReading) { return void cb(); } + + if (autoStoreModal[priv.channel]) { + autoStoreModal[priv.channel].delete(); + delete autoStoreModal[priv.channel]; + } + // View users have the wrong seed, thay can't retireve access directly + // Version 1 hashes don't support passwords + if (!priv.readOnly && !priv.oldVersionHash) { + sframeChan.event('EV_SHARE_OPEN', {hidden: true}); // Close share modal + UIElements.displayPasswordPrompt(common, { + fromServerError: true, + loaded: err.loaded, + }); + if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); } + (cb || function () {})(); + return; + } + msg = Messages.deletedError; if (err.loaded) { msg += Messages.errorCopy; } + if (toolbar && typeof toolbar.deleted === "function") { toolbar.deleted(); } } else if (err.type === 'ERESTRICTED') { msg = Messages.restrictedError; @@ -2561,7 +2581,6 @@ define([ msg = Messages.oo_deletedVersion; if (toolbar && typeof toolbar.failed === "function") { toolbar.failed(true); } } - var sframeChan = common.getSframeChannel(); sframeChan.event('EV_SHARE_OPEN', {hidden: true}); UI.errorLoadingScreen(msg, Boolean(err.loaded), Boolean(err.loaded)); (cb || function () {})(); @@ -2570,7 +2589,10 @@ define([ UIElements.displayPasswordPrompt = function (common, cfg, isError) { var error; if (isError) { error = setHTML(h('p.cp-password-error'), Messages.password_error); } + var info = h('p.cp-password-info', Messages.password_info); + var info_loaded = setHTML(h('p.cp-password-info'), Messages.errorCopy); + var password = UI.passwordInput({placeholder: Messages.password_placeholder}); var $password = $(password); var button = h('button.btn.btn-primary', Messages.password_submit); @@ -2582,6 +2604,21 @@ define([ var submit = function () { var value = $password.find('.cp-password-input').val(); + + // Password-prompt called from UIElements.onServerError + if (cfg.fromServerError) { + common.getSframeChannel().query('Q_PASSWORD_CHECK', value, function (err, obj) { + if (obj && obj.error) { + console.error(obj.error); + return void UI.warn(Messages.error); + } + // On success, outer will reload the page: this is a wrong password + UIElements.displayPasswordPrompt(common, cfg, true); + }); + return; + } + + // Initial load UI.addLoadingScreen({newProgress: true}); if (window.CryptPad_updateLoadingProgress) { window.CryptPad_updateLoadingProgress({ @@ -2595,6 +2632,8 @@ define([ } }); }; + + $password.find('.cp-password-input').on('keydown', function (e) { if (e.which === 13) { submit(); } }); $(button).on('click', function () { submit(); }); @@ -2602,12 +2641,13 @@ define([ var block = h('div#cp-loading-password-prompt', [ error, info, + cfg.loaded ? info_loaded : undefined, h('p.cp-password-form', [ password, button - ]) + ]), ]); - UI.errorLoadingScreen(block); + UI.errorLoadingScreen(block, Boolean(cfg.loaded), Boolean(cfg.loaded)); $password.find('.cp-password-input').focus(); }; diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 8bf01c965..d653bd72e 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -481,10 +481,20 @@ define([ }); }; + common.isNewChannel = function (href, password, _cb) { + var cb = Util.once(Util.mkAsync(_cb)); + var channel = Hash.hrefToHexChannelId(href, password); + postMessage('IS_NEW_CHANNEL', {channel: channel}, function (obj) { + var error = obj && obj.error; + if (error) { return void cb(error); } + if (!obj) { return void cb('ERROR'); } + cb (null, obj.isNew); + }, {timeout: -1}); + }; // This function is used when we want to open a pad. We first need // to check if it exists. With the cached drive, we need to wait for // the network to be available before we can continue. - common.isNewChannel = function (href, password, _cb) { + common.hasChannelHistory = function (href, password, _cb) { var cb = Util.once(Util.mkAsync(_cb)); var channel = Hash.hrefToHexChannelId(href, password); var error; @@ -1182,7 +1192,6 @@ define([ } else if (mailbox && typeof(mailbox) === "object") { m = {}; Object.keys(mailbox).forEach(function (ed) { - console.log(mailbox[ed]); try { m[ed] = newCrypto.encrypt(oldCrypto.decrypt(mailbox[ed], true, true)); } catch (e) { @@ -1217,6 +1226,7 @@ define([ cryptgetVal = JSON.stringify(parsed); } }), optsGet); + Cache.clearChannel(newSecret.channel, waitFor()); }).nThen(function (waitFor) { optsPut.metadata.restricted = oldMetadata.restricted; optsPut.metadata.allowed = oldMetadata.allowed; @@ -1598,6 +1608,7 @@ define([ } })); })); + Cache.clearChannel(newSecret.channel, waitFor()); }).nThen(function (waitFor) { // The new rt channel is ready // The blob uses its own encryption and doesn't need to be reencrypted @@ -2506,7 +2517,25 @@ define([ } if (parsedNew.hashData) { oldHref = newHref; } }; + // XXX if you're in noDrive mode, check if an FS_hash is added and reload if that's the case // Listen for login/logout in other tabs + if (rdyCfg.noDrive && !localStorage[Constants.fileHashKey]) { + window.addEventListener('storage', function (e) { + if (e.key !== Constants.fileHashKey) { return; } + // New entry added to FS_hash: drive created in another tab, reload + var o = e.oldValue; + var n = e.newValue; + if (!o && n) { + postMessage('HAS_DRIVE', null, function(obj) { + // If we're still in noDrive mode, reload + if (!obj.state) { + LocalStore.loginReload(); + } + // Otherwise this worker is connected, nothing to do + }); + } + }); + } window.addEventListener('storage', function (e) { if (e.key !== Constants.userHashKey) { return; } var o = e.oldValue; diff --git a/www/common/inner/access.js b/www/common/inner/access.js index 47b04a5c4..cb3fc57c9 100644 --- a/www/common/inner/access.js +++ b/www/common/inner/access.js @@ -25,10 +25,12 @@ define([ var sframeChan = common.getSframeChannel(); var metadataMgr = common.getMetadataMgr(); - var channel = data.channel; + var priv = metadataMgr.getPrivateData(); + var channel = data.channel || priv.channel; var owners = data.owners || []; var pending_owners = data.pending_owners || []; var teamOwner = data.teamId; + var title = opts.title; opts = opts || {}; var redrawAll = function () {}; @@ -115,7 +117,7 @@ define([ if (!friend) { return; } common.mailbox.sendTo("RM_OWNER", { channel: channel, - title: data.title, + title: data.title || title, pending: pending }, { channel: friend.notifications, @@ -271,7 +273,7 @@ define([ href: data.href || data.rohref, password: data.password, path: isTemplate ? ['template'] : undefined, - title: data.title || '', + title: data.title || title || "", teamId: obj.id }, waitFor(function (err) { if (err) { return void console.error(err); } @@ -320,6 +322,12 @@ define([ })); } }).nThen(function (waitFor) { + var href = data.href; + var hashes = priv.hashes || {}; + var bestHash = hashes.editHash || hashes.viewHash || hashes.fileHash; + if (data.fakeHref) { + href = Hash.hashToHref(bestHash, priv.app); + } sel.forEach(function (el) { var curve = $(el).attr('data-curve'); if (curve === user.curvePublic) { return; } @@ -327,9 +335,9 @@ define([ if (!friend) { return; } common.mailbox.sendTo("ADD_OWNER", { channel: channel, - href: data.href, - password: data.password, - title: data.title + href: href, + password: data.password || priv.password, + title: data.title || title }, { channel: friend.notifications, curvePublic: friend.curvePublic @@ -398,7 +406,8 @@ define([ var sframeChan = common.getSframeChannel(); var metadataMgr = common.getMetadataMgr(); - var channel = data.channel; + var priv = metadataMgr.getPrivateData(); + var channel = data.channel || priv.channel; var owners = data.owners || []; var restricted = data.restricted || false; var allowed = data.allowed || []; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 8ac1a77c3..f20a545ec 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -452,7 +452,7 @@ define([ account.note = obj.note; cb(obj); }); - }); + }, Cache); }; ////////////////////////////////////////////////////////////////// @@ -1710,6 +1710,10 @@ define([ var onError = function (err) { channel.bcast("PAD_ERROR", err); + if (err && err.type === "EDELETED" && Cache && Cache.clearChannel) { + Cache.clearChannel(data.channel); + } + // If this is a DELETED, EXPIRED or RESTRICTED pad, leave the channel if (["EDELETED", "EEXPIRED", "ERESTRICTED"].indexOf(err.type) === -1) { return; } Store.leavePad(null, data, function () {}); @@ -1720,11 +1724,13 @@ define([ postMessage(clientId, "PAD_CACHE"); }, onCacheReady: function () { + channel.hasCache = true; postMessage(clientId, "PAD_CACHE_READY"); }, onReady: function (pad) { var padData = pad.metadata || {}; channel.data = padData; + channel.ready = true; if (padData && padData.validateKey && store.messenger) { store.messenger.storeValidateKey(data.channel, padData.validateKey); } @@ -2941,6 +2947,13 @@ define([ */ var initialized = false; + // Are we still in noDrive mode? + Store.hasDrive = function (clientId, data, cb) { + cb({ + state: Boolean(store.proxy) + }); + }; + // If we load CryptPad for the first time from an existing pad, don't create a // drive automatically. var onNoDrive = function (clientId, cb) { diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index e444f47f5..c4d1d1301 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -15,6 +15,7 @@ define([ MIGRATE_ANON_DRIVE: Store.migrateAnonDrive, PING: function (cId, data, cb) { cb(); }, CACHE_DISABLE: Store.disableCache, + HAS_DRIVE: Store.hasDrive, // RPC UPDATE_PIN_LIMIT: Store.updatePinLimit, GET_PIN_LIMIT: Store.getPinLimit, diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index 8d791c8cd..131958d83 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -78,7 +78,7 @@ define([ }; var AppConfig; var Test; - var password, newPadPassword; + var password, newPadPassword, newPadPasswordForce; var initialPathInDrive; var burnAfterReading; @@ -312,6 +312,7 @@ define([ newPadPassword = Crypto.decrypt(newPad.pw, uKey); } catch (e) { console.error(e); } } + if (newPad.f) { newPadPasswordForce = 1; } if (newPad.d) { Cryptpad.fromFileData = newPad.d; var _parsed1 = Utils.Hash.parsePadUrl(Cryptpad.fromFileData.href); @@ -319,6 +320,7 @@ define([ delete Cryptpad.fromFileData; } } + } catch (e) { console.error(e, parsed.hashData.newPadOpts); } @@ -349,7 +351,7 @@ define([ } // We now need to check if there is a password and if we know the correct password. - // We'll use getFileSize and isNewChannel to detect incorrect passwords. + // We'll use getFileSize and hasChannelHistory to detect incorrect passwords. // First we'll get the password value from our drive (getPadAttribute), and we'll check // if the channel is valid. If the pad is not stored in our drive, we'll test with an @@ -397,15 +399,15 @@ define([ } }; if (parsed.type === "file") { - // `isNewChannel` doesn't work for files (not a channel) + // `hasChannelHistory` doesn't work for files (not a channel) // `getFileSize` is not adapted to channels because of metadata Cryptpad.getFileSize(currentPad.href, password, function (e, size) { next(e, size === 0); }); return; } - // Not a file, so we can use `isNewChannel` - Cryptpad.isNewChannel(currentPad.href, password, next); + // Not a file, so we can use `hasChannelHistory` + Cryptpad.hasChannelHistory(currentPad.href, password, next); }); sframeChan.event("EV_PAD_PASSWORD", cfg); }; @@ -474,17 +476,25 @@ define([ password = val; }), parsed.getUrl()); }).nThen(function (w) { + // If we've already tested this password and this is a redirect, force + if (typeof(newPadPassword) !== "undefined" && newPadPasswordForce) { + password = newPadPassword; + return void todo(); + } + + // If the pad is not stored and we have a newPadPassword, it probably + // comes from a notification: password prompt pre-filled if (!password && !stored && newPadPassword) { passwordCfg.value = newPadPassword; } // Pad not stored && password required: always ask for the password - if (!stored && parsed.hashData.password) { + if (!stored && parsed.hashData.password && !newPadPasswordForce) { return void askPassword(true, passwordCfg); } if (parsed.type === "file") { - // `isNewChannel` doesn't work for files (not a channel) + // `hasChannelHistory` doesn't work for files (not a channel) // `getFileSize` is not adapted to channels because of metadata Cryptpad.getFileSize(currentPad.href, password, w(function (e, size) { if (size !== 0) { return void todo(); } @@ -493,8 +503,8 @@ define([ })); return; } - // Not a file, so we can use `isNewChannel` - Cryptpad.isNewChannel(currentPad.href, password, w(function(e, isNew) { + // Not a file, so we can use `hasChannelHistory` + Cryptpad.hasChannelHistory(currentPad.href, password, w(function(e, isNew) { if (isNew && expire && expire < (+new Date())) { sframeChan.event("EV_EXPIRED_ERROR"); waitFor.abort(); @@ -541,8 +551,7 @@ define([ if (realtime) { // TODO we probably don't need to check again for password-protected pads - // (we use isNewChannel to test the password...) - Cryptpad.isNewChannel(currentPad.href, password, waitFor(function (e, isNew) { + Cryptpad.hasChannelHistory(currentPad.href, password, waitFor(function (e, isNew) { if (e) { return console.error(e); } isNewFile = Boolean(isNew); })); @@ -608,6 +617,7 @@ define([ feedbackAllowed: Utils.Feedback.state, isPresent: parsed.hashData && parsed.hashData.present, isEmbed: parsed.hashData && parsed.hashData.embed, + oldVersionHash: parsed.hashData && parsed.hashData.version < 2, // password isHistoryVersion: parsed.hashData && parsed.hashData.versionHash, notifications: notifs, accounts: { @@ -1674,6 +1684,45 @@ define([ }); }); + sframeChan.on('Q_PASSWORD_CHECK', function (pw, cb) { + Cryptpad.isNewChannel(currentPad.href, pw, function (e, isNew) { + if (isNew === false) { + var channel = Utils.Hash.hrefToHexChannelId(currentPad.href, pw); + + nThen(function (w) { + // If the pad is stored, update its data + var _secret = Utils.Hash.getSecrets(parsed.type, parsed.hash, pw); + var chan = _secret.channel; + var editH = Utils.Hash.getEditHashFromKeys(_secret); + var viewH = Utils.Hash.getViewHashFromKeys(_secret); + var href = Utils.Hash.hashToHref(editH, parsed.type); + var roHref = Utils.Hash.hashToHref(viewH, parsed.type); + Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl()); + Cryptpad.setPadAttribute('channel', chan, w(), parsed.getUrl()); + Cryptpad.setPadAttribute('href', href, w(), parsed.getUrl()); + Cryptpad.setPadAttribute('roHref', roHref, w(), parsed.getUrl()); + }).nThen(function () { + // Get redirect URL + var uHash = Utils.LocalStore.getUserHash(); + var uSecret = Utils.Hash.getSecrets('drive', uHash); + var uKey = uSecret.keys.cryptKey; + var url = Utils.Hash.getNewPadURL(currentPad.href, { + pw: Crypto.encrypt(pw, uKey), + f: 1 + }); + // redirect + window.location.href = url; + document.location.reload(); + }); + + return; + } + cb({ + error: e + }); + }); + }); + if (cfg.messaging) { sframeChan.on('Q_CHAT_OPENPADCHAT', function (data, cb) { Cryptpad.universal.execCommand({ diff --git a/www/common/toolbar.js b/www/common/toolbar.js index aafc356a5..2c208c1c9 100644 --- a/www/common/toolbar.js +++ b/www/common/toolbar.js @@ -565,7 +565,12 @@ MessengerUI, Messages) { h('span.cp-button-name', Messages.accessButton) ])); $accessBlock.click(function () { - Common.getSframeChannel().event('EV_ACCESS_OPEN'); + var title = (config.title && config.title.getTitle && config.title.getTitle()) + || (config.title && config.title.defaultName) + || ""; + Common.getSframeChannel().event('EV_ACCESS_OPEN', { + title: title + }); }); toolbar.$bottomM.append($accessBlock); diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json index 322c7cf7b..f3eff56bc 100644 --- a/www/common/translations/messages.fr.json +++ b/www/common/translations/messages.fr.json @@ -27,11 +27,11 @@ "padNotPinned": "Ce pad va expirer après 3 mois d'inactivité, {0}connectez-vous{1} ou {2}enregistrez-vous{3} pour le préserver.", "anonymousStoreDisabled": "L'administrateur de cette instance de CryptPad a désactivé le drive pour les utilisateurs non enregistrés. Vous devez vous connecter pour pouvoir utiliser CryptDrive.", "expiredError": "Ce pad a atteint sa date d'expiration est n'est donc plus disponible.", - "deletedError": "Ce pad a été supprimé par son propriétaire et n'est donc plus disponible.", + "deletedError": "Ce document a été supprimé et n'est plus disponible.", "inactiveError": "Ce pad a été supprimé en raison de son inactivité. Appuyez sur Échap pour créer un nouveau pad.", "chainpadError": "Une erreur critique est survenue lors de la mise à jour du contenu. Le pad est désormais en mode lecture seule afin de s'assurer que vous ne perdiez pas davantage de données.
Appuyez sur Échap pour voir le pad ou rechargez la page pour pouvoir le modifier à nouveau.", "invalidHashError": "L'URL du document demandé n'est pas valide.", - "errorCopy": " Vous pouvez accéder au contenu en appuyant sur Échap.
Quand vous fermerez cette page, il sera définitivement supprimé.", + "errorCopy": " Vous pouvez toujours utiliser la version actuelle en mode lecture seule en appuyant sur Échap.", "errorRedirectToHome": "Appuyez sur Échap pour retourner vers votre CryptDrive.", "newVersionError": "Une nouvelle version de CryptPad est disponible.
Rechargez la page pour utiliser la nouvelle version, ou appuyez sur Échap pour accéder au contenu actuel en mode hors-ligne.", "loading": "Chargement...", @@ -604,8 +604,8 @@ "creation_expiration": "Date d'expiration", "creation_passwordValue": "Mot de passe", "creation_newPadModalDescription": "Cliquer sur le type de document à créer. Vous pouvez aussi utiliser les touches Tab pour sélectionner un type et Entrée pour valider.", - "password_info": "Le pad auquel vous essayez d'accéder n'existe plus ou est protégé par un mot de passe. Entrez le bon mot de passe pour accéder à son contenu.", - "password_error": "Pad introuvable !
Cette erreur peut provenir de deux facteurs. Soit le mot de passe est faux, soit le pad a été supprimé du serveur.", + "password_info": "Le document auquel vous essayez d'accéder n'existe plus ou est protégé par un nouveau mot de passe. Entrez le bon mot de passe pour accéder au contenu.", + "password_error": "Document introuvable
Cette erreur peut provenir de deux facteurs : soit le mot de passe est faux, soit le document a été détruit.", "password_placeholder": "Tapez le mot de passe ici...", "password_submit": "Valider", "properties_addPassword": "Ajouter un mot de passe", diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index d0e4d20cb..51877b35f 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -29,11 +29,11 @@ "padNotPinnedVariable": "This pad will expire after {4} days of inactivity, {0}login{1} or {2}register{3} to preserve it.", "anonymousStoreDisabled": "The webmaster of this CryptPad instance has disabled the store for anonymous users. You have to log in to be able to use CryptDrive.", "expiredError": "This pad has reached its expiration time and is no longer available.", - "deletedError": "This pad has been deleted by its owner and is no longer available.", + "deletedError": "This document has been deleted and is no longer available.", "inactiveError": "This pad has been deleted due to inactivity. Press Esc to create a new pad.", "chainpadError": "A critical error occurred when updating your content. This page is in read-only mode to make sure you won't lose your work.
Hit Esc to continue to view this pad, or reload to try editing again.", "invalidHashError": "The document you've requested has an invalid URL.", - "errorCopy": " You can still access the content by pressing Esc.
Once you close this window you will not be able to access it again.", + "errorCopy": " You can still use the current version in read-only mode by pressing Esc.", "errorRedirectToHome": "Press Esc to be redirected to your CryptDrive.", "newVersionError": "A new version of CryptPad is available.
Reload to use the new version, or press escape to access your content in offline mode.", "loading": "Loading...", @@ -619,8 +619,8 @@ "creation_expiration": "Expiration date", "creation_passwordValue": "Password", "creation_newPadModalDescription": "Click on a document type to create it. You can also press Tab to select the type and press Enter to confirm.", - "password_info": "The pad you're trying to open no longer exist or is protected with a password. Enter the correct password to access its content.", - "password_error": "Pad not found!
This error can be caused by two factors: either the password in invalid, or the pad has been deleted from the server.", + "password_info": "The document you are trying to open no longer exist or is protected with a new password. Enter the correct password to access the content.", + "password_error": "Document not found
This error can be caused by two factors: either the password is invalid, or the document has been destroyed.", "password_placeholder": "Type the password here...", "password_submit": "Submit", "properties_addPassword": "Add a password", diff --git a/www/secureiframe/inner.js b/www/secureiframe/inner.js index 01635cbaa..1c5af4a57 100644 --- a/www/secureiframe/inner.js +++ b/www/secureiframe/inner.js @@ -89,9 +89,10 @@ define([ }; // Access modal - create['access'] = function () { + create['access'] = function (data) { require(['/common/inner/access.js'], function (Access) { Access.getAccessModal(common, { + title: data.title, onClose: function () { hideIframe(); }