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 = $('
', { + 'class': 'cp-settings-code-brackets cp-sidebarlayout-element' + }); + $('