From aaed0b939eaa04e3b9f380f34bedc6415bc4d959 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 27 Sep 2019 16:00:58 +0200 Subject: [PATCH 1/9] Transfer ownership from or to a team --- www/common/common-ui-elements.js | 119 +++++++++++++++++++++++++------ www/common/drive-ui.js | 4 ++ www/common/outer/async-store.js | 8 +-- 3 files changed, 104 insertions(+), 27 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 448dd779f..5542c1e53 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -104,6 +104,8 @@ define([ var channel = data.channel; var owners = data.owners || []; var pending_owners = data.pending_owners || []; + var teams = priv.teams; + var teamOwner = data.teamId; var redrawAll = function () {}; @@ -124,6 +126,12 @@ define([ return true; } }); + Object.keys(teams).some(function (id) { + if (teams[id].edPublic === ed) { + f = teams[id]; + f.teamId = id; + } + }); if (ed === edPublic) { f = f || user; if (f.name) { f.edPublic = edPublic; } @@ -155,6 +163,7 @@ define([ var toRemove = sel.map(function (el) { var ed = $(el).attr('data-ed'); if (!ed) { return; } + if (teamOwner && teams[teamOwner] && teams[teamOwner].edPublic === ed) { me = true; } if (ed === edPublic) { me = true; } return ed; }).filter(function (x) { return x; }); @@ -171,7 +180,8 @@ define([ sframeChan.query('Q_SET_PAD_METADATA', { channel: channel, command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS', - value: toRemove + value: toRemove, + teamId: teamOwner }, waitFor(function (err, res) { err = err || (res && res.error); if (err) { @@ -214,6 +224,7 @@ define([ // Add owners column var drawAdd = function () { + var $div = $(h('div.cp-share-column')); var _friends = JSON.parse(JSON.stringify(friends)); Object.keys(_friends).forEach(function (curve) { if (owners.indexOf(_friends[curve].edPublic) !== -1 || @@ -228,16 +239,44 @@ define([ }, function () { //console.log(arguments); }); - $div2 = $(addCol.div); + $div.append(addCol.div); + + if (priv.enableTeams) { + var teamsData = Util.tryParse(JSON.stringify(priv.teams)) || {}; + Object.keys(teamsData).forEach(function (id) { + var t = teamsData[id]; + t.teamId = id; + if (owners.indexOf(t.edPublic) !== -1 || pending_owners.indexOf(t.edPublic) !== -1) { + delete teamsData[id]; + } + }); + var teamsList = UIElements.getUserGrid('Or a team?', { // XXX + common: common, + noFilter: true, + data: teamsData + }, function () {}); + $div.append(teamsList.div); + } + // When clicking on the add button, we get the selected users. var addButton = h('button.no-margin', Messages.owner_addButton); $(addButton).click(function () { // Check selection - var $sel = $div2.find('.cp-usergrid-user.cp-selected'); + var $sel = $div.find('.cp-usergrid-user.cp-selected'); var sel = $sel.toArray(); if (!sel.length) { return; } var toAdd = sel.map(function (el) { - return friends[$(el).attr('data-curve')].edPublic; + var friend = friends[$(el).attr('data-curve')]; + if (!friend) { return; } + return friend.edPublic; + }).filter(function (x) { return x; }); + var toAddTeams = sel.map(function (el) { + var team = teamsData[$(el).attr('data-teamid')]; + if (!team || !team.edPublic) { return; } + return { + edPublic: team.edPublic, + id: $(el).attr('data-teamid') + }; }).filter(function (x) { return x; }); NThen(function (waitFor) { @@ -249,21 +288,57 @@ define([ } })); }).nThen(function (waitFor) { - // Send the command - sframeChan.query('Q_SET_PAD_METADATA', { - channel: channel, - command: 'ADD_PENDING_OWNERS', - value: toAdd - }, waitFor(function (err, res) { - err = err || (res && res.error); - if (err) { - waitFor.abort(); - redrawAll(); - var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden - : Messages.error; - return void UI.warn(text); - } - })); + if (toAddTeams.length) { + // Send the command + sframeChan.query('Q_SET_PAD_METADATA', { + channel: channel, + command: 'ADD_OWNERS', + value: toAddTeams.map(function (obj) { return obj.edPublic; }), + teamId: teamOwner + }, waitFor(function (err, res) { + err = err || (res && res.error); + if (err) { + waitFor.abort(); + redrawAll(); + var text = err === "INSUFFICIENT_PERMISSIONS" ? + Messages.fm_forbidden : Messages.error; + return void UI.warn(text); + } + // XXX add the pad to the team drive + var isTemplate = priv.isTemplate || data.isTemplate; + toAddTeams.forEach(function (obj) { + sframeChan.query('Q_STORE_IN_TEAM', { + href: data.href || data.rohref, + password: data.password, + path: isTemplate ? ['template'] : undefined, + title: data.title || '', + teamId: obj.id + }, waitFor(function (err) { + if (err) { return void console.error(err); } + console.warn(obj.id); + })); + }); + })); + } + }).nThen(function (waitFor) { + if (toAdd.length) { + // Send the command + sframeChan.query('Q_SET_PAD_METADATA', { + channel: channel, + command: 'ADD_PENDING_OWNERS', + value: toAdd, + teamId: teamOwner + }, waitFor(function (err, res) { + err = err || (res && res.error); + if (err) { + waitFor.abort(); + redrawAll(); + var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden + : Messages.error; + return void UI.warn(text); + } + })); + } }).nThen(function (waitFor) { sel.forEach(function (el) { var friend = friends[$(el).attr('data-curve')]; @@ -291,8 +366,8 @@ define([ UI.log(Messages.saved); }); }); - $div2.append(h('p', addButton)); - return $div2; + $div.append(h('p', addButton)); + return $div; }; redrawAll = function (md) { @@ -434,6 +509,7 @@ define([ if (owned && data.roHref && parsed.type !== 'drive' && parsed.hashData.type === 'pad') { var manageOwners = h('button.no-margin', Messages.owner_openModalButton); $(manageOwners).click(function () { + data.teamId = typeof(owned) !== "boolean" ? owned : undefined; var modal = createOwnerModal(common, data); UI.openCustomModal(modal, { wide: true, @@ -665,6 +741,7 @@ define([ UIElements.displayAvatar(common, $(avatar), data.avatar, name); return h('div.cp-usergrid-user'+(data.selected?'.cp-selected':'')+(config.large?'.large':''), { 'data-ed': data.edPublic, + 'data-teamid': data.teamId, 'data-curve': data.curvePublic || '', 'data-name': name.toLowerCase(), 'data-order': i, diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js index 813c9e71f..601c2a32d 100644 --- a/www/common/drive-ui.js +++ b/www/common/drive-ui.js @@ -3729,6 +3729,10 @@ define([ data.roHref = base + data.roHref; } + if (currentPath[0] === TEMPLATE) { + data.isTemplate = true; + } + if (manager.isSharedFolder(el)) { delete data.roHref; //data.noPassword = true; diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js index 21a474c26..535f699a7 100644 --- a/www/common/outer/async-store.js +++ b/www/common/outer/async-store.js @@ -1552,7 +1552,6 @@ define([ var href, title; - // XXX TEAMOWNER if (!res.some(function (obj) { if (obj.data && Array.isArray(obj.data.owners) && obj.data.owners.indexOf(edPublic) !== -1 && @@ -1612,11 +1611,8 @@ define([ Store.setPadMetadata = function (clientId, data, cb) { if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); } if (!data.command) { return void cb({ error: 'EINVAL' }); } - // XXX TEAMOWNER - // If owned by a team, we should use the team rpc here - // We'll need common-ui-elements to tell us the "owners" value or we can - // call getPadMetadata first - store.rpc.setMetadata(data, function (err, res) { + var s = getStore(data.teamId); + s.rpc.setMetadata(data, function (err, res) { if (err) { return void cb({ error: err }); } if (!Array.isArray(res) || !res.length) { return void cb({}); } cb(res[0]); From 44edbf8b39a8621da0b71fe2e031e6a66167e31d Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 27 Sep 2019 16:03:06 +0200 Subject: [PATCH 2/9] Remove some XXX --- www/common/common-ui-elements.js | 2 -- www/common/cryptpad-common.js | 1 - www/teams/main.js | 32 +------------------------------- 3 files changed, 1 insertion(+), 34 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 5542c1e53..03b0b534d 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -304,7 +304,6 @@ define([ Messages.fm_forbidden : Messages.error; return void UI.warn(text); } - // XXX add the pad to the team drive var isTemplate = priv.isTemplate || data.isTemplate; toAddTeams.forEach(function (obj) { sframeChan.query('Q_STORE_IN_TEAM', { @@ -505,7 +504,6 @@ define([ if (data.href || data.roHref) { parsed = Hash.parsePadUrl(data.href || data.roHref); } - // XXX Teams owner: transfer ownership if (owned && data.roHref && parsed.type !== 'drive' && parsed.hashData.type === 'pad') { var manageOwners = h('button.no-margin', Messages.owner_openModalButton); $(manageOwners).click(function () { diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js index 0516bf057..067112264 100644 --- a/www/common/cryptpad-common.js +++ b/www/common/cryptpad-common.js @@ -838,7 +838,6 @@ define([ postMessage('GET_PAD_METADATA', data, cb); }; - // XXX Teams: change the password of a pad owned by the team common.changePadPassword = function (Crypt, Crypto, data, cb) { var href = data.href; var newPassword = data.password; diff --git a/www/teams/main.js b/www/teams/main.js index 5c66c573e..d37de141c 100644 --- a/www/teams/main.js +++ b/www/teams/main.js @@ -36,31 +36,7 @@ define([ }; window.addEventListener('message', onMsg); }).nThen(function (/*waitFor*/) { - var teamId; // XXX - var afterSecrets = function (Cryptpad, Utils, secret, cb) { - return void cb(); - /* - var hash = window.location.hash.slice(1); - if (hash && Utils.LocalStore.isLoggedIn()) { - return; // XXX How to add a shared folder? - // Add a shared folder! - Cryptpad.addSharedFolder(teamId, secret, function (id) { - window.CryptPad_newSharedFolder = id; - cb(); - }); - return; - } else if (hash) { - var id = Utils.Util.createRandomInteger(); - window.CryptPad_newSharedFolder = id; - var data = { - href: Utils.Hash.getRelativeHref(window.location.href), - password: secret.password - }; - return void Cryptpad.loadSharedFolder(id, data, cb); - } - cb(); - */ - }; + var teamId; var addRpc = function (sframeChan, Cryptpad) { sframeChan.on('Q_SET_TEAM', function (data, cb) { teamId = data; @@ -72,11 +48,6 @@ define([ data.teamId = teamId; Cryptpad.userObjectCommand(data, cb); }); - // XXX no drive restore in teams? you could restore old keys... - /*sframeChan.on('Q_DRIVE_RESTORE', function (data, cb) { - data.teamId = teamId; - Cryptpad.restoreDrive(data, cb); - });*/ sframeChan.on('Q_DRIVE_GETOBJECT', function (data, cb) { if (!teamId) { return void cb({error: 'EINVAL'}); } if (data && data.sharedFolder) { @@ -109,7 +80,6 @@ define([ }); }; SFCommonO.start({ - afterSecrets: afterSecrets, noHash: true, noRealtime: true, //driveEvents: true, From 3fb0cc38ecc8cd1918cbacd52182648b7f1f21f8 Mon Sep 17 00:00:00 2001 From: yflory Date: Fri, 27 Sep 2019 18:04:48 +0200 Subject: [PATCH 3/9] Delete team --- www/common/outer/team.js | 86 +++++++++++++++++++++++++++++++++++-- www/common/proxy-manager.js | 2 +- www/teams/inner.js | 38 +++++++++++++++- 3 files changed, 120 insertions(+), 6 deletions(-) diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 810e0b3c0..d382a7609 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -15,10 +15,11 @@ define([ '/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad/chainpad.dist.js', '/bower_components/nthen/index.js', + '/bower_components/saferphore/index.js', '/bower_components/tweetnacl/nacl-fast.min.js', ], function (Util, Hash, Constants, Realtime, ProxyManager, UserObject, SF, Roster, Messaging, - Listmap, Crypto, CpNetflux, ChainPad, nThen) { + Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) { var Team = {}; var Nacl = window.nacl; @@ -73,8 +74,8 @@ define([ var closeTeam = function (ctx, teamId) { var team = ctx.teams[teamId]; if (!team) { return; } - team.listmap.stop(); - team.roster.stop(); + try { team.listmap.stop(); } catch (e) {} + try { team.roster.stop(); } catch (e) {} team.proxy = {}; delete ctx.teams[teamId]; delete ctx.store.proxy.teams[teamId]; @@ -509,6 +510,82 @@ define([ }); }; + var deleteTeam = function (ctx, data, cId, cb) { + var teamId = data.teamId; + if (!teamId) { return void cb({error: 'EINVAL'}); } + var team = ctx.teams[teamId]; + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!team || !teamData) { return void cb ({error: 'ENOENT'}); } + var state = team.roster.getState(); + var curvePublic = Util.find(ctx, ['store', 'proxy', 'curvePublic']); + var me = state.members[curvePublic]; + if (!me || me.role !== "OWNER") { return cb({ error: "EFORBIDDEN"}); } + + var edPublic = Util.find(ctx, ['store', 'proxy', 'edPublic']); + + nThen(function (waitFor) { + ctx.Store.anonRpcMsg(null, { + msg: 'GET_METADATA', + data: teamData.channel + }, waitFor(function (obj) { + // If we can't get owners, abort + if (obj && obj.error) { + waitFor.abort(); + return cb({ error: obj.error}); + } + // Check if we're an owner of the team drive + var metadata = obj[0]; + if (metadata && Array.isArray(metadata.owners) && + metadata.owners.indexOf(edPublic) !== -1) { return; } + // If w'e're not an owner, abort + waitFor.abort(); + cb({error: 'EFORBIDDEN'}); + })); + }).nThen(function (waitFor) { + team.proxy.delete = true; + // Delete the owned pads + var ownedPads = team.manager.getChannelsList('owned'); + var sem = Saferphore.create(10); + ownedPads.forEach(function (c) { + var w = waitFor(); + sem.take(function (give) { + team.rpc.removeOwnedChannel(c, give(function (err) { + if (err) { console.error(err); } + w(); + })); + }); + }); + }).nThen(function (waitFor) { + // Delete the pins log + team.rpc.removePins(waitFor(function (err) { + if (err) { console.error(err); } + console.error(err); + })); + // Delete the roster + var rosterChan = Util.find(teamData, ['keys', 'roster', 'channel']); + ctx.store.rpc.removeOwnedChannel(rosterChan, waitFor(function (err) { + if (err) { console.error(err); } + console.error(err); + })); + // Delete the chat + var chatChan = Util.find(teamData, ['keys', 'chat', 'channel']); + /* + ctx.store.rpc.removeOwnedChannel(chatChan, waitFor(function (err) { + if (err) { console.error(err); } + console.error(err); + })); + */ // XXX + // Delete the team drive + ctx.store.rpc.removeOwnedChannel(teamData.channel, waitFor(function (err) { + if (err) { console.error(err); } + console.error(err); + })); + }).nThen(function () { + cb(); + closeTeam(ctx, teamId); + }); + }; + var joinTeam = function (ctx, data, cId, cb) { var team = data.team; if (!team.hash || !team.channel || !team.password @@ -840,6 +917,9 @@ define([ if (cmd === 'REMOVE_USER') { return void removeUser(ctx, data, clientId, cb); } + if (cmd === 'DELETE_TEAM') { + return void deleteTeam(ctx, data, clientId, cb); + } if (cmd === 'CREATE_TEAM') { return void createTeam(ctx, data, clientId, cb); } diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js index ca914b57a..3908727c2 100644 --- a/www/common/proxy-manager.js +++ b/www/common/proxy-manager.js @@ -827,7 +827,7 @@ define([ // Don't push duplicates if (result.indexOf(data.channel) !== -1) { return; } // Return owned pads - if (_ownedByMe(Env, data.owners)) { + if (_ownedByMe(Env, data.owners) && data.owners.length === 1) { result.push(data.channel); } }; diff --git a/www/teams/inner.js b/www/teams/inner.js index e943dbf94..f7c319bc7 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -115,7 +115,8 @@ define([ ], 'admin': [ 'cp-team-name', - 'cp-team-avatar' + 'cp-team-avatar', + 'cp-team-delete', ], }; @@ -332,7 +333,7 @@ define([ var isOwner = Object.keys(privateData.teams || {}).some(function (id) { return privateData.teams[id].owner; - }); + }) && !privateData.devMode; // XXX if (Object.keys(privateData.teams || {}).length >= 3 || isOwner) { content.push(h('div.alert.alert-warning', { role:'alert' @@ -718,6 +719,39 @@ define([ }); }, true); + makeBlock('delete', function (common, cb) { // XXX makeBlock keys + var deleteTeam = h('button.btn.btn-danger', Messages.team_delete || "DELETE"); // XXX + var $ok = $('', {'class': 'fa fa-check', title: Messages.saved}).hide(); + var $spinner = $('', {'class': 'fa fa-spinner fa-pulse'}).hide(); + + var deleting = false; + $(deleteTeam).click(function () { + if (deleting) { return; } + UI.confirm("Are you sure", function (yes) { // XXX + if (deleting) { return; } + deleting = true; + $spinner.show(); + APP.module.execCommand("DELETE_TEAM", { + teamId: APP.team + }, function (obj) { + $spinner.hide(); + deleting = false + if (obj && obj.error) { + return void UI.warn(obj.error); + } + $ok.show(); + UI.log('DELETED'); // XXX + }); + }); + }); + + cb([ + deleteTeam, + $ok[0], + $spinner[0] + ]); + }, true); + var main = function () { var common; var readOnly; From 295a712942a04b3027e0403d5521e01236383041 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 30 Sep 2019 15:17:26 +0200 Subject: [PATCH 4/9] Transfer team ownership --- www/common/common-ui-elements.js | 128 ++++++++++++++- www/common/notifications.js | 3 + www/common/outer/mailbox-handlers.js | 21 ++- www/common/outer/team.js | 229 +++++++++++++++++++++++++-- www/teams/inner.js | 30 +++- 5 files changed, 389 insertions(+), 22 deletions(-) diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js index 03b0b534d..431b7e560 100644 --- a/www/common/common-ui-elements.js +++ b/www/common/common-ui-elements.js @@ -288,6 +288,7 @@ define([ } })); }).nThen(function (waitFor) { + // Add one of our teams as an owner if (toAddTeams.length) { // Send the command sframeChan.query('Q_SET_PAD_METADATA', { @@ -320,6 +321,7 @@ define([ })); } }).nThen(function (waitFor) { + // Offer ownership to a friend if (toAdd.length) { // Send the command sframeChan.query('Q_SET_PAD_METADATA', { @@ -1293,7 +1295,7 @@ define([ var team = privateData.teams[config.teamId]; if (!team) { return void UI.warn(Messages.error); } - var module = config.module || common.makeUniversal('team', { onEvent: function () {} }); + var module = config.module || common.makeUniversal('team'); var $div; var refreshButton = function () { @@ -3785,6 +3787,130 @@ define([ UI.proposal(div, todo); }; + UIElements.displayAddTeamOwnerModal = function (common, data) { + var priv = common.getMetadataMgr().getPrivateData(); + var user = common.getMetadataMgr().getUserData(); + var sframeChan = common.getSframeChannel(); + var msg = data.content.msg; + + var name = Util.fixHTML(msg.content.user.displayName) || Messages.anonymous; + var title = Util.fixHTML(msg.content.title); + + //var text = Messages._getKey('owner_team_add', [name, title]); // XXX + var text = name + ' wants you to be an owner of the team ' + title; // XXX + + var div = h('div', [ + UI.setHTML(h('p'), text), + ]); + + var answer = function (yes) { + common.mailbox.sendTo("ADD_OWNER_ANSWER", { + teamChannel: msg.content.teamChannel, + title: msg.content.title, + answer: yes, + user: { + displayName: user.name, + avatar: user.avatar, + profile: user.profile, + notifications: user.notifications, + curvePublic: user.curvePublic, + edPublic: priv.edPublic + } + }, { + channel: msg.content.user.notifications, + curvePublic: msg.content.user.curvePublic + }); + common.mailbox.dismiss(data, function (err) { + if (err) { console.log(err); } + }); + }; + var module = common.makeUniversal('team'); + + var addOwner = function (chan, waitFor, cb) { + // Remove yourself from the pending owners + sframeChan.query('Q_SET_PAD_METADATA', { + channel: chan, + command: 'ADD_OWNERS', + value: [priv.edPublic] + }, function (err, res) { + err = err || (res && res.error); + if (!err) { return; } + waitFor.abort(); + cb(err); + }); + }; + var removePending = function (chan, waitFor, cb) { + // Remove yourself from the pending owners + sframeChan.query('Q_SET_PAD_METADATA', { + channel: chan, + command: 'RM_PENDING_OWNERS', + value: [priv.edPublic] + }, waitFor(function (err, res) { + err = err || (res && res.error); + if (!err) { return; } + waitFor.abort(); + cb(err); + })); + }; + var changeAll = function (add, _cb) { + var f = add ? addOwner : removePending; + var cb = Util.once(_cb); + NThen(function (waitFor) { + f(msg.content.teamChannel, waitFor, cb); + f(msg.content.chatChannel, waitFor, cb); + f(msg.content.rosterChannel, waitFor, cb); + }).nThen(function () { cb(); }); + }; + + var todo = function (yes) { + if (yes) { + // ACCEPT + changeAll(true, function (err) { + if (err) { + console.error(err); + var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden + : Messages.error; + return void UI.warn(text); + } + UI.log(Messages.saved); + + // Send notification to the sender + answer(true); + + // Mark ourselves as "owner" in our local team data + module.execCommand("ANSWER_OWNERSHIP", { + teamChannel: msg.content.teamChannel, + answer: true + }, function (obj) { + if (obj && obj.error) { console.error(obj.error); } + }); + + // Remove yourself from the pending owners + changeAll(false, function (err) { + if (err) { console.error(err); } + }); + }); + return; + } + + // DECLINE + // Remove yourself from the pending owners + changeAll(false, function (err) { + if (err) { console.error(err); } + // Send notification to the sender + answer(false); + // Set our role back to ADMIN + module.execCommand("ANSWER_OWNERSHIP", { + teamChannel: msg.content.teamChannel, + answer: false + }, function (obj) { + if (obj && obj.error) { console.error(obj.error); } + }); + }); + }; + + UI.proposal(div, todo); + }; UIElements.getVerifiedFriend = function (common, curve, name) { var priv = common.getMetadataMgr().getPrivateData(); diff --git a/www/common/notifications.js b/www/common/notifications.js index c5a94c275..1222e8602 100644 --- a/www/common/notifications.js +++ b/www/common/notifications.js @@ -216,6 +216,9 @@ define([ // if not archived, add handlers if (!content.archived) { content.handler = function () { + if (msg.content.teamChannel) { + return void UIElements.displayAddTeamOwnerModal(common, data); + } UIElements.displayAddOwnerModal(common, data); }; } diff --git a/www/common/outer/mailbox-handlers.js b/www/common/outer/mailbox-handlers.js index d25cb283b..913f094cf 100644 --- a/www/common/outer/mailbox-handlers.js +++ b/www/common/outer/mailbox-handlers.js @@ -270,12 +270,12 @@ define([ var content = msg.content; if (msg.author !== content.user.curvePublic) { return void cb(true); } - if (!content.href || !content.title || !content.channel) { + if (!content.teamChannel && !(content.href && content.title && content.channel)) { console.log('Remove invalid notification'); return void cb(true); } - var channel = content.channel; + var channel = content.channel || content.teamChannel; if (addOwners[channel]) { return void cb(true); } addOwners[channel] = { @@ -286,7 +286,7 @@ define([ cb(false); }; removeHandlers['ADD_OWNER'] = function (ctx, box, data) { - var channel = data.content.channel; + var channel = data.content.channel || data.content.teamChannel; if (addOwners[channel]) { delete addOwners[channel]; } @@ -297,12 +297,23 @@ define([ var content = msg.content; if (msg.author !== content.user.curvePublic) { return void cb(true); } - if (!content.channel) { + if (!content.channel && !content.teamChannel) { console.log('Remove invalid notification'); return void cb(true); } - var channel = content.channel; + var channel = content.channel || content.teamChannel; + + // If our ownership rights for a team have been removed, update the owner flag + if (content.teamChannel) { + var teams = ctx.store.proxy.teams || {}; + Object.keys(teams).some(function (id) { + if (teams[id].channel === channel) { + teams[id].owner = false; + return true; + } + }); + } if (addOwners[channel] && content.pending) { return void cb(false, addOwners[channel]); diff --git a/www/common/outer/team.js b/www/common/outer/team.js index d382a7609..5512b782b 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -100,18 +100,6 @@ define([ if (membersChannel) { list.push(membersChannel); } if (mailboxChannel) { list.push(mailboxChannel); } - - - // XXX Add the team mailbox - /* - if (store.proxy.mailboxes) { - var mList = Object.keys(store.proxy.mailboxes).map(function (m) { - return store.proxy.mailboxes[m].channel; - }); - list = list.concat(mList); - } - */ - list.sort(); return list; }; @@ -186,7 +174,6 @@ define([ channel: secret.channel, secret: secret, validateKey: secret.keys.validateKey - // XXX owners: team owner + all admins? }; }; @@ -314,6 +301,11 @@ define([ userName: 'team', classic: true }; + cfg.onMetadataUpdate = function (md) { + var team = ctx.teams[id]; + if (!team) { return; } + ctx.emit('ROSTER_CHANGE', id, team.clients); + }; lm = Listmap.create(cfg); lm.proxy.on('ready', waitFor()); @@ -463,10 +455,14 @@ define([ } })); }).nThen(function () { + var id = Util.createRandomInteger(); + config.onMetadataUpdate = function (md) { + if (!team) { return; } + ctx.emit('ROSTER_CHANGE', id, team.clients); + }; var lm = Listmap.create(config); var proxy = lm.proxy; proxy.on('ready', function () { - var id = Util.createRandomInteger(); // Store keys in our drive var keys = { drive: { @@ -617,12 +613,43 @@ define([ var getTeamRoster = function (ctx, data, cId, cb) { var teamId = data.teamId; if (!teamId) { return void cb({error: 'EINVAL'}); } + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!teamData) { return void cb ({error: 'ENOENT'}); } var team = ctx.teams[teamId]; if (!team) { return void cb ({error: 'ENOENT'}); } if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } var state = team.roster.getState() || {}; var members = state.members || {}; + // Get pending owners + var md = team.listmap.metadata || {}; + if (Array.isArray(md.pending_owners)) { + // Get the members associated to the pending_owners' edPublic and mark them as such + md.pending_owners.forEach(function (ed) { + var member; + Object.keys(members).some(function (curve) { + if (members[curve].edPublic === ed) { + member = members[curve]; + return true; + } + }); + if ((!member || member.role !== 'OWNER') && teamData.owner) { + var removeOwnership = function (chan) { + ctx.Store.setPadMetadata(null, { + channel: chan, + command: 'RM_PENDING_OWNERS', + value: [ed], + }, function () {}); + }; + removeOwnership(teamData.channel); + removeOwnership(Util.find(teamData, ['keys', 'roster', 'channel'])); + removeOwnership(Util.find(teamData, ['keys', 'chat', 'channel'])); + return; + } + member.pendingOwner = true; + }); + } + // Add online status (using messenger data) var chatData = team.getChatData(); var online = ctx.store.messenger.getOnlineList(chatData.channel) || []; @@ -661,6 +688,159 @@ define([ }); }; + var offerOwnership = function (ctx, data, cId, _cb) { + var cb = Util.once(_cb); + var teamId = data.teamId; + if (!teamId) { return void cb({error: 'EINVAL'}); } + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!teamData) { return void cb ({error: 'ENOENT'}); } + var team = ctx.teams[teamId]; + if (!team) { return void cb ({error: 'ENOENT'}); } + if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } + if (!data.curvePublic) { return void cb({error: 'MISSING_DATA'}); } + var state = team.roster.getState(); + var user = state.members[data.curvePublic]; + nThen(function (waitFor) { + // Offer ownership to a friend + var onError = function (res) { + var err = res && res.error; + if (err) { + waitFor.abort(); + return void cb({error:err}); + } + }; + var addPendingOwner = function (chan) { + ctx.Store.setPadMetadata(null, { + channel: chan, + command: 'ADD_PENDING_OWNERS', + value: [user.edPublic], + }, waitFor(onError)); + }; + // Team proxy + addPendingOwner(teamData.channel); + // Team roster + addPendingOwner(Util.find(teamData, ['keys', 'roster', 'channel'])); + // Team chat + addPendingOwner(Util.find(teamData, ['keys', 'chat', 'channel'])); + }).nThen(function (waitFor) { + var obj = {}; + obj[user.curvePublic] = { + role: 'OWNER' + }; + team.roster.describe(obj, waitFor(function (err) { + if (err) { console.error(err); } + })); + }).nThen(function (waitFor) { + // Send mailbox to offer ownership + var myData = Messaging.createData(ctx.store.proxy, false); + ctx.store.mailbox.sendTo("ADD_OWNER", { + teamChannel: teamData.channel, + chatChannel: Util.find(teamData, ['keys', 'chat', 'channel']), + rosterChannel: Util.find(teamData, ['keys', 'roster', 'channel']), + title: teamData.metadata.name, + user: myData + }, { + channel: user.notifications, + curvePublic: user.curvePublic + }, waitFor()); + }).nThen(function () { + cb(); + }); + }; + + var revokeOwnership = function (ctx, teamId, user, _cb) { + var cb = Util.once(_cb); + if (!teamId) { return void cb({error: 'EINVAL'}); } + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!teamData) { return void cb ({error: 'ENOENT'}); } + var team = ctx.teams[teamId]; + if (!team) { return void cb ({error: 'ENOENT'}); } + var md = team.listmap.metadata || {}; + var isPendingOwner = (md.pending_owners || []).indexOf(user.edPublic) !== -1; + nThen(function (waitFor) { + var cmd = isPendingOwner ? 'RM_PENDING_OWNERS' : 'RM_OWNERS'; + + var onError = function (res) { + var err = res && res.error; + if (err) { + waitFor.abort(); + return void cb(err); + } + }; + var removeOwnership = function (chan) { + ctx.Store.setPadMetadata(null, { + channel: chan, + command: cmd, + value: [user.edPublic], + }, waitFor(onError)); + }; + // Team proxy + removeOwnership(teamData.channel); + // Team roster + removeOwnership(Util.find(teamData, ['keys', 'roster', 'channel'])); + // Team chat + removeOwnership(Util.find(teamData, ['keys', 'chat', 'channel'])); + }).nThen(function (waitFor) { + var obj = {}; + obj[user.curvePublic] = { + role: 'ADMIN', + pendingOwner: false + }; + team.roster.describe(obj, waitFor(function (err) { + if (err) { console.error(err); } + })); + }).nThen(function (waitFor) { + // Send mailbox to offer ownership + var myData = Messaging.createData(ctx.store.proxy, false); + ctx.store.mailbox.sendTo("RM_OWNER", { + teamChannel: teamData.channel, + title: teamData.metadata.name, + pending: isPendingOwner, + user: myData + }, { + channel: user.notifications, + curvePublic: user.curvePublic + }, waitFor()); + }).nThen(function () { + cb(); + }); + }; + + // We've received an offer to be an owner of the team. + // If we accept, we need to set the "owner" flag in our team data + // If we decline, we need to change our role back to "ADMIN" + var answerOwnership = function (ctx, data, cId, cb) { + var myTeams = ctx.store.proxy.teams; + var teamId; + Object.keys(myTeams).forEach(function (id) { + if (myTeams[id].channel === data.teamChannel) { + teamId = id; + return true; + } + }); + if (!teamId) { return void cb({error: 'EINVAL'}); } + var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]); + if (!teamData) { return void cb ({error: 'ENOENT'}); } + var team = ctx.teams[teamId]; + if (!team) { return void cb ({error: 'ENOENT'}); } + if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } + var obj = {}; + + // Accept + if (data.answer) { + teamData.owner = true; + return; + } + // Decline + obj[ctx.store.proxy.curvePublic] = { + role: 'ADMIN', + }; + team.roster.describe(obj, function (err) { + if (err) { return void cb({error: err}); } + cb(); + }); + }; + var describeUser = function (ctx, data, cId, cb) { var teamId = data.teamId; if (!teamId) { return void cb({error: 'EINVAL'}); } @@ -668,6 +848,21 @@ define([ if (!team) { return void cb ({error: 'ENOENT'}); } if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } if (!data.curvePublic || !data.data) { return void cb({error: 'MISSING_DATA'}); } + var state = team.roster.getState(); + var user = state.members[data.curvePublic]; + + // It it is an ownership revocation, we have to set it in pad metadata first + console.log(user.role, data.data.role); + if (user.role === "OWNER" && data.data.role !== "OWNER") { + revokeOwnership(ctx, teamId, user, function (err) { + console.error(err); + if (!err) { return; } + waitFor.abort(); + return void cb({error: err}); + }); + return; + } + var obj = {}; obj[data.curvePublic] = data.data; team.roster.describe(obj, function (err) { @@ -902,6 +1097,12 @@ define([ if (cmd === 'SET_TEAM_METADATA') { return void setTeamMetadata(ctx, data, clientId, cb); } + if (cmd === 'OFFER_OWNERSHIP') { + return void offerOwnership(ctx, data, clientId, cb); + } + if (cmd === 'ANSWER_OWNERSHIP') { + return void answerOwnership(ctx, data, clientId, cb); + } if (cmd === 'DESCRIBE_USER') { return void describeUser(ctx, data, clientId, cb); } diff --git a/www/teams/inner.js b/www/teams/inner.js index f7c319bc7..bb01e5e11 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -430,6 +430,9 @@ define([ common.displayAvatar($(avatar), data.avatar, data.displayName); // Name var name = h('span.cp-team-member-name', data.displayName); + if (data.pendingOwner) { + $(name).append(h('em', " PENDING")); + } // Status var status = h('span.cp-team-member-status'+(data.online ? '.online' : '')); // Actions @@ -438,6 +441,28 @@ define([ var isMe = me && me.curvePublic === data.curvePublic; var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1; var theirRole = ROLES.indexOf(data.role) || 0; + // If they're an admin and I am an owner, I can promote them to owner + if (!isMe && myRole > theirRole && theirRole === 1 && !data.pending) { + var promote = h('span.fa.fa-angle-double-up', { + title: "Offer ownership" // XXX + }); + $(promote).click(function () { + $(promote).hide(); + UI.confirm("Are you sure???", function (yes) { // XXX + APP.module.execCommand('OFFER_OWNERSHIP', { + teamId: APP.team, + curvePublic: data.curvePublic + }, function (obj) { + if (obj && obj.error) { + console.error(obj.error); + return void UI.warn(Messages.error); + } + UI.log("DONE"); // XXX + }); + }); + }); + $actions.append(promote); + } // If they're a member and I have a higher role than them, I can promote them to admin if (!isMe && myRole > theirRole && theirRole === 0 && !data.pending) { var promote = h('span.fa.fa-angle-double-up', { @@ -467,7 +492,8 @@ define([ $actions.append(demote); } // If I'm not a member and I have an equal or higher role than them, I can remove them - if (!isMe && myRole > 0 && myRole >= theirRole) { + // Note: we can't remove owners, we have to demote them first + if (!isMe && myRole > 0 && myRole >= theirRole && theirRole !== 2) { var remove = h('span.fa.fa-times', { title: Messages.team_rosterKick }); @@ -514,7 +540,7 @@ define([ var me = roster[userData.curvePublic] || {}; var owner = Object.keys(roster).filter(function (k) { if (roster[k].pending) { return; } - return roster[k].role === "OWNER"; + return roster[k].role === "OWNER" || roster[k].pendingOwner; }).map(function (k) { return makeMember(common, roster[k], me); }); From 7b4a72b3a224b493e8eb3fdec929c89a3f5a28dc Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 30 Sep 2019 15:20:44 +0200 Subject: [PATCH 5/9] lint compliance --- www/common/outer/team.js | 9 ++++----- www/teams/inner.js | 12 +++++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 5512b782b..1c3c29a70 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -301,7 +301,7 @@ define([ userName: 'team', classic: true }; - cfg.onMetadataUpdate = function (md) { + cfg.onMetadataUpdate = function () { var team = ctx.teams[id]; if (!team) { return; } ctx.emit('ROSTER_CHANGE', id, team.clients); @@ -456,7 +456,8 @@ define([ })); }).nThen(function () { var id = Util.createRandomInteger(); - config.onMetadataUpdate = function (md) { + config.onMetadataUpdate = function () { + var team = ctx.teams[id]; if (!team) { return; } ctx.emit('ROSTER_CHANGE', id, team.clients); }; @@ -852,12 +853,10 @@ define([ var user = state.members[data.curvePublic]; // It it is an ownership revocation, we have to set it in pad metadata first - console.log(user.role, data.data.role); if (user.role === "OWNER" && data.data.role !== "OWNER") { revokeOwnership(ctx, teamId, user, function (err) { - console.error(err); if (!err) { return; } - waitFor.abort(); + console.error(err); return void cb({error: err}); }); return; diff --git a/www/teams/inner.js b/www/teams/inner.js index bb01e5e11..8f858a751 100644 --- a/www/teams/inner.js +++ b/www/teams/inner.js @@ -443,12 +443,13 @@ define([ var theirRole = ROLES.indexOf(data.role) || 0; // If they're an admin and I am an owner, I can promote them to owner if (!isMe && myRole > theirRole && theirRole === 1 && !data.pending) { - var promote = h('span.fa.fa-angle-double-up', { + var promoteOwner = h('span.fa.fa-angle-double-up', { title: "Offer ownership" // XXX }); - $(promote).click(function () { - $(promote).hide(); + $(promoteOwner).click(function () { + $(promoteOwner).hide(); UI.confirm("Are you sure???", function (yes) { // XXX + if (!yes) { return; } APP.module.execCommand('OFFER_OWNERSHIP', { teamId: APP.team, curvePublic: data.curvePublic @@ -461,7 +462,7 @@ define([ }); }); }); - $actions.append(promote); + $actions.append(promoteOwner); } // If they're a member and I have a higher role than them, I can promote them to admin if (!isMe && myRole > theirRole && theirRole === 0 && !data.pending) { @@ -754,6 +755,7 @@ define([ $(deleteTeam).click(function () { if (deleting) { return; } UI.confirm("Are you sure", function (yes) { // XXX + if (!yes) { return; } if (deleting) { return; } deleting = true; $spinner.show(); @@ -761,7 +763,7 @@ define([ teamId: APP.team }, function (obj) { $spinner.hide(); - deleting = false + deleting = false; if (obj && obj.error) { return void UI.warn(obj.error); } From 1288f2893183b7def2e46cffce3a5cdd7711d064 Mon Sep 17 00:00:00 2001 From: yflory Date: Mon, 30 Sep 2019 15:49:02 +0200 Subject: [PATCH 6/9] Team deletion --- www/common/outer/team.js | 53 ++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/www/common/outer/team.js b/www/common/outer/team.js index 1c3c29a70..78e0d74ac 100644 --- a/www/common/outer/team.js +++ b/www/common/outer/team.js @@ -278,7 +278,9 @@ define([ }; - var openChannel = function (ctx, teamData, id, cb) { + var openChannel = function (ctx, teamData, id, _cb) { + var cb = Util.once(_cb); + var secret = Hash.getSecrets('team', teamData.hash, teamData.password); var crypto = Crypto.createEncryptor(secret.keys); @@ -286,7 +288,34 @@ define([ var roster; var lm; + + // Roster keys + var myKeys = { + curvePublic: ctx.store.proxy.curvePublic, + curvePrivate: ctx.store.proxy.curvePrivate + }; + var rosterData = keys.roster || {}; + var rosterKeys = rosterData.edit ? Crypto.Team.deriveMemberKeys(rosterData.edit, myKeys) + : Crypto.Team.deriveGuestKeys(rosterData.view || ''); + nThen(function (waitFor) { + ctx.store.anon_rpc.send("IS_NEW_CHANNEL", secret.channel, waitFor(function (e, response) { + if (response && response.length && typeof(response[0]) === 'boolean' && response[0]) { + // Channel is empty: remove this team + delete ctx.store.proxy.teams[id]; + waitFor.abort(); + cb({error: 'ENOENT'}); + } + })); + ctx.store.anon_rpc.send("IS_NEW_CHANNEL", rosterKeys.channel, waitFor(function (e, response) { + if (response && response.length && typeof(response[0]) === 'boolean' && response[0]) { + // Channel is empty: remove this team + delete ctx.store.proxy.teams[id]; + waitFor.abort(); + cb({error: 'ENOENT'}); + } + })); + }).nThen(function (waitFor) { // Load the proxy var cfg = { data: {}, @@ -308,15 +337,18 @@ define([ }; lm = Listmap.create(cfg); lm.proxy.on('ready', waitFor()); + lm.proxy.on('error', function (info) { + if (info && typeof (info.loaded) !== "undefined" && !info.loaded) { + cb({error:'ECONNECT'}); + } + if (info && info.error) { + if (info.error === "EDELETED" ) { + closeTeam(ctx, id); + } + } + }); // Load the roster - var myKeys = { - curvePublic: ctx.store.proxy.curvePublic, - curvePrivate: ctx.store.proxy.curvePrivate - }; - var rosterData = keys.roster || {}; - var rosterKeys = rosterData.edit ? Crypto.Team.deriveMemberKeys(rosterData.edit, myKeys) - : Crypto.Team.deriveGuestKeys(rosterData.view || ''); Roster.create({ network: ctx.store.network, channel: rosterKeys.channel, @@ -503,6 +535,11 @@ define([ if (info && typeof (info.loaded) !== "undefined" && !info.loaded) { cb({error:'ECONNECT'}); } + if (info && info.error) { + if (info.error === "EDELETED") { + closeTeam(ctx, id); + } + } }); }); }; From 7e1beeb4e92ede3da12f7725b3c1fa7d6acffc1d Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 30 Sep 2019 15:01:50 +0000 Subject: [PATCH 7/9] Translated using Weblate (English) Currently translated at 100.0% (1118 of 1118 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1118 of 1118 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1117 of 1117 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1116 of 1116 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1115 of 1115 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1114 of 1114 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1113 of 1113 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1112 of 1112 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1111 of 1111 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1110 of 1110 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1109 of 1109 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ Translated using Weblate (English) Currently translated at 100.0% (1108 of 1108 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/en/ --- www/common/translations/messages.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json index ed753d9d5..ed85b6752 100644 --- a/www/common/translations/messages.json +++ b/www/common/translations/messages.json @@ -42,7 +42,7 @@ "error": "Error", "saved": "Saved", "synced": "Everything is saved", - "deleted": "Pad deleted from your CryptDrive", + "deleted": "Deleted", "deletedFromServer": "Pad deleted from the server", "mustLogin": "You must be logged in to access this page", "disabledApp": "This application has been disabled. Contact the administrator of this CryptPad for more information.", @@ -1201,5 +1201,16 @@ "team_maxOwner": "Each user account is restricted to owning a single team.", "team_maxTeams": "Each user account can only be a member of {0} teams.", "team_listTitle": "Your teams", - "team_listSlot": "Available team slot" + "team_listSlot": "Available team slot", + "owner_addTeamText": "...or a team", + "owner_team_add": "{0} wants you to be an owner of the team {1}. Do you accept?", + "team_rosterPromoteOwner": "Offer ownership", + "team_ownerConfirm": "Co-owners can modify or delete the team and remove you as an owner. Are you sure?", + "team_kickConfirm": "{0} will know that you removed them from the team. Are you sure?", + "sent": "Message sent", + "team_pending": "Invited", + "team_deleteTitle": "Team deletion", + "team_deleteHint": "Delete the team and all documents owned exclusively by the team.", + "team_deleteButton": "Delete", + "team_deleteConfirm": "You are about to delete all of an entire team's data. This may impact other team members access to their data. This cannot be undone. Are you sure you want to proceed?" } From 794eba0fed3ee82d5305f54da69680cfe36692c2 Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 30 Sep 2019 15:01:50 +0000 Subject: [PATCH 8/9] Translated using Weblate (French) Currently translated at 100.0% (1118 of 1118 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/fr/ --- www/common/translations/messages.fr.json | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json index ea32345ad..561572adc 100644 --- a/www/common/translations/messages.fr.json +++ b/www/common/translations/messages.fr.json @@ -40,7 +40,7 @@ "error": "Erreur", "saved": "Enregistré", "synced": "Tout est enregistré", - "deleted": "Pad supprimé de votre CryptDrive", + "deleted": "Supprimé", "deletedFromServer": "Pad supprimé du serveur", "mustLogin": "Vous devez être enregistré pour avoir accès à cette page", "disabledApp": "Cette application a été désactivée. Pour plus d'information, veuillez contacter l'administrateur de ce CryptPad.", @@ -1201,5 +1201,16 @@ "team_maxOwner": "Chaque compte utilisateur ne peut être propriétaire que d'une seule équipe.", "team_maxTeams": "Chaque compte utilisateur ne peut être membre que de {0} équipes.", "team_listTitle": "Vos équipes", - "team_listSlot": "Emplacement d'équipe disponible" + "team_listSlot": "Emplacement d'équipe disponible", + "owner_addTeamText": "...ou à une équipe", + "owner_team_add": "{0} souhaite que vous soyez propriétaire de l'équipe {1}. Acceptez-vous ?", + "team_rosterPromoteOwner": "Proposer d'être propriétaire", + "team_ownerConfirm": "Les co-propriétaires seront en mesure de modifier ou supprimer l'équipe et pourront supprimer vos droits de propriétaire. Continuer ?", + "team_kickConfirm": "{0} sera informé que vous l'avez expulsé de l'équipe. Êtes-vous sûr ?", + "sent": "Message envoyé", + "team_pending": "Invité", + "team_deleteTitle": "Suppression de l'équipe", + "team_deleteHint": "Supprimer l'équipe et tous les documents dont elle est exclusivement propriétaire.", + "team_deleteButton": "Supprimer", + "team_deleteConfirm": "Vous êtes sur le point de supprimer les données d'une équipe entière. Cette action peut impacter l'accès à leur données pour d'autres membres de l'équipe. La suppression est irréversible. Êtes-vous sûr de vouloir continuer ?" } From 1fd8e2a08fd16ef2817457b0ab6df187fad5f557 Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 30 Sep 2019 15:01:50 +0000 Subject: [PATCH 9/9] Translated using Weblate (German) Currently translated at 100.0% (1107 of 1107 strings) Translation: CryptPad/App Translate-URL: http://weblate.cryptpad.fr/projects/cryptpad/app/de/ --- www/common/translations/messages.de.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/www/common/translations/messages.de.json b/www/common/translations/messages.de.json index 7aabe4605..b0a44067e 100644 --- a/www/common/translations/messages.de.json +++ b/www/common/translations/messages.de.json @@ -1197,5 +1197,9 @@ "team_nameHint": "Name des Teams festlegen", "team_avatarTitle": "Teamavatar", "team_avatarHint": "Maximale Größe ist 500 KB (png, jpg, jpeg, gif)", - "team_infoContent": "Jedes Team hat eigene CryptDrives, Speicherplatzbegrenzungen, Chats und Mitgliederlisten. Eigentümer können das gesamte Team löschen. Admins können Mitglieder einladen oder entfernen. Mitglieder können das Team verlassen." + "team_infoContent": "Jedes Team hat eigene CryptDrives, Speicherplatzbegrenzungen, Chats und Mitgliederlisten. Eigentümer können das gesamte Team löschen. Admins können Mitglieder einladen oder entfernen. Mitglieder können das Team verlassen.", + "team_maxOwner": "Jeder Benutzer kann nur Eigentümer eines Teams sein.", + "team_maxTeams": "Jeder Benutzer kann nur Mitglied von {0} Teams sein.", + "team_listTitle": "Deine Teams", + "team_listSlot": "Verfügbare Teamplätze" }