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

pull/1/head
yflory 5 years ago
commit 3ecad6fa73

@ -1,3 +1,50 @@
# HimalayanQuail (3.7.0)
## Goals
As we are getting closer to the end of our CryptPad Teams project we planned to spend this release addressing some of the difficulties that users have reported regarding the usage of our newer social features.
## Update notes
This release includes an upgrade to a newer version of JQuery which mitigates a minor vulnerability which could have contributed to the presence of an XSS attack. We weren't using the affected methods in the library, but there's no harm in updating as it will protect against the vulnerability affecting user data in the future.
We've also made some non-critical fixes to the server code, so you'll need to restart after pulling the latest code to take advantage of these improvements.
Update to 3.7.0 from 3.6.0 using the normal update procedure:
1. stop your server
2. pull the latest code via git
3. run `bower update`
4. restart your server
If you're using an up-to-date version of NPM you should find that running `npm update` prints a notice that one of the packages you've installed is seeking funding. Entering `npm fund` will print information about our OpenCollective funding campaign. If you're running a slightly older version of NPM and you wish to support CryptPad's development you can do so by visiting https://opencollective.com/cryptpad .
## Features
* Many users have contacted us via support tickets to ask how to add contacts on the platform. The easiest way is to share the link to your profile page. Once on that page registered users will be able to send a contact request which will appear in your notification tray. Because we believe you shouldn't have to read a manual to use CryptPad (and because we want to minimize how much time we spend answering support tickets) we've integrated this tip into the UI itself. Users that don't have any contacts on the platform will hopefully notice that the sharing menu's contacts tab now prompts them with this information, followed by a button to copy their profile page's URL to their clipboard.
* We've made a lot of other small changes that we hope will have a big impact on the usability of the sharing menu:
* the "Link" section of the modal which includes the URL generated from your chosen access rights has been restyled so that the URL is displayed in a multiline textarea so that users can better see the URL changing as they play with the other controls
* both the "Contacts" and "Link" section include short, unintrusive hints about how passwords interact with the different sharing methods:
* when sharing via a URL we indicate that the recipient will need to enter a password, allowing for the URL to be sent over an insecure channel without leaking your document's content
* when sharing directly with a contact via their encrypted mailbox the password is transferred automatically, since it is assumed that you intend for the recipient to gain access and the platform provides a secure channel through which all the relevant information can be delivered
* this information is only included in cases when the document is protected with a password to limit the amount of information the user has to process to complete their task
* we include brief and dismissable warning within the menu which indicates that URLs provide non-revocable access to documents so that new users of the platform understand the consequences of sharing
* in general we've tried to make the appearance of the modal more appealing and intuitive so that users naturally discover and adopt the workflows which are the most conducive to their privacy and security
* Our premium accounts platform authenticates that you are logged in on a given CryptPad instance by loading it in an iframe and requesting that it use one of your account's cryptographic keys to sign a message. Unfortunately, this process could be quite slow as it would load your CryptDrive and other information related to account, and some users reported that their browser timed out on this process. We've addressed this by loading only the account information required to prove your identity.
* We've also included some changes to CryptPad's server to allow users to share quotas between multiple accounts, though we still have work to do to make this behaviour functional on the web client.
* Spreadsheets now support password change!
* Kanban boards now render long column titles in a much more intuitive way, wrapping the text instead of truncating it.
* Our code editor now features support for Gantt charts in markdown mode via an improved Mermaidjs integration. We've also slowed down the rendering cycle so that updates are displayed once you stop typing for 400ms instead of 150ms, and improved the rendering methods so that all mermaid-generated charts are only redrawn if they have changed since the last time they were rendered. This results in a smoother reading experience while permitting other users to continue to edit the document.
* Finally, after a review of the code responsible for sanitizing the markdown code which we render as HTML, we've decided to remove SVG tags from our sanitizer's filter. This means that you can write SVG markup in the input field and see it rendered, in case you're into that kind of thing.
## Bug fixes
* It seems our "contacts" app broke along with the 3.5.0 release and nobody reported it. The regression was introduced when we made some changes to the teams chat integration. We've addressed the issue so that you can once again use the contacts app to chat directly with friends.
* We've found and fixed a "memory puddle" (a non-critical memory leak which was automatically mopped up every now and then). The fix probably won't have much noticeable impact but the server is now a little bit more correct
* We stumbled across a bug which wiped out the contents of a Kanban board and caused the application to crash if you navigated to the affected version of the document in history mode. If you notice that one of your documents was affected please contact us and we'll write a guide instructing you how to recover your content.
* We've found a few bugs lurking in our server which could have caused the amount of data stored in users' drives to be calculated incorrectly under very unlikely circumstances. We've fixed the issue and addressed a number of similar asynchrony-related code paths which should mitigate similar issues in the future.
* Lastly, we spotted some flaws in the code responsible for encrypting pad credentials in shared folders and teams such that viewers don't automatically gain access to the editing keys of a document when they should only have view access. There weren't any access control vulnerabilities, but an error was thrown under rare circumstances which could prevent affected users' drives from loading. We've guarded against the cause and made it such that any affected users will automatically repair their damaged drives.
# GoldenFrog release (3.6.0)
## Goals

@ -18,7 +18,7 @@
"tests"
],
"dependencies": {
"jquery": "~2.1.3",
"jquery": "2.2.4",
"tweetnacl": "0.12.2",
"components-font-awesome": "^4.6.3",
"ckeditor": "4.7.3",
@ -51,6 +51,7 @@
"requirejs-plugins": "^1.0.3"
},
"resolutions": {
"bootstrap": "^v4.0.0"
"bootstrap": "^v4.0.0",
"jquery": "2.2.4"
}
}

@ -103,7 +103,7 @@ define([
])*/
])
]),
h('div.cp-version-footer', "CryptPad v3.6.0 (GoldenFrog)")
h('div.cp-version-footer', "CryptPad v3.7.0 (HimalayanQuail)")
]);
};

@ -159,6 +159,9 @@
margin-bottom: @alertify_padding-base;
margin: 0;
overflow: auto;
:last-child {
margin-bottom: 0;
}
}
.alertify-tabs {
max-height: 100%;
@ -222,14 +225,22 @@
background-color: @alertify-input-fg;
color: @cryptpad_text_col;
border: 1px solid @alertify-input-bg;
margin-bottom: 15px;
margin-bottom: @alertify_padding-base;
width: 100%;
font-size: 100%;
padding: @alertify_padding-base;
&[readonly] {
background-color: @alertify-light-bg;
color: @cryptpad_text_col;
border-color: @alertify-input-fg;
border-color: @alertify-light-bg;
}
}
textarea {
overflow: hidden;
padding: 8px;
&[readonly] {
resize: none;
}
}
@ -509,5 +520,33 @@
overflow-x: auto;
}
}
// Bootstrap Alerts
.alert {
margin: 0px 0px @alertify_padding-base 0px;
font-size: 12px;
padding: 5px;
border-radius: 0px;
i {
margin-right: 10px;
}
&.alert-primary {
background-color: @alertify-base;
color: @alertify-fg;
border-color: @alertify-fg;
a {
color: @alertify-fg;
text-decoration: underline;
}
}
&.dismissable {
display: flex;
align-items: center;
span.fa-times {
font-size: @colortheme_app-font-size;
margin-left: 20px;
cursor: pointer;
}
}
}
}

