diff --git a/customize.dist/loading.js b/customize.dist/loading.js
index 11a82f770..600f3e8d4 100644
--- a/customize.dist/loading.js
+++ b/customize.dist/loading.js
@@ -118,6 +118,16 @@ define([], function () {
#cp-loading-password-prompt .cp-password-form button:hover {
background-color: #326599;
}
+#cp-loading-password-prompt ::placeholder {
+ color: #d9d9d9;
+ opacity: 1;
+}
+#cp-loading-password-prompt :-ms-input-placeholder {
+ color: #d9d9d9;
+}
+#cp-loading-password-prompt ::-ms-input-placeholder {
+ color: #d9d9d9;
+}
#cp-loading .cp-loading-spinner-container {
position: relative;
height: 100px;
diff --git a/customize.dist/src/less2/include/alertify.less b/customize.dist/src/less2/include/alertify.less
index bbc87f9c7..be4def5d7 100644
--- a/customize.dist/src/less2/include/alertify.less
+++ b/customize.dist/src/less2/include/alertify.less
@@ -116,7 +116,7 @@
}*/
}
- .dialog, .alert {
+ .dialog {
& > div {
background-color: @alertify-dialog-bg;
&.half {
@@ -205,6 +205,16 @@
}
}
+ ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
+ color: darken(@alertify-input-fg, 15%);
+ opacity: 1; /* Firefox */
+ }
+ :-ms-input-placeholder { /* Internet Explorer 10-11 */
+ color: darken(@alertify-input-fg, 15%);
+ }
+ ::-ms-input-placeholder { /* Microsoft Edge */
+ color: darken(@alertify-input-fg, 15%);
+ }
input:not(.form-control), textarea {
background-color: @alertify-input-bg;
color: @alertify-input-fg;
diff --git a/www/admin/app-admin.less b/www/admin/app-admin.less
index a1c1ee779..10e178308 100644
--- a/www/admin/app-admin.less
+++ b/www/admin/app-admin.less
@@ -18,5 +18,10 @@
display: flex;
flex-flow: column;
+
+ .cp-support-container {
+ display: flex;
+ flex-flow: column;
+ }
}
diff --git a/www/admin/inner.js b/www/admin/inner.js
index 4c335dd12..7478442c5 100644
--- a/www/admin/inner.js
+++ b/www/admin/inner.js
@@ -170,10 +170,36 @@ define([
var supportKey = ApiConfig.supportMailbox;
create['support-list'] = function () {
if (!supportKey || !APP.privateKey) { return; }
- var $div = makeBlock('support-list');
- $div.addClass('cp-support-container');
+ var $container = makeBlock('support-list');
+ var $div = $(h('div.cp-support-container')).appendTo($container);
var hashesById = {};
+ var reorder = function () {
+ var order = Object.keys(hashesById);
+ order.sort(function (id1, id2) {
+ var t1 = hashesById[id1];
+ var t2 = hashesById[id2];
+ if (!Array.isArray(t1)) { return 1; }
+ if (!Array.isArray(t2)) { return -1; }
+ var lastMsg1 = t1[t1.length - 1];
+ var lastMsg2 = t2[t2.length - 1];
+ var time1 = Util.find(lastMsg1, ['content', 'msg', 'content', 'time']);
+ var time2 = Util.find(lastMsg2, ['content', 'msg', 'content', 'time']);
+ var authorEd1 = Util.find(lastMsg1, ['content', 'msg', 'content', 'sender', 'edPublic']);
+ var authorEd2 = Util.find(lastMsg2, ['content', 'msg', 'content', 'sender', 'edPublic']);
+ var admin1 = ApiConfig.adminKeys.indexOf(authorEd1) !== -1;
+ var admin2 = ApiConfig.adminKeys.indexOf(authorEd2) !== -1;
+ // If one is answered and not the other, put the unanswered first
+ if (admin1 && !admin2) { return 1; }
+ if (!admin1 && admin2) { return -1; }
+ // Otherwise, sort them by time
+ return time2 - time1;
+ });
+ order.forEach(function (id, i) {
+ $div.find('[data-id="'+id+'"]').css('order', i);
+ });
+ };
+
// Register to the "support" mailbox
common.mailbox.subscribe(['supportadmin'], {
onMessage: function (data) {
@@ -219,11 +245,13 @@ define([
});
}
$ticket.append(APP.support.makeMessage(content, hash));
+ reorder();
}
});
- return $div;
+ return $container;
};
+
var checkAdminKey = function (priv) {
if (!supportKey) { return; }
return Hash.checkBoxKeyPair(priv, supportKey);
diff --git a/www/common/application_config_internal.js b/www/common/application_config_internal.js
index ce71d3927..70a2854f1 100644
--- a/www/common/application_config_internal.js
+++ b/www/common/application_config_internal.js
@@ -147,16 +147,24 @@ define(function() {
// Workers allow us to run the websockets connection and open the user drive in a separate thread.
// SharedWorkers allow us to load only one websocket and one user drive for all the browser tabs,
// making it much faster to open new tabs.
- // Warning: This is an experimental feature. It will be enabled by default once we're sure it's stable.
config.disableWorkers = false;
- // Shared folder are in a beta-test state. They are likely to disappear from a user's drive
- // spontaneously, resulting in the deletion of the entire folder's content.
- // We highly recommend to keep them disabled until they are stable enough to be enabled
- // by default by the CryptPad developers.
- config.disableSharedFolders = false;
-
config.surveyURL = "https://survey.cryptpad.fr/index.php/672782";
+ // Teams are always loaded during the initial loading screen (for the first tab only if
+ // SharedWorkers are available). Allowing users to be members of multiple teams can
+ // make them have a very slow loading time. To avoid impacting the user experience
+ // significantly, we're limiting the number of teams per user to 3 by default.
+ // You can change this value here.
+ //config.maxTeamsSlots = 3;
+
+ // Each team is considered as a registered user by the server. Users and teams are indistinguishable
+ // in the database so teams will offer the same storage limits as users by default.
+ // It means that each team created by a user can increase their storage limit by +100%.
+ // We're limiting the number of teams each user is able to own to 1 in order to make sure
+ // users don't use "fake" teams (1 member) just to increase their storage limit.
+ // You can change the value here.
+ // config.maxTeamsOwned = 1;
+
return config;
});
diff --git a/www/common/common-constants.js b/www/common/common-constants.js
index f34241b0a..83e8aae71 100644
--- a/www/common/common-constants.js
+++ b/www/common/common-constants.js
@@ -1,4 +1,4 @@
-define(function () {
+define(['/customize/application_config.js'], function (AppConfig) {
return {
// localStorage
userHashKey: 'User_hash',
@@ -16,7 +16,8 @@ define(function () {
tokenKey: 'loginToken',
displayPadCreationScreen: 'displayPadCreationScreen',
deprecatedKey: 'deprecated',
- MAX_TEAMS_SLOTS: 3,
+ MAX_TEAMS_SLOTS: AppConfig.maxTeamsSlots || 3,
+ MAX_TEAMS_OWNED: AppConfig.maxTeamsOwned || 1,
// Sub
plan: 'CryptPad_plan',
// Apps
diff --git a/www/common/common-ui-elements.js b/www/common/common-ui-elements.js
index 41bcfd5f6..207521574 100644
--- a/www/common/common-ui-elements.js
+++ b/www/common/common-ui-elements.js
@@ -548,6 +548,7 @@ define([
var sframeChan = common.getSframeChannel();
var changePwTitle = Messages.properties_changePassword;
var changePwConfirm = Messages.properties_confirmChange;
+ var isSharedFolder = parsed.type === 'drive';
if (!hasPassword) {
changePwTitle = Messages.properties_addPassword;
changePwConfirm = Messages.properties_confirmNew;
@@ -577,6 +578,7 @@ define([
password: newPass
}, function (err, data) {
if (err || data.error) {
+ console.error(err || data.error);
return void UI.alert(Messages.properties_passwordError);
}
UI.findOKButton().click();
@@ -589,7 +591,9 @@ define([
}, {force: true});
}
return void UI.alert(Messages.properties_passwordSuccess, function () {
- common.gotoURL(hasPassword && newPass ? undefined : (data.href || data.roHref));
+ if (!isSharedFolder) {
+ common.gotoURL(hasPassword && newPass ? undefined : (data.href || data.roHref));
+ }
}, {force: true});
});
});
@@ -2564,7 +2568,7 @@ define([
'target': '_blank',
'rel': 'noopener',
'href': AppConfig.surveyURL,
- 'class': 'fa fa-graduation-cap'
+ 'class': 'cp-toolbar-survey fa fa-graduation-cap'
},
content: h('span', Messages.survey)
});
@@ -2679,6 +2683,9 @@ define([
window.parent.location = origin+'/admin/';
}
});
+ $userAdmin.find('a.cp-toolbar-survey').click(function () {
+ Feedback.send('SURVEY_CLICKED');
+ });
$userAdmin.find('a.cp-toolbar-menu-profile').click(function () {
if (padType) {
window.open(origin+'/profile/');
diff --git a/www/common/cryptpad-common.js b/www/common/cryptpad-common.js
index bea457833..622d427ac 100644
--- a/www/common/cryptpad-common.js
+++ b/www/common/cryptpad-common.js
@@ -869,13 +869,15 @@ define([
}
var newHref = '/' + parsed.type + '/#' + newHash;
+ var isSharedFolder = parsed.type === 'drive';
+
var optsGet = {};
var optsPut = {
password: newPassword,
- metadata: {}
+ metadata: {},
+ initialState: isSharedFolder ? '{}' : undefined
};
-
Nthen(function (waitFor) {
if (parsed.hashData && parsed.hashData.password) {
common.getPadAttribute('password', waitFor(function (err, password) {
@@ -935,7 +937,9 @@ define([
}
var expire = oldMetadata.expire;
- optsPut.metadata.expire = (expire - (+new Date())) / 1000; // Lifetime in seconds
+ if (expire) {
+ optsPut.metadata.expire = (expire - (+new Date())) / 1000; // Lifetime in seconds
+ }
}).nThen(function (waitFor) {
Crypt.get(parsed.hash, waitFor(function (err, val) {
if (err) {
@@ -950,23 +954,22 @@ define([
}), optsPut);
}), optsGet);
}).nThen(function (waitFor) {
+ if (isSharedFolder) {
+ postMessage("UPDATE_SHARED_FOLDER_PASSWORD", {
+ href: href,
+ oldChannel: oldChannel,
+ password: newPassword
+ }, waitFor(function (obj) {
+ console.error(obj);
+ }));
+ return;
+ }
pad.leavePad({
channel: oldChannel
}, waitFor());
pad.onDisconnectEvent.fire(true);
}).nThen(function (waitFor) {
- common.removeOwnedChannel({
- channel: oldChannel,
- teamId: teamId
- }, waitFor(function (obj) {
- if (obj && obj.error) {
- waitFor.abort();
- return void cb(obj);
- }
- }));
- common.unpinPads([oldChannel], waitFor(), teamId);
- common.pinPads([newSecret.channel], waitFor(), teamId);
- }).nThen(function (waitFor) {
+ // Set the new password to our pad data
common.setPadAttribute('password', newPassword, waitFor(function (err) {
if (err) { warning = true; }
}), href);
@@ -983,6 +986,21 @@ define([
common.setPadAttribute('href', newHref, waitFor(function (err) {
if (err) { warning = true; }
}), href);
+ }).nThen(function (waitFor) {
+ // delete the old pad
+ common.removeOwnedChannel({
+ channel: oldChannel,
+ teamId: teamId
+ }, waitFor(function (obj) {
+ if (obj && obj.error) {
+ waitFor.abort();
+ return void cb(obj);
+ }
+ }));
+ if (!isSharedFolder) {
+ common.unpinPads([oldChannel], waitFor(), teamId);
+ common.pinPads([newSecret.channel], waitFor(), teamId);
+ }
}).nThen(function () {
cb({
warning: warning,
diff --git a/www/common/drive-ui.js b/www/common/drive-ui.js
index 7e08a1a72..be488e47e 100644
--- a/www/common/drive-ui.js
+++ b/www/common/drive-ui.js
@@ -3799,7 +3799,7 @@ define([
if (manager.isSharedFolder(el)) {
delete data.roHref;
//data.noPassword = true;
- data.noEditPassword = true;
+ //data.noEditPassword = true;
data.noExpiration = true;
// this is here to allow users to check the channel id of a shared folder
// we should remove it at some point
@@ -4443,6 +4443,7 @@ define([
refresh();
UI.removeLoadingScreen();
+ /*
if (!APP.team) {
sframeChan.query('Q_DRIVE_GETDELETED', null, function (err, data) {
var ids = manager.findChannels(data);
@@ -4457,6 +4458,79 @@ define([
UI.log(Messages._getKey('fm_deletedPads', [titles.join(', ')]));
});
}
+ */
+ var deprecated = files.sharedFoldersTemp;
+ var nt = nThen;
+ var passwordModal = function (fId, data, cb) {
+ var content = [];
+ var folderName = ''+ (data.lastTitle || Messages.fm_newFolder) +'';
+ content.push(UI.setHTML(h('p'), Messages._getKey('drive_sfPassword', [folderName])));
+ var newPassword = UI.passwordInput({
+ id: 'cp-app-prop-change-password',
+ placeholder: Messages.settings_changePasswordNew,
+ style: 'flex: 1;'
+ });
+ var passwordOk = h('button', Messages.properties_changePasswordButton);
+ var changePass = h('span.cp-password-container', [
+ newPassword,
+ passwordOk
+ ]);
+ content.push(changePass);
+ var div = h('div', content);
+
+ var locked = false;
+ $(passwordOk).click(function () {
+ if (locked) { return; }
+ var pw = $(newPassword).find('.cp-password-input').val();
+ locked = true;
+ $(div).find('.alert').remove();
+ $(passwordOk).html('').append(h('span.fa.fa-spinner.fa-spin', {style: 'margin-left: 0'}));
+ manager.restoreSharedFolder(fId, pw, function (err, obj) {
+ if (obj && obj.error) {
+ var wrong = h('div.alert.alert-danger', Messages.drive_sfPasswordError);
+ $(div).prepend(wrong);
+ $(passwordOk).text(Messages.properties_changePasswordButton);
+ locked = false;
+ return;
+ }
+ UI.findCancelButton($(div).closest('.alertify')).click();
+ cb();
+ });
+ });
+ var buttons = [{
+ className: 'primary',
+ name: Messages.forgetButton,
+ onClick: function () {
+ manager.delete([['sharedFoldersTemp', fId]], function () { });
+ },
+ keys: []
+ }, {
+ className: 'cancel',
+ name: Messages.later,
+ onClick: function () {},
+ keys: [27]
+ }];
+ return UI.dialog.customModal(div, {
+ buttons: buttons,
+ onClose: cb
+ });
+ };
+ if (typeof (deprecated) === "object") {
+ Object.keys(deprecated).forEach(function (fId) {
+ var data = deprecated[fId];
+ var sfId = manager.user.userObject.getSFIdFromHref(data.href);
+ if (folders[fId] || sfId) { // This shared folder is already stored in the drive...
+ return void manager.delete([['sharedFoldersTemp', fId]], function () { });
+ }
+ nt = nt(function (waitFor) {
+ UI.openCustomModal(passwordModal(fId, data, waitFor()));
+ }).nThen;
+ });
+ nt(function () {
+ refresh();
+ });
+ }
+
return {
refresh: refresh,
diff --git a/www/common/media-tag.js b/www/common/media-tag.js
index 9bdaddc25..a2beb3b4a 100644
--- a/www/common/media-tag.js
+++ b/www/common/media-tag.js
@@ -435,9 +435,14 @@
return mediaObject;
}
+ mediaObject.tag.innerHTML = '';
+
// Download the encrypted blob
download(src, function (err, u8Encrypted) {
if (err) {
+ if (err === "XHR_ERROR 404") {
+ mediaObject.tag.innerHTML = '
';
+ }
return void emit('error', err);
}
// Decrypt the blob
diff --git a/www/common/notifications.js b/www/common/notifications.js
index 1222e8602..ed44c2917 100644
--- a/www/common/notifications.js
+++ b/www/common/notifications.js
@@ -95,9 +95,7 @@ define([
if (msg.content.isTemplate) {
common.sessionStorage.put(Constants.newPadPathKey, ['template'], waitFor());
}
- if (msg.content.password) {
- common.sessionStorage.put('newPadPassword', msg.content.password, waitFor());
- }
+ common.sessionStorage.put('newPadPassword', msg.content.password || '', waitFor());
}).nThen(function () {
todo();
});
diff --git a/www/common/outer/async-store.js b/www/common/outer/async-store.js
index ee890579e..98806cee9 100644
--- a/www/common/outer/async-store.js
+++ b/www/common/outer/async-store.js
@@ -453,7 +453,7 @@ define([
Store.isNewChannel = function (clientId, data, cb) {
if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); }
- var channelId = Hash.hrefToHexChannelId(data.href, data.password);
+ var channelId = data.channel || Hash.hrefToHexChannelId(data.href, data.password);
store.anon_rpc.send("IS_NEW_CHANNEL", channelId, function (e, response) {
if (e) { return void cb({error: e}); }
if (response && response.length && typeof(response[0]) === 'boolean') {
@@ -1773,21 +1773,24 @@ define([
}
};
};
- Store.loadSharedFolder = function (teamId, id, data, cb) {
+ Store.loadSharedFolder = function (teamId, id, data, cb, isNew) {
var s = getStore(teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
- var rt = SF.load({
+ SF.load({
+ isNew: isNew,
network: store.network,
- store: s
+ store: s,
+ isNewChannel: Store.isNewChannel
}, id, data, cb);
- return rt;
};
- var loadSharedFolder = function (id, data, cb) {
- Store.loadSharedFolder(null, id, data, cb);
+ var loadSharedFolder = function (id, data, cb, isNew) {
+ Store.loadSharedFolder(null, id, data, cb, isNew);
};
Store.loadSharedFolderAnon = function (clientId, data, cb) {
- Store.loadSharedFolder(null, data.id, data.data, function () {
- cb();
+ Store.loadSharedFolder(null, data.id, data.data, function (rt) {
+ cb({
+ error: rt ? undefined : 'EDELETED'
+ });
});
};
Store.addSharedFolder = function (clientId, data, cb) {
@@ -1800,6 +1803,9 @@ define([
cb(id);
});
};
+ Store.updateSharedFolderPassword = function (clientId, data, cb) {
+ SF.updatePassword(Store, data, store.network, cb);
+ };
// Drive
Store.userObjectCommand = function (clientId, cmdData, cb) {
@@ -1884,6 +1890,27 @@ define([
});
};
registerProxyEvents = function (proxy, fId) {
+ if (!fId) {
+ // Listen for shared folder password change
+ proxy.on('change', ['drive', UserObject.SHARED_FOLDERS], function (o, n, p) {
+ if (p.length > 3 && p[3] === 'password') {
+ var id = p[2];
+ var data = proxy.drive[UserObject.SHARED_FOLDERS][id];
+ var href = store.manager.user.userObject.getHref ?
+ store.manager.user.userObject.getHref(data) : data.href;
+ var parsed = Hash.parsePadUrl(href);
+ var secret = Hash.getSecrets(parsed.type, parsed.hash, o);
+ SF.updatePassword({
+ oldChannel: secret.channel,
+ password: n,
+ href: href
+ }, store.network, function () {
+ console.log('Shared folder password changed');
+ });
+ return false;
+ }
+ });
+ }
proxy.on('change', [], function (o, n, p) {
if (fId) {
// Pin the new pads
@@ -2035,6 +2062,7 @@ define([
unpin: unpin,
loadSharedFolder: loadSharedFolder,
settings: proxy.settings,
+ Store: Store
}, {
outer: true,
removeOwnedChannel: function (channel, cb) { Store.removeOwnedChannel('', channel, cb); },
@@ -2117,6 +2145,7 @@ define([
proxy.settings.general.allowUserFeedback = true;
}
returned.feedback = proxy.settings.general.allowUserFeedback;
+ Feedback.init(returned.feedback);
if (typeof(cb) === 'function') { cb(returned); }
diff --git a/www/common/outer/sharedfolder.js b/www/common/outer/sharedfolder.js
index d4883bd10..ec23c0f76 100644
--- a/www/common/outer/sharedfolder.js
+++ b/www/common/outer/sharedfolder.js
@@ -67,7 +67,9 @@ define([
var cb = Util.once(_cb);
var network = config.network;
var store = config.store;
- var teamId = store.id || -1;
+ var isNew = config.isNew;
+ var isNewChannel = config.isNewChannel;
+ var teamId = store.id;
var handler = store.handleSharedFolder;
var href = store.manager.user.userObject.getHref(data);
@@ -76,85 +78,109 @@ define([
var secret = Hash.getSecrets('drive', parsed.hash, data.password);
var secondaryKey = secret.keys.secondaryKey;
- var sf = allSharedFolders[secret.channel];
- if (sf && sf.readOnly && secondaryKey) {
- // We were in readOnly mode and now we know the edit keys!
- SF.upgrade(secret.channel, secret);
- }
- if (sf && sf.ready && sf.rt) {
- // The shared folder is already loaded, return its data
- setTimeout(function () {
- var leave = function () { SF.leave(secret.channel, teamId); };
- var uo = store.manager.addProxy(id, sf.rt, leave, secondaryKey);
- SF.checkMigration(secondaryKey, sf.rt.proxy, uo, function () {
- cb(sf.rt, sf.metadata);
- });
- });
- sf.team.push(teamId);
- if (handler) { handler(id, sf.rt); }
- return sf.rt;
- }
- if (sf && sf.queue && sf.rt) {
- // The shared folder is loading, add our callbacks to the queue
- sf.queue.push({
- cb: cb,
- store: store,
- id: id
- });
- sf.team.push(teamId);
- if (handler) { handler(id, sf.rt); }
- return sf.rt;
- }
-
- sf = allSharedFolders[secret.channel] = {
- queue: [{
- cb: cb,
- store: store,
- id: id
- }],
- team: [store.id || -1],
- readOnly: Boolean(secondaryKey)
- };
-
- var owners = data.owners;
- var listmapConfig = {
- data: {},
- channel: secret.channel,
- readOnly: Boolean(secondaryKey),
- crypto: Crypto.createEncryptor(secret.keys),
- userName: 'sharedFolder',
- logLevel: 1,
- ChainPad: ChainPad,
- classic: true,
- network: network,
- metadata: {
- validateKey: secret.keys.validateKey || undefined,
- owners: owners
+ // If we try to load an existing shared folder (isNew === false) but this folder
+ // doesn't exist in the database, abort and cb
+ nThen(function (waitFor) {
+ isNewChannel(null, { channel: secret.channel }, waitFor(function (obj) {
+ if (obj.isNew && !isNew) {
+ store.manager.deprecateProxy(id, secret.channel);
+ waitFor.abort();
+ return void cb(null);
+ }
+ }));
+ }).nThen(function () {
+ var sf = allSharedFolders[secret.channel];
+ if (sf && sf.readOnly && secondaryKey) {
+ // We were in readOnly mode and now we know the edit keys!
+ SF.upgrade(secret.channel, secret);
}
- };
- var rt = sf.rt = Listmap.create(listmapConfig);
- rt.proxy.on('ready', function (info) {
- if (!Object.keys(rt.proxy).length) {
- // New Shared folder: no migration required
- rt.proxy.version = 2;
+ if (sf && sf.ready && sf.rt) {
+ // The shared folder is already loaded, return its data
+ setTimeout(function () {
+ var leave = function () { SF.leave(secret.channel, teamId); };
+ var uo = store.manager.addProxy(id, sf.rt, leave, secondaryKey);
+ SF.checkMigration(secondaryKey, sf.rt.proxy, uo, function () {
+ cb(sf.rt, sf.metadata);
+ });
+ });
+ sf.teams.push(store);
+ if (handler) { handler(id, sf.rt); }
+ return sf.rt;
}
- if (!sf.queue) {
- return;
+ if (sf && sf.queue && sf.rt) {
+ // The shared folder is loading, add our callbacks to the queue
+ sf.queue.push({
+ cb: cb,
+ store: store,
+ id: id
+ });
+ sf.teams.push(store);
+ if (handler) { handler(id, sf.rt); }
+ return sf.rt;
}
- sf.leave = info.leave;
- sf.metadata = info.metadata;
- sf.queue.forEach(function (obj) {
- var leave = function () { SF.leave(secret.channel, teamId); };
- var uo = obj.store.manager.addProxy(obj.id, rt, leave, secondaryKey);
- SF.checkMigration(secondaryKey, rt.proxy, uo, function () {
- obj.cb(sf.rt, sf.metadata);
+
+ sf = allSharedFolders[secret.channel] = {
+ queue: [{
+ cb: cb,
+ store: store,
+ id: id
+ }],
+ teams: [store],
+ readOnly: Boolean(secondaryKey)
+ };
+
+ var owners = data.owners;
+ var listmapConfig = {
+ data: {},
+ channel: secret.channel,
+ readOnly: Boolean(secondaryKey),
+ crypto: Crypto.createEncryptor(secret.keys),
+ userName: 'sharedFolder',
+ logLevel: 1,
+ ChainPad: ChainPad,
+ classic: true,
+ network: network,
+ metadata: {
+ validateKey: secret.keys.validateKey || undefined,
+ owners: owners
+ }
+ };
+ var rt = sf.rt = Listmap.create(listmapConfig);
+ rt.proxy.on('ready', function (info) {
+ if (isNew && !Object.keys(rt.proxy).length) {
+ // New Shared folder: no migration required
+ rt.proxy.version = 2;
+ }
+ if (!sf.queue) {
+ return;
+ }
+ sf.queue.forEach(function (obj) {
+ var leave = function () { SF.leave(secret.channel, teamId); };
+ var uo = obj.store.manager.addProxy(obj.id, rt, leave, secondaryKey);
+ SF.checkMigration(secondaryKey, rt.proxy, uo, function () {
+ obj.cb(sf.rt, info.metadata);
+ });
});
+ sf.metadata = info.metadata;
+ sf.ready = true;
+ delete sf.queue;
+ });
+ rt.proxy.on('error', function (info) {
+ if (info && info.error) {
+ if (info.error === "EDELETED" ) {
+ try {
+ // Deprecate the shared folder from each team
+ sf.teams.forEach(function (store) {
+ store.manager.deprecateProxy(id, secret.channel);
+ });
+ } catch (e) {}
+ delete allSharedFolders[secret.channel];
+ }
+ }
});
- sf.ready = true;
- delete sf.queue;
+
+ if (handler) { handler(id, rt); }
});
- if (handler) { handler(id, rt); }
- return rt;
};
SF.upgrade = function (channel, secret) {
@@ -173,8 +199,14 @@ define([
if (!sf) { return; }
var clients = sf.teams;
if (!Array.isArray(clients)) { return; }
- var idx = clients.indexOf(teamId);
- if (idx === -1) { return; }
+ var idx;
+ clients.some(function (store, i) {
+ if (store.id === teamId) {
+ idx = i;
+ return true;
+ }
+ });
+ if (typeof (idx) === "undefined") { return; }
// Remove the selected team
clients.splice(idx, 1);
@@ -185,6 +217,38 @@ define([
}
};
+ SF.updatePassword = function (Store, data, network, cb) {
+ var oldChannel = data.oldChannel;
+ var href = data.href;
+ var password = data.password;
+ var parsed = Hash.parsePadUrl(href);
+ var secret = Hash.getSecrets(parsed.type, parsed.hash, password);
+ var sf = allSharedFolders[oldChannel];
+ if (!sf) { return void cb({ error: 'ENOTFOUND' }); }
+ if (sf.rt && sf.rt.stop) {
+ sf.rt.stop();
+ }
+ var nt = nThen;
+ sf.teams.forEach(function (s) {
+ nt = nt(function (waitFor) {
+ var sfId = s.manager.user.userObject.getSFIdFromHref(href);
+ var shared = Util.find(s.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {};
+ if (!sfId || !shared[sfId]) { return; }
+ var sf = JSON.parse(JSON.stringify(shared[sfId]));
+ sf.password = password;
+ SF.load({
+ network: network,
+ store: s,
+ isNewChannel: Store.isNewChannel
+ }, sfId, sf, waitFor());
+ if (!s.rpc) { return; }
+ s.rpc.unpin([oldChannel], waitFor());
+ s.rpc.pin([secret.channel], waitFor());
+ }).nThen;
+ });
+ nt(cb);
+ };
+
/* loadSharedFolders
load all shared folder stored in a given drive
- store: user or team main store
@@ -193,35 +257,13 @@ define([
*/
SF.loadSharedFolders = function (Store, network, store, userObject, waitFor) {
var shared = Util.find(store.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {};
- // Check if any of our shared folder is expired or deleted by its owner.
- // If we don't check now, Listmap will create an empty proxy if it no longer exists on
- // the server.
nThen(function (waitFor) {
- var checkExpired = Object.keys(shared).map(function (fId) {
- return shared[fId].channel;
- });
- Store.getDeletedPads(null, {list: checkExpired}, waitFor(function (chans) {
- if (chans && chans.error) { return void console.error(chans.error); }
- if (!Array.isArray(chans) || !chans.length) { return; }
- var toDelete = [];
- Object.keys(shared).forEach(function (fId) {
- if (chans.indexOf(shared[fId].channel) !== -1
- && toDelete.indexOf(fId) === -1) {
- toDelete.push(fId);
- }
- });
- toDelete.forEach(function (fId) {
- var paths = userObject.findFile(Number(fId));
- userObject.delete(paths, waitFor(), true);
- delete shared[fId];
- });
- }));
- }).nThen(function (waitFor) {
Object.keys(shared).forEach(function (id) {
var sf = shared[id];
SF.load({
network: network,
- store: store
+ store: store,
+ isNewChannel: Store.isNewChannel
}, id, sf, waitFor());
});
}).nThen(waitFor());
diff --git a/www/common/outer/store-rpc.js b/www/common/outer/store-rpc.js
index c75e69b70..9accc5f29 100644
--- a/www/common/outer/store-rpc.js
+++ b/www/common/outer/store-rpc.js
@@ -56,6 +56,7 @@ define([
ADD_SHARED_FOLDER: Store.addSharedFolder,
LOAD_SHARED_FOLDER: Store.loadSharedFolderAnon,
RESTORE_SHARED_FOLDER: Store.restoreSharedFolder,
+ UPDATE_SHARED_FOLDER_PASSWORD: Store.updateSharedFolderPassword,
// Messaging
ANSWER_FRIEND_REQUEST: Store.answerFriendRequest,
SEND_FRIEND_REQUEST: Store.sendFriendRequest,
diff --git a/www/common/outer/team.js b/www/common/outer/team.js
index 59b56c47e..bc071cd07 100644
--- a/www/common/outer/team.js
+++ b/www/common/outer/team.js
@@ -9,6 +9,7 @@ define([
'/common/outer/sharedfolder.js',
'/common/outer/roster.js',
'/common/common-messaging.js',
+ '/common/common-feedback.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
@@ -18,7 +19,7 @@ define([
'/bower_components/saferphore/index.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
], function (Util, Hash, Constants, Realtime,
- ProxyManager, UserObject, SF, Roster, Messaging,
+ ProxyManager, UserObject, SF, Roster, Messaging, Feedback,
Listmap, Crypto, CpNetflux, ChainPad, nThen, Saferphore) {
var Team = {};
@@ -30,6 +31,27 @@ define([
var registerChangeEvents = function (ctx, team, proxy, fId) {
if (!team) { return; }
+ if (!fId) {
+ // Listen for shared folder password change
+ proxy.on('change', ['drive', UserObject.SHARED_FOLDERS], function (o, n, p) {
+ if (p.length > 3 && p[3] === 'password') {
+ var id = p[2];
+ var data = proxy.drive[UserObject.SHARED_FOLDERS][id];
+ var href = team.manager.user.userObject.getHref ?
+ team.manager.user.userObject.getHref(data) : data.href;
+ var parsed = Hash.parsePadUrl(href);
+ var secret = Hash.getSecrets(parsed.type, parsed.hash, o);
+ SF.updatePassword(ctx.Store, {
+ oldChannel: secret.channel,
+ password: n,
+ href: href
+ }, ctx.store.network, function () {
+ console.log('Shared folder password changed');
+ });
+ return false;
+ }
+ });
+ }
proxy.on('change', [], function (o, n, p) {
if (fId) {
// Pin the new pads
@@ -216,13 +238,13 @@ define([
}));
}).nThen(function () {
// Create the proxy manager
- var loadSharedFolder = function (id, data, cb) {
+ var loadSharedFolder = function (id, data, cb, isNew) {
SF.load({
+ isNew: isNew,
network: ctx.store.network,
- store: team
- }, id, data, function (id, rt) {
- cb(id, rt);
- });
+ store: team,
+ isNewChannel: ctx.Store.isNewChannel
+ }, id, data, cb);
};
var teamData = ctx.store.proxy.teams[team.id];
var hash = teamData.hash || teamData.roHash;
@@ -236,6 +258,7 @@ define([
settings: {
drive: Util.find(ctx.store, ['proxy', 'settings', 'drive'])
},
+ Store: ctx.Store
}, {
outer: true,
removeOwnedChannel: function (channel, cb) {
@@ -567,6 +590,7 @@ define([
proxy.drive = {};
onReady(ctx, id, lm, roster, keys, cId, function () {
+ Feedback.send('TEAM_CREATION');
ctx.updateMetadata();
cb();
});
@@ -682,6 +706,7 @@ define([
if (err) { console.error(err); }
}));
}).nThen(function () {
+ Feedback.send('TEAM_DELETION');
closeTeam(ctx, teamId);
cb();
});
diff --git a/www/common/outer/userObject.js b/www/common/outer/userObject.js
index 267b654e7..8cdbae7b4 100644
--- a/www/common/outer/userObject.js
+++ b/www/common/outer/userObject.js
@@ -30,6 +30,7 @@ define([
var TRASH = exp.TRASH;
var TEMPLATE = exp.TEMPLATE;
var SHARED_FOLDERS = exp.SHARED_FOLDERS;
+ var SHARED_FOLDERS_TEMP = exp.SHARED_FOLDERS_TEMP;
var debug = exp.debug;
@@ -109,6 +110,15 @@ define([
cb(null, id);
};
+ exp.deprecateSharedFolder = function (id) {
+ var data = files[SHARED_FOLDERS][id];
+ if (!data) { return; }
+ files[SHARED_FOLDERS_TEMP][id] = JSON.parse(JSON.stringify(data));
+ var paths = exp.findFile(Number(id));
+ exp.delete(paths, null, true);
+ delete files[SHARED_FOLDERS][id];
+ };
+
// FILES DATA
var spliceFileData = function (id) {
if (readOnly) { return; }
@@ -868,6 +878,22 @@ define([
}
}
};
+ var fixSharedFoldersTemp = function () {
+ if (sharedFolder) { return; }
+ if (typeof(files[SHARED_FOLDERS_TEMP]) !== "object") {
+ debug("SHARED_FOLDER_TEMP was not an object");
+ files[SHARED_FOLDERS_TEMP] = {};
+ }
+ // Remove deprecated shared folder if they were already added back
+ var sft = files[SHARED_FOLDERS_TEMP];
+ var sf = files[SHARED_FOLDERS];
+ for (var id in sft) {
+ if (sf[id]) {
+ delete sft[id];
+ }
+ }
+ };
+
var fixDrive = function () {
Object.keys(files).forEach(function (key) {
@@ -881,6 +907,7 @@ define([
fixFilesData();
fixDrive();
fixSharedFolders();
+ fixSharedFoldersTemp();
var ms = (+new Date() - t0) + 'ms';
if (JSON.stringify(files) !== before) {
diff --git a/www/common/proxy-manager.js b/www/common/proxy-manager.js
index ead731550..3ce2d59e9 100644
--- a/www/common/proxy-manager.js
+++ b/www/common/proxy-manager.js
@@ -27,6 +27,13 @@ define([
// Only in outer
userObject.fixFiles();
}
+ var proxy = lm.proxy;
+ if (proxy.metadata && proxy.metadata.title) {
+ var sf = Env.user.proxy[UserObject.SHARED_FOLDERS][id];
+ if (sf) {
+ sf.lastTitle = proxy.metadata.title;
+ }
+ }
Env.folders[id] = {
proxy: lm.proxy,
userObject: userObject,
@@ -43,6 +50,12 @@ define([
delete Env.folders[id];
};
+ // Password may have changed
+ var deprecateProxy = function (Env, id, channel) {
+ Env.unpinPads([channel], function () {});
+ Env.user.userObject.deprecateSharedFolder(id);
+ };
+
/*
Tools
*/
@@ -488,6 +501,11 @@ define([
// 2b. load the proxy
Env.loadSharedFolder(id, folderData, waitFor(function (rt, metadata) {
+ if (!rt) {
+ waitFor.abort();
+ return void cb({ error: 'EDELETED' });
+ }
+
if (!rt.proxy.metadata) { // Creating a new shared folder
rt.proxy.metadata = { title: data.name || Messages.fm_newFolder };
}
@@ -497,7 +515,7 @@ define([
if (metadata.owners) { fData.owners = metadata.owners; }
if (metadata.expire) { fData.expire = +metadata.expire; }
}
- }));
+ }), !Boolean(data.folderData));
}).nThen(function () {
Env.onSync(function () {
cb(id);
@@ -505,6 +523,42 @@ define([
});
};
+ var _restoreSharedFolder = function (Env, _data, cb) {
+ var fId = _data.id;
+ var newPassword = _data.password;
+ var temp = Util.find(Env, ['user', 'proxy', UserObject.SHARED_FOLDERS_TEMP]);
+ var data = temp && temp[fId];
+ if (!data) { return void cb({ error: 'EINVAL' }); }
+ if (!Env.Store) { return void cb({ error: 'ESTORE' }); }
+ var href = Env.user.userObject.getHref ? Env.user.userObject.getHref(data) : data.href;
+ var isNew = false;
+ nThen(function (waitFor) {
+ Env.Store.isNewChannel(null, {
+ href: href,
+ password: newPassword
+ }, waitFor(function (obj) {
+ if (!obj || obj.error) {
+ isNew = false;
+ return;
+ }
+ isNew = obj.isNew;
+ }));
+ }).nThen(function () {
+ if (isNew) {
+ return void cb({ error: 'ENOTFOUND' });
+ }
+ data.password = newPassword;
+ _addSharedFolder(Env, {
+ path: ['root'],
+ folderData: data,
+ }, function () {
+ delete temp[fId];
+ Env.onSync(cb);
+ });
+ });
+
+ };
+
// convert a folder to a Shared Folder
var _convertFolderToSharedFolder = function (Env, data, cb) {
return void cb({
@@ -611,6 +665,13 @@ define([
return void cb({error: 'E_NOTFOUND'});
}
+ // Deleted or password changed for a shared folder
+ if (data.paths.length === 1 && data.paths[0][0] === UserObject.SHARED_FOLDERS_TEMP) {
+ var temp = Util.find(Env, ['user', 'proxy', UserObject.SHARED_FOLDERS_TEMP]);
+ delete temp[data.paths[0][1]];
+ return void Env.onSync(cb);
+ }
+
var toUnpin = [];
var ownedRemoved;
nThen(function (waitFor) {
@@ -707,6 +768,7 @@ define([
var el = Env.user.userObject.find(resolved.path);
if (Env.user.userObject.isSharedFolder(el) && Env.folders[el]) {
Env.folders[el].proxy.metadata.title = data.newName;
+ Env.user.proxy[UserObject.SHARED_FOLDERS][el].lastTitle = data.value;
return void cb();
}
}
@@ -740,6 +802,8 @@ define([
_addFolder(Env, data, cb); break;
case 'addSharedFolder':
_addSharedFolder(Env, data, cb); break;
+ case 'restoreSharedFolder':
+ _restoreSharedFolder(Env, data, cb); break;
case 'convertFolderToSharedFolder':
_convertFolderToSharedFolder(Env, data, cb); break;
case 'delete':
@@ -966,6 +1030,7 @@ define([
pinPads: data.pin,
unpinPads: data.unpin,
onSync: data.onSync,
+ Store: data.Store,
loadSharedFolder: data.loadSharedFolder,
cfg: uoConfig,
edPublic: data.edPublic,
@@ -988,6 +1053,7 @@ define([
// Manager
addProxy: callWithEnv(addProxy),
removeProxy: callWithEnv(removeProxy),
+ deprecateProxy: callWithEnv(deprecateProxy),
addSharedFolder: callWithEnv(_addSharedFolder),
// Drive
command: callWithEnv(onCommand),
@@ -1059,6 +1125,15 @@ define([
}
}, cb);
};
+ var restoreSharedFolderInner = function (Env, fId, password, cb) {
+ return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", {
+ cmd: "restoreSharedFolder",
+ data: {
+ id: fId,
+ password: password
+ }
+ }, cb);
+ };
var convertFolderToSharedFolderInner = function (Env, path, owned, password, cb) {
return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "convertFolderToSharedFolder",
@@ -1262,6 +1337,7 @@ define([
emptyTrash: callWithEnv(emptyTrashInner),
addFolder: callWithEnv(addFolderInner),
addSharedFolder: callWithEnv(addSharedFolderInner),
+ restoreSharedFolder: callWithEnv(restoreSharedFolderInner),
convertFolderToSharedFolder: callWithEnv(convertFolderToSharedFolderInner),
delete: callWithEnv(deleteInner),
restore: callWithEnv(restoreInner),
diff --git a/www/common/sframe-common-codemirror.js b/www/common/sframe-common-codemirror.js
index 3f312aa68..d16bea97a 100644
--- a/www/common/sframe-common-codemirror.js
+++ b/www/common/sframe-common-codemirror.js
@@ -379,13 +379,14 @@ define([
};
exp.mkIndentSettings = function (metadataMgr) {
- var setIndentation = function (units, useTabs, fontSize, spellcheck) {
+ var setIndentation = function (units, useTabs, fontSize, spellcheck, brackets) {
if (typeof(units) !== 'number') { return; }
var doc = editor.getDoc();
editor.setOption('indentUnit', units);
editor.setOption('tabSize', units);
editor.setOption('indentWithTabs', useTabs);
editor.setOption('spellcheck', spellcheck);
+ editor.setOption('autoCloseBrackets', brackets);
editor.setOption("extraKeys", {
Tab: function() {
if (doc.somethingSelected()) {
@@ -415,11 +416,13 @@ define([
var useTabs = data[useTabsKey];
var fontSize = data[fontKey];
var spellcheck = data[spellcheckKey];
+ var brackets = data.brackets;
setIndentation(
typeof(indentUnit) === 'number'? indentUnit : 2,
typeof(useTabs) === 'boolean'? useTabs : false,
typeof(fontSize) === 'number' ? fontSize : 12,
- typeof(spellcheck) === 'boolean' ? spellcheck : false);
+ typeof(spellcheck) === 'boolean' ? spellcheck : false,
+ typeof(brackets) === 'boolean' ? brackets : true);
};
metadataMgr.onChangeLazy(updateIndentSettings);
updateIndentSettings();
diff --git a/www/common/sframe-common-outer.js b/www/common/sframe-common-outer.js
index 16210b743..d6a5111be 100644
--- a/www/common/sframe-common-outer.js
+++ b/www/common/sframe-common-outer.js
@@ -118,7 +118,7 @@ define([
msgEv.fire(msg);
});
SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) {
- sframeChan = sfc;
+ Utils.sframeChan = sframeChan = sfc;
}));
});
window.addEventListener('message', whenReady);
@@ -177,75 +177,88 @@ define([
Cryptpad.getShareHashes(secret, waitFor(function (err, h) { hashes = h; }));
};
- // Prompt the password here if we have a hash containing /p/
- // or get it from the pad attributes
- var needPassword = parsed.hashData && parsed.hashData.password;
- if (needPassword) {
- // Check if we have a password, and check if it is correct (file exists).
- // It we don't have a correct password, display the password prompt.
- // Maybe the file has been deleted from the server or the password has been changed.
- Cryptpad.getPadAttribute('password', waitFor(function (err, val) {
- var askPassword = function (wrongPasswordStored) {
- // Ask for the password and check if the pad exists
- // If the pad doesn't exist, it means the password isn't correct
- // or the pad has been deleted
- var correctPassword = waitFor();
- sframeChan.on('Q_PAD_PASSWORD_VALUE', function (data, cb) {
- password = data;
- var next = function (e, isNew) {
- if (Boolean(isNew)) {
- // Ask again in the inner iframe
- // We should receive a new Q_PAD_PASSWORD_VALUE
- cb(false);
+ if (!parsed.hashData) { // No hash, no need to check for a password
+ return void todo();
+ }
+
+ // We now need to check if there is a password and if we know the correct password.
+ // We'll use getFileSize and isNewChannel to detect incorrect passwords.
+
+ // First we'll get the password value from our drive (getPadAttribute), and we'll check
+ // if the channel is valid. If the pad is not stored in our drive, we'll test with an
+ // empty password instead.
+
+ // If this initial check returns a valid channel, open the pad.
+ // If the channel is invalid:
+ // Option 1: this is a password-protected pad not stored in our drive --> password prompt
+ // Option 2: this is a pad stored in our drive
+ // 2a: 'edit' pad or file --> password-prompt
+ // 2b: 'view' pad no '/p/' --> the seed is incorrect
+ // 2c: 'view' pad and '/p/' and a wrong password stored --> the seed is incorrect
+ // 2d: 'view' pad and '/p/' and password never stored (security feature) --> password-prompt
+
+ Cryptpad.getPadAttribute('password', waitFor(function (err, val) {
+ var askPassword = function (wrongPasswordStored) {
+ // Ask for the password and check if the pad exists
+ // If the pad doesn't exist, it means the password isn't correct
+ // or the pad has been deleted
+ var correctPassword = waitFor();
+ sframeChan.on('Q_PAD_PASSWORD_VALUE', function (data, cb) {
+ password = data;
+ var next = function (e, isNew) {
+ if (Boolean(isNew)) {
+ // Ask again in the inner iframe
+ // We should receive a new Q_PAD_PASSWORD_VALUE
+ cb(false);
+ } else {
+ todo();
+ if (wrongPasswordStored) {
+ // Store the correct password
+ nThen(function (w) {
+ Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl());
+ Cryptpad.setPadAttribute('channel', secret.channel, w(), parsed.getUrl());
+ }).nThen(correctPassword);
} else {
- todo();
- if (wrongPasswordStored) {
- // Store the correct password
- Cryptpad.setPadAttribute('password', password, function () {
- correctPassword();
- }, parsed.getUrl());
- } else {
- correctPassword();
- }
- cb(true);
+ correctPassword();
}
- };
- if (parsed.type === "file") {
- // `isNewChannel` doesn't work for files (not a channel)
- // `getFileSize` is not adapted to channels because of metadata
- Cryptpad.getFileSize(window.location.href, password, function (e, size) {
- next(e, size === 0);
- });
- return;
+ cb(true);
}
- // Not a file, so we can use `isNewChannel`
- Cryptpad.isNewChannel(window.location.href, password, next);
- });
- sframeChan.event("EV_PAD_PASSWORD");
- };
-
- if (!val && sessionStorage.newPadPassword) {
- val = sessionStorage.newPadPassword;
- delete sessionStorage.newPadPassword;
- }
+ };
+ if (parsed.type === "file") {
+ // `isNewChannel` doesn't work for files (not a channel)
+ // `getFileSize` is not adapted to channels because of metadata
+ Cryptpad.getFileSize(window.location.href, password, function (e, size) {
+ next(e, size === 0);
+ });
+ return;
+ }
+ // Not a file, so we can use `isNewChannel`
+ Cryptpad.isNewChannel(window.location.href, password, next);
+ });
+ sframeChan.event("EV_PAD_PASSWORD");
+ };
- if (val) {
- password = val;
- Cryptpad.getFileSize(window.location.href, password, waitFor(function (e, size) {
- if (size !== 0) {
- return void todo();
- }
- // Wrong password or deleted file?
- askPassword(true);
- }));
- } else {
- askPassword();
+ if (!val && sessionStorage.newPadPassword) {
+ val = sessionStorage.newPadPassword;
+ delete sessionStorage.newPadPassword;
+ }
+
+ password = val;
+ Cryptpad.getFileSize(window.location.href, password, waitFor(function (e, size) {
+ if (size !== 0) {
+ return void todo();
}
- }), parsed.getUrl());
- return;
- }
- // If no password, continue...
- todo();
+ if (parsed.hashData.mode === 'view' && (val || !parsed.hashData.password)) {
+ // Error, wrong password stored, the view seed has changed with the password
+ // password will never work
+ sframeChan.event("EV_PAD_PASSWORD_ERROR");
+ waitFor.abort();
+ return;
+ }
+ // Wrong password or deleted file?
+ askPassword(true);
+ }));
+ }), parsed.getUrl());
}
}).nThen(function (waitFor) {
if (cfg.afterSecrets) {
diff --git a/www/common/sframe-common.js b/www/common/sframe-common.js
index 2f22a2b57..c73e43996 100644
--- a/www/common/sframe-common.js
+++ b/www/common/sframe-common.js
@@ -599,6 +599,10 @@ define([
UIElements.displayPasswordPrompt(funcs);
});
+ ctx.sframeChan.on("EV_PAD_PASSWORD_ERROR", function () {
+ UI.errorLoadingScreen(Messages.password_error_seed);
+ });
+
ctx.sframeChan.on('EV_LOADING_INFO', function (data) {
UI.updateLoadingProgress(data, 'drive');
});
diff --git a/www/common/translations/messages.de.json b/www/common/translations/messages.de.json
index 62be310eb..70f4f946d 100644
--- a/www/common/translations/messages.de.json
+++ b/www/common/translations/messages.de.json
@@ -1220,5 +1220,6 @@
"survey": "CryptPad-Umfrage",
"team_title": "Team: {0}",
"team_quota": "Speicherplatzbegrenzung deines Teams",
- "drive_quota": "Deine Speicherplatzbegrenzung"
+ "drive_quota": "Deine Speicherplatzbegrenzung",
+ "settings_codeBrackets": "Klammern automatisch schließen"
}
diff --git a/www/common/translations/messages.fr.json b/www/common/translations/messages.fr.json
index 5ede7e788..2e4574c72 100644
--- a/www/common/translations/messages.fr.json
+++ b/www/common/translations/messages.fr.json
@@ -1220,5 +1220,6 @@
"team_demoteMeConfirm": "Vous êtes sur le point de renoncer à vos droits. Vous ne serez pas en mesure d'annuler cette action. Continuer ?",
"team_title": "Équipe : {0}",
"team_quota": "Limite de stockage de votre équipe",
- "drive_quota": "Votre limite de stockage"
+ "drive_quota": "Votre limite de stockage",
+ "settings_codeBrackets": "Fermer automatiquement les parenthèses"
}
diff --git a/www/common/translations/messages.json b/www/common/translations/messages.json
index ddc6c8124..a811b3e1b 100644
--- a/www/common/translations/messages.json
+++ b/www/common/translations/messages.json
@@ -1220,5 +1220,6 @@
"team_demoteMeConfirm": "You are about to give up your rights. You will not be able to undo this action. Are you sure?",
"team_title": "Team: {0}",
"team_quota": "Your team's storage limit",
- "drive_quota": "Your storage limit"
+ "drive_quota": "Your storage limit",
+ "settings_codeBrackets": "Auto-close brackets"
}
diff --git a/www/common/userObject.js b/www/common/userObject.js
index c57bc0bb0..cce6982c4 100644
--- a/www/common/userObject.js
+++ b/www/common/userObject.js
@@ -14,6 +14,7 @@ define([
var TRASH = module.TRASH = "trash";
var TEMPLATE = module.TEMPLATE = "template";
var SHARED_FOLDERS = module.SHARED_FOLDERS = "sharedFolders";
+ var SHARED_FOLDERS_TEMP = module.SHARED_FOLDERS_TEMP = "sharedFoldersTemp"; // Maybe deleted or new password
// Create untitled documents when no name is given
var getLocaleDate = function () {
@@ -93,6 +94,7 @@ define([
exp.TRASH = TRASH;
exp.TEMPLATE = TEMPLATE;
exp.SHARED_FOLDERS = SHARED_FOLDERS;
+ exp.SHARED_FOLDERS_TEMP = SHARED_FOLDERS_TEMP;
var sharedFolder = exp.sharedFolder = config.sharedFolder;
exp.id = config.id;
@@ -447,11 +449,17 @@ define([
return Util.deduplicateString(ret);
};
- var getIdFromHref = exp.getIdFromHref = function (href) {
+ var getIdFromHref = exp.getIdFromHref = function (_href) {
var result;
+ var noPassword = function (str) {
+ if (!str) { return; }
+ var value = str.replace(/\/p\/?/, '/');
+ return Hash.getRelativeHref(value);
+ };
+ var href = noPassword(_href);
getFiles([FILES_DATA]).some(function (id) {
- if (getHref(files[FILES_DATA][id]) === href ||
- files[FILES_DATA][id].roHref === href) {
+ if (noPassword(getHref(files[FILES_DATA][id])) === href ||
+ noPassword(files[FILES_DATA][id].roHref) === href) {
result = id;
return true;
}
@@ -459,11 +467,17 @@ define([
return result;
};
- exp.getSFIdFromHref = function (href) {
+ exp.getSFIdFromHref = function (_href) {
var result;
+ var noPassword = function (str) {
+ if (!str) { return; }
+ var value = str.replace(/\/p\/?/, '/');
+ return Hash.getRelativeHref(value);
+ };
+ var href = noPassword(_href);
getFiles([SHARED_FOLDERS]).some(function (id) {
- if (getHref(files[SHARED_FOLDERS][id]) === href ||
- files[SHARED_FOLDERS][id].roHref === href) {
+ if (noPassword(getHref(files[SHARED_FOLDERS][id])) === href ||
+ noPassword(files[SHARED_FOLDERS][id].roHref) === href) {
result = id;
return true;
}
diff --git a/www/drive/inner.js b/www/drive/inner.js
index 5e2527600..4a0de6042 100644
--- a/www/drive/inner.js
+++ b/www/drive/inner.js
@@ -50,6 +50,14 @@ define([
sframeChan.query('Q_DRIVE_GETOBJECT', {
sharedFolder: fId
}, waitFor(function (err, newObj) {
+ if (!APP.loggedIn && APP.newSharedFolder) {
+ if (!newObj || !Object.keys(newObj).length) {
+ // Empty anon drive: deleted
+ var msg = Messages.deletedError + '
' + Messages.errorRedirectToHome;
+ setTimeout(function () { UI.errorLoadingScreen(msg, false, function () {}); });
+ APP.newSharedFolder = null;
+ }
+ }
folders[fId] = folders[fId] || {};
copyObjectValue(folders[fId], newObj);
folders[fId].readOnly = !secret.keys.secondaryKey;
diff --git a/www/settings/inner.js b/www/settings/inner.js
index cfcf57da3..d67f62818 100644
--- a/www/settings/inner.js
+++ b/www/settings/inner.js
@@ -85,6 +85,7 @@ define([
'code': [
'cp-settings-code-indent-unit',
'cp-settings-code-indent-type',
+ 'cp-settings-code-brackets',
'cp-settings-code-font-size',
'cp-settings-code-spellcheck',
],
@@ -1445,6 +1446,35 @@ define([
return $div;
};
+ create['code-brackets'] = function () {
+ var key = 'brackets';
+
+ var $div = $('