Merge branch 'filePassword' into staging

This commit is contained in:
yflory
2019-11-08 15:28:50 +01:00
35 changed files with 2126 additions and 660 deletions

View File

@@ -118,6 +118,16 @@ define([], function () {
#cp-loading-password-prompt .cp-password-form button:hover { #cp-loading-password-prompt .cp-password-form button:hover {
background-color: #326599; 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 { #cp-loading .cp-loading-spinner-container {
position: relative; position: relative;
height: 100px; height: 100px;

View File

@@ -116,7 +116,7 @@
}*/ }*/
} }
.dialog, .alert { .dialog {
& > div { & > div {
background-color: @alertify-dialog-bg; background-color: @alertify-dialog-bg;
&.half { &.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 { input:not(.form-control), textarea {
background-color: @alertify-input-bg; background-color: @alertify-input-bg;
color: @alertify-input-fg; color: @alertify-input-fg;

View File

@@ -466,6 +466,7 @@
padding: 0.25em 0.75em; padding: 0.25em 0.75em;
margin: 1em; margin: 1em;
background: @drive_info-box-bg; background: @drive_info-box-bg;
cursor: default;
span { span {
cursor: pointer; cursor: pointer;
float: right; float: right;
@@ -978,5 +979,28 @@
flex: 1; flex: 1;
} }
} }
#cp-app-drive-edition-state {
height: @variables_bar-height;
display: flex;
align-items: center;
justify-content: center;
background-color: lighten(@colortheme_drive-bg, 32%);
color: black;
font-weight: bold;
text-transform: uppercase;
cursor: default;
}
#cp-app-drive-connection-state {
height: @variables_bar-height;
display: flex;
align-items: center;
justify-content: center;
background-color: #eb675e;
color: white;
font-weight: bold;
text-transform: uppercase;
cursor: default;
}
} }

2
rpc.js
View File

@@ -857,6 +857,7 @@ var removeOwnedChannel = function (Env, channelId, unsafeKey, cb) {
if (err) { if (err) {
return void cb("E_PROOF_REMOVAL"); return void cb("E_PROOF_REMOVAL");
} }
cb();
}); });
} }
@@ -869,6 +870,7 @@ var removeOwnedChannel = function (Env, channelId, unsafeKey, cb) {
if (err) { if (err) {
return void cb("E_PROOF_REMOVAL"); return void cb("E_PROOF_REMOVAL");
} }
cb();
}); });
}); });
} }

View File

