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

This commit is contained in:
yflory
2019-12-20 14:28:44 +01:00
42 changed files with 1934 additions and 319 deletions

View File

@@ -20,7 +20,7 @@ define(function() {
* users and these users will be redirected to the login page if they still try to access
* the app
*/
config.registeredOnlyTypes = ['teams', 'file', 'contacts', 'oodoc', 'ooslide', 'sheet', 'notifications'];
config.registeredOnlyTypes = ['file', 'contacts', 'oodoc', 'ooslide', 'sheet', 'notifications'];
/* CryptPad is available is multiple languages, but only English and French are maintained
* by the developers. The other languages may be outdated, and any missing string for a langauge

View File

@@ -231,10 +231,14 @@ Version 1
}
if (['invite'].indexOf(type) !== -1) {
parsed.type = 'invite';
if (hashArr[1] && hashArr[1] === '1') {
parsed.version = 1;
parsed.channel = hashArr[2];
parsed.pubkey = hashArr[3].replace(/-/g, '/');
if (hashArr[1] && hashArr[1] === '2') {
parsed.version = 2;
parsed.app = hashArr[2];
parsed.mode = hashArr[3];
parsed.key = hashArr[4];
options = hashArr.slice(5);
parsed.password = options.indexOf('p') !== -1;
return parsed;
}
return parsed;

View File

@@ -475,7 +475,7 @@ define([
opt = opt || {};
var inputBlock = opt.password ? UI.passwordInput() : dialog.textInput();
var input = opt.password ? $(inputBlock).find('input')[0] : inputBlock;
var input = $(inputBlock).is('input') ? inputBlock : $(inputBlock).find('input')[0];
input.value = typeof(def) === 'string'? def: '';
var message;
@@ -592,6 +592,7 @@ define([
}];
var modal = dialog.customModal(content, {buttons: buttons});
UI.openCustomModal(modal);
return modal;
};
UI.log = function (msg) {

View File

@@ -84,6 +84,7 @@ define([
var myData = createData(store.proxy, false);
if (store.proxy.friends) {
store.proxy.friends.me = myData;
delete store.proxy.friends.me.channel;
}
if (store.modules['team']) {
store.modules['team'].updateMyData(myData);

View File

@@ -14,11 +14,13 @@ define([
'/customize/application_config.js',
'/customize/pages.js',
'/bower_components/nthen/index.js',
'/common/invitation.js',
'css!/customize/fonts/cptools/style.css',
'/bower_components/croppie/croppie.min.js',
'css!/bower_components/croppie/croppie.css',
], function ($, Config, Util, Hash, Language, UI, Constants, Feedback, h, MediaTag, Clipboard,
Messages, AppConfig, Pages, NThen) {
Messages, AppConfig, Pages, NThen, InviteInner) {
var UIElements = {};
// Configure MediaTags to use our local viewer
@@ -1557,8 +1559,11 @@ define([
var team = privateData.teams[config.teamId];
if (!team) { return void UI.warn(Messages.error); }
var origin = privateData.origin;
var module = config.module || common.makeUniversal('team');
// Invite contacts
var $div;
var refreshButton = function () {
if (!$div) { return; }
@@ -1572,48 +1577,226 @@ define([
$btn.prop('disabled', 'disabled');
}
};
var list = UIElements.getUserGrid(Messages.team_pickFriends, {
common: common,
data: config.friends,
large: true
}, refreshButton);
$div = $(list.div);
refreshButton();
var getContacts = function () {
var list = UIElements.getUserGrid(Messages.team_pickFriends, {
common: common,
data: config.friends,
large: true
}, refreshButton);
var div = h('div.contains-nav');
var $div = $(div);
$div.append(list.div);
var contactsButtons = [{
className: 'primary',
name: Messages.team_inviteModalButton,
onClick: function () {
var $sel = $div.find('.cp-usergrid-user.cp-selected');
var sel = $sel.toArray();
if (!sel.length) { return; }
var buttons = [{
sel.forEach(function (el) {
var curve = $(el).attr('data-curve');
module.execCommand('INVITE_TO_TEAM', {
teamId: config.teamId,
user: config.friends[curve]
}, function (obj) {
if (obj && obj.error) {
console.error(obj.error);
return UI.warn(Messages.error);
}
});
});
},
keys: [13]
}];
return {
content: div,
buttons: contactsButtons
};
};
var friendsObject = hasFriends ? getContacts() : noContactsMessage(common);
var friendsList = friendsObject.content;
var contactsButtons = friendsObject.buttons;
contactsButtons.unshift({
className: 'cancel',
name: Messages.cancel,
onClick: function () {},
keys: [27]
});
var contactsContent = h('div.cp-share-modal', [
friendsList
]);
var frameContacts = UI.dialog.customModal(contactsContent, {
buttons: contactsButtons,
});
var linkName, linkPassword, linkMessage, linkError, linkSpinText;
var linkForm, linkSpin, linkResult;
var linkWarning;
// Invite from link
var dismissButton = h('span.fa.fa-times');
var linkContent = h('div.cp-share-modal', [
h('p', Messages.team_inviteLinkTitle ),
linkError = h('div.alert.alert-danger.cp-teams-invite-alert', {style : 'display: none;'}),
linkForm = h('div.cp-teams-invite-form', [
linkName = h('input', {
placeholder: Messages.team_inviteLinkTempName
}),
h('br'),
h('div.cp-teams-invite-block', [
h('span', Messages.team_inviteLinkSetPassword),
h('a.cp-teams-help.fa.fa-question-circle', {
href: origin + '/faq.html#security-pad_password',
target: "_blank",
'data-tippy-placement': "right"
})
]),
linkPassword = UI.passwordInput({
id: 'cp-teams-invite-password',
placeholder: Messages.login_password
}),
h('div.cp-teams-invite-block',
h('span', Messages.team_inviteLinkNote)
),
linkMessage = h('textarea.cp-teams-invite-message', {
placeholder: Messages.team_inviteLinkNoteMsg,
rows: 3
})
]),
linkSpin = h('div.cp-teams-invite-spinner', {
style: 'display: none;'
}, [
h('i.fa.fa-spinner.fa-spin'),
linkSpinText = h('span', Messages.team_inviteLinkLoading)
]),
linkResult = h('div', {
style: 'display: none;'
}, h('textarea', {
readonly: 'readonly'
})),
linkWarning = h('div.cp-teams-invite-alert.alert.alert-warning.dismissable', {
style: "display: none;"
}, [
h('span.cp-inline-alert-text', Messages.team_inviteLinkWarning),
dismissButton
])
]);
$(linkMessage).keydown(function (e) {
if (e.which === 13) {
e.stopPropagation();
}
});
var localStore = window.cryptpadStore;
localStore.get('hide-alert-teamInvite', function (val) {
if (val === '1') { return; }
$(linkWarning).show();
$(dismissButton).on('click', function () {
localStore.put('hide-alert-teamInvite', '1');
$(linkWarning).remove();
});
});
var $linkContent = $(linkContent);
var href;
var process = function () {
var $nav = $linkContent.closest('.alertify').find('nav');
$(linkError).text('').hide();
var name = $(linkName).val();
var pw = $(linkPassword).find('input').val();
var msg = $(linkMessage).val();
var hash = Hash.createRandomHash('invite', pw);
var hashData = Hash.parseTypeHash('invite', hash);
href = origin + '/teams/#' + hash;
if (!name || !name.trim()) {
$(linkError).text(Messages.team_inviteLinkErrorName).show();
return true;
}
var seeds = InviteInner.deriveSeeds(hashData.key);
var salt = InviteInner.deriveSalt(pw, AppConfig.loginSalt);
var bytes64;
NThen(function (waitFor) {
$(linkForm).hide();
$(linkSpin).show();
$nav.find('button.cp-teams-invite-create').hide();
$nav.find('button.cp-teams-invite-copy').show();
setTimeout(waitFor(), 150);
}).nThen(function (waitFor) {
InviteInner.deriveBytes(seeds.scrypt, salt, waitFor(function (_bytes) {
bytes64 = _bytes;
}));
}).nThen(function (waitFor) {
module.execCommand('CREATE_INVITE_LINK', {
name: name,
password: pw,
message: msg,
bytes64: bytes64,
hash: hash,
teamId: config.teamId,
seeds: seeds,
}, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
$(linkSpin).hide();
$(linkForm).show();
$nav.find('button.cp-teams-invite-create').show();
$nav.find('button.cp-teams-invite-copy').hide();
return void $(linkError).text(Messages.team_inviteLinkError).show();
}
// Display result here
$(linkSpin).hide();
$(linkResult).show().find('textarea').text(href);
$nav.find('button.cp-teams-invite-copy').prop('disabled', '');
}));
});
return true;
};
var linkButtons = [{
className: 'cancel',
name: Messages.cancel,
onClick: function () {},
keys: [27]
}, {
className: 'primary',
name: Messages.team_inviteModalButton,
className: 'primary cp-teams-invite-create',
name: Messages.team_inviteLinkCreate,
onClick: function () {
var $sel = $div.find('.cp-usergrid-user.cp-selected');
var sel = $sel.toArray();
if (!sel.length) { return; }
sel.forEach(function (el) {
var curve = $(el).attr('data-curve');
module.execCommand('INVITE_TO_TEAM', {
teamId: config.teamId,
user: config.friends[curve]
}, function (obj) {
if (obj && obj.error) {
console.error(obj.error);
return UI.warn(Messages.error);
}
});
});
return process();
},
keys: [13]
keys: []
}, {
className: 'primary cp-teams-invite-copy',
name: Messages.team_inviteLinkCopy,
onClick: function () {
if (!href) { return; }
var success = Clipboard.copy(href);
if (success) { UI.log(Messages.shareSuccess); }
},
keys: []
}];
var content = h('div', [
list.div
]);
var frameLink = UI.dialog.customModal(linkContent, {
buttons: linkButtons,
});
$(frameLink).find('.cp-teams-invite-copy').prop('disabled', 'disabled').hide();
var modal = UI.dialog.customModal(content, {buttons: buttons});
// Create modal
var tabs = [{
title: Messages.share_contactCategory,
icon: "fa fa-address-book",
content: frameContacts,
active: hasFriends
}, {
title: Messages.share_linkCategory,
icon: "fa fa-link",
content: frameLink,
active: !hasFriends
}];
var modal = UI.dialog.tabs(tabs);
UI.openCustomModal(modal);
};
@@ -2073,12 +2256,12 @@ define([
for (var k in actions) {
$('<button>', {
'data-type': k,
'class': 'fa ' + actions[k].icon,
'class': 'pure-button fa ' + actions[k].icon,
title: Messages['mdToolbar_' + k] || k
}).click(onClick).appendTo($toolbar);
}
$('<button>', {
'class': 'fa fa-question cp-markdown-help',
'class': 'pure-button fa fa-question cp-markdown-help',
title: Messages.mdToolbar_help
}).click(function () {
var href = Messages.mdToolbar_tutorial;
@@ -3977,7 +4160,7 @@ define([
};
var content = h('div.cp-share-modal', [
setHTML(h('p'), text)
setHTML(h('p'), text),
]);
UI.proposal(content, todo);
};

View File

@@ -29,11 +29,18 @@ define([
};
var makeConfig = function (hash, opt) {
var secret;
if (typeof(hash) === 'string') {
// We can't use cryptget with a file or a user so we can use 'pad' as hash type
var secret = Hash.getSecrets('pad', hash, opt.password);
secret = Hash.getSecrets('pad', hash, opt.password);
} else if (typeof(hash) === 'object') {
// we may want to just supply options directly
// and this is the easiest place to do it
secret = hash;
}
if (!secret.keys) { secret.keys = secret.key; } // support old hashses
var config = {
websocketURL: NetConfig.getWebsocketURL(),
websocketURL: NetConfig.getWebsocketURL(opt.origin),
channel: secret.channel,
validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys),
@@ -95,7 +102,7 @@ define([
opt = opt || {};
var config = makeConfig(hash, opt);
var Session = { cb: cb, };
var Session = { cb: cb, hasNetwork: Boolean(opt.network) };
config.onReady = function (info) {
var realtime = Session.session = info.realtime;
@@ -105,12 +112,13 @@ define([
var to = setTimeout(function () {
cb(new Error("Timeout"));
}, 5000);
}, 15000);
Realtime.whenRealtimeSyncs(realtime, function () {
clearTimeout(to);
var doc = realtime.getAuthDoc();
realtime.abort();
finish(Session, void 0);
finish(Session, void 0, doc);
});
};
overwrite(config, opt);

View File

@@ -779,6 +779,11 @@ define([
postMessage("SEND_FRIEND_REQUEST", data, cb);
};
// Team
common.anonGetPreviewContent = function (data, cb) {
postMessage("ANON_GET_PREVIEW_CONTENT", data, cb);
};
// Onlyoffice
var onlyoffice = common.onlyoffice = {};
onlyoffice.execCommand = function (data, cb) {

54
www/common/invitation.js Normal file
View File

@@ -0,0 +1,54 @@
(function () {
var factory = function (Util, Nacl, Scrypt) {
var Invite = {};
Invite.deriveSeeds = function (safeSeed) {
// take the hash of the provided seed
var seed = safeSeed.replace(/\-/g, '/');
var u8_seed = Nacl.hash(Nacl.util.decodeBase64(seed));
// hash the first half again for scrypt's input
var subseed1 = Nacl.hash(u8_seed.subarray(0, 32));
// hash the remainder for the invite content
var subseed2 = Nacl.hash(u8_seed.subarray(32));
return {
scrypt: Nacl.util.encodeBase64(subseed1),
preview: Nacl.util.encodeBase64(subseed2),
};
};
Invite.deriveSalt = function (password, instance_salt) {
return (password || '') + (instance_salt || '');
};
// seed => bytes64
Invite.deriveBytes = function (scrypt_seed, salt, cb) {
Scrypt(scrypt_seed,
salt,
8, // memoryCost (n)
1024, // block size parameter (r)
192, // dkLen
200, // interruptStep
cb,
'base64'); // format, could be 'base64'
};
return Invite;
};
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = factory(
require("../common-util"),
require("tweetnacl/nacl-fast"),
require("scrypt-async")
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
'/bower_components/scrypt-async/scrypt-async.min.js',
], function (Util) {
return factory(Util, window.nacl, window.scrypt);
});
}
}());

View File

@@ -435,8 +435,6 @@
return mediaObject;
}
mediaObject.tag.innerHTML = '<img style="width: 100px; height: 100px;">';
// Download the encrypted blob
download(src, function (err, u8Encrypted) {
if (err) {

View File

@@ -3,9 +3,10 @@ define([
'/customize/messages.js',
'/common/common-util.js',
'/common/common-interface.js',
'/common/common-ui-elements.js',
'/common/hyperscript.js',
'/common/diffMarked.js',
], function ($, Messages, Util, UI, h, DiffMd) {
], function ($, Messages, Util, UI, UIElements, h, DiffMd) {
'use strict';
var debug = console.log;
@@ -13,6 +14,8 @@ define([
var MessengerUI = {};
var mutedUsers = {};
var dataQuery = function (id) {
return '[data-key="' + id + '"]';
};
@@ -67,8 +70,11 @@ define([
h('div.cp-app-contacts-category-content')
]),
h('div.cp-app-contacts-friends.cp-app-contacts-category', [
h('div.cp-app-contacts-category-content'),
h('h2.cp-app-contacts-category-title', Messages.contacts_friends),
h('button.cp-app-contacts-muted-button',[
h('i.fa.fa-bell-slash'),
Messages.contacts_manageMuted
]),
h('div.cp-app-contacts-category-content.cp-contacts-friends')
]),
h('div.cp-app-contacts-rooms.cp-app-contacts-category', [
h('div.cp-app-contacts-category-content'),
@@ -184,7 +190,7 @@ define([
markup.message = function (msg) {
if (msg.type !== 'MSG') { return; }
var curvePublic = msg.author;
var name = typeof msg.name !== "undefined" ?
var name = (typeof msg.name !== "undefined" || !contactsData[msg.author]) ?
(msg.name || Messages.anonymous) :
contactsData[msg.author].displayName;
var d = msg.time ? new Date(msg.time) : undefined;
@@ -486,6 +492,20 @@ define([
}
};
var unmuteUser = function (curve) {
execCommand('UNMUTE_USER', curve, function (e) {
if (e) { return void console.error(e); }
});
};
var muteUser = function (data) {
execCommand('MUTE_USER', {
curvePublic: data.curvePublic,
name: data.displayName || data.name,
avatar: data.avatar
}, function (e /*, removed */) {
if (e) { return void console.error(e); }
});
};
var removeFriend = function (curvePublic) {
execCommand('REMOVE_FRIEND', curvePublic, function (e /*, removed */) {
if (e) { return void console.error(e); }
@@ -496,9 +516,23 @@ define([
var roomEl = h('div.cp-app-contacts-friend.cp-avatar', {
'data-key': id,
'data-user': room.isFriendChat ? userlist[0].curvePublic : '',
title: room.name
});
var curve;
if (room.isFriendChat) {
var __channel = state.channels[id];
curve = __channel.curvePublic;
}
var unmute = h('span.cp-app-contacts-remove.fa.fa-bell.cp-unmute-icon', {
title: Messages.contacts_unmute || 'unmute',
style: (curve && mutedUsers[curve]) ? undefined : 'display: none;'
});
var mute = h('span.cp-app-contacts-remove.fa.fa-bell-slash.cp-mute-icon', {
title: Messages.contacts_mute || 'mute',
style: (curve && mutedUsers[curve]) ? 'display: none;' : undefined
});
var remove = h('span.cp-app-contacts-remove.fa.fa-user-times', {
title: Messages.contacts_remove
});
@@ -511,8 +545,12 @@ define([
});
var rightCol = h('span.cp-app-contacts-right-col', [
h('span.cp-app-contacts-name', [room.name]),
room.isFriendChat ? remove :
(room.isPadChat || room.isTeamChat) ? undefined : leaveRoom,
h('span.cp-app-contacts-icons', [
room.isFriendChat ? mute : undefined,
room.isFriendChat ? unmute : undefined,
room.isFriendChat ? remove :
(room.isPadChat || room.isTeamChat) ? undefined : leaveRoom,
])
]);
var friendData = room.isFriendChat ? userlist[0] : {};
@@ -523,23 +561,43 @@ define([
if (friendData.profile) { window.open(origin + '/profile/#' + friendData.profile); }
});
$(unmute).on('click dblclick', function (e) {
e.stopPropagation();
var channel = state.channels[id];
if (!channel.isFriendChat) { return; }
var curvePublic = channel.curvePublic;
$(mute).show();
$(unmute).hide();
unmuteUser(curvePublic);
});
$(mute).on('click dblclick', function (e) {
e.stopPropagation();
var channel = state.channels[id];
if (!channel.isFriendChat) { return; }
var curvePublic = channel.curvePublic;
var friend = contactsData[curvePublic] || friendData;
$(mute).hide();
$(unmute).show();
muteUser(friend);
});
$(remove).click(function (e) {
e.stopPropagation();
var channel = state.channels[id];
if (!channel.isFriendChat) { return; }
var curvePublic = channel.curvePublic;
var friend = contactsData[curvePublic] || friendData;
UI.confirm(Messages._getKey('contacts_confirmRemove', [
Util.fixHTML(friend.name)
]), function (yes) {
var content = h('div', [
UI.setHTML(h('p'), Messages._getKey('contacts_confirmRemove', [Util.fixHTML(friend.name)])),
]);
UI.confirm(content, function (yes) {
if (!yes) { return; }
removeFriend(curvePublic, function (e) {
if (e) { return void console.error(e); }
});
removeFriend(curvePublic);
// TODO remove friend from userlist ui
// FIXME seems to trigger EJOINED from netflux-websocket (from server);
// (tried to join a channel in which you were already present)
}, undefined, true);
});
});
if (friendData.avatar && avatars[friendData.avatar]) {
@@ -792,6 +850,65 @@ define([
// var onJoinRoom
// var onLeaveRoom
var updateMutedList = function () {
execCommand('GET_MUTED_USERS', null, function (err, muted) {
if (err) { return void console.error(err); }
mutedUsers = muted;
var $button = $userlist.find('.cp-app-contacts-muted-button');
$('.cp-app-contacts-friend[data-user]')
.find('.cp-mute-icon').show();
$('.cp-app-contacts-friend[data-user]')
.find('.cp-unmute-icon').hide();
if (!muted || Object.keys(muted).length === 0) {
$button.hide();
return;
}
var rows = Object.keys(muted).map(function (curve) {
$('.cp-app-contacts-friend[data-user="'+curve+'"]')
.find('.cp-mute-icon').hide();
$('.cp-app-contacts-friend[data-user="'+curve+'"]')
.find('.cp-unmute-icon').show();
var data = muted[curve];
var avatar = h('span.cp-avatar');
var button = h('button', {
'data-user': curve
}, [
h('i.fa.fa-bell'),
Messages.contacts_unmute || 'unmute'
]);
UIElements.displayAvatar(common, $(avatar), data.avatar, data.name);
$(button).click(function () {
unmuteUser(curve, button);
execCommand('UNMUTE_USER', curve, function (e, data) {
if (e) { return void console.error(e); }
$(button).closest('div').remove();
if (!data) { $button.hide(); }
$('.cp-app-contacts-friend[data-user="'+curve+'"]')
.find('.cp-mute-icon').show();
if ($('.cp-contacts-muted-table').find('.cp-contacts-muted-user').length === 0) {
UI.findOKButton().click();
}
});
});
return h('div.cp-contacts-muted-user', [
h('span', avatar),
h('span', data.name),
button
]);
});
var content = h('div', [
h('h4', Messages.contacts_mutedUsers),
h('div.cp-contacts-muted-table', rows)
]);
$button.off('click');
$button.click(function () {
UI.alert(content);
}).show();
});
};
var ready = false;
var onMessengerReady = function () {
@@ -806,6 +923,8 @@ define([
rooms.forEach(initializeRoom);
});
updateMutedList();
$container.removeClass('cp-app-contacts-initializing');
};
@@ -882,6 +1001,10 @@ define([
onUpdateData(data);
return;
}
if (cmd === 'UPDATE_MUTED') {
updateMutedList();
return;
}
if (cmd === 'MESSAGE') {
onMessage(data);
return;

View File

@@ -1273,6 +1273,12 @@ define([
});
};
Store.anonGetPreviewContent = function (clientId, data, cb) {
Team.anonGetPreviewContent({
store: store
}, data, cb);
};
// Get hashes for the share button
// If we can find a stronger hash
Store.getStrongerHash = function (clientId, data, _cb) {

View File

@@ -1,79 +1,82 @@
(function () {
var factory = function (Util, Cred, nThen) {
nThen = nThen; // XXX
var factory = function (Util, Cred, Nacl) {
var Invite = {};
/*
TODO key derivation
var encode64 = Nacl.util.encodeBase64;
var decode64 = Nacl.util.decodeBase64;
// ed and curve keys can be random...
Invite.generateKeys = function () {
var ed = Nacl.sign.keyPair();
var curve = Nacl.box.keyPair();
return {
edPublic: encode64(ed.publicKey),
edPrivate: encode64(ed.secretKey),
curvePublic: encode64(curve.publicKey),
curvePrivate: encode64(curve.secretKey),
};
};
Invite.generateSignPair = function () {
var ed = Nacl.sign.keyPair();
return {
validateKey: encode64(ed.publicKey),
signKey: encode64(ed.secretKey),
};
};
var b64ToChannelKeys = function (b64) {
var dispense = Cred.dispenser(decode64(b64));
return {
channel: Util.uint8ArrayToHex(dispense(16)),
cryptKey: dispense(Nacl.secretbox.keyLength),
};
};
// the secret invite values (cryptkey and channel) can be derived
// from the link seed and (optional) password
Invite.deriveInviteKeys = b64ToChannelKeys;
// the preview values (cryptkey and channel) are less sensitive than the invite values
// as they cannot be leveraged to access any further content on their own
// unless the message contains secrets.
// derived from the link seed alone.
Invite.derivePreviewKeys = b64ToChannelKeys;
Invite.createRosterEntry = function (roster, data, cb) {
var toInvite = {};
toInvite[data.curvePublic] = data.content;
roster.invite(toInvite, cb);
};
/* INPUTS
* password (for scrypt)
* message (personal note)
* link hash
* bytes64 (scrypt output)
* preview_hash
scrypt(seed, passwd) => {
curve: {
private,
public,
},
ed: {
private,
public,
}
cryptKey,
channel
}
*/
var BYTES_REQUIRED = 256;
/* IO / FUNCTIONALITY
Invite.deriveKeys = function (seed, passwd, cb) {
cb = cb; // XXX
// TODO validate has cb
// TODO onceAsync the cb
// TODO cb with err if !(seed && passwd)
* creator
* generate a random signKey (prevent writes to preview channel)
* encrypt and upload the preview content
* via CryptGet
* owned by:
* the ephemeral edPublic
* the invite creator
* create a roster entry for the invitation
* with encrypted notes for the creator
* redeemer
* get the preview content
* redeem the invite
* add yourself to the roster
* add the team to your proxy-manager
Cred.deriveFromPassphrase(seed, passwd, BYTES_REQUIRED, function (bytes) {
var dispense = Cred.dispenser(bytes);
dispense = dispense; // XXX
// edPriv => edPub
// curvePriv => curvePub
// channel
// cryptKey
});
};
Invite.createSeed = function () {
// XXX
// return a seed
};
Invite.create = function (cb) {
cb = cb; // XXX
// TODO validate has cb
// TODO onceAsync the cb
// TODO cb with err if !(seed && passwd)
// required
// password
// validateKey
// creatorEdPublic
// for owner
// ephemeral
// signingKey
// for owner to write invitation
// derived
// edPriv
// edPublic
// for invitee ownership
// curvePriv
// curvePub
// for acceptance OR
// authenticated decline message via mailbox
// channel
// for owned deletion
// for team pinning
// cryptKey
// for protecting channel content
};
*/
return Invite;
};
@@ -81,15 +84,16 @@ var factory = function (Util, Cred, nThen) {
module.exports = factory(
require("../common-util"),
require("../common-credential.js"),
require("nthen")
require("nthen"),
require("tweetnacl/nacl-fast")
);
} else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) {
define([
'/common/common-util.js',
'/common/common-credential.js',
'/bower_components/nthen/index.js',
], function (Util, Cred, nThen) {
return factory(Util, nThen);
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Cred) {
return factory(Util, Cred, window.nacl);
});
}
}());

View File

@@ -12,6 +12,13 @@ define([
var handlers = {};
var removeHandlers = {};
var isMuted = function (ctx, data) {
var muted = ctx.store.proxy.mutedUsers || {};
var curvePublic = Util.find(data, ['msg', 'author']);
if (!curvePublic) { return false; }
return Boolean(muted[curvePublic]);
};
// Store the friend request displayed to avoid duplicates
var friendRequest = {};
handlers['FRIEND_REQUEST'] = function (ctx, box, data, cb) {
@@ -21,6 +28,8 @@ define([
return void cb(true);
}
if (isMuted(ctx, data)) { return void cb(true); }
// Don't show duplicate friend request: if we already have a friend request
// in memory from the same user, dismiss the new one
if (friendRequest[data.msg.author]) { return void cb(true); }
@@ -30,10 +39,22 @@ define([
// If the user is already in our friend list, automatically accept the request
if (Messaging.getFriend(ctx.store.proxy, data.msg.author) ||
ctx.store.proxy.friends_pending[data.msg.author]) {
delete ctx.store.proxy.friends_pending[data.msg.author];
Messaging.acceptFriendRequest(ctx.store, data.msg.content, function (obj) {
if (obj && obj.error) {
return void cb();
}
Messaging.addToFriendList({
proxy: ctx.store.proxy,
realtime: ctx.store.realtime,
pinPads: ctx.pinPads
}, data.msg.content, function (err) {
if (err) { console.error(err); }
if (ctx.store.messenger) {
ctx.store.messenger.onFriendAdded(data.msg.content);
}
});
ctx.updateMetadata();
cb(true);
});
return;
@@ -170,6 +191,8 @@ define([
var content = msg.content;
// content.name, content.title, content.href, content.password
if (isMuted(ctx, data)) { return void cb(true); }
var channel = Hash.hrefToHexChannelId(content.href, content.password);
var parsed = Hash.parsePadUrl(content.href);
var mode = parsed.hashData && parsed.hashData.mode || 'n/a';
@@ -212,6 +235,9 @@ define([
supportMessage = true;
cb();
};
removeHandlers['SUPPORT_MESSAGE'] = function () {
supportMessage = false;
};
// Incoming edit rights request: add data before sending it to inner
handlers['REQUEST_PAD_ACCESS'] = function (ctx, box, data, cb) {
@@ -220,6 +246,8 @@ define([
if (msg.author !== content.user.curvePublic) { return void cb(true); }
if (isMuted(ctx, data)) { return void cb(true); }
var channel = content.channel;
var res = ctx.store.manager.findChannel(channel);
@@ -270,6 +298,9 @@ define([
var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); }
if (isMuted(ctx, data)) { return void cb(true); }
if (!content.teamChannel && !(content.href && content.title && content.channel)) {
console.log('Remove invalid notification');
return void cb(true);
@@ -327,6 +358,9 @@ define([
var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); }
if (isMuted(ctx, data)) { return void cb(true); }
if (!content.team) {
console.log('Remove invalid notification');
return void cb(true);

View File

@@ -428,20 +428,21 @@ define([
}
var channel = ctx.channels[data.channel];
if (!channel) {
return void cb({error: "NO_SUCH_CHANNEL"});
}
// Unfriend with mailbox
if (ctx.store.mailbox && data.curvePublic && data.notifications) {
Messaging.removeFriend(ctx.store, curvePublic, function (obj) {
if (obj && obj.error) { return void cb({error:obj.error}); }
ctx.updateMetadata();
cb(obj);
});
return;
}
// Unfriend with channel
if (!channel) {
return void cb({error: "NO_SUCH_CHANNEL"});
}
try {
var msg = [Types.unfriend, proxy.curvePublic, +new Date()];
var msgStr = JSON.stringify(msg);
@@ -458,6 +459,40 @@ define([
}
};
var getAllClients = function (ctx) {
var all = [];
Array.prototype.push.apply(all, ctx.friendsClients);
Object.keys(ctx.channels).forEach(function (id) {
Array.prototype.push.apply(all, ctx.channels[id].clients);
});
return Util.deduplicateString(all);
};
var muteUser = function (ctx, data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var proxy = ctx.store.proxy;
var muted = proxy.mutedUsers = proxy.mutedUsers || {};
if (muted[data.curvePublic]) { return void cb(); }
muted[data.curvePublic] = data;
ctx.emit('UPDATE_MUTED', null, getAllClients(ctx));
cb();
};
var unmuteUser = function (ctx, curvePublic, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var proxy = ctx.store.proxy;
var muted = proxy.mutedUsers = proxy.mutedUsers || {};
delete muted[curvePublic];
ctx.emit('UPDATE_MUTED', null, getAllClients(ctx));
cb(Object.keys(muted).length);
};
var getMutedUsers = function (ctx, cb) {
var proxy = ctx.store.proxy;
if (cb) {
return void cb(proxy.mutedUsers || {});
}
return proxy.mutedUsers || {};
};
var openChannel = function (ctx, data) {
var proxy = ctx.store.proxy;
var network = ctx.store.network;
@@ -664,7 +699,14 @@ define([
nThen(function (waitFor) {
// Load or get all friends channels
Object.keys(friends).forEach(function (key) {
if (key === 'me') { return; }
if (key === 'me') {
// At some point a bug inserted a friend's channel into our "me" data.
// This led to displaying our name instead of our friend's name in the
// contacts app. The following line is here to prevent this issue to happen
// again.
delete friends.me.channel;
return;
}
var friend = clone(friends[key]);
if (typeof(friend) !== 'object') { return; }
if (!friend.channel) { return; }
@@ -887,15 +929,6 @@ define([
});
};
var getAllClients = function (ctx) {
var all = [];
Array.prototype.push.apply(all, ctx.friendsClients);
Object.keys(ctx.channels).forEach(function (id) {
Array.prototype.push.apply(all, ctx.channels[id].clients);
});
return Util.deduplicateString(all);
};
Msg.init = function (cfg, waitFor, emit) {
var messenger = {};
var store = cfg.store;
@@ -911,6 +944,9 @@ define([
range_requests: {}
};
store.proxy.on('change', ['mutedUsers'], function () {
ctx.emit('UPDATE_MUTED', null, getAllClients(ctx));
});
ctx.store.network.on('message', function(msg, sender) {
onDirectMessage(ctx, msg, sender);
@@ -942,6 +978,12 @@ define([
var channel = friend.channel;
if (!channel) { return; }
// Already friend? don't load the channel a second time
var chanId = friend.channel;
var chan = ctx.channels[chanId];
if (chan) { return; }
// Load the channel and add the friend to the contacts app
loadFriend(ctx, null, friend, function () {
emit('FRIEND', {
curvePublic: friend.curvePublic,
@@ -990,6 +1032,9 @@ define([
if (cmd === 'GET_ROOMS') {
return void getRooms(ctx, data, cb);
}
if (cmd === 'GET_MUTED_USERS') {
return void getMutedUsers(ctx, cb);
}
if (cmd === 'GET_USERLIST') {
return void getUserList(ctx, data, cb);
}
@@ -1002,6 +1047,12 @@ define([
if (cmd === 'REMOVE_FRIEND') {
return void removeFriend(ctx, data, cb);
}
if (cmd === 'MUTE_USER') {
return void muteUser(ctx, data, cb);
}
if (cmd === 'UNMUTE_USER') {
return void unmuteUser(ctx, data, cb);
}
if (cmd === 'GET_STATUS') {
return void getStatus(ctx, data, cb);
}

View File

@@ -3,13 +3,18 @@ define([
], function (ApiConfig) {
var Config = {};
Config.getWebsocketURL = function () {
Config.getWebsocketURL = function (origin) {
if (!ApiConfig.websocketPath) { return ApiConfig.websocketURL; }
var path = ApiConfig.websocketPath;
if (/^ws{1,2}:\/\//.test(path)) { return path; }
var protocol = window.location.protocol.replace(/http/, 'ws');
var host = window.location.host;
var l = window.location;
if (origin && window && window.document) {
l = document.createElement("a");
l.href = origin;
}
var protocol = l.protocol.replace(/http/, 'ws');
var host = l.host;
var url = protocol + '//' + host + path;
return url;

View File

@@ -364,6 +364,98 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
return changed;
};
commands.INVITE = function (args, author, roster) {
// an invitation is created with an ephemeral curve public key
// that key is ultimately given to the user you'd like on your team
// that user can exploit their possession of the public key to remove
// the pending invitation with their actual data.
if (!isMap(args)) { throw new Error('INVALID_ARGS'); }
if (!roster.internal.initialized) { throw new Error("UNINITIALIED"); }
if (typeof(roster.state.members) === 'undefined') {
throw new Error("CANNOT+INVITE_TO_UNINITIALIED_ROSTER");
}
var members = roster.state.members;
Object.keys(args).forEach(function (curve) {
if (!isValidId(curve)) {
console.log(curve, curve.length);
throw new Error("INVALID_CURVE_KEY");
}
// reject commandws wehere the members are not proper objects
if (!isMap(args[curve])) { throw new Error("INVALID_CONTENT"); }
if (members[curve]) { throw new Error("ARLEADY_PRESENT"); }
var data = args[curve];
// if no role was provided, assume VIEWER
if (typeof(data.role) !== 'string') { data.role = "VIEWER"; }
// assume that invitations are 'pending' unless stated otherwise
if (typeof(data.pending) === 'undefined') { data.pending = true; }
if (!canAddRole(author, data.role, members)) {
throw new Error("INSUFFICIENT_PERMISSIONS");
}
if (typeof(data.displayName) !== 'string' || !data.displayName) { throw new Error("DISPLAYNAME_REQUIRED"); }
//if (typeof(data.notifications) !== 'string') { throw new Error("NOTIFICATIONS_REQUIRED"); }
});
/*
{
<ephemeralCurveKey>: {
role: ??? || 'VIEWER',
displayName: '',
pending: true,
}
}
*/
var changed = false;
Object.keys(args).forEach(function (curve) {
changed = true;
members[curve] = args[curve];
});
return changed;
};
commands.ACCEPT = function (args, author, roster) {
if (!roster.internal.initialized) { throw new Error("UNINITIALIED"); }
if (typeof(roster.state.members) === 'undefined') {
throw new Error("CANNOT_ADD_TO_UNINITIALIED_ROSTER");
}
// an ACCEPT command replaces a pending invitation's curve key with a new one
// after which the invited member can use their actual curve key to describe themselves
// the author must have been invited already...
var members = roster.state.members;
// so you must already be in the members list
if (!isMap(members[author])) { throw new Error("INSUFFICIENT_PERMISSIONS"); }
// and your membership must indicate that you are 'pending'
if (!members[author].pending) { throw new Error("ALREADY_PRESENT"); }
// args should be a string
if (typeof(args) !== 'string') { throw new Error("INVALID_ARGS"); }
// ...and a valid curve key
if (!isValidId(args)) { throw new Error("INVALID_CURVE_KEY"); }
var curve = args;
// and the curve key must not already be a member
if (typeof(members[curve]) !== 'undefined') { throw new Error("MEMBER_ALREADY_PRESENT"); }
// copy the new profile from the old one
members[curve] = Util.clone(members[author]);
// and erase the old one
delete members[author];
return true;
};
var handleCommand = function (content, author, roster) {
if (!(Array.isArray(content) && typeof(author) === 'string')) {
throw new Error("INVALID ARGUMENTS");
@@ -671,6 +763,31 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
send(['METADATA', data], cb);
};
// supports multiple invite
roster.invite = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var state = ref.state;
if (!state) { return cb("UNINITIALIZED"); }
if (!ref.internal.initialized) { return cb("UNINITIALIZED"); }
if (!isMap(_data)) { return void cb("INVALID_ARGUMENTS"); }
var data = Util.clone(_data);
Object.keys(data).forEach(function (curve) {
if (!isValidId(curve) || isMap(ref.state.members[curve])) { return delete data[curve]; }
});
send(['INVITE', data], cb);
};
roster.accept = function (_data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (typeof(_data) !== 'string' || !isValidId(_data)) {
return void cb("INVALID_ARGUMENTS");
}
send([ 'ACCEPT', _data ], cb);
};
nThen(function (w) {
// get metadata so we know the owners and validateKey
anon_rpc.send('GET_METADATA', channel, function (err, data) {

View File

@@ -60,6 +60,8 @@ define([
// Messaging
ANSWER_FRIEND_REQUEST: Store.answerFriendRequest,
SEND_FRIEND_REQUEST: Store.sendFriendRequest,
// Team invitation
ANON_GET_PREVIEW_CONTENT: Store.anonGetPreviewContent,
// OnlyOffice
OO_COMMAND: Store.onlyoffice.execCommand,
// Cursor

View File

@@ -10,6 +10,8 @@ define([
'/common/outer/roster.js',
'/common/common-messaging.js',
'/common/common-feedback.js',
'/common/outer/invitation.js',
'/common/cryptget.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
@@ -19,7 +21,7 @@ define([
'/bower_components/saferphore/index.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Hash, Constants, Realtime,
ProxyManager, UserObject, SF, Roster, Messaging, Feedback,
ProxyManager, UserObject, SF, Roster, Messaging, Feedback, Invite, Crypt,
Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) {
var Team = {};
@@ -137,6 +139,15 @@ define([
if (membersChannel) { list.push(membersChannel); }
if (mailboxChannel) { list.push(mailboxChannel); }
var state = store.roster.getState();
if (state.members) {
Object.keys(state.members).forEach(function (curve) {
var m = state.members[curve];
if (m.inviteChannel && m.pending) { list.push(m.inviteChannel); }
if (m.previewChannel && m.pending) { list.push(m.previewChannel); }
});
}
list.sort();
return list;
};
@@ -1259,6 +1270,290 @@ define([
ctx.store.messenger.openTeamChat(team.getChatData(), onUpdate, cId, cb);
};
var createInviteLink = function (ctx, data, cId, _cb) {
var cb = Util.mkAsync(Util.once(_cb));
var teamId = data.teamId;
var team = ctx.teams[data.teamId];
var seeds = data.seeds; // {scrypt, preview}
var bytes64 = data.bytes64;
if (!teamId || !team) { return void cb({error: 'EINVAL'}); }
var roster = team.roster;
var teamName;
try {
teamName = roster.getState().metadata.name;
} catch (err) {
return void cb({ error: "TEAM_NAME_ERR" });
}
var message = data.message;
var name = data.name;
/*
var password = data.password;
var hash = data.hash;
*/
// derive { channel, cryptKey} for the preview content channel
var previewKeys = Invite.derivePreviewKeys(seeds.preview);
// derive {channel, cryptkey} for the invite content channel
var inviteKeys = Invite.deriveInviteKeys(bytes64);
// randomly generate ephemeral keys for ownership of the above content
// and a placeholder in the roster
var ephemeralKeys = Invite.generateKeys();
nThen(function (w) {
(function () {
// a random signing keypair to prevent further writes to the channel
// we don't need to remember it cause we're only writing once
var sign = Invite.generateSignPair(); // { validateKey, signKey}
var putOpts = {
initialState: '{}',
network: ctx.store.network,
metadata: {
owners: [ctx.store.proxy.edPublic, ephemeralKeys.edPublic]
}
};
putOpts.metadata.validateKey = sign.validateKey;
// visible with only the invite link
var previewContent = {
teamName: teamName,
message: message,
author: Messaging.createData(ctx.store.proxy, false),
displayName: name,
};
var cryptput_config = {
channel: previewKeys.channel,
type: 'pad',
version: 2,
keys: { // what would normally be provided by getSecrets
cryptKey: previewKeys.cryptKey,
validateKey: sign.validateKey, // sent to historyKeeper
signKey: sign.signKey, // b64EdPrivate
},
};
Crypt.put(cryptput_config, JSON.stringify(previewContent), w(function (err /*, doc */) {
if (err) {
console.error("CRYPTPUT_ERR", err);
w.abort();
return void cb({ error: "SET_PREVIEW_CONTENT" });
}
}), putOpts);
}());
(function () {
// a different random signing key so that the server can't correlate these documents
// as components of an invite
var sign = Invite.generateSignPair(); // { validateKey, signKey}
var putOpts = {
initialState: '{}',
network: ctx.store.network,
metadata: {
owners: [ctx.store.proxy.edPublic, ephemeralKeys.edPublic]
}
};
putOpts.metadata.validateKey = sign.validateKey;
// available only with the link and the content
var inviteContent = {
teamData: getInviteData(ctx, teamId, false),
ephemeral: {
edPublic: ephemeralKeys.edPublic,
edPrivate: ephemeralKeys.edPrivate,
curvePublic: ephemeralKeys.curvePublic,
curvePrivate: ephemeralKeys.curvePrivate,
},
};
var cryptput_config = {
channel: inviteKeys.channel,
type: 'pad',
version: 2,
keys: {
cryptKey: inviteKeys.cryptKey,
validateKey: sign.validateKey,
signKey: sign.signKey,
},
};
Crypt.put(cryptput_config, JSON.stringify(inviteContent), w(function (err /*, doc */) {
if (err) {
console.error("CRYPTPUT_ERR", err);
w.abort();
return void cb({ error: "SET_PREVIEW_CONTENT" });
}
}), putOpts);
}());
}).nThen(function (w) {
team.pin([inviteKeys.channel, previewKeys.channel], function (obj) {
if (obj && obj.error) { console.error(obj.error); }
});
Invite.createRosterEntry(team.roster, {
curvePublic: ephemeralKeys.curvePublic,
content: {
curvePublic: ephemeralKeys.curvePublic,
displayName: data.name,
pending: true,
inviteChannel: inviteKeys.channel,
previewChannel: previewKeys.channel,
}
}, w(function (err) {
if (err) {
w.abort();
cb(err);
}
}));
}).nThen(function () {
// call back empty if everything worked
cb();
});
};
var getPreviewContent = function (ctx, data, cId, cb) {
var seeds = data.seeds;
var previewKeys;
try {
previewKeys = Invite.derivePreviewKeys(seeds.preview);
} catch (err) {
return void cb({ error: "INVALID_SEEDS" });
}
Crypt.get({ // secrets
channel: previewKeys.channel,
type: 'pad',
version: 2,
keys: {
cryptKey: previewKeys.cryptKey,
},
}, function (err, val) {
if (err) { return void cb({ error: err }); }
if (!val) { return void cb({ error: 'DELETED' }); }
var json = Util.tryParse(val);
if (!json) { return void cb({ error: "parseError" }); }
console.error("JSON", json);
cb(json);
}, { // cryptget opts
network: ctx.store.network,
initialState: '{}',
});
};
var getInviteContent = function (ctx, data, cId, cb) {
var bytes64 = data.bytes64;
var previewKeys;
try {
previewKeys = Invite.deriveInviteKeys(bytes64);
} catch (err) {
return void cb({ error: "INVALID_SEEDS" });
}
Crypt.get({ // secrets
channel: previewKeys.channel,
type: 'pad',
version: 2,
keys: {
cryptKey: previewKeys.cryptKey,
},
}, function (err, val) {
if (err) { return void cb({error: err}); }
if (!val) { return void cb({error: 'DELETED'}); }
var json = Util.tryParse(val);
if (!json) { return void cb({error: "parseError"}); }
cb(json);
}, { // cryptget opts
network: ctx.store.network,
initialState: '{}',
});
};
var acceptLinkInvitation = function (ctx, data, cId, cb) {
var inviteContent;
var rosterState;
nThen(function (waitFor) {
// Get team keys and ephemeral keys
getInviteContent(ctx, data, cId, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
return void cb(obj);
}
inviteContent = obj;
}));
}).nThen(function (waitFor) {
// Check if you're already a member of this team
var chan = Util.find(inviteContent, ['teamData', 'channel']);
var myTeams = ctx.store.proxy.teams || {};
var isMember = Object.keys(myTeams).some(function (k) {
var t = myTeams[k];
return t.channel === chan;
});
if (isMember) {
waitFor.abort();
return void cb({error: 'ALREADY_MEMBER'});
}
// Accept the roster invitation: relplace our ephemeral keys with our user keys
var rosterData = Util.find(inviteContent, ['teamData', 'keys', 'roster']);
var myKeys = inviteContent.ephemeral;
if (!rosterData || !myKeys) {
waitFor.abort();
return void cb({error: 'INVALID_INVITE_CONTENT'});
}
var rosterKeys = Crypto.Team.deriveMemberKeys(rosterData.edit, myKeys);
Roster.create({
network: ctx.store.network,
channel: rosterData.channel,
keys: rosterKeys,
anon_rpc: ctx.store.anon_rpc,
}, waitFor(function (err, roster) {
if (err) {
waitFor.abort();
console.error(err);
return void cb({error: 'ROSTER_ERROR'});
}
var myData = Messaging.createData(ctx.store.proxy, false);
var state = roster.getState();
rosterState = state.members[myKeys.curvePublic];
roster.accept(myData.curvePublic, waitFor(function (err) {
roster.stop();
if (err) {
waitFor.abort();
console.error(err);
return void cb({error: 'ACCEPT_ERROR'});
}
}));
}));
}).nThen(function () {
var tempRpc = {};
initRpc(ctx, tempRpc, inviteContent.ephemeral, function (err) {
if (err) { return; }
var rpc = tempRpc.rpc;
if (rosterState.inviteChannel) {
rpc.removeOwnedChannel(rosterState.inviteChannel, function (err) {
if (err) { console.error(err); }
});
}
if (rosterState.previewChannel) {
rpc.removeOwnedChannel(rosterState.previewChannel, function (err) {
if (err) { console.error(err); }
});
}
});
// Add the team to our list and join...
joinTeam(ctx, {
team: inviteContent.teamData
}, cId, cb);
});
};
Team.init = function (cfg, waitFor, emit) {
var team = {};
var store = cfg.store;
@@ -1412,11 +1707,24 @@ define([
if (cmd === 'GET_EDITABLE_FOLDERS') {
return void getEditableFolders(ctx, data, clientId, cb);
}
if (cmd === 'CREATE_INVITE_LINK') {
return void createInviteLink(ctx, data, clientId, cb);
}
if (cmd === 'GET_PREVIEW_CONTENT') {
return void getPreviewContent(ctx, data, clientId, cb);
}
if (cmd === 'ACCEPT_LINK_INVITATION') {
return void acceptLinkInvitation(ctx, data, clientId, cb);
}
};
return team;
};
Team.anonGetPreviewContent = function (cfg, data, cb) {
getPreviewContent(cfg, data, null, cb);
};
return Team;
});

View File

@@ -675,6 +675,10 @@ define([
Cryptpad.messaging.answerFriendRequest(data, cb);
});
sframeChan.on('Q_ANON_GET_PREVIEW_CONTENT', function (data, cb) {
Cryptpad.anonGetPreviewContent(data, cb);
});
// History
sframeChan.on('Q_GET_FULL_HISTORY', function (data, cb) {
var crypto = Crypto.createEncryptor(secret.keys);

View File

@@ -1251,5 +1251,35 @@
"share_linkPasswordAlert": "Dieses Element ist passwortgeschützt. Wenn du diesen Link teilst, muss der Empfänger das Passwort eingeben.",
"share_contactPasswordAlert": "Dieses Element ist passwortgeschützt. Weil du es mit einem CryptPad-Kontakt teilst, muss der Empfänger das Passwort nicht eingeben.",
"share_embedPasswordAlert": "Dieses Element ist passwortgeschützt. Wenn du dieses Pad einbettest, werden Betrachter nach dem Passwort gefragt.",
"passwordFaqLink": "Mehr über Passwörter erfahren"
"passwordFaqLink": "Mehr über Passwörter erfahren",
"share_noContactsLoggedIn": "Du hast noch keine Kontakte bei CryptPad. Teile den Link zu deinem Profil, damit andere dir Kontaktanfragen senden können.",
"share_copyProfileLink": "Profil-Link kopieren",
"share_noContactsNotLoggedIn": "Logge dich ein oder registriere dich, um deine Kontakte zu sehen und neue hinzuzufügen.",
"contacts_mute": "Stummschalten",
"contacts_unmute": "Stummschaltung aufheben",
"contacts_manageMuted": "Stummschaltungen verwalten",
"contacts_mutedUsers": "Stummgeschaltete Accounts",
"contacts_muteInfo": "Du erhältst keine Benachrichtigungen oder Mitteilungen von stummgeschalteten Nutzern.<br>Sie werden nicht erfahren, dass du sie stummgeschaltet hast. ",
"team_inviteLinkTitle": "Erstelle eine personalisierte Einladung für dieses Team",
"team_inviteLinkTempName": "Vorläufiger Name (sichtbar in der Liste ausstehender Einladungen)",
"team_inviteLinkSetPassword": "Link mit einem Passwort schützen (empfohlen)",
"team_inviteLinkNote": "Persönliche Nachricht hinzufügen",
"team_inviteLinkNoteMsg": "Diese Nachricht wird angezeigt, bevor der Empfänger entscheidet, ob er diesem Team beitreten möchte.",
"team_inviteLinkLoading": "Dein Link wird generiert",
"team_inviteLinkWarning": "Die erste Person, die auf diesen Link zugreift, kann diesem Team beitreten und dessen Inhalte einsehen. Teile ihn sorgfältig.",
"team_inviteLinkErrorName": "Bitte gib einen Namen für die eingeladene Person ein. Er kann später geändert werden. ",
"team_inviteLinkCreate": "Link erstellen",
"team_inviteLinkCopy": "Link kopieren",
"team_inviteFrom": "Von:",
"team_inviteFromMsg": "{0} hat dich ins Team <b>{1}</b> eingeladen",
"team_invitePleaseLogin": "Bitte logge dich ein oder registriere dich, um diese Einladung anzunehmen.",
"team_inviteEnterPassword": "Bitte gib das Passwort für die Einladung ein, um fortzufahren.",
"team_invitePasswordLoading": "Einladung wird entschlüsselt",
"team_inviteJoin": "Team beitreten",
"team_inviteTitle": "Team-Einladung",
"team_inviteGetData": "Team-Daten werden abgerufen",
"team_cat_link": "Einladungslink",
"team_links": "Einladungslinks",
"team_inviteInvalidLinkError": "Dieser Einladungslink ist ungültig.",
"team_inviteLinkError": "Bei der Erstellung des Links ist ein Fehler aufgetreten."
}

View File

@@ -1,2 +1,37 @@
{
"type": {
"pad": "Teksti",
"code": "Koodi",
"poll": "Kysely",
"kanban": "Kanban",
"slide": "Esitys",
"drive": "CryptDrive",
"whiteboard": "Tussitaulu",
"file": "Tiedosto",
"media": "Media",
"todo": "Tehtävälista",
"contacts": "Yhteystiedot",
"sheet": "Taulukko (Beta)",
"teams": "Teams"
},
"button_newpad": "Uusi Teksti-padi",
"button_newcode": "Uusi Koodi-padi",
"button_newpoll": "Uusi Kysely",
"button_newslide": "Uusi Esitys",
"button_newwhiteboard": "Uusi Tussitaulu",
"button_newkanban": "Uusi Kanban",
"button_newsheet": "Uusi Taulukko",
"common_connectionLost": "<b>Yhteys palvelimelle katkennut</b><br>Sovellus on vain luku-tilassa, kunnes yhteys palaa.",
"websocketError": "Yhdistäminen websocket-palvelimelle epäonnistui...",
"typeError": "Tämä padi ei ole yhteensopiva valitun sovelluksen kanssa",
"onLogout": "Olet kirjautunut ulos, {0}klikkaa tästä{1} kirjautuaksesi sisään tai paina <em>Esc-näppäintä</em> käyttääksesi padia vain luku-tilassa.",
"wrongApp": "Reaaliaikaisen sisällön näyttäminen selaimessa epäonnistui. Ole hyvä ja yritä sivun lataamista uudelleen.",
"padNotPinned": "Tämä padi vanhenee kolmen kuukauden käyttämättömyyden jälkeen, {0}kirjaudu sisään{1} tai [2}rekisteröidy{3} säilyttääksesi sen.",
"padNotPinnedVariable": "Tämä padi vanhenee {4} päivän käyttämättömyyden jälkeen, {0}kirjaudu sisään{1} tai {2}rekisteröidy{3} säilyttääksesi sen.",
"anonymousStoreDisabled": "Tämän CryptPad-instanssin ylläpitäjä on estänyt anonyymien käyttäjien pääsyn tallennustilaan. Kirjaudu sisään käyttääksesi CryptDrivea.",
"expiredError": "Tämä padi on vanhentunut, eikä se ole enää saatavilla.",
"deletedError": "Tämä padi on poistettu omistajansa toimesta, eikä se ole enää saatavilla.",
"inactiveError": "Tämä padi on poistettu käyttämättömyyden vuoksi. Paina Esc-näppäintä luodaksesi uuden padin.",
"chainpadError": "Sisältöä päivitettäessä tapahtui vakava virhe. Tämä sivu on vain luku-tilassa, jotta tekemäsi muutokset eivät katoaisi.<br>Paina <em>Esc-näppäintä</em> jatkaaksesi padin katselua vain luku-tilassa, tai lataa sivu uudelleen yrittääksesi muokkaamista.",
"invalidHashError": "Pyytämäsi dokumentin URL-osoite on virheellinen."
}

View File

@@ -1254,5 +1254,32 @@
"passwordFaqLink": "En lire plus sur les mots de passe",
"share_noContactsLoggedIn": "Vous n'avez pas encore ajouté de contacts sur CryptPad. Partagez le lien de votre profil pour que l'on vous envoie des demandes de contact.",
"share_copyProfileLink": "Copier le lien du profil",
"share_noContactsNotLoggedIn": "Connectez-vous ou enregistrez-vous pour voir vos contacts ou en ajouter de nouveaux."
"share_noContactsNotLoggedIn": "Connectez-vous ou enregistrez-vous pour voir vos contacts ou en ajouter de nouveaux.",
"contacts_mute": "Masquer",
"contacts_unmute": "Réafficher",
"contacts_manageMuted": "Comptes masqués",
"contacts_mutedUsers": "Comptes masqués",
"contacts_muteInfo": "Vous ne receverez plus de notifications ou de messages si vous masquez ce compte.<br>L'utilisateur ne sera pas informé que vous l'avez masqué. ",
"team_inviteLinkTitle": "Créer une invitation personnalisée à cette équipe",
"team_inviteLinkTempName": "Nom temporaire (apparaît dans la liste des invitations en cours)",
"team_inviteLinkSetPassword": "Protéger le lien avec un mot de passe (recommandé)",
"team_inviteLinkNote": "Ajouter un message personnalisé",
"team_inviteLinkNoteMsg": "Ce message sera affiché avant que le destinataire décide de rejoindre cette équipe.",
"team_inviteLinkLoading": "Lien en cours de création",
"team_inviteLinkWarning": "La première personne qui accédera à ce lien pourra devenir membre de l'équipe et voir son contenu. Partagez le avec prudence.",
"team_inviteLinkErrorName": "Merci de remplir un nom de la personne que vous invitez. Ce nom pourra être changé . ",
"team_inviteLinkCreate": "Créer le lien",
"team_inviteLinkCopy": "Copier le lien",
"team_inviteFrom": "De :",
"team_inviteFromMsg": "{0} vous a invité à rejoindre l'équipe <b>{1}</b>",
"team_invitePleaseLogin": "Merci de vous inscrire ou de vous connecter pour accepter cette invitation.",
"team_inviteEnterPassword": "Entrez le mot de passe pour continuer.",
"team_invitePasswordLoading": "Déchiffrement de l'invitation",
"team_inviteJoin": "Rejoindre l'équipe",
"team_inviteTitle": "Invitation à une équipe",
"team_inviteGetData": "Obtention des données de l'équipe",
"team_cat_link": "Invitation",
"team_links": "Liens d'invitation",
"team_inviteInvalidLinkError": "Ce lien d'invitation n'est pas valide.",
"team_inviteLinkError": "Erreur lors de la génération du lien."
}

View File

@@ -1254,5 +1254,32 @@
"passwordFaqLink": "Read more about passwords",
"share_noContactsLoggedIn": "You are not connected with anyone on CryptPad yet. Share the link to your profile for people to send you contact requests.",
"share_copyProfileLink": "Copy profile link",
"share_noContactsNotLoggedIn": "Log in or register to see your existing contacts and add new ones."
"share_noContactsNotLoggedIn": "Log in or register to see your existing contacts and add new ones.",
"contacts_mute": "Mute",
"contacts_unmute": "Unmute",
"contacts_manageMuted": "Manage muted",
"contacts_mutedUsers": "Muted accounts",
"contacts_muteInfo": "You will not receive any notifications or messages from muted users.<br>They will not know you have muted them. ",
"team_inviteLinkTitle": "Create a personalized invitation to this team",
"team_inviteLinkTempName": "Temporary name (visible in pending invitations list)",
"team_inviteLinkSetPassword": "Protect the link with a password (recommended)",
"team_inviteLinkNote": "Add a personal message",
"team_inviteLinkNoteMsg": "This message will be shown before the recipient decides whether to join this team.",
"team_inviteLinkLoading": "Generating your link",
"team_inviteLinkWarning": "The first person to access this link will be able to join this team and view its contents. Share it carefully.",
"team_inviteLinkErrorName": "Please add a name for the person you're inviting. They can change it later. ",
"team_inviteLinkCreate": "Create link",
"team_inviteLinkCopy": "Copy link",
"team_inviteFrom": "From:",
"team_inviteFromMsg": "{0} has invited you to join the team <b>{1}</b>",
"team_invitePleaseLogin": "Please log in or register to accept this invitation.",
"team_inviteEnterPassword": "Please enter the invitation password to continue.",
"team_invitePasswordLoading": "Decrypting invitation",
"team_inviteJoin": "Join team",
"team_inviteTitle": "Team invitation",
"team_inviteGetData": "Getting team data",
"team_cat_link": "Invitation Link",
"team_links": "Invitation Links",
"team_inviteInvalidLinkError": "This invitation link is not valid.",
"team_inviteLinkError": "There was an error while creating the link."
}