diff --git a/customize.dist/translations/messages.ja.js b/customize.dist/translations/messages.ja.js new file mode 100644 index 000000000..7293bfc50 --- /dev/null +++ b/customize.dist/translations/messages.ja.js @@ -0,0 +1,14 @@ +/* + * You can override the translation text using this file. + * The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js) + in a 'customize' directory (/customize/translations/messages.{LANG}.js). + * If you want to check all the existing translation keys, you can open the internal language file + but you should not change it directly (/common/translations/messages.{LANG}.js) +*/ +define(['/common/translations/messages.ja.js'], function (Messages) { + // Replace the existing keys in your copied file here: + // Messages.button_newpad = "New Rich Text Document"; + + return Messages; +}); + diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 207521574..6df68a3f9 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -564,14 +564,18 @@ define([ newPassword, passwordOk ]); + var pLocked = false; $(passwordOk).click(function () { var newPass = $(newPassword).find('input').val(); if (data.password === newPass || (!data.password && !newPass)) { return void UI.alert(Messages.properties_passwordSame); } + if (pLocked) { return; } + pLocked = true; UI.confirm(changePwConfirm, function (yes) { - if (!yes) { return; } + if (!yes) { pLocked = false; return; } + $(passwordOk).html('').append(h('span.fa.fa-spinner.fa-spin', {style: 'margin-left: 0'})); sframeChan.query("Q_PAD_PASSWORD_CHANGE", { teamId: typeof(owned) !== "boolean" ? owned : undefined, href: data.href || data.roHref, @@ -579,6 +583,8 @@ define([ }, function (err, data) { if (err || data.error) { console.error(err || data.error); + pLocked = false; + $(passwordOk).text(Messages.properties_changePasswordButton); return void UI.alert(Messages.properties_passwordError); } UI.findOKButton().click(); @@ -2065,10 +2071,7 @@ define([ var cryptKey = Hash.encodeBase64(secret.keys && secret.keys.cryptKey); var src = origin + Hash.getBlobPathFromHex(hexFileName); common.getFileSize(hexFileName, function (e, data) { - if (e || !data) { - displayDefault(); - return void console.error(e || "404 avatar"); - } + if (e || !data) { return void displayDefault(); } if (typeof data !== "number") { return void displayDefault(); } if (Util.bytesToMegabytes(data) > 0.5) { return void displayDefault(); } var $img = $('').appendTo($container); diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 622d427ac..dab5fc89d 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -959,9 +959,7 @@ define([ href: href, oldChannel: oldChannel, password: newPassword - }, waitFor(function (obj) { - console.error(obj); - })); + }, waitFor()); return; } pad.leavePad({ @@ -998,8 +996,10 @@ define([ } })); if (!isSharedFolder) { - common.unpinPads([oldChannel], waitFor(), teamId); - common.pinPads([newSecret.channel], waitFor(), teamId); + postMessage("CHANGE_PAD_PASSWORD_PIN", { + oldChannel: oldChannel, + channel: newSecret.channel + }, waitFor()); } }).nThen(function () { cb({ diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index be488e47e..50682edb6 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -1878,6 +1878,12 @@ define([ $span.addClass('cp-app-drive-element-sharedf'); _addOwnership($span, $state, data); + var hrefData = Hash.parsePadUrl(data.href || data.roHref); + if (hrefData.hashData && hrefData.hashData.password) { + var $password = $passwordIcon.clone().appendTo($state); + $password.attr('title', Messages.fm_passwordProtected || ''); + } + var $shared = $sharedIcon.clone().appendTo($state); $shared.attr('title', Messages.fm_canBeShared); } @@ -4515,7 +4521,7 @@ define([ onClose: cb }); }; - if (typeof (deprecated) === "object") { + if (typeof (deprecated) === "object" && APP.editable) { Object.keys(deprecated).forEach(function (fId) { var data = deprecated[fId]; var sfId = manager.user.userObject.getSFIdFromHref(data.href); diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 98806cee9..f22f9ab5d 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1526,8 +1526,7 @@ define([ Store.leavePad = function (clientId, data, cb) { var channel = channels[data.channel]; if (!channel || !channel.cpNf) { return void cb ({error: 'EINVAL'}); } - channel.cpNf.stop(); - delete channels[data.channel]; + Store.dropChannel(data.channel); cb(); }; Store.sendPadMsg = function (clientId, data, cb) { @@ -1542,6 +1541,20 @@ define([ channel.sendMessage(msg, clientId, cb); }; + // Unpin and pin the new channel in all team when changing a pad password + Store.changePadPasswordPin = function (clientId, data, cb) { + var oldChannel = data.oldChannel; + var channel = data.channel; + nThen(function (waitFor) { + getAllStores().forEach(function (s) { + var allData = s.manager.findChannel(channel); + if (!allData.length) { return; } + s.rpc.unpin([oldChannel], waitFor()); + s.rpc.pin([channel], waitFor()); + }); + }).nThen(cb); + }; + // requestPadAccess is used to check if we have a way to contact the owner // of the pad AND to send the request if we want // data.send === false ==> check if we can contact them @@ -1831,7 +1844,7 @@ define([ // Clients management var driveEventClients = []; - var dropChannel = function (chanId) { + var dropChannel = Store.dropChannel = function (chanId) { try { store.messenger.leavePad(chanId); } catch (e) { console.error(e); } @@ -1900,7 +1913,7 @@ define([ store.manager.user.userObject.getHref(data) : data.href; var parsed = Hash.parsePadUrl(href); var secret = Hash.getSecrets(parsed.type, parsed.hash, o); - SF.updatePassword({ + SF.updatePassword(Store, { oldChannel: secret.channel, password: n, href: href @@ -2044,6 +2057,15 @@ define([ /////////////////////// Init ///////////////////////////////////// ////////////////////////////////////////////////////////////////// + Store.refreshDriveUI = function () { + getAllStores().forEach(function (_s) { + var send = _s.id ? _s.sendEvent : sendDriveEvent; + send('DRIVE_CHANGE', { + path: ['drive', UserObject.FILES_DATA] + }); + }); + }; + var onReady = function (clientId, returned, cb) { var proxy = store.proxy; var unpin = function (data, cb) { diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js index ec23c0f76..2ebb718ec 100644 --- a/www/common/outer/sharedfolder.js +++ b/www/common/outer/sharedfolder.js @@ -103,29 +103,31 @@ define([ cb(sf.rt, sf.metadata); }); }); - sf.teams.push(store); - if (handler) { handler(id, sf.rt); } - return sf.rt; - } - if (sf && sf.queue && sf.rt) { - // The shared folder is loading, add our callbacks to the queue - sf.queue.push({ + sf.teams.push({ cb: cb, store: store, id: id }); - sf.teams.push(store); if (handler) { handler(id, sf.rt); } - return sf.rt; + return; + } + if (sf && !sf.ready && sf.rt) { + // The shared folder is loading, add our callbacks to the queue + sf.teams.push({ + cb: cb, + store: store, + id: id + }); + if (handler) { handler(id, sf.rt); } + return; } sf = allSharedFolders[secret.channel] = { - queue: [{ + teams: [{ cb: cb, store: store, id: id }], - teams: [store], readOnly: Boolean(secondaryKey) }; @@ -151,10 +153,10 @@ define([ // New Shared folder: no migration required rt.proxy.version = 2; } - if (!sf.queue) { + if (!sf.teams) { return; } - sf.queue.forEach(function (obj) { + sf.teams.forEach(function (obj) { var leave = function () { SF.leave(secret.channel, teamId); }; var uo = obj.store.manager.addProxy(obj.id, rt, leave, secondaryKey); SF.checkMigration(secondaryKey, rt.proxy, uo, function () { @@ -163,15 +165,17 @@ define([ }); sf.metadata = info.metadata; sf.ready = true; - delete sf.queue; }); rt.proxy.on('error', function (info) { if (info && info.error) { if (info.error === "EDELETED" ) { try { // Deprecate the shared folder from each team - sf.teams.forEach(function (store) { - store.manager.deprecateProxy(id, secret.channel); + // XXX We can't deprecate a read-only proxy: the read-only seed will change... + // We can only remove it + sf.teams.forEach(function (obj) { + console.log(obj.store.id, obj.store, obj.id); + obj.store.manager.deprecateProxy(obj.id, secret.channel); }); } catch (e) {} delete allSharedFolders[secret.channel]; @@ -200,8 +204,8 @@ define([ var clients = sf.teams; if (!Array.isArray(clients)) { return; } var idx; - clients.some(function (store, i) { - if (store.id === teamId) { + clients.some(function (obj, i) { + if (obj.store.id === teamId) { idx = i; return true; } @@ -217,6 +221,7 @@ define([ } }; + // Update the password locally SF.updatePassword = function (Store, data, network, cb) { var oldChannel = data.oldChannel; var href = data.href; @@ -229,13 +234,18 @@ define([ sf.rt.stop(); } var nt = nThen; - sf.teams.forEach(function (s) { + sf.teams.forEach(function (obj) { + // XXX if we're a viewer in this team, we can't update the keys nt = nt(function (waitFor) { - var sfId = s.manager.user.userObject.getSFIdFromHref(href); + var s = obj.store; + var sfId = obj.id; var shared = Util.find(s.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {}; if (!sfId || !shared[sfId]) { return; } var sf = JSON.parse(JSON.stringify(shared[sfId])); sf.password = password; + sf.channel = secret.channel; + sf.href = '/drive/#'+Hash.getEditHashFromKeys(secret); // XXX encrypt + sf.roHref = '/drive/#'+Hash.getViewHashFromKeys(secret); SF.load({ network: network, store: s, diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js index 9accc5f29..2ef490876 100644 --- a/www/common/outer/store-rpc.js +++ b/www/common/outer/store-rpc.js @@ -79,6 +79,7 @@ define([ GIVE_PAD_ACCESS: Store.givePadAccess, GET_PAD_METADATA: Store.getPadMetadata, SET_PAD_METADATA: Store.setPadMetadata, + CHANGE_PAD_PASSWORD_PIN: Store.changePadPasswordPin, // Drive DRIVE_USEROBJECT: Store.userObjectCommand, // Settings, diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index 76651cf2f..788c59fae 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -54,6 +54,9 @@ define([ var deprecateProxy = function (Env, id, channel) { Env.unpinPads([channel], function () {}); Env.user.userObject.deprecateSharedFolder(id); + if (Env.Store && Env.Store.refreshDriveUI) { + Env.Store.refreshDriveUI(); + } }; /* @@ -547,7 +550,12 @@ define([ if (isNew) { return void cb({ error: 'ENOTFOUND' }); } + var parsed = Hash.parsePadUrl(href); + var secret = Hash.getSecrets(parsed.type, parsed.hash, newPassword); data.password = newPassword; + data.channel = secret.channel; + data.href = '/drive/#'+Hash.getEditHashFromKeys(secret); // XXX encrypt + data.roHref = '/drive/#'+Hash.getViewHashFromKeys(secret); _addSharedFolder(Env, { path: ['root'], folderData: data, diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js index d6a5111be..48e78775d 100644 --- a/www/common/sframe-common-outer.js +++ b/www/common/sframe-common-outer.js @@ -174,7 +174,16 @@ define([ var parsed = Utils.Hash.parsePadUrl(window.location.href); var todo = function () { secret = Utils.secret = Utils.Hash.getSecrets(parsed.type, void 0, password); - Cryptpad.getShareHashes(secret, waitFor(function (err, h) { hashes = h; })); + Cryptpad.getShareHashes(secret, waitFor(function (err, h) { + hashes = h; + if (password && !parsed.hashData.password) { + var ohc = window.onhashchange; + window.onhashchange = function () {}; + window.location.hash = h.fileHash || h.editHash || h.viewHash || window.location.hash; + window.onhashchange = ohc; + ohc({reset: true}); + } + })); }; if (!parsed.hashData) { // No hash, no need to check for a password @@ -197,68 +206,96 @@ define([ // 2c: 'view' pad and '/p/' and a wrong password stored --> the seed is incorrect // 2d: 'view' pad and '/p/' and password never stored (security feature) --> password-prompt - Cryptpad.getPadAttribute('password', waitFor(function (err, val) { - var askPassword = function (wrongPasswordStored) { - // Ask for the password and check if the pad exists - // 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; - var next = function (e, isNew) { - if (Boolean(isNew)) { - // Ask again in the inner iframe - // We should receive a new Q_PAD_PASSWORD_VALUE - cb(false); + var askPassword = function (wrongPasswordStored) { + // Ask for the password and check if the pad exists + // 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; + var next = function (e, isNew) { + if (Boolean(isNew)) { + // Ask again in the inner iframe + // We should receive a new Q_PAD_PASSWORD_VALUE + cb(false); + } else { + todo(); + if (wrongPasswordStored) { + // Store the correct password + nThen(function (w) { + // XXX noPasswordStored: return; ? + Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl()); + Cryptpad.setPadAttribute('channel', secret.channel, w(), parsed.getUrl()); + if (parsed.hashData.mode === 'edit') { + var href = window.location.pathname + '#' + Utils.Hash.getEditHashFromKeys(secret); + Cryptpad.setPadAttribute('href', href, w(), parsed.getUrl()); + var roHref = window.location.pathname + '#' + Utils.Hash.getViewHashFromKeys(secret); + Cryptpad.setPadAttribute('roHref', roHref, w(), parsed.getUrl()); + } + }).nThen(correctPassword); } else { - todo(); - if (wrongPasswordStored) { - // Store the correct password - nThen(function (w) { - Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl()); - Cryptpad.setPadAttribute('channel', secret.channel, w(), parsed.getUrl()); - }).nThen(correctPassword); - } else { - correctPassword(); - } - cb(true); + correctPassword(); } - }; - 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; + cb(true); } - // Not a file, so we can use `isNewChannel` - Cryptpad.isNewChannel(window.location.href, password, next); - }); - sframeChan.event("EV_PAD_PASSWORD"); - }; + }; + 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"); + }; - if (!val && sessionStorage.newPadPassword) { - val = sessionStorage.newPadPassword; + var done = waitFor(); + var stored = false; + nThen(function (w) { + Cryptpad.getPadAttribute('title', w(function (err, data) { + stored = (!err && typeof (data) === "string"); + })); + Cryptpad.getPadAttribute('password', w(function (err, val) { + password = val; + }), parsed.getUrl()); + }).nThen(function (w) { + if (!password && !stored && sessionStorage.newPadPassword) { + password = sessionStorage.newPadPassword; delete sessionStorage.newPadPassword; } - password = val; - Cryptpad.getFileSize(window.location.href, password, waitFor(function (e, size) { - if (size !== 0) { - return void todo(); - } - if (parsed.hashData.mode === 'view' && (val || !parsed.hashData.password)) { + 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, w(function (e, size) { + if (size !== 0) { return void todo(); } + // Wrong password or deleted file? + askPassword(true); + })); + return; + } + // Not a file, so we can use `isNewChannel` + Cryptpad.isNewChannel(window.location.href, password, w(function(e, isNew) { + if (!isNew) { return void todo(); } + if (parsed.hashData.mode === 'view' && (password || !parsed.hashData.password)) { // Error, wrong password stored, the view seed has changed with the password // password will never work sframeChan.event("EV_PAD_PASSWORD_ERROR"); waitFor.abort(); return; } + if (!stored && !parsed.hashData.password) { + // We've received a link without /p/ and it doesn't work without a password: abort + return void todo(); + } // Wrong password or deleted file? askPassword(true); })); - }), parsed.getUrl()); + }).nThen(done); } }).nThen(function (waitFor) { if (cfg.afterSecrets) { diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json index 2e4574c72..8c9f7a476 100644 --- a/www/common/translations/messages.fr.json +++ b/www/common/translations/messages.fr.json @@ -1221,5 +1221,9 @@ "team_title": "Équipe : {0}", "team_quota": "Limite de stockage de votre équipe", "drive_quota": "Votre limite de stockage", - "settings_codeBrackets": "Fermer automatiquement les parenthèses" + "settings_codeBrackets": "Fermer automatiquement les parenthèses", + "team_viewers": "Lecteurs", + "drive_sfPassword": "Votre dossier partagé {0} n'est plus disponible. Il a soit été supprimé par son propriétaire ou il est protégé par un nouveau mot de passe. Vous pouvez supprimer ce dossier de votre CryptDrive ou retrouver l'accès en tapant le nouveau mot de passe.", + "drive_sfPasswordError": "Mot de passe incorrect", + "password_error_seed": "Pad introuvable !
Cette erreur peut provenir de deux facteurs. Soit un mot de passe a été ajouté ou modifié, soit le pad a été supprimé par son propriétaire." } diff --git a/www/common/translations/messages.ja.json b/www/common/translations/messages.ja.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/www/common/translations/messages.ja.json @@ -0,0 +1 @@ +{} diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index a811b3e1b..e45b7dec4 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -1221,5 +1221,9 @@ "team_title": "Team: {0}", "team_quota": "Your team's storage limit", "drive_quota": "Your storage limit", - "settings_codeBrackets": "Auto-close brackets" + "settings_codeBrackets": "Auto-close brackets", + "team_viewers": "Viewers", + "drive_sfPassword": "Your shared folder {0} is no longer available. It has either been deleted by its owner or it is now protected with a new password. You can remove this folder from your CryptDrive, or recover access using the new password.", + "drive_sfPasswordError": "Wrong password", + "password_error_seed": "Pad not found!
This error can be caused by two factors: either a password was added/changed, or the pad has been deleted from the server." } diff --git a/www/common/userObject.js b/www/common/userObject.js index cce6982c4..bdfff2caa 100644 --- a/www/common/userObject.js +++ b/www/common/userObject.js @@ -453,8 +453,8 @@ define([ var result; var noPassword = function (str) { if (!str) { return; } - var value = str.replace(/\/p\/?/, '/'); - return Hash.getRelativeHref(value); + var parsed = Hash.parsePadUrl(str); + return parsed.getUrl().replace(/\/p\/?/, '/'); }; var href = noPassword(_href); getFiles([FILES_DATA]).some(function (id) { @@ -471,8 +471,8 @@ define([ var result; var noPassword = function (str) { if (!str) { return; } - var value = str.replace(/\/p\/?/, '/'); - return Hash.getRelativeHref(value); + var parsed = Hash.parsePadUrl(str); + return parsed.getUrl().replace(/\/p\/?/, '/'); }; var href = noPassword(_href); getFiles([SHARED_FOLDERS]).some(function (id) { diff --git a/www/teams/inner.js b/www/teams/inner.js index bf5732340..34358a70e 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -398,7 +398,7 @@ define([ var isOwner = Object.keys(privateData.teams || {}).filter(function (id) { return privateData.teams[id].owner; - }).length >= Constants.MAX_TEAMS_OWNED; // && !privateData.devMode; + }).length >= Constants.MAX_TEAMS_OWNED && !privateData.devMode; var getWarningBox = function () { return h('div.alert.alert-warning', {