@@ -144,7 +144,10 @@ var upload_cancel = function (Env, safeKey, fileSize, cb) {
var session = Env.getSession(safeKey); var session = Env.getSession(safeKey);
session.pendingUploadSize = fileSize; session.pendingUploadSize = fileSize;
session.currentUploadSize = 0; session.currentUploadSize = 0;
if (session.blobstage) { session.blobstage.close(); } if (session.blobstage) {
session.blobstage.close();
delete session.blobstage;
}
var path = makeStagePath(Env, safeKey); var path = makeStagePath(Env, safeKey);

View File

@@ -420,68 +420,6 @@ Version 1
}; };
// STORAGE // STORAGE
Hash.findWeaker = function (href, channel, recents) {
var parsed = parsePadUrl(href);
if (!parsed.hash) { return false; }
// We can't have a weaker hash if we're already in view mode
if (parsed.hashData && parsed.hashData.mode === 'view') { return; }
var weaker;
Object.keys(recents).some(function (id) {
var pad = recents[id];
if (pad.href || !pad.roHref) {
// This pad has an edit link, so it can't be weaker
return;
}
var p = parsePadUrl(pad.roHref);
if (p.type !== parsed.type) { return; } // Not the same type
if (p.hash === parsed.hash) { return; } // Same hash, not stronger
if (channel !== pad.channel) { return; } // Not the same channel
var pHash = p.hashData;
var parsedHash = parsed.hashData;
if (!parsedHash || !pHash) { return; }
// We don't have stronger/weaker versions of files or users
if (pHash.type !== 'pad' && parsedHash.type !== 'pad') { return; }
if (pHash.version !== parsedHash.version) { return; }
if (pHash.mode === 'view' && parsedHash.mode === 'edit') {
weaker = pad;
return true;
}
return;
});
return weaker;
};
Hash.findStronger = function (href, channel, recents) {
var parsed = parsePadUrl(href);
if (!parsed.hash) { return false; }
var parsedHash = parsed.hashData;
// We can't have a stronger hash if we're already in edit mode
if (!parsedHash || parsedHash.mode === 'edit') { return; }
// We don't have stronger/weaker versions of files or users
if (parsedHash.type !== 'pad') { return; }
var stronger;
Object.keys(recents).some(function (id) {
var pad = recents[id];
// Not the same channel? reject
if (channel !== pad.channel) { return; }
// If this pad doesn't have an edit link, it can't be stronger
if (!pad.href || !pad.roHref) { return; }
// This is a pad with an EDIT href and using the same channel as our target
// ==> it is stronger
stronger = pad;
return true;
});
return stronger;
};
Hash.hrefToHexChannelId = function (href, password) { Hash.hrefToHexChannelId = function (href, password) {
var parsed = Hash.parsePadUrl(href); var parsed = Hash.parsePadUrl(href);
if (!parsed || !parsed.hash) { return; } if (!parsed || !parsed.hash) { return; }

View File

@@ -79,8 +79,10 @@ define([
waitFor.abort(); waitFor.abort();
return void cb(err || 'EEMPTY'); return void cb(err || 'EEMPTY');
} }
delete val.owners; if (!val.fileType) {
delete val.expire; delete val.owners;
delete val.expire;
}
Util.extend(data, val); Util.extend(data, val);
if (data.href) { data.href = base + data.href; } if (data.href) { data.href = base + data.href; }
if (data.roHref) { data.roHref = base + data.roHref; } if (data.roHref) { data.roHref = base + data.roHref; }
@@ -434,6 +436,7 @@ define([
} else { } else {
Object.keys(priv.teams || {}).some(function (id) { Object.keys(priv.teams || {}).some(function (id) {
var team = priv.teams[id] || {}; var team = priv.teams[id] || {};
if (team.viewer) { return; }
if (data.owners.indexOf(team.edPublic) === -1) { return; } if (data.owners.indexOf(team.edPublic) === -1) { return; }
owned = id; owned = id;
return true; return true;
@@ -492,7 +495,7 @@ define([
data: _owners, data: _owners,
large: true large: true
}, function () {}); }, function () {});
if (_ownersGrid) { if (_ownersGrid && Object.keys(_owners).length) {
$d.append(_ownersGrid.div); $d.append(_ownersGrid.div);
} else { } else {
$d.append([ $d.append([
@@ -548,13 +551,17 @@ define([
$d.append(password); $d.append(password);
} }
if (!data.noEditPassword && owned && parsed.hashData.type === 'pad' && parsed.type !== "sheet") { // FIXME SHEET fix password change for sheets if (!data.noEditPassword && owned && parsed.type !== "sheet") { // FIXME SHEET fix password change for sheets
var sframeChan = common.getSframeChannel(); var sframeChan = common.getSframeChannel();
var isFile = parsed.hashData.type === 'file';
var isSharedFolder = parsed.type === 'drive';
var changePwTitle = Messages.properties_changePassword; var changePwTitle = Messages.properties_changePassword;
var changePwConfirm = Messages.properties_confirmChange; var changePwConfirm = isFile ? Messages.properties_confirmChangeFile : Messages.properties_confirmChange;
if (!hasPassword) { if (!hasPassword) {
changePwTitle = Messages.properties_addPassword; changePwTitle = Messages.properties_addPassword;
changePwConfirm = Messages.properties_confirmNew; changePwConfirm = isFile ? Messages.properties_confirmNewFile : Messages.properties_confirmNew;
} }
$('<label>', {'for': 'cp-app-prop-change-password'}) $('<label>', {'for': 'cp-app-prop-change-password'})
.text(changePwTitle).appendTo($d); .text(changePwTitle).appendTo($d);
@@ -567,23 +574,58 @@ define([
newPassword, newPassword,
passwordOk passwordOk
]); ]);
var pLocked = false;
$(passwordOk).click(function () { $(passwordOk).click(function () {
var newPass = $(newPassword).find('input').val(); var newPass = $(newPassword).find('input').val();
if (data.password === newPass || if (data.password === newPass ||
(!data.password && !newPass)) { (!data.password && !newPass)) {
return void UI.alert(Messages.properties_passwordSame); return void UI.alert(Messages.properties_passwordSame);
} }
if (pLocked) { return; }
pLocked = true;
UI.confirm(changePwConfirm, function (yes) { UI.confirm(changePwConfirm, function (yes) {
if (!yes) { return; } if (!yes) { pLocked = false; return; }
sframeChan.query("Q_PAD_PASSWORD_CHANGE", { $(passwordOk).html('').append(h('span.fa.fa-spinner.fa-spin', {style: 'margin-left: 0'}));
var q = isFile ? 'Q_BLOB_PASSWORD_CHANGE' : 'Q_PAD_PASSWORD_CHANGE';
// If this is a file password change, register to the upload events:
// * if there is a pending upload, ask if we shoudl interrupt
// * display upload progress
var onPending;
var onProgress;
if (isFile) {
onPending = sframeChan.on('Q_BLOB_PASSWORD_CHANGE_PENDING', function (data, cb) {
onPending.stop();
UI.confirm(Messages.upload_uploadPending, function (yes) {
cb({cancel: yes});
});
});
onProgress = sframeChan.on('EV_BLOB_PASSWORD_CHANGE_PROGRESS', function (data) {
if (typeof (data) !== "number") { return; }
var p = Math.round(data);
$(passwordOk).text(p + '%');
});
}
sframeChan.query(q, {
teamId: typeof(owned) !== "boolean" ? owned : undefined, teamId: typeof(owned) !== "boolean" ? owned : undefined,
href: data.href || data.roHref, href: data.href || data.roHref,
password: newPass password: newPass
}, function (err, data) { }, function (err, data) {
$(passwordOk).text(Messages.properties_changePasswordButton);
pLocked = false;
if (err || data.error) { if (err || data.error) {
console.error(err || data.error);
return void UI.alert(Messages.properties_passwordError); return void UI.alert(Messages.properties_passwordError);
} }
UI.findOKButton().click(); UI.findOKButton().click();
if (isFile) {
onProgress.stop();
$(passwordOk).text(Messages.properties_changePasswordButton);
var alertMsg = data.warning ? Messages.properties_passwordWarningFile
: Messages.properties_passwordSuccessFile;
return void UI.alert(alertMsg, undefined, {force: true});
}
// If we didn't have a password, we have to add the /p/ // If we didn't have a password, we have to add the /p/
// If we had a password and we changed it to a new one, we just have to reload // If we had a password and we changed it to a new one, we just have to reload
// If we had a password and we removed it, we have to remove the /p/ // If we had a password and we removed it, we have to remove the /p/
@@ -593,7 +635,9 @@ define([
}, {force: true}); }, {force: true});
} }
return void UI.alert(Messages.properties_passwordSuccess, function () { 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}); }, {force: true});
}); });
}); });
@@ -623,7 +667,7 @@ define([
}; };
var getPadProperties = function (common, data, cb) { var getPadProperties = function (common, data, cb) {
var $d = $('<div>'); var $d = $('<div>');
if (!data || (!data.href && !data.roHref)) { return void cb(void 0, $d); } if (!data) { return void cb(void 0, $d); }
if (data.href) { if (data.href) {
$('<label>', {'for': 'cp-app-prop-link'}).text(Messages.editShare).appendTo($d); $('<label>', {'for': 'cp-app-prop-link'}).text(Messages.editShare).appendTo($d);
@@ -871,6 +915,7 @@ define([
// config.teamId only exists when we're trying to share a pad from a team drive // config.teamId only exists when we're trying to share a pad from a team drive
// In this case, we don't want to share the pad with the current team // In this case, we don't want to share the pad with the current team
if (config.teamId && config.teamId === id) { return; } if (config.teamId && config.teamId === id) { return; }
if (!teamsData[id].hasSecondaryKey) { return; }
var t = teamsData[id]; var t = teamsData[id];
teams[t.edPublic] = { teams[t.edPublic] = {
notifications: true, notifications: true,
@@ -984,7 +1029,7 @@ define([
var hashes = config.hashes; var hashes = config.hashes;
var common = config.common; var common = config.common;
if (!hashes) { return; } if (!hashes || (!hashes.editHash && !hashes.viewHash)) { return; }
// Share link tab // Share link tab
var hasFriends = Object.keys(config.friends || {}).length !== 0; var hasFriends = Object.keys(config.friends || {}).length !== 0;
@@ -992,7 +1037,12 @@ define([
var friendsList = hasFriends ? createShareWithFriends(config, onFriendShare) : undefined; var friendsList = hasFriends ? createShareWithFriends(config, onFriendShare) : undefined;
var friendsUIClass = hasFriends ? '.cp-share-columns' : ''; var friendsUIClass = hasFriends ? '.cp-share-columns' : '';
var mainShareColumn = h('div.cp-share-column.contains-nav', [ var content = [];
var sfContent = [
h('label', Messages.sharedFolders_share),
h('br'),
];
var shareContent = [
h('label', Messages.share_linkAccess), h('label', Messages.share_linkAccess),
h('br'), h('br'),
UI.createRadio('cp-share-editable', 'cp-share-editable-true', UI.createRadio('cp-share-editable', 'cp-share-editable-true',
@@ -1000,18 +1050,21 @@ define([
UI.createRadio('cp-share-editable', 'cp-share-editable-false', UI.createRadio('cp-share-editable', 'cp-share-editable-false',
Messages.share_linkView, false, { mark: {tabindex:1} }), Messages.share_linkView, false, { mark: {tabindex:1} }),
h('br'), h('br'),
];
var padContent = [
h('label', Messages.share_linkOptions), h('label', Messages.share_linkOptions),
h('br'), h('br'),
UI.createCheckbox('cp-share-embed', Messages.share_linkEmbed, false, { mark: {tabindex:1} }), UI.createCheckbox('cp-share-embed', Messages.share_linkEmbed, false, { mark: {tabindex:1} }),
UI.createCheckbox('cp-share-present', Messages.share_linkPresent, false, { mark: {tabindex:1} }), UI.createCheckbox('cp-share-present', Messages.share_linkPresent, false, { mark: {tabindex:1} }),
h('br'), h('br'),
UI.dialog.selectable('', { id: 'cp-share-link-preview', tabindex: 1 }), ];
]); if (config.sharedFolder) { Array.prototype.push.apply(content, sfContent); }
Array.prototype.push.apply(content, shareContent);
if (!config.sharedFolder) { Array.prototype.push.apply(content, padContent); }
content.push(UI.dialog.selectable('', { id: 'cp-share-link-preview', tabindex: 1 }));
var mainShareColumn = h('div.cp-share-column.contains-nav', content);
var link = h('div.cp-share-modal' + friendsUIClass); var link = h('div.cp-share-modal' + friendsUIClass);
if (!hashes.editHash) {
$(link).find('#cp-share-editable-false').attr('checked', true);
$(link).find('#cp-share-editable-true').removeAttr('checked').attr('disabled', true);
}
var saveValue = function () { var saveValue = function () {
var edit = Util.isChecked($(link).find('#cp-share-editable-true')); var edit = Util.isChecked($(link).find('#cp-share-editable-true'));
var embed = Util.isChecked($(link).find('#cp-share-embed')); var embed = Util.isChecked($(link).find('#cp-share-embed'));
@@ -1050,21 +1103,32 @@ define([
if (success) { UI.log(Messages.shareSuccess); } if (success) { UI.log(Messages.shareSuccess); }
}, },
keys: [13] keys: [13]
}, {
className: 'primary',
name: Messages.share_linkOpen,
onClick: function () {
saveValue();
var v = getLinkValue();
window.open(v);
},
keys: [[13, 'ctrl']]
}]; }];
if (!config.sharedFolder) {
shareButtons.push({
className: 'primary',
name: Messages.share_linkOpen,
onClick: function () {
saveValue();
var v = getLinkValue();
window.open(v);
},
keys: [[13, 'ctrl']]
});
}
var $link = $(link); var $link = $(link);
$(mainShareColumn).append(UI.dialog.getButtons(shareButtons, config.onClose)).appendTo($link); $(mainShareColumn).append(UI.dialog.getButtons(shareButtons, config.onClose)).appendTo($link);
$(friendsList).appendTo($link); $(friendsList).appendTo($link);
if (!hashes.editHash) {
$(link).find('#cp-share-editable-false').attr('checked', true);
$(link).find('#cp-share-editable-true').removeAttr('checked').attr('disabled', true);
} else if (!hashes.viewHash) {
$(link).find('#cp-share-editable-false').removeAttr('checked').attr('disabled', true);
$(link).find('#cp-share-editable-true').attr('checked', true);
}
$(link).find('#cp-share-link-preview').val(getLinkValue()); $(link).find('#cp-share-link-preview').val(getLinkValue());
$(link).find('input[type="radio"], input[type="checkbox"]').on('change', function () { $(link).find('input[type="radio"], input[type="checkbox"]').on('change', function () {
$(link).find('#cp-share-link-preview').val(getLinkValue()); $(link).find('#cp-share-link-preview').val(getLinkValue());
@@ -1126,7 +1190,7 @@ define([
} }
common.getAttribute(['general', 'share'], function (err, val) { common.getAttribute(['general', 'share'], function (err, val) {
val = val || {}; val = val || {};
if (val.edit === false || !hashes.editHash) { if ((val.edit === false && hashes.viewHash) || !hashes.editHash) {
$(link).find('#cp-share-editable-false').prop('checked', true); $(link).find('#cp-share-editable-false').prop('checked', true);
$(link).find('#cp-share-editable-true').prop('checked', false); $(link).find('#cp-share-editable-true').prop('checked', false);
} else { } else {
@@ -1135,12 +1199,17 @@ define([
} }
if (val.embed) { $(link).find('#cp-share-embed').prop('checked', true); } if (val.embed) { $(link).find('#cp-share-embed').prop('checked', true); }
if (val.present) { $(link).find('#cp-share-present').prop('checked', true); } if (val.present) { $(link).find('#cp-share-present').prop('checked', true); }
if (config.sharedFolder) {
delete val.embed;
delete val.present;
}
$(link).find('#cp-share-link-preview').val(getLinkValue(val)); $(link).find('#cp-share-link-preview').val(getLinkValue(val));
}); });
common.getMetadataMgr().onChange(function () { common.getMetadataMgr().onChange(function () {
// "hashes" is only available is the secure "share" app // "hashes" is only available is the secure "share" app
hashes = common.getMetadataMgr().getPrivateData().hashes; var _hashes = common.getMetadataMgr().getPrivateData().hashes;
if (!hashes) { return; } if (!_hashes) { return; }
hashes = _hashes;
$(link).find('#cp-share-link-preview').val(getLinkValue()); $(link).find('#cp-share-link-preview').val(getLinkValue());
}); });
return tabs; return tabs;
@@ -1242,47 +1311,6 @@ define([
} }
return tabs; return tabs;
}; };
UIElements.createSFShareModal = function (config) {
var origin = config.origin;
var pathname = config.pathname;
var hashes = config.hashes;
if (!hashes.editHash) { throw new Error("You must provide a valid hash"); }
var url = origin + pathname + '#' + hashes.editHash;
// Share link tab
var hasFriends = Object.keys(config.friends || {}).length !== 0;
var friendsList = hasFriends ? createShareWithFriends(config) : undefined;
var friendsUIClass = hasFriends ? '.cp-share-columns' : '';
var mainShareColumn = h('div.cp-share-column.contains-nav', [
h('div.cp-share-column', [
h('label', Messages.sharedFolders_share),
h('br'),
hasFriends ? h('p', Messages.share_description) : undefined,
UI.dialog.selectable(url, { id: 'cp-share-link-preview', tabindex: 1 })
])
]);
var link = h('div.cp-share-modal' + friendsUIClass);
var linkButtons = [{
className: 'cancel',
name: Messages.cancel,
onClick: function () {},
keys: [27]
}];
var shareButtons = [{
className: 'primary',
name: Messages.share_linkCopy,
onClick: function () {
var success = Clipboard.copy(url);
if (success) { UI.log(Messages.shareSuccess); }
},
keys: [13]
}];
var $link = $(link);
$(mainShareColumn).append(UI.dialog.getButtons(shareButtons, config.onClose)).appendTo($link);
$(friendsList).appendTo($link);
return UI.dialog.customModal(link, {buttons: linkButtons});
};
UIElements.createInviteTeamModal = function (config) { UIElements.createInviteTeamModal = function (config) {
var common = config.common; var common = config.common;
@@ -1533,8 +1561,12 @@ define([
} }
UI.confirm(msg, function (yes) { UI.confirm(msg, function (yes) {
if (!yes) { return; } if (!yes) { return; }
sframeChan.query('Q_MOVE_TO_TRASH', null, function (err) { sframeChan.query('Q_MOVE_TO_TRASH', null, function (err, obj) {
if (err) { return void callback(err); } err = err || (obj && obj.error);
if (err) {
callback(err);
return void UI.warn(Messages.fm_forbidden);
}
var cMsg = common.isLoggedIn() ? Messages.movedToTrash : Messages.deleted; var cMsg = common.isLoggedIn() ? Messages.movedToTrash : Messages.deleted;
var msg = common.fixLinks($('<div>').html(cMsg)); var msg = common.fixLinks($('<div>').html(cMsg));
UI.alert(msg); UI.alert(msg);
@@ -2081,10 +2113,7 @@ define([
var cryptKey = Hash.encodeBase64(secret.keys && secret.keys.cryptKey); var cryptKey = Hash.encodeBase64(secret.keys && secret.keys.cryptKey);
var src = origin + Hash.getBlobPathFromHex(hexFileName); var src = origin + Hash.getBlobPathFromHex(hexFileName);
common.getFileSize(hexFileName, function (e, data) { common.getFileSize(hexFileName, function (e, data) {
if (e || !data) { if (e || !data) { return void displayDefault(); }
displayDefault();
return void console.error(e || "404 avatar");
}
if (typeof data !== "number") { return void displayDefault(); } if (typeof data !== "number") { return void displayDefault(); }
if (Util.bytesToMegabytes(data) > 0.5) { return void displayDefault(); } if (Util.bytesToMegabytes(data) > 0.5) { return void displayDefault(); }
var $img = $('<media-tag>').appendTo($container); var $img = $('<media-tag>').appendTo($container);
@@ -3561,6 +3590,7 @@ define([
} }
return void UI.warn(Messages.autostore_error); return void UI.warn(Messages.autostore_error);
} }
$(document).trigger('cpPadStored');
delete autoStoreModal[priv.channel]; delete autoStoreModal[priv.channel];
modal.delete(); modal.delete();
UIElements.displayCrowdfunding(common); UIElements.displayCrowdfunding(common);

View File

@@ -6,6 +6,7 @@ define([
'/common/common-messaging.js', '/common/common-messaging.js',
'/common/common-constants.js', '/common/common-constants.js',
'/common/common-feedback.js', '/common/common-feedback.js',
'/common/userObject.js',
'/common/outer/local-store.js', '/common/outer/local-store.js',
'/common/outer/worker-channel.js', '/common/outer/worker-channel.js',
'/common/outer/login-block.js', '/common/outer/login-block.js',
@@ -13,7 +14,7 @@ define([
'/customize/application_config.js', '/customize/application_config.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
], function (Config, Messages, Util, Hash, ], function (Config, Messages, Util, Hash,
Messaging, Constants, Feedback, LocalStore, Channel, Block, Messaging, Constants, Feedback, UserObject, LocalStore, Channel, Block,
AppConfig, Nthen) { AppConfig, Nthen) {
/* This file exposes functionality which is specific to Cryptpad, but not to /* This file exposes functionality which is specific to Cryptpad, but not to
@@ -158,11 +159,12 @@ define([
}); });
}; };
common.addSharedFolder = function (teamId, secret, cb) { common.addSharedFolder = function (teamId, secret, cb) {
var href = (secret.keys && secret.keys.editKeyStr) ? '/drive/#' + Hash.getEditHashFromKeys(secret) : undefined;
postMessage("ADD_SHARED_FOLDER", { postMessage("ADD_SHARED_FOLDER", {
teamId: teamId, teamId: teamId,
path: ['root'], path: ['root'],
folderData: { folderData: {
href: '/drive/#' + Hash.getEditHashFromKeys(secret), href: href,
roHref: '/drive/#' + Hash.getViewHashFromKeys(secret), roHref: '/drive/#' + Hash.getViewHashFromKeys(secret),
channel: secret.channel, channel: secret.channel,
password: secret.password, password: secret.password,
@@ -372,7 +374,8 @@ define([
common.getMetadata = function (cb) { common.getMetadata = function (cb) {
postMessage("GET_METADATA", null, function (obj) { var parsed = Hash.parsePadUrl(window.location.href);
postMessage("GET_METADATA", parsed && parsed.type, function (obj) {
if (obj && obj.error) { return void cb(obj.error); } if (obj && obj.error) { return void cb(obj.error); }
cb(null, obj); cb(null, obj);
}); });
@@ -867,12 +870,16 @@ define([
} }
var newHref = '/' + parsed.type + '/#' + newHash; var newHref = '/' + parsed.type + '/#' + newHash;
var isSharedFolder = parsed.type === 'drive';
var optsGet = {}; var optsGet = {};
var optsPut = { var optsPut = {
password: newPassword, password: newPassword,
metadata: {} metadata: {},
initialState: isSharedFolder ? '{}' : undefined
}; };
var cryptgetVal;
Nthen(function (waitFor) { Nthen(function (waitFor) {
if (parsed.hashData && parsed.hashData.password) { if (parsed.hashData && parsed.hashData.password) {
@@ -933,38 +940,46 @@ define([
} }
var expire = oldMetadata.expire; 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) { }).nThen(function (waitFor) {
Crypt.get(parsed.hash, waitFor(function (err, val) { Crypt.get(parsed.hash, waitFor(function (err, val) {
if (err) { if (err) {
waitFor.abort(); waitFor.abort();
return void cb({ error: err }); return void cb({ error: err });
} }
Crypt.put(newHash, val, waitFor(function (err) { cryptgetVal = val;
if (err) { if (isSharedFolder) {
waitFor.abort(); var parsed = JSON.parse(val || '{}');
return void cb({ error: err }); var oldKey = parsed.version === 2 && oldSecret.keys.secondaryKey;
} var newKey = newSecret.keys.secondaryKey;
}), optsPut); UserObject.reencrypt(oldKey, newKey, parsed);
cryptgetVal = JSON.stringify(parsed);
}
}), optsGet); }), optsGet);
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
Crypt.put(newHash, cryptgetVal, waitFor(function (err) {
if (err) {
waitFor.abort();
return void cb({ error: err });
}
}), optsPut);
}).nThen(function (waitFor) {
if (isSharedFolder) {
postMessage("UPDATE_SHARED_FOLDER_PASSWORD", {
href: href,
oldChannel: oldChannel,
password: newPassword
}, waitFor());
return;
}
pad.leavePad({ pad.leavePad({
channel: oldChannel channel: oldChannel
}, waitFor()); }, waitFor());
pad.onDisconnectEvent.fire(true); pad.onDisconnectEvent.fire(true);
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
common.removeOwnedChannel({ // Set the new password to our pad data
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) {
common.setPadAttribute('password', newPassword, waitFor(function (err) { common.setPadAttribute('password', newPassword, waitFor(function (err) {
if (err) { warning = true; } if (err) { warning = true; }
}), href); }), href);
@@ -981,6 +996,23 @@ define([
common.setPadAttribute('href', newHref, waitFor(function (err) { common.setPadAttribute('href', newHref, waitFor(function (err) {
if (err) { warning = true; } if (err) { warning = true; }
}), href); }), 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) {
postMessage("CHANGE_PAD_PASSWORD_PIN", {
oldChannel: oldChannel,
channel: newSecret.channel
}, waitFor());
}
}).nThen(function () { }).nThen(function () {
cb({ cb({
warning: warning, warning: warning,
@@ -991,6 +1023,138 @@ define([
}); });
}; };
common.changeBlobPassword = function (data, handlers, cb) {
var href = data.href;
var newPassword = data.password;
var teamId = data.teamId;
if (!href) { return void cb({ error: 'EINVAL_HREF' }); }
var parsed = Hash.parsePadUrl(href);
if (!parsed.hash) { return void cb({ error: 'EINVAL_HREF' }); }
if (parsed.hashData.type !== 'file') { return void cb({ error: 'EINVAL_TYPE' }); }
var newSecret;
var newHash;
if (parsed.hashData.version >= 2) {
newSecret = Hash.getSecrets(parsed.type, parsed.hash, newPassword);
if (!(newSecret.keys && newSecret.keys.fileKeyStr)) {
return void cb({error: 'EAUTH'});
}
newHash = Hash.getFileHashFromKeys(newSecret);
} else {
newHash = Hash.createRandomHash(parsed.type, newPassword);
newSecret = Hash.getSecrets(parsed.type, newHash, newPassword);
}
var newHref = '/' + parsed.type + '/#' + newHash;
var fileHost = Config.fileHost || window.location.origin || '';
/*
1. get old password
2. get owners
*/
var oldPassword;
var decrypted;
var oldChannel;
var warning;
var FileCrypto;
var MediaTag;
var Upload;
Nthen(function (waitFor) {
if (parsed.hashData && parsed.hashData.password) {
common.getPadAttribute('password', waitFor(function (err, password) {
oldPassword = password || '';
}), href);
}
}).nThen(function (waitFor) {
require([
'/file/file-crypto.js',
'/common/media-tag.js',
'/common/outer/upload.js',
'/bower_components/tweetnacl/nacl-fast.min.js'
], waitFor(function (_FileCrypto, _MT, _Upload) {
FileCrypto = _FileCrypto;
MediaTag = _MT;
Upload = _Upload;
}));
}).nThen(function (waitFor) {
var oldSecret = Hash.getSecrets(parsed.type, parsed.hash, oldPassword);
oldChannel = oldSecret.channel;
var src = fileHost + Hash.getBlobPathFromHex(oldChannel);
var key = oldSecret.keys && oldSecret.keys.cryptKey;
var cryptKey = window.nacl.util.encodeBase64(key);
var mt = document.createElement('media-tag');
mt.setAttribute('src', src);
mt.setAttribute('data-crypto-key', 'cryptpad:'+cryptKey);
MediaTag(mt).on('complete', waitFor(function (_decrypted) {
decrypted = _decrypted;
})).on('error', function (err) {
waitFor.abort();
cb({error: err});
console.error(err);
});
}).nThen(function (waitFor) {
var reader = new FileReader();
reader.readAsArrayBuffer(decrypted.content);
reader.onloadend = waitFor(function() {
decrypted.u8 = new Uint8Array(reader.result);
});
}).nThen(function (waitFor) {
var key = newSecret.keys && newSecret.keys.cryptKey;
var onError = function (err) {
waitFor.abort();
cb({error: err});
};
Upload.uploadU8(common, {
teamId: teamId,
u8: decrypted.u8,
metadata: decrypted.metadata,
key: key,
id: newSecret.channel,
owned: true,
onError: onError,
onPending: handlers.onPending,
updateProgress: handlers.updateProgress,
}, waitFor());
}).nThen(function (waitFor) {
// Set the new password to our pad data
common.setPadAttribute('password', newPassword, waitFor(function (err) {
if (err) { warning = true; }
}), href);
common.setPadAttribute('channel', newSecret.channel, waitFor(function (err) {
if (err) { warning = true; }
}), href);
if (parsed.hashData.password && newPassword) { return; } // same hash
common.setPadAttribute('href', newHref, waitFor(function (err) {
if (err) { warning = true; }
}), href);
}).nThen(function (waitFor) {
// delete the old pad
common.removeOwnedChannel({
channel: oldChannel,
teamId: teamId
}, waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
return void cb(obj);
}
}));
postMessage("CHANGE_PAD_PASSWORD_PIN", {
oldChannel: oldChannel,
channel: newSecret.channel
}, waitFor());
}).nThen(function () {
cb({
warning: warning,
hash: newHash,
href: newHref,
});
});
};
common.changeUserPassword = function (Crypt, edPublic, data, cb) { common.changeUserPassword = function (Crypt, edPublic, data, cb) {
if (!edPublic) { if (!edPublic) {
return void cb({ return void cb({
@@ -1200,6 +1364,11 @@ define([
if (!parsed.type || !parsed.hashData) { return void cb('E_INVALID_HREF'); } if (!parsed.type || !parsed.hashData) { return void cb('E_INVALID_HREF'); }
hashes = Hash.getHashes(secret); hashes = Hash.getHashes(secret);
// If the current href is an edit one, return the existing hashes
var parsedHash = parsed.hashData;
if (!parsedHash || parsedHash.mode === 'edit') { return void cb(null, hashes); }
if (parsedHash.type !== 'pad') { return void cb(null, hashes); }
if (secret.version === 0) { if (secret.version === 0) {
// It means we're using an old hash // It means we're using an old hash
hashes.editHash = window.location.hash.slice(1); hashes.editHash = window.location.hash.slice(1);
@@ -1212,9 +1381,7 @@ define([
} }
postMessage("GET_STRONGER_HASH", { postMessage("GET_STRONGER_HASH", {
href: window.location.href, channel: secret.channel
channel: secret.channel,
password: secret.password
}, function (hash) { }, function (hash) {
if (hash) { hashes.editHash = hash; } if (hash) { hashes.editHash = hash; }
cb(null, hashes); cb(null, hashes);

View File

@@ -252,15 +252,18 @@ define([
return APP.store[LS_SEARCHCURSOR] || 0; return APP.store[LS_SEARCHCURSOR] || 0;
}; };
// Handle disconnect/reconnect
var setEditable = function (state) { var setEditable = function (state) {
APP.editable = state; if (APP.closed || !APP.$content || !$.contains(document.documentElement, APP.$content[0])) { return; }
if (APP.closed || (APP.$content && !$.contains(document.documentElement, APP.$content[0]))) { return; } APP.editable = !APP.readOnly && state;
if (!state) { if (!state) {
APP.$content.addClass('cp-app-drive-readonly'); APP.$content.addClass('cp-app-drive-readonly');
$('#cp-app-drive-connection-state').show();
$('[draggable="true"]').attr('draggable', false); $('[draggable="true"]').attr('draggable', false);
} }
else { else {
APP.$content.removeClass('cp-app-drive-readonly'); APP.$content.removeClass('cp-app-drive-readonly');
$('#cp-app-drive-connection-state').hide();
$('[draggable="false"]').attr('draggable', true); $('[draggable="false"]').attr('draggable', true);
} }
}; };
@@ -526,10 +529,13 @@ define([
var files = proxy.drive; var files = proxy.drive;
var history = driveConfig.history || {}; var history = driveConfig.history || {};
var edPublic = driveConfig.edPublic || priv.edPublic; var edPublic = driveConfig.edPublic || priv.edPublic;
config.editKey = driveConfig.editKey;
APP.origin = priv.origin; APP.origin = priv.origin;
APP.hideDuplicateOwned = Util.find(priv, ['settings', 'drive', 'hideDuplicate']); APP.hideDuplicateOwned = Util.find(priv, ['settings', 'drive', 'hideDuplicate']);
APP.closed = false; APP.closed = false;
var $readOnly = $('#cp-app-drive-edition-state');
var updateObject = driveConfig.updateObject; var updateObject = driveConfig.updateObject;
var updateSharedFolders = driveConfig.updateSharedFolders; var updateSharedFolders = driveConfig.updateSharedFolders;
@@ -542,7 +548,11 @@ define([
Object.keys(folders).forEach(function (id) { Object.keys(folders).forEach(function (id) {
var f = folders[id]; var f = folders[id];
manager.addProxy(id, f); var sfData = files.sharedFolders[id] || {};
var href = manager.user.userObject.getHref(sfData);
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets('drive', parsed.hash, sfData.password);
manager.addProxy(id, {proxy: f}, null, secret.keys.secondaryKey);
}); });
// UI containers // UI containers
@@ -603,9 +613,7 @@ define([
} }
} }
if (!APP.readOnly) { APP.editable = !APP.readOnly;
setEditable(true);
}
var appStatus = { var appStatus = {
isReady: true, isReady: true,
_onReady: [], _onReady: [],
@@ -1086,8 +1094,10 @@ define([
var show = []; var show = [];
var filter; var filter;
var editable = true;
if (type === "content") { if (type === "content") {
if (APP.$content.data('readOnlyFolder')) { editable = false; }
// Return true in filter to hide // Return true in filter to hide
filter = function ($el, className) { filter = function ($el, className) {
if (className === 'newfolder') { return; } if (className === 'newfolder') { return; }
@@ -1112,6 +1122,10 @@ define([
paths.forEach(function (p) { paths.forEach(function (p) {
var path = p.path; var path = p.path;
var $element = p.element; var $element = p.element;
if (APP.$content.data('readOnlyFolder') &&
manager.isSubpath(path, currentPath)) { editable = false; }
if (!$element.closest("#cp-app-drive-tree").length) { if (!$element.closest("#cp-app-drive-tree").length) {
hide.push('expandall'); hide.push('expandall');
hide.push('collapseall'); hide.push('collapseall');
@@ -1151,9 +1165,12 @@ define([
hide.push('openro'); // Remove open 'view' mode hide.push('openro'); // Remove open 'view' mode
} }
// if it's not a plain text file // if it's not a plain text file
var metadata = manager.getFileData(manager.find(path)); // XXX: there is a bug with this code in anon shared folder, so we disable it
if (!metadata || !Util.isPlainTextFile(metadata.fileType, metadata.title)) { if (APP.loggedIn || !APP.newSharedFolder) {
hide.push('openincode'); var metadata = manager.getFileData(manager.find(path));
if (!metadata || !Util.isPlainTextFile(metadata.fileType, metadata.title)) {
hide.push('openincode');
}
} }
} else if ($element.is('.cp-app-drive-element-sharedf')) { } else if ($element.is('.cp-app-drive-element-sharedf')) {
if (containsFolder) { if (containsFolder) {
@@ -1204,6 +1221,9 @@ define([
hide.push('removesf'); hide.push('removesf');
} }
} }
if ($element.closest('[data-ro]').length) {
editable = false;
}
}); });
if (paths.length > 1) { if (paths.length > 1) {
hide.push('restore'); hide.push('restore');
@@ -1250,7 +1270,8 @@ define([
var filtered = []; var filtered = [];
show.forEach(function (className) { show.forEach(function (className) {
var $el = $contextMenu.find('.cp-app-drive-context-' + className); var $el = $contextMenu.find('.cp-app-drive-context-' + className);
if (!APP.editable && $el.is('.cp-app-drive-context-editable')) { return; } if ((!APP.editable || !editable) && $el.is('.cp-app-drive-context-editable')) { return; }
if ((!APP.editable || !editable) && $el.is('.cp-app-drive-context-editable')) { return; }
if (filter($el, className)) { return; } if (filter($el, className)) { return; }
$el.parent('li').show(); $el.parent('li').show();
filtered.push('.cp-app-drive-context-' + className); filtered.push('.cp-app-drive-context-' + className);
@@ -1657,6 +1678,13 @@ define([
$('.cp-app-drive-element-droppable').removeClass('cp-app-drive-element-droppable'); $('.cp-app-drive-element-droppable').removeClass('cp-app-drive-element-droppable');
var data = ev.dataTransfer.getData("text"); var data = ev.dataTransfer.getData("text");
var newPath = findDropPath(ev.target);
if (!newPath) { return; }
var sfId = manager.isInSharedFolder(newPath);
if (sfId && folders[sfId] && folders[sfId].readOnly) {
return void UI.warn(Messages.fm_forbidden);
}
// Don't use the normal drop handler for file upload // Don't use the normal drop handler for file upload
var fileDrop = ev.dataTransfer.files; var fileDrop = ev.dataTransfer.files;
if (fileDrop.length) { return void onFileDrop(fileDrop, ev); } if (fileDrop.length) { return void onFileDrop(fileDrop, ev); }
@@ -1674,8 +1702,6 @@ define([
} }
}); });
var newPath = findDropPath(ev.target);
if (!newPath) { return; }
if (sharedF && manager.isPathIn(newPath, [TRASH])) { if (sharedF && manager.isPathIn(newPath, [TRASH])) {
return void deletePaths(null, movedPaths); return void deletePaths(null, movedPaths);
} }
@@ -1777,6 +1803,11 @@ define([
if (!manager.isFile(element)) { return; } if (!manager.isFile(element)) { return; }
var data = manager.getFileData(element); var data = manager.getFileData(element);
if (!Object.keys(data).length) {
return true;
}
var href = data.href || data.roHref; var href = data.href || data.roHref;
if (!data) { return void logError("No data for the file", element); } if (!data) { return void logError("No data for the file", element); }
@@ -1850,6 +1881,7 @@ define([
if (!element || !manager.isFolder(element)) { return; } if (!element || !manager.isFolder(element)) { return; }
// The element with the class '.name' is underlined when the 'li' is hovered // The element with the class '.name' is underlined when the 'li' is hovered
var $state = $('<span>', {'class': 'cp-app-drive-element-state'}); var $state = $('<span>', {'class': 'cp-app-drive-element-state'});
var $ro;
if (manager.isSharedFolder(element)) { if (manager.isSharedFolder(element)) {
var data = manager.getSharedFolderData(element); var data = manager.getSharedFolderData(element);
key = data && data.title ? data.title : key; key = data && data.title ? data.title : key;
@@ -1857,8 +1889,21 @@ define([
$span.addClass('cp-app-drive-element-sharedf'); $span.addClass('cp-app-drive-element-sharedf');
_addOwnership($span, $state, data); _addOwnership($span, $state, data);
var hrefData = Hash.parsePadUrl(data.href || data.roHref);
if (hrefData.hashData && hrefData.hashData.password) {
var $password = $passwordIcon.clone().appendTo($state);
$password.attr('title', Messages.fm_passwordProtected || '');
}
if (hrefData.hashData && hrefData.hashData.mode === 'view') {
$ro = $readonlyIcon.clone().appendTo($state);
$ro.attr('title', Messages.readonly);
}
var $shared = $sharedIcon.clone().appendTo($state); var $shared = $sharedIcon.clone().appendTo($state);
$shared.attr('title', Messages.fm_canBeShared); $shared.attr('title', Messages.fm_canBeShared);
} else if ($content.data('readOnlyFolder') || APP.readOnly) {
$ro = $readonlyIcon.clone().appendTo($state);
$ro.attr('title', Messages.readonly);
} }
var sf = manager.hasSubfolder(element); var sf = manager.hasSubfolder(element);
@@ -1931,13 +1976,18 @@ define([
if (isTrash) { return; } if (isTrash) { return; }
openFile(root[key]); openFile(root[key]);
}); });
var invalid;
if (isFolder) { if (isFolder) {
addFolderData(element, key, $element); invalid = addFolderData(element, key, $element);
} else { } else {
addFileData(element, $element); invalid = addFileData(element, $element);
}
if (invalid) {
return;
} }
$element.addClass(liClass); $element.addClass(liClass);
addDragAndDropHandlers($element, newPath, isFolder, !isTrash); var droppable = !isTrash && !APP.$content.data('readOnlyFolder');
addDragAndDropHandlers($element, newPath, isFolder, droppable);
$element.click(function(e) { $element.click(function(e) {
e.stopPropagation(); e.stopPropagation();
onElementClick(e, $element); onElementClick(e, $element);
@@ -2501,23 +2551,32 @@ define([
$sharedIcon.clone().appendTo($shareBlock); $sharedIcon.clone().appendTo($shareBlock);
$('<span>').text(Messages.shareButton).appendTo($shareBlock); $('<span>').text(Messages.shareButton).appendTo($shareBlock);
var data = manager.getSharedFolderData(id); var data = manager.getSharedFolderData(id);
var parsed = Hash.parsePadUrl(data.href); var parsed = (data.href && data.href.indexOf('#') !== -1) ? Hash.parsePadUrl(data.href) : {};
if (!parsed || !parsed.hash) { return void console.error("Invalid href: "+data.href); } var roParsed = Hash.parsePadUrl(data.roHref) || {};
if (!parsed.hash && !roParsed.hash) { return void console.error("Invalid href: "+(data.href || data.roHref)); }
var friends = common.getFriends(); var friends = common.getFriends();
var teams = common.getMetadataMgr().getPrivateData().teams; var teams = common.getMetadataMgr().getPrivateData().teams;
var _wide = Object.keys(friends).length || Object.keys(teams).length; var _wide = Object.keys(friends).length || Object.keys(teams).length;
var modal = UIElements.createSFShareModal({ var ro = folders[id] && folders[id].version >= 2;
var modal = UIElements.createShareModal({
teamId: APP.team, teamId: APP.team,
origin: APP.origin, origin: APP.origin,
pathname: "/drive/", pathname: "/drive/",
friends: friends, friends: friends,
title: data.title, title: data.title,
password: data.password, password: data.password,
sharedFolder: true,
common: common, common: common,
hashes: { hashes: {
editHash: parsed.hash editHash: parsed.hash,
viewHash: ro && roParsed.hash,
} }
}); });
// If we're a viewer and this is an old shared folder (no read-only mode), we
// can't share the read-only URL and we don't have access to the edit one.
// We should hide the share button.
if (!modal) { return; }
modal = UI.dialog.tabs(modal);
$shareBlock.click(function () { $shareBlock.click(function () {
UI.openCustomModal(modal, { UI.openCustomModal(modal, {
wide: _wide wide: _wide
@@ -2777,6 +2836,7 @@ define([
return $container; return $container;
}; };
var createGhostIcon = function ($list) { var createGhostIcon = function ($list) {
if (APP.$content.data('readOnlyFolder') || !APP.editable) { return; }
var isInRoot = currentPath[0] === ROOT; var isInRoot = currentPath[0] === ROOT;
var $element = $('<li>', { var $element = $('<li>', {
'class': 'cp-app-drive-element-row cp-app-drive-element-grid cp-app-drive-new-ghost' 'class': 'cp-app-drive-element-row cp-app-drive-element-grid cp-app-drive-new-ghost'
@@ -3200,6 +3260,7 @@ define([
if (!APP.editable) { debug("Read-only mode"); } if (!APP.editable) { debug("Read-only mode"); }
if (!appStatus.isReady && !force) { return; } if (!appStatus.isReady && !force) { return; }
// Fix path obvious issues
if (!path || path.length === 0) { if (!path || path.length === 0) {
// Only Trash and Root are available in not-owned files manager // Only Trash and Root are available in not-owned files manager
if (!path || displayedCategories.indexOf(path[0]) === -1) { if (!path || displayedCategories.indexOf(path[0]) === -1) {
@@ -3217,7 +3278,7 @@ define([
path = [ROOT]; path = [ROOT];
} }
// Get path data
appStatus.ready(false); appStatus.ready(false);
currentPath = path; currentPath = path;
var s = $content.scrollTop() || 0; var s = $content.scrollTop() || 0;
@@ -3239,6 +3300,7 @@ define([
currentPath = path; currentPath = path;
} }
// Make sure the path is valid
var root = isVirtual ? undefined : manager.find(path); var root = isVirtual ? undefined : manager.find(path);
if (manager.isSharedFolder(root)) { if (manager.isSharedFolder(root)) {
// ANON_SHARED_FOLDER // ANON_SHARED_FOLDER
@@ -3261,6 +3323,7 @@ define([
} }
if (!isSearch) { delete APP.Search.oldLocation; } if (!isSearch) { delete APP.Search.oldLocation; }
// Display the tree and build the content
APP.resetTree(); APP.resetTree();
if (displayedCategories.indexOf(SEARCH) !== -1 && $tree.find('#cp-app-drive-tree-search-input').length) { if (displayedCategories.indexOf(SEARCH) !== -1 && $tree.find('#cp-app-drive-tree-search-input').length) {
// in history mode we want to focus the version number input // in history mode we want to focus the version number input
@@ -3293,9 +3356,24 @@ define([
var $list = $('<ul>').appendTo($dirContent); var $list = $('<ul>').appendTo($dirContent);
// NewButton can be undefined if we're in read only mode
createNewButton(isInRoot, $toolbar.find('.cp-app-drive-toolbar-leftside'));
var sfId = manager.isInSharedFolder(currentPath); var sfId = manager.isInSharedFolder(currentPath);
var readOnlyFolder = false;
if (APP.readOnly) {
// Read-only drive (team?)
$readOnly.show();
} else if (folders[sfId] && folders[sfId].readOnly) {
// If readonly shared folder...
$readOnly.show();
readOnlyFolder = true;
} else {
$readOnly.hide();
}
$content.data('readOnlyFolder', readOnlyFolder);
// NewButton can be undefined if we're in read only mode
if (!readOnlyFolder) {
createNewButton(isInRoot, $toolbar.find('.cp-app-drive-toolbar-leftside'));
}
if (sfId) { if (sfId) {
var sfData = manager.getSharedFolderData(sfId); var sfData = manager.getSharedFolderData(sfId);
var parsed = Hash.parsePadUrl(sfData.href); var parsed = Hash.parsePadUrl(sfData.href);
@@ -3305,6 +3383,7 @@ define([
sframeChan.event('EV_DRIVE_SET_HASH', ''); sframeChan.event('EV_DRIVE_SET_HASH', '');
} }
createTitle($toolbar.find('.cp-app-drive-path'), path); createTitle($toolbar.find('.cp-app-drive-path'), path);
if (APP.mobile()) { if (APP.mobile()) {
@@ -3515,6 +3594,7 @@ define([
var newPath = path.slice(); var newPath = path.slice();
newPath.push(key); newPath.push(key);
var isSharedFolder = manager.isSharedFolder(root[key]); var isSharedFolder = manager.isSharedFolder(root[key]);
var sfId = manager.isInSharedFolder(newPath) || (isSharedFolder && root[key]);
var $icon, isCurrentFolder, subfolder; var $icon, isCurrentFolder, subfolder;
if (isSharedFolder) { if (isSharedFolder) {
var fId = root[key]; var fId = root[key];
@@ -3536,13 +3616,19 @@ define([
(isCurrentFolder ? $folderOpenedEmptyIcon : $folderEmptyIcon) : (isCurrentFolder ? $folderOpenedEmptyIcon : $folderEmptyIcon) :
(isCurrentFolder ? $folderOpenedIcon : $folderIcon); (isCurrentFolder ? $folderOpenedIcon : $folderIcon);
} }
var $element = createTreeElement(key, $icon.clone(), newPath, true, true, subfolder, isCurrentFolder, isSharedFolder); var f = folders[sfId];
var editable = !(f && f.readOnly);
var $element = createTreeElement(key, $icon.clone(), newPath, true, editable,
subfolder, isCurrentFolder, isSharedFolder);
$element.appendTo($list); $element.appendTo($list);
$element.find('>.cp-app-drive-element-row').contextmenu(openContextMenu('tree')); $element.find('>.cp-app-drive-element-row').contextmenu(openContextMenu('tree'));
if (isSharedFolder) { if (isSharedFolder) {
$element.find('>.cp-app-drive-element-row') $element.find('>.cp-app-drive-element-row')
.addClass('cp-app-drive-element-sharedf'); .addClass('cp-app-drive-element-sharedf');
} }
if (sfId && !editable) {
$element.attr('data-ro', true);
}
createTree($element, newPath); createTree($element, newPath);
}); });
}; };
@@ -3743,9 +3829,10 @@ define([
} }
if (manager.isSharedFolder(el)) { if (manager.isSharedFolder(el)) {
delete data.roHref; var ro = folders[el] && folders[el].version >= 2;
if (!ro) { delete data.roHref; }
//data.noPassword = true; //data.noPassword = true;
data.noEditPassword = true; //data.noEditPassword = true;
data.noExpiration = true; data.noExpiration = true;
// this is here to allow users to check the channel id of a shared folder // this is here to allow users to check the channel id of a shared folder
// we should remove it at some point // we should remove it at some point
@@ -3968,25 +4055,7 @@ define([
var teams = common.getMetadataMgr().getPrivateData().teams; var teams = common.getMetadataMgr().getPrivateData().teams;
var _wide = Object.keys(friends).length || Object.keys(teams).length; var _wide = Object.keys(friends).length || Object.keys(teams).length;
if (manager.isSharedFolder(el)) { if (manager.isFolder(el) && !manager.isSharedFolder(el)) { // Folder
data = manager.getSharedFolderData(el);
parsed = Hash.parsePadUrl(data.href);
modal = UIElements.createSFShareModal({
teamId: APP.team,
origin: APP.origin,
pathname: "/drive/",
friends: friends,
title: data.title,
common: common,
password: data.password,
hashes: {
editHash: parsed.hash
}
});
return void UI.openCustomModal(modal, {
wide: _wide
});
} else if (manager.isFolder(el)) { // Folder
// if folder is inside SF // if folder is inside SF
return UI.warn('ERROR: Temporarily disabled'); // XXX CONVERT return UI.warn('ERROR: Temporarily disabled'); // XXX CONVERT
/*if (manager.isInSharedFolder(paths[0].path)) { /*if (manager.isInSharedFolder(paths[0].path)) {
@@ -4021,10 +4090,12 @@ define([
}); });
}*/ }*/
} else { // File } else { // File
data = manager.getFileData(el); var sf = manager.isSharedFolder(el);
parsed = Hash.parsePadUrl(data.href); data = sf ? manager.getSharedFolderData(el) : manager.getFileData(el);
parsed = (data.href && data.href.indexOf('#') !== -1) ? Hash.parsePadUrl(data.href) : {};
var roParsed = Hash.parsePadUrl(data.roHref); var roParsed = Hash.parsePadUrl(data.roHref);
var padType = parsed.type || roParsed.type; var padType = parsed.type || roParsed.type;
var ro = !sf || (folders[el] && folders[el].version >= 2);
var padData = { var padData = {
teamId: APP.team, teamId: APP.team,
origin: APP.origin, origin: APP.origin,
@@ -4033,7 +4104,7 @@ define([
password: data.password, password: data.password,
hashes: { hashes: {
editHash: parsed.hash, editHash: parsed.hash,
viewHash: roParsed.hash, viewHash: ro && roParsed.hash,
fileHash: parsed.hash fileHash: parsed.hash
}, },
fileData: { fileData: {
@@ -4042,6 +4113,7 @@ define([
}, },
isTemplate: paths[0].path[0] === 'template', isTemplate: paths[0].path[0] === 'template',
title: data.title, title: data.title,
sharedFolder: sf,
common: common common: common
}; };
modal = padType === 'file' ? UIElements.createFileShareModal(padData) modal = padType === 'file' ? UIElements.createFileShareModal(padData)
@@ -4403,6 +4475,7 @@ define([
refresh(); refresh();
UI.removeLoadingScreen(); UI.removeLoadingScreen();
/*
if (!APP.team) { if (!APP.team) {
sframeChan.query('Q_DRIVE_GETDELETED', null, function (err, data) { sframeChan.query('Q_DRIVE_GETDELETED', null, function (err, data) {
var ids = manager.findChannels(data); var ids = manager.findChannels(data);
@@ -4417,6 +4490,79 @@ define([
UI.log(Messages._getKey('fm_deletedPads', [titles.join(', ')])); 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 = '<b>'+ (data.lastTitle || Messages.fm_newFolder) +'</b>';
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" && APP.editable) {
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 { return {
refresh: refresh, refresh: refresh,

View File

@@ -52,7 +52,8 @@ define([
var cancel = function () { var cancel = function () {
cancelled = true; cancelled = true;
}; };
var parsed = Hash.parsePadUrl(fData.href || fData.roHref); var href = (fData.href && fData.href.indexOf('#') !== -1) ? fData.href : fData.roHref;
var parsed = Hash.parsePadUrl(href);
var hash = parsed.hash; var hash = parsed.hash;
var name = fData.filename || fData.title; var name = fData.filename || fData.title;
var secret = Hash.getSecrets('file', hash, fData.password); var secret = Hash.getSecrets('file', hash, fData.password);
@@ -88,7 +89,8 @@ define([
cancelled = true; cancelled = true;
}; };
var parsed = Hash.parsePadUrl(pData.href || pData.roHref); var href = (pData.href && pData.href.indexOf('#') !== -1) ? pData.href : pData.roHref;
var parsed = Hash.parsePadUrl(href);
var name = pData.filename || pData.title; var name = pData.filename || pData.title;
var opts = { var opts = {
password: pData.password password: pData.password
@@ -137,7 +139,8 @@ define([
}); });
} }
var parsed = Hash.parsePadUrl(fData.href || fData.roHref); var href = (fData.href && fData.href.indexOf('#') !== -1) ? fData.href : fData.roHref;
var parsed = Hash.parsePadUrl(href);
if (['pad', 'file'].indexOf(parsed.hashData.type) === -1) { return; } if (['pad', 'file'].indexOf(parsed.hashData.type) === -1) { return; }
// waitFor is used to make sure all the pads and files are process before downloading the zip. // waitFor is used to make sure all the pads and files are process before downloading the zip.

View File

@@ -27,6 +27,7 @@ define([
if (parsed) { if (parsed) {
var proxy = proxyData.proxy; var proxy = proxyData.proxy;
var oldFo = FO.init(parsed.drive, { var oldFo = FO.init(parsed.drive, {
readOnly: false,
loggedIn: true, loggedIn: true,
outer: true outer: true
}); });
@@ -38,7 +39,7 @@ define([
var data = oldFo.getFileData(id); var data = oldFo.getFileData(id);
var channel = data.channel; var channel = data.channel;
var datas = manager.findChannel(channel, true); var datas = manager.findChannel(channel);
// Do not migrate a pad if we already have it, it would create a duplicate // Do not migrate a pad if we already have it, it would create a duplicate
// in the drive // in the drive
if (datas.length !== 0) { if (datas.length !== 0) {
@@ -49,7 +50,9 @@ define([
// We want to merge an edit pad: check if we have the same channel // We want to merge an edit pad: check if we have the same channel
// but read-only and upgrade it in that case // but read-only and upgrade it in that case
datas.forEach(function (pad) { datas.forEach(function (pad) {
if (pad.data && !pad.data.href) { pad.data.href = data.href; } if (pad.data && !pad.data.href) {
pad.userObject.setHref(channel, null, data.href);
}
}); });
return; return;
} }

View File

@@ -43,7 +43,7 @@ define([
MessengerUI.create = function ($container, common, toolbar) { MessengerUI.create = function ($container, common, toolbar) {
var metadataMgr = common.getMetadataMgr(); var metadataMgr = common.getMetadataMgr();
var origin = metadataMgr.getPrivateData().origin; var origin = metadataMgr.getPrivateData().origin;
var readOnly = metadataMgr.getPrivateData().readOnly; var readOnly = metadataMgr.getPrivateData().readOnly || toolbar.readOnly;
var isApp = typeof(toolbar) !== "undefined"; var isApp = typeof(toolbar) !== "undefined";

View File

@@ -145,10 +145,14 @@ define([
n = n.nThen(function (w) { n = n.nThen(function (w) {
setTimeout(w(function () { setTimeout(w(function () {
el = data[k]; el = data[k];
if (!el.href || (el.roHref && false)) { if (!el.href) {
// Already migrated // Already migrated
return void progress(7, Math.round(100*i/padsLength)); return void progress(7, Math.round(100*i/padsLength));
} }
if (el.href.indexOf('#') === -1) {
// Encrypted href: already migrated
return void progress(7, Math.round(100*i/padsLength));
}
parsed = Hash.parsePadUrl(el.href); parsed = Hash.parsePadUrl(el.href);
if (parsed.hashData.type !== "pad") { if (parsed.hashData.type !== "pad") {
// No read-only mode for files // No read-only mode for files

View File

@@ -95,9 +95,7 @@ define([
if (msg.content.isTemplate) { if (msg.content.isTemplate) {
common.sessionStorage.put(Constants.newPadPathKey, ['template'], waitFor()); 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 () { }).nThen(function () {
todo(); todo();
}); });

View File

@@ -74,7 +74,6 @@ define([
}).nThen(function () { cb(); }); }).nThen(function () { cb(); });
}; };
// OKTEAM
Store.get = function (clientId, data, cb) { Store.get = function (clientId, data, cb) {
var s = getStore(data.teamId); var s = getStore(data.teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); } if (!s) { return void cb({ error: 'ENOTFOUND' }); }
@@ -454,7 +453,7 @@ define([
Store.isNewChannel = function (clientId, data, cb) { Store.isNewChannel = function (clientId, data, cb) {
if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); } 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) { store.anon_rpc.send("IS_NEW_CHANNEL", channelId, function (e, response) {
if (e) { return void cb({error: e}); } if (e) { return void cb({error: e}); }
if (response && response.length && typeof(response[0]) === 'boolean') { if (response && response.length && typeof(response[0]) === 'boolean') {
@@ -551,9 +550,9 @@ define([
}; };
// Get the metadata for sframe-common-outer // Get the metadata for sframe-common-outer
Store.getMetadata = function (clientId, data, cb) { Store.getMetadata = function (clientId, app, cb) {
var disableThumbnails = Util.find(store.proxy, ['settings', 'general', 'disableThumbnails']); var disableThumbnails = Util.find(store.proxy, ['settings', 'general', 'disableThumbnails']);
var teams = (store.modules['team'] && store.modules['team'].getTeamsData()) || {}; var teams = (store.modules['team'] && store.modules['team'].getTeamsData(app)) || {};
var metadata = { var metadata = {
// "user" is shared with everybody via the userlist // "user" is shared with everybody via the userlist
user: { user: {
@@ -987,17 +986,23 @@ define([
}; };
Store.moveToTrash = function (clientId, data, cb) { Store.moveToTrash = function (clientId, data, cb) {
var href = Hash.getRelativeHref(data.href); var href = Hash.getRelativeHref(data.href);
var allErrors = true;
nThen(function (waitFor) { nThen(function (waitFor) {
getAllStores().forEach(function (s) { getAllStores().forEach(function (s) {
var deleted = s.userObject.forget(href); var deleted = s.userObject.forget(href);
if (!deleted) { return; } if (!deleted) { return; }
allErrors = false;
var send = s.id ? s.sendEvent : sendDriveEvent; var send = s.id ? s.sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', { send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA] path: ['drive', UserObject.FILES_DATA]
}, clientId); }, clientId);
onSync(s.id, waitFor()); onSync(s.id, waitFor());
}); });
}).nThen(cb); }).nThen(function () {
cb({
error: allErrors ? 'FORBIDDEN' : undefined
});
});
}; };
Store.setPadTitle = function (clientId, data, cb) { Store.setPadTitle = function (clientId, data, cb) {
var title = data.title; var title = data.title;
@@ -1045,7 +1050,7 @@ define([
if (data.teamId && s.id !== data.teamId) { return; } if (data.teamId && s.id !== data.teamId) { return; }
if (storeLocally && s.id) { return; } if (storeLocally && s.id) { return; }
var res = s.manager.findChannel(channel); var res = s.manager.findChannel(channel, true);
if (res.length) { if (res.length) {
sendTo.push(s.id); sendTo.push(s.id);
} }
@@ -1081,7 +1086,7 @@ define([
// If all of the weaker ones were in the trash, add the stronger to ROOT // If all of the weaker ones were in the trash, add the stronger to ROOT
obj.userObject.restoreHref(href); obj.userObject.restoreHref(href);
} }
pad.href = href; obj.userObject.setHref(channel, null, href);
}); });
// Pads owned by us ("us" can be a user or a team) that are not in our "main" drive // Pads owned by us ("us" can be a user or a team) that are not in our "main" drive
@@ -1266,21 +1271,17 @@ define([
// Get hashes for the share button // Get hashes for the share button
// If we can find a stronger hash // If we can find a stronger hash
Store.getStrongerHash = function (clientId, data, cb) { Store.getStrongerHash = function (clientId, data, _cb) {
var found = getAllStores().some(function (s) { var cb = Util.once(_cb);
var allPads = Util.find(s.proxy, ['drive', 'filesData']) || {};
// If we have a stronger version in drive, add it and add a redirect button var found = getAllStores().some(function (s) {
var stronger = Hash.findStronger(data.href, data.channel, allPads); var stronger = s.manager.getEditHash(data.channel);
if (stronger) { if (stronger) {
var parsed2 = Hash.parsePadUrl(stronger.href); cb(stronger);
cb(parsed2.hash);
return true; return true;
} }
}); });
if (!found) { if (!found) { cb(); }
cb();
}
}; };
// Universal // Universal
@@ -1474,7 +1475,7 @@ define([
onMetadataUpdate: function (metadata) { onMetadataUpdate: function (metadata) {
channel.data = metadata || {}; channel.data = metadata || {};
getAllStores().forEach(function (s) { getAllStores().forEach(function (s) {
var allData = s.manager.findChannel(data.channel); var allData = s.manager.findChannel(data.channel, true);
allData.forEach(function (obj) { allData.forEach(function (obj) {
obj.data.owners = metadata.owners; obj.data.owners = metadata.owners;
obj.data.atime = +new Date(); obj.data.atime = +new Date();
@@ -1531,8 +1532,7 @@ define([
Store.leavePad = function (clientId, data, cb) { Store.leavePad = function (clientId, data, cb) {
var channel = channels[data.channel]; var channel = channels[data.channel];
if (!channel || !channel.cpNf) { return void cb ({error: 'EINVAL'}); } if (!channel || !channel.cpNf) { return void cb ({error: 'EINVAL'}); }
channel.cpNf.stop(); Store.dropChannel(data.channel);
delete channels[data.channel];
cb(); cb();
}; };
Store.sendPadMsg = function (clientId, data, cb) { Store.sendPadMsg = function (clientId, data, cb) {
@@ -1547,6 +1547,20 @@ define([
channel.sendMessage(msg, clientId, cb); channel.sendMessage(msg, clientId, cb);
}; };
// Unpin and pin the new channel in all team when changing a pad password
Store.changePadPasswordPin = function (clientId, data, cb) {
var oldChannel = data.oldChannel;
var channel = data.channel;
nThen(function (waitFor) {
getAllStores().forEach(function (s) {
var allData = s.manager.findChannel(channel);
if (!allData.length) { return; }
s.rpc.unpin([oldChannel], waitFor());
s.rpc.pin([channel], waitFor());
});
}).nThen(cb);
};
// requestPadAccess is used to check if we have a way to contact the owner // requestPadAccess is used to check if we have a way to contact the owner
// of the pad AND to send the request if we want // of the pad AND to send the request if we want
// data.send === false ==> check if we can contact them // data.send === false ==> check if we can contact them
@@ -1640,7 +1654,7 @@ define([
// Update owners and expire time in the drive // Update owners and expire time in the drive
getAllStores().forEach(function (s) { getAllStores().forEach(function (s) {
var allData = s.manager.findChannel(data.channel); var allData = s.manager.findChannel(data.channel, true);
var changed = false; var changed = false;
allData.forEach(function (obj) { allData.forEach(function (obj) {
if (Sortify(obj.data.owners) !== Sortify(metadata.owners)) { if (Sortify(obj.data.owners) !== Sortify(metadata.owners)) {
@@ -1778,21 +1792,24 @@ define([
} }
}; };
}; };
Store.loadSharedFolder = function (teamId, id, data, cb) { Store.loadSharedFolder = function (teamId, id, data, cb, isNew) {
var s = getStore(teamId); var s = getStore(teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); } if (!s) { return void cb({ error: 'ENOTFOUND' }); }
var rt = SF.load({ SF.load({
isNew: isNew,
network: store.network, network: store.network,
store: s store: s,
isNewChannel: Store.isNewChannel
}, id, data, cb); }, id, data, cb);
return rt;
}; };
var loadSharedFolder = function (id, data, cb) { var loadSharedFolder = function (id, data, cb, isNew) {
Store.loadSharedFolder(null, id, data, cb); Store.loadSharedFolder(null, id, data, cb, isNew);
}; };
Store.loadSharedFolderAnon = function (clientId, data, cb) { Store.loadSharedFolderAnon = function (clientId, data, cb) {
Store.loadSharedFolder(null, data.id, data.data, function () { Store.loadSharedFolder(null, data.id, data.data, function (rt) {
cb(); cb({
error: rt ? undefined : 'EDELETED'
});
}); });
}; };
Store.addSharedFolder = function (clientId, data, cb) { Store.addSharedFolder = function (clientId, data, cb) {
@@ -1805,6 +1822,9 @@ define([
cb(id); cb(id);
}); });
}; };
Store.updateSharedFolderPassword = function (clientId, data, cb) {
SF.updatePassword(Store, data, store.network, cb);
};
// Drive // Drive
Store.userObjectCommand = function (clientId, cmdData, cb) { Store.userObjectCommand = function (clientId, cmdData, cb) {
@@ -1830,7 +1850,7 @@ define([
// Clients management // Clients management
var driveEventClients = []; var driveEventClients = [];
var dropChannel = function (chanId) { var dropChannel = Store.dropChannel = function (chanId) {
try { try {
store.messenger.leavePad(chanId); store.messenger.leavePad(chanId);
} catch (e) { console.error(e); } } catch (e) { console.error(e); }
@@ -1889,6 +1909,27 @@ define([
}); });
}; };
registerProxyEvents = function (proxy, fId) { 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(Store, {
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) { proxy.on('change', [], function (o, n, p) {
if (fId) { if (fId) {
// Pin the new pads // Pin the new pads
@@ -2022,6 +2063,15 @@ define([
/////////////////////// Init ///////////////////////////////////// /////////////////////// Init /////////////////////////////////////
////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////
Store.refreshDriveUI = function () {
getAllStores().forEach(function (_s) {
var send = _s.id ? _s.sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
});
});
};
var onReady = function (clientId, returned, cb) { var onReady = function (clientId, returned, cb) {
var proxy = store.proxy; var proxy = store.proxy;
var unpin = function (data, cb) { var unpin = function (data, cb) {
@@ -2039,7 +2089,8 @@ define([
pin: pin, pin: pin,
unpin: unpin, unpin: unpin,
loadSharedFolder: loadSharedFolder, loadSharedFolder: loadSharedFolder,
settings: proxy.settings settings: proxy.settings,
Store: Store
}, { }, {
outer: true, outer: true,
removeOwnedChannel: function (channel, cb) { Store.removeOwnedChannel('', channel, cb); }, removeOwnedChannel: function (channel, cb) { Store.removeOwnedChannel('', channel, cb); },
@@ -2048,7 +2099,8 @@ define([
log: function (msg) { log: function (msg) {
// broadcast to all drive apps // broadcast to all drive apps
sendDriveEvent("DRIVE_LOG", msg); sendDriveEvent("DRIVE_LOG", msg);
} },
rt: store.realtime
}); });
var userObject = store.userObject = manager.user.userObject; var userObject = store.userObject = manager.user.userObject;
addSharedFolderHandler(); addSharedFolderHandler();

View File

@@ -249,13 +249,13 @@ define([
if (msg.author !== content.user.curvePublic) { return void cb(true); } if (msg.author !== content.user.curvePublic) { return void cb(true); }
var channel = content.channel; var channel = content.channel;
var res = ctx.store.manager.findChannel(channel); var res = ctx.store.manager.findChannel(channel, true);
var title; var title;
res.forEach(function (obj) { res.forEach(function (obj) {
if (obj.data && !obj.data.href) { if (obj.data && !obj.data.href) {
if (!title) { title = obj.data.filename || obj.data.title; } if (!title) { title = obj.data.filename || obj.data.title; }
obj.data.href = content.href; obj.userObject.setHref(channel, null, content.href);
} }
}); });
@@ -415,6 +415,40 @@ define([
cb(false); cb(false);
}; };
handlers['TEAM_EDIT_RIGHTS'] = function (ctx, box, data, cb) {
var msg = data.msg;
var content = msg.content;
if (msg.author !== content.user.curvePublic) { return void cb(true); }
if (!content.teamData) {
console.log('Remove invalid notification');
return void cb(true);
}
// Make sure we are a member of this team
var myTeams = Util.find(ctx, ['store', 'proxy', 'teams']) || {};
var teamId;
var team;
Object.keys(myTeams).some(function (k) {
var _team = myTeams[k];
if (_team.channel === content.teamData.channel) {
teamId = k;
team = _team;
return true;
}
});
if (!teamId) { return void cb(true); }
try {
var module = ctx.store.modules['team'];
// changeMyRights returns true if we can't change our rights
module.changeMyRights(teamId, content.state, content.teamData);
} catch (e) { console.error(e); }
cb(true);
};
return { return {
add: function (ctx, box, data, cb) { add: function (ctx, box, data, cb) {

View File

@@ -814,6 +814,7 @@ define([
var cb = Util.once(Util.mkAsync(function () { var cb = Util.once(Util.mkAsync(function () {
ctx.emit('TEAMCHAT_READY', chanId, [clientId]); ctx.emit('TEAMCHAT_READY', chanId, [clientId]);
_cb({ _cb({
readOnly: typeof(secret.keys) === "object" && !secret.keys.validateKey,
channel: chanId channel: chanId
}); });
})); }));

View File

@@ -14,7 +14,7 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
user0CurveKey: { user0CurveKey: {
notifications: "", // required notifications: "", // required
displayName: "", // required displayName: "", // required
role: "OWNER|ADMIN|MEMBER", // MEMBER if not specified role: "OWNER|ADMIN|MEMBER|VIEWER", // VIEWER if not specified
profile: "", profile: "",
title: "" title: ""
}, },
@@ -53,7 +53,7 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
}; };
var isValidRole = function (role) { var isValidRole = function (role) {
return ['OWNER', 'ADMIN', 'MEMBER'].indexOf(role) !== -1; return ['OWNER', 'ADMIN', 'MEMBER', 'VIEWER'].indexOf(role) !== -1;
}; };
var canAddRole = function (author, role, members) { var canAddRole = function (author, role, members) {
@@ -65,8 +65,8 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
// owners can add any valid role they want // owners can add any valid role they want
if (authorRole === 'OWNER') { return true; } if (authorRole === 'OWNER') { return true; }
// admins can add other admins or members // admins can add other admins or members or viewers
if (authorRole === "ADMIN") { return ['ADMIN', 'MEMBER'].indexOf(role) !== -1; } if (authorRole === "ADMIN") { return ['ADMIN', 'MEMBER', 'VIEWER'].indexOf(role) !== -1; }
// (MEMBER, other) can't add anyone of any role // (MEMBER, other) can't add anyone of any role
return false; return false;
}; };
@@ -105,7 +105,7 @@ var factory = function (Util, Hash, CPNetflux, Sortify, nThen, Crypto) {
// owners can remove anyone they want // owners can remove anyone they want
if (authorRole === 'OWNER') { return true; } if (authorRole === 'OWNER') { return true; }
// admins can remove other admins or members // admins can remove other admins or members
if (authorRole === "ADMIN") { return ["ADMIN", "MEMBER"].indexOf(role) !== -1; } if (authorRole === "ADMIN") { return ["ADMIN", "MEMBER", "VIEWER"].indexOf(role) !== -1; }
// MEMBERS and non-members cannot remove anyone of any role // MEMBERS and non-members cannot remove anyone of any role
return false; return false;
}; };

View File

@@ -19,81 +19,218 @@ define([
var allSharedFolders = {}; var allSharedFolders = {};
SF.load = function (config, id, data, cb) { // No version: visible edit
// Version 2: encrypted edit links
SF.checkMigration = function (secondaryKey, proxy, uo, cb) {
var drive = proxy.drive || proxy;
// View access: can't migrate
if (!secondaryKey) { return void cb(); }
// Already migrated: nothing to do
if (drive.version >= 2) { return void cb(); }
// Not yet migrating: migrate
if (!drive.migrateRo) { return void uo.migrateReadOnly(cb); }
// Already migrating: wait for the end...
var done = false;
var to;
var it = setInterval(function () {
if (drive.version >= 2) {
done = true;
clearTimeout(to);
clearInterval(it);
return void cb();
}
}, 100);
to = setTimeout(function () {
clearInterval(it);
uo.migrateReadOnly(function () {
done = true;
cb();
});
}, 20000);
var path = proxy.drive ? ['drive', 'version'] : ['version'];
proxy.on('change', path, function () {
if (done) { return; }
if (drive.version >= 2) {
done = true;
clearTimeout(to);
clearInterval(it);
cb();
}
});
};
// SFMIGRATION: only needed if we want a manual migration from the share modal...
SF.migrate = function (channel) {
var sf = allSharedFolders[channel];
if (!sf) { return; }
var clients = sf.teams;
if (!Array.isArray(clients) || !clients.length) { return; }
var c = clients[0];
// No secondaryKey? ==> already migrated ==> abort
if (!c.secondaryKey) { return; }
var f = Util.find(c, ['store', 'manager', 'folders', c.id]);
// Can't find the folder: abort
if (!f) { return; }
// Already migrated: abort
if (!f.proxy || f.proxy.version) { return; }
f.userObject.migrateReadOnly(function () {
clients.forEach(function (obj) {
var uo = Util.find(obj, ['store', 'manager', 'folders', obj.id, 'userObject']);
uo.setReadOnly(false, obj.secondarykey);
});
});
};
SF.load = function (config, id, data, _cb) {
var cb = Util.once(_cb);
var network = config.network; var network = config.network;
var store = config.store; 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 handler = store.handleSharedFolder;
var parsed = Hash.parsePadUrl(data.href); var href = store.manager.user.userObject.getHref(data);
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets('drive', parsed.hash, data.password); var secret = Hash.getSecrets('drive', parsed.hash, data.password);
// If we don't have valid keys, abort and remove the proxy to make sure
var sf = allSharedFolders[secret.channel]; // we don't block the drive permanently
if (sf && sf.ready && sf.rt) { if (!secret.keys) {
// The shared folder is already loaded, return its data store.manager.deprecateProxy(id);
setTimeout(function () { return void cb(null);
var leave = function () { SF.leave(secret.channel, teamId); };
store.manager.addProxy(id, sf.rt.proxy, leave);
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;
} }
var secondaryKey = secret.keys.secondaryKey;
sf = allSharedFolders[secret.channel] = { // If we try to load an existing shared folder (isNew === false) but this folder
queue: [{ // doesn't exist in the database, abort and cb
cb: cb, nThen(function (waitFor) {
store: store, isNewChannel(null, { channel: secret.channel }, waitFor(function (obj) {
id: id if (obj.isNew && !isNew) {
}], store.manager.deprecateProxy(id, secret.channel);
team: [store.id || -1] waitFor.abort();
}; return void cb(null);
}
var owners = data.owners; }));
var listmapConfig = { }).nThen(function () {
data: {}, var sf = allSharedFolders[secret.channel];
channel: secret.channel, if (sf && sf.readOnly && secondaryKey) {
readOnly: false, // We were in readOnly mode and now we know the edit keys!
crypto: Crypto.createEncryptor(secret.keys), SF.upgrade(secret.channel, secret);
userName: 'sharedFolder',
logLevel: 1,
ChainPad: ChainPad,
classic: true,
network: network,
metadata: {
validateKey: secret.keys.validateKey || undefined,
owners: owners
} }
}; if (sf && sf.ready && sf.rt) {
var rt = sf.rt = Listmap.create(listmapConfig); // The shared folder is already loaded, return its data
rt.proxy.on('ready', function (info) { setTimeout(function () {
if (!sf.queue) { var leave = function () { SF.leave(secret.channel, teamId); };
/*
var uo = store.manager.addProxy(id, sf.rt, leave, secondaryKey);
// NOTE: Shared folder migration, disable for now
SF.checkMigration(secondaryKey, sf.rt.proxy, uo, function () {
cb(sf.rt, sf.metadata);
});
*/
store.manager.addProxy(id, sf.rt, leave, secondaryKey);
cb(sf.rt, sf.metadata);
});
sf.teams.push({
cb: cb,
store: store,
id: id
});
if (handler) { handler(id, sf.rt); }
return; return;
} }
sf.queue.forEach(function (obj) { if (sf && !sf.ready && sf.rt) {
var leave = function () { SF.leave(secret.channel, teamId); }; // The shared folder is loading, add our callbacks to the queue
obj.store.manager.addProxy(obj.id, rt.proxy, leave); sf.teams.push({
obj.cb(rt, info.metadata); cb: cb,
store: store,
secondaryKey: secondaryKey,
id: id
});
if (handler) { handler(id, sf.rt); }
return;
}
sf = allSharedFolders[secret.channel] = {
teams: [{
cb: cb,
store: store,
secondaryKey: secondaryKey,
id: id
}],
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.teams) {
return;
}
sf.teams.forEach(function (obj) {
var leave = function () { SF.leave(secret.channel, teamId); };
/*
var uo = obj.store.manager.addProxy(obj.id, rt, leave, obj.secondaryKey);
// NOTE: Shared folder migration, disable for now
SF.checkMigration(secondaryKey, rt.proxy, uo, function () {
obj.cb(sf.rt, info.metadata);
});
*/
obj.store.manager.addProxy(obj.id, rt, leave, obj.secondaryKey);
obj.cb(sf.rt, info.metadata);
});
sf.metadata = info.metadata;
sf.ready = true;
}); });
sf.leave = info.leave; rt.proxy.on('error', function (info) {
sf.metadata = info.metadata; if (info && info.error) {
sf.ready = true; if (info.error === "EDELETED" ) {
delete sf.queue; try {
// Deprecate the shared folder from each team
// We can only hide it
sf.teams.forEach(function (obj) {
obj.store.manager.deprecateProxy(obj.id, secret.channel);
});
} catch (e) {}
delete allSharedFolders[secret.channel];
}
}
});
if (handler) { handler(id, rt); }
}); });
if (handler) { handler(id, rt); } };
return rt;
SF.upgrade = function (channel, secret) {
var sf = allSharedFolders[channel];
if (!sf || !sf.readOnly) { return; }
if (!sf.rt.setReadOnly) { return; }
if (!secret.keys || !secret.keys.editKeyStr) { return; }
var crypto = Crypto.createEncryptor(secret.keys);
sf.readOnly = false;
sf.rt.setReadOnly(false, crypto);
}; };
SF.leave = function (channel, teamId) { SF.leave = function (channel, teamId) {
@@ -101,8 +238,14 @@ define([
if (!sf) { return; } if (!sf) { return; }
var clients = sf.teams; var clients = sf.teams;
if (!Array.isArray(clients)) { return; } if (!Array.isArray(clients)) { return; }
var idx = clients.indexOf(teamId); var idx;
if (idx === -1) { return; } clients.some(function (obj, i) {
if (obj.store.id === teamId) {
idx = i;
return true;
}
});
if (typeof (idx) === "undefined") { return; }
// Remove the selected team // Remove the selected team
clients.splice(idx, 1); clients.splice(idx, 1);
@@ -113,6 +256,42 @@ define([
} }
}; };
// Update the password locally
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) {
try { sf.rt.stop(); } catch (e) {}
}
var nt = nThen;
sf.teams.forEach(function (obj) {
nt = nt(function (waitFor) {
var s = obj.store;
var sfId = obj.id;
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(function () {
cb();
});
};
/* loadSharedFolders /* loadSharedFolders
load all shared folder stored in a given drive load all shared folder stored in a given drive
- store: user or team main store - store: user or team main store
@@ -121,35 +300,13 @@ define([
*/ */
SF.loadSharedFolders = function (Store, network, store, userObject, waitFor) { SF.loadSharedFolders = function (Store, network, store, userObject, waitFor) {
var shared = Util.find(store.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {}; 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) { 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) { Object.keys(shared).forEach(function (id) {
var sf = shared[id]; var sf = shared[id];
SF.load({ SF.load({
network: network, network: network,
store: store store: store,
isNewChannel: Store.isNewChannel
}, id, sf, waitFor()); }, id, sf, waitFor());
}); });
}).nThen(waitFor()); }).nThen(waitFor());

View File

@@ -56,6 +56,7 @@ define([
ADD_SHARED_FOLDER: Store.addSharedFolder, ADD_SHARED_FOLDER: Store.addSharedFolder,
LOAD_SHARED_FOLDER: Store.loadSharedFolderAnon, LOAD_SHARED_FOLDER: Store.loadSharedFolderAnon,
RESTORE_SHARED_FOLDER: Store.restoreSharedFolder, RESTORE_SHARED_FOLDER: Store.restoreSharedFolder,
UPDATE_SHARED_FOLDER_PASSWORD: Store.updateSharedFolderPassword,
// Messaging // Messaging
ANSWER_FRIEND_REQUEST: Store.answerFriendRequest, ANSWER_FRIEND_REQUEST: Store.answerFriendRequest,
SEND_FRIEND_REQUEST: Store.sendFriendRequest, SEND_FRIEND_REQUEST: Store.sendFriendRequest,
@@ -78,6 +79,7 @@ define([
GIVE_PAD_ACCESS: Store.givePadAccess, GIVE_PAD_ACCESS: Store.givePadAccess,
GET_PAD_METADATA: Store.getPadMetadata, GET_PAD_METADATA: Store.getPadMetadata,
SET_PAD_METADATA: Store.setPadMetadata, SET_PAD_METADATA: Store.setPadMetadata,
CHANGE_PAD_PASSWORD_PIN: Store.changePadPasswordPin,
// Drive // Drive
DRIVE_USEROBJECT: Store.userObjectCommand, DRIVE_USEROBJECT: Store.userObjectCommand,
// Settings, // Settings,

View File

@@ -31,6 +31,33 @@ define([
var registerChangeEvents = function (ctx, team, proxy, fId) { var registerChangeEvents = function (ctx, team, proxy, fId) {
if (!team) { return; } 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);
// We've received a new password, we should update it locally
// NOTE: this is an async call because new password means new roHref!
// We need to wait for the new roHref in the proxy before calling the handlers
// because a read-only team will use it when connecting to the new channel
setTimeout(function () {
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) { proxy.on('change', [], function (o, n, p) {
if (fId) { if (fId) {
// Pin the new pads // Pin the new pads
@@ -82,6 +109,7 @@ define([
try { team.listmap.stop(); } catch (e) {} try { team.listmap.stop(); } catch (e) {}
try { team.roster.stop(); } catch (e) {} try { team.roster.stop(); } catch (e) {}
team.proxy = {}; team.proxy = {};
team.stopped = true;
delete ctx.teams[teamId]; delete ctx.teams[teamId];
delete ctx.store.proxy.teams[teamId]; delete ctx.store.proxy.teams[teamId];
ctx.emit('LEAVE_TEAM', teamId, team.clients); ctx.emit('LEAVE_TEAM', teamId, team.clients);
@@ -141,8 +169,10 @@ define([
roster: roster roster: roster
}; };
// Subscribe to events
if (cId) { team.clients.push(cId); } if (cId) { team.clients.push(cId); }
// Listen for roster changes
roster.on('change', function () { roster.on('change', function () {
var state = roster.getState(); var state = roster.getState();
var me = Util.find(ctx, ['store', 'proxy', 'curvePublic']); var me = Util.find(ctx, ['store', 'proxy', 'curvePublic']);
@@ -159,16 +189,19 @@ define([
rosterData.lastKnownHash = hash; rosterData.lastKnownHash = hash;
}); });
// Update metadata
var state = roster.getState(); var state = roster.getState();
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', id]); var teamData = Util.find(ctx, ['store', 'proxy', 'teams', id]);
if (teamData) { teamData.metadata = state.metadata; } if (teamData) { teamData.metadata = state.metadata; }
// Broadcast an event to all the tabs displaying this team
team.sendEvent = function (q, data, sender) { team.sendEvent = function (q, data, sender) {
ctx.emit(q, data, team.clients.filter(function (cId) { ctx.emit(q, data, team.clients.filter(function (cId) {
return cId !== sender; return cId !== sender;
})); }));
}; };
// Provide team chat keys to the messenger app
team.getChatData = function () { team.getChatData = function () {
var chatKeys = keys.chat || {}; var chatKeys = keys.chat || {};
var hash = chatKeys.edit || chatKeys.view; var hash = chatKeys.edit || chatKeys.view;
@@ -178,13 +211,15 @@ define([
teamId: id, teamId: id,
channel: secret.channel, channel: secret.channel,
secret: secret, secret: secret,
validateKey: secret.keys.validateKey validateKey: chatKeys.validateKey
}; };
}; };
var secret;
team.pin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); }; team.pin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
team.unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); }; team.unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
nThen(function (waitFor) { nThen(function (waitFor) {
// Init Team RPC
if (!keys.drive.edPrivate) { return; } if (!keys.drive.edPrivate) { return; }
initRpc(ctx, team, keys.drive, waitFor(function (err) { initRpc(ctx, team, keys.drive, waitFor(function (err) {
if (err) { return; } if (err) { return; }
@@ -208,14 +243,18 @@ define([
}; };
})); }));
}).nThen(function () { }).nThen(function () {
var loadSharedFolder = function (id, data, cb) { // Create the proxy manager
var loadSharedFolder = function (id, data, cb, isNew) {
SF.load({ SF.load({
isNew: isNew,
network: ctx.store.network, network: ctx.store.network,
store: team store: team,
}, id, data, function (id, rt) { isNewChannel: ctx.Store.isNewChannel
cb(id, rt); }, id, data, cb);
});
}; };
var teamData = ctx.store.proxy.teams[team.id];
var hash = teamData.hash || teamData.roHash;
secret = Hash.getSecrets('team', hash, teamData.password);
var manager = team.manager = ProxyManager.create(proxy.drive, { var manager = team.manager = ProxyManager.create(proxy.drive, {
onSync: function (cb) { ctx.Store.onSync(id, cb); }, onSync: function (cb) { ctx.Store.onSync(id, cb); },
edPublic: keys.drive.edPublic, edPublic: keys.drive.edPublic,
@@ -224,7 +263,8 @@ define([
loadSharedFolder: loadSharedFolder, loadSharedFolder: loadSharedFolder,
settings: { settings: {
drive: Util.find(ctx.store, ['proxy', 'settings', 'drive']) drive: Util.find(ctx.store, ['proxy', 'settings', 'drive'])
} },
Store: ctx.Store
}, { }, {
outer: true, outer: true,
removeOwnedChannel: function (channel, cb) { removeOwnedChannel: function (channel, cb) {
@@ -245,13 +285,19 @@ define([
log: function (msg) { log: function (msg) {
// broadcast to all drive apps // broadcast to all drive apps
team.sendEvent("DRIVE_LOG", msg); team.sendEvent("DRIVE_LOG", msg);
} },
rt: team.realtime,
editKey: secret.keys.secondaryKey,
readOnly: Boolean(!secret.keys.secondaryKey)
}); });
team.secondaryKey = secret && secret.keys.secondaryKey;
team.userObject = manager.user.userObject; team.userObject = manager.user.userObject;
team.userObject.fixFiles(); team.userObject.fixFiles();
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
// Load the shared folders
ctx.teams[id] = team; ctx.teams[id] = team;
registerChangeEvents(ctx, team, proxy); registerChangeEvents(ctx, team, proxy);
SF.checkMigration(team.secondaryKey, proxy, team.userObject, waitFor());
SF.loadSharedFolders(ctx.Store, ctx.store.network, team, team.userObject, waitFor); SF.loadSharedFolders(ctx.Store, ctx.store.network, team, team.userObject, waitFor);
}).nThen(function () { }).nThen(function () {
if (!team.rpc) { return; } if (!team.rpc) { return; }
@@ -288,10 +334,20 @@ define([
var openChannel = function (ctx, teamData, id, _cb) { var openChannel = function (ctx, teamData, id, _cb) {
var cb = Util.once(_cb); var cb = Util.once(_cb);
var secret = Hash.getSecrets('team', teamData.hash, teamData.password); var hash = teamData.hash || teamData.roHash;
var secret = Hash.getSecrets('team', hash, teamData.password);
var crypto = Crypto.createEncryptor(secret.keys); var crypto = Crypto.createEncryptor(secret.keys);
if (!teamData.roHash) {
teamData.roHash = Hash.getViewHashFromKeys(secret);
}
var keys = teamData.keys; var keys = teamData.keys;
if (!keys.chat.validateKey && keys.chat.edit) {
var chatSecret = Hash.getSecrets('chat', keys.chat.edit);
keys.chat.validateKey = chatSecret.keys.validateKey;
}
var roster; var roster;
var lm; var lm;
@@ -365,6 +421,7 @@ define([
}, waitFor(function (err, _roster) { }, waitFor(function (err, _roster) {
if (err) { if (err) {
waitFor.abort(); waitFor.abort();
console.error(err);
return void cb({error: 'ROSTER_ERROR'}); return void cb({error: 'ROSTER_ERROR'});
} }
roster = _roster; roster = _roster;
@@ -413,6 +470,7 @@ define([
var password = Hash.createChannelId(); var password = Hash.createChannelId();
var hash = Hash.createRandomHash('team', password); var hash = Hash.createRandomHash('team', password);
var secret = Hash.getSecrets('team', hash, password); var secret = Hash.getSecrets('team', hash, password);
var roHash = Hash.getViewHashFromKeys(secret);
var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey
var rosterSeed = Crypto.Team.createSeed(); var rosterSeed = Crypto.Team.createSeed();
@@ -502,6 +560,7 @@ define([
}; };
var lm = Listmap.create(config); var lm = Listmap.create(config);
var proxy = lm.proxy; var proxy = lm.proxy;
proxy.version = 2; // No migration needed
proxy.on('ready', function () { proxy.on('ready', function () {
// Store keys in our drive // Store keys in our drive
var keys = { var keys = {
@@ -512,6 +571,7 @@ define([
chat: { chat: {
edit: chatHashes.editHash, edit: chatHashes.editHash,
view: chatHashes.viewHash, view: chatHashes.viewHash,
validateKey: chatSecret.keys.validateKey,
channel: chatSecret.channel channel: chatSecret.channel
}, },
roster: { roster: {
@@ -524,6 +584,7 @@ define([
owner: true, owner: true,
channel: secret.channel, channel: secret.channel,
hash: hash, hash: hash,
roHash: roHash,
password: password, password: password,
keys: keys, keys: keys,
//members: membersHashes.editHash, //members: membersHashes.editHash,
@@ -659,7 +720,7 @@ define([
var joinTeam = function (ctx, data, cId, cb) { var joinTeam = function (ctx, data, cId, cb) {
var team = data.team; var team = data.team;
if (!team.hash || !team.channel || !team.password if (!(team.hash || team.roHash) || !team.channel || !team.password
|| !team.keys || !team.metadata) { return void cb({error: 'EINVAL'}); } || !team.keys || !team.metadata) { return void cb({error: 'EINVAL'}); }
var id = Util.createRandomInteger(); var id = Util.createRandomInteger();
ctx.store.proxy.teams[id] = team; ctx.store.proxy.teams[id] = team;
@@ -737,6 +798,25 @@ define([
cb(members); cb(members);
}; };
// Return folders with edit rights available to everybody (decrypted pad href)
var getEditableFolders = function (ctx, data, cId, cb) {
var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
var folders = team.manager.folders || {};
var ids = Object.keys(folders).filter(function (id) {
return !folders[id].proxy.version;
});
cb(ids.map(function (id) {
var uo = Util.find(team, ['user', 'userObject']);
return {
name: Util.find(folders, [id, 'proxy', 'metadata', 'title']),
path: uo ? uo.findFile(id)[0] : []
};
}));
};
var getTeamMetadata = function (ctx, data, cId, cb) { var getTeamMetadata = function (ctx, data, cId, cb) {
var teamId = data.teamId; var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); } if (!teamId) { return void cb({error: 'EINVAL'}); }
@@ -918,6 +998,97 @@ define([
}); });
}; };
var getInviteData = function (ctx, teamId, edit) {
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return {}; }
var data = Util.clone(teamData);
if (!edit) {
// Delete edit keys
delete data.hash;
delete data.keys.drive.edPrivate;
delete data.keys.chat.edit;
}
// Delete owner key
delete data.owner;
return data;
};
// Update my edit rights in listmap (only upgrade) and userObject (upgrade and downgrade)
// We also need to propagate the changes to the shared folders
var updateMyRights = function (ctx, teamId, hash) {
if (!teamId) { return true; }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return true; }
var team = ctx.teams[teamId];
var secret = Hash.getSecrets('team', hash || teamData.roHash, teamData.password);
// Upgrade the listmap if we can
SF.upgrade(teamData.channel, secret);
// Set the new readOnly value in userObject
if (team.userObject) {
team.userObject.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey);
}
// Upgrade the shared folders
var folders = Util.find(team, ['proxy', 'drive', 'sharedFolders']);
Object.keys(folders || {}).forEach(function (sfId) {
var data = team.manager.getSharedFolderData(sfId);
var parsed = Hash.parsePadUrl(data.href || data.roHref);
var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password);
SF.upgrade(secret.channel, secret);
var uo = Util.find(team, ['manager', 'folders', sfId, 'userObject']);
if (uo) {
uo.setReadOnly(!secret.keys.secondaryKey, secret.keys.secondaryKey);
}
});
ctx.updateMetadata();
ctx.emit('ROSTER_CHANGE_RIGHTS', teamId, team.clients);
};
var changeMyRights = function (ctx, teamId, state, data) {
if (!teamId) { return true; }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return true; }
var team = ctx.teams[teamId];
if (!team) { return true; }
if (teamData.channel !== data.channel || teamData.password !== data.password) { return true; }
if (state) {
teamData.hash = data.hash;
teamData.keys.drive.edPrivate = data.keys.drive.edPrivate;
teamData.keys.chat.edit = data.keys.chat.edit;
var secret = Hash.getSecrets('team', data.hash, teamData.password);
team.secondaryKey = secret && secret.keys.secondaryKey;
} else {
delete teamData.hash;
delete teamData.keys.drive.edPrivate;
delete teamData.keys.chat.edit;
delete team.secondaryKey;
}
updateMyRights(ctx, teamId, data.hash);
};
var changeEditRights = function (ctx, teamId, user, state, cb) {
if (!teamId) { return void cb({error: 'EINVAL'}); }
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return void cb ({error: 'ENOENT'}); }
var team = ctx.teams[teamId];
if (!team) { return void cb ({error: 'ENOENT'}); }
// Send mailbox to offer ownership
var myData = Messaging.createData(ctx.store.proxy, false);
ctx.store.mailbox.sendTo("TEAM_EDIT_RIGHTS", {
state: state,
teamData: getInviteData(ctx, teamId, state),
user: myData
}, {
channel: user.notifications,
curvePublic: user.curvePublic
}, cb);
};
var describeUser = function (ctx, data, cId, cb) { var describeUser = function (ctx, data, cId, cb) {
var teamId = data.teamId; var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); } if (!teamId) { return void cb({error: 'EINVAL'}); }
@@ -931,13 +1102,27 @@ define([
// It it is an ownership revocation, we have to set it in pad metadata first // It it is an ownership revocation, we have to set it in pad metadata first
if (user.role === "OWNER" && data.data.role !== "OWNER") { if (user.role === "OWNER" && data.data.role !== "OWNER") {
revokeOwnership(ctx, teamId, user, function (err) { revokeOwnership(ctx, teamId, user, function (err) {
if (!err) { return; } if (!err) { return void cb(); }
console.error(err); console.error(err);
return void cb({error: err}); return void cb({error: err});
}); });
return; return;
} }
// Viewer to editor
if (user.role === "VIEWER" && data.data.role !== "VIEWER") {
changeEditRights(ctx, teamId, user, true, function (err) {
return void cb({error: err});
});
}
// Editor to viewer
if (user.role !== "VIEWER" && data.data.role === "VIEWER") {
changeEditRights(ctx, teamId, user, false, function (err) {
return void cb({error: err});
});
}
var obj = {}; var obj = {};
obj[data.curvePublic] = data.data; obj[data.curvePublic] = data.data;
team.roster.describe(obj, function (err) { team.roster.describe(obj, function (err) {
@@ -946,15 +1131,6 @@ define([
}); });
}; };
// TODO send guest keys only in the future
var getInviteData = function (ctx, teamId) {
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', teamId]);
if (!teamData) { return {}; }
var data = Util.clone(teamData);
delete data.owner;
return data;
};
var inviteToTeam = function (ctx, data, cId, cb) { var inviteToTeam = function (ctx, data, cId, cb) {
var teamId = data.teamId; var teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); } if (!teamId) { return void cb({error: 'EINVAL'}); }
@@ -969,6 +1145,7 @@ define([
var obj = {}; var obj = {};
obj[user.curvePublic] = user; obj[user.curvePublic] = user;
obj[user.curvePublic].role = 'VIEWER';
team.roster.add(obj, function (err) { team.roster.add(obj, function (err) {
if (err && err !== 'NO_CHANGE') { return void cb({error: err}); } if (err && err !== 'NO_CHANGE') { return void cb({error: err}); }
ctx.store.mailbox.sendTo('INVITE_TO_TEAM', { ctx.store.mailbox.sendTo('INVITE_TO_TEAM', {
@@ -1094,9 +1271,21 @@ define([
if (err) { return; } if (err) { return; }
})); }));
// Listen for changes in our access rights (if another worker receives edit access)
ctx.store.proxy.on('change', ['teams'], function (o, n, p) {
if (p[2] !== 'hash') { return; }
updateMyRights(ctx, p[1], n);
});
ctx.store.proxy.on('remove', ['teams'], function (o, p) {
if (p[2] !== 'hash') { return; }
updateMyRights(ctx, p[1]);
});
Object.keys(teams).forEach(function (id) { Object.keys(teams).forEach(function (id) {
ctx.onReadyHandlers[id] = []; ctx.onReadyHandlers[id] = [];
openChannel(ctx, teams[id], id, waitFor(function () { openChannel(ctx, teams[id], id, waitFor(function (err) {
if (err) { return void console.error(err); }
console.debug('Team '+id+' ready'); console.debug('Team '+id+' ready');
})); }));
}); });
@@ -1104,15 +1293,25 @@ define([
team.getTeam = function (id) { team.getTeam = function (id) {
return ctx.teams[id]; return ctx.teams[id];
}; };
team.getTeamsData = function () { team.getTeamsData = function (app) {
var t = {}; var t = {};
var safe = false;
if (['drive', 'teams', 'settings'].indexOf(app) !== -1) { safe = true; }
Object.keys(teams).forEach(function (id) { Object.keys(teams).forEach(function (id) {
t[id] = { t[id] = {
owner: teams[id].owner, owner: teams[id].owner,
name: teams[id].metadata.name, name: teams[id].metadata.name,
edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']), edPublic: Util.find(teams[id], ['keys', 'drive', 'edPublic']),
avatar: Util.find(teams[id], ['metadata', 'avatar']) avatar: Util.find(teams[id], ['metadata', 'avatar']),
viewer: !Util.find(teams[id], ['keys', 'drive', 'edPrivate']),
}; };
if (safe && ctx.teams[id]) {
t[id].secondaryKey = ctx.teams[id].secondaryKey;
}
if (ctx.teams[id]) {
t[id].hasSecondaryKey = Boolean(ctx.teams[id].secondaryKey);
}
}); });
return t; return t;
}; };
@@ -1136,6 +1335,9 @@ define([
}); });
}; };
team.changeMyRights = function (id, edit, teamData) {
changeMyRights(ctx, id, edit, teamData);
};
team.updateMyData = function (data) { team.updateMyData = function (data) {
Object.keys(ctx.teams).forEach(function (id) { Object.keys(ctx.teams).forEach(function (id) {
var team = ctx.teams[id]; var team = ctx.teams[id];
@@ -1199,6 +1401,9 @@ define([
if (cmd === 'CREATE_TEAM') { if (cmd === 'CREATE_TEAM') {
return void createTeam(ctx, data, clientId, cb); return void createTeam(ctx, data, clientId, cb);
} }
if (cmd === 'GET_EDITABLE_FOLDERS') {
return void getEditableFolders(ctx, data, clientId, cb);
}
}; };
return team; return team;

View File

@@ -7,6 +7,82 @@ define([
var Nacl = window.nacl; var Nacl = window.nacl;
var module = {}; var module = {};
module.uploadU8 =function (common, data, cb) {
var teamId = data.teamId;
var u8 = data.u8;
var metadata = data.metadata;
var key = data.key;
var onError = data.onError || function () {};
var onPending = data.onPending || function () {};
var updateProgress = data.updateProgress || function () {};
var owned = data.owned;
var id = data.id;
var next = FileCrypto.encrypt(u8, metadata, key);
var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata);
var sendChunk = function (box, cb) {
var enc = Nacl.util.encodeBase64(box);
common.uploadChunk(teamId, enc, function (e, msg) {
cb(e, msg);
});
};
var actual = 0;
var again = function (err, box) {
if (err) { onError(err); }
if (box) {
actual += box.length;
var progressValue = (actual / estimate * 100);
progressValue = Math.min(progressValue, 100);
updateProgress(progressValue);
return void sendChunk(box, function (e) {
if (e) { return console.error(e); }
next(again);
});
}
if (actual !== estimate) {
console.error('Estimated size does not match actual size');
}
// if not box then done
common.uploadComplete(teamId, id, owned, function (e) {
if (e) { return void console.error(e); }
var uri = ['', 'blob', id.slice(0,2), id].join('/');
console.log("encrypted blob is now available as %s", uri);
cb();
});
};
common.uploadStatus(teamId, estimate, function (e, pending) {
if (e) {
console.error(e);
onError(e);
return;
}
if (pending) {
return void onPending(function () {
// if the user wants to cancel the pending upload to execute that one
common.uploadCancel(teamId, estimate, function (e) {
if (e) {
return void console.error(e);
}
next(again);
});
});
}
next(again);
});
};
module.upload = function (file, noStore, common, updateProgress, onComplete, onError, onPending) { module.upload = function (file, noStore, common, updateProgress, onComplete, onError, onPending) {
var u8 = file.blob; // This is not a blob but a uint8array var u8 = file.blob; // This is not a blob but a uint8array
var metadata = file.metadata; var metadata = file.metadata;
@@ -50,85 +126,36 @@ define([
metadata.owners = [edPublic]; metadata.owners = [edPublic];
})); }));
}).nThen(function () { }).nThen(function () {
var next = FileCrypto.encrypt(u8, metadata, key); module.uploadU8(common, {
teamId: teamId,
u8: u8,
metadata: metadata,
key: key,
id: id,
owned: owned,
onError: onError,
onPending: onPending,
updateProgress: updateProgress,
}, function () {
if (noStore) { return void onComplete(href); }
var estimate = FileCrypto.computeEncryptedSize(u8.length, metadata); var title = metadata.name;
var data = {
var sendChunk = function (box, cb) { teamId: teamId,
var enc = Nacl.util.encodeBase64(box); title: title || "",
common.uploadChunk(teamId, enc, function (e, msg) { href: href,
cb(e, msg); path: path,
password: password,
channel: id,
owners: metadata.owners,
forceSave: forceSave
};
common.setPadTitle(data, function (err) {
if (err) { return void console.error(err); }
onComplete(href);
common.setPadAttribute('fileType', metadata.type, null, href);
common.setPadAttribute('owners', metadata.owners, null, href);
}); });
};
var actual = 0;
var again = function (err, box) {
if (err) { throw new Error(err); }
if (box) {
actual += box.length;
var progressValue = (actual / estimate * 100);
progressValue = Math.min(progressValue, 100);
updateProgress(progressValue);
return void sendChunk(box, function (e) {
if (e) { return console.error(e); }
next(again);
});
}
if (actual !== estimate) {
console.error('Estimated size does not match actual size');
}
// if not box then done
common.uploadComplete(teamId, id, owned, function (e) {
if (e) { return void console.error(e); }
var uri = ['', 'blob', id.slice(0,2), id].join('/');
console.log("encrypted blob is now available as %s", uri);
var title = metadata.name;
if (noStore) { return void onComplete(href); }
var data = {
teamId: teamId,
title: title || "",
href: href,
path: path,
password: password,
channel: id,
owners: metadata.owners,
forceSave: forceSave
};
common.setPadTitle(data, function (err) {
if (err) { return void console.error(err); }
onComplete(href);
common.setPadAttribute('fileType', metadata.type, null, href);
common.setPadAttribute('owners', metadata.owners, null, href);
});
});
};
common.uploadStatus(teamId, estimate, function (e, pending) {
if (e) {
console.error(e);
onError(e);
return;
}
if (pending) {
return void onPending(function () {
// if the user wants to cancel the pending upload to execute that one
common.uploadCancel(teamId, estimate, function (e) {
if (e) {
return void console.error(e);
}
next(again);
});
});
}
next(again);
}); });
}); });

View File

@@ -21,6 +21,8 @@ define([
var sharedFolder = config.sharedFolder; var sharedFolder = config.sharedFolder;
var edPublic = config.edPublic; var edPublic = config.edPublic;
var readOnly = config.readOnly;
var ROOT = exp.ROOT; var ROOT = exp.ROOT;
var FILES_DATA = exp.FILES_DATA; var FILES_DATA = exp.FILES_DATA;
var OLD_FILES_DATA = exp.OLD_FILES_DATA; var OLD_FILES_DATA = exp.OLD_FILES_DATA;
@@ -28,16 +30,37 @@ define([
var TRASH = exp.TRASH; var TRASH = exp.TRASH;
var TEMPLATE = exp.TEMPLATE; var TEMPLATE = exp.TEMPLATE;
var SHARED_FOLDERS = exp.SHARED_FOLDERS; var SHARED_FOLDERS = exp.SHARED_FOLDERS;
var SHARED_FOLDERS_TEMP = exp.SHARED_FOLDERS_TEMP;
var debug = exp.debug; var debug = exp.debug;
exp._setReadOnly = function (state) {
readOnly = state;
if (!readOnly) { exp.fixFiles(); }
};
exp.setHref = function (channel, id, href) {
if (!id && !channel) { return; }
if (readOnly) { return; }
var ids = id ? [id] : exp.findChannels([channel]);
ids.forEach(function (i) {
var data = exp.getFileData(i, true);
data.href = exp.cryptor.encrypt(href);
});
};
exp.setPadAttribute = function (href, attr, value, cb) { exp.setPadAttribute = function (href, attr, value, cb) {
cb = cb || function () {}; cb = cb || function () {};
if (readOnly) { return void cb('EFORBIDDEN'); }
var id = exp.getIdFromHref(href); var id = exp.getIdFromHref(href);
if (!id) { return void cb("E_INVAL_HREF"); } if (!id) { return void cb("E_INVAL_HREF"); }
if (!attr || !attr.trim()) { return void cb("E_INVAL_ATTR"); } if (!attr || !attr.trim()) { return void cb("E_INVAL_ATTR"); }
var data = exp.getFileData(id); var data = exp.getFileData(id, true);
data[attr] = clone(value); if (attr === "href") {
exp.setHref(null, id, value);
} else {
data[attr] = clone(value);
}
cb(null); cb(null);
}; };
exp.getPadAttribute = function (href, attr, cb) { exp.getPadAttribute = function (href, attr, cb) {
@@ -48,21 +71,35 @@ define([
cb(null, clone(data[attr])); cb(null, clone(data[attr]));
}; };
exp.pushData = function (data, cb) { exp.pushData = function (_data, cb) {
if (typeof cb !== "function") { cb = function () {}; } if (typeof cb !== "function") { cb = function () {}; }
if (readOnly) { return void cb('EFORBIDDEN'); }
var id = Util.createRandomInteger(); var id = Util.createRandomInteger();
var data = clone(_data);
// If we were given an edit link, encrypt its value if needed
if (data.href && data.href.indexOf('#') !== -1) { data.href = exp.cryptor.encrypt(data.href); }
files[FILES_DATA][id] = data; files[FILES_DATA][id] = data;
cb(null, id); cb(null, id);
}; };
exp.pushSharedFolder = function (data, cb) { exp.pushSharedFolder = function (_data, cb) {
if (typeof cb !== "function") { cb = function () {}; } if (typeof cb !== "function") { cb = function () {}; }
if (readOnly) { return void cb('EFORBIDDEN'); }
var data = clone(_data);
// Check if we already have this shared folder in our drive // Check if we already have this shared folder in our drive
var exists;
if (Object.keys(files[SHARED_FOLDERS]).some(function (k) { if (Object.keys(files[SHARED_FOLDERS]).some(function (k) {
return files[SHARED_FOLDERS][k].channel === data.channel; if (files[SHARED_FOLDERS][k].channel === data.channel) {
// We already know this shared folder. Check if we can get better access rights
if (data.href && !files[SHARED_FOLDERS][k].href) {
files[SHARED_FOLDERS][k].href = data.href;
}
exists = k;
return true;
}
})) { })) {
return void cb ('EEXISTS'); return void cb ('EEXISTS', exists);
} }
// Add the folder // Add the folder
@@ -70,12 +107,23 @@ define([
return void cb("EAUTH"); return void cb("EAUTH");
} }
var id = Util.createRandomInteger(); var id = Util.createRandomInteger();
if (data.href && data.href.indexOf('#') !== -1) { data.href = exp.cryptor.encrypt(data.href); }
files[SHARED_FOLDERS][id] = data; files[SHARED_FOLDERS][id] = data;
cb(null, id); 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 // FILES DATA
var spliceFileData = function (id) { var spliceFileData = function (id) {
if (readOnly) { return; }
delete files[FILES_DATA][id]; delete files[FILES_DATA][id];
}; };
@@ -83,6 +131,7 @@ define([
// FILES_DATA. If there are owned pads, remove them from server too. // FILES_DATA. If there are owned pads, remove them from server too.
exp.checkDeletedFiles = function (cb) { exp.checkDeletedFiles = function (cb) {
if (!loggedIn && !config.testMode) { return void cb(); } if (!loggedIn && !config.testMode) { return void cb(); }
if (readOnly) { return void cb('EFORBIDDEN'); }
var filesList = exp.getFiles([ROOT, 'hrefArray', TRASH]); var filesList = exp.getFiles([ROOT, 'hrefArray', TRASH]);
var toClean = []; var toClean = [];
@@ -119,6 +168,7 @@ define([
if (channelId) { toClean.push(channelId); } if (channelId) { toClean.push(channelId); }
if (exp.isSharedFolder(id)) { if (exp.isSharedFolder(id)) {
delete files[SHARED_FOLDERS][id]; delete files[SHARED_FOLDERS][id];
if (config.removeProxy) { config.removeProxy(id); }
} else { } else {
spliceFileData(id); spliceFileData(id);
} }
@@ -128,21 +178,22 @@ define([
cb(null, toClean, ownedRemoved); cb(null, toClean, ownedRemoved);
}; };
var deleteHrefs = function (ids) { var deleteHrefs = function (ids) {
if (readOnly) { return; }
ids.forEach(function (obj) { ids.forEach(function (obj) {
var idx = files[obj.root].indexOf(obj.id); var idx = files[obj.root].indexOf(obj.id);
files[obj.root].splice(idx, 1); files[obj.root].splice(idx, 1);
}); });
}; };
var deleteMultipleTrashRoot = function (roots) { var deleteMultipleTrashRoot = function (roots) {
if (readOnly) { return; }
roots.forEach(function (obj) { roots.forEach(function (obj) {
var idx = files[TRASH][obj.name].indexOf(obj.el); var idx = files[TRASH][obj.name].indexOf(obj.el);
files[TRASH][obj.name].splice(idx, 1); files[TRASH][obj.name].splice(idx, 1);
}); });
}; };
exp.deleteMultiplePermanently = function (paths, nocheck, cb) { exp.deleteMultiplePermanently = function (paths, nocheck, cb) {
var hrefPaths = paths.filter(function(x) { return exp.isPathIn(x, ['hrefArray']); }); if (readOnly) { return void cb('EFORBIDDEN'); }
var rootPaths = paths.filter(function(x) { return exp.isPathIn(x, [ROOT]); });
var trashPaths = paths.filter(function(x) { return exp.isPathIn(x, [TRASH]); });
var allFilesPaths = paths.filter(function(x) { return exp.isPathIn(x, [FILES_DATA]); }); var allFilesPaths = paths.filter(function(x) { return exp.isPathIn(x, [FILES_DATA]); });
if (!loggedIn && !config.testMode) { if (!loggedIn && !config.testMode) {
@@ -154,6 +205,10 @@ define([
return void cb(); return void cb();
} }
var hrefPaths = paths.filter(function(x) { return exp.isPathIn(x, ['hrefArray']); });
var rootPaths = paths.filter(function(x) { return exp.isPathIn(x, [ROOT]); });
var trashPaths = paths.filter(function(x) { return exp.isPathIn(x, [TRASH]); });
var ids = []; var ids = [];
hrefPaths.forEach(function (path) { hrefPaths.forEach(function (path) {
var id = exp.find(path); var id = exp.find(path);
@@ -200,6 +255,7 @@ define([
// From another drive // From another drive
exp.copyFromOtherDrive = function (path, element, data, key) { exp.copyFromOtherDrive = function (path, element, data, key) {
if (readOnly) { return; }
// Copy files data // Copy files data
// We have to remove pads that are already in the current proxy to make sure // We have to remove pads that are already in the current proxy to make sure
// we won't create duplicates // we won't create duplicates
@@ -209,11 +265,15 @@ define([
id = Number(id); id = Number(id);
// Find and maybe update existing pads with the same channel id // Find and maybe update existing pads with the same channel id
var d = data[id]; var d = data[id];
// If we were given an edit link, encrypt its value if needed
if (d.href) { d.href = exp.cryptor.encrypt(d.href); }
var found = false; var found = false;
for (var i in files[FILES_DATA]) { for (var i in files[FILES_DATA]) {
if (files[FILES_DATA][i].channel === d.channel) { if (files[FILES_DATA][i].channel === d.channel) {
// Update href? // Update href?
if (!files[FILES_DATA][i].href) { files[FILES_DATA][i].href = d.href; } if (!files[FILES_DATA][i].href) {
files[FILES_DATA][i].href = d.href;
}
found = true; found = true;
break; break;
} }
@@ -222,7 +282,7 @@ define([
toRemove.push(id); toRemove.push(id);
return; return;
} }
files[FILES_DATA][id] = data[id]; files[FILES_DATA][id] = d;
}); });
// Remove existing pads from the "element" variable // Remove existing pads from the "element" variable
@@ -255,6 +315,8 @@ define([
// From the same drive // From the same drive
var pushToTrash = function (name, element, path) { var pushToTrash = function (name, element, path) {
if (readOnly) { return; }
var trash = files[TRASH]; var trash = files[TRASH];
if (typeof(trash[name]) === "undefined") { trash[name] = []; } if (typeof(trash[name]) === "undefined") { trash[name] = []; }
var trashArray = trash[name]; var trashArray = trash[name];
@@ -265,6 +327,7 @@ define([
trashArray.push(trashElement); trashArray.push(trashElement);
}; };
exp.copyElement = function (elementPath, newParentPath) { exp.copyElement = function (elementPath, newParentPath) {
if (readOnly) { return; }
if (exp.comparePath(elementPath, newParentPath)) { return; } // Nothing to do... if (exp.comparePath(elementPath, newParentPath)) { return; } // Nothing to do...
var element = exp.find(elementPath); var element = exp.find(elementPath);
var newParent = exp.find(newParentPath); var newParent = exp.find(newParentPath);
@@ -312,6 +375,8 @@ define([
// FORGET (move with href not path) // FORGET (move with href not path)
exp.forget = function (href) { exp.forget = function (href) {
if (readOnly) { return; }
var id = exp.getIdFromHref(href); var id = exp.getIdFromHref(href);
if (!id) { return; } if (!id) { return; }
if (!loggedIn && !config.testMode) { if (!loggedIn && !config.testMode) {
@@ -328,6 +393,8 @@ define([
// If all the occurences of an href are in the trash, remove them and add the file in root. // If all the occurences of an href are in the trash, remove them and add the file in root.
// This is use with setPadTitle when we open a stronger version of a deleted pad // This is use with setPadTitle when we open a stronger version of a deleted pad
exp.restoreHref = function (href) { exp.restoreHref = function (href) {
if (readOnly) { return; }
var idO = exp.getIdFromHref(href); var idO = exp.getIdFromHref(href);
if (!idO || !exp.isFile(idO)) { return; } if (!idO || !exp.isFile(idO)) { return; }
@@ -350,6 +417,8 @@ define([
}; };
exp.add = function (id, path) { exp.add = function (id, path) {
if (readOnly) { return; }
if (!loggedIn && !config.testMode) { return; } if (!loggedIn && !config.testMode) { return; }
id = Number(id); id = Number(id);
var data = files[FILES_DATA][id] || files[SHARED_FOLDERS][id]; var data = files[FILES_DATA][id] || files[SHARED_FOLDERS][id];
@@ -377,6 +446,8 @@ define([
}; };
exp.setFolderData = function (path, key, value, cb) { exp.setFolderData = function (path, key, value, cb) {
if (readOnly) { return; }
var folder = exp.find(path); var folder = exp.find(path);
if (!exp.isFolder(folder) || exp.isSharedFolder(folder)) { return; } if (!exp.isFolder(folder) || exp.isSharedFolder(folder)) { return; }
if (!exp.hasFolderData(folder)) { if (!exp.hasFolderData(folder)) {
@@ -393,7 +464,41 @@ define([
* INTEGRITY CHECK * INTEGRITY CHECK
*/ */
var onSync = function (next) {
if (exp.rt) {
exp.rt.sync();
Realtime.whenRealtimeSyncs(exp.rt, next);
} else {
window.setTimeout(next, 1000);
}
};
exp.migrateReadOnly = function (cb) {
if (readOnly || !config.editKey) { return void cb({error: 'EFORBIDDEN'}); }
if (files.version >= 2) { return void cb(); } // Already migrated, nothing to do
files.migrateRo = 1;
var next = function () {
var copy = JSON.parse(JSON.stringify(files));
exp.reencrypt(config.editKey, config.editKey, copy);
setTimeout(function () {
if (files.version >= 2) {
// Already migrated by another user while we were re-encrypting
return void cb();
}
Object.keys(copy).forEach(function (k) {
files[k] = copy[k];
});
files.version = 2;
delete files.migrateRo;
onSync(cb);
}, 1000);
};
onSync(next);
};
exp.migrate = function (cb) { exp.migrate = function (cb) {
if (readOnly) { return void cb(); }
// Make sure unsorted doesn't exist anymore // Make sure unsorted doesn't exist anymore
// Note: Unsorted only works with the old structure where pads are href // Note: Unsorted only works with the old structure where pads are href
// It should be called before the migration code // It should be called before the migration code
@@ -471,13 +576,7 @@ define([
delete files.migrate; delete files.migrate;
todo(); todo();
}; };
if (exp.rt) { onSync(next);
exp.rt.sync();
// TODO
Realtime.whenRealtimeSyncs(exp.rt, next);
} else {
window.setTimeout(next, 1000);
}
} catch(e) { } catch(e) {
console.error(e); console.error(e);
todo(); todo();
@@ -498,8 +597,12 @@ define([
// - All files in filesData should be either in 'root', 'trash' or 'unsorted'. If that's not the case, copy the fily to 'unsorted' // - All files in filesData should be either in 'root', 'trash' or 'unsorted'. If that's not the case, copy the fily to 'unsorted'
// * TEMPLATE: Contains only files (href), and does not contains files that are in ROOT // * TEMPLATE: Contains only files (href), and does not contains files that are in ROOT
// We can't fix anything in read-only mode: abort
if (readOnly) { return; }
if (silent) { debug = function () {}; } if (silent) { debug = function () {}; }
var t0 = +new Date();
debug("Cleaning file system..."); debug("Cleaning file system...");
var before = JSON.stringify(files); var before = JSON.stringify(files);
@@ -536,7 +639,10 @@ define([
// We have an old file (href) which is not in filesData: add it // We have an old file (href) which is not in filesData: add it
var id = Util.createRandomInteger(); var id = Util.createRandomInteger();
var key = Hash.createChannelId(); var key = Hash.createChannelId();
files[FILES_DATA][id] = {href: element[el], filename: el}; files[FILES_DATA][id] = {
href: exp.cryptor.encrypt(element[el]),
filename: el
};
element[key] = id; element[key] = id;
delete element[el]; delete element[el];
} }
@@ -562,7 +668,10 @@ define([
if (typeof obj.element === "string") { if (typeof obj.element === "string") {
// We have an old file (href) which is not in filesData: add it // We have an old file (href) which is not in filesData: add it
var id = Util.createRandomInteger(); var id = Util.createRandomInteger();
files[FILES_DATA][id] = {href: obj.element, filename: el}; files[FILES_DATA][id] = {
href: exp.cryptor.encrypt(obj.element),
filename: el
};
obj.element = id; obj.element = id;
} }
if (exp.isFolder(obj.element)) { fixRoot(obj.element); } if (exp.isFolder(obj.element)) { fixRoot(obj.element); }
@@ -607,7 +716,9 @@ define([
if (typeof el === "string") { if (typeof el === "string") {
// We have an old file (href) which is not in filesData: add it // We have an old file (href) which is not in filesData: add it
var id = Util.createRandomInteger(); var id = Util.createRandomInteger();
files[FILES_DATA][id] = {href: el}; files[FILES_DATA][id] = {
href: exp.cryptor.encrypt(el)
};
us[idx] = id; us[idx] = id;
} }
if (typeof el === "number") { if (typeof el === "number") {
@@ -653,7 +764,18 @@ define([
continue; continue;
} }
var parsed = Hash.parsePadUrl(el.href || el.roHref); var href;
try {
href = el.href && ((el.href.indexOf('#') !== -1) ? el.href : exp.cryptor.decrypt(el.href));
} catch (e) {}
if (href && href.indexOf('#') === -1) {
// If we can't decrypt the href, it means we don't have the correct secondaryKey and we're in readOnly mode:
// abort now, we won't be able to fix anything anyway
continue;
}
var parsed = Hash.parsePadUrl(href || el.roHref);
var secret; var secret;
// Clean invalid hash // Clean invalid hash
@@ -670,9 +792,9 @@ define([
} }
// If we have an edit link, check the view link // If we have an edit link, check the view link
if (el.href && parsed.hashData.type === "pad" && parsed.hashData.version) { if (href && parsed.hashData.type === "pad" && parsed.hashData.version) {
if (parsed.hashData.mode === "view") { if (parsed.hashData.mode === "view") {
el.roHref = el.href; el.roHref = href;
delete el.href; delete el.href;
} else if (!el.roHref) { } else if (!el.roHref) {
secret = Hash.getSecrets(parsed.type, parsed.hash, el.password); secret = Hash.getSecrets(parsed.type, parsed.hash, el.password);
@@ -691,7 +813,7 @@ define([
} }
// Fix href // Fix href
if (el.href && /^https*:\/\//.test(el.href)) { el.href = Hash.getRelativeHref(el.href); } if (href && href.slice(0,1) !== '/') { el.href = exp.cryptor.encrypt(Hash.getRelativeHref(el.href)); }
// Fix creation time // Fix creation time
if (!el.ctime) { el.ctime = el.atime; } if (!el.ctime) { el.ctime = el.atime; }
// Fix title // Fix title
@@ -732,8 +854,13 @@ define([
el = sf[id]; el = sf[id];
id = Number(id); id = Number(id);
var href;
try {
href = el.href && ((el.href.indexOf('#') !== -1) ? el.href : exp.cryptor.decrypt(el.href));
} catch (e) {}
// Fix undefined hash // Fix undefined hash
parsed = Hash.parsePadUrl(el.href || el.roHref); parsed = Hash.parsePadUrl(href || el.roHref);
secret = Hash.getSecrets('drive', parsed.hash, el.password); secret = Hash.getSecrets('drive', parsed.hash, el.password);
if (!secret.keys) { if (!secret.keys) {
delete sf[id]; delete sf[id];
@@ -748,6 +875,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 () { var fixDrive = function () {
Object.keys(files).forEach(function (key) { Object.keys(files).forEach(function (key) {
@@ -761,12 +904,14 @@ define([
fixFilesData(); fixFilesData();
fixDrive(); fixDrive();
fixSharedFolders(); fixSharedFolders();
fixSharedFoldersTemp();
var ms = (+new Date() - t0) + 'ms';
if (JSON.stringify(files) !== before) { if (JSON.stringify(files) !== before) {
debug("Your file system was corrupted. It has been cleaned so that the pads you visit can be stored safely"); debug("Your file system was corrupted. It has been cleaned so that the pads you visit can be stored safely.", ms);
return; return;
} }
debug("File system was clean"); debug("File system was clean.", ms);
}; };
return exp; return exp;

View File

@@ -2,9 +2,10 @@ define([
'/common/userObject.js', '/common/userObject.js',
'/common/common-util.js', '/common/common-util.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/outer/sharedfolder.js',
'/customize/messages.js', '/customize/messages.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
], function (UserObject, Util, Hash, Messages, nThen) { ], function (UserObject, Util, Hash, SF, Messages, nThen) {
var getConfig = function (Env) { var getConfig = function (Env) {
@@ -14,24 +15,33 @@ define([
}; };
// Add a shared folder to the list // Add a shared folder to the list
var addProxy = function (Env, id, proxy, leave) { var addProxy = function (Env, id, lm, leave, editKey) {
var cfg = getConfig(Env); var cfg = getConfig(Env);
cfg.sharedFolder = true; cfg.sharedFolder = true;
cfg.id = id; cfg.id = id;
var userObject = UserObject.init(proxy, cfg); cfg.editKey = editKey;
cfg.rt = lm.realtime;
cfg.readOnly = Boolean(!editKey);
var userObject = UserObject.init(lm.proxy, cfg);
if (userObject.fixFiles) { if (userObject.fixFiles) {
// Only in outer // Only in outer
userObject.fixFiles(); 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] = { Env.folders[id] = {
proxy: proxy, proxy: lm.proxy,
userObject: userObject, userObject: userObject,
leave: leave leave: leave
}; };
return userObject; return userObject;
}; };
// TODO: Remove a shared folder from the list
var removeProxy = function (Env, id) { var removeProxy = function (Env, id) {
var f = Env.folders[id]; var f = Env.folders[id];
if (!f) { return; } if (!f) { return; }
@@ -39,6 +49,24 @@ define([
delete Env.folders[id]; delete Env.folders[id];
}; };
// Password may have changed
var deprecateProxy = function (Env, id, channel) {
if (Env.user.userObject.readOnly) {
// In a read-only team, we can't deprecate a shared folder
// Use a empty object with a deprecated flag...
var lm = { proxy: { deprecated: true } };
removeProxy(Env, id);
addProxy(Env, id, lm, function () {});
return void Env.Store.refreshDriveUI();
}
if (channel) { Env.unpinPads([channel], function () {}); }
Env.user.userObject.deprecateSharedFolder(id);
removeProxy(Env, id);
if (Env.Store && Env.Store.refreshDriveUI) {
Env.Store.refreshDriveUI();
}
};
/* /*
Tools Tools
*/ */
@@ -80,11 +108,16 @@ define([
// Return files data objects associated to a channel for setPadTitle // Return files data objects associated to a channel for setPadTitle
// All occurences are returned, in drive or shared folders // All occurences are returned, in drive or shared folders
var findChannel = function (Env, channel) { // If "editable" is true, the data returned is a proxy, otherwise
// it's a cloned object (NOTE: href should never be edited directly)
var findChannel = function (Env, channel, editable) {
var ret = []; var ret = [];
Env.user.userObject.findChannels([channel], true).forEach(function (id) { Env.user.userObject.findChannels([channel], true).forEach(function (id) {
var data = Env.user.proxy[UserObject.SHARED_FOLDERS][id] || // Check in shared folders, then clone if needed
Env.user.userObject.getFileData(id); var data = Env.user.proxy[UserObject.SHARED_FOLDERS][id];
if (data && !editable) { data = JSON.parse(JSON.stringify(data)); }
// If it's not a shared folder, check the pads
if (!data) { data = Env.user.userObject.getFileData(id, editable); }
ret.push({ ret.push({
data: data, data: data,
userObject: Env.user.userObject userObject: Env.user.userObject
@@ -94,7 +127,7 @@ define([
Env.folders[fId].userObject.findChannels([channel]).forEach(function (id) { Env.folders[fId].userObject.findChannels([channel]).forEach(function (id) {
ret.push({ ret.push({
fId: fId, fId: fId,
data: Env.folders[fId].userObject.getFileData(id), data: Env.folders[fId].userObject.getFileData(id, editable),
userObject: Env.folders[fId].userObject userObject: Env.folders[fId].userObject
}); });
}); });
@@ -102,6 +135,8 @@ define([
return ret; return ret;
}; };
// Return files data objects associated to a given href for setPadAttribute... // Return files data objects associated to a given href for setPadAttribute...
// If "editable" is true, the data returned is a proxy, otherwise
// it's a cloned object (NOTE: href should never be edited directly)
var findHref = function (Env, href) { var findHref = function (Env, href) {
var ret = []; var ret = [];
var id = Env.user.userObject.getIdFromHref(href); var id = Env.user.userObject.getIdFromHref(href);
@@ -156,16 +191,33 @@ define([
return ret; return ret;
}; };
var _getFileData = function (Env, id) { var _getFileData = function (Env, id, editable) {
var userObjects = _getUserObjects(Env); var userObjects = _getUserObjects(Env);
var data = {}; var data = {};
userObjects.some(function (uo) { userObjects.some(function (uo) {
data = uo.getFileData(id); data = uo.getFileData(id, editable);
if (Object.keys(data).length) { return true; } if (data && Object.keys(data).length) { return true; }
}); });
return data; return data;
}; };
var getSharedFolderData = function (Env, id) {
if (!Env.folders[id]) { return {}; }
var obj = Env.folders[id].proxy.metadata || {};
for (var k in Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {}) {
var data = JSON.parse(JSON.stringify(Env.user.proxy[UserObject.SHARED_FOLDERS][id][k]));
if (k === "href" && data.indexOf('#') === -1) {
try {
data = Env.user.userObject.cryptor.decrypt(data);
} catch (e) {}
}
if (k === "href" && data.indexOf('#') === -1) { data = undefined; }
obj[k] = data;
}
return obj;
};
// Transform an absolute path into a path relative to the correct shared folder // Transform an absolute path into a path relative to the correct shared folder
var _resolvePath = function (Env, path) { var _resolvePath = function (Env, path) {
var res = { var res = {
@@ -279,11 +331,6 @@ define([
filesData[f] = userObject.getFileData(f); filesData[f] = userObject.getFileData(f);
}); });
// TODO RO
// Encrypt or decrypt edit link here
// filesData.forEach(function (d) { d.href = encrypt(d.href); });
data.push({ data.push({
el: el, el: el,
data: filesData, data: filesData,
@@ -299,6 +346,21 @@ define([
return data; return data;
}; };
var getEditHash = function (Env, channel) {
var res = findChannel(Env, channel);
var stronger;
res.some(function (obj) {
if (!obj || !obj.data || !obj.data.href) { return; }
var parsed = Hash.parsePadUrl(obj.data.href);
var parsedHash = parsed.hashData;
if (!parsedHash || parsedHash.mode === 'view') { return; }
// We've found an edit hash!
stronger = parsed.hash;
return true;
});
return stronger;
};
/* /*
Drive RPC Drive RPC
*/ */
@@ -436,7 +498,14 @@ define([
Env.pinPads([folderData.channel], waitFor()); Env.pinPads([folderData.channel], waitFor());
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
// 1. add the shared folder to our list of shared folders // 1. add the shared folder to our list of shared folders
// NOTE: pushSharedFolder will encrypt the href directly in the object if needed
Env.user.userObject.pushSharedFolder(folderData, waitFor(function (err, folderId) { Env.user.userObject.pushSharedFolder(folderData, waitFor(function (err, folderId) {
if (err === "EEXISTS" && folderData.href && folderId) {
var parsed = Hash.parsePadUrl(folderData.href);
var secret = Hash.getSecrets('drive', parsed.hash, folderData.password);
SF.upgrade(secret.channel, secret);
Env.folders[folderId].userObject.setReadOnly(false, secret.keys.secondaryKey);
}
if (err) { if (err) {
waitFor.abort(); waitFor.abort();
return void cb(err); return void cb(err);
@@ -449,6 +518,11 @@ define([
// 2b. load the proxy // 2b. load the proxy
Env.loadSharedFolder(id, folderData, waitFor(function (rt, metadata) { 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 if (!rt.proxy.metadata) { // Creating a new shared folder
rt.proxy.metadata = { title: data.name || Messages.fm_newFolder }; rt.proxy.metadata = { title: data.name || Messages.fm_newFolder };
} }
@@ -458,7 +532,7 @@ define([
if (metadata.owners) { fData.owners = metadata.owners; } if (metadata.owners) { fData.owners = metadata.owners; }
if (metadata.expire) { fData.expire = +metadata.expire; } if (metadata.expire) { fData.expire = +metadata.expire; }
} }
})); }), !Boolean(data.folderData));
}).nThen(function () { }).nThen(function () {
Env.onSync(function () { Env.onSync(function () {
cb(id); cb(id);
@@ -466,6 +540,49 @@ 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' });
}
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets(parsed.type, parsed.hash, newPassword);
data.password = newPassword;
data.channel = secret.channel;
if (secret.keys.editKeyStr) {
data.href = '/drive/#'+Hash.getEditHashFromKeys(secret);
}
data.roHref = '/drive/#'+Hash.getViewHashFromKeys(secret);
_addSharedFolder(Env, {
path: ['root'],
folderData: data,
}, function () {
delete temp[fId];
Env.onSync(cb);
});
});
};
// convert a folder to a Shared Folder // convert a folder to a Shared Folder
var _convertFolderToSharedFolder = function (Env, data, cb) { var _convertFolderToSharedFolder = function (Env, data, cb) {
return void cb({ return void cb({
@@ -572,6 +689,13 @@ define([
return void cb({error: 'E_NOTFOUND'}); 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 toUnpin = [];
var ownedRemoved; var ownedRemoved;
nThen(function (waitFor)  { nThen(function (waitFor)  {
@@ -668,6 +792,7 @@ define([
var el = Env.user.userObject.find(resolved.path); var el = Env.user.userObject.find(resolved.path);
if (Env.user.userObject.isSharedFolder(el) && Env.folders[el]) { if (Env.user.userObject.isSharedFolder(el) && Env.folders[el]) {
Env.folders[el].proxy.metadata.title = data.newName; Env.folders[el].proxy.metadata.title = data.newName;
Env.user.proxy[UserObject.SHARED_FOLDERS][el].lastTitle = data.value;
return void cb(); return void cb();
} }
} }
@@ -701,6 +826,8 @@ define([
_addFolder(Env, data, cb); break; _addFolder(Env, data, cb); break;
case 'addSharedFolder': case 'addSharedFolder':
_addSharedFolder(Env, data, cb); break; _addSharedFolder(Env, data, cb); break;
case 'restoreSharedFolder':
_restoreSharedFolder(Env, data, cb); break;
case 'convertFolderToSharedFolder': case 'convertFolderToSharedFolder':
_convertFolderToSharedFolder(Env, data, cb); break; _convertFolderToSharedFolder(Env, data, cb); break;
case 'delete': case 'delete':
@@ -722,6 +849,9 @@ define([
if (!data.attr || !data.attr.trim()) { return void cb("E_INVAL_ATTR"); } if (!data.attr || !data.attr.trim()) { return void cb("E_INVAL_ATTR"); }
var sfId = Env.user.userObject.getSFIdFromHref(data.href); var sfId = Env.user.userObject.getSFIdFromHref(data.href);
if (sfId) { if (sfId) {
if (data.attr === "href") {
data.value = Env.user.userObject.cryptor.encrypt(data.value);
}
Env.user.proxy[UserObject.SHARED_FOLDERS][sfId][data.attr] = data.value; Env.user.proxy[UserObject.SHARED_FOLDERS][sfId][data.attr] = data.value;
} }
var datas = findHref(Env, data.href); var datas = findHref(Env, data.href);
@@ -927,16 +1057,20 @@ define([
pinPads: data.pin, pinPads: data.pin,
unpinPads: data.unpin, unpinPads: data.unpin,
onSync: data.onSync, onSync: data.onSync,
Store: data.Store,
loadSharedFolder: data.loadSharedFolder, loadSharedFolder: data.loadSharedFolder,
cfg: uoConfig, cfg: uoConfig,
edPublic: data.edPublic, edPublic: data.edPublic,
settings: data.settings, settings: data.settings,
user: { user: {
proxy: proxy, proxy: proxy,
userObject: UserObject.init(proxy, uoConfig)
}, },
folders: {} folders: {}
}; };
uoConfig.removeProxy = function (id) {
removeProxy(Env, id);
};
Env.user.userObject = UserObject.init(proxy, uoConfig);
var callWithEnv = function (f) { var callWithEnv = function (f) {
return function () { return function () {
@@ -949,6 +1083,7 @@ define([
// Manager // Manager
addProxy: callWithEnv(addProxy), addProxy: callWithEnv(addProxy),
removeProxy: callWithEnv(removeProxy), removeProxy: callWithEnv(removeProxy),
deprecateProxy: callWithEnv(deprecateProxy),
addSharedFolder: callWithEnv(_addSharedFolder), addSharedFolder: callWithEnv(_addSharedFolder),
// Drive // Drive
command: callWithEnv(onCommand), command: callWithEnv(onCommand),
@@ -956,12 +1091,14 @@ define([
setPadAttribute: callWithEnv(setPadAttribute), setPadAttribute: callWithEnv(setPadAttribute),
getTagsList: callWithEnv(getTagsList), getTagsList: callWithEnv(getTagsList),
getSecureFilesList: callWithEnv(getSecureFilesList), getSecureFilesList: callWithEnv(getSecureFilesList),
getSharedFolderData: callWithEnv(getSharedFolderData),
// Store // Store
getChannelsList: callWithEnv(getChannelsList), getChannelsList: callWithEnv(getChannelsList),
addPad: callWithEnv(addPad), addPad: callWithEnv(addPad),
// Tools // Tools
findChannel: callWithEnv(findChannel), findChannel: callWithEnv(findChannel),
findHref: callWithEnv(findHref), findHref: callWithEnv(findHref),
getEditHash: callWithEnv(getEditHash),
user: Env.user, user: Env.user,
folders: Env.folders folders: Env.folders
}; };
@@ -1018,6 +1155,15 @@ define([
} }
}, cb); }, 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) { var convertFolderToSharedFolderInner = function (Env, path, owned, password, cb) {
return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", { return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "convertFolderToSharedFolder", cmd: "convertFolderToSharedFolder",
@@ -1125,15 +1271,6 @@ define([
return Env.user.userObject.getOwnedPads(Env.edPublic); return Env.user.userObject.getOwnedPads(Env.edPublic);
}; };
var getSharedFolderData = function (Env, id) {
if (!Env.folders[id]) { return {}; }
var obj = Env.folders[id].proxy.metadata || {};
for (var k in Env.user.proxy[UserObject.SHARED_FOLDERS][id] || {}) {
obj[k] = Env.user.proxy[UserObject.SHARED_FOLDERS][id][k];
}
return obj;
};
var getFolderData = function (Env, path) { var getFolderData = function (Env, path) {
var resolved = _resolvePath(Env, path); var resolved = _resolvePath(Env, path);
if (!resolved || !resolved.userObject) { return {}; } if (!resolved || !resolved.userObject) { return {}; }
@@ -1230,6 +1367,7 @@ define([
emptyTrash: callWithEnv(emptyTrashInner), emptyTrash: callWithEnv(emptyTrashInner),
addFolder: callWithEnv(addFolderInner), addFolder: callWithEnv(addFolderInner),
addSharedFolder: callWithEnv(addSharedFolderInner), addSharedFolder: callWithEnv(addSharedFolderInner),
restoreSharedFolder: callWithEnv(restoreSharedFolderInner),
convertFolderToSharedFolder: callWithEnv(convertFolderToSharedFolderInner), convertFolderToSharedFolder: callWithEnv(convertFolderToSharedFolderInner),
delete: callWithEnv(deleteInner), delete: callWithEnv(deleteInner),
restore: callWithEnv(restoreInner), restore: callWithEnv(restoreInner),

View File

@@ -118,7 +118,7 @@ define([
msgEv.fire(msg); msgEv.fire(msg);
}); });
SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) { SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) {
sframeChan = sfc; Utils.sframeChan = sframeChan = sfc;
})); }));
}); });
window.addEventListener('message', whenReady); window.addEventListener('message', whenReady);
@@ -174,78 +174,128 @@ define([
var parsed = Utils.Hash.parsePadUrl(window.location.href); var parsed = Utils.Hash.parsePadUrl(window.location.href);
var todo = function () { var todo = function () {
secret = Utils.secret = Utils.Hash.getSecrets(parsed.type, void 0, password); secret = Utils.secret = Utils.Hash.getSecrets(parsed.type, void 0, password);
Cryptpad.getShareHashes(secret, waitFor(function (err, h) { hashes = h; })); Cryptpad.getShareHashes(secret, waitFor(function (err, h) {
hashes = h;
if (password && !parsed.hashData.password) {
var ohc = window.onhashchange;
window.onhashchange = function () {};
window.location.hash = h.fileHash || h.editHash || h.viewHash || window.location.hash;
window.onhashchange = ohc;
ohc({reset: true});
}
}));
}; };
// Prompt the password here if we have a hash containing /p/ if (!parsed.hashData) { // No hash, no need to check for a password
// or get it from the pad attributes return void todo();
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);
} else {
todo();
if (wrongPasswordStored) {
// Store the correct password
Cryptpad.setPadAttribute('password', password, function () {
correctPassword();
}, parsed.getUrl());
} else {
correctPassword();
}
cb(true);
}
};
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 && sessionStorage.newPadPassword) {
val = sessionStorage.newPadPassword;
delete sessionStorage.newPadPassword;
}
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();
}
}), parsed.getUrl());
return;
} }
// If no password, continue...
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
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) {
// XXX noPasswordStored: return; ?
Cryptpad.setPadAttribute('password', password, w(), parsed.getUrl());
Cryptpad.setPadAttribute('channel', secret.channel, w(), parsed.getUrl());
if (parsed.hashData.mode === 'edit') {
var href = window.location.pathname + '#' + Utils.Hash.getEditHashFromKeys(secret);
Cryptpad.setPadAttribute('href', href, w(), parsed.getUrl());
var roHref = window.location.pathname + '#' + Utils.Hash.getViewHashFromKeys(secret);
Cryptpad.setPadAttribute('roHref', roHref, w(), parsed.getUrl());
}
}).nThen(correctPassword);
} else {
correctPassword();
}
cb(true);
}
};
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");
};
var done = waitFor();
var stored = false;
nThen(function (w) {
Cryptpad.getPadAttribute('title', w(function (err, data) {
stored = (!err && typeof (data) === "string");
}));
Cryptpad.getPadAttribute('password', w(function (err, val) {
password = val;
}), parsed.getUrl());
}).nThen(function (w) {
if (!password && !stored && sessionStorage.newPadPassword) {
password = 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, w(function (e, size) {
if (size !== 0) { return void todo(); }
// Wrong password or deleted file?
askPassword(true);
}));
return;
}
// Not a file, so we can use `isNewChannel`
Cryptpad.isNewChannel(window.location.href, password, w(function(e, isNew) {
if (!isNew) { return void todo(); }
if (parsed.hashData.mode === 'view' && (password || !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;
}
if (!stored && !parsed.hashData.password) {
// We've received a link without /p/ and it doesn't work without a password: abort
return void todo();
}
// Wrong password or deleted file?
askPassword(true);
}));
}).nThen(done);
} }
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
if (cfg.afterSecrets) { if (cfg.afterSecrets) {
@@ -433,6 +483,10 @@ define([
Cryptpad.mailbox.execCommand(data, cb); Cryptpad.mailbox.execCommand(data, cb);
}); });
sframeChan.on('Q_STORE_IN_TEAM', function (data, cb) {
Cryptpad.storeInTeam(data, cb);
});
}; };
addCommonRpc(sframeChan); addCommonRpc(sframeChan);
@@ -465,10 +519,6 @@ define([
setDocumentTitle(); setDocumentTitle();
}); });
sframeChan.on('Q_STORE_IN_TEAM', function (data, cb) {
Cryptpad.storeInTeam(data, cb);
});
sframeChan.on('EV_SET_HASH', function (hash) { sframeChan.on('EV_SET_HASH', function (hash) {
window.location.hash = hash; window.location.hash = hash;
}); });
@@ -941,6 +991,22 @@ define([
}); });
}); });
sframeChan.on('Q_BLOB_PASSWORD_CHANGE', function (data, cb) {
data.href = data.href || window.location.href;
var onPending = function (cb) {
sframeChan.query('Q_BLOB_PASSWORD_CHANGE_PENDING', null, function (err, obj) {
if (obj && obj.cancel) { cb(); }
});
};
var updateProgress = function (p) {
sframeChan.event('EV_BLOB_PASSWORD_CHANGE_PROGRESS', p);
};
Cryptpad.changeBlobPassword(data, {
onPending: onPending,
updateProgress: updateProgress
}, cb);
});
sframeChan.on('Q_PAD_PASSWORD_CHANGE', function (data, cb) { sframeChan.on('Q_PAD_PASSWORD_CHANGE', function (data, cb) {
data.href = data.href || window.location.href; data.href = data.href || window.location.href;
Cryptpad.changePadPassword(Cryptget, Crypto, data, cb); Cryptpad.changePadPassword(Cryptget, Crypto, data, cb);

View File

@@ -599,6 +599,10 @@ define([
UIElements.displayPasswordPrompt(funcs); 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) { ctx.sframeChan.on('EV_LOADING_INFO', function (data) {
UI.updateLoadingProgress(data, 'drive'); UI.updateLoadingProgress(data, 'drive');
}); });

View File

@@ -2,11 +2,11 @@ define([
'/customize/application_config.js', '/customize/application_config.js',
'/common/common-util.js', '/common/common-util.js',
'/common/common-hash.js', '/common/common-hash.js',
'/common/common-realtime.js',
'/common/common-constants.js', '/common/common-constants.js',
'/common/outer/userObject.js', '/common/outer/userObject.js',
'/customize/messages.js' '/customize/messages.js',
], function (AppConfig, Util, Hash, Realtime, Constants, OuterFO, Messages) { '/bower_components/chainpad-crypto/crypto.js',
], function (AppConfig, Util, Hash, Constants, OuterFO, Messages, Crypto) {
var module = {}; var module = {};
var ROOT = module.ROOT = "root"; var ROOT = module.ROOT = "root";
@@ -14,6 +14,9 @@ define([
var TRASH = module.TRASH = "trash"; var TRASH = module.TRASH = "trash";
var TEMPLATE = module.TEMPLATE = "template"; var TEMPLATE = module.TEMPLATE = "template";
var SHARED_FOLDERS = module.SHARED_FOLDERS = "sharedFolders"; var SHARED_FOLDERS = module.SHARED_FOLDERS = "sharedFolders";
var SHARED_FOLDERS_TEMP = module.SHARED_FOLDERS_TEMP = "sharedFoldersTemp"; // Maybe deleted or new password
var FILES_DATA = module.FILES_DATA = Constants.storageKey;
var OLD_FILES_DATA = module.OLD_FILES_DATA = Constants.oldStorageKey;
// Create untitled documents when no name is given // Create untitled documents when no name is given
var getLocaleDate = function () { var getLocaleDate = function () {
@@ -29,14 +32,98 @@ define([
return name; return name;
}; };
var createCryptor = module.createCryptor = function (key) {
var cryptor = {};
if (!key) {
cryptor.encrypt = function (x) { return x; };
cryptor.decrypt = function (x) { return x; };
return cryptor;
}
try {
var c = Crypto.createEncryptor(key);
cryptor.encrypt = function (href) {
// Never encrypt blob href, they are always read-only
if (href.slice(0,7) === '/file/#') { return href; }
return c.encrypt(href);
};
cryptor.decrypt = c.decrypt;
} catch (e) {
console.error(e);
}
return cryptor;
};
module.getHref = function (pad, cryptor) {
if (pad.href && pad.href.indexOf('#') !== -1) {
// Href exists and is not encrypted: return href
return pad.href;
}
if (pad.href) {
// Href exists and is encrypted
var d = cryptor.decrypt(pad.href);
// If we can decrypt, return the decrypted value, otherwise continue and return roHref
if (d && d.indexOf('#') !== -1) {
return d;
}
}
return pad.roHref;
};
module.reencrypt = function (oldKey, newKey, obj) {
if (!obj) { return void console.error("Nothing to reencrypt"); }
var oldCryptor = createCryptor(oldKey);
var newCryptor = createCryptor(newKey);
Object.keys(obj[FILES_DATA]).forEach(function (id) {
var data = obj[FILES_DATA][id] || {};
// If this pad has a visible href, encrypt it
// "&& data.roHref" is here to make sure this is not a "file"
if (data.href && data.roHref && !data.fileType) {
var _href = (data.href && data.href.indexOf('#') === -1) ? oldCryptor.decrypt(data.href) : data.href;
if (!_href) { return; }
data.href = newCryptor.encrypt(_href);
}
});
Object.keys(obj[SHARED_FOLDERS] || {}).forEach(function (id) {
var data = obj[SHARED_FOLDERS][id] || {};
// If this folder has a visible href, encrypt it
if (data.href) {
var _href = (data.href && data.href.indexOf('#') === -1) ? oldCryptor.decrypt(data.href) : data.href;
if (!_href) { return; }
data.href = newCryptor.encrypt(_href);
}
});
Object.keys(obj[SHARED_FOLDERS_TEMP] || {}).forEach(function (id) {
var data = obj[SHARED_FOLDERS_TEMP][id] || {};
// If this folder has a visible href, encrypt it
if (data.href) {
var _href = (data.href && data.href.indexOf('#') === -1) ? oldCryptor.decrypt(data.href) : data.href;
if (!_href) { return; }
data.href = newCryptor.encrypt(_href);
}
});
};
module.init = function (files, config) { module.init = function (files, config) {
var exp = {}; var exp = {};
exp.cryptor = createCryptor(config.editKey);
exp.setReadOnly = function (state, key) {
config.editKey = key;
exp.cryptor = createCryptor(key);
exp.cryptor.k = Math.random();
exp.readOnly = state;
if (exp._setReadOnly) {
// Change outer
exp._setReadOnly(state);
}
};
exp.readOnly = config.readOnly;
exp.reencrypt = module.reencrypt;
exp.getDefaultName = module.getDefaultName; exp.getDefaultName = module.getDefaultName;
var sframeChan = config.sframeChan; var sframeChan = config.sframeChan;
var FILES_DATA = module.FILES_DATA = exp.FILES_DATA = Constants.storageKey;
var OLD_FILES_DATA = module.OLD_FILES_DATA = exp.OLD_FILES_DATA = Constants.oldStorageKey;
var NEW_FOLDER_NAME = Messages.fm_newFolder || 'New folder'; var NEW_FOLDER_NAME = Messages.fm_newFolder || 'New folder';
var NEW_FILE_NAME = Messages.fm_newFile || 'New file'; var NEW_FILE_NAME = Messages.fm_newFile || 'New file';
@@ -45,6 +132,9 @@ define([
exp.TRASH = TRASH; exp.TRASH = TRASH;
exp.TEMPLATE = TEMPLATE; exp.TEMPLATE = TEMPLATE;
exp.SHARED_FOLDERS = SHARED_FOLDERS; exp.SHARED_FOLDERS = SHARED_FOLDERS;
exp.SHARED_FOLDERS_TEMP = SHARED_FOLDERS_TEMP;
exp.FILES_DATA = FILES_DATA;
exp.OLD_FILES_DATA = OLD_FILES_DATA;
var sharedFolder = exp.sharedFolder = config.sharedFolder; var sharedFolder = exp.sharedFolder = config.sharedFolder;
exp.id = config.id; exp.id = config.id;
@@ -92,6 +182,10 @@ define([
return a; return a;
}; };
var getHref = exp.getHref = function (pad) {
return module.getHref(pad, exp.cryptor);
};
var type = function (dat) { var type = function (dat) {
return dat === null? 'null': Array.isArray(dat)?'array': typeof(dat); return dat === null? 'null': Array.isArray(dat)?'array': typeof(dat);
}; };
@@ -205,9 +299,25 @@ define([
}; };
// Get data from AllFiles (Cryptpad_RECENTPADS) // Get data from AllFiles (Cryptpad_RECENTPADS)
var getFileData = exp.getFileData = function (file) { var getFileData = exp.getFileData = function (file, editable) {
if (!file) { return; } if (!file) { return; }
return files[FILES_DATA][file] || {}; var data = files[FILES_DATA][file] || {};
if (!editable) {
data = JSON.parse(JSON.stringify(data));
if (data.href && data.href.indexOf('#') === -1) {
// Encrypted href: decrypt it if we can, otherwise remove it
if (config.editKey) {
try {
data.href = exp.cryptor.decrypt(data.href);
} catch (e) {
delete data.href;
}
} else {
delete data.href;
}
}
}
return data;
}; };
exp.getFolderData = function (folder) { exp.getFolderData = function (folder) {
@@ -379,11 +489,17 @@ define([
return Util.deduplicateString(ret); return Util.deduplicateString(ret);
}; };
var getIdFromHref = exp.getIdFromHref = function (href) { var getIdFromHref = exp.getIdFromHref = function (_href) {
var result; var result;
var noPassword = function (str) {
if (!str) { return; }
var parsed = Hash.parsePadUrl(str);
return parsed.getUrl().replace(/\/p\/?/, '/');
};
var href = noPassword(_href);
getFiles([FILES_DATA]).some(function (id) { getFiles([FILES_DATA]).some(function (id) {
if (files[FILES_DATA][id].href === href || if (noPassword(getHref(files[FILES_DATA][id])) === href ||
files[FILES_DATA][id].roHref === href) { noPassword(files[FILES_DATA][id].roHref) === href) {
result = id; result = id;
return true; return true;
} }
@@ -391,11 +507,17 @@ define([
return result; return result;
}; };
exp.getSFIdFromHref = function (href) { exp.getSFIdFromHref = function (_href) {
var result; var result;
var noPassword = function (str) {
if (!str) { return; }
var parsed = Hash.parsePadUrl(str);
return parsed.getUrl().replace(/\/p\/?/, '/');
};
var href = noPassword(_href);
getFiles([SHARED_FOLDERS]).some(function (id) { getFiles([SHARED_FOLDERS]).some(function (id) {
if (files[SHARED_FOLDERS][id].href === href || if (noPassword(getHref(files[SHARED_FOLDERS][id])) === href ||
files[SHARED_FOLDERS][id].roHref === href) { noPassword(files[SHARED_FOLDERS][id].roHref) === href) {
result = id; result = id;
return true; return true;
} }

View File

@@ -97,7 +97,7 @@ define([
for (var i = 0; i<old.length; i++) { for (var i = 0; i<old.length; i++) {
try { try {
pad = old[i]; pad = old[i];
href = pad.href || pad.roHref; href = (pad.href && pad.href.indexOf('#') !== -1) ? pad.href : pad.roHref;
chan = channelByHref[href]; chan = channelByHref[href];
if (!chan && href) { if (!chan && href) {
parsed = Hash.parsePadUrl(href); parsed = Hash.parsePadUrl(href);
@@ -121,7 +121,7 @@ define([
for (var id in ids) { for (var id in ids) {
try { try {
pad = ids[id]; pad = ids[id];
href = pad.href || pad.roHref; href = (pad.href && pad.href.indexOf('#') !== -1) ? pad.href : pad.roHref;
chan = pad.channel || channelByHref[href]; chan = pad.channel || channelByHref[href];
if (!chan) { if (!chan) {
if (href) { if (href) {

View File

@@ -16,6 +16,8 @@
</div> </div>
<div id="cp-app-drive-content-container"> <div id="cp-app-drive-content-container">
<div id="cp-app-drive-toolbar"></div> <div id="cp-app-drive-toolbar"></div>
<div id="cp-app-drive-connection-state" style="display: none"></div>
<div id="cp-app-drive-edition-state" style="display: none"></div>
<div id="cp-app-drive-content" tabindex="2"></div> <div id="cp-app-drive-content" tabindex="2"></div>
</div> </div>
</div> </div>

View File

@@ -3,6 +3,7 @@ define([
'/common/toolbar3.js', '/common/toolbar3.js',
'/common/drive-ui.js', '/common/drive-ui.js',
'/common/common-util.js', '/common/common-util.js',
'/common/common-hash.js',
'/common/common-interface.js', '/common/common-interface.js',
'/common/common-feedback.js', '/common/common-feedback.js',
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
@@ -19,6 +20,7 @@ define([
Toolbar, Toolbar,
DriveUI, DriveUI,
Util, Util,
Hash,
UI, UI,
Feedback, Feedback,
nThen, nThen,
@@ -41,14 +43,30 @@ define([
var oldIds = Object.keys(folders); var oldIds = Object.keys(folders);
nThen(function (waitFor) { nThen(function (waitFor) {
Object.keys(drive.sharedFolders).forEach(function (fId) { Object.keys(drive.sharedFolders).forEach(function (fId) {
var sfData = drive.sharedFolders[fId] || {};
var href = (sfData.href && sfData.href.indexOf('#') !== -1) ? sfData.href : sfData.roHref;
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets('drive', parsed.hash, sfData.password);
sframeChan.query('Q_DRIVE_GETOBJECT', { sframeChan.query('Q_DRIVE_GETOBJECT', {
sharedFolder: fId sharedFolder: fId
}, waitFor(function (err, newObj) { }, waitFor(function (err, newObj) {
if (!APP.loggedIn && APP.newSharedFolder) {
if (!newObj || !Object.keys(newObj).length) {
// Empty anon drive: deleted
var msg = Messages.deletedError + '<br>' + Messages.errorRedirectToHome;
setTimeout(function () { UI.errorLoadingScreen(msg, false, function () {}); });
APP.newSharedFolder = null;
}
}
folders[fId] = folders[fId] || {}; folders[fId] = folders[fId] || {};
copyObjectValue(folders[fId], newObj); copyObjectValue(folders[fId], newObj);
folders[fId].readOnly = !secret.keys.secondaryKey;
if (manager && oldIds.indexOf(fId) === -1) { if (manager && oldIds.indexOf(fId) === -1) {
manager.addProxy(fId, folders[fId]); manager.addProxy(fId, { proxy: folders[fId] }, null, secret.keys.secondaryKey);
} }
var readOnly = !secret.keys.editKeyStr;
if (!manager || !manager.folders[fId]) { return; }
manager.folders[fId].userObject.setReadOnly(readOnly, secret.keys.secondaryKey);
})); }));
}); });
}).nThen(function () { }).nThen(function () {
@@ -60,7 +78,10 @@ define([
copyObjectValue(obj, newObj); copyObjectValue(obj, newObj);
if (!APP.loggedIn && APP.newSharedFolder) { if (!APP.loggedIn && APP.newSharedFolder) {
obj.drive.sharedFolders = obj.drive.sharedFolders || {}; obj.drive.sharedFolders = obj.drive.sharedFolders || {};
obj.drive.sharedFolders[APP.newSharedFolder] = {}; obj.drive.sharedFolders[APP.newSharedFolder] = {
href: APP.anonSFHref,
password: APP.anonSFPassword
};
} }
cb(); cb();
}); });
@@ -95,6 +116,8 @@ define([
})); }));
SFCommon.create(waitFor(function (c) { common = c; })); SFCommon.create(waitFor(function (c) { common = c; }));
}).nThen(function (waitFor) { }).nThen(function (waitFor) {
$('#cp-app-drive-connection-state').text(Messages.disconnected);
$('#cp-app-drive-edition-state').text(Messages.readonly);
var privReady = Util.once(waitFor()); var privReady = Util.once(waitFor());
var metadataMgr = common.getMetadataMgr(); var metadataMgr = common.getMetadataMgr();
if (JSON.stringify(metadataMgr.getPrivateData()) !== '{}') { if (JSON.stringify(metadataMgr.getPrivateData()) !== '{}') {
@@ -119,6 +142,8 @@ define([
var privateData = metadataMgr.getPrivateData(); var privateData = metadataMgr.getPrivateData();
if (privateData.newSharedFolder) { if (privateData.newSharedFolder) {
APP.newSharedFolder = privateData.newSharedFolder; APP.newSharedFolder = privateData.newSharedFolder;
APP.anonSFHref = privateData.anonSFHref;
APP.anonSFPassword = privateData.password;
} }
var sframeChan = common.getSframeChannel(); var sframeChan = common.getSframeChannel();
@@ -198,7 +223,7 @@ define([
}; };
// Add a "Burn this drive" button // Add a "Burn this drive" button
if (!APP.loggedIn) { if (!APP.loggedIn && !APP.readOnly) {
APP.$burnThisDrive = common.createButton(null, true).click(function () { APP.$burnThisDrive = common.createButton(null, true).click(function () {
UI.confirm(Messages.fm_burnThisDrive, function (yes) { UI.confirm(Messages.fm_burnThisDrive, function (yes) {
if (!yes) { return; } if (!yes) { return; }

View File

@@ -107,12 +107,17 @@ define([
sframeChan.event('EV_DRIVE_REMOVE', data); sframeChan.event('EV_DRIVE_REMOVE', data);
}); });
}; };
var addData = function (meta) {
if (!window.CryptPad_newSharedFolder) { return; }
meta.anonSFHref = window.location.href;
};
SFCommonO.start({ SFCommonO.start({
afterSecrets: afterSecrets, afterSecrets: afterSecrets,
noHash: true, noHash: true,
noRealtime: true, noRealtime: true,
driveEvents: true, driveEvents: true,
addRpc: addRpc, addRpc: addRpc,
addData: addData,
isDrive: true, isDrive: true,
}); });
}); });

View File

@@ -106,9 +106,12 @@ define([
// Add pad attributes when the file is saved in the drive // Add pad attributes when the file is saved in the drive
Title.onTitleChange(function () { Title.onTitleChange(function () {
var owners = metadata.owners; var owners = metadata.owners;
if (owners) { if (owners) { common.setPadAttribute('owners', owners); }
common.setPadAttribute('owners', owners); common.setPadAttribute('fileType', metadata.type);
} });
$(document).on('cpPadStored', function () {
var owners = metadata.owners;
if (owners) { common.setPadAttribute('owners', owners); }
common.setPadAttribute('fileType', metadata.type); common.setPadAttribute('fileType', metadata.type);
}); });

View File

@@ -959,12 +959,12 @@ define([
$(list).appendTo(errors); $(list).appendTo(errors);
errs.forEach(function (err) { errs.forEach(function (err) {
if (!err.data) { return; } if (!err.data) { return; }
var href = err.data.href || err.data.roHref; var href = (err.data.href && err.data.href.indexOf('#') !== -1) ? err.data.href : err.data.roHref;
$(h('div', [ $(h('div', [
h('div.title', err.data.filename || err.data.title), h('div.title', err.data.filename || err.data.title),
h('div.link', [ h('div.link', [
h('a', { h('a', {
href: err.data.href || err.data.roHref, href: href,
target: '_blank' target: '_blank'
}, privateData.origin + href) }, privateData.origin + href)
]), ]),

View File

@@ -138,6 +138,11 @@
} }
.cp-team-roster { .cp-team-roster {
.avatar_main(50px); .avatar_main(50px);
.cp-app-team-roster-header {
button:not(:last-child) {
margin-right: 5px;
}
}
.cp-team-roster-member { .cp-team-roster-member {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -184,5 +189,25 @@
} }
} }
} }
#cp-teams-roster-dialog {
table {
width: 100%;
table-layout: fixed;
&.cp-teams-generic {
margin-bottom: 30px;
}
}
p {
text-align: left;
}
ul {
text-align: left;
padding-left: 30px;
}
li {
font-weight: bold;
}
}
} }

View File

@@ -11,6 +11,7 @@ define([
'/bower_components/nthen/index.js', '/bower_components/nthen/index.js',
'/common/sframe-common.js', '/common/sframe-common.js',
'/common/proxy-manager.js', '/common/proxy-manager.js',
'/common/userObject.js',
'/common/hyperscript.js', '/common/hyperscript.js',
'/customize/application_config.js', '/customize/application_config.js',
'/common/messenger-ui.js', '/common/messenger-ui.js',
@@ -32,6 +33,7 @@ define([
nThen, nThen,
SFCommon, SFCommon,
ProxyManager, ProxyManager,
UserObject,
h, h,
AppConfig, AppConfig,
MessengerUI, MessengerUI,
@@ -52,14 +54,30 @@ define([
var oldIds = Object.keys(folders); var oldIds = Object.keys(folders);
nThen(function (waitFor) { nThen(function (waitFor) {
Object.keys(drive.sharedFolders).forEach(function (fId) { Object.keys(drive.sharedFolders).forEach(function (fId) {
var sfData = drive.sharedFolders[fId] || {};
var href = UserObject.getHref(sfData, APP.cryptor);
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets('drive', parsed.hash, sfData.password);
sframeChan.query('Q_DRIVE_GETOBJECT', { sframeChan.query('Q_DRIVE_GETOBJECT', {
sharedFolder: fId sharedFolder: fId
}, waitFor(function (err, newObj) { }, waitFor(function (err, newObj) {
if (newObj && newObj.deprecated) {
delete folders[fId];
delete drive.sharedFolders[fId];
if (manager && manager.folders) {
delete manager.folders[fId];
}
return;
}
folders[fId] = folders[fId] || {}; folders[fId] = folders[fId] || {};
copyObjectValue(folders[fId], newObj); copyObjectValue(folders[fId], newObj);
folders[fId].readOnly = !secret.keys.secondaryKey;
if (manager && oldIds.indexOf(fId) === -1) { if (manager && oldIds.indexOf(fId) === -1) {
manager.addProxy(fId, folders[fId]); manager.addProxy(fId, { proxy: folders[fId] }, null, secret.keys.secondaryKey);
} }
var readOnly = !secret.keys.editKeyStr;
if (!manager || !manager.folders[fId]) { return; }
manager.folders[fId].userObject.setReadOnly(readOnly, secret.keys.secondaryKey);
})); }));
}); });
}).nThen(function () { }).nThen(function () {
@@ -69,16 +87,36 @@ define([
var updateObject = function (sframeChan, obj, cb) { var updateObject = function (sframeChan, obj, cb) {
sframeChan.query('Q_DRIVE_GETOBJECT', null, function (err, newObj) { sframeChan.query('Q_DRIVE_GETOBJECT', null, function (err, newObj) {
copyObjectValue(obj, newObj); copyObjectValue(obj, newObj);
if (!driveAPP.loggedIn && driveAPP.newSharedFolder) {
obj.drive.sharedFolders = obj.drive.sharedFolders || {};
obj.drive.sharedFolders[driveAPP.newSharedFolder] = {};
}
cb(); cb();
}); });
}; };
var setEditable = DriveUI.setEditable; var setEditable = DriveUI.setEditable;
var closeTeam = function (common, cb) {
var sframeChan = common.getSframeChannel();
APP.module.execCommand('SUBSCRIBE', null, function () {
sframeChan.query('Q_SET_TEAM', null, function (err) {
if (err) { return void console.error(err); }
if (APP.drive && APP.drive.close) { APP.drive.close(); }
$('.cp-toolbar-title-value').text(Messages.type.teams);
sframeChan.event('EV_SET_TAB_TITLE', Messages.type.teams);
APP.team = null;
APP.teamEdPublic = null;
APP.drive = null;
APP.cryptor = null;
APP.buildUI(common);
if (APP.usageBar) {
APP.usageBar.stop();
APP.usageBar = null;
}
if (cb) {
cb(common);
}
});
});
};
var mainCategories = { var mainCategories = {
'list': [ 'list': [
'cp-team-list', 'cp-team-list',
@@ -93,23 +131,7 @@ define([
var teamCategories = { var teamCategories = {
'back': { 'back': {
onClick: function (common) { onClick: function (common) {
var sframeChan = common.getSframeChannel(); closeTeam(common);
APP.module.execCommand('SUBSCRIBE', null, function () {
sframeChan.query('Q_SET_TEAM', null, function (err) {
if (err) { return void console.error(err); }
if (APP.drive && APP.drive.close) { APP.drive.close(); }
$('.cp-toolbar-title-value').text(Messages.type.teams);
sframeChan.event('EV_SET_TAB_TITLE', Messages.type.teams);
APP.team = null;
APP.teamEdPublic = null;
APP.drive = null;
APP.buildUI(common);
if (APP.usageBar) {
APP.usageBar.stop();
APP.usageBar = null;
}
});
});
} }
}, },
'drive': [ 'drive': [
@@ -242,6 +264,8 @@ define([
// Team APP // Team APP
var loadTeam = function (common, id) { var loadTeam = function (common, id) {
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var sframeChan = common.getSframeChannel(); var sframeChan = common.getSframeChannel();
var proxy = {}; var proxy = {};
var folders = {}; var folders = {};
@@ -260,13 +284,18 @@ define([
$limitContainer.attr('title', Messages.team_quota); $limitContainer.attr('title', Messages.team_quota);
}, true); }, true);
driveAPP.team = id; driveAPP.team = id;
// Provide secondaryKey
var teamData = (privateData.teams || {})[id] || {};
driveAPP.readOnly = !teamData.secondaryKey;
var drive = DriveUI.create(common, { var drive = DriveUI.create(common, {
proxy: proxy, proxy: proxy,
folders: folders, folders: folders,
updateObject: updateObject, updateObject: updateObject,
updateSharedFolders: updateSharedFolders, updateSharedFolders: updateSharedFolders,
APP: driveAPP, APP: driveAPP,
edPublic: APP.teamEdPublic edPublic: APP.teamEdPublic,
editKey: teamData.secondaryKey
}); });
APP.drive = drive; APP.drive = drive;
driveAPP.refresh = drive.refresh; driveAPP.refresh = drive.refresh;
@@ -306,8 +335,26 @@ define([
}); });
var MAX_TEAMS_SLOTS = Constants.MAX_TEAMS_SLOTS; var MAX_TEAMS_SLOTS = Constants.MAX_TEAMS_SLOTS;
var refreshList = function (common, cb) { var openTeam = function (common, id, team) {
var sframeChan = common.getSframeChannel(); var sframeChan = common.getSframeChannel();
APP.module.execCommand('SUBSCRIBE', id, function () {
var t = Messages._getKey('team_title', [Util.fixHTML(team.metadata.name)]);
sframeChan.query('Q_SET_TEAM', id, function (err) {
if (err) { return void console.error(err); }
// Change title
$('.cp-toolbar-title-value').text(t);
sframeChan.event('EV_SET_TAB_TITLE', t);
// Get secondary key
var secret = Hash.getSecrets('team', team.hash || team.roHash, team.password);
APP.cryptor = UserObject.createCryptor(secret.keys.secondaryKey);
// Load data
APP.team = id;
APP.teamEdPublic = Util.find(team, ['keys', 'drive', 'edPublic']);
buildUI(common, true, team.owner);
});
});
};
var refreshList = function (common, cb) {
var content = []; var content = [];
APP.module.execCommand('LIST_TEAMS', null, function (obj) { APP.module.execCommand('LIST_TEAMS', null, function (obj) {
if (!obj) { return; } if (!obj) { return; }
@@ -343,19 +390,7 @@ define([
])); ]));
common.displayAvatar($(avatar), team.metadata.avatar, team.metadata.name); common.displayAvatar($(avatar), team.metadata.avatar, team.metadata.name);
$(btn).click(function () { $(btn).click(function () {
APP.module.execCommand('SUBSCRIBE', id, function () { openTeam(common, id, team);
var t = Messages._getKey('team_title', [Util.fixHTML(team.metadata.name)]);
sframeChan.query('Q_SET_TEAM', id, function (err) {
if (err) { return void console.error(err); }
// Change title
$('.cp-toolbar-title-value').text(t);
sframeChan.event('EV_SET_TAB_TITLE', t);
// Load data
APP.team = id;
APP.teamEdPublic = Util.find(team, ['keys', 'drive', 'edPublic']);
buildUI(common, true, team.owner);
});
});
}); });
}); });
content.push(h('div.cp-team-list-container', list)); content.push(h('div.cp-team-list-container', list));
@@ -374,7 +409,7 @@ define([
var isOwner = Object.keys(privateData.teams || {}).filter(function (id) { var isOwner = Object.keys(privateData.teams || {}).filter(function (id) {
return privateData.teams[id].owner; return privateData.teams[id].owner;
}).length >= Constants.MAX_TEAMS_OWNED; // && !privateData.devMode; }).length >= Constants.MAX_TEAMS_OWNED && !privateData.devMode;
var getWarningBox = function () { var getWarningBox = function () {
return h('div.alert.alert-warning', { return h('div.alert.alert-warning', {
@@ -439,6 +474,8 @@ define([
h('div#cp-app-drive-tree'), h('div#cp-app-drive-tree'),
h('div#cp-app-drive-content-container', [ h('div#cp-app-drive-content-container', [
h('div#cp-app-drive-toolbar'), h('div#cp-app-drive-toolbar'),
h('div#cp-app-drive-connection-state', {style: "display: none;"}, Messages.disconnected),
h('div#cp-app-drive-edition-state', {style: "display: none;"}, Messages.readonly),
h('div#cp-app-drive-content', {tabindex:2}) h('div#cp-app-drive-content', {tabindex:2})
]) ])
]) ])
@@ -462,7 +499,7 @@ define([
}); });
}; };
var ROLES = ['MEMBER', 'ADMIN', 'OWNER']; var ROLES = ['VIEWER', 'MEMBER', 'ADMIN', 'OWNER'];
var describeUser = function (common, curvePublic, data, icon) { var describeUser = function (common, curvePublic, data, icon) {
APP.module.execCommand('DESCRIBE_USER', { APP.module.execCommand('DESCRIBE_USER', {
teamId: APP.team, teamId: APP.team,
@@ -500,10 +537,11 @@ define([
var actions = h('span.cp-team-member-actions'); var actions = h('span.cp-team-member-actions');
var $actions = $(actions); var $actions = $(actions);
var isMe = me && me.curvePublic === data.curvePublic; var isMe = me && me.curvePublic === data.curvePublic;
var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1; var myRole = me ? (ROLES.indexOf(me.role) || 1) : -1;
var theirRole = ROLES.indexOf(data.role) || 0; var theirRole = ROLES.indexOf(data.role);
var ADMIN = ROLES.indexOf('ADMIN');
// If they're an admin and I am an owner, I can promote them to owner // If they're an admin and I am an owner, I can promote them to owner
if (!isMe && myRole > theirRole && theirRole === 1 && !data.pending) { if (!isMe && myRole > theirRole && theirRole === ADMIN && !data.pending) {
var promoteOwner = h('span.fa.fa-angle-double-up', { var promoteOwner = h('span.fa.fa-angle-double-up', {
title: Messages.team_rosterPromoteOwner title: Messages.team_rosterPromoteOwner
}); });
@@ -525,28 +563,28 @@ define([
}); });
$actions.append(promoteOwner); $actions.append(promoteOwner);
} }
// If they're a member and I have a higher role than them, I can promote them to admin // If they're a viewer/member and I have a higher role than them, I can promote them to admin
if (!isMe && myRole > theirRole && theirRole === 0 && !data.pending) { if (!isMe && myRole >= ADMIN && theirRole < ADMIN && !data.pending) {
var promote = h('span.fa.fa-angle-double-up', { var promote = h('span.fa.fa-angle-double-up', {
title: Messages.team_rosterPromote title: Messages.team_rosterPromote
}); });
$(promote).click(function () { $(promote).click(function () {
$(promote).hide(); $(promote).hide();
describeUser(common, data.curvePublic, { describeUser(common, data.curvePublic, {
role: 'ADMIN' role: ROLES[theirRole + 1]
}, promote); }, promote);
}); });
$actions.append(promote); $actions.append(promote);
} }
// If I'm not a member and I have an equal or higher role than them, I can demote them // If I'm not a member and I have an equal or higher role than them, I can demote them
// (if they're not already a MEMBER) // (if they're not already a MEMBER)
if (myRole >= theirRole && theirRole > 0 && !data.pending) { if (myRole >= theirRole && myRole >= ADMIN && theirRole > 0 && !data.pending) {
var demote = h('span.fa.fa-angle-double-down', { var demote = h('span.fa.fa-angle-double-down', {
title: Messages.team_rosterDemote title: Messages.team_rosterDemote
}); });
$(demote).click(function () { $(demote).click(function () {
var todo = function () { var todo = function () {
var role = ROLES[theirRole - 1] || 'MEMBER'; var role = ROLES[theirRole - 1] || 'VIEWER';
$(demote).hide(); $(demote).hide();
describeUser(common, data.curvePublic, { describeUser(common, data.curvePublic, {
role: role role: role
@@ -560,13 +598,13 @@ define([
} }
todo(); todo();
}); });
if (!(isMe && myRole === 2 && !otherOwners)) { if (!(isMe && myRole === 3 && !otherOwners)) {
$actions.append(demote); $actions.append(demote);
} }
} }
// If I'm not a member and I have an equal or higher role than them, I can remove them // If I'm at least an admin and I have an equal or higher role than them, I can remove them
// Note: we can't remove owners, we have to demote them first // Note: we can't remove owners, we have to demote them first
if (!isMe && myRole > 0 && myRole >= theirRole && theirRole !== 2) { if (!isMe && myRole >= ADMIN && myRole >= theirRole && theirRole !== ROLES.indexOf('OWNER')) {
var remove = h('span.fa.fa-times', { var remove = h('span.fa.fa-times', {
title: Messages.team_rosterKick title: Messages.team_rosterKick
}); });
@@ -632,6 +670,12 @@ define([
}).map(function (k) { }).map(function (k) {
return makeMember(common, roster[k], me); return makeMember(common, roster[k], me);
}); });
var viewers = Object.keys(roster).filter(function (k) {
if (roster[k].pending) { return; }
return roster[k].role === "VIEWER";
}).map(function (k) {
return makeMember(common, roster[k], me);
});
var pending = Object.keys(roster).filter(function (k) { var pending = Object.keys(roster).filter(function (k) {
if (!roster[k].pending) { return; } if (!roster[k].pending) { return; }
return roster[k].role === "MEMBER" || !roster[k].role; return roster[k].role === "MEMBER" || !roster[k].role;
@@ -666,7 +710,7 @@ define([
$header.append(invite); $header.append(invite);
} }
if (me && (me.role === 'ADMIN' || me.role === 'MEMBER')) { if (me && (me.role !== 'OWNER')) {
var leave = h('button.btn.btn-danger', Messages.team_leaveButton); var leave = h('button.btn.btn-danger', Messages.team_leaveButton);
$(leave).click(function () { $(leave).click(function () {
UI.confirm(Messages.team_leaveConfirm, function (yes) { UI.confirm(Messages.team_leaveConfirm, function (yes) {
@@ -683,6 +727,58 @@ define([
$header.append(leave); $header.append(leave);
} }
var table = h('button.btn.btn-primary', Messages.teams_table);
$(table).click(function (e) {
e.stopPropagation();
var $blockContainer = UIElements.createModal({
id: 'cp-teams-roster-dialog',
}).show();
var makeRow = function (arr, first) {
return arr.map(function (val) {
return h(first ? 'th' : 'td', val);
});
};
// Global rights
var rows = [];
var firstRow = ['', Messages.share_linkView, Messages.share_linkEdit,
Messages.teams_table_admins, Messages.teams_table_owners];
rows.push(h('tr', makeRow(firstRow, true)));
rows.push(h('tr', makeRow([Messages.team_viewers, 'x', '', '', ''])));
rows.push(h('tr', makeRow([Messages.team_members, 'x', 'x', '', ''])));
rows.push(h('tr', makeRow([Messages.team_admins, 'x', 'x', 'x', ''])));
rows.push(h('tr', makeRow([Messages.team_owner, 'x', 'x', 'x', 'x'])));
var t = h('table.cp-teams-generic', rows);
var content = [
h('h4', Messages.teams_table_generic),
h('p', Messages.teams_table_genericHint),
t
];
APP.module.execCommand('GET_EDITABLE_FOLDERS', {
teamId: APP.team
}, function (arr) {
console.log(arr);
if (!Array.isArray(arr) || !arr.length) {
return void $blockContainer.find('.cp-modal').append(content);
}
content.push(h('h4', Messages.teams_table_specific));
content.push(h('p', Messages.teams_table_specificHint));
var paths = arr.map(function (obj) {
obj.path.push(obj.name);
return h('li', obj.path.join('/'));
});
content.push(h('ul', paths));
var rows = [];
rows.push(h('tr', makeRow(firstRow, true)));
rows.push(h('tr', makeRow([Messages.team_viewers, 'x', 'x', '', ''])));
content.push(h('table', rows));
$blockContainer.find('.cp-modal').append(content);
});
});
$header.append(table);
var noPending = pending.length ? '' : '.cp-hidden'; var noPending = pending.length ? '' : '.cp-hidden';
return [ return [
@@ -693,6 +789,8 @@ define([
h('div', admins), h('div', admins),
h('h3', Messages.team_members), h('h3', Messages.team_members),
h('div', members), h('div', members),
h('h3', Messages.team_viewers || 'VIEWERS'),
h('div', viewers),
h('h3'+noPending, Messages.team_pending), h('h3'+noPending, Messages.team_pending),
h('div'+noPending, pending) h('div'+noPending, pending)
]; ];
@@ -716,7 +814,8 @@ define([
common.setTeamChat(obj.channel); common.setTeamChat(obj.channel);
MessengerUI.create($(container), common, { MessengerUI.create($(container), common, {
chat: $('.cp-team-cat-chat'), chat: $('.cp-team-cat-chat'),
team: true team: true,
readOnly: obj.readOnly
}); });
cb(content); cb(content);
}); });
@@ -880,6 +979,21 @@ define([
]); ]);
}, true); }, true);
var redrawTeam = function (common) {
if (!APP.team) { return; }
var teamId = APP.team;
APP.module.execCommand('LIST_TEAMS', null, function (obj) {
if (!obj) { return; }
if (obj.error) { return void console.error(obj.error); }
var team = obj[teamId];
if (!team) { return; }
closeTeam(common, function () {
openTeam(common, teamId, team);
});
});
};
var main = function () { var main = function () {
var common; var common;
var readOnly; var readOnly;
@@ -911,9 +1025,6 @@ define([
common.setTabTitle(Messages.type.teams); common.setTabTitle(Messages.type.teams);
// Drive data // Drive data
if (privateData.newSharedFolder) {
driveAPP.newSharedFolder = privateData.newSharedFolder;
}
driveAPP.disableSF = !privateData.enableSF && AppConfig.disableSharedFolders; driveAPP.disableSF = !privateData.enableSF && AppConfig.disableSharedFolders;
// Toolbar // Toolbar
@@ -949,6 +1060,10 @@ define([
} }
return; return;
} }
if (ev === 'ROSTER_CHANGE_RIGHTS') {
redrawTeam(common);
return;
}
}; };
APP.module = common.makeUniversal('team', { APP.module = common.makeUniversal('team', {