Merge branch 'filePassword' into staging

pull/1/head
yflory 5 years ago
commit 553f68fce8

@ -118,6 +118,16 @@ define([], function () {
#cp-loading-password-prompt .cp-password-form button:hover {
background-color: #326599;
}
#cp-loading-password-prompt ::placeholder {
color: #d9d9d9;
opacity: 1;
}
#cp-loading-password-prompt :-ms-input-placeholder {
color: #d9d9d9;
}
#cp-loading-password-prompt ::-ms-input-placeholder {
color: #d9d9d9;
}
#cp-loading .cp-loading-spinner-container {
position: relative;
height: 100px;

@ -116,7 +116,7 @@
}*/
}
.dialog, .alert {
.dialog {
& > div {
background-color: @alertify-dialog-bg;
&.half {
@ -205,6 +205,16 @@
}
}
::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
color: darken(@alertify-input-fg, 15%);
opacity: 1; /* Firefox */
}
:-ms-input-placeholder { /* Internet Explorer 10-11 */
color: darken(@alertify-input-fg, 15%);
}
::-ms-input-placeholder { /* Microsoft Edge */
color: darken(@alertify-input-fg, 15%);
}
input:not(.form-control), textarea {
background-color: @alertify-input-bg;
color: @alertify-input-fg;

@ -466,6 +466,7 @@
padding: 0.25em 0.75em;
margin: 1em;
background: @drive_info-box-bg;
cursor: default;
span {
cursor: pointer;
float: right;
@ -978,5 +979,28 @@
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;
}
}

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

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

@ -420,68 +420,6 @@ Version 1
};
// 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) {
var parsed = Hash.parsePadUrl(href);
if (!parsed || !parsed.hash) { return; }

