Merge branch 'staging' of github.com:xwiki-labs/cryptpad into staging

pull/1/head
ansuz 5 years ago
commit a725f69ee7

@ -104,6 +104,8 @@ define([
var channel = data.channel; var channel = data.channel;
var owners = data.owners || []; var owners = data.owners || [];
var pending_owners = data.pending_owners || []; var pending_owners = data.pending_owners || [];
var teams = priv.teams;
var teamOwner = data.teamId;
var redrawAll = function () {}; var redrawAll = function () {};
@ -124,6 +126,12 @@ define([
return true; return true;
} }
}); });
Object.keys(teams).some(function (id) {
if (teams[id].edPublic === ed) {
f = teams[id];
f.teamId = id;
}
});
if (ed === edPublic) { if (ed === edPublic) {
f = f || user; f = f || user;
if (f.name) { f.edPublic = edPublic; } if (f.name) { f.edPublic = edPublic; }
@ -155,6 +163,7 @@ define([
var toRemove = sel.map(function (el) { var toRemove = sel.map(function (el) {
var ed = $(el).attr('data-ed'); var ed = $(el).attr('data-ed');
if (!ed) { return; } if (!ed) { return; }
if (teamOwner && teams[teamOwner] && teams[teamOwner].edPublic === ed) { me = true; }
if (ed === edPublic) { me = true; } if (ed === edPublic) { me = true; }
return ed; return ed;
}).filter(function (x) { return x; }); }).filter(function (x) { return x; });
@ -171,7 +180,8 @@ define([
sframeChan.query('Q_SET_PAD_METADATA', { sframeChan.query('Q_SET_PAD_METADATA', {
channel: channel, channel: channel,
command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS', command: pending ? 'RM_PENDING_OWNERS' : 'RM_OWNERS',
value: toRemove value: toRemove,
teamId: teamOwner
}, waitFor(function (err, res) { }, waitFor(function (err, res) {
err = err || (res && res.error); err = err || (res && res.error);
if (err) { if (err) {
@ -214,6 +224,7 @@ define([
// Add owners column // Add owners column
var drawAdd = function () { var drawAdd = function () {
var $div = $(h('div.cp-share-column'));
var _friends = JSON.parse(JSON.stringify(friends)); var _friends = JSON.parse(JSON.stringify(friends));
Object.keys(_friends).forEach(function (curve) { Object.keys(_friends).forEach(function (curve) {
if (owners.indexOf(_friends[curve].edPublic) !== -1 || if (owners.indexOf(_friends[curve].edPublic) !== -1 ||
@ -228,16 +239,44 @@ define([
}, function () { }, function () {
//console.log(arguments); //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. // When clicking on the add button, we get the selected users.
var addButton = h('button.no-margin', Messages.owner_addButton); var addButton = h('button.no-margin', Messages.owner_addButton);
$(addButton).click(function () { $(addButton).click(function () {
// Check selection // Check selection
var $sel = $div2.find('.cp-usergrid-user.cp-selected'); var $sel = $div.find('.cp-usergrid-user.cp-selected');
var sel = $sel.toArray(); var sel = $sel.toArray();
if (!sel.length) { return; } if (!sel.length) { return; }
var toAdd = sel.map(function (el) { 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; }); }).filter(function (x) { return x; });
NThen(function (waitFor) { NThen(function (waitFor) {
@ -249,21 +288,58 @@ define([
} }
})); }));
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
// Send the command // Add one of our teams as an owner
sframeChan.query('Q_SET_PAD_METADATA', { if (toAddTeams.length) {
channel: channel, // Send the command
command: 'ADD_PENDING_OWNERS', sframeChan.query('Q_SET_PAD_METADATA', {
value: toAdd channel: channel,
}, waitFor(function (err, res) { command: 'ADD_OWNERS',
err = err || (res && res.error); value: toAddTeams.map(function (obj) { return obj.edPublic; }),
if (err) { teamId: teamOwner
waitFor.abort(); }, waitFor(function (err, res) {
redrawAll(); err = err || (res && res.error);
var text = err === "INSUFFICIENT_PERMISSIONS" ? Messages.fm_forbidden if (err) {
: Messages.error; waitFor.abort();
return void UI.warn(text); redrawAll();
} var text = err === "INSUFFICIENT_PERMISSIONS" ?
})); Messages.fm_forbidden : Messages.error;
return void UI.warn(text);
}
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) {
// Offer ownership to a friend
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) { }).nThen(function (waitFor) {
sel.forEach(function (el) { sel.forEach(function (el) {
var friend = friends[$(el).attr('data-curve')]; var friend = friends[$(el).attr('data-curve')];
@ -291,8 +367,8 @@ define([
UI.log(Messages.saved); UI.log(Messages.saved);
}); });
}); });
$div2.append(h('p', addButton)); $div.append(h('p', addButton));
return $div2; return $div;
}; };
redrawAll = function (md) { redrawAll = function (md) {
@ -430,10 +506,10 @@ define([
if (data.href || data.roHref) { if (data.href || data.roHref) {
parsed = Hash.parsePadUrl(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') { if (owned && data.roHref && parsed.type !== 'drive' && parsed.hashData.type === 'pad') {
var manageOwners = h('button.no-margin', Messages.owner_openModalButton); var manageOwners = h('button.no-margin', Messages.owner_openModalButton);
$(manageOwners).click(function () { $(manageOwners).click(function () {
data.teamId = typeof(owned) !== "boolean" ? owned : undefined;
var modal = createOwnerModal(common, data); var modal = createOwnerModal(common, data);
UI.openCustomModal(modal, { UI.openCustomModal(modal, {
wide: true, wide: true,
@ -665,6 +741,7 @@ define([
UIElements.displayAvatar(common, $(avatar), data.avatar, name); UIElements.displayAvatar(common, $(avatar), data.avatar, name);
return h('div.cp-usergrid-user'+(data.selected?'.cp-selected':'')+(config.large?'.large':''), { return h('div.cp-usergrid-user'+(data.selected?'.cp-selected':'')+(config.large?'.large':''), {
'data-ed': data.edPublic, 'data-ed': data.edPublic,
'data-teamid': data.teamId,
'data-curve': data.curvePublic || '', 'data-curve': data.curvePublic || '',
'data-name': name.toLowerCase(), 'data-name': name.toLowerCase(),
'data-order': i, 'data-order': i,
@ -1218,7 +1295,7 @@ define([
var team = privateData.teams[config.teamId]; var team = privateData.teams[config.teamId];
if (!team) { return void UI.warn(Messages.error); } 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 $div;
var refreshButton = function () { var refreshButton = function () {
@ -3710,6 +3787,130 @@ define([
UI.proposal(div, todo); 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) { UIElements.getVerifiedFriend = function (common, curve, name) {
var priv = common.getMetadataMgr().getPrivateData(); var priv = common.getMetadataMgr().getPrivateData();

@ -838,7 +838,6 @@ define([
postMessage('GET_PAD_METADATA', data, cb); 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) { common.changePadPassword = function (Crypt, Crypto, data, cb) {
var href = data.href; var href = data.href;
var newPassword = data.password; var newPassword = data.password;

@ -3729,6 +3729,10 @@ define([
data.roHref = base + data.roHref; data.roHref = base + data.roHref;
} }
if (currentPath[0] === TEMPLATE) {
data.isTemplate = true;
}
if (manager.isSharedFolder(el)) { if (manager.isSharedFolder(el)) {
delete data.roHref; delete data.roHref;
//data.noPassword = true; //data.noPassword = true;

@ -216,6 +216,9 @@ define([
// if not archived, add handlers // if not archived, add handlers
if (!content.archived) { if (!content.archived) {
content.handler = function () { content.handler = function () {
if (msg.content.teamChannel) {
return void UIElements.displayAddTeamOwnerModal(common, data);
}
UIElements.displayAddOwnerModal(common, data); UIElements.displayAddOwnerModal(common, data);
}; };
} }

@ -1552,7 +1552,6 @@ define([
var href, title; var href, title;
// XXX TEAMOWNER
if (!res.some(function (obj) { if (!res.some(function (obj) {
if (obj.data && if (obj.data &&
Array.isArray(obj.data.owners) && obj.data.owners.indexOf(edPublic) !== -1 && Array.isArray(obj.data.owners) && obj.data.owners.indexOf(edPublic) !== -1 &&
@ -1612,11 +1611,8 @@ define([
Store.setPadMetadata = function (clientId, data, cb) { Store.setPadMetadata = function (clientId, data, cb) {
if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); } if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); }
if (!data.command) { return void cb({ error: 'EINVAL' }); } if (!data.command) { return void cb({ error: 'EINVAL' }); }
// XXX TEAMOWNER var s = getStore(data.teamId);
// If owned by a team, we should use the team rpc here s.rpc.setMetadata(data, function (err, res) {
// 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) {
if (err) { return void cb({ error: err }); } if (err) { return void cb({ error: err }); }
if (!Array.isArray(res) || !res.length) { return void cb({}); } if (!Array.isArray(res) || !res.length) { return void cb({}); }
cb(res[0]); cb(res[0]);

@ -270,12 +270,12 @@ define([
var content = msg.content; var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); } 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'); console.log('Remove invalid notification');
return void cb(true); return void cb(true);
} }
var channel = content.channel; var channel = content.channel || content.teamChannel;
if (addOwners[channel]) { return void cb(true); } if (addOwners[channel]) { return void cb(true); }
addOwners[channel] = { addOwners[channel] = {
@ -286,7 +286,7 @@ define([
cb(false); cb(false);
}; };
removeHandlers['ADD_OWNER'] = function (ctx, box, data) { removeHandlers['ADD_OWNER'] = function (ctx, box, data) {
var channel = data.content.channel; var channel = data.content.channel || data.content.teamChannel;
if (addOwners[channel]) { if (addOwners[channel]) {
delete addOwners[channel]; delete addOwners[channel];
} }
@ -297,12 +297,23 @@ define([
var content = msg.content; var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); } if (msg.author !== content.user.curvePublic) { return void cb(true); }
if (!content.channel) { if (!content.channel && !content.teamChannel) {
console.log('Remove invalid notification'); console.log('Remove invalid notification');
return void cb(true); 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) { if (addOwners[channel] && content.pending) {
return void cb(false, addOwners[channel]); return void cb(false, addOwners[channel]);

@ -15,10 +15,11 @@ define([
'/bower_components/chainpad-netflux/chainpad-netflux.js', '/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/chainpad/chainpad.dist.js', '/bower_components/chainpad/chainpad.dist.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
'/bower_components/saferphore/index.js',
'/bower_components/tweetnacl/nacl-fast.min.js', '/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Hash, Constants, Realtime, ], function (Util, Hash, Constants, Realtime,
ProxyManager, UserObject, SF, Roster, Messaging, ProxyManager, UserObject, SF, Roster, Messaging,
Listmap, Crypto, CpNetflux, ChainPad, nThen) { Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) {
var Team = {}; var Team = {};
var Nacl = window.nacl; var Nacl = window.nacl;
@ -73,8 +74,8 @@ define([
var closeTeam = function (ctx, teamId) { var closeTeam = function (ctx, teamId) {
var team = ctx.teams[teamId]; var team = ctx.teams[teamId];
if (!team) { return; } if (!team) { return; }
team.listmap.stop(); try { team.listmap.stop(); } catch (e) {}
team.roster.stop(); try { team.roster.stop(); } catch (e) {}
team.proxy = {}; team.proxy = {};
delete ctx.teams[teamId]; delete ctx.teams[teamId];
delete ctx.store.proxy.teams[teamId]; delete ctx.store.proxy.teams[teamId];
@ -99,18 +100,6 @@ define([
if (membersChannel) { list.push(membersChannel); } if (membersChannel) { list.push(membersChannel); }
if (mailboxChannel) { list.push(mailboxChannel); } 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(); list.sort();
return list; return list;
}; };
@ -185,7 +174,6 @@ define([
channel: secret.channel, channel: secret.channel,
secret: secret, secret: secret,
validateKey: secret.keys.validateKey validateKey: secret.keys.validateKey
// XXX owners: team owner + all admins?
}; };
}; };
@ -290,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 secret = Hash.getSecrets('team', teamData.hash, teamData.password);
var crypto = Crypto.createEncryptor(secret.keys); var crypto = Crypto.createEncryptor(secret.keys);
@ -298,7 +288,34 @@ define([
var roster; var roster;
var lm; 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) { 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 // Load the proxy
var cfg = { var cfg = {
data: {}, data: {},
@ -313,17 +330,25 @@ define([
userName: 'team', userName: 'team',
classic: true classic: true
}; };
cfg.onMetadataUpdate = function () {
var team = ctx.teams[id];
if (!team) { return; }
ctx.emit('ROSTER_CHANGE', id, team.clients);
};
lm = Listmap.create(cfg); lm = Listmap.create(cfg);
lm.proxy.on('ready', waitFor()); 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 // 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({ Roster.create({
network: ctx.store.network, network: ctx.store.network,
channel: rosterKeys.channel, channel: rosterKeys.channel,
@ -462,10 +487,15 @@ define([
} }
})); }));
}).nThen(function () { }).nThen(function () {
var id = Util.createRandomInteger();
config.onMetadataUpdate = function () {
var team = ctx.teams[id];
if (!team) { return; }
ctx.emit('ROSTER_CHANGE', id, team.clients);
};
var lm = Listmap.create(config); var lm = Listmap.create(config);
var proxy = lm.proxy; var proxy = lm.proxy;
proxy.on('ready', function () { proxy.on('ready', function () {
var id = Util.createRandomInteger();
// Store keys in our drive // Store keys in our drive
var keys = { var keys = {
drive: { drive: {
@ -505,10 +535,91 @@ define([
if (info && typeof (info.loaded) !== "undefined" && !info.loaded) { if (info && typeof (info.loaded) !== "undefined" && !info.loaded) {
cb({error:'ECONNECT'}); cb({error:'ECONNECT'});
} }
if (info && info.error) {
if (info.error === "EDELETED") {
closeTeam(ctx, id);
}
}
}); });
}); });
}; };
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 joinTeam = function (ctx, data, cId, cb) {
var team = data.team; var team = data.team;
if (!team.hash || !team.channel || !team.password if (!team.hash || !team.channel || !team.password
@ -540,12 +651,43 @@ define([
var getTeamRoster = function (ctx, data, cId, cb) { var getTeamRoster = function (ctx, data, cId, cb) {
var teamId = data.teamId; var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); } 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]; var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); } if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
var state = team.roster.getState() || {}; var state = team.roster.getState() || {};
var members = state.members || {}; 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) // Add online status (using messenger data)
var chatData = team.getChatData(); var chatData = team.getChatData();
var online = ctx.store.messenger.getOnlineList(chatData.channel) || []; var online = ctx.store.messenger.getOnlineList(chatData.channel) || [];
@ -584,6 +726,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 describeUser = function (ctx, data, cId, cb) {
var teamId = data.teamId; var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); } if (!teamId) { return void cb({error: 'EINVAL'}); }
@ -591,6 +886,19 @@ define([
if (!team) { return void cb ({error: 'ENOENT'}); } if (!team) { return void cb ({error: 'ENOENT'}); }
if (!team.roster) { return void cb({error: 'NO_ROSTER'}); } if (!team.roster) { return void cb({error: 'NO_ROSTER'}); }
if (!data.curvePublic || !data.data) { return void cb({error: 'MISSING_DATA'}); } 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
if (user.role === "OWNER" && data.data.role !== "OWNER") {
revokeOwnership(ctx, teamId, user, function (err) {
if (!err) { return; }
console.error(err);
return void cb({error: err});
});
return;
}
var obj = {}; var obj = {};
obj[data.curvePublic] = data.data; obj[data.curvePublic] = data.data;
team.roster.describe(obj, function (err) { team.roster.describe(obj, function (err) {
@ -825,6 +1133,12 @@ define([
if (cmd === 'SET_TEAM_METADATA') { if (cmd === 'SET_TEAM_METADATA') {
return void setTeamMetadata(ctx, data, clientId, cb); 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') { if (cmd === 'DESCRIBE_USER') {
return void describeUser(ctx, data, clientId, cb); return void describeUser(ctx, data, clientId, cb);
} }
@ -840,6 +1154,9 @@ define([
if (cmd === 'REMOVE_USER') { if (cmd === 'REMOVE_USER') {
return void removeUser(ctx, data, clientId, cb); return void removeUser(ctx, data, clientId, cb);
} }
if (cmd === 'DELETE_TEAM') {
return void deleteTeam(ctx, data, clientId, cb);
}
if (cmd === 'CREATE_TEAM') { if (cmd === 'CREATE_TEAM') {
return void createTeam(ctx, data, clientId, cb); return void createTeam(ctx, data, clientId, cb);
} }

@ -827,7 +827,7 @@ define([
// Don't push duplicates // Don't push duplicates
if (result.indexOf(data.channel) !== -1) { return; } if (result.indexOf(data.channel) !== -1) { return; }
// Return owned pads // Return owned pads
if (_ownedByMe(Env, data.owners)) { if (_ownedByMe(Env, data.owners) && data.owners.length === 1) {
result.push(data.channel); result.push(data.channel);
} }
}; };

@ -1197,5 +1197,9 @@
"team_nameHint": "Name des Teams festlegen", "team_nameHint": "Name des Teams festlegen",
"team_avatarTitle": "Teamavatar", "team_avatarTitle": "Teamavatar",
"team_avatarHint": "Maximale Größe ist 500 KB (png, jpg, jpeg, gif)", "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"
} }

@ -40,7 +40,7 @@
"error": "Erreur", "error": "Erreur",
"saved": "Enregistré", "saved": "Enregistré",
"synced": "Tout est enregistré", "synced": "Tout est enregistré",
"deleted": "Pad supprimé de votre CryptDrive", "deleted": "Supprimé",
"deletedFromServer": "Pad supprimé du serveur", "deletedFromServer": "Pad supprimé du serveur",
"mustLogin": "Vous devez être enregistré pour avoir accès à cette page", "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.", "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_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_maxTeams": "Chaque compte utilisateur ne peut être membre que de {0} équipes.",
"team_listTitle": "Vos é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 <b>{1}</b>. 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 ?"
} }

@ -42,7 +42,7 @@
"error": "Error", "error": "Error",
"saved": "Saved", "saved": "Saved",
"synced": "Everything is saved", "synced": "Everything is saved",
"deleted": "Pad deleted from your CryptDrive", "deleted": "Deleted",
"deletedFromServer": "Pad deleted from the server", "deletedFromServer": "Pad deleted from the server",
"mustLogin": "You must be logged in to access this page", "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.", "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_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_maxTeams": "Each user account can only be a member of {0} teams.",
"team_listTitle": "Your 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 <b>{1}</b>. 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?"
} }

@ -115,7 +115,8 @@ define([
], ],
'admin': [ 'admin': [
'cp-team-name', '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) { var isOwner = Object.keys(privateData.teams || {}).some(function (id) {
return privateData.teams[id].owner; return privateData.teams[id].owner;
}); }) && !privateData.devMode; // XXX
if (Object.keys(privateData.teams || {}).length >= 3 || isOwner) { if (Object.keys(privateData.teams || {}).length >= 3 || isOwner) {
content.push(h('div.alert.alert-warning', { content.push(h('div.alert.alert-warning', {
role:'alert' role:'alert'
@ -429,6 +430,9 @@ define([
common.displayAvatar($(avatar), data.avatar, data.displayName); common.displayAvatar($(avatar), data.avatar, data.displayName);
// Name // Name
var name = h('span.cp-team-member-name', data.displayName); var name = h('span.cp-team-member-name', data.displayName);
if (data.pendingOwner) {
$(name).append(h('em', " PENDING"));
}
// Status // Status
var status = h('span.cp-team-member-status'+(data.online ? '.online' : '')); var status = h('span.cp-team-member-status'+(data.online ? '.online' : ''));
// Actions // Actions
@ -437,6 +441,29 @@ define([
var isMe = me && me.curvePublic === data.curvePublic; var isMe = me && me.curvePublic === data.curvePublic;
var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1; var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1;
var theirRole = ROLES.indexOf(data.role) || 0; 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 promoteOwner = h('span.fa.fa-angle-double-up', {
title: "Offer ownership" // XXX
});
$(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
}, function (obj) {
if (obj && obj.error) {
console.error(obj.error);
return void UI.warn(Messages.error);
}
UI.log("DONE"); // XXX
});
});
});
$actions.append(promoteOwner);
}
// If they're a member and I have a higher role than them, I can promote them to admin // 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) { if (!isMe && myRole > theirRole && theirRole === 0 && !data.pending) {
var promote = h('span.fa.fa-angle-double-up', { var promote = h('span.fa.fa-angle-double-up', {
@ -466,7 +493,8 @@ define([
$actions.append(demote); $actions.append(demote);
} }
// If I'm not a member and I have an equal or higher role than them, I can remove them // 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', { var remove = h('span.fa.fa-times', {
title: Messages.team_rosterKick title: Messages.team_rosterKick
}); });
@ -513,7 +541,7 @@ define([
var me = roster[userData.curvePublic] || {}; var me = roster[userData.curvePublic] || {};
var owner = Object.keys(roster).filter(function (k) { var owner = Object.keys(roster).filter(function (k) {
if (roster[k].pending) { return; } if (roster[k].pending) { return; }
return roster[k].role === "OWNER"; return roster[k].role === "OWNER" || roster[k].pendingOwner;
}).map(function (k) { }).map(function (k) {
return makeMember(common, roster[k], me); return makeMember(common, roster[k], me);
}); });
@ -718,6 +746,40 @@ define([
}); });
}, true); }, true);
makeBlock('delete', function (common, cb) { // XXX makeBlock keys
var deleteTeam = h('button.btn.btn-danger', Messages.team_delete || "DELETE"); // XXX
var $ok = $('<span>', {'class': 'fa fa-check', title: Messages.saved}).hide();
var $spinner = $('<span>', {'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 (!yes) { return; }
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 main = function () {
var common; var common;
var readOnly; var readOnly;

@ -36,31 +36,7 @@ define([
}; };
window.addEventListener('message', onMsg); window.addEventListener('message', onMsg);
}).nThen(function (/*waitFor*/) { }).nThen(function (/*waitFor*/) {
var teamId; // XXX var teamId;
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 addRpc = function (sframeChan, Cryptpad) { var addRpc = function (sframeChan, Cryptpad) {
sframeChan.on('Q_SET_TEAM', function (data, cb) { sframeChan.on('Q_SET_TEAM', function (data, cb) {
teamId = data; teamId = data;
@ -72,11 +48,6 @@ define([
data.teamId = teamId; data.teamId = teamId;
Cryptpad.userObjectCommand(data, cb); 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) { sframeChan.on('Q_DRIVE_GETOBJECT', function (data, cb) {
if (!teamId) { return void cb({error: 'EINVAL'}); } if (!teamId) { return void cb({error: 'EINVAL'}); }
if (data && data.sharedFolder) { if (data && data.sharedFolder) {
@ -109,7 +80,6 @@ define([
}); });
}; };
SFCommonO.start({ SFCommonO.start({
afterSecrets: afterSecrets,
noHash: true, noHash: true,
noRealtime: true, noRealtime: true,
//driveEvents: true, //driveEvents: true,

Loading…
Cancel
Save