@ -1,9 +1,12 @@
@import (reference) "./tools.less";
@import (reference) "./colortheme-all.less";
.avatar_vars(
@width: 30px
) {
@avatar-width: @width;
@avatar-font-size: @width / 1.2;
@avatar-default-bg: #D9D8D8;
@avatar-default-fg: darken(@avatar-default-bg, 40%);
}
.avatar_main(@width: 30px) {
--LessLoader_require: LessLoader_currentFile();
@ -30,16 +33,16 @@
justify-content: center;
align-items: center;
border-radius: 4px;
overflow: hidden;
box-sizing: content-box;
}
.cp-avatar-default {
.tools_unselectable();
background: white;
color: black;
background: @avatar-default-bg;
color: @avatar-default-fg;
font-size: @avatar-font-size;
font-size: var(--avatar-font-size);
text-transform: capitalize;
}
media-tag {
min-height: @avatar-width;

@ -1,12 +1,16 @@
@import (reference) "./colortheme-all.less";
@import (reference) "./variables.less";
.modals-ui-elements_main() {
--LessLoader_require: LessLoader_currentFile();
}
& {
.cp-spacer {
height: @variables_padding;
}
// Share modal
.msg.cp-inline-radio-group {
overflow: unset !important;
padding: 0px @variables_padding;
.radio-group {
display: flex;
flex-direction: row;

@ -1,3 +1,4 @@
@import (reference) "./colortheme-all.less";
.password_main() {
--LessLoader_require: LessLoader_currentFile();
}
@ -17,7 +18,7 @@
justify-content: center;
cursor: pointer;
&:hover {
background-color: rgba(0,0,0,0.1);
color: darken(@colortheme_alertify-primary, 10%);
}
}
}

@ -6,16 +6,18 @@
--LessLoader_require: LessLoader_currentFile();
};
& {
.cp-usergrid-container {
margin-bottom: 12px !important; // even when last child of .msg
.cp-usergrid-grid {
display: flex;
flex-wrap: wrap;
margin: -3px;
margin-bottom: 6px;
}
&:not(.large) {
.cp-usergrid-grid {
margin: -3px;
margin-bottom: 6px;
max-height: 130px;
overflow-y: auto;
@media screen and (max-height: 515px) {
max-height: unset; // remove double scrollbar
}
}
&.cp-usergrid-empty {
@ -28,17 +30,22 @@
input {
flex: 1;
min-width: 0;
margin: 0;
margin-bottom: 0 !important;
height: 38px;
&::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: @cryptpad_color_grey;
opacity: 1; /* Firefox */
}
}
margin-bottom: 15px;
margin-bottom: 10px;
&:empty {
margin: 0;
display: none;
}
button:last-child {
margin-right: 0px !important;
}
}
.cp-usergrid-user {
width: 70px;
@ -58,33 +65,48 @@
background-color: @colortheme_alertify-primary;
color: @colortheme_alertify-primary-text;
order: -1 !important;
.cp-usergrid-avatar {
media-tag, .cp-avatar-default {
opacity: 0.7;
}
}
}
.cp-usergrid-user-avatar {
min-height: 40px;
}
.cp-usergrid-user-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
text-align: center;
line-height: 18px;
line-height: 20px;
flex: 1;
}
border: 1px solid @colortheme_alertify-primary;
&:not(.large) {
.avatar_main(40px);
}
&.large {
.avatar_main(25px);
width: 140px;
width: 145px;
height: 35px;
flex-flow: row;
margin: 0;
margin-right: 15px;
margin-bottom: 1px;
&:nth-child(3n) {
margin-right: 0;
margin: 3px;
flex-basis: calc(33.3333333% - 6px);
flex-shrink: 1;
min-width: 0;
.cp-usergrid-user-name {
margin-left: 5px;
text-align: left;
line-height: 150%;
color: @cryptpad_text_col;
}
}
&.cp-selected {
.cp-usergrid-user-name {
color: @colortheme_alertify-primary-text;
}
}
}

@ -0,0 +1,14 @@
/*
* You can override the translation text using this file.
* The recommended method is to make a copy of this file (/customize.dist/translations/messages.{LANG}.js)
in a 'customize' directory (/customize/translations/messages.{LANG}.js).
* If you want to check all the existing translation keys, you can open the internal language file
but you should not change it directly (/common/translations/messages.{LANG}.js)
*/
define(['/common/translations/messages.fi.js'], function (Messages) {
// Replace the existing keys in your copied file here:
// Messages.button_newpad = "New Rich Text Document";
return Messages;
});

@ -8,6 +8,8 @@ If the result of IO or computation is requested while an identical request
is already in progress, wait until the first one completes and provide its
result to every routine that requested it.
Asynchrony is guaranteed.
## Usage
Provide:
@ -51,11 +53,12 @@ module.exports = function (/* task */) {
var args = Array.prototype.slice.call(arguments);
//if (map[id] && map[id].length > 1) { console.log("BATCH-READ DID ITS JOB for [%s][%s]", task, id); }
map[id].forEach(function (h) {
h.apply(null, args);
setTimeout(function () {
map[id].forEach(function (h) {
h.apply(null, args);
});
delete map[id];
});
delete map[id];
});
};
};

@ -4,7 +4,7 @@ q(id, function (next) {
// whatever you need to do....
// when you're done
next();
next(); // guaranteed to be asynchronous :D
});
*/
@ -16,9 +16,11 @@ module.exports = function () {
var map = {};
var next = function (id) {
if (map[id] && map[id].length === 0) { return void delete map[id]; }
var task = map[id].shift();
task(fix1(next, id));
setTimeout(function () {
if (map[id] && map[id].length === 0) { return void delete map[id]; }
var task = map[id].shift();
task(fix1(next, id));
});
};
return function (id, task) {

2
package-lock.json generated

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

@ -1,12 +1,16 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"version": "3.6.0",
"version": "3.7.0",
"license": "AGPL-3.0+",
"repository": {
"type": "git",
"url": "git://github.com/xwiki-labs/cryptpad.git"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/cryptpad"
},
"dependencies": {
"chainpad-crypto": "^0.2.2",
"chainpad-server": "^3.0.5",

@ -228,13 +228,15 @@ var truthyKeys = function (O) {
});
};
var getChannelList = function (Env, publicKey, cb) {
var getChannelList = function (Env, publicKey, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
loadUserPins(Env, publicKey, function (pins) {
cb(truthyKeys(pins));
});
};
var getFileSize = function (Env, channel, cb) {
var getFileSize = function (Env, channel, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!isValidId(channel)) { return void cb('INVALID_CHAN'); }
if (channel.length === 32) {
if (typeof(Env.msgStore.getChannelSize) !== 'function') {
@ -416,20 +418,46 @@ var getDeletedPads = function (Env, channels, cb) {
const batchTotalSize = BatchRead("GET_TOTAL_SIZE");
var getTotalSize = function (Env, publicKey, cb) {
batchTotalSize(publicKey, cb, function (done) {
var bytes = 0;
return void getChannelList(Env, publicKey, function (channels) {
if (!channels) { return done('INVALID_PIN_LIST'); } // unexpected
var unescapedKey = unescapeKeyCharacters(publicKey);
var limit = Env.limits[unescapedKey];
// Get a common key if multiple users share the same quota, otherwise take the public key
var batchKey = (limit && Array.isArray(limit.users)) ? limit.users.join('') : publicKey;
nThen(function (w) {
channels.forEach(function (channel) { // TODO semaphore?
getFileSize(Env, channel, w(function (e, size) {
if (!e) { bytes += size; }
batchTotalSize(batchKey, cb, function (done) {
var channels = [];
var bytes = 0;
nThen(function (waitFor) {
// Get the channels list for our user account
getChannelList(Env, publicKey, waitFor(function (_channels) {
if (!_channels) {
waitFor.abort();
return done('INVALID_PIN_LIST');
}
Array.prototype.push.apply(channels, _channels);
}));
// Get the channels list for users sharing our quota
if (limit && Array.isArray(limit.users) && limit.users.length > 1) {
limit.users.forEach(function (key) {
if (key === unescapedKey) { return; } // Don't count ourselves twice
getChannelList(Env, key, waitFor(function (_channels) {
if (!_channels) { return; } // Broken user, don't count their quota
Array.prototype.push.apply(channels, _channels);
}));
});
}).nThen(function () {
done(void 0, bytes);
}
}).nThen(function (waitFor) {
// Get size of the channels
var list = []; // Contains the channels already counted in the quota to avoid duplicates
channels.forEach(function (channel) { // TODO semaphore?
if (list.indexOf(channel) !== -1) { return; }
list.push(channel);
getFileSize(Env, channel, waitFor(function (e, size) {
if (!e) { bytes += size; }
}));
});
}).nThen(function () {
done(void 0, bytes);
});
});
};

@ -102,7 +102,9 @@ var getMetadataAtPath = function (Env, path, _cb) {
var closeChannel = function (env, channelName, cb) {
if (!env.channels[channelName]) { return void cb(); }
try {
env.channels[channelName].writeStream.close();
if (typeof(Util.find(env, [ 'channels', channelName, 'writeStream', 'close'])) === 'function') {
env.channels[channelName].writeStream.close();
}
delete env.channels[channelName];
env.openFiles--;
cb();

@ -1,12 +1,17 @@
define([
'jquery',
'/common/cryptpad-common.js',
'/common/cryptget.js',
'/common/pinpad.js',
'/common/common-constants.js',
'/common/outer/local-store.js',
'/common/outer/login-block.js',
'/common/outer/network-config.js',
'/customize/login.js',
'/common/test.js',
'/bower_components/nthen/index.js',
'/bower_components/netflux-websocket/netflux-client.js',
'/bower_components/tweetnacl/nacl-fast.min.js'
], function ($, Cryptpad, Constants, LocalStore, Test, nThen) {
], function ($, Crypt, Pinpad, Constants, LocalStore, Block, NetConfig, Login, Test, nThen, Netflux) {
var Nacl = window.nacl;
var signMsg = function (msg, privKey) {
@ -27,9 +32,57 @@ define([
sessionStorage[Constants.userHashKey];
var proxy;
var rpc;
var network;
var rpcError;
var loadProxy = function (hash) {
nThen(function (waitFor) {
var wsUrl = NetConfig.getWebsocketURL();
var w = waitFor();
Netflux.connect(wsUrl).then(function (_network) {
network = _network;
w();
}, function (err) {
rpcError = err;
console.error(err);
});
}).nThen(function (waitFor) {
Crypt.get(hash, waitFor(function (err, val) {
if (err) {
waitFor.abort();
console.error(err);
return;
}
try {
var parsed = JSON.parse(val);
proxy = parsed;
} catch (e) {
console.log("Can't parse user drive", e);
}
}), {
network: network
});
}).nThen(function (waitFor) {
if (!network) { return void waitFor.abort(); }
Pinpad.create(network, proxy, waitFor(function (e, call) {
if (e) {
rpcError = e;
return void waitFor.abort();
}
rpc = call;
}));
}).nThen(function () {
Test(function () {
// This is only here to maybe trigger an error.
window.drive = proxy['drive'];
Test.passed();
});
});
};
var whenReady = function (cb) {
if (proxy) { return void cb(); }
if (proxy && (rpc || rpcError)) { return void cb(); }
console.log('CryptPad not ready...');
setTimeout(function () {
whenReady(cb);
@ -45,6 +98,17 @@ define([
console.log('CP receiving', data);
if (data.cmd === 'PING') {
ret.res = 'PONG';
} else if (data.cmd === 'LOGIN') {
Login.loginOrRegister(data.data.name, data.data.password, false, false, function (err) {
if (err) {
ret.error = 'LOGIN_ERROR';
srcWindow.postMessage(JSON.stringify(ret), domain);
return;
}
loadProxy(LocalStore.getUserHash());
srcWindow.postMessage(JSON.stringify(ret), domain);
});
return;
} else if (data.cmd === 'SIGN') {
if (!AUTHORIZED_DOMAINS.filter(function (x) { return x.test(domain); }).length) {
ret.error = "UNAUTH_DOMAIN";
@ -63,7 +127,16 @@ define([
}
} else if (data.cmd === 'UPDATE_LIMIT') {
return void whenReady(function () {
Cryptpad.updatePinLimit(function (e, limit, plan, note) {
if (rpcError) {
// Tell the user on accounts that there was an issue and they need to wait maximum 24h or contact an admin
ret.warning = true;
srcWindow.postMessage(JSON.stringify(ret), domain);
return;
}
rpc.updatePinLimits(function (e, limit, plan, note) {
if (e) {
ret.warning = true;
}
ret.res = [limit, plan, note];
srcWindow.postMessage(JSON.stringify(ret), domain);
});
@ -74,18 +147,8 @@ define([
srcWindow.postMessage(JSON.stringify(ret), domain);
});
nThen(function (waitFor) {
Cryptpad.ready(waitFor());
}).nThen(function (waitFor) {
Cryptpad.getUserObject(null, waitFor(function (obj) {
proxy = obj;
}));
}).nThen(function () {
console.log('IFRAME READY');
Test(function () {
// This is only here to maybe trigger an error.
window.drive = proxy['drive'];
Test.passed();
});
});
var userHash = LocalStore.getUserHash();
if (userHash) {
loadProxy(userHash);
}
});

@ -131,7 +131,7 @@ define([
if (['markdown', 'gfm'].indexOf(CodeMirror.highlightMode) === -1) { return; }
if (!$previewButton.is('.cp-toolbar-button-active')) { return; }
forceDrawPreview();
}, 150);
}, 400);
var previewTo;
$previewButton.click(function () {

@ -121,6 +121,10 @@ text.actor {
font-size: 11px;
text-height: 14px;
}
.sectionTitle, .titleText {
font-weight: bold;
}
/* Grid and axis */
.grid .tick {
stroke: lightgrey;

@ -6,22 +6,24 @@ define([
CodeMirror.defineSimpleMode("orgmode", {
start: [
{regex: /^(^\*{1,6}\s)(TODO|DOING|WAITING|NEXT){0,1}(CANCELLED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED){0,1}(.*)$/, token: ["header org-level-star", "header org-todo", "header org-done", "header"]},
{regex: /(\*\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header level1 org-level-star","header level1 org-todo","header level1 org-done", "header level1 org-priority", "header level1", "header level1 void", "header level1 comment"]},
{regex: /(\*{1,}\s)(TODO|DOING|WAITING|NEXT|PENDING|)(CANCELLED|CANCELED|CANCEL|DEFERRED|DONE|REJECTED|STOP|STOPPED|)(\s+\[\#[A-C]\]\s+|)(.*?)(?:(\s{10,}|))(\:[\S]+\:|)$/, sol: true, token: ["header org-level-star","header org-todo","header org-done", "header org-priority", "header", "header void", "header comment"]},
{regex: /(\+[^\+]+\+)/, token: ["strikethrough"]},
{regex: /(\*[^\*]+\*)/, token: ["strong"]},
{regex: /(\/[^\/]+\/)/, token: ["em"]},
{regex: /(\_[^\_]+\_)/, token: ["link"]},
{regex: /(\~[^\~]+\~)/, token: ["comment"]},
{regex: /(\=[^\=]+\=)/, token: ["comment"]},
{regex: /\[\[[^\[\]]*\]\[[^\[\]]*\]\]/, token: "url"}, // links
{regex: /\[[xX\s]?\]/, token: 'qualifier'}, // checkbox
{regex: /\#\+BEGIN_[A-Z]*/, token: "comment", next: "env"}, // comments
{regex: /:?[A-Z_]+\:.*/, token: "comment"}, // property drawers
{regex: /(\#\+[A-Z_]*)(\:.*)/, token: ["keyword", 'qualifier']}, // environments
{regex: /(CLOCK\:|SHEDULED\:)(\s.+)/, token: ["comment", "keyword"]}
{regex: /\[\[[^\[\]]+\]\[[^\[\]]+\]\]/, token: "org-url"}, // links
{regex: /\[\[[^\[\]]+\]\]/, token: "org-image"}, // image
{regex: /\[[xX\s\-\_]\]/, token: 'qualifier org-toggle'}, // checkbox
{regex: /\#\+(?:(BEGIN|begin))_[a-zA-Z]*/, token: "comment", next: "env", sol: true}, // comments
{regex: /:?[A-Z_]+\:.*/, token: "comment", sol: true}, // property drawers
{regex: /(\#\+[a-zA-Z_]*)(\:.*)/, token: ["keyword", 'qualifier'], sol: true}, // environments
{regex: /(CLOCK\:|SHEDULED\:|DEADLINE\:)(\s.+)/, token: ["comment", "keyword"]}
],
env: [
{regex: /.*?\#\+END_[A-Z]*/, token: "comment", next: "start"},
{regex: /\#\+(?:(END|end))_[a-zA-Z]*/, token: "comment", next: "start", sol: true},
{regex: /.*/, token: "comment"}
]
});

@ -127,6 +127,18 @@ define([
return input;
};
dialog.selectableArea = function (value, opt) {
var attrs = merge({
readonly: 'readonly',
}, opt);
var input = h('textarea', attrs);
$(input).val(value).click(function () {
input.select();
});
return input;
};
dialog.okButton = function (content, classString) {
var sel = typeof(classString) === 'string'? 'button.ok.' + classString:'button.ok.primary';
return h(sel, { tabindex: '2', }, content || Messages.okButton);
@ -187,7 +199,8 @@ define([
dialog.tabs = function (tabs) {
var contents = [];
var titles = [];
tabs.forEach(function (tab) {
var active = 0;
tabs.forEach(function (tab, i) {
if (!tab.content || !tab.title) { return; }
var content = h('div.alertify-tabs-content', tab.content);
var title = h('span.alertify-tabs-title', tab.title);
@ -203,10 +216,11 @@ define([
});
titles.push(title);
contents.push(content);
if (tab.active) { active = i; }
});
if (contents.length) {
$(contents[0]).addClass('alertify-tabs-content-active');
$(titles[0]).addClass('alertify-tabs-active');
$(contents[active]).addClass('alertify-tabs-content-active');
$(titles[active]).addClass('alertify-tabs-active');
}
return h('div.alertify-tabs', [
h('div.alertify-tabs-titles', titles),

@ -147,6 +147,7 @@ define([
: Messages.owner_removeText;
var removeCol = UIElements.getUserGrid(msg, {
common: common,
large: true,
data: _owners,
noFilter: true
}, function () {
@ -238,6 +239,7 @@ define([
});
var addCol = UIElements.getUserGrid(Messages.owner_addText, {
common: common,
large: true,
data: _friends
}, function () {
//console.log(arguments);
@ -254,6 +256,7 @@ define([
});
var teamsList = UIElements.getUserGrid(Messages.owner_addTeamText, {
common: common,
large: true,
noFilter: true,
data: teamsData
}, function () {});
@ -551,9 +554,10 @@ define([
$d.append(password);
}
if (!data.noEditPassword && owned && parsed.type !== "sheet") { // FIXME SHEET fix password change for sheets
if (!data.noEditPassword && owned) { // FIXME SHEET fix password change for sheets
var sframeChan = common.getSframeChannel();
var isOO = parsed.type === 'sheet';
var isFile = parsed.hashData.type === 'file';
var isSharedFolder = parsed.type === 'drive';
@ -586,7 +590,8 @@ define([
UI.confirm(changePwConfirm, function (yes) {
if (!yes) { pLocked = false; return; }
$(passwordOk).html('').append(h('span.fa.fa-spinner.fa-spin', {style: 'margin-left: 0'}));
var q = isFile ? 'Q_BLOB_PASSWORD_CHANGE' : 'Q_PAD_PASSWORD_CHANGE';
var q = isFile ? 'Q_BLOB_PASSWORD_CHANGE' :
(isOO ? 'Q_OO_PASSWORD_CHANGE' : 'Q_PAD_PASSWORD_CHANGE');
// If this is a file password change, register to the upload events:
// * if there is a pending upload, ask if we shoudl interrupt
@ -737,12 +742,22 @@ define([
UIElements.getProperties = function (common, data, cb) {
var c1;
var c2;
var button = [{
className: 'primary',
name: Messages.okButton,
onClick: function () {},
keys: [13]
}];
NThen(function (waitFor) {
getPadProperties(common, data, waitFor(function (e, c) {
c1 = c[0];
c1 = UI.dialog.customModal(c[0], {
buttons: button
});
}));
getRightsProperties(common, data, waitFor(function (e, c) {
c2 = c[0];
c2 = UI.dialog.customModal(c[0], {
buttons: button
});
}));
}).nThen(function () {
var tabs = UI.dialog.tabs([{
@ -782,8 +797,6 @@ define([
var noOthers = icons.length === 0 ? '.cp-usergrid-empty' : '';
var buttonSelect = h('button', Messages.share_selectAll);
var buttonDeselect = h('button', Messages.share_deselectAll);
var inputFilter = h('input', {
placeholder: Messages.share_filterFriend
});
@ -791,9 +804,7 @@ define([
var div = h('div.cp-usergrid-container' + noOthers + (config.large?'.large':''), [
label ? h('label', label) : undefined,
h('div.cp-usergrid-filter', (config.noFilter || config.noSelect) ? undefined : [
inputFilter,
buttonSelect,
buttonDeselect
inputFilter
]),
]);
var $div = $(div);
@ -806,23 +817,8 @@ define([
$div.find('.cp-usergrid-user:not(.cp-selected):not([data-name*="'+name+'"])').hide();
}
};
$(inputFilter).on('keydown keyup change', redraw);
$(buttonSelect).click(function () {
$div.find('.cp-usergrid-user:not(.cp-selected):visible').addClass('cp-selected');
onSelect();
});
$(buttonDeselect).click(function () {
$div.find('.cp-usergrid-user.cp-selected').removeClass('cp-selected').each(function (i, el) {
var order = $(el).attr('data-order');
if (!order) { return; }
$(el).attr('style', 'order:'+order);
});
redraw();
onSelect();
});
$(div).append(h('div.cp-usergrid-grid', icons));
if (!config.noSelect) {
$div.on('click', '.cp-usergrid-user', function () {
@ -880,10 +876,11 @@ define([
delete friends[curve];
});
var friendsList = UIElements.getUserGrid(null, {
var friendsList = UIElements.getUserGrid(Messages.share_linkFriends, {
common: common,
data: friends,
noFilter: false
noFilter: false,
large: true
}, refreshButtons);
var friendDiv = friendsList.div;
$div.append(friendDiv);
@ -909,6 +906,7 @@ define([
var teamsList = UIElements.getUserGrid(Messages.share_linkTeam, {
common: common,
noFilter: true,
large: true,
data: teams
}, refreshButtons);
$div.append(teamsList.div);
@ -1002,10 +1000,53 @@ define([
});
return {
content: div,
button: shareButton
buttons: [shareButton]
};
};
var noContactsMessage = function(common){
var metadataMgr = common.getMetadataMgr();
var data = metadataMgr.getUserData();
var origin = metadataMgr.getPrivateData().origin;
if (common.isLoggedIn()) {
return {
content: h('p', Messages.share_noContactsLoggedIn),
buttons: [{
className: 'secondary',
name: Messages.share_copyProfileLink,
onClick: function () {
var profile = data.profile ? (origin + '/profile/#' + data.profile) : '';
var success = Clipboard.copy(profile);
if (success) { UI.log(Messages.shareSuccess); }
},
keys: [13]
}]
};
} else {
return {
content: h('p', Messages.share_noContactsNotLoggedIn),
buttons: [{
className: 'secondary',
name: Messages.login_register,
onClick: function () {
common.setLoginRedirect(function () {
common.gotoURL('/register/');
});
}
}, {
className: 'secondary',
name: Messages.login_login,
onClick: function () {
common.setLoginRedirect(function () {
common.gotoURL('/login/');
});
}
}
]
};
}
};
UIElements.createShareModal = function (config) {
var origin = config.origin;
var pathname = config.pathname;
@ -1014,12 +1055,29 @@ define([
if (!hashes || (!hashes.editHash && !hashes.viewHash)) { return; }
// check if the pad is password protected
var hash = hashes.editHash || hashes.viewHash;
var href = origin + pathname + '#' + hash;
var parsedHref = Hash.parsePadUrl(href);
var hasPassword = parsedHref.hashData.password;
var makeFaqLink = function () {
var link = h('span', [
h('i.fa.fa-question-circle'),
h('a', {href: '#'}, Messages.passwordFaqLink)
]);
$(link).click(function () {
common.openURL(config.origin + "/faq.html#security-pad_password");
});
return link;
};
var parsed = Hash.parsePadUrl(pathname);
var canPresent = ['code', 'slide'].indexOf(parsed.type) !== -1;
var rights = h('div.msg.cp-inline-radio-group', [
h('label', Messages.share_linkAccess),
h('br'),
h('div.radio-group',[
UI.createRadio('accessRights', 'cp-share-editable-false',
Messages.share_linkView, true, { mark: {tabindex:1} }),
@ -1066,9 +1124,42 @@ define([
h('br'),
] : [
UI.createCheckbox('cp-share-embed', Messages.share_linkEmbed, false, { mark: {tabindex:1} }),
h('br'),
];
linkContent.push(UI.dialog.selectable('', { id: 'cp-share-link-preview', tabindex: 1 }));
linkContent.push(h('div.cp-spacer'));
linkContent.push(UI.dialog.selectableArea('', { id: 'cp-share-link-preview', tabindex: 1, rows:3}));
// Show alert if the pad is password protected
if (hasPassword) {
linkContent.push(h('div.alert.alert-primary', [
h('i.fa.fa-lock'),
Messages.share_linkPasswordAlert, h('br'),
makeFaqLink()
]));
}
// warning about sharing links
var localStore = window.cryptpadStore;
var dismissButton = h('span.fa.fa-times');
var shareLinkWarning = h('div.alert.alert-warning.dismissable',
{ style: 'display: none;' },
[
h('span.cp-inline-alert-text', Messages.share_linkWarning),
dismissButton
]);
linkContent.push(shareLinkWarning);
localStore.get('hide-alert-shareLinkWarning', function (val) {
if (val === '1') { return; }
$(shareLinkWarning).show();
$(dismissButton).on('click', function () {
localStore.put('hide-alert-shareLinkWarning', '1');
$(shareLinkWarning).remove();
});
});
var link = h('div.cp-share-modal', linkContent);
var $link = $(link);
@ -1125,20 +1216,31 @@ define([
var hasFriends = Object.keys(config.friends || {}).length !== 0;
var onFriendShare = Util.mkEvent();
var friendsObject = hasFriends ? createShareWithFriends(config, onFriendShare, getLinkValue) : {
content: h('p', Messages.team_noFriend),
button: {}
};
var friendsObject = hasFriends ? createShareWithFriends(config, onFriendShare, getLinkValue) : noContactsMessage(common);
var friendsList = friendsObject.content;
onFriendShare.reg(saveValue);
// XXX Don't display access rights if no contacts
var contactsContent = h('div.cp-share-modal');
$(contactsContent).append(friendsList);
var $contactsContent = $(contactsContent);
var contactButtons = [makeCancelButton(),
friendsObject.button];
$contactsContent.append(friendsList);
// Show alert if the pad is password protected
if (hasPassword) {
$contactsContent.append(h('div.alert.alert-primary', [
h('i.fa.fa-unlock'),
Messages.share_contactPasswordAlert, h('br'),
makeFaqLink()
]));
}
var contactButtons = friendsObject.buttons;
contactButtons.unshift(makeCancelButton());
var frameContacts = UI.dialog.customModal(contactsContent, {
buttons: contactButtons,
@ -1154,9 +1256,18 @@ define([
};
var embedContent = [
h('p', Messages.viewEmbedTag),
h('br'),
UI.dialog.selectable(getEmbedValue(), { id: 'cp-embed-link-preview', tabindex: 1 })
UI.dialog.selectableArea(getEmbedValue(), { id: 'cp-embed-link-preview', tabindex: 1, rows: 3})
];
// Show alert if the pad is password protected
if (hasPassword) {
embedContent.push(h('div.alert.alert-primary', [
h('i.fa.fa-lock'), ' ',
Messages.share_embedPasswordAlert, h('br'),
makeFaqLink()
]));
}
var embedButtons = [
makeCancelButton(), {
className: 'primary',
@ -1185,13 +1296,15 @@ define([
// Create modal
var tabs = [{
title: Messages.share_linkCategory,
icon: "fa fa-link",
content: frameLink
}, {
title: Messages.share_contactCategory,
icon: "fa fa-address-book",
content: frameContacts
content: frameContacts,
active: hasFriends
}, {
title: Messages.share_linkCategory,
icon: "fa fa-link",
content: frameLink,
active: !hasFriends
}, {
title: Messages.share_embedCategory,
icon: "fa fa-code",
@ -1259,6 +1372,21 @@ define([
if (!hashes.fileHash) { throw new Error("You must provide a file hash"); }
var url = origin + pathname + '#' + hashes.fileHash;
// check if the file is password protected
var parsedHref = Hash.parsePadUrl(url);
var hasPassword = parsedHref.hashData.password;
var makeFaqLink = function () {
var link = h('span', [
h('i.fa.fa-question-circle'),
h('a', {href: '#'}, Messages.passwordFaqLink)
]);
$(link).click(function () {
common.openURL(config.origin + "/faq.html#security-pad_password");
});
return link;
};
var getLinkValue = function () { return url; };
var makeCancelButton = function() {
@ -1270,9 +1398,40 @@ define([
// Share link tab
var linkContent = [
UI.dialog.selectable(getLinkValue(), { id: 'cp-share-link-preview', tabindex: 1 })
UI.dialog.selectableArea(getLinkValue(), { id: 'cp-share-link-preview', tabindex: 1, rows:2 })
];
// Show alert if the pad is password protected
if (hasPassword) {
linkContent.push(h('div.alert.alert-primary', [
h('i.fa.fa-lock'),
Messages.share_linkPasswordAlert, h('br'),
makeFaqLink()
]));
}
// warning about sharing links
var localStore = window.cryptpadStore;
var dismissButton = h('span.fa.fa-times');
var shareLinkWarning = h('div.alert.alert-warning.dismissable',
{ style: 'display: none;' },
[
h('span.cp-inline-alert-text', Messages.share_linkWarning),
dismissButton
]);
linkContent.push(shareLinkWarning);
localStore.get('hide-alert-shareLinkWarning', function (val) {
if (val === '1') { return; }
$(shareLinkWarning).show();
$(dismissButton).on('click', function () {
localStore.put('hide-alert-shareLinkWarning', '1');
$(shareLinkWarning).remove();
});
});
var link = h('div.cp-share-modal', linkContent);
var linkButtons = [
@ -1298,17 +1457,24 @@ define([
// share with contacts tab
var hasFriends = Object.keys(config.friends || {}).length !== 0;
var friendsObject = hasFriends ? createShareWithFriends(config, null, getLinkValue) : {
content: h('p', Messages.share_noContacts),
button: {}
};
var friendsObject = hasFriends ? createShareWithFriends(config, null, getLinkValue) : noContactsMessage(common);
var friendsList = friendsObject.content;
var contactsContent = h('div.cp-share-modal');
$(contactsContent).append(friendsList);
var $contactsContent = $(contactsContent);
$contactsContent.append(friendsList);
// Show alert if the pad is password protected
if (hasPassword) {
$contactsContent.append(h('div.alert.alert-primary', [
h('i.fa.fa-unlock'),
Messages.share_contactPasswordAlert, h('br'),
makeFaqLink()
]));
}
var contactButtons = [makeCancelButton(),
friendsObject.button];
var contactButtons = friendsObject.buttons;
contactButtons.unshift(makeCancelButton());
var frameContacts = UI.dialog.customModal(contactsContent, {
buttons: contactButtons,
@ -1319,12 +1485,20 @@ define([
// Embed tab
var embed = h('div.cp-share-modal', [
h('p', Messages.fileEmbedScript),
h('br'),
UI.dialog.selectable(common.getMediatagScript()),
h('p', Messages.fileEmbedTag),
h('br'),
UI.dialog.selectable(common.getMediatagFromHref(fileData)),
]);
// Show alert if the pad is password protected
if (hasPassword) {
embed.append(h('div.alert.alert-primary', [
h('i.fa.fa-lock'), ' ',
Messages.share_embedPasswordAlert, h('br'),
makeFaqLink()
]));
}
var embedButtons = [{
className: 'cancel',
name: Messages.cancel,
@ -1347,13 +1521,15 @@ define([
// Create modal
var tabs = [{
title: Messages.share_linkCategory,
icon: "fa fa-link",
content: frameLink
}, {
title: Messages.share_contactCategory,
icon: "fa fa-address-book",
content: frameContacts
content: frameContacts,
active: hasFriends,
}, {
title: Messages.share_linkCategory,
icon: "fa fa-link",
content: frameLink,
active: !hasFriends
}, {
title: Messages.share_embedCategory,
icon: "fa fa-code",
@ -1399,6 +1575,7 @@ define([
var list = UIElements.getUserGrid(Messages.team_pickFriends, {
common: common,
data: config.friends,
large: true
}, refreshButton);
$div = $(list.div);
refreshButton();
@ -1757,7 +1934,7 @@ define([
if (e) { return void console.error(e); }
UIElements.getProperties(common, data, function (e, $prop) {
if (e) { return void console.error(e); }
UI.alert($prop[0], undefined, true);
UI.openCustomModal($prop[0]);
});
});
});

@ -1155,6 +1155,242 @@ define([
});
};
common.changeOOPassword = function (data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
var href = data.href;
var newPassword = data.password;
var teamId = data.teamId;
if (!href) { return void cb({ error: 'EINVAL_HREF' }); }
var parsed = Hash.parsePadUrl(href);
if (!parsed.hash) { return void cb({ error: 'EINVAL_HREF' }); }
if (parsed.type !== 'sheet') { return void cb({ error: 'EINVAL_TYPE' }); }
var warning = false;
var newHash, newRoHref;
var oldSecret;
var oldMetadata;
var oldRtChannel;
var privateData;
var padData;
var newSecret;
if (parsed.hashData.version >= 2) {
newSecret = Hash.getSecrets(parsed.type, parsed.hash, newPassword);
if (!(newSecret.keys && newSecret.keys.editKeyStr)) {
return void cb({error: 'EAUTH'});
}
newHash = Hash.getEditHashFromKeys(newSecret);
}
var newHref = '/' + parsed.type + '/#' + newHash;
var newRtChannel = Hash.createChannelId();
var Crypt, Crypto;
var cryptgetVal;
var optsPut = {
password: newPassword,
metadata: {
validateKey: newSecret.keys.validateKey
},
};
Nthen(function (waitFor) {
common.getPadAttribute('', waitFor(function (err, _data) {
padData = _data;
}), href);
}).nThen(function (waitFor) {
oldSecret = Hash.getSecrets(parsed.type, parsed.hash, padData.password);
require([
'/common/cryptget.js',
'/bower_components/chainpad-crypto/crypto.js',
], waitFor(function (_Crypt, _Crypto) {
Crypt = _Crypt;
Crypto = _Crypto;
}));
common.getPadMetadata({channel: oldSecret.channel}, waitFor(function (metadata) {
oldMetadata = metadata;
}));
common.getMetadata(waitFor(function (err, data) {
if (err) {
waitFor.abort();
return void cb({ error: err });
}
privateData = data.priv;
}));
}).nThen(function (waitFor) {
// Check if we're allowed to change the password
var owners = oldMetadata.owners;
optsPut.metadata.owners = owners;
var edPublic = teamId ? (privateData.teams[teamId] || {}).edPublic : privateData.edPublic;
var isOwner = Array.isArray(owners) && edPublic && owners.indexOf(edPublic) !== -1;
if (!isOwner) {
// We're not an owner, we shouldn't be able to change the password!
waitFor.abort();
return void cb({ error: 'EPERM' });
}
var mailbox = oldMetadata.mailbox;
if (mailbox) {
// Create the encryptors to be able to decrypt and re-encrypt the mailboxes
var oldCrypto = Crypto.createEncryptor(oldSecret.keys);
var newCrypto = Crypto.createEncryptor(newSecret.keys);
var m;
if (typeof(mailbox) === "string") {
try {
m = newCrypto.encrypt(oldCrypto.decrypt(mailbox, true, true));
} catch (e) {}
} else if (mailbox && typeof(mailbox) === "object") {
m = {};
Object.keys(mailbox).forEach(function (ed) {
try {
m[ed] = newCrypto.encrypt(oldCrypto.decrypt(mailbox[ed], true, true));
} catch (e) {
console.error(e);
}
});
}
optsPut.metadata.mailbox = m;
}
var expire = oldMetadata.expire;
if (expire) {
optsPut.metadata.expire = (expire - (+new Date())) / 1000; // Lifetime in seconds
}
// Get last cp (cryptget)
Crypt.get(parsed.hash, waitFor(function (err, val) {
if (err) {
waitFor.abort();
return void cb({ error: err });
}
try {
cryptgetVal = JSON.parse(val);
if (!cryptgetVal.content) {
waitFor.abort();
return void cb({ error: 'INVALID_CONTENT' });
}
} catch (e) {
waitFor.abort();
return void cb({ error: 'CANT_PARSE' });
}
}), {
password: padData.password
});
}).nThen(function (waitFor) {
// Re-encrypt rtchannel
oldRtChannel = Util.find(cryptgetVal, ['content', 'channel']);
var newCrypto = Crypto.createEncryptor(newSecret.keys);
var oldCrypto = Crypto.createEncryptor(oldSecret.keys);
var cps = Util.find(cryptgetVal, ['content', 'hashes']);
var l = Object.keys(cps).length;
var lastCp = l ? cps[l] : {};
cryptgetVal.content.hashes = {};
common.getHistory({
channel: oldRtChannel,
lastKnownHash: lastCp.hash
}, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
console.error(obj);
return void cb(obj.error);
}
var msgs = obj;
var newHistory = msgs.map(function (str) {
try {
var d = oldCrypto.decrypt(str, true, true);
return newCrypto.encrypt(d);
} catch (e) {
console.log(e);
waitFor.abort();
return void cb({error: e});
}
});
// Update last knwon hash in cryptgetVal
if (lastCp) {
lastCp.hash = newHistory[0].slice(0, 64);
lastCp.index = 50;
cryptgetVal.content.hashes[1] = lastCp;
}
common.onlyoffice.execCommand({
cmd: 'REENCRYPT',
data: {
channel: newRtChannel,
msgs: newHistory,
metadata: optsPut.metadata
}
}, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
console.warn(obj);
return void cb(obj.error);
}
}));
}));
}).nThen(function (waitFor) {
// The new rt channel is ready
// The blob uses its own encryption and doesn't need to be reencrypted
cryptgetVal.content.channel = newRtChannel;
Crypt.put(newHash, JSON.stringify(cryptgetVal), waitFor(function (err) {
if (err) {
waitFor.abort();
return void cb({ error: err });
}
}), optsPut);
}).nThen(function (waitFor) {
pad.leavePad({
channel: oldSecret.channel
}, waitFor());
pad.onDisconnectEvent.fire(true);
}).nThen(function (waitFor) {
// Set the new password to our pad data
common.setPadAttribute('password', newPassword, waitFor(function (err) {
if (err) { warning = true; }
}), href);
common.setPadAttribute('channel', newSecret.channel, waitFor(function (err) {
if (err) { warning = true; }
}), href);
common.setPadAttribute('rtChannel', newRtChannel, waitFor(function (err) {
if (err) { warning = true; }
}), href);
var viewHash = Hash.getViewHashFromKeys(newSecret);
newRoHref = '/' + parsed.type + '/#' + viewHash;
common.setPadAttribute('roHref', newRoHref, waitFor(function (err) {
if (err) { warning = true; }
}), href);
if (parsed.hashData.password && newPassword) { return; } // same hash
common.setPadAttribute('href', newHref, waitFor(function (err) {
if (err) { warning = true; }
}), href);
}).nThen(function (waitFor) {
// delete the old pad
common.removeOwnedChannel({
channel: oldSecret.channel,
teamId: teamId
}, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
console.info(obj);
return void cb(obj.error);
}
common.removeOwnedChannel({
channel: oldRtChannel,
teamId: teamId
}, waitFor());
}));
}).nThen(function () {
cb({
warning: warning,
hash: newHash,
href: newHref,
roHref: newRoHref
});
});
};
common.changeUserPassword = function (Crypt, edPublic, data, cb) {
if (!edPublic) {
return void cb({
@ -1350,6 +1586,9 @@ define([
common.getFullHistory = function (data, cb) {
postMessage("GET_FULL_HISTORY", data, cb, {timeout: 180000});
};
common.getHistory = function (data, cb) {
postMessage("GET_HISTORY", data, cb, {timeout: 180000});
};
common.getHistoryRange = function (data, cb) {
postMessage("GET_HISTORY_RANGE", data, cb);
};

@ -84,7 +84,7 @@ define([
var defaultCode = renderer.code;
renderer.code = function (code, language) {
if (language === 'mermaid' && (code.match(/^sequenceDiagram/) || code.match(/^graph/))) {
if (language === 'mermaid' && code.match(/^(graph|pie|gantt|sequenceDiagram|classDiagram|gitGraph)/)) {
return '<pre class="mermaid">'+code+'</pre>';
} else {
return defaultCode.apply(renderer, arguments);
@ -197,7 +197,6 @@ define([
'APPLET',
'VIDEO', // privacy implications of videos are the same as images
'AUDIO', // same with audio
'SVG'
];
var unsafeTag = function (info) {
/*if (info.node && $(info.node).parents('media-tag').length) {
@ -307,8 +306,39 @@ define([
var Dom = domFromHTML($('<div>').append($div).html());
$content[0].normalize();
$content.find('pre.mermaid[data-processed="true"]').remove();
var mermaid_source = [];
var mermaid_cache = {};
// iterate over the unrendered mermaid inputs, caching their source as you go
$(newDomFixed).find('pre.mermaid').each(function (index, el) {
if (el.childNodes.length === 1 && el.childNodes[0].nodeType === 3) {
var src = el.childNodes[0].wholeText;
el.setAttribute('mermaid-source', src);
mermaid_source[index] = src;
}
});
// iterate over rendered mermaid charts
$content.find('pre.mermaid:not([processed="true"])').each(function (index, el) {
// retrieve the attached source code which it was drawn
var src = el.getAttribute('mermaid-source');
// check if that source exists in the set of charts which are about to be rendered
if (mermaid_source.indexOf(src) === -1) {
// if it's not, then you can remove it
if (el.parentNode && el.parentNode.children.length) {
el.parentNode.removeChild(el);
}
} else if (el.childNodes.length === 1 && el.childNodes[0].nodeType !== 3) {
// otherwise, confirm that the content of the rendered chart is not a text node
// and keep a copy of it
mermaid_cache[src] = el.childNodes[0];
}
});
var oldDom = domFromHTML($content[0].outerHTML);
var patch = makeDiff(oldDom, Dom, id);
if (typeof(patch) === 'string') {
throw new Error(patch);
@ -348,8 +378,32 @@ define([
var target = document.getElementById($a.attr('data-href'));
if (target) { target.scrollIntoView(); }
});
// loop over mermaid elements in the rendered content
$content.find('pre.mermaid').each(function (index, el) {
// since you've simply drawn the content that was supplied via markdown
// you can assume that the index of your rendered charts matches that
// of those in the markdown source.
var src = mermaid_source[index];
el.setAttribute('mermaid-source', src);
var cached = mermaid_cache[src];
// check if you had cached a pre-rendered instance of the supplied source
if (typeof(cached) !== 'object') { return; }
// if there's a cached rendering, empty out the contained source code
// which would otherwise be drawn again.
// apparently this is the fastest way to empty out an element
while (el.firstChild) { el.removeChild(el.firstChild); } //el.innerHTML = '';
// insert the cached graph
el.appendChild(cached);
// and set a flag indicating that this graph need not be reprocessed
el.setAttribute('data-processed', true);
});
try {
Mermaid.init();
// finally, draw any graphs which have changed and were thus not cached
Mermaid.init(undefined, $content.find('pre.mermaid:not([data-processed="true"])'));
} catch (e) { console.error(e); }
}
};

@ -79,7 +79,7 @@ define([
var faColor = 'cptools-palette';
var faTrash = 'fa-trash';
var faDelete = 'fa-eraser';
var faProperties = 'fa-database';
var faProperties = 'fa-info-circle';
var faTags = 'fa-hashtag';
var faUploadFiles = 'cptools-file-upload';
var faUploadFolder = 'cptools-folder-upload';
@ -4187,7 +4187,7 @@ define([
}
getProperties(el, function (e, $prop) {
if (e) { return void logError(e); }
UI.alert($prop[0], undefined, true);
UI.openCustomModal($prop[0]);
});
}
else if ($this.hasClass("cp-app-drive-context-hashtag")) {

@ -1693,7 +1693,7 @@ define([
// GET_FULL_HISTORY from sframe-common-outer
Store.getFullHistory = function (clientId, data, cb) {
var network = store.network;
var hkn = network.historyKeeper;
var hk = network.historyKeeper;
//var crypto = Crypto.createEncryptor(data.keys);
// Get the history messages and send them to the iframe
var parse = function (msg) {
@ -1708,8 +1708,10 @@ define([
var onMsg = function (msg) {
if (completed) { return; }
var parsed = parse(msg);
if (!parsed) { return; }
if (parsed[0] === 'FULL_HISTORY_END') {
cb(msgs);
network.off('message', onMsg);
completed = true;
return;
}
@ -1726,12 +1728,69 @@ define([
}
};
network.on('message', onMsg);
network.sendto(hkn, JSON.stringify(['GET_FULL_HISTORY', data.channel, data.validateKey]));
network.sendto(hk, JSON.stringify(['GET_FULL_HISTORY', data.channel, data.validateKey]));
};
Store.getHistory = function (clientId, data, cb) {
var network = store.network;
var hk = network.historyKeeper;
var parse = function (msg) {
try {
return JSON.parse(msg);
} catch (e) {
return null;
}
};
var msgs = [];
var completed = false;
var onMsg = function (msg, sender) {
if (completed) { return; }
if (sender !== hk) { return; }
var parsed = parse(msg);
if (!parsed) { return; }
// Ignore the metadata message
if (parsed.validateKey && parsed.channel) { return; }
if (parsed.error && parsed.channel) {
if (parsed.channel === data.channel) {
network.off('message', onMsg);
completed = true;
cb({error: parsed.error});
}
return;
}
// End of history: cb
if (parsed.state === 1 && parsed.channel) {
if (parsed.channel !== data.channel) { return; }
cb(msgs);
network.off('message', onMsg);
completed = true;
return;
}
msg = parsed[4];
// Keep only the history for our channel
if (parsed[3] !== data.channel) { return; }
if (msg) {
msg = msg.replace(/cp\|(([A-Za-z0-9+\/=]+)\|)?/, '');
msgs.push(msg);
}
};
network.on('message', onMsg);
var cfg = {
lastKnownHash: data.lastKnownHash
};
var msg = ['GET_HISTORY', data.channel, cfg];
network.sendto(hk, JSON.stringify(msg));
};
Store.getHistoryRange = function (clientId, data, cb) {
var network = store.network;
var hkn = network.historyKeeper;
var hk = network.historyKeeper;
var parse = function (msg) {
try {
return JSON.parse(msg);
@ -1779,7 +1838,7 @@ define([
};
network.on('message', onMsg);
network.sendto(hkn, JSON.stringify(['GET_HISTORY_RANGE', data.channel, {
network.sendto(hk, JSON.stringify(['GET_HISTORY_RANGE', data.channel, {
from: data.lastKnownHash,
cpCount: 2,
txid: txid

@ -200,6 +200,41 @@ define([
}));
};
var reencrypt = function (ctx, data, cId, cb) {
var channel = data.channel;
var network = ctx.store.network;
var onOpen = function (wc) {
var hk = network.historyKeeper;
var cfg = {
metadata: data.metadata
};
var msg = ['GET_HISTORY', wc.id, cfg];
network.sendto(hk, JSON.stringify(msg));
data.msgs.forEach(function (msg) {
wc.bcast(msg);
});
wc.leave();
cb();
};
ctx.store.anon_rpc.send("IS_NEW_CHANNEL", channel, function (e, response) {
if (e) { return void cb({error: e}); }
var isNew;
if (response && response.length && typeof(response[0]) === 'boolean') {
isNew = response[0];
} else {
cb({error: 'INVALID_RESPONSE'});
}
if (!isNew) { return void cb({error: 'EEXISTS'}); }
// Channel is new: we can push our reencrypted history
network.join(channel).then(onOpen, function (err) {
return void cb({error: err});
});
});
};
var leaveChannel = function (ctx, padChan) {
// Leave channel and prevent reconnect when we leave a pad
Object.keys(ctx.channels).some(function (ooChan) {
@ -267,6 +302,9 @@ define([
if (cmd === 'OPEN_CHANNEL') {
return void openChannel(ctx, data, clientId, cb);
}
if (cmd === 'REENCRYPT') {
return void reencrypt(ctx, data, clientId, cb);
}
};
return oo;

@ -73,6 +73,7 @@ define([
JOIN_PAD: Store.joinPad,
LEAVE_PAD: Store.leavePad,
GET_FULL_HISTORY: Store.getFullHistory,
GET_HISTORY: Store.getHistory,
GET_HISTORY_RANGE: Store.getHistoryRange,
IS_NEW_CHANNEL: Store.isNewChannel,
REQUEST_PAD_ACCESS: Store.requestPadAccess,

@ -769,18 +769,18 @@ define([
continue;
}
var href;
var decryptedHref;
try {
href = el.href && ((el.href.indexOf('#') !== -1) ? el.href : exp.cryptor.decrypt(el.href));
decryptedHref = el.href && ((el.href.indexOf('#') !== -1) ? el.href : exp.cryptor.decrypt(el.href));
} catch (e) {}
if (href && href.indexOf('#') === -1) {
if (decryptedHref && decryptedHref.indexOf('#') === -1) {
// If we can't decrypt the href, it means we don't have the correct secondaryKey and we're in readOnly mode:
// abort now, we won't be able to fix anything anyway
continue;
}
var parsed = Hash.parsePadUrl(href || el.roHref);
var parsed = Hash.parsePadUrl(decryptedHref || el.roHref);
var secret;
// Clean invalid hash
@ -797,9 +797,9 @@ define([
}
// If we have an edit link, check the view link
if (href && parsed.hashData.type === "pad" && parsed.hashData.version) {
if (decryptedHref && parsed.hashData.type === "pad" && parsed.hashData.version) {
if (parsed.hashData.mode === "view") {
el.roHref = href;
el.roHref = decryptedHref;
delete el.href;
} else if (!el.roHref) {
secret = Hash.getSecrets(parsed.type, parsed.hash, el.password);
@ -818,7 +818,9 @@ define([
}
// Fix href
if (href && href.slice(0,1) !== '/') { el.href = exp.cryptor.encrypt(Hash.getRelativeHref(href)); }
if (decryptedHref && decryptedHref.slice(0,1) !== '/') {
el.href = exp.cryptor.encrypt(Hash.getRelativeHref(decryptedHref));
}
// Fix creation time
if (!el.ctime) { el.ctime = el.atime; }
// Fix title

@ -484,10 +484,29 @@ define([
Cryptpad.mailbox.execCommand(data, cb);
});
sframeChan.on('Q_SET_LOGIN_REDIRECT', function (data, cb) {
sessionStorage.redirectTo = window.location.href;
cb();
});
sframeChan.on('Q_STORE_IN_TEAM', function (data, cb) {
Cryptpad.storeInTeam(data, cb);
});
sframeChan.on('EV_GOTO_URL', function (url) {
if (url) {
window.location.href = url;
} else {
window.location.reload();
}
});
sframeChan.on('EV_OPEN_URL', function (url) {
if (url) {
window.open(url);
}
});
};
addCommonRpc(sframeChan);
@ -634,11 +653,6 @@ define([
Notifier.notify(data);
});
sframeChan.on('Q_SET_LOGIN_REDIRECT', function (data, cb) {
sessionStorage.redirectTo = window.location.href;
cb();
});
sframeChan.on('Q_MOVE_TO_TRASH', function (data, cb) {
cb = cb || $.noop;
if (readOnly && hashes.editHash) {
@ -956,20 +970,6 @@ define([
});
});
sframeChan.on('EV_GOTO_URL', function (url) {
if (url) {
window.location.href = url;
} else {
window.location.reload();
}
});
sframeChan.on('EV_OPEN_URL', function (url) {
if (url) {
window.open(url);
}
});
sframeChan.on('Q_PIN_GET_USAGE', function (teamId, cb) {
Cryptpad.isOverPinLimit(teamId, function (err, overLimit, data) {
cb({
@ -1008,6 +1008,11 @@ define([
}, cb);
});
sframeChan.on('Q_OO_PASSWORD_CHANGE', function (data, cb) {
data.href = data.href || window.location.href;
Cryptpad.changeOOPassword(data, cb);
});
sframeChan.on('Q_PAD_PASSWORD_CHANGE', function (data, cb) {
data.href = data.href || window.location.href;
Cryptpad.changePadPassword(Cryptget, Crypto, data, cb);

@ -754,6 +754,10 @@
"crypto": {
"q": "Welche Kryptografie benutzt ihr?",
"a": "CryptPad basiert auf zwei quelloffenen Kryptografiebibliotheken: <a href='https://github.com/dchest/tweetnacl-js' target='_blank'>tweetnacl.js</a> und <a href='https://github.com/dchest/scrypt-async-js' target='_blank'>scrypt-async.js</a>.<br><br>Scrypt ist eine <em>Passwort-basierte Schlüsselableitungsfunktion</em>. Wir werden sie, um deinen Benutzernamen und dein Passwort in ein einzigartiges Schlüsselpaar umzuwandeln. Dieses sichert den Zugang zu deinem CryptDrive, so dass nur du auf die Liste deiner Pads zugreifen kannst.<br><br> Wir verwenden die Verschlüsselung <em>xsalsa20-poly1305</em> und <em>x25519-xsalsa20-poly1305</em> von tweetnacl, um Dokumente und den Chatverlauf zu verschlüsseln."
},
"pad_password": {
"q": "Was passiert, wenn ich einen Ordner oder ein Pad mit einem Passwort schütze?",
"a": "Du kannst Pads oder geteilte Ordner bei der Erstellung mit einem Passwort schützen. Du kannst auch jederzeit im Eigenschaften-Menü ein Passwort setzen/ändern/entfernen.<br><br>Passwörter für Pads und geteilte Ordner sollen den Link schützen, wenn du ihn über einen unsicheren Kanal wie Mail oder Textnachricht teilst. Wenn jemand den Link abfängt, aber nicht das Passwort kennt, kann er nicht auf dein Dokument zugreifen.<br><br>Beim Teilen mit Kontakten oder Teams innerhalb von CryptPad wird die Kommunikation verschlüsselt und es wird angenommen, dass der Empfänger auf das Dokument zugreifen darf. Daher wird das Passwort zusammen mit dem Pad geteilt. Der Empfänger, und du selbst, werden beim Öffnen des Dokuments <b>nicht</b> danach gefragt."
}
},
"usability": {
@ -1160,7 +1164,7 @@
"owner_request_declined": "{0} hat deine Einladung abgelehnt, ein Eigentümer von <b>{1}</b> zu sein",
"owner_removed": "{0} hat dich als Eigentümer von <b>{1}</b> entfernt",
"owner_removedPending": "{0} hat die Einladung zur Eigentümerschaft von <b>{1}</b> zurückgezogen",
"share_linkTeam": "Mit einem Team teilen",
"share_linkTeam": "Zu Team-Drive hinzufügen",
"team_inviteModalButton": "Einladen",
"team_pickFriends": "Freunde auswählen, um sie in dieses Team einzuladen",
"team_noFriend": "Du bist derzeit mit keinen Freunden auf CryptPad verbunden.",
@ -1242,5 +1246,10 @@
"teams_table_owners": "Team verwalten",
"teams_table_role": "Rolle",
"share_contactCategory": "Kontakte",
"pad_wordCount": "Wörter: {0}"
"pad_wordCount": "Wörter: {0}",
"share_linkWarning": "Dieser Link enthält die Schlüssel zu deinem Dokument. Empfänger erhalten unwiderruflichen Zugriff zu deinen Inhalten.",
"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"
}

@ -761,6 +761,10 @@
"crypto": {
"q": "Quelle cryptographie utilisez-vous ?",
"a": "CryptPad est basé sur deux librairies open-source de cryptographie : <a href=\"https://github.com/dchest/tweetnacl-js\" target=\"_blank\">tweetnacl.js</a> et <a href=\"https://github.com/dchest/scrypt-async-js\" target=\"_blank\">scrypt-async.js</a>.<br><b>Scrypt</b> est une <em>fonction de dérivation de clé</em> basée sur un mot de passe. Nous l'utilisons pour transformer votre nom d'utilisateur et votre mot de passe en un unique ensemble de clés qui sécurise l'accès à votre CryptDrive afin que vous seul puissiez accéder à votre liste de pads.<br>Nous utilisons les outils de chiffrement <em>xsalsa20-poly1305</em> et <em>x25519-xsalsa20-poly1305</em> fournis par <b>tweetnacl</b> pour chiffrer vos pads et l'historique du chat respectivement."
},
"pad_password": {
"q": "Que se passe t'il quand je protège un pad/dossier avec un mot de passe ?",
"a": "Vous pouvez protéger tout nouveau pad ou dossier avec un mot de passe. Vous pouvez aussi utiliser le menu <em>propriétés</em> pour ajouter/changer/supprimer un mot de passe par la suite. <br><br>Les mots de passe sur les pads et dossiers partagés sont faits pour protéger les liens quand ils sont envoyés de manière non sécurisée (par exemple par email ou SMS). Si quelqu'un intercepte le lien sans avoir le mot de passe, ils n'auront pas accès à votre document.<br><br>Quand vous partagez avec vos contacts ou équipes sur CryptPad, les communications sont chiffrées et nous partons du principe que vous voulez donner l'accès. C'est pourquoi le mot de passe est alors stocké et envoyé avec le pad quand vous l'envoyez. Les destinataires, ou vous même, n'ont <b>pas</b> a le donner pour ouvrir le document."
}
},
"usability": {
@ -1072,7 +1076,7 @@
"share_selectAll": "Tout",
"share_deselectAll": "Aucun",
"share_filterFriend": "Rechercher par nom",
"share_linkFriends": "Partager avec des amis",
"share_linkFriends": "Partager avec des contacts",
"share_withFriends": "Partager",
"notifications_dismiss": "Cacher",
"fm_info_sharedFolderHistory": "Vous regardez l'historique de votre dossier partagé <b>{0}</b><br/>Votre CryptDrive restera en lecture seule pendant la navigation.",
@ -1162,7 +1166,7 @@
"owner_removed": "{0} a supprimé vos droits de propriétaire de <b>{1}</b>",
"owner_removedPending": "{0} a annulé l'offre de co-propriété reçue pour <b>{1}</b>",
"padNotPinnedVariable": "Ce pad va expirer après {4} jours d'inactivité, {0}connectez-vous{1} ou {2}enregistrez-vous{3} pour le préserver.",
"share_linkTeam": "Partager avec une équipe",
"share_linkTeam": "Ajouter au CryptDrive d'une équipe",
"team_pickFriends": "Choisissez les amis à inviter dans cette équipe",
"team_inviteModalButton": "Inviter",
"team_noFriend": "Vous n'avez pas encore ajouté d'ami sur CryptPad.",
@ -1242,5 +1246,10 @@
"teams_table_owners": "Gérer l'équipe",
"teams_table_role": "Rôle",
"share_contactCategory": "Contacts",
"pad_wordCount": "Mots : {0}"
"pad_wordCount": "Mots : {0}",
"share_linkWarning": "Ce lien contient les clés de votre document. Le destinataire aura un accès irrévocable à votre contenu.",
"share_linkPasswordAlert": "Cet élément est protégé par un mot de passe. Quand vous partagez le lien, le destinataire doit saisir le mot de passe.",
"share_contactPasswordAlert": "Cet élément est protégé par un mot de passe. Quand vous le partagez avec un contact CryptPad, le destinataire n'a pas a saisir le mot de passe.",
"share_embedPasswordAlert": "Cet élément est protégé par un mot de passe. Quand vous l'intégrez à une page, les visiteurs doivent saisir le mot de passe.",
"passwordFaqLink": "En lire plus sur les mots de passe"
}

@ -779,6 +779,10 @@
"crypto": {
"q": "What cryptography do you use?",
"a": "CryptPad is based upon two open-source cryptography libraries: <a href='https://github.com/dchest/tweetnacl-js' target='_blank'>tweetnacl.js</a> and <a href='https://github.com/dchest/scrypt-async-js' target='_blank'>scrypt-async.js</a>.<br><br>Scrypt is a <em>password-based key derivation algorithm</em>. We use it to turn your username and password into a unique keyring which secures access to your CryptDrive such that only you can access your list of pads.<br><br>We use the <em>xsalsa20-poly1305</em> and <em>x25519-xsalsa20-poly1305</em> cyphers provided by tweetnacl to encrypt pads and chat history, respectively."
},
"pad_password": {
"q": "What happens when I protect a pad/folder with a password?",
"a": "You can protect any pad or shared folder with a password when you create it. You can also use the properties menu to set/change/remove a password at any time.<br><br>Pad and shared folder passwords are intended to protect the link when you share it over potentially insecure channels such as email or text message. If someone intercepts your link but does not have the password they will not be able to read your document.<br><br>When sharing within CryptPad with your contacts or teams, communications are encrypted and we assume you want them to access your document. Therefore the password is remembered and sent with the pad when you share it. The recipient, or yourself, are <b>not</b> asked for it when they open the document."
}
},
"usability": {
@ -1089,7 +1093,7 @@
"share_selectAll": "Select all",
"share_deselectAll": "Deselect all",
"share_filterFriend": "Search by name",
"share_linkFriends": "Share with friends",
"share_linkFriends": "Share with contacts",
"share_withFriends": "Share",
"notifications_dismiss": "Dismiss",
"fm_info_sharedFolderHistory": "This is only the history of your shared folder: <b>{0}</b><br/>Your CryptDrive will stay in read-only mode while you navigate.",
@ -1164,7 +1168,7 @@
"owner_request_declined": "{0} has declined your offer to be an owner of <b>{1}</b>",
"owner_removed": "{0} has removed your ownership of <b>{1}</b>",
"owner_removedPending": "{0} has canceled your ownership offer for <b>{1}</b>",
"share_linkTeam": "Share with a team",
"share_linkTeam": "Add to team drive",
"team_pickFriends": "Choose which friends to invite to this team",
"team_inviteModalButton": "Invite",
"team_noFriend": "You haven't connected with any friends on CryptPad yet.",
@ -1242,5 +1246,10 @@
"teams_table_admins": "Manage members",
"teams_table_owners": "Manage team",
"teams_table_role": "Role",
"pad_wordCount": "Words: {0}"
"pad_wordCount": "Words: {0}",
"share_linkWarning": "This link contains the keys to your document. Recipients will gain non-revokable access to your content.",
"share_linkPasswordAlert": "This item is password protected. When you send the link the recipient will have to enter the password.",
"share_contactPasswordAlert": "This item is password protected. Because you are sharing it with a CryptPad contact, the recipient will not have to enter the password.",
"share_embedPasswordAlert": "This item is password protected. When you embed this pad, viewers will be asked for the password.",
"passwordFaqLink": "Read more about passwords"
}

@ -54,6 +54,7 @@
.kanban-item-text {
cursor: text;
overflow-wrap: anywhere;
flex: 1;
}
}
@ -67,7 +68,7 @@
margin-right: 10px;
min-width: 0;
overflow: hidden;
white-space: nowrap;
//white-space: nowrap;
text-overflow: ellipsis;
}
#kanban-edit {

@ -286,7 +286,7 @@ define([
kanban.inEditMode = true;
// create a form to enter element
var boardId = $(el.parentNode.parentNode).attr("data-id");
var $item = $('<div>', {'class': 'kanban-item'});
var $item = $('<div>', {'class': 'kanban-item new-item'});
var $input = getInput().val(name).appendTo($item);
kanban.addForm(boardId, $item[0]);
$input.focus();

@ -147,9 +147,13 @@
self.drake = self.dragula(self.boardContainer, {
moves: function (el, source, handle, sibling) {
if (self.options.readOnly) { return false; }
if (el.classList.contains('new-item')) { return false; }
return handle.classList.contains('kanban-item');
},
accepts: function (el, target, source, sibling) {
if (sibling === null) {
return false;
}
if (self.options.readOnly) { return false; }
return true;
},
@ -349,7 +353,7 @@
titleBoard = document.createElement('div');
titleBoard.classList.add('kanban-title-board');
titleBoard.innerHTML = board.title;
titleBoard.setAttribute('title', board.title);
//titleBoard.setAttribute('title', board.title);
titleBoard.clickfn = board.boardTitleClick;
__onboardTitleClickHandler(titleBoard);
headerBoard.appendChild(titleBoard);

@ -51,6 +51,7 @@ define([
'cp-settings-info-block',
'cp-settings-displayname',
'cp-settings-language-selector',
'cp-settings-resettips',
'cp-settings-logout-everywhere',
'cp-settings-autostore',
'cp-settings-userfeedback',
@ -67,7 +68,6 @@ define([
],
'drive': [
'cp-settings-drive-duplicate',
'cp-settings-resettips',
'cp-settings-thumbnails',
'cp-settings-drive-backup',
'cp-settings-drive-import-local',
@ -835,7 +835,7 @@ define([
var localStore = window.cryptpadStore;
$button.click(function () {
Object.keys(localStore.store).forEach(function (k) {
if(k.slice(0, 9) === "hide-info") {
if(/^(hide-(info|alert))/.test(k)) {
localStore.put(k, null);
}
});

Loading…
Cancel
Save