@ -79,8 +79,10 @@ define([
waitFor.abort();
return void cb(err || 'EEMPTY');
}
if (!val.fileType) {
delete val.owners;
delete val.expire;
}
Util.extend(data, val);
if (data.href) { data.href = base + data.href; }
if (data.roHref) { data.roHref = base + data.roHref; }
@ -434,6 +436,7 @@ define([
} else {
Object.keys(priv.teams || {}).some(function (id) {
var team = priv.teams[id] || {};
if (team.viewer) { return; }
if (data.owners.indexOf(team.edPublic) === -1) { return; }
owned = id;
return true;
@ -492,7 +495,7 @@ define([
data: _owners,
large: true
}, function () {});
if (_ownersGrid) {
if (_ownersGrid && Object.keys(_owners).length) {
$d.append(_ownersGrid.div);
} else {
$d.append([
@ -548,13 +551,17 @@ define([
$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 isFile = parsed.hashData.type === 'file';
var isSharedFolder = parsed.type === 'drive';
var changePwTitle = Messages.properties_changePassword;
var changePwConfirm = Messages.properties_confirmChange;
var changePwConfirm = isFile ? Messages.properties_confirmChangeFile : Messages.properties_confirmChange;
if (!hasPassword) {
changePwTitle = Messages.properties_addPassword;
changePwConfirm = Messages.properties_confirmNew;
changePwConfirm = isFile ? Messages.properties_confirmNewFile : Messages.properties_confirmNew;
}
$('<label>', {'for': 'cp-app-prop-change-password'})
.text(changePwTitle).appendTo($d);
@ -567,23 +574,58 @@ define([
newPassword,
passwordOk
]);
var pLocked = false;
$(passwordOk).click(function () {
var newPass = $(newPassword).find('input').val();
if (data.password === newPass ||
(!data.password && !newPass)) {
return void UI.alert(Messages.properties_passwordSame);
}
if (pLocked) { return; }
pLocked = true;
UI.confirm(changePwConfirm, function (yes) {
if (!yes) { return; }
sframeChan.query("Q_PAD_PASSWORD_CHANGE", {
if (!yes) { pLocked = false; return; }
$(passwordOk).html('').append(h('span.fa.fa-spinner.fa-spin', {style: 'margin-left: 0'}));
var q = isFile ? 'Q_BLOB_PASSWORD_CHANGE' : 'Q_PAD_PASSWORD_CHANGE';
// 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,
href: data.href || data.roHref,
password: newPass
}, function (err, data) {
$(passwordOk).text(Messages.properties_changePasswordButton);
pLocked = false;
if (err || data.error) {
console.error(err || data.error);
return void UI.alert(Messages.properties_passwordError);
}
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 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/
@ -593,7 +635,9 @@ define([
}, {force: true});
}
return void UI.alert(Messages.properties_passwordSuccess, function () {
if (!isSharedFolder) {
common.gotoURL(hasPassword && newPass ? undefined : (data.href || data.roHref));
}
}, {force: true});
});
});
@ -623,7 +667,7 @@ define([
};
var getPadProperties = function (common, data, cb) {
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) {
$('<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
// In this case, we don't want to share the pad with the current team
if (config.teamId && config.teamId === id) { return; }
if (!teamsData[id].hasSecondaryKey) { return; }
var t = teamsData[id];
teams[t.edPublic] = {
notifications: true,
@ -984,7 +1029,7 @@ define([
var hashes = config.hashes;
var common = config.common;
if (!hashes) { return; }
if (!hashes || (!hashes.editHash && !hashes.viewHash)) { return; }
// Share link tab
var hasFriends = Object.keys(config.friends || {}).length !== 0;
@ -992,7 +1037,12 @@ define([
var friendsList = hasFriends ? createShareWithFriends(config, onFriendShare) : undefined;
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('br'),
UI.createRadio('cp-share-editable', 'cp-share-editable-true',
@ -1000,18 +1050,21 @@ define([
UI.createRadio('cp-share-editable', 'cp-share-editable-false',
Messages.share_linkView, false, { mark: {tabindex:1} }),
h('br'),
];
var padContent = [
h('label', Messages.share_linkOptions),
h('br'),
UI.createCheckbox('cp-share-embed', Messages.share_linkEmbed, false, { mark: {tabindex:1} }),
UI.createCheckbox('cp-share-present', Messages.share_linkPresent, false, { mark: {tabindex:1} }),
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);
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 edit = Util.isChecked($(link).find('#cp-share-editable-true'));
var embed = Util.isChecked($(link).find('#cp-share-embed'));
@ -1050,7 +1103,9 @@ define([
if (success) { UI.log(Messages.shareSuccess); }
},
keys: [13]
}, {
}];
if (!config.sharedFolder) {
shareButtons.push({
className: 'primary',
name: Messages.share_linkOpen,
onClick: function () {
@ -1059,12 +1114,21 @@ define([
window.open(v);
},
keys: [[13, 'ctrl']]
}];
});
}
var $link = $(link);
$(mainShareColumn).append(UI.dialog.getButtons(shareButtons, config.onClose)).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('input[type="radio"], input[type="checkbox"]').on('change', function () {
$(link).find('#cp-share-link-preview').val(getLinkValue());
@ -1126,7 +1190,7 @@ define([
}
common.getAttribute(['general', 'share'], function (err, 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-true').prop('checked', false);
} else {
@ -1135,12 +1199,17 @@ define([
}
if (val.embed) { $(link).find('#cp-share-embed').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));
});
common.getMetadataMgr().onChange(function () {
// "hashes" is only available is the secure "share" app
hashes = common.getMetadataMgr().getPrivateData().hashes;
if (!hashes) { return; }
var _hashes = common.getMetadataMgr().getPrivateData().hashes;
if (!_hashes) { return; }
hashes = _hashes;
$(link).find('#cp-share-link-preview').val(getLinkValue());
});
return tabs;
@ -1242,47 +1311,6 @@ define([
}
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) {
var common = config.common;
@ -1533,8 +1561,12 @@ define([
}
UI.confirm(msg, function (yes) {
if (!yes) { return; }
sframeChan.query('Q_MOVE_TO_TRASH', null, function (err) {
if (err) { return void callback(err); }
sframeChan.query('Q_MOVE_TO_TRASH', null, function (err, obj) {
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 msg = common.fixLinks($('<div>').html(cMsg));
UI.alert(msg);
@ -2081,10 +2113,7 @@ define([
var cryptKey = Hash.encodeBase64(secret.keys && secret.keys.cryptKey);
var src = origin + Hash.getBlobPathFromHex(hexFileName);
common.getFileSize(hexFileName, function (e, data) {
if (e || !data) {
displayDefault();
return void console.error(e || "404 avatar");
}
if (e || !data) { return void displayDefault(); }
if (typeof data !== "number") { return void displayDefault(); }
if (Util.bytesToMegabytes(data) > 0.5) { return void displayDefault(); }
var $img = $('<media-tag>').appendTo($container);
@ -3561,6 +3590,7 @@ define([
}
return void UI.warn(Messages.autostore_error);
}
$(document).trigger('cpPadStored');
delete autoStoreModal[priv.channel];
modal.delete();
UIElements.displayCrowdfunding(common);

@ -6,6 +6,7 @@ define([
'/common/common-messaging.js',
'/common/common-constants.js',
'/common/common-feedback.js',
'/common/userObject.js',
'/common/outer/local-store.js',
'/common/outer/worker-channel.js',
'/common/outer/login-block.js',
@ -13,7 +14,7 @@ define([
'/customize/application_config.js',
'/bower_components/nthen/index.js',
], function (Config, Messages, Util, Hash,
Messaging, Constants, Feedback, LocalStore, Channel, Block,
Messaging, Constants, Feedback, UserObject, LocalStore, Channel, Block,
AppConfig, Nthen) {
/* This file exposes functionality which is specific to Cryptpad, but not to
@ -158,11 +159,12 @@ define([
});
};
common.addSharedFolder = function (teamId, secret, cb) {
var href = (secret.keys && secret.keys.editKeyStr) ? '/drive/#' + Hash.getEditHashFromKeys(secret) : undefined;
postMessage("ADD_SHARED_FOLDER", {
teamId: teamId,
path: ['root'],
folderData: {
href: '/drive/#' + Hash.getEditHashFromKeys(secret),
href: href,
roHref: '/drive/#' + Hash.getViewHashFromKeys(secret),
channel: secret.channel,
password: secret.password,
@ -372,7 +374,8 @@ define([
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); }
cb(null, obj);
});
@ -867,12 +870,16 @@ define([
}
var newHref = '/' + parsed.type + '/#' + newHash;
var isSharedFolder = parsed.type === 'drive';
var optsGet = {};
var optsPut = {
password: newPassword,
metadata: {}
metadata: {},
initialState: isSharedFolder ? '{}' : undefined
};
var cryptgetVal;
Nthen(function (waitFor) {
if (parsed.hashData && parsed.hashData.password) {
@ -933,26 +940,64 @@ define([
}
var expire = oldMetadata.expire;
if (expire) {
optsPut.metadata.expire = (expire - (+new Date())) / 1000; // Lifetime in seconds
}
}).nThen(function (waitFor) {
Crypt.get(parsed.hash, waitFor(function (err, val) {
if (err) {
waitFor.abort();
return void cb({ error: err });
}
Crypt.put(newHash, val, waitFor(function (err) {
cryptgetVal = val;
if (isSharedFolder) {
var parsed = JSON.parse(val || '{}');
var oldKey = parsed.version === 2 && oldSecret.keys.secondaryKey;
var newKey = newSecret.keys.secondaryKey;
UserObject.reencrypt(oldKey, newKey, parsed);
cryptgetVal = JSON.stringify(parsed);
}
}), optsGet);
}).nThen(function (waitFor) {
Crypt.put(newHash, cryptgetVal, waitFor(function (err) {
if (err) {
waitFor.abort();
return void cb({ error: err });
}
}), optsPut);
}), optsGet);
}).nThen(function (waitFor) {
if (isSharedFolder) {
postMessage("UPDATE_SHARED_FOLDER_PASSWORD", {
href: href,
oldChannel: oldChannel,
password: newPassword
}, waitFor());
return;
}
pad.leavePad({
channel: oldChannel
}, waitFor());
pad.onDisconnectEvent.fire(true);
}).nThen(function (waitFor) {
// Set the new password to our pad data
common.setPadAttribute('password', newPassword, waitFor(function (err) {
if (err) { warning = true; }
}), href);
common.setPadAttribute('channel', newSecret.channel, waitFor(function (err) {
if (err) { warning = true; }
}), href);
var viewHash = Hash.getViewHashFromKeys(newSecret);
newRoHref = '/' + parsed.type + '/#' + viewHash;
common.setPadAttribute('roHref', newRoHref, waitFor(function (err) {
if (err) { warning = true; }
}), href);
if (parsed.hashData.password && newPassword) { return; } // same hash
common.setPadAttribute('href', newHref, waitFor(function (err) {
if (err) { warning = true; }
}), href);
}).nThen(function (waitFor) {
// delete the old pad
common.removeOwnedChannel({
channel: oldChannel,
teamId: teamId
@ -962,31 +1007,150 @@ define([
return void cb(obj);
}
}));
common.unpinPads([oldChannel], waitFor(), teamId);
common.pinPads([newSecret.channel], waitFor(), teamId);
if (!isSharedFolder) {
postMessage("CHANGE_PAD_PASSWORD_PIN", {
oldChannel: oldChannel,
channel: newSecret.channel
}, waitFor());
}
}).nThen(function () {
cb({
warning: warning,
hash: newHash,
href: newHref,
roHref: newRoHref
});
});
};
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);
var viewHash = Hash.getViewHashFromKeys(newSecret);
newRoHref = '/' + parsed.type + '/#' + viewHash;
common.setPadAttribute('roHref', newRoHref, waitFor(function (err) {
if (err) { warning = true; }
}), href);
if (parsed.hashData.password && newPassword) { return; } // same hash
common.setPadAttribute('href', newHref, waitFor(function (err) {
if (err) { warning = true; }
}), href);
}).nThen(function (waitFor) {
// delete the old pad
common.removeOwnedChannel({
channel: 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,
roHref: newRoHref
});
});
};
@ -1200,6 +1364,11 @@ define([
if (!parsed.type || !parsed.hashData) { return void cb('E_INVALID_HREF'); }
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) {
// It means we're using an old hash
hashes.editHash = window.location.hash.slice(1);
@ -1212,9 +1381,7 @@ define([
}
postMessage("GET_STRONGER_HASH", {
href: window.location.href,
channel: secret.channel,
password: secret.password
channel: secret.channel
}, function (hash) {
if (hash) { hashes.editHash = hash; }
cb(null, hashes);

@ -252,15 +252,18 @@ define([
return APP.store[LS_SEARCHCURSOR] || 0;
};
// Handle disconnect/reconnect
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) {
APP.$content.addClass('cp-app-drive-readonly');
$('#cp-app-drive-connection-state').show();
$('[draggable="true"]').attr('draggable', false);
}
else {
APP.$content.removeClass('cp-app-drive-readonly');
$('#cp-app-drive-connection-state').hide();
$('[draggable="false"]').attr('draggable', true);
}
};
@ -526,10 +529,13 @@ define([
var files = proxy.drive;
var history = driveConfig.history || {};
var edPublic = driveConfig.edPublic || priv.edPublic;
config.editKey = driveConfig.editKey;
APP.origin = priv.origin;
APP.hideDuplicateOwned = Util.find(priv, ['settings', 'drive', 'hideDuplicate']);
APP.closed = false;
var $readOnly = $('#cp-app-drive-edition-state');
var updateObject = driveConfig.updateObject;
var updateSharedFolders = driveConfig.updateSharedFolders;
@ -542,7 +548,11 @@ define([
Object.keys(folders).forEach(function (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
@ -603,9 +613,7 @@ define([
}
}
if (!APP.readOnly) {
setEditable(true);
}
APP.editable = !APP.readOnly;
var appStatus = {
isReady: true,
_onReady: [],
@ -1086,8 +1094,10 @@ define([
var show = [];
var filter;
var editable = true;
if (type === "content") {
if (APP.$content.data('readOnlyFolder')) { editable = false; }
// Return true in filter to hide
filter = function ($el, className) {
if (className === 'newfolder') { return; }
@ -1112,6 +1122,10 @@ define([
paths.forEach(function (p) {
var path = p.path;
var $element = p.element;
if (APP.$content.data('readOnlyFolder') &&
manager.isSubpath(path, currentPath)) { editable = false; }
if (!$element.closest("#cp-app-drive-tree").length) {
hide.push('expandall');
hide.push('collapseall');
@ -1151,10 +1165,13 @@ define([
hide.push('openro'); // Remove open 'view' mode
}
// if it's not a plain text file
// XXX: there is a bug with this code in anon shared folder, so we disable it
if (APP.loggedIn || !APP.newSharedFolder) {
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')) {
if (containsFolder) {
// More than 1 folder selected: cannot create a new subfolder
@ -1204,6 +1221,9 @@ define([
hide.push('removesf');
}
}
if ($element.closest('[data-ro]').length) {
editable = false;
}
});
if (paths.length > 1) {
hide.push('restore');
@ -1250,7 +1270,8 @@ define([
var filtered = [];
show.forEach(function (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; }
$el.parent('li').show();
filtered.push('.cp-app-drive-context-' + className);
@ -1657,6 +1678,13 @@ define([
$('.cp-app-drive-element-droppable').removeClass('cp-app-drive-element-droppable');
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
var fileDrop = ev.dataTransfer.files;
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])) {
return void deletePaths(null, movedPaths);
}
@ -1777,6 +1803,11 @@ define([
if (!manager.isFile(element)) { return; }
var data = manager.getFileData(element);
if (!Object.keys(data).length) {
return true;
}
var href = data.href || data.roHref;
if (!data) { return void logError("No data for the file", element); }
@ -1850,6 +1881,7 @@ define([
if (!element || !manager.isFolder(element)) { return; }
// The element with the class '.name' is underlined when the 'li' is hovered
var $state = $('<span>', {'class': 'cp-app-drive-element-state'});
var $ro;
if (manager.isSharedFolder(element)) {
var data = manager.getSharedFolderData(element);
key = data && data.title ? data.title : key;
@ -1857,8 +1889,21 @@ define([
$span.addClass('cp-app-drive-element-sharedf');
_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);
$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);
@ -1931,13 +1976,18 @@ define([
if (isTrash) { return; }
openFile(root[key]);
});
var invalid;
if (isFolder) {
addFolderData(element, key, $element);
invalid = addFolderData(element, key, $element);
} else {
addFileData(element, $element);
invalid = addFileData(element, $element);
}
if (invalid) {
return;
}
$element.addClass(liClass);
addDragAndDropHandlers($element, newPath, isFolder, !isTrash);
var droppable = !isTrash && !APP.$content.data('readOnlyFolder');
addDragAndDropHandlers($element, newPath, isFolder, droppable);
$element.click(function(e) {
e.stopPropagation();
onElementClick(e, $element);
@ -2501,23 +2551,32 @@ define([
$sharedIcon.clone().appendTo($shareBlock);
$('<span>').text(Messages.shareButton).appendTo($shareBlock);
var data = manager.getSharedFolderData(id);
var parsed = Hash.parsePadUrl(data.href);
if (!parsed || !parsed.hash) { return void console.error("Invalid href: "+data.href); }
var parsed = (data.href && data.href.indexOf('#') !== -1) ? Hash.parsePadUrl(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 teams = common.getMetadataMgr().getPrivateData().teams;
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,
origin: APP.origin,
pathname: "/drive/",
friends: friends,
title: data.title,
password: data.password,
sharedFolder: true,
common: common,
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 () {
UI.openCustomModal(modal, {
wide: _wide
@ -2777,6 +2836,7 @@ define([
return $container;
};
var createGhostIcon = function ($list) {
if (APP.$content.data('readOnlyFolder') || !APP.editable) { return; }
var isInRoot = currentPath[0] === ROOT;
var $element = $('<li>', {
'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 (!appStatus.isReady && !force) { return; }
// Fix path obvious issues
if (!path || path.length === 0) {
// Only Trash and Root are available in not-owned files manager
if (!path || displayedCategories.indexOf(path[0]) === -1) {
@ -3217,7 +3278,7 @@ define([
path = [ROOT];
}
// Get path data
appStatus.ready(false);
currentPath = path;
var s = $content.scrollTop() || 0;
@ -3239,6 +3300,7 @@ define([
currentPath = path;
}
// Make sure the path is valid
var root = isVirtual ? undefined : manager.find(path);
if (manager.isSharedFolder(root)) {
// ANON_SHARED_FOLDER
@ -3261,6 +3323,7 @@ define([
}
if (!isSearch) { delete APP.Search.oldLocation; }
// Display the tree and build the content
APP.resetTree();
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
@ -3293,9 +3356,24 @@ define([
var $list = $('<ul>').appendTo($dirContent);
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'));
var sfId = manager.isInSharedFolder(currentPath);
}
if (sfId) {
var sfData = manager.getSharedFolderData(sfId);
var parsed = Hash.parsePadUrl(sfData.href);
@ -3305,6 +3383,7 @@ define([
sframeChan.event('EV_DRIVE_SET_HASH', '');
}
createTitle($toolbar.find('.cp-app-drive-path'), path);
if (APP.mobile()) {
@ -3515,6 +3594,7 @@ define([
var newPath = path.slice();
newPath.push(key);
var isSharedFolder = manager.isSharedFolder(root[key]);
var sfId = manager.isInSharedFolder(newPath) || (isSharedFolder && root[key]);
var $icon, isCurrentFolder, subfolder;
if (isSharedFolder) {
var fId = root[key];
@ -3536,13 +3616,19 @@ define([
(isCurrentFolder ? $folderOpenedEmptyIcon : $folderEmptyIcon) :
(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.find('>.cp-app-drive-element-row').contextmenu(openContextMenu('tree'));
if (isSharedFolder) {
$element.find('>.cp-app-drive-element-row')
.addClass('cp-app-drive-element-sharedf');
}
if (sfId && !editable) {
$element.attr('data-ro', true);
}
createTree($element, newPath);
});
};
@ -3743,9 +3829,10 @@ define([
}
if (manager.isSharedFolder(el)) {
delete data.roHref;
var ro = folders[el] && folders[el].version >= 2;
if (!ro) { delete data.roHref; }
//data.noPassword = true;
data.noEditPassword = true;
//data.noEditPassword = true;
data.noExpiration = true;
// this is here to allow users to check the channel id of a shared folder
// we should remove it at some point
@ -3968,25 +4055,7 @@ define([
var teams = common.getMetadataMgr().getPrivateData().teams;
var _wide = Object.keys(friends).length || Object.keys(teams).length;
if (manager.isSharedFolder(el)) {
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 (manager.isFolder(el) && !manager.isSharedFolder(el)) { // Folder
// if folder is inside SF
return UI.warn('ERROR: Temporarily disabled'); // XXX CONVERT
/*if (manager.isInSharedFolder(paths[0].path)) {
@ -4021,10 +4090,12 @@ define([
});
}*/
} else { // File
data = manager.getFileData(el);
parsed = Hash.parsePadUrl(data.href);
var sf = manager.isSharedFolder(el);
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 padType = parsed.type || roParsed.type;
var ro = !sf || (folders[el] && folders[el].version >= 2);
var padData = {
teamId: APP.team,
origin: APP.origin,
@ -4033,7 +4104,7 @@ define([
password: data.password,
hashes: {
editHash: parsed.hash,
viewHash: roParsed.hash,
viewHash: ro && roParsed.hash,
fileHash: parsed.hash
},
fileData: {
@ -4042,6 +4113,7 @@ define([
},
isTemplate: paths[0].path[0] === 'template',
title: data.title,
sharedFolder: sf,
common: common
};
modal = padType === 'file' ? UIElements.createFileShareModal(padData)
@ -4403,6 +4475,7 @@ define([
refresh();
UI.removeLoadingScreen();
/*
if (!APP.team) {
sframeChan.query('Q_DRIVE_GETDELETED', null, function (err, data) {
var ids = manager.findChannels(data);
@ -4417,6 +4490,79 @@ define([
UI.log(Messages._getKey('fm_deletedPads', [titles.join(', ')]));
});
}
*/
var deprecated = files.sharedFoldersTemp;
var nt = nThen;
var passwordModal = function (fId, data, cb) {
var content = [];
var folderName = '<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 {
refresh: refresh,

@ -52,7 +52,8 @@ define([
var cancel = function () {
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 name = fData.filename || fData.title;
var secret = Hash.getSecrets('file', hash, fData.password);
@ -88,7 +89,8 @@ define([
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 opts = {
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; }
// waitFor is used to make sure all the pads and files are process before downloading the zip.

@ -27,6 +27,7 @@ define([
if (parsed) {
var proxy = proxyData.proxy;
var oldFo = FO.init(parsed.drive, {
readOnly: false,
loggedIn: true,
outer: true
});
@ -38,7 +39,7 @@ define([
var data = oldFo.getFileData(id);
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
// in the drive
if (datas.length !== 0) {
@ -49,7 +50,9 @@ define([
// We want to merge an edit pad: check if we have the same channel
// but read-only and upgrade it in that case
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;
}

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

@ -145,10 +145,14 @@ define([
n = n.nThen(function (w) {
setTimeout(w(function () {
el = data[k];
if (!el.href || (el.roHref && false)) {
if (!el.href) {
// Already migrated
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);
if (parsed.hashData.type !== "pad") {
// No read-only mode for files

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

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

@ -249,13 +249,13 @@ define([
if (msg.author !== content.user.curvePublic) { return void cb(true); }
var channel = content.channel;
var res = ctx.store.manager.findChannel(channel);
var res = ctx.store.manager.findChannel(channel, true);
var title;
res.forEach(function (obj) {
if (obj.data && !obj.data.href) {
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);
};
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 {
add: function (ctx, box, data, cb) {

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

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

@ -19,53 +19,154 @@ define([
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 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 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);
// If we don't have valid keys, abort and remove the proxy to make sure
// we don't block the drive permanently
if (!secret.keys) {
store.manager.deprecateProxy(id);
return void cb(null);
}
var secondaryKey = secret.keys.secondaryKey;
// If we try to load an existing shared folder (isNew === false) but this folder
// doesn't exist in the database, abort and cb
nThen(function (waitFor) {
isNewChannel(null, { channel: secret.channel }, waitFor(function (obj) {
if (obj.isNew && !isNew) {
store.manager.deprecateProxy(id, secret.channel);
waitFor.abort();
return void cb(null);
}
}));
}).nThen(function () {
var sf = allSharedFolders[secret.channel];
if (sf && sf.readOnly && secondaryKey) {
// We were in readOnly mode and now we know the edit keys!
SF.upgrade(secret.channel, secret);
}
if (sf && sf.ready && sf.rt) {
// The shared folder is already loaded, return its data
setTimeout(function () {
var leave = function () { SF.leave(secret.channel, teamId); };
store.manager.addProxy(id, sf.rt.proxy, leave);
/*
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.team.push(teamId);
sf.teams.push({
cb: cb,
store: store,
id: id
});
if (handler) { handler(id, sf.rt); }
return sf.rt;
return;
}
if (sf && sf.queue && sf.rt) {
if (sf && !sf.ready && sf.rt) {
// The shared folder is loading, add our callbacks to the queue
sf.queue.push({
sf.teams.push({
cb: cb,
store: store,
secondaryKey: secondaryKey,
id: id
});
sf.team.push(teamId);
if (handler) { handler(id, sf.rt); }
return sf.rt;
return;
}
sf = allSharedFolders[secret.channel] = {
queue: [{
teams: [{
cb: cb,
store: store,
secondaryKey: secondaryKey,
id: id
}],
team: [store.id || -1]
readOnly: !Boolean(secondaryKey)
};
var owners = data.owners;
var listmapConfig = {
data: {},
channel: secret.channel,
readOnly: false,
readOnly: !Boolean(secondaryKey),
crypto: Crypto.createEncryptor(secret.keys),
userName: 'sharedFolder',
logLevel: 1,
@ -79,21 +180,57 @@ define([
};
var rt = sf.rt = Listmap.create(listmapConfig);
rt.proxy.on('ready', function (info) {
if (!sf.queue) {
if (isNew && !Object.keys(rt.proxy).length) {
// New Shared folder: no migration required
rt.proxy.version = 2;
}
if (!sf.teams) {
return;
}
sf.queue.forEach(function (obj) {
sf.teams.forEach(function (obj) {
var leave = function () { SF.leave(secret.channel, teamId); };
obj.store.manager.addProxy(obj.id, rt.proxy, leave);
obj.cb(rt, info.metadata);
/*
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.leave = info.leave;
sf.metadata = info.metadata;
sf.ready = true;
delete sf.queue;
});
rt.proxy.on('error', function (info) {
if (info && info.error) {
if (info.error === "EDELETED" ) {
try {
// Deprecate the shared folder from each team
// 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); }
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) {
@ -101,8 +238,14 @@ define([
if (!sf) { return; }
var clients = sf.teams;
if (!Array.isArray(clients)) { return; }
var idx = clients.indexOf(teamId);
if (idx === -1) { return; }
var idx;
clients.some(function (obj, i) {
if (obj.store.id === teamId) {
idx = i;
return true;
}
});
if (typeof (idx) === "undefined") { return; }
// Remove the selected team
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
load all shared folder stored in a given drive
- store: user or team main store
@ -121,35 +300,13 @@ define([
*/
SF.loadSharedFolders = function (Store, network, store, userObject, waitFor) {
var shared = Util.find(store.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {};
// Check if any of our shared folder is expired or deleted by its owner.
// If we don't check now, Listmap will create an empty proxy if it no longer exists on
// the server.
nThen(function (waitFor) {
var checkExpired = Object.keys(shared).map(function (fId) {
return shared[fId].channel;
});
Store.getDeletedPads(null, {list: checkExpired}, waitFor(function (chans) {
if (chans && chans.error) { return void console.error(chans.error); }
if (!Array.isArray(chans) || !chans.length) { return; }
var toDelete = [];
Object.keys(shared).forEach(function (fId) {
if (chans.indexOf(shared[fId].channel) !== -1
&& toDelete.indexOf(fId) === -1) {
toDelete.push(fId);
}
});
toDelete.forEach(function (fId) {
var paths = userObject.findFile(Number(fId));
userObject.delete(paths, waitFor(), true);
delete shared[fId];
});
}));
}).nThen(function (waitFor) {
Object.keys(shared).forEach(function (id) {
var sf = shared[id];
SF.load({
network: network,
store: store
store: store,
isNewChannel: Store.isNewChannel
}, id, sf, waitFor());
});
}).nThen(waitFor());

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

@ -31,6 +31,33 @@ define([
var registerChangeEvents = function (ctx, team, proxy, fId) {
if (!team) { return; }
if (!fId) {
// Listen for shared folder password change
proxy.on('change', ['drive', UserObject.SHARED_FOLDERS], function (o, n, p) {
if (p.length > 3 && p[3] === 'password') {
var id = p[2];
var data = proxy.drive[UserObject.SHARED_FOLDERS][id];
var href = team.manager.user.userObject.getHref ?
team.manager.user.userObject.getHref(data) : data.href;
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets(parsed.type, parsed.hash, o);
// 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) {
if (fId) {
// Pin the new pads
@ -82,6 +109,7 @@ define([
try { team.listmap.stop(); } catch (e) {}
try { team.roster.stop(); } catch (e) {}
team.proxy = {};
team.stopped = true;
delete ctx.teams[teamId];
delete ctx.store.proxy.teams[teamId];
ctx.emit('LEAVE_TEAM', teamId, team.clients);
@ -141,8 +169,10 @@ define([
roster: roster
};
// Subscribe to events
if (cId) { team.clients.push(cId); }
// Listen for roster changes
roster.on('change', function () {
var state = roster.getState();
var me = Util.find(ctx, ['store', 'proxy', 'curvePublic']);
@ -159,16 +189,19 @@ define([
rosterData.lastKnownHash = hash;
});
// Update metadata
var state = roster.getState();
var teamData = Util.find(ctx, ['store', 'proxy', 'teams', id]);
if (teamData) { teamData.metadata = state.metadata; }
// Broadcast an event to all the tabs displaying this team
team.sendEvent = function (q, data, sender) {
ctx.emit(q, data, team.clients.filter(function (cId) {
return cId !== sender;
}));
};
// Provide team chat keys to the messenger app
team.getChatData = function () {
var chatKeys = keys.chat || {};
var hash = chatKeys.edit || chatKeys.view;
@ -178,13 +211,15 @@ define([
teamId: id,
channel: secret.channel,
secret: secret,
validateKey: secret.keys.validateKey
validateKey: chatKeys.validateKey
};
};
var secret;
team.pin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
team.unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
nThen(function (waitFor) {
// Init Team RPC
if (!keys.drive.edPrivate) { return; }
initRpc(ctx, team, keys.drive, waitFor(function (err) {
if (err) { return; }
@ -208,14 +243,18 @@ define([
};
}));
}).nThen(function () {
var loadSharedFolder = function (id, data, cb) {
// Create the proxy manager
var loadSharedFolder = function (id, data, cb, isNew) {
SF.load({
isNew: isNew,
network: ctx.store.network,
store: team
}, id, data, function (id, rt) {
cb(id, rt);
});
store: team,
isNewChannel: ctx.Store.isNewChannel
}, id, data, cb);
};
var teamData = ctx.store.proxy.teams[team.id];
var hash = teamData.hash || teamData.roHash;
secret = Hash.getSecrets('team', hash, teamData.password);
var manager = team.manager = ProxyManager.create(proxy.drive, {
onSync: function (cb) { ctx.Store.onSync(id, cb); },
edPublic: keys.drive.edPublic,
@ -224,7 +263,8 @@ define([
loadSharedFolder: loadSharedFolder,
settings: {
drive: Util.find(ctx.store, ['proxy', 'settings', 'drive'])
}
},
Store: ctx.Store
}, {
outer: true,
removeOwnedChannel: function (channel, cb) {
@ -245,13 +285,19 @@ define([
log: function (msg) {
// broadcast to all drive apps
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.fixFiles();
}).nThen(function (waitFor) {
// Load the shared folders
ctx.teams[id] = team;
registerChangeEvents(ctx, team, proxy);
SF.checkMigration(team.secondaryKey, proxy, team.userObject, waitFor());
SF.loadSharedFolders(ctx.Store, ctx.store.network, team, team.userObject, waitFor);
}).nThen(function () {
if (!team.rpc) { return; }
@ -288,10 +334,20 @@ define([
var openChannel = function (ctx, teamData, id, _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);
if (!teamData.roHash) {
teamData.roHash = Hash.getViewHashFromKeys(secret);
}
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 lm;
@ -365,6 +421,7 @@ define([
}, waitFor(function (err, _roster) {
if (err) {
waitFor.abort();
console.error(err);
return void cb({error: 'ROSTER_ERROR'});
}
roster = _roster;
@ -413,6 +470,7 @@ define([
var password = Hash.createChannelId();
var hash = Hash.createRandomHash('team', password);
var secret = Hash.getSecrets('team', hash, password);
var roHash = Hash.getViewHashFromKeys(secret);
var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey
var rosterSeed = Crypto.Team.createSeed();
@ -502,6 +560,7 @@ define([
};
var lm = Listmap.create(config);
var proxy = lm.proxy;
proxy.version = 2; // No migration needed
proxy.on('ready', function () {
// Store keys in our drive
var keys = {
@ -512,6 +571,7 @@ define([
chat: {
edit: chatHashes.editHash,
view: chatHashes.viewHash,
validateKey: chatSecret.keys.validateKey,
channel: chatSecret.channel
},
roster: {
@ -524,6 +584,7 @@ define([
owner: true,
channel: secret.channel,
hash: hash,
roHash: roHash,
password: password,
keys: keys,
//members: membersHashes.editHash,
@ -659,7 +720,7 @@ define([
var joinTeam = function (ctx, data, cId, cb) {
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'}); }
var id = Util.createRandomInteger();
ctx.store.proxy.teams[id] = team;
@ -737,6 +798,25 @@ define([
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 teamId = data.teamId;
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 teamId = data.teamId;
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
if (user.role === "OWNER" && data.data.role !== "OWNER") {
revokeOwnership(ctx, teamId, user, function (err) {
if (!err) { return; }
if (!err) { return void cb(); }
console.error(err);
return void cb({error: err});
});
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 = {};
obj[data.curvePublic] = data.data;
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 teamId = data.teamId;
if (!teamId) { return void cb({error: 'EINVAL'}); }
@ -969,6 +1145,7 @@ define([
var obj = {};
obj[user.curvePublic] = user;
obj[user.curvePublic].role = 'VIEWER';
team.roster.add(obj, function (err) {
if (err && err !== 'NO_CHANGE') { return void cb({error: err}); }
ctx.store.mailbox.sendTo('INVITE_TO_TEAM', {
@ -1094,9 +1271,21 @@ define([
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) {
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');
}));
});
@ -1104,15 +1293,25 @@ define([
team.getTeam = function (id) {
return ctx.teams[id];
};
team.getTeamsData = function () {
team.getTeamsData = function (app) {
var t = {};
var safe = false;
if (['drive', 'teams', 'settings'].indexOf(app) !== -1) { safe = true; }
Object.keys(teams).forEach(function (id) {
t[id] = {
owner: teams[id].owner,
name: teams[id].metadata.name,
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;
};
@ -1136,6 +1335,9 @@ define([
});
};
team.changeMyRights = function (id, edit, teamData) {
changeMyRights(ctx, id, edit, teamData);
};
team.updateMyData = function (data) {
Object.keys(ctx.teams).forEach(function (id) {
var team = ctx.teams[id];
@ -1199,6 +1401,9 @@ define([
if (cmd === 'CREATE_TEAM') {
return void createTeam(ctx, data, clientId, cb);
}
if (cmd === 'GET_EDITABLE_FOLDERS') {
return void getEditableFolders(ctx, data, clientId, cb);
}
};
return team;

@ -7,6 +7,82 @@ define([
var Nacl = window.nacl;
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) {
var u8 = file.blob; // This is not a blob but a uint8array
var metadata = file.metadata;
@ -50,47 +126,20 @@ define([
metadata.owners = [edPublic];
}));
}).nThen(function () {
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) { 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;
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 title = metadata.name;
var data = {
teamId: teamId,
title: title || "",
@ -108,28 +157,6 @@ define([
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);
});
});
};

@ -21,6 +21,8 @@ define([
var sharedFolder = config.sharedFolder;
var edPublic = config.edPublic;
var readOnly = config.readOnly;
var ROOT = exp.ROOT;
var FILES_DATA = exp.FILES_DATA;
var OLD_FILES_DATA = exp.OLD_FILES_DATA;
@ -28,16 +30,37 @@ define([
var TRASH = exp.TRASH;
var TEMPLATE = exp.TEMPLATE;
var SHARED_FOLDERS = exp.SHARED_FOLDERS;
var SHARED_FOLDERS_TEMP = exp.SHARED_FOLDERS_TEMP;
var debug = exp.debug;
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) {
cb = cb || function () {};
if (readOnly) { return void cb('EFORBIDDEN'); }
var id = exp.getIdFromHref(href);
if (!id) { return void cb("E_INVAL_HREF"); }
if (!attr || !attr.trim()) { return void cb("E_INVAL_ATTR"); }
var data = exp.getFileData(id);
var data = exp.getFileData(id, true);
if (attr === "href") {
exp.setHref(null, id, value);
} else {
data[attr] = clone(value);
}
cb(null);
};
exp.getPadAttribute = function (href, attr, cb) {
@ -48,21 +71,35 @@ define([
cb(null, clone(data[attr]));
};
exp.pushData = function (data, cb) {
exp.pushData = function (_data, cb) {
if (typeof cb !== "function") { cb = function () {}; }
if (readOnly) { return void cb('EFORBIDDEN'); }
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;
cb(null, id);
};
exp.pushSharedFolder = function (data, cb) {
exp.pushSharedFolder = function (_data, cb) {
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
var exists;
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
@ -70,12 +107,23 @@ define([
return void cb("EAUTH");
}
var id = Util.createRandomInteger();
if (data.href && data.href.indexOf('#') !== -1) { data.href = exp.cryptor.encrypt(data.href); }
files[SHARED_FOLDERS][id] = data;
cb(null, id);
};
exp.deprecateSharedFolder = function (id) {
var data = files[SHARED_FOLDERS][id];
if (!data) { return; }
files[SHARED_FOLDERS_TEMP][id] = JSON.parse(JSON.stringify(data));
var paths = exp.findFile(Number(id));
exp.delete(paths, null, true);
delete files[SHARED_FOLDERS][id];
};
// FILES DATA
var spliceFileData = function (id) {
if (readOnly) { return; }
delete files[FILES_DATA][id];
};
@ -83,6 +131,7 @@ define([
// FILES_DATA. If there are owned pads, remove them from server too.
exp.checkDeletedFiles = function (cb) {
if (!loggedIn && !config.testMode) { return void cb(); }
if (readOnly) { return void cb('EFORBIDDEN'); }
var filesList = exp.getFiles([ROOT, 'hrefArray', TRASH]);
var toClean = [];
@ -119,6 +168,7 @@ define([
if (channelId) { toClean.push(channelId); }
if (exp.isSharedFolder(id)) {
delete files[SHARED_FOLDERS][id];
if (config.removeProxy) { config.removeProxy(id); }
} else {
spliceFileData(id);
}
@ -128,21 +178,22 @@ define([
cb(null, toClean, ownedRemoved);
};
var deleteHrefs = function (ids) {
if (readOnly) { return; }
ids.forEach(function (obj) {
var idx = files[obj.root].indexOf(obj.id);
files[obj.root].splice(idx, 1);
});
};
var deleteMultipleTrashRoot = function (roots) {
if (readOnly) { return; }
roots.forEach(function (obj) {
var idx = files[TRASH][obj.name].indexOf(obj.el);
files[TRASH][obj.name].splice(idx, 1);
});
};
exp.deleteMultiplePermanently = function (paths, nocheck, 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]); });
if (readOnly) { return void cb('EFORBIDDEN'); }
var allFilesPaths = paths.filter(function(x) { return exp.isPathIn(x, [FILES_DATA]); });
if (!loggedIn && !config.testMode) {
@ -154,6 +205,10 @@ define([
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 = [];
hrefPaths.forEach(function (path) {
var id = exp.find(path);
@ -200,6 +255,7 @@ define([
// From another drive
exp.copyFromOtherDrive = function (path, element, data, key) {
if (readOnly) { return; }
// Copy files data
// We have to remove pads that are already in the current proxy to make sure
// we won't create duplicates
@ -209,11 +265,15 @@ define([
id = Number(id);
// Find and maybe update existing pads with the same channel 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;
for (var i in files[FILES_DATA]) {
if (files[FILES_DATA][i].channel === d.channel) {
// 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;
break;
}
@ -222,7 +282,7 @@ define([
toRemove.push(id);
return;
}
files[FILES_DATA][id] = data[id];
files[FILES_DATA][id] = d;
});
// Remove existing pads from the "element" variable
@ -255,6 +315,8 @@ define([
// From the same drive
var pushToTrash = function (name, element, path) {
if (readOnly) { return; }
var trash = files[TRASH];
if (typeof(trash[name]) === "undefined") { trash[name] = []; }
var trashArray = trash[name];
@ -265,6 +327,7 @@ define([
trashArray.push(trashElement);
};
exp.copyElement = function (elementPath, newParentPath) {
if (readOnly) { return; }
if (exp.comparePath(elementPath, newParentPath)) { return; } // Nothing to do...
var element = exp.find(elementPath);
var newParent = exp.find(newParentPath);
@ -312,6 +375,8 @@ define([
// FORGET (move with href not path)
exp.forget = function (href) {
if (readOnly) { return; }
var id = exp.getIdFromHref(href);
if (!id) { return; }
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.
// This is use with setPadTitle when we open a stronger version of a deleted pad
exp.restoreHref = function (href) {
if (readOnly) { return; }
var idO = exp.getIdFromHref(href);
if (!idO || !exp.isFile(idO)) { return; }
@ -350,6 +417,8 @@ define([
};
exp.add = function (id, path) {
if (readOnly) { return; }
if (!loggedIn && !config.testMode) { return; }
id = Number(id);
var data = files[FILES_DATA][id] || files[SHARED_FOLDERS][id];
@ -377,6 +446,8 @@ define([
};
exp.setFolderData = function (path, key, value, cb) {
if (readOnly) { return; }
var folder = exp.find(path);
if (!exp.isFolder(folder) || exp.isSharedFolder(folder)) { return; }
if (!exp.hasFolderData(folder)) {
@ -393,7 +464,41 @@ define([
* 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) {
if (readOnly) { return void cb(); }
// Make sure unsorted doesn't exist anymore
// Note: Unsorted only works with the old structure where pads are href
// It should be called before the migration code
@ -471,13 +576,7 @@ define([
delete files.migrate;
todo();
};
if (exp.rt) {
exp.rt.sync();
// TODO
Realtime.whenRealtimeSyncs(exp.rt, next);
} else {
window.setTimeout(next, 1000);
}
onSync(next);
} catch(e) {
console.error(e);
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'
// * 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 () {}; }
var t0 = +new Date();
debug("Cleaning file system...");
var before = JSON.stringify(files);
@ -536,7 +639,10 @@ define([
// We have an old file (href) which is not in filesData: add it
var id = Util.createRandomInteger();
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;
delete element[el];
}
@ -562,7 +668,10 @@ define([
if (typeof obj.element === "string") {
// We have an old file (href) which is not in filesData: add it
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;
}
if (exp.isFolder(obj.element)) { fixRoot(obj.element); }
@ -607,7 +716,9 @@ define([
if (typeof el === "string") {
// We have an old file (href) which is not in filesData: add it
var id = Util.createRandomInteger();
files[FILES_DATA][id] = {href: el};
files[FILES_DATA][id] = {
href: exp.cryptor.encrypt(el)
};
us[idx] = id;
}
if (typeof el === "number") {
@ -653,7 +764,18 @@ define([
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;
// Clean invalid hash
@ -670,9 +792,9 @@ define([
}
// 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") {
el.roHref = el.href;
el.roHref = href;
delete el.href;
} else if (!el.roHref) {
secret = Hash.getSecrets(parsed.type, parsed.hash, el.password);
@ -691,7 +813,7 @@ define([
}
// 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
if (!el.ctime) { el.ctime = el.atime; }
// Fix title
@ -732,8 +854,13 @@ define([
el = sf[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
parsed = Hash.parsePadUrl(el.href || el.roHref);
parsed = Hash.parsePadUrl(href || el.roHref);
secret = Hash.getSecrets('drive', parsed.hash, el.password);
if (!secret.keys) {
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 () {
Object.keys(files).forEach(function (key) {
@ -761,12 +904,14 @@ define([
fixFilesData();
fixDrive();
fixSharedFolders();
fixSharedFoldersTemp();
var ms = (+new Date() - t0) + 'ms';
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;
}
debug("File system was clean");
debug("File system was clean.", ms);
};
return exp;

@ -2,9 +2,10 @@ define([
'/common/userObject.js',
'/common/common-util.js',
'/common/common-hash.js',
'/common/outer/sharedfolder.js',
'/customize/messages.js',
'/bower_components/nthen/index.js',
], function (UserObject, Util, Hash, Messages, nThen) {
], function (UserObject, Util, Hash, SF, Messages, nThen) {
var getConfig = function (Env) {
@ -14,24 +15,33 @@ define([
};
// 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);
cfg.sharedFolder = true;
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) {
// Only in outer
userObject.fixFiles();
}
var proxy = lm.proxy;
if (proxy.metadata && proxy.metadata.title) {
var sf = Env.user.proxy[UserObject.SHARED_FOLDERS][id];
if (sf) {
sf.lastTitle = proxy.metadata.title;
}
}
Env.folders[id] = {
proxy: proxy,
proxy: lm.proxy,
userObject: userObject,
leave: leave
};
return userObject;
};
// TODO: Remove a shared folder from the list
var removeProxy = function (Env, id) {
var f = Env.folders[id];
if (!f) { return; }
@ -39,6 +49,24 @@ define([
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
*/
@ -80,11 +108,16 @@ define([
// Return files data objects associated to a channel for setPadTitle
// 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 = [];
Env.user.userObject.findChannels([channel], true).forEach(function (id) {
var data = Env.user.proxy[UserObject.SHARED_FOLDERS][id] ||
Env.user.userObject.getFileData(id);
// Check in shared folders, then clone if needed
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({
data: data,
userObject: Env.user.userObject
@ -94,7 +127,7 @@ define([
Env.folders[fId].userObject.findChannels([channel]).forEach(function (id) {
ret.push({
fId: fId,
data: Env.folders[fId].userObject.getFileData(id),
data: Env.folders[fId].userObject.getFileData(id, editable),
userObject: Env.folders[fId].userObject
});
});
@ -102,6 +135,8 @@ define([
return ret;
};
// 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 ret = [];
var id = Env.user.userObject.getIdFromHref(href);
@ -156,16 +191,33 @@ define([
return ret;
};
var _getFileData = function (Env, id) {
var _getFileData = function (Env, id, editable) {
var userObjects = _getUserObjects(Env);
var data = {};
userObjects.some(function (uo) {
data = uo.getFileData(id);
if (Object.keys(data).length) { return true; }
data = uo.getFileData(id, editable);
if (data && Object.keys(data).length) { return true; }
});
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
var _resolvePath = function (Env, path) {
var res = {
@ -279,11 +331,6 @@ define([
filesData[f] = userObject.getFileData(f);
});
// TODO RO
// Encrypt or decrypt edit link here
// filesData.forEach(function (d) { d.href = encrypt(d.href); });
data.push({
el: el,
data: filesData,
@ -299,6 +346,21 @@ define([
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
*/
@ -436,7 +498,14 @@ define([
Env.pinPads([folderData.channel], waitFor());
}).nThen(function (waitFor) {
// 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) {
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) {
waitFor.abort();
return void cb(err);
@ -449,6 +518,11 @@ define([
// 2b. load the proxy
Env.loadSharedFolder(id, folderData, waitFor(function (rt, metadata) {
if (!rt) {
waitFor.abort();
return void cb({ error: 'EDELETED' });
}
if (!rt.proxy.metadata) { // Creating a new shared folder
rt.proxy.metadata = { title: data.name || Messages.fm_newFolder };
}
@ -458,7 +532,7 @@ define([
if (metadata.owners) { fData.owners = metadata.owners; }
if (metadata.expire) { fData.expire = +metadata.expire; }
}
}));
}), !Boolean(data.folderData));
}).nThen(function () {
Env.onSync(function () {
cb(id);
@ -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
var _convertFolderToSharedFolder = function (Env, data, cb) {
return void cb({
@ -572,6 +689,13 @@ define([
return void cb({error: 'E_NOTFOUND'});
}
// Deleted or password changed for a shared folder
if (data.paths.length === 1 && data.paths[0][0] === UserObject.SHARED_FOLDERS_TEMP) {
var temp = Util.find(Env, ['user', 'proxy', UserObject.SHARED_FOLDERS_TEMP]);
delete temp[data.paths[0][1]];
return void Env.onSync(cb);
}
var toUnpin = [];
var ownedRemoved;
nThen(function (waitFor)  {
@ -668,6 +792,7 @@ define([
var el = Env.user.userObject.find(resolved.path);
if (Env.user.userObject.isSharedFolder(el) && Env.folders[el]) {
Env.folders[el].proxy.metadata.title = data.newName;
Env.user.proxy[UserObject.SHARED_FOLDERS][el].lastTitle = data.value;
return void cb();
}
}
@ -701,6 +826,8 @@ define([
_addFolder(Env, data, cb); break;
case 'addSharedFolder':
_addSharedFolder(Env, data, cb); break;
case 'restoreSharedFolder':
_restoreSharedFolder(Env, data, cb); break;
case 'convertFolderToSharedFolder':
_convertFolderToSharedFolder(Env, data, cb); break;
case 'delete':
@ -722,6 +849,9 @@ define([
if (!data.attr || !data.attr.trim()) { return void cb("E_INVAL_ATTR"); }
var sfId = Env.user.userObject.getSFIdFromHref(data.href);
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;
}
var datas = findHref(Env, data.href);
@ -927,16 +1057,20 @@ define([
pinPads: data.pin,
unpinPads: data.unpin,
onSync: data.onSync,
Store: data.Store,
loadSharedFolder: data.loadSharedFolder,
cfg: uoConfig,
edPublic: data.edPublic,
settings: data.settings,
user: {
proxy: proxy,
userObject: UserObject.init(proxy, uoConfig)
},
folders: {}
};
uoConfig.removeProxy = function (id) {
removeProxy(Env, id);
};
Env.user.userObject = UserObject.init(proxy, uoConfig);
var callWithEnv = function (f) {
return function () {
@ -949,6 +1083,7 @@ define([
// Manager
addProxy: callWithEnv(addProxy),
removeProxy: callWithEnv(removeProxy),
deprecateProxy: callWithEnv(deprecateProxy),
addSharedFolder: callWithEnv(_addSharedFolder),
// Drive
command: callWithEnv(onCommand),
@ -956,12 +1091,14 @@ define([
setPadAttribute: callWithEnv(setPadAttribute),
getTagsList: callWithEnv(getTagsList),
getSecureFilesList: callWithEnv(getSecureFilesList),
getSharedFolderData: callWithEnv(getSharedFolderData),
// Store
getChannelsList: callWithEnv(getChannelsList),
addPad: callWithEnv(addPad),
// Tools
findChannel: callWithEnv(findChannel),
findHref: callWithEnv(findHref),
getEditHash: callWithEnv(getEditHash),
user: Env.user,
folders: Env.folders
};
@ -1018,6 +1155,15 @@ define([
}
}, cb);
};
var restoreSharedFolderInner = function (Env, fId, password, cb) {
return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "restoreSharedFolder",
data: {
id: fId,
password: password
}
}, cb);
};
var convertFolderToSharedFolderInner = function (Env, path, owned, password, cb) {
return void Env.sframeChan.query("Q_DRIVE_USEROBJECT", {
cmd: "convertFolderToSharedFolder",
@ -1125,15 +1271,6 @@ define([
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 resolved = _resolvePath(Env, path);
if (!resolved || !resolved.userObject) { return {}; }
@ -1230,6 +1367,7 @@ define([
emptyTrash: callWithEnv(emptyTrashInner),
addFolder: callWithEnv(addFolderInner),
addSharedFolder: callWithEnv(addSharedFolderInner),
restoreSharedFolder: callWithEnv(restoreSharedFolderInner),
convertFolderToSharedFolder: callWithEnv(convertFolderToSharedFolderInner),
delete: callWithEnv(deleteInner),
restore: callWithEnv(restoreInner),

@ -118,7 +118,7 @@ define([
msgEv.fire(msg);
});
SFrameChannel.create(msgEv, postMsg, waitFor(function (sfc) {
sframeChan = sfc;
Utils.sframeChan = sframeChan = sfc;
}));
});
window.addEventListener('message', whenReady);
@ -174,17 +174,38 @@ define([
var parsed = Utils.Hash.parsePadUrl(window.location.href);
var todo = function () {
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/
// or get it from the pad attributes
var needPassword = parsed.hashData && parsed.hashData.password;
if (needPassword) {
// Check if we have a password, and check if it is correct (file exists).
// It we don't have a correct password, display the password prompt.
// Maybe the file has been deleted from the server or the password has been changed.
Cryptpad.getPadAttribute('password', waitFor(function (err, val) {
if (!parsed.hashData) { // No hash, no need to check for a password
return void todo();
}
// We now need to check if there is a password and if we know the correct password.
// We'll use getFileSize and isNewChannel to detect incorrect passwords.
// First we'll get the password value from our drive (getPadAttribute), and we'll check
// if the channel is valid. If the pad is not stored in our drive, we'll test with an
// empty password instead.
// If this initial check returns a valid channel, open the pad.
// If the channel is invalid:
// Option 1: this is a password-protected pad not stored in our drive --> password prompt
// Option 2: this is a pad stored in our drive
// 2a: 'edit' pad or file --> password-prompt
// 2b: 'view' pad no '/p/' --> the seed is incorrect
// 2c: 'view' pad and '/p/' and a wrong password stored --> the seed is incorrect
// 2d: 'view' pad and '/p/' and password never stored (security feature) --> password-prompt
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
@ -201,9 +222,17 @@ define([
todo();
if (wrongPasswordStored) {
// Store the correct password
Cryptpad.setPadAttribute('password', password, function () {
correctPassword();
}, parsed.getUrl());
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();
}
@ -224,28 +253,49 @@ define([
sframeChan.event("EV_PAD_PASSWORD");
};
if (!val && sessionStorage.newPadPassword) {
val = sessionStorage.newPadPassword;
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 (val) {
password = val;
Cryptpad.getFileSize(window.location.href, password, waitFor(function (e, size) {
if (size !== 0) {
return void todo();
}
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);
}));
} else {
askPassword();
return;
}
}), parsed.getUrl());
// 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 no password, continue...
todo();
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) {
if (cfg.afterSecrets) {
@ -433,6 +483,10 @@ define([
Cryptpad.mailbox.execCommand(data, cb);
});
sframeChan.on('Q_STORE_IN_TEAM', function (data, cb) {
Cryptpad.storeInTeam(data, cb);
});
};
addCommonRpc(sframeChan);
@ -465,10 +519,6 @@ define([
setDocumentTitle();
});
sframeChan.on('Q_STORE_IN_TEAM', function (data, cb) {
Cryptpad.storeInTeam(data, cb);
});
sframeChan.on('EV_SET_HASH', function (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) {
data.href = data.href || window.location.href;
Cryptpad.changePadPassword(Cryptget, Crypto, data, cb);

@ -599,6 +599,10 @@ define([
UIElements.displayPasswordPrompt(funcs);
});
ctx.sframeChan.on("EV_PAD_PASSWORD_ERROR", function () {
UI.errorLoadingScreen(Messages.password_error_seed);
});
ctx.sframeChan.on('EV_LOADING_INFO', function (data) {
UI.updateLoadingProgress(data, 'drive');
});

@ -2,11 +2,11 @@ define([
'/customize/application_config.js',
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-realtime.js',
'/common/common-constants.js',
'/common/outer/userObject.js',
'/customize/messages.js'
], function (AppConfig, Util, Hash, Realtime, Constants, OuterFO, Messages) {
'/customize/messages.js',
'/bower_components/chainpad-crypto/crypto.js',
], function (AppConfig, Util, Hash, Constants, OuterFO, Messages, Crypto) {
var module = {};
var ROOT = module.ROOT = "root";
@ -14,6 +14,9 @@ define([
var TRASH = module.TRASH = "trash";
var TEMPLATE = module.TEMPLATE = "template";
var SHARED_FOLDERS = module.SHARED_FOLDERS = "sharedFolders";
var SHARED_FOLDERS_TEMP = module.SHARED_FOLDERS_TEMP = "sharedFoldersTemp"; // Maybe deleted or new password
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
var getLocaleDate = function () {
@ -29,14 +32,98 @@ define([
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) {
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;
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_FILE_NAME = Messages.fm_newFile || 'New file';
@ -45,6 +132,9 @@ define([
exp.TRASH = TRASH;
exp.TEMPLATE = TEMPLATE;
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;
exp.id = config.id;
@ -92,6 +182,10 @@ define([
return a;
};
var getHref = exp.getHref = function (pad) {
return module.getHref(pad, exp.cryptor);
};
var type = function (dat) {
return dat === null? 'null': Array.isArray(dat)?'array': typeof(dat);
};
@ -205,9 +299,25 @@ define([
};
// Get data from AllFiles (Cryptpad_RECENTPADS)
var getFileData = exp.getFileData = function (file) {
var getFileData = exp.getFileData = function (file, editable) {
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) {
@ -379,11 +489,17 @@ define([
return Util.deduplicateString(ret);
};
var getIdFromHref = exp.getIdFromHref = function (href) {
var getIdFromHref = exp.getIdFromHref = function (_href) {
var result;
var noPassword = function (str) {
if (!str) { return; }
var parsed = Hash.parsePadUrl(str);
return parsed.getUrl().replace(/\/p\/?/, '/');
};
var href = noPassword(_href);
getFiles([FILES_DATA]).some(function (id) {
if (files[FILES_DATA][id].href === href ||
files[FILES_DATA][id].roHref === href) {
if (noPassword(getHref(files[FILES_DATA][id])) === href ||
noPassword(files[FILES_DATA][id].roHref) === href) {
result = id;
return true;
}
@ -391,11 +507,17 @@ define([
return result;
};
exp.getSFIdFromHref = function (href) {
exp.getSFIdFromHref = function (_href) {
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) {
if (files[SHARED_FOLDERS][id].href === href ||
files[SHARED_FOLDERS][id].roHref === href) {
if (noPassword(getHref(files[SHARED_FOLDERS][id])) === href ||
noPassword(files[SHARED_FOLDERS][id].roHref) === href) {
result = id;
return true;
}

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

@ -16,6 +16,8 @@
</div>
<div id="cp-app-drive-content-container">
<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>
</div>

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

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

@ -106,9 +106,12 @@ define([
// Add pad attributes when the file is saved in the drive
Title.onTitleChange(function () {
var owners = metadata.owners;
if (owners) {
common.setPadAttribute('owners', owners);
}
if (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);
});

@ -959,12 +959,12 @@ define([
$(list).appendTo(errors);
errs.forEach(function (err) {
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.title', err.data.filename || err.data.title),
h('div.link', [
h('a', {
href: err.data.href || err.data.roHref,
href: href,
target: '_blank'
}, privateData.origin + href)
]),

@ -138,6 +138,11 @@
}
.cp-team-roster {
.avatar_main(50px);
.cp-app-team-roster-header {
button:not(:last-child) {
margin-right: 5px;
}
}
.cp-team-roster-member {
display: flex;
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;
}
}
}

@ -11,6 +11,7 @@ define([
'/bower_components/nthen/index.js',
'/common/sframe-common.js',
'/common/proxy-manager.js',
'/common/userObject.js',
'/common/hyperscript.js',
'/customize/application_config.js',
'/common/messenger-ui.js',
@ -32,6 +33,7 @@ define([
nThen,
SFCommon,
ProxyManager,
UserObject,
h,
AppConfig,
MessengerUI,
@ -52,14 +54,30 @@ define([
var oldIds = Object.keys(folders);
nThen(function (waitFor) {
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', {
sharedFolder: fId
}, 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] || {};
copyObjectValue(folders[fId], newObj);
folders[fId].readOnly = !secret.keys.secondaryKey;
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 () {
@ -69,30 +87,13 @@ define([
var updateObject = function (sframeChan, obj, cb) {
sframeChan.query('Q_DRIVE_GETOBJECT', null, function (err, newObj) {
copyObjectValue(obj, newObj);
if (!driveAPP.loggedIn && driveAPP.newSharedFolder) {
obj.drive.sharedFolders = obj.drive.sharedFolders || {};
obj.drive.sharedFolders[driveAPP.newSharedFolder] = {};
}
cb();
});
};
var setEditable = DriveUI.setEditable;
var mainCategories = {
'list': [
'cp-team-list',
],
'create': [
'cp-team-create',
],
'general': [
'cp-team-info',
],
};
var teamCategories = {
'back': {
onClick: function (common) {
var closeTeam = function (common, cb) {
var sframeChan = common.getSframeChannel();
APP.module.execCommand('SUBSCRIBE', null, function () {
sframeChan.query('Q_SET_TEAM', null, function (err) {
@ -103,13 +104,34 @@ define([
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 = {
'list': [
'cp-team-list',
],
'create': [
'cp-team-create',
],
'general': [
'cp-team-info',
],
};
var teamCategories = {
'back': {
onClick: function (common) {
closeTeam(common);
}
},
'drive': [
@ -242,6 +264,8 @@ define([
// Team APP
var loadTeam = function (common, id) {
var metadataMgr = common.getMetadataMgr();
var privateData = metadataMgr.getPrivateData();
var sframeChan = common.getSframeChannel();
var proxy = {};
var folders = {};
@ -260,13 +284,18 @@ define([
$limitContainer.attr('title', Messages.team_quota);
}, true);
driveAPP.team = id;
// Provide secondaryKey
var teamData = (privateData.teams || {})[id] || {};
driveAPP.readOnly = !teamData.secondaryKey;
var drive = DriveUI.create(common, {
proxy: proxy,
folders: folders,
updateObject: updateObject,
updateSharedFolders: updateSharedFolders,
APP: driveAPP,
edPublic: APP.teamEdPublic
edPublic: APP.teamEdPublic,
editKey: teamData.secondaryKey
});
APP.drive = drive;
driveAPP.refresh = drive.refresh;
@ -306,8 +335,26 @@ define([
});
var MAX_TEAMS_SLOTS = Constants.MAX_TEAMS_SLOTS;
var refreshList = function (common, cb) {
var openTeam = function (common, id, team) {
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 = [];
APP.module.execCommand('LIST_TEAMS', null, function (obj) {
if (!obj) { return; }
@ -343,19 +390,7 @@ define([
]));
common.displayAvatar($(avatar), team.metadata.avatar, team.metadata.name);
$(btn).click(function () {
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);
// Load data
APP.team = id;
APP.teamEdPublic = Util.find(team, ['keys', 'drive', 'edPublic']);
buildUI(common, true, team.owner);
});
});
openTeam(common, id, team);
});
});
content.push(h('div.cp-team-list-container', list));
@ -374,7 +409,7 @@ define([
var isOwner = Object.keys(privateData.teams || {}).filter(function (id) {
return privateData.teams[id].owner;
}).length >= Constants.MAX_TEAMS_OWNED; // && !privateData.devMode;
}).length >= Constants.MAX_TEAMS_OWNED && !privateData.devMode;
var getWarningBox = function () {
return h('div.alert.alert-warning', {
@ -439,6 +474,8 @@ define([
h('div#cp-app-drive-tree'),
h('div#cp-app-drive-content-container', [
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})
])
])
@ -462,7 +499,7 @@ define([
});
};
var ROLES = ['MEMBER', 'ADMIN', 'OWNER'];
var ROLES = ['VIEWER', 'MEMBER', 'ADMIN', 'OWNER'];
var describeUser = function (common, curvePublic, data, icon) {
APP.module.execCommand('DESCRIBE_USER', {
teamId: APP.team,
@ -500,10 +537,11 @@ define([
var actions = h('span.cp-team-member-actions');
var $actions = $(actions);
var isMe = me && me.curvePublic === data.curvePublic;
var myRole = me ? (ROLES.indexOf(me.role) || 0) : -1;
var theirRole = ROLES.indexOf(data.role) || 0;
var myRole = me ? (ROLES.indexOf(me.role) || 1) : -1;
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 (!isMe && myRole > theirRole && theirRole === 1 && !data.pending) {
if (!isMe && myRole > theirRole && theirRole === ADMIN && !data.pending) {
var promoteOwner = h('span.fa.fa-angle-double-up', {
title: Messages.team_rosterPromoteOwner
});
@ -525,28 +563,28 @@ define([
});
$actions.append(promoteOwner);
}
// If they're a member and I have a higher role than them, I can promote them to admin
if (!isMe && myRole > theirRole && theirRole === 0 && !data.pending) {
// If they're a viewer/member and I have a higher role than them, I can promote them to admin
if (!isMe && myRole >= ADMIN && theirRole < ADMIN && !data.pending) {
var promote = h('span.fa.fa-angle-double-up', {
title: Messages.team_rosterPromote
});
$(promote).click(function () {
$(promote).hide();
describeUser(common, data.curvePublic, {
role: 'ADMIN'
role: ROLES[theirRole + 1]
}, 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 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', {
title: Messages.team_rosterDemote
});
$(demote).click(function () {
var todo = function () {
var role = ROLES[theirRole - 1] || 'MEMBER';
var role = ROLES[theirRole - 1] || 'VIEWER';
$(demote).hide();
describeUser(common, data.curvePublic, {
role: role
@ -560,13 +598,13 @@ define([
}
todo();
});
if (!(isMe && myRole === 2 && !otherOwners)) {
if (!(isMe && myRole === 3 && !otherOwners)) {
$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
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', {
title: Messages.team_rosterKick
});
@ -632,6 +670,12 @@ define([
}).map(function (k) {
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) {
if (!roster[k].pending) { return; }
return roster[k].role === "MEMBER" || !roster[k].role;
@ -666,7 +710,7 @@ define([
$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);
$(leave).click(function () {
UI.confirm(Messages.team_leaveConfirm, function (yes) {
@ -683,6 +727,58 @@ define([
$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';
return [
@ -693,6 +789,8 @@ define([
h('div', admins),
h('h3', Messages.team_members),
h('div', members),
h('h3', Messages.team_viewers || 'VIEWERS'),
h('div', viewers),
h('h3'+noPending, Messages.team_pending),
h('div'+noPending, pending)
];
@ -716,7 +814,8 @@ define([
common.setTeamChat(obj.channel);
MessengerUI.create($(container), common, {
chat: $('.cp-team-cat-chat'),
team: true
team: true,
readOnly: obj.readOnly
});
cb(content);
});
@ -880,6 +979,21 @@ define([
]);
}, 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 common;
var readOnly;
@ -911,9 +1025,6 @@ define([
common.setTabTitle(Messages.type.teams);
// Drive data
if (privateData.newSharedFolder) {
driveAPP.newSharedFolder = privateData.newSharedFolder;
}
driveAPP.disableSF = !privateData.enableSF && AppConfig.disableSharedFolders;
// Toolbar
@ -949,6 +1060,10 @@ define([
}
return;
}
if (ev === 'ROSTER_CHANGE_RIGHTS') {
redrawTeam(common);
return;
}
};
APP.module = common.makeUniversal('team', {

Loading…
Cancel
Save