Merge branch 'staging' into merge-owned

pull/1/head
ansuz 5 years ago
commit f63e787fc6

@ -1,3 +1,56 @@
# Thylacine release (3.19.0)
## Goals
The intent of this release was to catch up on our backlog of bug fixes and minor usability improvements.
## Update notes
This release features an update to our clientside dependencies.
To update to 3.19.0 from 3.18.1:
1. Stop your server
2. Get the latest code with git
3. Get the latest clientside dependencies with `bower update`
4. Restart your server
## Features
* The most notable change in this release is that the use of "safe links" (introduced in our 3.11.0 release) has been made the new default for documents. This means that when you open a document that is stored in your drive your browser's address bar will not contain the encryption keys for the document, only an identifier used to look up those encryption keys which are stored in your drive. This makes it less likely that you'll leak access to your documents during video meetings, when sharing screenshots, or when using shared computers that store the history of pages you've viewed.
* To share access to documents with links, you'll need to use the _share menu_ which has recently been made more prominent in the platform's toolbars
* This setting is configurable, so you can still choose to disable the use of safe links via your settings page.
* We've updated the layout of the "user admin menu" which can be found in the top-right corner by clicking your avatar. It features an "About CryptPad" menu which displays the version of the instance you're using as well as some resources which are otherwise only available via the footer of static pages.
* We often receive support tickets in languages that we don't speak, which forces us to use translation services in order to answer questions. To address this issue, we've made it possible for admins to display a notice indicating which languages they speak. An example configuration is provided in `customize.dist/application_config.js`.
* We've integrated two PRs:
1. [Only list premium features when subscriptions are enabled](https://github.com/xwiki-labs/cryptpad/pull/538).
2. [Add privacy policy option](https://github.com/xwiki-labs/cryptpad/pull/537).
* We found it cumbersome to add new cards to the top of our Kanban columns, since we had to create a new card at the bottom and then drag it to the top. In response, we've broken up the rather large "new card" button into two buttons, one which adds a card at the top, and another which adds a new card at the bottom.
* We've made it easier to use tags for files in the drive:
1. You can now select multiple files and apply a set of tags to all of them.
2. Hitting "enter" in an empty tag prompt field will submit the current list of tags.
* We've also made a few tweaks to the kanban layout:
1. The "trash bar" only appears while you are actively dragging a card.
2. The "tag list" now takes up more of the available width, while the button to clear the currently applied tag filter has been moved to the left, replacing the "filter by tag" hint text.
* We've received requests to enable translations for a number of languages over the last few months. The following languages are enabled on [our weblate instance](https://weblate.cryptpad.fr/projects/cryptpad/app/), but have yet to be translated.
* Arabic
* Hindi
* Telugu
* Turkish
* Unregistered users were able to open up the "filepicker modal" in spreadsheets. It was already possible to embed an image which they'd already stored in their drive, but it was not clear why they were not able to upload a new image. We now display a disabled upload button with a tooltip to log in or register in order to upload images.
* Finally, we've updated the styles in our presentation editor to better match our recent toolbar redesign and the mermaidjs integration.
## Bug fixes
* We now preserve formatting in multi-line messages in team invitations.
* The slide editor exhibited some strange behaviour where the page would reload the first time you entered "present mode" after creating the document. We've also fixed some issues with printing.
* We now prevent the local resizing of images in the rich text editor while it is locked due to disconnection or the lack of edit rights.
* We've updated our marked.js dependency to the latest version in order to correct some minor rendering bugs.
* Unregistered users are now redirected to the login page when they visit the support page.
* We've removed the unsupported "rename" entry from the right-click menu in unregistered users drives.
* After a deep investigation we found and fixed the cause of a bug in which user accounts spontaneously removed themselves from teams. A flaw in the serverside cache caused clients to load an incomplete account of the team's membership which caused the team to appear to have been deleted. Unfortunately, the client responded by removing the corrupt team credentials from their account. Our fix will prevent future corruptions, but does not restore unintentionally removed teams.
* Lastly, we've added a "Hind" font to the spreadsheet editor which introduces basic support for Devanagari characters.
# Smilodon's revenge (3.18.1)
Our next major release (3.19.0) is still a few weeks away.

@ -62,7 +62,13 @@ define([
var imprintUrl = AppConfig.imprint && (typeof(AppConfig.imprint) === "boolean" ?
'/imprint.html' : AppConfig.imprint);
Pages.versionString = "CryptPad v3.18.1 (Smilodon's revenge)";
Pages.versionString = "CryptPad v3.19.0 (Thylacine)";
// used for the about menu
Pages.imprintLink = AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined;
Pages.privacyLink = footLink(AppConfig.privacy, 'privacy');
Pages.githubLink = footLink('https://github.com/xwiki-labs/cryptpad', null, 'GitHub');
Pages.faqLink = footLink('/faq.html', 'faq_link');
Pages.infopageFooter = function () {
return h('footer', [
@ -74,24 +80,14 @@ define([
languageSelector()
])
], ''),
/*footerCol('footer_applications', [
footLink('/drive/', 'main_drive'),
footLink('/pad/', 'main_richText'),
footLink('/code/', 'main_code'),
footLink('/slide/', 'main_slide'),
footLink('/poll/', 'main_poll'),
footLink('/kanban/', 'main_kanban'),
footLink('/whiteboard/', null, Msg.type.whiteboard)
]),*/
footerCol('footer_product', [
footLink('https://cryptpad.fr/what-is-cryptpad.html', 'topbar_whatIsCryptpad'),
footLink('/faq.html', 'faq_link'),
footLink('https://github.com/xwiki-labs/cryptpad', null, 'GitHub'),
footLink('/what-is-cryptpad.html', 'topbar_whatIsCryptpad'),
Pages.faqLink,
Pages.githubLink,
footLink('https://opencollective.com/cryptpad/contribute/', 'footer_donate'),
]),
footerCol('footer_aboutUs', [
/*footLink('https://blog.cryptpad.fr', 'blog'),
footLink('https://labs.xwiki.com', null, 'XWiki Labs'),*/
/*footLink('https://blog.cryptpad.fr', 'blog'), */
footLink('http://www.xwiki.com', null, 'XWiki SAS'),
footLink('https://www.open-paas.org', null, 'OpenPaaS'),
footLink('/about.html', 'footer_team'),
@ -99,15 +95,9 @@ define([
]),
footerCol('footer_legal', [
footLink('/terms.html', 'footer_tos'),
footLink(AppConfig.privacy, 'privacy'),
AppConfig.imprint ? footLink(imprintUrl, 'imprint') : undefined,
Pages.privacyLink,
Pages.imprintLink,
]),
/*footerCol('footer_contact', [
footLink('https://riot.im/app/#/room/#cryptpad:matrix.org', null, 'Chat'),
footLink('https://twitter.com/cryptpad', null, 'Twitter'),
footLink('https://github.com/xwiki-labs/cryptpad', null, 'GitHub'),
footLink('/contact.html', null, 'Email')
])*/
])
]),
h('div.cp-version-footer', Pages.versionString)
@ -150,13 +140,9 @@ define([
h('a.navbar-brand', { href: '/index.html'}),
button,
h('div.collapse.navbar-collapse.justify-content-end#menuCollapse', [
//h('a.nav-item.nav-link', { href: '/what-is-cryptpad.html'}, Msg.topbar_whatIsCryptpad), // Moved the FAQ
//h('a.nav-item.nav-link', { href: '/faq.html'}, Msg.faq_link),
h('a.nav-item.nav-link', { href: 'https://blog.cryptpad.fr/'}, Msg.blog),
h('a.nav-item.nav-link', { href: '/features.html'}, Msg.pricing),
h('a.nav-item.nav-link', { href: '/privacy.html'}, Msg.privacy),
//h('a.nav-item.nav-link', { href: '/contact.html'}, Msg.contact),
//h('a.nav-item.nav-link', { href: '/about.html'}, Msg.about),
].concat(rightLinks))
);
};

@ -10,7 +10,6 @@
margin: 15px 0;
cursor: pointer;
height: @variables_bar-height;
line-height: @variables_bar-height - 10px;
.fa, .cptools {
display: inline-flex;
justify-content: center;

@ -4,6 +4,9 @@
@msg-bg: #eee;
@fromme-bg: #ddd;
.cp-support-form-container {
div {
margin-bottom: 10px;
}
[type="text"] {
width: @sidebar_button-width;
margin-bottom: 10px;
@ -15,6 +18,18 @@
height: 300px;
}
}
.cp-support-attachments {
display: flex;
.fa {
cursor: pointer;
margin-right: 10px;
}
&> span {
border: 1px solid #ddd;
margin-right: 5px;
padding: 10px;
}
}
.cp-support-container {
.cp-support-list-ticket {
display: flex;

@ -325,6 +325,9 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash) {
}
}));
}).nThen((waitFor) => {
/* TODO we can skip updating the index if there's nobody in the channel.
Populating it might actually be the cause of a memory leak.
*/
getIndex(Env, id, waitFor((err, index) => {
if (err) {
Log.warn("HK_STORE_MESSAGE_INDEX", err.stack);
@ -340,7 +343,12 @@ const storeMessage = function (Env, channel, msg, isCp, optionalMessageHash) {
line: ((index.line || 0) + 1)
});
}
if (optionalMessageHash) {
/* This 'getIndex' call will construct a new index if one does not already exist.
If that is the case then our message will already be present and updating our offset map
can actually cause it to become incorrect, leading to incorrect behaviour when clients connect
with a lastKnownHash. We avoid this by only assigning new offsets to the map.
*/
if (optionalMessageHash && typeof(index.offsetByHash[optionalMessageHash]) === 'undefined') {
index.offsetByHash[optionalMessageHash] = index.size;
index.offsets++;
}

2
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "cryptpad",
"version": "3.18.1",
"version": "3.19.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

@ -1,7 +1,7 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "3.18.1",
"version": "3.19.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",

@ -0,0 +1,116 @@
/* globals process */
var Client = require("../../lib/client");
var Nacl = require("tweetnacl/nacl-fast");
var nThen = require("nthen");
var CPNetflux = require("../../www/bower_components/chainpad-netflux/chainpad-netflux");
var Hash = require("../../www/common/common-hash");
var Rpc = require("../../www/common/rpc");
var HK = require("../../lib/hk-util");
var identity = function (x) {
return x;
};
var crypto = {
encrypt: identity,
decrypt: identity,
};
var N = 2;
var BREAK;
BREAK = 1;
var client;
nThen(function (w) {
//console.log("Creating client");
Client.create(w(function (err, _client) {
if (err) {
console.error(err);
process.exit(1);
}
client = _client;
}));
}).nThen(function (w) {
//console.log("Creating RPC module");
Rpc.createAnonymous(client.config.network, w(function (err, rpc) {
if (err) {
w.abort();
return void console.error('ANON_RPC_CONNECT_ERR');
}
client.anonRpc = rpc;
}));
}).nThen(function (w) {
var done = w();
//console.log("sending random messages");
client.channel = Hash.createChannelId();
if (BREAK) {
CPNetflux.start({
//lastKnownHash: HK.getHash(client.sent[0]),
network: client.config.network,
channel: client.channel,
crypto: crypto,
noChainPad: true,
onReady: w(),
//onMessage: onMessage,
});
}
// send a few random messages to a channel
client.sent = [];
var i = N;
var send = function () {
//console.log(i);
if (i-- <= 0) { return void done(); }
var ciphertext = Nacl.util.encodeBase64(Nacl.randomBytes(256));
client.anonRpc.send('WRITE_PRIVATE_MESSAGE', [
client.channel,
ciphertext
], function (err) {
if (err) {
console.error(err);
process.exit(1);
}
client.sent.push(ciphertext);
console.log("sent: %s", ciphertext);
//setTimeout(send, 500);
send();
});
};
send();
}).nThen(function () {
//process.exit(1);
// connect to that channel with a lastKnownHash
// check if the first message received has the hash that you asked for
console.log();
var lkh = HK.getHash(client.sent[0]);
var i = 0;
var onMessage = function (msg, user, vKey, isCp, hash /*, author */) {
if (i === 0 && hash !== lkh) {
console.error('incorrect hash: [%s]', hash);
process.exit(1);
}
console.log(msg);
if (++i >= N) {
process.exit(1);
}
};
CPNetflux.start({
lastKnownHash: lkh,
network: client.config.network,
channel: client.channel,
crypto: crypto,
noChainPad: true,
onMessage: onMessage,
});
});

@ -185,6 +185,20 @@ define([
var $container = makeBlock('support-list');
var $div = $(h('div.cp-support-container')).appendTo($container);
var catContainer = h('div.cp-dropdown-container');
$div.append(catContainer);
var category = 'all';
var $drop = APP.support.makeCategoryDropdown(catContainer, function (key) {
category = key;
if (key === 'all') {
$div.find('.cp-support-list-ticket').show();
return;
}
$div.find('.cp-support-list-ticket').hide();
$div.find('.cp-support-list-ticket[data-cat="'+key+'"]').show();
}, true);
$drop.setValue('all');
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var cat = privateData.category || '';
@ -277,6 +291,9 @@ define([
UI.alert(Messages.error);
});
});
if (category !== 'all' && $ticket.attr('data-cat') !== category) {
$ticket.hide();
}
}
$ticket.append(APP.support.makeMessage(content, hash));
reorder();

@ -328,11 +328,7 @@ define([
var input = dialog.textInput();
var tagger = dialog.frame([
dialog.message([
Messages.tags_add,
h('br'),
Messages.tags_searchHint,
]),
dialog.message([ Messages.tags_add ]),
input,
h('center', h('small', Messages.tags_notShared)),
dialog.nav(),

@ -313,26 +313,30 @@ define([
var $friends = $div.find('.cp-usergrid-user.cp-selected');
$friends.each(function (i, el) {
var curve = $(el).attr('data-curve');
// Check if the selected element is a friend or a team
if (curve) { // Friend
if (!curve || !friends[curve]) { return; }
var friend = friends[curve];
if (!friend.notifications || !friend.curvePublic) { return; }
common.mailbox.sendTo("SHARE_PAD", {
href: href,
password: config.password,
isTemplate: config.isTemplate,
name: myName,
title: title
}, {
channel: friend.notifications,
curvePublic: friend.curvePublic
});
return;
}
// Team
var ed = $(el).attr('data-ed');
var friend = curve && friends[curve];
var team = teams[ed];
// If the selected element is a friend or a team without edit right,
// send a notification
var mailbox = friend || ((team && team.viewer) ? team : undefined);
if (mailbox) { // Friend
if (friends[curve] && !mailbox.notifications) { return; }
if (mailbox.notifications && mailbox.curvePublic) {
common.mailbox.sendTo("SHARE_PAD", {
href: href,
password: config.password,
isTemplate: config.isTemplate,
name: myName,
title: title
}, {
viewed: team && team.id,
channel: mailbox.notifications,
curvePublic: mailbox.curvePublic
});
return;
}
}
// If it's a team with edit right, add the pad directly
if (!team) { return; }
sframeChan.query('Q_STORE_IN_TEAM', {
href: href,
@ -450,10 +454,11 @@ define([
// config.teamId only exists when we're trying to share a pad from a team drive
// In this case, we don't want to share the pad with the current team
if (config.teamId && config.teamId === id) { return; }
if (!teamsData[id].hasSecondaryKey) { return; }
var t = teamsData[id];
teams[t.edPublic] = {
notifications: true,
viewer: !teamsData[id].hasSecondaryKey,
notifications: t.notifications,
curvePublic: t.curvePublic,
displayName: t.name,
edPublic: t.edPublic,
avatar: t.avatar,
@ -1569,7 +1574,6 @@ define([
button = $('<button>', {
title: Messages.printButtonTitle2,
'class': "fa fa-print cp-toolbar-icon-print",
// XXX people don't realize this does PDF (https://github.com/xwiki-labs/cryptpad/issues/357#issuecomment-640711724)
}).append($('<span>', {'class': 'cp-toolbar-drawer-element'}).text(Messages.printText));
break;
case 'history':
@ -2204,7 +2208,9 @@ define([
if (config.isSelect && value) {
var $val = $innerblock.find('[data-value="'+value+'"]');
setActive($val);
$innerblock.scrollTop($val.position().top + $innerblock.scrollTop());
try {
$innerblock.scrollTop($val.position().top + $innerblock.scrollTop());
} catch (e) {}
}
if (config.feedback) { Feedback.send(config.feedback); }
};
@ -2303,29 +2309,38 @@ define([
var priv = metadataMgr.getPrivateData();
var origin = priv.origin;
Messages.help_faq = "Review our list of frequently asked questions"; // XXX
var faqLine = h('p',
h('a', {
target: '_blank',
rel: 'noreferrer noopener',
href: origin + '/faq.html',
}, Messages.help_faq)
);
// XXX link to the most recent changelog/release notes
// XXX FAQ
// XXX GitHub
// XXX privacy policy
// XXX legal notice
var content = h('div', [
// CryptPad version number
// TODO link to the most recent changelog/release notes
// https://github.com/xwiki-labs/cryptpad/releases/latest/ ?
var template = function (line, link) {
if (!line || !link) { return; }
var p = $('<p>').html(line)[0];
var sub = link.cloneNode(true);
/* This is a hack to make relative URLs point to the main domain
instead of the sandbox domain. It will break if the admins have specified
some less common URL formats for their customizable links, such as if they've
used a protocal-relative absolute URL. The URL API isn't quite safe to use
because of IE (thanks, Bill). */
var href = sub.getAttribute('href');
if (/^\//.test(href)) { sub.setAttribute('href', origin + href); }
var a = p.querySelector('a');
if (!a) { return; }
sub.innerText = a.innerText;
p.replaceChild(sub, a);
return p;
};
var legalLine = template(Messages.info_imprintFlavour, Pages.imprintLink);
var privacyLine = template(Messages.info_privacyFlavour, Pages.privacyLink);
var faqLine = template(Messages.help.generic.more, Pages.faqLink);
var content = h('div.cp-info-menu-container', [
h('h6', Pages.versionString),
// First point users to our FAQ
h('hr'),
legalLine,
privacyLine,
faqLine,
// Link to the support ticket form in case their
// question isn't answered by the FAQ
//supportLine,
]);
var buttons = [
@ -2373,15 +2388,21 @@ define([
content: $userAdminContent.html()
});
}
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': origin+'/index.html',
'class': 'fa fa-home'
},
content: h('span', Messages.homePage)
});
if (accountName && !AppConfig.disableProfile) {
options.push({
tag: 'a',
attributes: {'class': 'cp-toolbar-menu-profile fa fa-user-circle'},
content: h('span', Messages.profileButton),
action: function () {
if (padType) {
window.open(origin+'/profile/');
} else {
window.parent.location = origin+'/profile/';
}
},
});
}
if (padType !== 'drive' || (!accountName && priv.newSharedFolder)) {
options.push({
tag: 'a',
@ -2415,29 +2436,6 @@ define([
content: h('span', Messages.type.contacts)
});
}
options.push({ tag: 'hr' });
// Add the change display name button if not in read only mode
if (config.changeNameButtonCls && config.displayChangeName && !AppConfig.disableProfile) {
options.push({
tag: 'a',
attributes: {'class': config.changeNameButtonCls + ' fa fa-user'},
content: h('span', Messages.user_rename)
});
}
if (accountName && !AppConfig.disableProfile) {
options.push({
tag: 'a',
attributes: {'class': 'cp-toolbar-menu-profile fa fa-user-circle'},
content: h('span', Messages.profileButton),
action: function () {
if (padType) {
window.open(origin+'/profile/');
} else {
window.parent.location = origin+'/profile/';
}
},
});
}
if (padType !== 'settings') {
options.push({
tag: 'a',
@ -2452,6 +2450,7 @@ define([
},
});
}
options.push({ tag: 'hr' });
// Add administration panel link if the user is an admin
if (priv.edPublic && Array.isArray(Config.adminKeys) && Config.adminKeys.indexOf(priv.edPublic) !== -1) {
@ -2482,30 +2481,6 @@ define([
},
});
}
options.push({ tag: 'hr' });
if (Config.allowSubscriptions) {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': priv.plan ? priv.accounts.upgradeURL : origin+'/features.html',
'class': 'fa fa-star-o'
},
content: h('span', priv.plan ? Messages.settings_cat_subscription : Messages.pricing)
});
}
if (!priv.plan && !Config.removeDonateButton) {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'rel': 'noopener',
'href': priv.accounts.donateURL,
'class': 'fa fa-gift'
},
content: h('span', Messages.crowdfunding_button2)
});
}
if (AppConfig.surveyURL) {
options.push({
tag: 'a',
@ -2521,7 +2496,6 @@ define([
},
});
}
Messages.user_about = 'About CryptPad'; // XXX
options.push({
tag: 'a',
attributes: {
@ -2533,6 +2507,49 @@ define([
},
});
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': origin+'/index.html',
'class': 'fa fa-home'
},
content: h('span', Messages.homePage)
});
// Add the change display name button if not in read only mode
/*
if (config.changeNameButtonCls && config.displayChangeName && !AppConfig.disableProfile) {
options.push({
tag: 'a',
attributes: {'class': config.changeNameButtonCls + ' fa fa-user'},
content: h('span', Messages.user_rename)
});
}*/
options.push({ tag: 'hr' });
if (Config.allowSubscriptions) {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'href': priv.plan ? priv.accounts.upgradeURL : origin+'/features.html',
'class': 'fa fa-star-o'
},
content: h('span', priv.plan ? Messages.settings_cat_subscription : Messages.pricing)
});
}
if (!priv.plan && !Config.removeDonateButton) {
options.push({
tag: 'a',
attributes: {
'target': '_blank',
'rel': 'noopener',
'href': priv.accounts.donateURL,
'class': 'fa fa-gift'
},
content: h('span', Messages.crowdfunding_button2)
});
}
options.push({ tag: 'hr' });
// Add login or logout button depending on the current status
if (priv.loggedIn) {

@ -943,6 +943,7 @@ define([
// Ctrl+A select all
if (e.which === 65 && (e.ctrlKey || (e.metaKey && APP.isMac))) {
e.preventDefault();
$content.find('.cp-app-drive-element:not(.cp-app-drive-element-selected)')
.each(function (idx, element) {
selectElement($(element));

@ -84,6 +84,10 @@ define([
// Share pad
Messages.notification_padSharedTeam = "{0} has shared a pad with the team {2}: <b>{1}</b>"; // XXX
Messages.notification_fileSharedTeam = "{0} has shared a file with the team {2}: <b>{1}</b>"; // XXX
Messages.notification_folderSharedTeam = "{0} has shared a pad with the team {2}: <b>{1}</b>"; // XXX
handlers['SHARE_PAD'] = function(common, data) {
var content = data.content;
var msg = content.msg;
@ -91,10 +95,22 @@ define([
var key = type === 'drive' ? 'notification_folderShared' :
(type === 'file' ? 'notification_fileShared' :
'notification_padShared');
var teamNotification = /^team-/.test(data.type) && Number(data.type.slice(5));
var teamName = '';
if (teamNotification) {
var privateData = common.getMetadataMgr().getPrivateData();
var teamsData = Util.tryParse(JSON.stringify(privateData.teams)) || {};
var team = teamsData[teamNotification];
if (!team || !team.name) { return; }
key += "Team";
teamName = Util.fixHTML(team.name);
}
var name = Util.fixHTML(msg.content.name) || Messages.anonymous;
var title = Util.fixHTML(msg.content.title);
content.getFormatText = function() {
return Messages._getKey(key, [name, title]);
return Messages._getKey(key, [name, title, teamName]);
};
content.handler = function() {
var todo = function() {
@ -105,6 +121,9 @@ define([
if (msg.content.isTemplate) {
common.sessionStorage.put(Constants.newPadPathKey, ['template'], waitFor());
}
if (teamNotification) {
common.sessionStorage.put(Constants.newPadTeamKey, teamNotification, waitFor());
}
common.sessionStorage.put('newPadPassword', msg.content.password || '', waitFor());
}).nThen(function() {
todo();
@ -388,8 +407,6 @@ define([
};
handlers['SAFE_LINKS_DEFAULT'] = function (common, data) {
Messages.settings_safeLinkDefault = "SAFE LINKS ARE NOW DEFAULT"; // XXX
var content = data.content;
content.getFormatText = function () {
return Messages.settings_safeLinkDefault;

@ -122,7 +122,6 @@ define([
if (!state && !readOnly) {
$('#cp-app-oo-editor').append(h('div#cp-app-oo-offline'));
}
debug(state);
};
var deleteOffline = function () {
@ -462,6 +461,20 @@ define([
});
}
};
var deleteLastCp = function () {
var hashes = content.hashes;
if (!hashes || !Object.keys(hashes).length) { return; }
var i = 0;
var idx = Object.keys(hashes).map(Number).sort(function (a, b) {
return a-b;
});
var lastIndex = idx[idx.length - 1 - i];
delete content.hashes[lastIndex];
APP.onLocal();
APP.realtime.onSettle(function () {
UI.log(Messages.saved);
});
};
var restoreLastCp = function () {
content.saveLock = myOOId;
APP.onLocal();
@ -492,6 +505,89 @@ define([
}, to);
};
var loadInitDocument = function (type, useNewDefault) {
var newText;
switch (type) {
case 'sheet' :
newText = EmptyCell(useNewDefault);
break;
case 'oodoc':
newText = EmptyDoc();
break;
case 'ooslide':
newText = EmptySlide();
break;
default:
newText = '';
}
return new Blob([newText], {type: 'text/plain'});
};
var loadLastDocument = function (lastCp, onCpError, cb) {
ooChannel.cpIndex = lastCp.index || 0;
var parsed = Hash.parsePadUrl(lastCp.file);
var secret = Hash.getSecrets('file', parsed.hash);
if (!secret || !secret.channel) { return; }
var hexFileName = secret.channel;
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (/^4/.test('' + this.status)) {
onCpError();
return void console.error('XHR error', this.status);
}
var arrayBuffer = xhr.response;
if (arrayBuffer) {
var u8 = new Uint8Array(arrayBuffer);
FileCrypto.decrypt(u8, key, function (err, decrypted) {
if (err) { return void console.error(err); }
var blob = new Blob([decrypted.content], {type: 'plain/text'});
if (cb) {
return cb(blob, getFileType());
}
startOO(blob, getFileType());
});
}
};
xhr.onerror = function () {
onCpError();
};
xhr.send(null);
};
Messages.oo_refresh = "Refresh"; // XXX read-only corner popup when receiving remote updates
Messages.oo_refreshText = "out of date"; // XXX read-only corner popup when receiving remote updates
var refreshReadOnly = function () {
var cancel = h('button.cp-corner-cancel', Messages.cancel);
var reload = h('button.cp-corner-primary', [
h('i.fa.fa-refresh'),
Messages.oo_refresh
]);
var actions = h('div', [cancel, reload]);
var m = UI.cornerPopup(Messages.oo_refreshText, actions, '');
$(reload).click(function () {
ooChannel.ready = false;
var lastCp = getLastCp();
loadLastDocument(lastCp, function () {
var file = getFileType();
var type = common.getMetadataMgr().getPrivateData().ooType;
var blob = loadInitDocument(type, true);
resetData(blob, file);
}, function (blob, file) {
resetData(blob, file);
});
delete APP.refreshPopup;
m.delete();
});
$(cancel).click(function () {
delete APP.refreshPopup;
m.delete();
});
};
var openRtChannel = function (cb) {
if (rtChannel.ready) { return void cb(); }
@ -515,6 +611,18 @@ define([
break;
case 'MESSAGE':
if (ooChannel.ready) {
// In read-only mode, push the message to the queue and prompt
// the user to refresh OO (without reloading the page)
if (readOnly) {
ooChannel.queue.push(obj.data);
if (APP.refreshPopup) { return; }
APP.refreshPopup = true;
// Don't "spam" the user instantly and no more than
// 1 popup every 30s
setTimeout(refreshReadOnly, 30000);
return;
}
ooChannel.send(obj.data.msg);
ooChannel.lastHash = obj.data.hash;
ooChannel.cpIndex++;
@ -972,8 +1080,10 @@ define([
ooChannel.queue.forEach(function (data) {
ooChannel.send(data.msg);
});
var last = ooChannel.queue.pop();
if (last) { ooChannel.lastHash = last.hash; }
if (!readOnly) {
var last = ooChannel.queue.pop();
if (last) { ooChannel.lastHash = last.hash; }
}
ooChannel.cpIndex += ooChannel.queue.length;
// Apply existing locks
deleteOfflineLocks();
@ -1004,7 +1114,7 @@ define([
UI.openCustomModal(UI.dialog.customModal(div, {buttons: []}));
setTimeout(function () {
makeCheckpoint(true);
}, 1000);
}, 5000);
}
}
}
@ -1433,41 +1543,6 @@ define([
}, 100);
};
var loadLastDocument = function (lastCp, onCpError, cb) {
ooChannel.cpIndex = lastCp.index || 0;
var parsed = Hash.parsePadUrl(lastCp.file);
var secret = Hash.getSecrets('file', parsed.hash);
if (!secret || !secret.channel) { return; }
var hexFileName = secret.channel;
var fileHost = privateData.fileHost || privateData.origin;
var src = fileHost + Hash.getBlobPathFromHex(hexFileName);
var key = secret.keys && secret.keys.cryptKey;
var xhr = new XMLHttpRequest();
xhr.open('GET', src, true);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
if (/^4/.test('' + this.status)) {
onCpError();
return void console.error('XHR error', this.status);
}
var arrayBuffer = xhr.response;
if (arrayBuffer) {
var u8 = new Uint8Array(arrayBuffer);
FileCrypto.decrypt(u8, key, function (err, decrypted) {
if (err) { return void console.error(err); }
var blob = new Blob([decrypted.content], {type: 'plain/text'});
if (cb) {
return cb(blob, getFileType());
}
startOO(blob, getFileType());
});
}
};
xhr.onerror = function () {
onCpError();
};
xhr.send(null);
};
var loadDocument = function (noCp, useNewDefault, i) {
if (ooLoaded) { return; }
var type = common.getMetadataMgr().getPrivateData().ooType;
@ -1497,7 +1572,7 @@ define([
default:
newText = '';
}
var blob = new Blob([newText], {type: 'text/plain'});
var blob = loadInitDocument(type, useNewDefault);
startOO(blob, file);
};
@ -1584,6 +1659,14 @@ define([
$save.appendTo(toolbar.$bottomM);
}
if (window.CP_DEV_MODE || DISPLAY_RESTORE_BUTTON) {
common.createButton('', true, {
name: 'delete',
icon: 'fa-trash',
hiddenReadOnly: true
}).click(function () {
if (initializing) { return void console.error('initializing'); }
deleteLastCp();
}).attr('title', 'Delete last checkpoint').appendTo(toolbar.$bottomM);
common.createButton('', true, {
name: 'restore',
icon: 'fa-history',
@ -1609,6 +1692,7 @@ define([
}
if (common.isLoggedIn()) {
window.CryptPad_deleteLastCp = deleteLastCp;
var $importXLSX = common.createButton('import', true, {
accept: accept,
binary : ["ods", "xlsx", "odt", "docx", "odp", "pptx"]
@ -1765,10 +1849,10 @@ define([
var latest = getLastCp(true);
var newLatest = getLastCp();
if (newLatest.index > latest.index) {
ooChannel.queue = [];
var hasDrawings = checkDrawings();
if (hasDrawings) {
ooChannel.ready = false;
ooChannel.queue = [];
}
// New checkpoint
sframeChan.query('Q_OO_SAVE', {

@ -751,8 +751,7 @@ define([
force: true
}, waitFor());
}).nThen(function () {
// XXX users need to login after registration if they register after account deletion (token issue?)
// XXX delete block
// TODO delete block
// Log out current worker
postMessage(clientId, "DELETE_ACCOUNT", token, function () {});
store.network.disconnect();

@ -109,6 +109,17 @@ proxy.mailboxes = {
});
var ciphertext = crypto.encrypt(text, user.curvePublic);
// If we've sent this message to one of our teams' mailbox, we may want to "dismiss" it
// automatically
if (user.viewed) {
var team = Util.find(ctx, ['store', 'proxy', 'teams', user.viewed]);
if (team) {
var hash = ciphertext.slice(0,64);
var viewed = Util.find(team, ['keys', 'mailbox', 'viewed']);
if (Array.isArray(viewed)) { viewed.push(hash); }
}
}
anonRpc.send("WRITE_PRIVATE_MESSAGE", [
user.channel,
ciphertext
@ -126,10 +137,9 @@ proxy.mailboxes = {
var dismiss = function (ctx, data, cId, cb) {
var type = data.type;
var hash = data.hash;
var m = Util.find(ctx, ['store', 'proxy', 'mailboxes', type]);
if (!m) { return void cb({error: 'NOT_FOUND'}); }
var box = ctx.boxes[type];
if (!box) { return void cb({error: 'NOT_LOADED'}); }
var m = box.data || {};
// If the hash in in our history, get the index from the history:
// - if the index is 0, we can change our lastKnownHash
@ -191,7 +201,16 @@ proxy.mailboxes = {
};
var openChannel = function (ctx, type, m, onReady) {
var leaveChannel = function (ctx, type, cb) {
var box = ctx.boxes[type];
if (!box) { return void cb(); }
if (!box.cpNf || typeof(box.cpNf.stop) !== "function") { return void cb('EINVAL'); }
box.cpNf.stop();
delete ctx.boxes[type];
};
var openChannel = function (ctx, type, m, onReady, opts) {
console.error(type, m, opts);
opts = opts || {};
var box = ctx.boxes[type] = {
channel: m.channel,
type: type,
@ -210,7 +229,8 @@ proxy.mailboxes = {
console.error(e);
}
box.queue.push(msg);
}
},
data: m
};
if (!Crypto.Mailbox) {
return void console.error("chainpad-crypto is outdated and doesn't support mailboxes.");
@ -224,7 +244,7 @@ proxy.mailboxes = {
channel: m.channel,
noChainPad: true,
crypto: crypto,
owners: [ctx.store.proxy.edPublic],
owners: opts.owners || [ctx.store.proxy.edPublic],
lastKnownHash: m.lastKnownHash
};
cfg.onConnectionChange = function () {}; // Allow reconnections in chainpad-netflux
@ -346,7 +366,7 @@ proxy.mailboxes = {
// Continue
onReady();
};
CpNetflux.start(cfg);
box.cpNf = CpNetflux.start(cfg);
};
var initializeHistory = function (ctx) {
@ -467,6 +487,19 @@ proxy.mailboxes = {
}
});
Object.keys(store.proxy.teams || {}).forEach(function (teamId) {
var team = store.proxy.teams[teamId];
if (!team) { return; }
var teamMailbox = team.keys.mailbox || {};
if (!teamMailbox.channel) { return; }
var opts = {
owners: [Util.find(team, ['keys', 'drive', 'edPublic'])]
};
openChannel(ctx, 'team-'+teamId, teamMailbox, function () {
//console.log('Mailbox team', teamId);
}, opts);
});
mailbox.post = function (box, type, content) {
var b = ctx.boxes[box];
if (!b) { return; }
@ -477,9 +510,12 @@ proxy.mailboxes = {
});
};
mailbox.open = function (key, m, cb) {
if (TYPES.indexOf(key) === -1) { return; }
openChannel(ctx, key, m, cb);
mailbox.open = function (key, m, cb, team, opts) {
if (TYPES.indexOf(key) === -1 && !team) { return; }
openChannel(ctx, key, m, cb, opts);
};
mailbox.close = function (key, cb) {
leaveChannel(ctx, key, cb);
};
mailbox.dismiss = function (data, cb) {

@ -126,6 +126,9 @@ define([
delete ctx.store.proxy.teams[teamId];
ctx.emit('LEAVE_TEAM', teamId, team.clients);
ctx.updateMetadata();
ctx.store.mailbox.close('team-'+teamId, function () {
// Close team mailbox
});
};
var getTeamChannelList = function (ctx, id) {
@ -494,6 +497,8 @@ define([
var roHash = Hash.getViewHashFromKeys(secret);
var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey
var curvePair = Nacl.box.keyPair();
var rosterSeed = Crypto.Team.createSeed();
var rosterKeys = Crypto.Team.deriveMemberKeys(rosterSeed, {
curvePublic: ctx.store.proxy.curvePublic,
@ -585,6 +590,14 @@ define([
proxy.on('ready', function () {
// Store keys in our drive
var keys = {
mailbox: {
channel: Hash.createChannelId(),
viewed: [],
keys: {
curvePrivate: Nacl.util.encodeBase64(curvePair.secretKey),
curvePublic: Nacl.util.encodeBase64(curvePair.publicKey)
}
},
drive: {
edPrivate: Nacl.util.encodeBase64(keyPair.secretKey),
edPublic: Nacl.util.encodeBase64(keyPair.publicKey)
@ -601,7 +614,7 @@ define([
view: rosterKeys.viewKeyStr,
}
};
ctx.store.proxy.teams[id] = {
var t = ctx.store.proxy.teams[id] = {
owner: true,
channel: secret.channel,
hash: hash,
@ -618,6 +631,11 @@ define([
onReady(ctx, id, lm, roster, keys, cId, function () {
Feedback.send('TEAM_CREATION');
ctx.store.mailbox.open('team-'+id, t.keys.mailbox, function () {
// Team mailbox loaded
}, true, {
owners: t.keys.drive.edPublic
});
ctx.updateMetadata();
cb();
});
@ -720,6 +738,11 @@ define([
team.rpc.removePins(waitFor(function (err) {
if (err) { console.error(err); }
}));
// Delete the mailbox
var mailboxChan = Util.find(teamData, ['keys', 'mailbox', 'channel']);
team.rpc.removeOwnedChannel(mailboxChan, waitFor(function (err) {
if (err) { console.error(err); }
}));
// Delete the roster
var rosterChan = Util.find(teamData, ['keys', 'roster', 'channel']);
ctx.store.rpc.removeOwnedChannel(rosterChan, waitFor(function (err) {
@ -750,6 +773,12 @@ define([
ctx.onReadyHandlers[id] = [];
openChannel(ctx, team, id, function (obj) {
if (!(obj && obj.error)) { console.debug('Team joined:' + id); }
var t = ctx.store.proxy.teams[id];
ctx.store.mailbox.open('team-'+id, t.keys.mailbox, function () {
// Team mailbox loaded
}, true, {
owners: t.keys.drive.edPublic
});
ctx.updateMetadata();
cb(obj);
});
@ -1566,6 +1595,25 @@ define([
});
};
var deriveMailbox = function (team) {
if (!team) { return; }
if (team.keys && team.keys.mailbox) { return team.keys.mailbox; }
var strSeed = Util.find(team, ['keys', 'roster', 'edit']);
if (!strSeed) { return; }
var hash = Nacl.hash(Nacl.util.decodeUTF8(strSeed));
var seed = hash.slice(0,32);
var mailboxChannel = Util.uint8ArrayToHex(hash.slice(32,48));
var curvePair = Nacl.box.keyPair.fromSecretKey(seed);
return {
channel: mailboxChannel,
viewed: [],
keys: {
curvePrivate: Nacl.util.encodeBase64(curvePair.secretKey),
curvePublic: Nacl.util.encodeBase64(curvePair.publicKey)
}
};
};
Team.init = function (cfg, waitFor, emit) {
var team = {};
var store = cfg.store;
@ -1595,6 +1643,9 @@ define([
Object.keys(teams).forEach(function (id) {
ctx.onReadyHandlers[id] = [];
if (!Util.find(teams, [id, 'keys', 'mailbox'])) {
teams[id].keys.mailbox = deriveMailbox(teams[id]);
}
openChannel(ctx, teams[id], id, waitFor(function (err) {
if (err) { return void console.error(err); }
console.debug('Team '+id+' ready');
@ -1615,6 +1666,8 @@ define([
edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']),
avatar: Util.find(teams[id], ['metadata', 'avatar']),
viewer: !Util.find(teams[id], ['keys', 'drive', 'edPrivate']),
notifications: Util.find(teams[id], ['keys', 'mailbox', 'channel']),
curvePublic: Util.find(teams[id], ['keys', 'mailbox', 'keys', 'curvePublic']),
};
if (safe && ctx.teams[id]) {

@ -105,11 +105,14 @@ define([
});
// Call the onMessage handlers
var isNotification = function (type) {
return type === "notifications" || /^team-/.test(type);
};
var pushMessage = function (data, handler) {
var todo = function (f) {
try {
var el;
if (data.type === 'notifications') {
if (isNotification(data.type)) {
Notifications.add(Common, data);
el = createElement(data);
}
@ -129,7 +132,7 @@ define([
onViewedHandlers.forEach(function (f) {
try {
f(data);
if (data.type === 'notifications') {
if (isNotification(data.type)) {
Notifications.remove(Common, data);
}
} catch (e) {
@ -173,20 +176,24 @@ define([
execCommand('SUBSCRIBE', null, function () {});
subscribed = true;
}
var teams = types.indexOf('team') !== -1;
if (typeof(cfg.onViewed) === "function") {
onViewedHandlers.push(function (data) {
if (types.indexOf(data.type) === -1) { return; }
var type = data.type;
if (types.indexOf(type) === -1 && !(teams && /^team-/.test(type))) { return; }
cfg.onViewed(data);
});
}
if (typeof(cfg.onMessage) === "function") {
onMessageHandlers.push(function (data, el) {
if (types.indexOf(data.type) === -1) { return; }
var type = data.type;
if (types.indexOf(type) === -1 && !(teams && /^team-/.test(type))) { return; }
console.log('okokok');
cfg.onMessage(data, el);
});
}
Object.keys(history).forEach(function (type) {
if (types.indexOf(type) === -1) { return; }
if (types.indexOf(type) === -1 && !(teams && /^team-/.test(type))) { return; }
history[type].forEach(function (data) {
pushMessage({
type: type,

@ -1025,8 +1025,9 @@ MessengerUI, Messages) {
$button.addClass('fa-bell');
};
Common.mailbox.subscribe(['notifications'], {
Common.mailbox.subscribe(['notifications', 'team'], {
onMessage: function (data, el) {
console.log(data, el, div);
if (el) {
$(div).prepend(el);
}

@ -148,7 +148,6 @@
"or": "o",
"tags_title": "Etiquetes (només vostres)",
"tags_add": "Actualitza les etiquetes d'aquesta pàgina",
"tags_searchHint": "Inicieu una cerca amb # al vostre CryptDrive per trobar els vostres documents etiquetats.",
"tags_notShared": "Les vostres etiquetes no es comparteixen amb altres persones usuàries",
"tags_duplicate": "Etiquetes duplicades: {0}",
"tags_noentry": "No podeu etiquetar un document esborrat!",

@ -109,7 +109,7 @@
"newButton": "Neu",
"newButtonTitle": "Neues Pad erstellen",
"uploadButton": "Hochladen",
"uploadButtonTitle": "Eine neue Datei in den aktuellen Ordner hochladen",
"uploadButtonTitle": "Eine neue Datei zum CryptDrive hochladen",
"saveTemplateButton": "Als Vorlage speichern",
"saveTemplatePrompt": "Bitte gib einen Titel für die Vorlage ein",
"templateSaved": "Vorlage gespeichert!",
@ -145,8 +145,7 @@
"filePicker_filter": "Dateien nach Namen filtern",
"or": "oder",
"tags_title": "Tags (nur für dich)",
"tags_add": "Die Tags dieser Seite bearbeiten",
"tags_searchHint": "Beginne die Suche in deinem CryptDrive mit #, um getaggte Dokumente zu finden.",
"tags_add": "Tags der ausgewählten Pads bearbeiten",
"tags_notShared": "Deine Tags werden nicht mit anderen Benutzern geteilt",
"tags_duplicate": "Doppeltes Tag: {0}",
"tags_noentry": "Du kannst keine Tags zu einem gelöschten Pad hinzufügen!",
@ -831,7 +830,7 @@
"help": {
"title": "Mit CryptPad anfangen",
"generic": {
"more": "Erfahre mehr über die Nutzung von CryptPad, indem du unsere <a href=\"/faq.html\" target=\"_blank\">FAQ</a> liest",
"more": "Erfahre mehr über die Nutzung von CryptPad, indem du unsere <a href=\"/faq.html\" target=\"_blank\">FAQ</a> liest.",
"share": "Teile dieses Dokument mit der Schaltfläche <i class=\"fa fa-shhare-alt\"></i> <b>Teilen</b> und verwalte die Zugriffsrechte mit <i class=\"fa fa-unlock-alt\"></i> <b>Zugriff</b>.",
"save": "Alle Änderungen werden automatisch synchronisiert. Du musst sie also nicht selbst speichern"
},
@ -1377,5 +1376,10 @@
"toolbar_insert": "Einfügen",
"toolbar_savetodrive": "Als Bild speichern",
"slide_backCol": "Hintergrundfarbe",
"slide_textCol": "Textfarbe"
"slide_textCol": "Textfarbe",
"support_languagesPreamble": "Das Support-Team spricht die folgenden Sprachen:",
"settings_safeLinkDefault": "Sichere Links sind nun standardmäßig aktiviert. Bitte verwende zum Kopieren von Links das Menü <i class=\"fa fa-shhare-alt\"></i> <b>Teilen</b> und nicht die Adressleiste des Browsers.",
"info_imprintFlavour": "<a>Rechtliche Informationen über die Administratoren dieses Servers</a>.",
"info_privacyFlavour": "Unsere <a>Datenschutzerklärung</a> beschreibt, wie wir deine Daten verarbeiten.",
"user_about": "Über CryptPad"
}

@ -119,7 +119,6 @@
"or": "ή",
"tags_title": "Ετικέτες (για εσάς μόνο)",
"tags_add": "Ενημερώστε τις ετικέτες αυτής της σελίδας",
"tags_searchHint": "Ξεκινήστε μια αναζήτηση με το σύμβολο # στο CryptDrive σας για να βρείτε pads με ετικέτες.",
"tags_notShared": "Οι ετικέτες σας δεν μοιράζονται με άλλους χρήστες",
"tags_duplicate": "Διπλή ετικέτα: {0}",
"tags_noentry": "Δεν μπορείτε να βάλετε ετικέτα σε διεγραμένο pad!",

@ -472,7 +472,6 @@
"printBackgroundRemove": "Eliminar este fondo de pantalla",
"tags_title": "Etiquetas (sólo para tí)",
"tags_add": "Actualizar las etiquetas de esta página",
"tags_searchHint": "Comenzar una búsqueda con # en tú CryptDrive para encontrar las notas etiquetadas.",
"tags_notShared": "Tus etiquetas no están compartidas con otros usuarios",
"tags_duplicate": "Duplicar etiquetas:{0}",
"tags_noentry": "No puedes etiquetar una nota eliminada!",

@ -151,7 +151,6 @@
"or": "tai",
"tags_title": "Tunnisteet (vain sinulle)",
"tags_add": "Päivitä sivun tunnisteet",
"tags_searchHint": "Aloita hakusi CryptDrivessa #-merkillä löytääksesi tunnisteita sisältävät padit.",
"tags_notShared": "Tunnisteitasi ei jaeta muiden käyttäjien kanssa",
"tags_duplicate": "Kaksinkertainen tunniste: {0}",
"tags_noentry": "Et voi lisätä tunnistetta poistettuun padiin!",

@ -111,7 +111,7 @@
"newButtonTitle": "Créer un nouveau pad",
"uploadButton": "Importer des fichiers",
"uploadFolderButton": "Importer un dossier",
"uploadButtonTitle": "Importer un nouveau fichier dans le dossier actuel",
"uploadButtonTitle": "Importer un nouveau fichier dans votre CryptDrive",
"saveTemplateButton": "Sauver en tant que modèle",
"saveTemplatePrompt": "Choisir un titre pour ce modèle",
"templateSaved": "Modèle enregistré !",
@ -147,8 +147,7 @@
"filePicker_filter": "Filtrez les fichiers par leur nom",
"or": "ou",
"tags_title": "Mots-clés du pad (pour vous uniquement)",
"tags_add": "Modifier les mots-clés du pad",
"tags_searchHint": "Commencez une recherche par # dans votre CryptDrive pour retrouver vos pads par mot-clé.",
"tags_add": "Modifier les mots-clés de la sélection",
"tags_notShared": "Vos mots-clés ne sont pas partagés avec les autres utilisateurs",
"tags_duplicate": "Mot-clé déjà présent : {0}",
"tags_noentry": "Vous ne pouvez pas ajouter de mots-clés à un pad supprimé !",
@ -838,7 +837,7 @@
"help": {
"title": "Pour bien démarrer",
"generic": {
"more": "Apprenez-en davantage sur le fonctionnement de CryptPad en lisant notre <a href=\"/faq.html\" target=\"_blank\">FAQ</a>",
"more": "Apprenez-en davantage sur le fonctionnement de CryptPad en lisant notre <a href=\"/faq.html\" target=\"_blank\">FAQ</a>.",
"share": "Partagez ce document avec le bouton <i class=\"fa fa-shhare-alt\"></i> <b>Partager</b> et gérez les droits d'accès avec le bouton <i class=\"fa fa-unlock-alt\"></i> <b>Accès</b>.",
"save": "Tous les changements effectués sont enregistrés automatiquement"
},
@ -1377,5 +1376,10 @@
"toolbar_savetodrive": "Sauvegarder image",
"toolbar_insert": "Insérer",
"toolbar_theme": "Thème",
"todo_move": "Votre liste de tâches est désormais dans le kanban <b>{0}</b> dans votre Drive."
"todo_move": "Votre liste de tâches est désormais dans le kanban <b>{0}</b> dans votre Drive.",
"settings_safeLinkDefault": "Les liens sécurisés sont désormais activés par défaut. Veuillez utiliser le menu <i class=\"fa fa-shhare-alt\"></i> <b>Partager</b> pour copier les liens plutôt que la barre d'adresse de votre navigateur.",
"support_languagesPreamble": "L'équipe de support parle les langues suivantes :",
"info_privacyFlavour": "<a>Description de la confidentialité</a> de vos données.",
"user_about": "À propos de CryptPad",
"info_imprintFlavour": "<a>Informations légales sur les administateurs de cette instance</a>."
}

@ -148,7 +148,6 @@
"or": "o",
"tags_title": "Tag (mostrati solo a te)",
"tags_add": "Aggiorna i tag di questa pagina",
"tags_searchHint": "Inizia una ricerca con # nel tuo CryptDrive per trovare i pad taggati.",
"tags_notShared": "I tuoi tag non sono condivisi con altri utenti",
"tags_duplicate": "Duplica tag: {0}",
"tags_noentry": "Non puoi taggare un pad eliminato!",

@ -114,7 +114,7 @@
"newButtonTitle": "Create a new pad",
"uploadButton": "Upload files",
"uploadFolderButton": "Upload folder",
"uploadButtonTitle": "Upload a new file to the current folder",
"uploadButtonTitle": "Upload a new file to your CryptDrive",
"saveTemplateButton": "Save as template",
"saveTemplatePrompt": "Choose a title for the template",
"templateSaved": "Template saved!",
@ -150,8 +150,7 @@
"filePicker_filter": "Filter files by name",
"or": "or",
"tags_title": "Tags (for you only)",
"tags_add": "Update this page's tags",
"tags_searchHint": "Start a search with # in your CryptDrive to find your tagged pads.",
"tags_add": "Update the tags for selected pads",
"tags_notShared": "Your tags are not shared with other users",
"tags_duplicate": "Duplicate tag: {0}",
"tags_noentry": "You can't tag a deleted pad!",
@ -856,7 +855,7 @@
"help": {
"title": "Getting started",
"generic": {
"more": "Learn more about how CryptPad can work for you by reading our <a href=\"/faq.html\" target=\"_blank\">FAQ</a>",
"more": "Learn more about how CryptPad can work for you by reading our <a href=\"/faq.html\" target=\"_blank\">FAQ</a>.",
"share": "Share this document with the <i class=\"fa fa-shhare-alt\"></i> <b>Share</b> button, and manage access rights with <i class=\"fa fa-unlock-alt\"></i> <b>Access</b>.",
"save": "All your changes are synced automatically so you never need to save"
},
@ -1377,5 +1376,10 @@
"code_editorTheme": "Editor theme",
"toolbar_file": "File",
"slide_backCol": "Background color",
"slide_textCol": "Text color"
"slide_textCol": "Text color",
"support_languagesPreamble": "The support team speaks the following languages:",
"settings_safeLinkDefault": "Safe Links are now turned on by default. Please use the <i class=\"fa fa-shhare-alt\"></i> <b>Share</b> menu to copy links rather than your browser's address bar.",
"info_imprintFlavour": "<a>Legal information about the administrators of this instance</a>.",
"user_about": "About CryptPad",
"info_privacyFlavour": "Our <a>privacy policy</a> describes how we treat your data."
}

@ -140,7 +140,6 @@
"or": "eller",
"tags_title": "Tags (kun for ditt bruk)",
"tags_add": "Oppdater tags for denne sida",
"tags_searchHint": "Søk med # i CryptDriven din for å finne pads med slike tags.",
"tags_notShared": "Dine tags deles ikke med andre brukerer",
"tags_duplicate": "Dupliser tag:{0}",
"tags_noentry": "Du kan ikke tagge en sletta pad!",

@ -74,7 +74,6 @@
"tags_noentry": "U kunt een verwijderde werkomgeving niet markeren!",
"tags_duplicate": "Gedupliceerde markering: {0}",
"tags_notShared": "Uw markeringen worden niet gedeeld met andere gebruikers",
"tags_searchHint": "Begin een zoekopdracht met # in uw CryptDrive om gemarkeerde werkomgevingen te vinden.",
"tags_add": "Werk de markeringen van deze pagina bij",
"tags_title": "Markeringen (alleen voor u)",
"or": "of",

@ -392,7 +392,6 @@
"or": "",
"tags_title": "",
"tags_add": "",
"tags_searchHint": "",
"tags_notShared": "",
"tags_duplicate": "",
"tags_noentry": "",

@ -382,7 +382,6 @@
"or": "sau",
"tags_title": "Etichete (doar pentru tine)",
"tags_add": "Updatează etichetele acestei pagini",
"tags_searchHint": "Începe o căutare cu # în CryptDrive-ul tău pentru a găsi pad-uri etichetate",
"tags_notShared": "Etichetele tale nu sunt împărțite cu alți utilizatori",
"tags_duplicate": "Duplică eticheta: {0}",
"tags_noentry": "Nu poți eticheta un pad șters",
@ -579,5 +578,6 @@
"fc_color": "Schimbă culoarea",
"fm_morePads": "Mai mult",
"uploadFolderButton": "Încarcă dosar",
"storageStatus": "Capacitate de stocare:<br /><b>{0}</b> utilizat din <b>{1}</b>"
"storageStatus": "Capacitate de stocare:<br /><b>{0}</b> utilizat din <b>{1}</b>",
"fc_collapseAll": "Restrânge"
}

@ -140,7 +140,6 @@
"or": "или",
"tags_title": "Теги (только для вас)",
"tags_add": "Обновить теги страницы",
"tags_searchHint": "Начните поиск в вашем CryptDrive при помощи # чтобы найти пэды с тегами.",
"tags_notShared": "Ваши теги не разделяются с другими пользователями",
"button_newsheet": "Новый Лист",
"newButtonTitle": "создать новую запись",

@ -38,7 +38,6 @@
"tags_noentry": "Du kan inte tagga ett raderat dokument!",
"tags_duplicate": "Duplicera tagg: {0}",
"tags_notShared": "Dina taggar är inte delade med andra användare",
"tags_searchHint": "Påbörja en sökning med # i din CryptDrive för att hitta dina taggade dokument.",
"tags_add": "Uppdatera taggar för denna sida",
"tags_title": "Taggar (endast för dig)",
"or": "eller",

@ -3,6 +3,7 @@
@import (reference) "../../customize/src/less2/include/tools.less";
@import (reference) "../../customize/src/less2/include/markdown.less";
@import (reference) "../../customize/src/less2/include/avatar.less";
@import (reference) "../../customize/src/less2/include/buttons.less";
// body
&.cp-app-kanban {
@ -309,28 +310,45 @@
position: relative;
min-height: 50px;
.cp-kanban-filterTags {
.buttons_main();
display: inline-flex;
align-items: baseline;
align-items: center;
flex: 1;
max-width: 80%;
//max-width: 80%;
min-width: 150px;
.cp-kanban-filterTags-reset {
cursor: pointer;
margin-left: 10px;
.cp-kanban-filterTags-toggle {
min-width: 100px;
display: flex;
flex-flow: column;
flex-shrink: 0;
& > * {
visibility: hidden;
}
& > span {
display: inline-block;
height: 38px;
line-height: 38px;
}
& > button {
margin-top: -38px;
}
}
button.cp-kanban-filterTags-reset {
cursor: pointer;
white-space: normal !important;
.tools_unselectable();
i {
margin-right: 5px;
}
}
.cp-kanban-filterTags-name {
flex-shrink: 0;
}
.cp-kanban-filterTags-list {
margin-right: 10px;
margin-left: 10px;
display: flex;
flex-wrap: wrap;
&:not(:empty) {
margin-top: -5px;
}
em {
font-size: 14px;
color: lighten(@cryptpad_text_col, 10%);
@ -421,14 +439,28 @@
}
}
#kanban-trash {
height: 60px;
font-size: 40px;
display: flex;
height: 1px;
font-size: 0px;
/* CSS transitions are nice to look at, but it seems some interaction of "display: flex" here
makes the horizontal scrollbar stop working, so we need "display: none" for this state, but
CSS transitions are disabled when one state has "display: none". We can accomplish this in
js, but js animations are more prone to bugs and I'd rather live with a slight jank than
have the trash get stuck in some intermediary animation state under heavy use. --ansuz
*/
display: none; // flex;
//transition: opacity 400ms, height 400ms, font-size 400ms;
align-items: center;
justify-content: center;
position: relative;
width: 100%;
//pointer-events: none;
&.kanban-trash-active, &.kanban-trash-suggest {
display: flex;
height: 60px;
font-size: 40px;
}
i {
position: fixed;
}

@ -874,14 +874,27 @@ define([
// Tags filter
var existing = getExistingTags(kanban.options.boards);
var list = h('div.cp-kanban-filterTags-list');
var reset = h('span.cp-kanban-filterTags-reset', [h('i.fa.fa-times'), Messages.kanban_clearFilter]);
var reset = h('button.btn.btn-cancel.cp-kanban-filterTags-reset', [
h('i.fa.fa-times'),
Messages.kanban_clearFilter
]);
var hint = h('span.cp-kanban-filterTags-name', Messages.kanban_tags);
var tags = h('div.cp-kanban-filterTags', [
h('span.cp-kanban-filterTags-name', Messages.kanban_tags),
h('span.cp-kanban-filterTags-toggle', [
hint,
reset,
]),
list,
reset
]);
var $reset = $(reset);
var $list = $(list);
var $hint = $(hint);
var setTagFilterState = function (bool) {
$hint.css('visibility', bool? 'hidden': 'visible');
$reset.css('visibility', bool? 'visible': 'hidden');
};
setTagFilterState();
var getTags = function () {
return $list.find('span.active').map(function () {
@ -890,11 +903,7 @@ define([
};
var commitTags = function () {
var t = getTags();
if (t.length) {
$reset.css('visibility', '');
} else {
$reset.css('visibility', 'hidden');
}
setTagFilterState(t.length);
//framework._.sfCommon.setPadAttribute('tagsFilter', t);
kanban.options.tags = t;
kanban.setBoards(kanban.options.boards);
@ -938,14 +947,11 @@ define([
return $(this).data('tag') === t;
}).addClass('active');
});
if (tags.length) {
$reset.css('visibility', '');
} else {
$reset.css('visibility', 'hidden');
}
setTagFilterState(tags.length);
//framework._.sfCommon.setPadAttribute('tagsFilter', tags);
};
$reset.css('visibility', 'hidden').click(function () {
setTagFilterState();
$reset.click(function () {
setTags([]);
commitTags();
});

@ -18,6 +18,15 @@
display: flex;
flex-flow: column;
.cp-support-form-attachments {
.fa {
cursor: pointer;
}
&> span {
padding: 10px;
}
}
.cp-support-language-list {
.cp-support-language {
margin-left: 5px;

@ -149,7 +149,6 @@ define([
})
);
Messages.support_languagesPreamble = "This server's administrators speak the following languages:"; // XXX
var $div = $(
h('div.cp-support-language', [
Messages.support_languagesPreamble,
@ -166,8 +165,6 @@ define([
var form = APP.support.makeForm();
$div.find('button').before(form);
var id = Util.uid();
$div.find('button').click(function () {
@ -183,6 +180,7 @@ define([
$('.cp-sidebarlayout-category[data-category="tickets"]').click();
}
});
$div.find('button').before(form);
return $div;
};

@ -6,8 +6,9 @@ define([
'/common/common-hash.js',
'/common/common-util.js',
'/common/clipboard.js',
'/common/common-ui-elements.js',
'/customize/messages.js',
], function ($, ApiConfig, h, UI, Hash, Util, Clipboard, Messages) {
], function ($, ApiConfig, h, UI, Hash, Util, Clipboard, UIElements, Messages) {
var send = function (ctx, id, type, data, dest) {
var common = ctx.common;
@ -61,9 +62,15 @@ define([
};
var sendForm = function (ctx, id, form, dest) {
var $title = $(form).find('.cp-support-form-title');
var $content = $(form).find('.cp-support-form-msg');
var $form = $(form);
var $cat = $form.find('.cp-support-form-category');
var $title = $form.find('.cp-support-form-title');
var $content = $form.find('.cp-support-form-msg');
// XXX block submission until pending uploads are complete?
var $attachments = $form.find('.cp-support-attachments');
var category = $cat.val().trim(); // XXX make category a required field?
var title = $title.val().trim();
if (!title) {
return void UI.alert(Messages.support_formTitleError);
@ -72,18 +79,61 @@ define([
if (!content) {
return void UI.alert(Messages.support_formContentError);
}
$cat.val('');
$content.val('');
$title.val('');
var attachments = [];
$attachments.find('> span').each(function (i, el) {
var $el = $(el);
attachments.push({
href: $el.attr('data-href'),
name: $el.attr('data-name')
});
});
$attachments.html('');
send(ctx, id, 'TICKET', {
category: category,
title: title,
attachments: attachments,
message: content,
}, dest);
return true;
};
var makeForm = function (cb, title) {
Messages.support_cat_account = "User account"; // XXX
Messages.support_cat_data = "Loss of content"; // XXX
Messages.support_cat_bug = "Bug report"; // XXX
Messages.support_cat_other = "Other"; // XXX
Messages.support_cat_all = "All"; // XXX
Messages.support_category = "Category"; // XXX
Messages.support_attachments = "Attachments"; // XXX
Messages.support_addAttachment = "Add attachment"; // XXX
var makeCategoryDropdown = function (ctx, container, onChange, all) {
var categories = ['account', 'data', 'bug', 'other'];
if (all) { categories.push('all'); }
categories = categories.map(function (key) {
return {
tag: 'a',
content: h('span', Messages['support_cat_'+key]),
action: function () {
onChange(key);
}
};
});
var dropdownCfg = {
text: Messages.support_category,
options: categories,
container: $(container),
isSelect: true
};
return UIElements.createDropdown(dropdownCfg);
};
var makeForm = function (ctx, cb, title) {
var button;
if (typeof(cb) === "function") {
@ -93,8 +143,21 @@ define([
var cancel = title ? h('button.btn.btn-secondary', Messages.cancel) : undefined;
var category = h('input.cp-support-form-category', {
type: 'hidden',
value: ''
});
var catContainer = h('div.cp-dropdown-container' + (title ? '.cp-hidden': ''));
makeCategoryDropdown(ctx, catContainer, function (key) {
$(category).val(key);
});
var attachments, addAttachment;
var content = [
h('hr'),
category,
catContainer,
h('input.cp-support-form-title' + (title ? '.cp-hidden' : ''), {
placeholder: Messages.support_formTitle,
type: 'text',
@ -104,11 +167,54 @@ define([
h('textarea.cp-support-form-msg', {
placeholder: Messages.support_formMessage
}),
h('label', Messages.support_attachments),
attachments = h('div.cp-support-attachments'),
addAttachment = h('button', Messages.support_addAttachment),
h('hr'),
button,
cancel
];
$(addAttachment).click(function () {
var $input = $('<input>', {
'type': 'file',
'style': 'display: none;',
'multiple': 'multiple',
'accept': 'image/*'
}).on('change', function (e) {
var files = Util.slice(e.target.files);
files.forEach(function (file) {
// XXX validate that the href is hosted on the same instance
// use relative URLs or compare it against a list or allowed domains?
var ev = {};
ev.callback = function (data) {
var x, a;
var span = h('span', {
'data-name': data.name,
'data-href': data.url
}, [
x = h('i.fa.fa-times'),
a = h('a', {
href: '#'
}, data.name)
]);
$(x).click(function () {
$(span).remove();
});
$(a).click(function (e) {
e.preventDefault();
ctx.common.openURL(data.url);
});
$(attachments).append(span);
};
// The empty object allows us to bypass the file upload modal
ctx.FM.handleFile(file, ev, {});
});
});
$input.click();
});
var form = h('div.cp-support-form-container', content);
$(cancel).click(function () {
@ -125,6 +231,7 @@ define([
var privateData = metadataMgr.getPrivateData();
var ticketTitle = content.title + ' (#' + content.id + ')';
var ticketCategory;
var answer = h('button.btn.btn-primary.cp-support-answer', Messages.support_answer);
var close = h('button.btn.btn-danger.cp-support-close', Messages.support_close);
var hide = h('button.btn.btn-danger.cp-support-hide', Messages.support_remove);
@ -137,6 +244,7 @@ define([
var url;
if (ctx.isAdmin) {
ticketCategory = Messages['support_cat_'+(content.category || 'all')] + ' - ';
url = h('button.btn.btn-primary.fa.fa-clipboard');
$(url).click(function () {
var link = privateData.origin + privateData.pathname + '#' + 'support-' + content.id;
@ -146,9 +254,10 @@ define([
}
var $ticket = $(h('div.cp-support-list-ticket', {
'data-cat': content.category,
'data-id': content.id
}, [
h('h2', [ticketTitle, url]),
h('h2', [ticketCategory, ticketTitle, url]),
actions
]));
@ -179,7 +288,7 @@ define([
$(answer).click(function () {
$ticket.find('.cp-support-form-container').remove();
$(actions).hide();
var form = makeForm(function () {
var form = makeForm(ctx, function () {
var sent = sendForm(ctx, content.id, form, content.sender);
if (sent) {
$(actions).show();
@ -215,6 +324,21 @@ define([
ev.stopPropagation();
});
var attachments = (content.attachments || []).map(function (obj) {
if (!obj || !obj.name || !obj.href) { return; }
var a = h('a', {
href: '#'
}, obj.name);
// XXX disallow remote URLs
$(a).click(function (e) {
e.preventDefault();
ctx.common.openURL(obj.href);
});
return h('span', [
a
]);
});
var adminClass = (fromAdmin? '.cp-support-fromadmin': '');
var premiumClass = (fromPremium && !fromAdmin? '.cp-support-frompremium': '');
var name = Util.fixHTML(content.sender.name) || Messages.anonymous;
@ -226,6 +350,7 @@ define([
h('span.cp-support-message-time', content.time ? new Date(content.time).toLocaleString() : '')
]),
h('pre.cp-support-message-content', content.message),
h('div.cp-support-attachments', attachments),
isAdmin ? userData : undefined,
]);
};
@ -257,10 +382,25 @@ define([
adminKeys: Array.isArray(ApiConfig.adminKeys)? ApiConfig.adminKeys.slice(): [],
};
var fmConfig = {
body: $('body'),
onUploaded: function (ev, data) {
if (ev.callback) {
ev.callback(data);
}
}
};
ctx.FM = common.createFileManager(fmConfig);
ui.sendForm = function (id, form, dest) {
return sendForm(ctx, id, form, dest);
};
ui.makeForm = makeForm;
ui.makeForm = function (cb, title) {
return makeForm(ctx, cb, title);
};
ui.makeCategoryDropdown = function (container, onChange, all) {
return makeCategoryDropdown(ctx, container, onChange, all);
};
ui.makeTicket = function ($div, content, onHide) {
return makeTicket(ctx, $div, content, onHide);
};

Loading…
Cancel
Save