You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cryptpad/www/common/outer/async-store.js

2979 lines
120 KiB
JavaScript

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

define([
'json.sortify',
'/common/userObject.js',
'/common/proxy-manager.js',
'/common/migrate-user-object.js',
'/common/common-hash.js',
'/common/common-util.js',
'/common/common-constants.js',
'/common/common-feedback.js',
'/common/common-realtime.js',
'/common/common-messaging.js',
'/common/pinpad.js',
'/common/outer/cache-store.js',
'/common/outer/sharedfolder.js',
'/common/outer/cursor.js',
'/common/outer/onlyoffice.js',
'/common/outer/mailbox.js',
'/common/outer/profile.js',
'/common/outer/team.js',
'/common/outer/messenger.js',
'/common/outer/history.js',
'/common/outer/network-config.js',
'/customize/application_config.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad/chainpad.dist.js',
'/bower_components/chainpad-netflux/chainpad-netflux.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/nthen/index.js',
'/bower_components/saferphore/index.js',
], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback,
Realtime, Messaging, Pinpad, Cache,
SF, Cursor, OnlyOffice, Mailbox, Profile, Team, Messenger, History,
NetConfig, AppConfig,
Crypto, ChainPad, CpNetflux, Listmap, nThen, Saferphore) {
var onReadyEvt = Util.mkEvent(true);
var onCacheReadyEvt = Util.mkEvent(true);
// Number of days before deleting the cache for a channel or blob
var CACHE_MAX_AGE = 90; // DAYS
// Default settings for new users
var NEW_USER_SETTINGS = {
drive: {
hideDuplicate: true
},
pad: {
width: true
},
security: {
unsafeLinks: false
},
general: {
allowUserFeedback: true
}
};
var create = function () {
var Store = window.Cryptpad_Store = {};
var postMessage = function () {};
var broadcast = function () {};
var sendDriveEvent = function () {};
var registerProxyEvents = function () {};
var store = window.CryptPad_AsyncStore = {
modules: {}
};
var getStore = function (teamId) {
if (!teamId) { return store; }
try {
var teams = store.modules['team'];
var team = teams.getTeam(teamId);
if (!team) {
console.error('Team not found', teamId);
return;
}
return team;
} catch (e) {
console.error(e);
console.error('Team not found', teamId);
return;
}
};
var onSync = Store.onSync = function (teamId, cb) {
var s = getStore(teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
nThen(function (waitFor) {
Realtime.whenRealtimeSyncs(s.realtime, waitFor());
if (s.sharedFolders && typeof (s.sharedFolders) === "object") {
for (var k in s.sharedFolders) {
if (!s.sharedFolders[k].realtime) { continue; } // Deprecated
Realtime.whenRealtimeSyncs(s.sharedFolders[k].realtime, waitFor());
}
}
}).nThen(function () { cb(); });
};
Store.get = function (clientId, data, cb) {
var s = getStore(data.teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
cb(Util.find(s.proxy, data.key));
};
Store.set = function (clientId, data, cb) {
var s = getStore(data.teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
var path = data.key.slice();
var key = path.pop();
var obj = Util.find(s.proxy, path);
if (!obj || typeof(obj) !== "object") { return void cb({error: 'INVALID_PATH'}); }
if (typeof data.value === "undefined") {
delete obj[key];
} else {
obj[key] = data.value;
}
if (!data.teamId) {
broadcast([clientId], "UPDATE_METADATA");
if (Array.isArray(path) && path[0] === 'profile' && store.messenger) {
Messaging.updateMyData(store);
}
}
onSync(data.teamId, cb);
};
Store.getSharedFolder = function (clientId, data, cb) {
var s = getStore(data.teamId);
var id = data.id;
var proxy;
if (!s || !s.manager) { return void cb({ error: 'ENOTFOUND' }); }
if (s.manager.folders[id]) {
proxy = Util.clone(s.manager.folders[id].proxy);
proxy.offline = Boolean(s.manager.folders[id].offline);
// If it is loaded, return the shared folder proxy
return void cb(proxy);
} else {
// Otherwise, check if we know this shared folder
var shared = Util.find(s.proxy, ['drive', UserObject.SHARED_FOLDERS]) || {};
if (shared[id]) {
// If we know the shared folder, load it and return the proxy
return void Store.loadSharedFolder(data.teamId, id, shared[id], function () {
cb(s.manager.folders[id].proxy);
});
}
}
cb({});
};
Store.restoreSharedFolder = function (clientId, data, cb) {
if (!data.sfId || !data.drive) { return void cb({error:'EINVAL'}); }
var s = getStore(data.teamId);
if (s.sharedFolders[data.sfId]) {
Object.keys(data.drive).forEach(function (k) {
s.sharedFolders[data.sfId].proxy[k] = data.drive[k];
});
Object.keys(s.sharedFolders[data.sfId].proxy).forEach(function (k) {
if (data.drive[k]) { return; }
delete s.sharedFolders[data.sfId].proxy[k];
});
}
onSync(data.teamId, cb);
};
Store.hasSigningKeys = function () {
if (!store.proxy) { return; }
return typeof(store.proxy.edPrivate) === 'string' &&
typeof(store.proxy.edPublic) === 'string';
};
Store.hasCurveKeys = function () {
if (!store.proxy) { return; }
return typeof(store.proxy.curvePrivate) === 'string' &&
typeof(store.proxy.curvePublic) === 'string';
};
var getUserChannelList = function () {
var userChannel = store.driveChannel;
if (!userChannel) { return null; }
// Get the list of pads' channel ID in your drive
// This list is filtered so that it doesn't include pad owned by other users
// It now includes channels from shared folders
var list = store.manager.getChannelsList('pin');
// Get the avatar & profile
var profile = store.proxy.profile;
if (profile) {
var profileChan = profile.edit ? Hash.hrefToHexChannelId('/profile/#' + profile.edit, null) : null;
if (profileChan) { list.push(profileChan); }
var avatarChan = profile.avatar ? Hash.hrefToHexChannelId(profile.avatar, null) : null;
if (avatarChan) { list.push(avatarChan); }
}
if (store.proxy.todo) {
list.push(Hash.hrefToHexChannelId('/todo/#' + store.proxy.todo, null));
}
if (store.proxy.friends) {
var fList = Messaging.getFriendChannelsList(store.proxy);
list = list.concat(fList);
}
if (store.proxy.mailboxes) {
var mList = Object.keys(store.proxy.mailboxes).map(function (m) {
return store.proxy.mailboxes[m].channel;
});
list = list.concat(mList);
}
list.push(userChannel);
list.sort();
return list;
};
var getExpirableChannelList = function () {
return store.manager.getChannelsList('expirable');
};
var getCanonicalChannelList = function (expirable) {
var list = expirable ? getExpirableChannelList() : getUserChannelList();
return Util.deduplicateString(list).sort();
};
//////////////////////////////////////////////////////////////////
/////////////////////// RPC //////////////////////////////////////
//////////////////////////////////////////////////////////////////
// pinPads needs to support the old format where data is an array of channel IDs
// and the new format where data is an object with "teamId" and "pads"
Store.pinPads = function (clientId, data, cb) {
if (!data) { return void cb({error: 'EINVAL'}); }
var s = getStore(data && data.teamId);
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
if (typeof(cb) !== 'function') {
console.error('expected a callback');
cb = function () {};
}
var pads = data.pads || data;
s.rpc.pin(pads, function (e, hash) {
if (e) { return void cb({error: e}); }
cb({hash: hash});
});
};
// unpinPads needs to support the old format where data is an array of channel IDs
// and the new format where data is an object with "teamId" and "pads"
Store.unpinPads = function (clientId, data, cb) {
if (!data) { return void cb({error: 'EINVAL'}); }
var s = getStore(data && data.teamId);
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
var pads = data.pads || data;
s.rpc.unpin(pads, function (e, hash) {
if (e) { return void cb({error: e}); }
cb({hash: hash});
});
};
var account = {};
Store.getPinnedUsage = function (clientId, data, cb) {
var s = getStore(data && data.teamId);
if (!s || !s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
s.rpc.getFileListSize(function (err, bytes) {
if (!s.id && typeof(bytes) === 'number') {
account.usage = bytes;
}
cb({bytes: bytes});
});
};
// Update for all users from accounts and return current user limits
Store.updatePinLimit = function (clientId, data, cb) {
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
store.rpc.updatePinLimits(function (e, limit, plan, note) {
if (e) { return void cb({error: e}); }
account.limit = limit;
account.plan = plan;
account.note = note;
cb(account);
});
};
// Get current user limits
Store.getPinLimit = function (clientId, data, cb) {
var s = getStore(data && data.teamId);
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
var ALWAYS_REVALIDATE = true;
if (ALWAYS_REVALIDATE || typeof(account.limit) !== 'number' ||
typeof(account.plan) !== 'string' ||
typeof(account.note) !== 'string') {
return void s.rpc.getLimit(function (e, limit, plan, note) {
if (e) { return void cb({error: e}); }
var data = s.id ? {} : account;
data.limit = limit;
data.plan = plan;
data.note = note;
cb(data);
});
}
cb(account);
};
// clearOwnedChannel is only used for private chat at the moment
Store.clearOwnedChannel = function (clientId, data, cb) {
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
store.rpc.clearOwnedChannel(data, function (err) {
cb({error:err});
});
};
Store.removeOwnedChannel = function (clientId, data, cb) {
// "data" used to be a string (channelID), now it can also be an object
// data.force tells us we can safely remove the drive ID
var channel = data;
var force = false;
var teamId;
if (data && typeof(data) === "object") {
channel = data.channel;
force = data.force;
teamId = data.teamId;
}
if (channel === store.driveChannel && !force) {
return void cb({error: 'User drive removal blocked!'});
}
var s = getStore(teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
s.rpc.removeOwnedChannel(channel, function (err) {
cb({error:err});
});
};
var arePinsSynced = function (cb) {
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
var list = getCanonicalChannelList(false);
var local = Hash.hashChannelList(list);
store.rpc.getServerHash(function (e, hash) {
if (e) { return void cb(e); }
cb(null, hash === local);
});
};
var resetPins = function (cb) {
if (!store.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
var list = getCanonicalChannelList(false);
store.rpc.reset(list, function (e, hash) {
if (e) { return void cb(e); }
cb(null, hash);
});
};
Store.uploadComplete = function (clientId, data, cb) {
var s = getStore(data.teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
if (data.owned) {
// Owned file
s.rpc.ownedUploadComplete(data.id, function (err, res) {
if (err) { return void cb({error:err}); }
cb(res);
});
return;
}
// Normal upload
s.rpc.uploadComplete(data.id, function (err, res) {
if (err) { return void cb({error:err}); }
cb(res);
});
};
Store.uploadStatus = function (clientId, data, cb) {
var s = getStore(data.teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
s.rpc.uploadStatus(data.size, function (err, res) {
if (err) { return void cb({error:err}); }
cb(res);
});
};
Store.uploadCancel = function (clientId, data, cb) {
var s = getStore(data.teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
s.rpc.uploadCancel(data.size, function (err, res) {
if (err) { return void cb({error:err}); }
cb(res);
});
};
Store.uploadChunk = function (clientId, data, cb) {
var s = getStore(data.teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
if (!s.rpc) { return void cb({error: 'RPC_NOT_READY'}); }
s.rpc.send.unauthenticated('UPLOAD', data.chunk, function (e, msg) {
cb({
error: e,
msg: msg
});
});
};
Store.writeLoginBlock = function (clientId, data, cb) {
store.rpc.writeLoginBlock(data, function (e, res) {
cb({
error: e,
data: res
});
});
};
Store.removeLoginBlock = function (clientId, data, cb) {
store.rpc.removeLoginBlock(data, function (e, res) {
cb({
error: e,
data: res
});
});
};
var initRpc = function (clientId, data, cb) {
if (!store.loggedIn) { return cb(); }
if (store.rpc) { return void cb(account); }
Pinpad.create(store.network, store.proxy, function (e, call) {
if (e) { return void cb({error: e}); }
store.rpc = call;
Store.getPinLimit(null, null, function (obj) {
if (obj.error) { console.error(obj.error); }
account.limit = obj.limit;
account.plan = obj.plan;
account.note = obj.note;
cb(obj);
});
});
};
//////////////////////////////////////////////////////////////////
////////////////// ANON RPC //////////////////////////////////////
//////////////////////////////////////////////////////////////////
Store.anonRpcMsg = function (clientId, data, cb) {
if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); }
store.anon_rpc.send(data.msg, data.data, function (err, res) {
if (err) { return void cb({error: err}); }
cb(res);
});
};
Store.getFileSize = function (clientId, data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); }
var channelId = data.channel || Hash.hrefToHexChannelId(data.href, data.password);
store.anon_rpc.send("GET_FILE_SIZE", channelId, function (e, response) {
if (e) { return void cb({error: e}); }
if (response && response.length && typeof(response[0]) === 'number') {
if (response[0] === 0) { Cache.clearChannel(channelId); }
return void cb({size: response[0]});
} else {
cb({error: 'INVALID_RESPONSE'});
}
});
};
Store.isNewChannel = function (clientId, data, cb) {
if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); }
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') {
if (response[0]) { Cache.clearChannel(channelId); }
return void cb({
isNew: response[0]
});
} else {
cb({error: 'INVALID_RESPONSE'});
}
});
};
Store.getMultipleFileSize = function (clientId, data, cb) {
if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); }
if (!Array.isArray(data.files)) {
return void cb({error: 'INVALID_FILE_LIST'});
}
store.anon_rpc.send('GET_MULTIPLE_FILE_SIZE', data.files, function (e, res) {
if (e) { return void cb({error: e}); }
if (res && res.length && typeof(res[0]) === 'object') {
cb({size: res[0]});
} else {
cb({error: 'UNEXPECTED_RESPONSE'});
}
});
};
Store.getDeletedPads = function (clientId, data, cb) {
if (!store.anon_rpc) { return void cb({error: 'ANON_RPC_NOT_READY'}); }
var list = (data && data.list) || getCanonicalChannelList(true);
if (!Array.isArray(list)) {
return void cb({error: 'INVALID_FILE_LIST'});
}
store.anon_rpc.send('GET_DELETED_PADS', list, function (e, res) {
if (e) { return void cb({error: e}); }
if (res && res.length && Array.isArray(res[0])) {
cb(res[0]);
} else {
cb({error: 'UNEXPECTED_RESPONSE'});
}
});
};
var initAnonRpc = function (clientId, data, cb) {
if (store.anon_rpc) { return void cb(); }
require([
'/common/rpc.js',
], function (Rpc) {
Rpc.createAnonymous(store.network, function (e, call) {
if (e) { return void cb({error: e}); }
store.anon_rpc = call;
cb();
});
});
};
//////////////////////////////////////////////////////////////////
/////////////////////// Store ////////////////////////////////////
//////////////////////////////////////////////////////////////////
var getAllStores = Store.getAllStores = function () {
var stores = [store];
var teamModule = store.modules['team'];
if (teamModule) {
var teams = teamModule.getTeams().map(function (id) {
return teamModule.getTeam(id);
});
Array.prototype.push.apply(stores, teams);
}
return stores;
};
// Get or create the user color for the cursor position
var getRandomColor = function () {
var getColor = function () {
return Math.floor(Math.random() * 156) + 70;
};
return '#' + getColor().toString(16) +
getColor().toString(16) +
getColor().toString(16);
};
var getUserColor = function () {
var color = Util.find(store.proxy, ['settings', 'general', 'cursor', 'color']);
if (!color) {
color = getRandomColor();
Store.setAttribute(null, {
attr: ['general', 'cursor', 'color'],
value: color
}, function () {});
}
return color;
};
// Get the metadata for sframe-common-outer
Store.getMetadata = function (clientId, app, cb) {
var disableThumbnails = Util.find(store.proxy, ['settings', 'general', 'disableThumbnails']);
var teams = (store.modules['team'] && store.modules['team'].getTeamsData(app)) || {};
var metadata = {
// "user" is shared with everybody via the userlist
user: {
name: store.proxy[Constants.displayNameKey] || "",
uid: store.proxy.uid,
avatar: Util.find(store.proxy, ['profile', 'avatar']),
profile: Util.find(store.proxy, ['profile', 'view']),
color: getUserColor(),
notifications: Util.find(store.proxy, ['mailboxes', 'notifications', 'channel']),
curvePublic: store.proxy.curvePublic,
},
// "priv" is not shared with other users but is needed by the apps
priv: {
clientId: clientId,
edPublic: store.proxy.edPublic,
friends: store.proxy.friends || {},
settings: store.proxy.settings,
thumbnails: disableThumbnails === false,
isDriveOwned: Boolean(Util.find(store, ['driveMetadata', 'owners'])),
support: Util.find(store.proxy, ['mailboxes', 'support', 'channel']),
driveChannel: store.driveChannel,
pendingFriends: store.proxy.friends_pending || {},
supportPrivateKey: Util.find(store.proxy, ['mailboxes', 'supportadmin', 'keys', 'curvePrivate']),
accountName: store.proxy.login_name || '',
offline: store.offline,
teams: teams,
plan: account.plan
}
};
cb(JSON.parse(JSON.stringify(metadata)));
};
var makePad = function (href, roHref, title) {
var now = +new Date();
return {
href: href,
roHref: roHref,
atime: now,
ctime: now,
title: title || UserObject.getDefaultName(Hash.parsePadUrl(href)),
};
};
Store.addPad = function (clientId, data, cb) {
if (!data.href && !data.roHref) { return void cb({error:'NO_HREF'}); }
var secret;
if (!data.roHref) {
var parsed = Hash.parsePadUrl(data.href);
if (parsed.hashData.type === "pad") {
secret = Hash.getSecrets(parsed.type, parsed.hash, data.password);
data.roHref = '/' + parsed.type + '/#' + Hash.getViewHashFromKeys(secret);
}
}
var pad = makePad(data.href, data.roHref, data.title);
if (data.owners) { pad.owners = data.owners; }
if (data.expire) { pad.expire = data.expire; }
if (data.password) { pad.password = data.password; }
if (data.channel || secret) { pad.channel = data.channel || secret.channel; }
var s = getStore(data.teamId);
if (!s || !s.manager) { return void cb({ error: 'ENOTFOUND' }); }
s.manager.addPad(data.path, pad, function (e) {
if (e) { return void cb({error: e}); }
// Send a CHANGE events to all the teams because we may have just
// added a pad to a shared folder stored in multiple teams
getAllStores().forEach(function (_s) {
var send = _s.id ? _s.sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
}, clientId);
});
onSync(data.teamId, cb);
});
};
var getOwnedPads = function () {
var list = store.manager.getChannelsList('owned');
if (store.proxy.todo) {
// No password for todo
list.push(Hash.hrefToHexChannelId('/todo/#' + store.proxy.todo, null));
}
if (store.proxy.profile && store.proxy.profile.edit) {
// No password for profile
list.push(Hash.hrefToHexChannelId('/profile/#' + store.proxy.profile.edit, null));
}
if (store.proxy.mailboxes) {
Object.keys(store.proxy.mailboxes || {}).forEach(function (id) {
if (id === 'supportadmin') { return; }
var m = store.proxy.mailboxes[id];
list.push(m.channel);
});
}
if (store.proxy.teams) {
Object.keys(store.proxy.teams || {}).forEach(function (id) {
var t = store.proxy.teams[id];
if (t.owner) {
list.push(t.channel);
list.push(t.keys.roster.channel);
list.push(t.keys.chat.channel);
}
});
}
return list;
};
var removeOwnedPads = function (waitFor) {
// Delete owned pads
var edPublic = Util.find(store, ['proxy', 'edPublic']);
var ownedPads = getOwnedPads();
var sem = Saferphore.create(10);
ownedPads.forEach(function (c) {
var w = waitFor();
sem.take(function (give) {
var otherOwners = false;
nThen(function (_w) {
// Don't check server metadata for blobs
if (c.length !== 32) { return; }
Store.anonRpcMsg(null, {
msg: 'GET_METADATA',
data: c
}, _w(function (obj) {
if (obj && obj.error) {
give();
return void _w.abort();
}
var md = obj[0];
var isOwner = md && Array.isArray(md.owners) && md.owners.indexOf(edPublic) !== -1;
if (!isOwner) {
give();
return void _w.abort();
}
otherOwners = md.owners.some(function (ed) { return ed !== edPublic; });
}));
}).nThen(function (_w) {
if (otherOwners) {
Store.setPadMetadata(null, {
channel: c,
command: 'RM_OWNERS',
value: [edPublic],
}, _w());
return;
}
// We're the only owner: delete the pad
store.rpc.removeOwnedChannel(c, _w(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function () {
give();
w();
});
});
});
};
Store.deleteAccount = function (clientId, data, cb) {
var edPublic = store.proxy.edPublic;
Store.anonRpcMsg(clientId, {
msg: 'GET_METADATA',
data: store.driveChannel
}, function (data) {
var metadata = data[0];
// Owned drive
if (metadata && metadata.owners && metadata.owners.length === 1 &&
metadata.owners.indexOf(edPublic) !== -1) {
var token;
nThen(function (waitFor) {
self.accountDeletion = clientId;
// Log out from other workers
var token = Math.floor(Math.random()*Number.MAX_SAFE_INTEGER);
store.proxy[Constants.tokenKey] = token;
onSync(null, waitFor());
}).nThen(function (waitFor) {
removeOwnedPads(waitFor);
}).nThen(function (waitFor) {
// Delete Pin Store
store.rpc.removePins(waitFor(function (err) {
if (err) { console.error(err); }
}));
}).nThen(function (waitFor) {
// Delete Drive
Store.removeOwnedChannel(clientId, {
channel: store.driveChannel,
force: true
}, waitFor());
}).nThen(function () {
// TODO delete block
// Log out current worker
postMessage(clientId, "DELETE_ACCOUNT", token, function () {});
store.network.disconnect();
cb({
state: true
});
});
return;
}
// Not owned drive
var toSign = {
intent: 'Please delete my account.'
};
toSign.drive = store.driveChannel;
toSign.edPublic = edPublic;
var signKey = Crypto.Nacl.util.decodeBase64(store.proxy.edPrivate);
var proof = Crypto.Nacl.sign.detached(Crypto.Nacl.util.decodeUTF8(Sortify(toSign)), signKey);
var check = Crypto.Nacl.sign.detached.verify(Crypto.Nacl.util.decodeUTF8(Sortify(toSign)),
proof,
Crypto.Nacl.util.decodeBase64(edPublic));
if (!check) { console.error('signed message failed verification'); }
var proofTxt = Crypto.Nacl.util.encodeBase64(proof);
cb({
proof: proofTxt,
toSign: JSON.parse(Sortify(toSign))
});
});
};
/**
* add a "What is CryptPad?" pad in the drive
* data
* - driveReadme
* - driveReadmeTitle
*/
Store.createReadme = function (clientId, data, cb) {
require(['/common/cryptget.js'], function (Crypt) {
var hash = Hash.createRandomHash('pad');
Crypt.put(hash, data.driveReadme, function (e) {
if (e) {
return void cb({ error: "Error while creating the default pad:"+ e});
}
var href = '/pad/#' + hash;
var channel = Hash.hrefToHexChannelId(href, null);
var fileData = {
href: href,
channel: channel,
title: data.driveReadmeTitle,
owners: [ store.proxy.edPublic ],
};
Store.addPad(clientId, fileData, cb);
}, {
metadata: {
owners: [ store.proxy.edPublic ],
},
});
});
};
/**
* Merge the anonymous drive into the user drive at registration
* data
* - anonHash
*/
Store.migrateAnonDrive = function (clientId, data, cb) {
require(['/common/mergeDrive.js'], function (Merge) {
var hash = data.anonHash;
Merge.anonDriveIntoUser(store, hash, cb);
});
};
// Set the display name (username) in the proxy
Store.setDisplayName = function (clientId, value, cb) {
if (store.modules['profile']) {
store.modules['profile'].setName(value);
}
store.proxy[Constants.displayNameKey] = value;
broadcast([clientId], "UPDATE_METADATA");
Messaging.updateMyData(store);
onSync(null, cb);
};
// Reset the drive part of the userObject (from settings)
Store.resetDrive = function (clientId, data, cb) {
nThen(function (waitFor) {
removeOwnedPads(waitFor);
}).nThen(function () {
store.proxy.drive = store.userObject.getStructure();
sendDriveEvent('DRIVE_CHANGE', {
path: ['drive', 'filesData']
}, clientId);
onSync(null, cb);
});
};
/**
* Settings & pad attributes
* data
* - href (String)
* - attr (Array)
* - value (String)
*/
Store.setPadAttribute = function (clientId, data, cb) {
nThen(function (waitFor) {
getAllStores().forEach(function (s) {
s.manager.setPadAttribute(data, waitFor(function () {
var send = s.id ? s.sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
}, clientId);
onSync(s.id, waitFor());
}));
});
}).nThen(cb); // cb when all managers are synced
};
Store.getPadAttribute = function (clientId, data, cb) {
var res = {};
nThen(function (waitFor) {
getAllStores().forEach(function (s) {
s.manager.getPadAttribute(data, waitFor(function (err, val) {
if (err) { return; }
if (!val || typeof(val) !== "object") { return void console.error("Not an object!"); }
if (!res.value || res.atime < val.atime) {
res.atime = val.atime;
res.value = val.value;
}
}));
});
}).nThen(function () {
cb(res.value);
});
};
var getAttributeObject = function (attr) {
if (typeof attr === "string") {
console.error('DEPRECATED: use setAttribute with an array, not a string');
return {
path: ['settings'],
obj: store.proxy.settings,
key: attr
};
}
if (!Array.isArray(attr)) { return void console.error("Attribute must be string or array"); }
if (attr.length === 0) { return void console.error("Attribute can't be empty"); }
var obj = store.proxy.settings;
attr.forEach(function (el, i) {
if (i === attr.length-1) { return; }
if (!obj[el]) {
obj[el] = {};
}
else if (typeof obj[el] !== "object") { return void console.error("Wrong attribute"); }
obj = obj[el];
});
return {
path: ['settings'].concat(attr),
obj: obj,
key: attr[attr.length-1]
};
};
Store.setAttribute = function (clientId, data, cb) {
try {
var object = getAttributeObject(data.attr);
object.obj[object.key] = data.value;
} catch (e) { return void cb({error: e}); }
onSync(null, function () {
cb();
broadcast([], "UPDATE_METADATA");
});
};
Store.getAttribute = function (clientId, data, cb) {
var object;
try {
object = getAttributeObject(data.attr);
} catch (e) { return void cb({error: e}); }
cb(object.obj[object.key]);
};
// Tags
Store.listAllTags = function (clientId, data, cb) {
var tags = {};
getAllStores().forEach(function (s) {
var l = s.manager.getTagsList();
Object.keys(l).forEach(function (tag) {
tags[tag] = (tags[tag] || 0) + l[tag];
});
});
cb(tags);
};
// Templates
// Note: maybe we should get templates "per team" to avoid creating a document with a template
// from a different team
Store.getTemplates = function (clientId, data, cb) {
// No templates in shared folders: we don't need the manager here
var res = [];
var channels = [];
getAllStores().forEach(function (s) {
var templateFiles = s.userObject.getFiles(['template']);
templateFiles.forEach(function (f) {
var data = s.userObject.getFileData(f);
// Don't push duplicates
if (channels.indexOf(data.channel) !== -1) { return; }
channels.push(data.channel);
// Puhs a copy of the data
res.push(JSON.parse(JSON.stringify(data)));
});
});
cb(res);
};
Store.incrementTemplateUse = function (clientId, href) {
// No templates in shared folders: we don't need the manager here
getAllStores().forEach(function (s) {
s.userObject.getPadAttribute(href, 'used', function (err, data) {
// This is a not critical function, abort in case of error to make sure we won't
// create any issue with the user object or the async store
if (err) { return; }
var used = typeof data === "number" ? ++data : 1;
s.userObject.setPadAttribute(href, 'used', used);
});
});
};
// Pads
Store.isOnlyInSharedFolder = function (clientId, channel, cb) {
var res = false;
// The main drive doesn't have an fId (folder ID)
var isInMainDrive = function (obj) { return !obj.fId; };
// A pad is only in a shared folder if it is in one of our managers
// AND if if it not in any "main" drive
getAllStores().some(function (s) {
var _res = s.manager.findChannel(channel);
// Not in this manager: go the the next one
if (!_res.length) { return; }
// if the pad is in the main drive, cb(false)
if (_res.some(isInMainDrive)) {
res = false;
return true;
}
// Otherwise the pad is only in a shared folder in this manager:
// set the result to true and check the next manager
res = true;
});
return cb (res);
};
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(function () {
cb({
error: allErrors ? 'FORBIDDEN' : undefined
});
});
};
Store.setPadTitle = function (clientId, data, cb) {
onReadyEvt.reg(function () {
var title = data.title;
var href = data.href;
var channel = data.channel;
var p = Hash.parsePadUrl(href);
var h = p.hashData;
if (title.trim() === "") { title = UserObject.getDefaultName(p); }
if (AppConfig.disableAnonymousStore && !store.loggedIn) {
return void cb({ notStored: true });
}
if (p.type === "debug") {
return void cb({ notStored: true });
}
var channelData = Store.channels && Store.channels[channel];
var owners;
if (channelData && channelData.wc && channel === channelData.wc.id) {
owners = channelData.data.owners || undefined;
}
if (data.owners) {
owners = data.owners;
}
var expire;
if (channelData && channelData.wc && channel === channelData.wc.id) {
expire = +channelData.data.expire || undefined;
}
if (data.expire) {
expire = data.expire;
}
var storeLocally = data.teamId === -1;
if (data.teamId === -1) { data.teamId = undefined; }
// If a teamId is provided, it means we want to store the pad in a specific
// team drive. In this case, we just need to check if the pad is already
// stored in this team drive.
// If no team ID is provided, this may be a pad shared with its URL.
// We need to check if the pad is stored in any managers (user or teams).
// If it is stored, update its data, otherwise ask the user if they want to store it
var allData = [];
var sendTo = [];
var inMyDrive;
getAllStores().forEach(function (s) {
if (data.teamId && s.id !== data.teamId) { return; }
if (storeLocally && s.id) { return; }
// If this is an edit link but we don't have edit rights, this entry is not useful
if (h.mode === "edit" && s.id && !s.secondaryKey) {
return;
}
var res = s.manager.findChannel(channel, true);
if (res.length) {
sendTo.push(s.id);
}
// If we've just accepted ownership for a pad stored in a shared folder,
// we need to make a copy of this pad in our drive. We're going to check
// if the pad is stored in our MAIN drive.
// We only need to check this if the current manager is the target (data.teamId)
if (data.teamId === s.id) {
inMyDrive = res.some(function (obj) {
return !obj.fId;
});
}
Array.prototype.push.apply(allData, res);
});
var contains = allData.length !== 0;
if (store.offline && !contains) {
return void cb({ error: 'OFFLINE' });
} else if (store.offline) {
return void cb();
}
allData.forEach(function (obj) {
var pad = obj.data;
pad.atime = +new Date();
pad.title = title;
if (owners || h.type !== "file") {
// OWNED_FILES
// Never remove owner for files
pad.owners = owners;
}
pad.expire = expire;
if (h.mode === 'view') { return; }
// If we only have rohref, it means we have a stronger href
if (!pad.href) {
// If we have a stronger url, remove the possible weaker from the trash.
// If all of the weaker ones were in the trash, add the stronger to ROOT
obj.userObject.restoreHref(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
// (meaning they are stored in a shared folder) must be added to the "main" drive.
// This is to make sure owners always have control over owned data.
var edPublic = data.teamId ?
Util.find(store.proxy, ['teams', data.teamId, 'keys', 'edPublic']) :
store.proxy.edPublic;
var ownedByMe = Array.isArray(owners) && owners.indexOf(edPublic) !== -1;
// Add the pad if it does not exist in our drive
if (!contains) { // || (ownedByMe && !inMyDrive)) {
var autoStore = Util.find(store.proxy, ['settings', 'general', 'autostore']);
if (autoStore !== 1 && !data.forceSave && !data.path && !ownedByMe) {
// send event to inner to display the corner popup
postMessage(clientId, "AUTOSTORE_DISPLAY_POPUP", {
autoStore: autoStore
});
return void cb({ notStored: true });
} else {
var roHref;
if (h.mode === "view") {
roHref = href;
href = undefined;
}
Store.addPad(clientId, {
teamId: data.teamId,
href: href,
roHref: roHref,
channel: channel,
title: title,
owners: owners,
expire: expire,
password: data.password,
path: data.path
}, cb);
// Let inner know that dropped files shouldn't trigger the popup
postMessage(clientId, "AUTOSTORE_DISPLAY_POPUP", {
stored: true
});
return;
}
}
sendTo.forEach(function (teamId) {
var send = teamId ? getStore(teamId).sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
}, clientId);
});
// Let inner know that dropped files shouldn't trigger the popup
postMessage(clientId, "AUTOSTORE_DISPLAY_POPUP", {
stored: true
});
nThen(function (waitFor) {
sendTo.forEach(function (teamId) {
onSync(teamId, waitFor());
});
}).nThen(cb);
});
};
// Filepicker app
// Get a map of file data corresponding to the given filters.
// The keys used in the map represent the ID of the "files"
// TODO maybe we shouldn't mix results from teams and from the user?
Store.getSecureFilesList = function (clientId, query, cb) {
var list = {};
var types = query.types;
var where = query.where;
var filter = query.filter || {};
var isFiltered = function (type, data) {
var filtered;
var fType = filter.fileType || [];
if (type === 'file' && fType.length) {
if (!data.fileType) { return true; }
filtered = !fType.some(function (t) {
return data.fileType.indexOf(t) === 0;
});
}
return filtered;
};
var channels = [];
getAllStores().forEach(function (s) {
s.manager.getSecureFilesList(where).forEach(function (obj) {
var data = obj.data;
if (channels.indexOf(data.channel) !== -1) { return; }
var id = obj.id;
if (data.channel) { channels.push(data.channel); }
var parsed = Hash.parsePadUrl(data.href || data.roHref);
if ((!types || types.length === 0 || types.indexOf(parsed.type) !== -1) &&
!isFiltered(parsed.type, data)) {
list[id] = data;
}
});
});
cb(list);
};
// Get the first pad we can find in any of our drives and return its file data
// NOTE: This is currently only used for template: this won't search inside shared folders
Store.getPadData = function (clientId, id, cb) {
var res = {};
getAllStores().some(function (s) {
var d = s.userObject.getFileData(id);
if (!d.roHref && !d.href) { return; }
res = d;
return true;
});
cb(res);
};
Store.getPadDataFromChannel = function (clientId, obj, cb) {
var channel = obj.channel;
var edit = obj.edit;
var isFile = obj.file;
var res;
var viewRes;
getAllStores().some(function (s) {
var chans = s.manager.findChannel(channel);
if (!Array.isArray(chans)) { return; }
return chans.some(function (pad) {
if (!pad || !pad.data) { return; }
var data = pad.data;
// We've found a match: return the value and stop the loops
if ((edit && data.href) || (!edit && data.roHref) || isFile) {
res = data;
return true;
}
// We've found a weaker match: store it for now
if (edit && !viewRes && data.roHref) {
viewRes = data;
}
});
});
var result = res || viewRes;
// If we're not fully synced yet and we don't have a result, wait for the ready event
if (!result && store.offline) {
onReadyEvt.reg(function () {
Store.getPadDataFromChannel(clientId, obj, cb);
});
return;
}
// Call back with the best value we can get
cb(result || {});
};
// Hidden hash: if a pad is deleted, we may have to switch back to full hash
// in some tabs
Store.checkDeletedPad = function (channel) {
if (!channel) { return; }
// Check if the pad is still stored in one of our drives
Store.getPadDataFromChannel(null, {
channel: channel,
isFile: true // we don't care if it's view or edit
}, function (res) {
// If it is stored, abort
if (Object.keys(res).length) { return; }
// Otherwise, tell all the tabs that this channel was deleted and give them the hrefs
broadcast([], "CHANNEL_DELETED", channel);
});
};
// Messaging (manage friends from the userlist)
Store.answerFriendRequest = function (clientId, obj, cb) {
var value = obj.value;
var data = obj.data;
if (data.type !== 'notifications') { return void cb ({error: 'EINVAL'}); }
var hash = data.content.hash;
var msg = data.content.msg;
var dismiss = function (cb) {
cb = cb || function () {};
store.mailbox.dismiss({
hash: hash,
type: 'notifications'
}, cb);
};
// If we accept the request, add the friend to the list
if (value) {
Messaging.acceptFriendRequest(store, msg.content.user, function (obj) {
if (obj && obj.error) { return void cb(obj); }
Messaging.addToFriendList({
proxy: store.proxy,
realtime: store.realtime,
pinPads: function (data, cb) { Store.pinPads(null, data, cb); },
}, msg.content.user, function (err) {
if (store.messenger) {
store.messenger.onFriendAdded(msg.content.user);
}
broadcast([], "UPDATE_METADATA");
if (err) { return void cb({error: err}); }
dismiss(cb);
});
});
return;
}
// Otherwise, just remove the notification
Messaging.declineFriendRequest(store, msg.content.user, function (obj) {
broadcast([], "UPDATE_METADATA");
cb(obj);
});
dismiss();
};
Store.sendFriendRequest = function (clientId, data, cb) {
var friend = Messaging.getFriend(store.proxy, data.curvePublic);
if (friend) { return void cb({error: 'ALREADY_FRIEND'}); }
if (!data.notifications || !data.curvePublic) { return void cb({error: 'INVALID_USER'}); }
store.proxy.friends_pending = store.proxy.friends_pending || {};
var p = store.proxy.friends_pending[data.curvePublic];
if (p) {
return void cb({error: 'ALREADY_SENT'});
}
store.proxy.friends_pending[data.curvePublic] = {
time: +new Date(),
channel: data.notifications,
curvePublic: data.curvePublic
};
broadcast([], "UPDATE_METADATA");
store.mailbox.sendTo('FRIEND_REQUEST', {
user: Messaging.createData(store.proxy)
}, {
channel: data.notifications,
curvePublic: data.curvePublic
}, function (obj) {
cb(obj);
});
};
Store.cancelFriendRequest = function (data, cb) {
if (!data.curvePublic || !data.notifications) {
return void cb({error: 'EINVAL'});
}
var proxy = store.proxy;
var f = Messaging.getFriend(proxy, data.curvePublic);
if (f) {
// Already friend
console.error("You can't cancel an accepted friend request");
return void cb({error: 'ALREADY_FRIEND'});
}
var pending = Util.find(store, ['proxy', 'friends_pending']) || {};
if (!pending) { return void cb(); }
store.mailbox.sendTo('CANCEL_FRIEND_REQUEST', {
user: Messaging.createData(store.proxy)
}, {
channel: data.notifications,
curvePublic: data.curvePublic
}, function (obj) {
if (obj && obj.error) { return void cb(obj); }
delete store.proxy.friends_pending[data.curvePublic];
broadcast([], "UPDATE_METADATA");
onSync(null, function () {
cb(obj);
});
});
};
Store.anonGetPreviewContent = function (clientId, data, cb) {
Team.anonGetPreviewContent({
store: store
}, data, cb);
};
// Get hashes for the share button
// If we can find a stronger hash
Store.getStrongerHash = function (clientId, data, _cb) {
var cb = Util.once(_cb);
var found = getAllStores().some(function (s) {
var stronger = s.manager.getEditHash(data.channel);
if (stronger) {
cb(stronger);
return true;
}
});
if (!found) { cb(); }
};
// Universal
Store.universal = {
execCommand: function (clientId, obj, cb) {
onReadyEvt.reg(function () {
var type = obj.type;
var data = obj.data;
if (store.modules[type]) {
store.modules[type].execCommand(clientId, data, cb);
} else {
return void cb({error: type + ' is disabled'});
}
});
}
};
var loadUniversal = function (Module, type, waitFor, clientId) {
if (store.modules[type]) { return; }
store.modules[type] = Module.init({
Store: Store,
store: store,
updateLoadingProgress: function (data) {
data.type = "team";
postMessage(clientId, 'LOADING_DRIVE', data);
},
updateMetadata: function () {
broadcast([], "UPDATE_METADATA");
},
pinPads: function (data, cb) { Store.pinPads(null, data, cb); },
}, waitFor, function (ev, data, clients) {
clients.forEach(function (cId) {
postMessage(cId, 'UNIVERSAL_EVENT', {
type: type,
data: {
ev: ev,
data: data
}
});
});
});
};
// OnlyOffice
Store.onlyoffice = {
execCommand: function (clientId, data, cb) {
if (!store.onlyoffice) { return void cb({error: 'OnlyOffice is disabled'}); }
store.onlyoffice.execCommand(clientId, data, cb);
}
};
// Cursor
Store.cursor = {
execCommand: function (clientId, data, cb) {
// The cursor module can only be used when the store is ready
onReadyEvt.reg(function () {
if (!store.cursor) { return void cb ({error: 'Cursor channel is disabled'}); }
store.cursor.execCommand(clientId, data, cb);
});
}
};
// Mailbox
Store.mailbox = {
execCommand: function (clientId, data, cb) {
// The mailbox can only be used when the store is ready
onReadyEvt.reg(function () {
if (!store.loggedIn) { return void cb(); }
if (!store.mailbox) { return void cb ({error: 'Mailbox is disabled'}); }
store.mailbox.execCommand(clientId, data, cb);
});
}
};
// Admin
Store.adminRpc = function (clientId, data, cb) {
store.rpc.adminRpc(data, function (err, res) {
if (err) { return void cb({error: err}); }
cb(res);
});
};
Store.addAdminMailbox = function (clientId, data, cb) {
var priv = data;
var pub = Hash.getBoxPublicFromSecret(priv);
if (!priv || !pub) { return void cb({error: 'EINVAL'}); }
var channel = Hash.getChannelIdFromKey(pub);
var mailboxes = store.proxy.mailboxes = store.proxy.mailboxes || {};
var box = mailboxes.supportadmin = {
channel: channel,
viewed: [],
lastKnownHash: '',
keys: {
curvePublic: pub,
curvePrivate: priv
}
};
Store.pinPads(null, [channel], function () {});
store.mailbox.open('supportadmin', box, function () {
console.log('ready');
});
onSync(null, cb);
};
//////////////////////////////////////////////////////////////////
/////////////////////// PAD //////////////////////////////////////
//////////////////////////////////////////////////////////////////
var channels = Store.channels = store.channels = {};
Store.getSnapshot = function (clientId, data, cb) {
Store.getHistoryRange(clientId, {
cpCount: 1,
channel: data.channel,
lastKnownHash: data.hash
}, cb);
};
var getVersionHash = function (clientId, data) {
var validateKey;
var fakeNetflux = Hash.createChannelId();
nThen(function (waitFor) {
Store.getPadMetadata(null, {
channel: data.channel
}, waitFor(function (md) {
if (md && md.rejected) {
postMessage(clientId, "PAD_ERROR", {type: "ERESTRICTED"});
waitFor.abort();
return;
}
validateKey = md.validateKey;
}));
}).nThen(function () {
Store.getHistoryRange(clientId, {
cpCount: 1,
channel: data.channel,
lastKnownHash: data.versionHash
}, function (obj) {
if (obj && obj.error) {
postMessage(clientId, "PAD_ERROR", obj.error);
return;
}
var msgs = obj.messages || [];
if (msgs.length && msgs[msgs.length - 1].serverHash !== data.versionHash) {
postMessage(clientId, "PAD_ERROR", {type: "HASH_NOT_FOUND"});
return;
}
postMessage(clientId, "PAD_CONNECT", {
myID: fakeNetflux,
id: data.channel,
members: [fakeNetflux]
});
(obj.messages || []).forEach(function (data) {
postMessage(clientId, "PAD_MESSAGE", {
msg: data.msg,
time: data.time,
user: fakeNetflux.slice(0,16), // fake history keeper to avoid validate
});
});
if (validateKey && store.messenger) {
store.messenger.storeValidateKey(data.channel, validateKey);
}
postMessage(clientId, "PAD_READY");
});
});
};
Store.joinPad = function (clientId, data) {
if (data.versionHash) {
return void getVersionHash(clientId, data);
}
var isNew = typeof channels[data.channel] === "undefined";
var channel = channels[data.channel] = channels[data.channel] || {
queue: [],
data: {},
clients: [],
bcast: function (cmd, data, notMe) {
channel.clients.forEach(function (cId) {
if (cId === notMe) { return; }
postMessage(cId, cmd, data);
});
},
history: [],
pushHistory: function (msg, isCp) {
if (isCp) {
// the current message is a checkpoint.
// push it to your worker's history, prepending it with cp|
// cp| and anything else related to checkpoints has already
// been stripped by chainpad-netflux-worker or within async store
// when the message was outgoing.
channel.history.push('cp|' + msg);
// since the latest message is a checkpoint, we are able to drop
// some of the older history, but we can't rely on checkpoints being
// correct, as they might be checkpoints from different forks
var i;
for (i = channel.history.length - 101; i > 0; i--) {
if (/^cp\|/.test(channel.history[i])) { break; }
}
channel.history = channel.history.slice(Math.max(i, 0));
return;
}
channel.history.push(msg);
}
};
if (channel.clients.indexOf(clientId) === -1) {
channel.clients.push(clientId);
}
if (!isNew && channel.wc) {
postMessage(clientId, "PAD_CONNECT", {
myID: channel.wc.myID,
id: channel.wc.id,
members: channel.wc.members
});
channel.wc.members.forEach(function (m) {
postMessage(clientId, "PAD_JOIN", m);
});
channel.history.forEach(function (msg) {
postMessage(clientId, "PAD_MESSAGE", {
msg: CpNetflux.removeCp(msg),
user: channel.wc.myID,
validateKey: channel.data.validateKey
});
});
postMessage(clientId, "PAD_READY");
return;
}
var onError = function (err) {
channel.bcast("PAD_ERROR", err);
// If this is a DELETED, EXPIRED or RESTRICTED pad, leave the channel
if (["EDELETED", "EEXPIRED", "ERESTRICTED"].indexOf(err.type) === -1) { return; }
Store.leavePad(null, data, function () {});
};
var conf = {
Cache: Cache, // ICE pad cache
onCacheStart: function () {
postMessage(clientId, "PAD_CACHE");
},
onCacheReady: function () {
postMessage(clientId, "PAD_CACHE_READY");
},
onReady: function (pad) {
var padData = pad.metadata || {};
channel.data = padData;
if (padData && padData.validateKey && store.messenger) {
store.messenger.storeValidateKey(data.channel, padData.validateKey);
}
onReadyEvt.reg(function () {
postMessage(clientId, "PAD_READY", pad.noCache);
});
},
onMessage: function (m, user, validateKey, isCp, hash) {
channel.lastHash = hash;
channel.pushHistory(m, isCp);
channel.bcast("PAD_MESSAGE", {
user: user,
msg: m,
validateKey: validateKey
});
},
onJoin: function (m) {
channel.bcast("PAD_JOIN", m);
},
onLeave: function (m) {
channel.bcast("PAD_LEAVE", m);
},
onError: onError,
onChannelError: onError,
onRejected: function (allowed, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
// There is an allow list: check if we can authenticate
if (!Array.isArray(allowed)) { return void cb('EINVAL'); }
if (!store.loggedIn || !store.proxy.edPublic) { return void cb('EFORBIDDEN'); }
var rpc;
var teamModule = store.modules['team'];
var teams = (teamModule && teamModule.getTeams()) || [];
if (allowed.indexOf(store.proxy.edPublic) !== -1) {
// We are allowed: use our own rpc
rpc = store.rpc;
} else if (teams.some(function (teamId) {
// We're not allowed: check our teams
var ed = Util.find(store, ['proxy', 'teams', teamId, 'keys', 'drive', 'edPublic']);
if (allowed.indexOf(ed) === -1) { return false; }
// This team is allowed: use its rpc
var t = teamModule.getTeam(teamId);
rpc = t.rpc;
return true;
})) {}
if (!rpc) { return void cb('EFORBIDDEN'); }
rpc.send('COOKIE', '', function (err) {
cb(err);
});
},
onConnectionChange: function (info) {
if (!info.state) {
channel.bcast("PAD_DISCONNECT");
}
},
onMetadataUpdate: function (metadata) {
channel.data = metadata || {};
getAllStores().forEach(function (s) {
var allData = s.manager.findChannel(data.channel, true);
allData.forEach(function (obj) {
obj.data.owners = metadata.owners;
obj.data.atime = +new Date();
if (metadata.expire) {
obj.data.expire = +metadata.expire;
}
});
var send = s.sendEvent || sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
});
});
channel.bcast("PAD_METADATA", metadata);
},
crypto: {
// The encryption and decryption is done in the outer window.
// This async-store only deals with already encrypted messages.
encrypt: function (m) { return m; },
decrypt: function (m) { return m; }
},
noChainPad: true,
channel: data.channel,
metadata: data.metadata,
network: store.network || store.networkPromise,
//readOnly: data.readOnly,
onConnect: function (wc, sendMessage) {
channel.sendMessage = function (msg, cId, cb) {
// Send to server
sendMessage(msg, function (err) {
if (err) {
return void cb({ error: err });
}
// Broadcast to other tabs
channel.lastHash = msg.slice(0,64);
channel.pushHistory(CpNetflux.removeCp(msg), /^cp\|/.test(msg));
channel.bcast("PAD_MESSAGE", {
user: wc.myID,
msg: CpNetflux.removeCp(msg),
validateKey: channel.data.validateKey
}, cId);
cb();
});
};
channel.wc = wc;
channel.queue.forEach(function (data) {
channel.sendMessage(data.message, clientId);
});
channel.queue = [];
channel.bcast("PAD_CONNECT", {
myID: wc.myID,
id: wc.id,
members: wc.members
});
}
};
channel.cpNf = CpNetflux.start(conf);
};
Store.leavePad = function (clientId, data, cb) {
var channel = channels[data.channel];
if (!channel || !channel.cpNf) { return void cb ({error: 'EINVAL'}); }
Store.dropChannel(data.channel);
cb();
};
Store.sendPadMsg = function (clientId, data, cb) {
var msg = data.msg;
var channel = channels[data.channel];
if (!channel) {
return; }
if (!channel.wc) {
channel.queue.push(msg);
return void cb();
}
channel.sendMessage(msg, clientId, cb);
};
Store.corruptedCache = function (clientId, channel) {
var chan = channels[channel];
if (!chan || !chan.cpNf) { return; }
Cache.clearChannel(channel);
if (!chan.cpNf.resetCache) { return; }
chan.cpNf.resetCache();
};
// 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
// data.send === true ==> send the request
Store.requestPadAccess = function (clientId, data, cb) {
var owner = data.owner;
// If the owner was not is the pad metadata, check if it is a friend.
// We'll contact the first owner for whom we know the mailbox
/* // TODO decide whether we want to re-enable this feature for our own contacts
// communicate the exception to users that 'muting' won't apply to friends
check mailbox in our contacts is not compatible with the new "mute pad" feature
var owners = data.owners;
if (!owner && Array.isArray(owners)) {
var friends = store.proxy.friends || {};
// If we have friends, check if an owner is one of them (with a mailbox)
if (Object.keys(friends).filter(function (curve) { return curve !== 'me'; }).length) {
owners.some(function (edPublic) {
return Object.keys(friends).some(function (curve) {
if (curve === "me") { return; }
if (edPublic === friends[curve].edPublic &&
friends[curve].notifications) {
owner = friends[curve];
return true;
}
});
});
}
}
*/
// If send is true, send the request to the owner.
if (owner) {
if (data.send) {
store.mailbox.sendTo('REQUEST_PAD_ACCESS', {
channel: data.channel
}, {
channel: owner.notifications,
curvePublic: owner.curvePublic
}, function () {
cb({state: true});
});
return;
}
return void cb({state: true});
}
cb({state: false});
};
Store.givePadAccess = function (clientId, data, cb) {
var edPublic = store.proxy.edPublic;
var channel = data.channel;
var res = store.manager.findChannel(channel);
if (!data.user || !data.user.notifications || !data.user.curvePublic) {
return void cb({error: 'EINVAL'});
}
var href, title;
if (!res.some(function (obj) {
if (obj.data &&
Array.isArray(obj.data.owners) && obj.data.owners.indexOf(edPublic) !== -1 &&
obj.data.href) {
href = obj.data.href;
title = obj.data.title;
return true;
}
})) { return void cb({error: 'ENOTFOUND'}); }
store.mailbox.sendTo("GIVE_PAD_ACCESS", {
channel: channel,
href: href,
title: title
}, {
channel: data.user.notifications,
curvePublic: data.user.curvePublic
});
cb();
};
Store.getLastHash = function (clientId, data, cb) {
var chan = channels[data.channel];
if (!chan) { return void cb({error: 'ENOCHAN'}); }
if (!chan.lastHash) { return void cb({error: 'EINVAL'}); }
cb({
hash: chan.lastHash
});
};
// Delete a pad received with a burn after reading URL
var notifyOwnerPadRemoved = function (data, obj) {
var channel = data.channel;
var href = data.href;
var parsed = Hash.parsePadUrl(href);
var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password);
if (obj && obj.error) { return; }
if (!obj.mailbox) { return; }
// Decrypt the mailbox
var crypto = Crypto.createEncryptor(secret.keys);
var m = [];
try {
if (typeof (obj.mailbox) === "string") {
m.push(crypto.decrypt(obj.mailbox, true, true));
} else {
Object.keys(obj.mailbox).forEach(function (k) {
m.push(crypto.decrypt(obj.mailbox[k], true, true));
});
}
} catch (e) {
console.error(e);
}
// Tell all the owners that the pad was deleted from the server
var curvePublic = store.proxy.curvePublic;
m.forEach(function (obj) {
var mb = JSON.parse(obj);
if (mb.curvePublic === curvePublic) { return; }
store.mailbox.sendTo('OWNED_PAD_REMOVED', {
channel: channel
}, {
channel: mb.notifications,
curvePublic: mb.curvePublic
}, function () {});
});
};
Store.burnPad = function (clientId, data) {
var channel = data.channel;
var ownerKey = Crypto.b64AddSlashes(data.ownerKey || '');
if (!channel || !ownerKey) { return void console.error("Can't delete BAR pad"); }
try {
var signKey = Hash.decodeBase64(ownerKey);
var pair = Crypto.Nacl.sign.keyPair.fromSecretKey(signKey);
Pinpad.create(store.network, {
edPublic: Hash.encodeBase64(pair.publicKey),
edPrivate: Hash.encodeBase64(pair.secretKey)
}, function (e, rpc) {
if (e) { return void console.error(e); }
Store.getPadMetadata(null, {
channel: channel
}, function (md) {
rpc.removeOwnedChannel(channel, function (err) {
if (err) { return void console.error(err); }
// Notify owners that the pad was removed
notifyOwnerPadRemoved(data, md);
});
});
});
} catch (e) {
console.error(e);
}
};
// Fetch the latest version of the metadata on the server and return it.
// If the pad is stored in our drive, update the local values of "owners" and "expire"
Store.getPadMetadata = function (clientId, data, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
if (store.offline || !store.anon_rpc) { return void cb({ error: 'OFFLINE' }); }
if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); }
if (data.channel.length !== 32) { return void cb({ error: 'EINVAL'}); }
store.anon_rpc.send('GET_METADATA', data.channel, function (err, obj) {
if (err) { return void cb({error: err}); }
var metadata = (obj && obj[0]) || {};
cb(metadata);
// If you don't have access to the metadata, stop here
// (we can't update the local data)
if (metadata.rejected) { return; }
// Update owners and expire time in the drive
getAllStores().forEach(function (s) {
var allData = s.manager.findChannel(data.channel, true);
var changed = false;
allData.forEach(function (obj) {
if (Sortify(obj.data.owners) !== Sortify(metadata.owners)) {
changed = true;
}
obj.data.owners = metadata.owners;
obj.data.atime = +new Date();
if (metadata.expire) {
obj.data.expire = +metadata.expire;
}
});
// If we had to change the "owners" field, redraw the drive UI
if (!changed) { return; }
var send = s.sendEvent || sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
});
});
});
};
Store.setPadMetadata = function (clientId, data, cb) {
if (!data.channel) { return void cb({ error: 'ENOTFOUND'}); }
if (!data.command) { return void cb({ error: 'EINVAL' }); }
var s = getStore(data.teamId);
s.rpc.setMetadata(data, function (err, res) {
if (err) { return void cb({ error: err }); }
if (!Array.isArray(res) || !res.length) { return void cb({}); }
cb(res[0]);
});
};
// GET_FULL_HISTORY from sframe-common-outer
Store.getFullHistory = function (clientId, data, cb) {
var network = store.network;
var hk = network.historyKeeper;
//var crypto = Crypto.createEncryptor(data.keys);
// Get the history messages and send them to the iframe
var parse = function (msg) {
try {
return JSON.parse(msg);
} catch (e) {
return null;
}
};
var msgs = [];
var completed = false;
var onMsg = function (msg) {
if (completed) { return; }
var parsed = parse(msg);
if (!parsed) { return; }
if (parsed[0] === 'FULL_HISTORY_END') {
cb(msgs);
network.off('message', onMsg);
completed = true;
return;
}
if (parsed[0] !== 'FULL_HISTORY') { return; }
if (parsed[1] && parsed[1].validateKey) { // First message
return;
}
if (parsed[1][3] !== data.channel) { return; }
msg = parsed[1][4];
if (msg) {
msg = msg.replace(/cp\|(([A-Za-z0-9+\/=]+)\|)?/, '');
//var decryptedMsg = crypto.decrypt(msg, true);
if (data.debug) {
msgs.push({
serverHash: msg.slice(0,64),
msg: msg,
author: parsed[1][1],
time: parsed[1][5]
});
} else {
msgs.push(msg);
}
}
};
network.on('message', onMsg);
network.sendto(hk, JSON.stringify(['GET_FULL_HISTORY', data.channel, data.validateKey]));
};
Store.getHistory = function (clientId, data, _cb, full) {
var cb = Util.once(Util.mkAsync(_cb));
var network = store.network;
var hk = network.historyKeeper;
var parse = function (msg) {
try {
return JSON.parse(msg);
} catch (e) {
return null;
}
};
var txid = Math.floor(Math.random() * 1000000);
var msgs = [];
var completed = false;
var onMsg = function (msg, sender) {
if (completed) { return; }
if (sender !== hk) { return; }
var parsed = parse(msg);
if (!parsed) { return; }
if (parsed.txid && parsed.txid !== txid) { return; }
// Ignore the metadata message
if (parsed.validateKey && parsed.channel) { return; }
if (parsed.error && parsed.channel) {
if (parsed.channel === data.channel) {
network.off('message', onMsg);
completed = true;
cb({error: parsed.error});
}
return;
}
// End of history: cb
if (parsed.state === 1 && parsed.channel) {
if (parsed.channel !== data.channel) { return; }
cb(msgs);
network.off('message', onMsg);
completed = true;
return;
}
if (Array.isArray(parsed) && parsed[0] && parsed[0] !== txid) { return; }
// Keep only the history for our channel
if (parsed[3] !== data.channel) { return; }
// If we want the full messages, push the parsed data
if (parsed[4] && full) {
msgs.push({
msg: msg,
hash: parsed[4].slice(0,64)
});
return;
}
// Otherwise, push the messages
msg = parsed[4];
if (msg) {
msg = msg.replace(/cp\|(([A-Za-z0-9+\/=]+)\|)?/, '');
msgs.push(msg);
}
};
network.on('message', onMsg);
var cfg = {
txid: txid,
lastKnownHash: data.lastKnownHash
};
var msg = ['GET_HISTORY', data.channel, cfg];
network.sendto(hk, JSON.stringify(msg));
};
Store.getHistoryRange = function (clientId, data, cb) {
var network = store.network;
var hk = network.historyKeeper;
var parse = function (msg) {
try {
return JSON.parse(msg);
} catch (e) {
return null;
}
};
var msgs = [];
var first = true;
var fullHistory = false;
var completed = false;
var lastKnownHash;
var txid = Util.uid();
var onMsg = function (msg) {
if (completed) { return; }
var parsed = parse(msg);
if (parsed[1] !== txid) { console.log('bad txid'); return; }
if (parsed[0] === 'HISTORY_RANGE_END') {
cb({
messages: msgs,
isFull: fullHistory,
lastKnownHash: lastKnownHash
});
completed = true;
return;
}
if (parsed[0] !== 'HISTORY_RANGE') { return; }
if (parsed[2] && parsed[1].validateKey) { // Metadata
return;
}
if (parsed[2][3] !== data.channel) { return; }
msg = parsed[2][4];
if (msg) {
if (first) {
// If the first message if not a checkpoint, it means it is the first
// message of the pad, so we have the full history!
if (!/^cp\|/.test(msg) && !data.toHash) { fullHistory = true; }
lastKnownHash = msg.slice(0,64);
first = false;
}
msg = msg.replace(/cp\|(([A-Za-z0-9+\/=]+)\|)?/, '');
msgs.push({
serverHash: msg.slice(0,64),
msg: msg,
author: parsed[2][1],
time: parsed[2][5]
});
}
};
network.on('message', onMsg);
network.sendto(hk, JSON.stringify(['GET_HISTORY_RANGE', data.channel, {
from: data.lastKnownHash,
to: data.toHash,
cpCount: data.cpCount || 2, // Ignored if "to" is provided
txid: txid
}]));
};
// SHARED FOLDERS
var addSharedFolderHandler = function () {
store.sharedFolders = {};
store.handleSharedFolder = function (id, rt) {
if (!rt) {
delete store.sharedFolders[id];
return;
}
store.sharedFolders[id] = rt;
if (store.driveEvents) {
registerProxyEvents(rt.proxy, id);
}
};
};
Store.loadSharedFolder = function (teamId, id, data, cb, isNew) {
var s = getStore(teamId);
if (!s) { return void cb({ error: 'ENOTFOUND' }); }
SF.load({
isNew: isNew,
network: store.network || store.networkPromise,
store: s,
isNewChannel: Store.isNewChannel
}, 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 (rt) {
cb({
error: rt ? undefined : 'EDELETED'
});
});
};
Store.addSharedFolder = function (clientId, data, cb) {
onReadyEvt.reg(function () {
var s = getStore(data.teamId);
s.manager.addSharedFolder(data, function (id) {
if (id && typeof(id) === "object" && id.error) {
return void cb(id);
}
var send = data.teamId ? s.sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
}, clientId);
cb(id);
});
});
};
Store.updateSharedFolderPassword = function (clientId, data, cb) {
SF.updatePassword(Store, data, store.network, cb);
};
// Drive
Store.userObjectCommand = function (clientId, cmdData, cb) {
if (!cmdData || !cmdData.cmd) { return; }
//var data = cmdData.data;
var s = getStore(cmdData.teamId);
if (s.offline) {
var send = s.id ? s.sendEvent : sendDriveEvent;
send('NETWORK_DISCONNECT');
return void cb({ error: 'OFFLINE' });
}
var cb2 = function (data2) {
// Send the CHANGE event to all the stores because the command may have
// affected data from a shared folder used by multiple teams.
getAllStores().forEach(function (_s) {
var send = _s.id ? _s.sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
}, clientId);
});
onSync(cmdData.teamId, function () {
cb(data2);
});
};
s.manager.command(cmdData, cb2);
};
// Clients management
var driveEventClients = [];
var dropChannel = Store.dropChannel = function (chanId) {
console.error('Drop channel', chanId);
try {
store.messenger.leavePad(chanId);
} catch (e) { console.error(e); }
try {
store.cursor.leavePad(chanId);
} catch (e) { console.error(e); }
try {
store.onlyoffice.leavePad(chanId);
} catch (e) { console.error(e); }
try {
Cache.leaveChannel(chanId);
} catch (e) { console.error(e); }
if (!Store.channels[chanId]) { return; }
if (Store.channels[chanId].cpNf) {
Store.channels[chanId].cpNf.stop();
}
delete Store.channels[chanId];
};
Store._removeClient = function (clientId) {
var driveIdx = driveEventClients.indexOf(clientId);
if (driveIdx !== -1) {
driveEventClients.splice(driveIdx, 1);
}
try {
store.cursor.removeClient(clientId);
} catch (e) { console.error(e); }
try {
store.onlyoffice.removeClient(clientId);
} catch (e) { console.error(e); }
try {
if (store.mailbox) {
store.mailbox.removeClient(clientId);
}
} catch (e) { console.error(e); }
Object.keys(store.modules).forEach(function (key) {
if (!store.modules[key]) { return; }
if (!store.modules[key].removeClient) { return; }
try {
store.modules[key].removeClient(clientId);
} catch (e) { console.error(e); }
});
Object.keys(Store.channels).forEach(function (chanId) {
var chanIdx = Store.channels[chanId].clients.indexOf(clientId);
if (chanIdx !== -1) {
Store.channels[chanId].clients.splice(chanIdx, 1);
}
if (Store.channels[chanId].clients.length === 0) {
dropChannel(chanId);
}
});
};
// Special events
sendDriveEvent = function (q, data, sender) {
driveEventClients.forEach(function (cId) {
if (cId === sender) { return; }
postMessage(cId, q, data);
});
};
registerProxyEvents = function (proxy, fId) {
if (!proxy) { return; }
if (proxy.deprecated || proxy.restricted) { 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 = 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
if (p[0] === UserObject.FILES_DATA && typeof(n) === "object" && n.channel && !n.owners) {
var toPin = [n.channel];
// Also pin the onlyoffice channels if they exist
if (n.rtChannel) { toPin.push(n.rtChannel); }
if (n.lastVersion) { toPin.push(n.lastVersion); }
Store.pinPads(null, toPin, function (obj) { console.error(obj); });
}
// Unpin the deleted pads (deleted <=> changed to undefined)
if (p[0] === UserObject.FILES_DATA && typeof(o) === "object" && o.channel && !n) {
var toUnpin = [o.channel];
var c = store.manager.findChannel(o.channel);
var exists = c.some(function (data) {
return data.fId !== fId;
});
if (!exists) { // Unpin
// Also unpin the onlyoffice channels if they exist
if (o.rtChannel) { toUnpin.push(o.rtChannel); }
if (o.lastVersion) { toUnpin.push(o.lastVersion); }
Store.unpinPads(null, toUnpin, function (obj) { console.error(obj); });
}
}
}
if (o && !n && Array.isArray(p) && (p[0] === UserObject.FILES_DATA ||
(p[0] === 'drive' && p[1] === UserObject.FILES_DATA))) {
setTimeout(function () {
Store.checkDeletedPad(o && o.channel);
});
}
sendDriveEvent('DRIVE_CHANGE', {
id: fId,
old: o,
new: n,
path: p
});
});
proxy.on('remove', [], function (o, p) {
sendDriveEvent('DRIVE_REMOVE', {
id: fId,
old: o,
path: p
});
});
};
Store._subscribeToDrive = function (clientId) {
if (driveEventClients.indexOf(clientId) === -1) {
driveEventClients.push(clientId);
}
if (!store.driveEvents) {
store.driveEvents = true;
registerProxyEvents(store.proxy);
Object.keys(store.manager.folders).forEach(function (fId) {
var proxy = store.manager.folders[fId].proxy;
registerProxyEvents(proxy, fId);
});
}
};
/*
var loadProfile = function (waitFor) {
store.profile = Profile.init({
store: store,
updateMetadata: function () {
broadcast([], "UPDATE_METADATA");
},
pinPads: function (data, cb) { Store.pinPads(null, data, cb); },
}, waitFor, function (ev, data, clients) {
clients.forEach(function (cId) {
postMessage(cId, 'PROFILE_EVENT', {
ev: ev,
data: data
});
});
});
};
*/
var loadCursor = function () {
store.cursor = Cursor.init(store, function (ev, data, clients) {
clients.forEach(function (cId) {
postMessage(cId, 'CURSOR_EVENT', {
ev: ev,
data: data
});
});
});
};
var loadOnlyOffice = function () {
store.onlyoffice = OnlyOffice.init(store, function (ev, data, clients) {
clients.forEach(function (cId) {
postMessage(cId, 'OO_EVENT', {
ev: ev,
data: data
});
});
});
};
var loadMailbox = function (waitFor) {
if (!store.loggedIn || !store.proxy.edPublic) {
return;
}
store.mailbox = Mailbox.init({
Store: Store,
store: store,
updateMetadata: function () {
broadcast([], "UPDATE_METADATA");
},
updateDrive: function () {
sendDriveEvent('DRIVE_CHANGE', {
path: ['drive', 'filesData']
});
},
pinPads: function (data, cb) { Store.pinPads(null, data, cb); },
}, waitFor, function (ev, data, clients, _cb) {
var cb = Util.once(_cb || function () {});
clients.forEach(function (cId) {
postMessage(cId, 'MAILBOX_EVENT', {
ev: ev,
data: data
}, cb);
});
});
};
//////////////////////////////////////////////////////////////////
/////////////////////// Init /////////////////////////////////////
//////////////////////////////////////////////////////////////////
Store.refreshDriveUI = function () {
getAllStores().forEach(function (_s) {
var send = _s.id ? _s.sendEvent : sendDriveEvent;
send('DRIVE_CHANGE', {
path: ['drive', UserObject.FILES_DATA]
});
});
};
var onCacheReady = function (clientId, cb) {
var proxy = store.proxy;
if (store.manager) { return void cb(); }
var unpin = function (data, cb) {
if (!store.loggedIn) { return void cb(); }
Store.unpinPads(null, data, cb);
};
var pin = function (data, cb) {
if (!store.loggedIn) { return void cb(); }
Store.pinPads(null, data, cb);
};
var manager = store.manager = ProxyManager.create(proxy.drive, {
onSync: function (cb) { onSync(null, cb); },
edPublic: proxy.edPublic,
pin: pin,
unpin: unpin,
loadSharedFolder: loadSharedFolder,
settings: proxy.settings,
removeOwnedChannel: function (channel, cb) { Store.removeOwnedChannel('', channel, cb); },
Store: Store
}, {
outer: true,
edPublic: store.proxy.edPublic,
loggedIn: store.loggedIn,
log: function (msg) {
// broadcast to all drive apps
sendDriveEvent("DRIVE_LOG", msg);
},
rt: store.realtime
});
var userObject = store.userObject = manager.user.userObject;
addSharedFolderHandler();
userObject.migrate(cb);
};
// onReady: called when the drive is synced (not using the cache anymore)
// "cb" is wrapped in Util.once() and may have already been called
// if we have a local cache
var onReady = function (clientId, returned, cb) {
console.error('READY');
store.ready = true;
var proxy = store.proxy;
var manager = store.manager;
var userObject = store.userObject;
nThen(function (waitFor) {
if (manager) { return; }
if (!proxy.settings) { proxy.settings = NEW_USER_SETTINGS; }
if (!proxy.friends_pending) { proxy.friends_pending = {}; }
onCacheReady(clientId, waitFor());
manager = store.manager;
userObject = store.userObject;
}).nThen(function (waitFor) {
initAnonRpc(null, null, waitFor());
initRpc(null, null, waitFor());
postMessage(clientId, 'LOADING_DRIVE', {
type: 'migrate',
progress: 0
});
}).nThen(function (waitFor) {
Migrate(proxy, waitFor(), function (version, progress) {
postMessage(clientId, 'LOADING_DRIVE', {
type: 'migrate',
progress: progress
});
}, store);
}).nThen(function (waitFor) {
postMessage(clientId, 'LOADING_DRIVE', {
type: 'sf',
progress: 0
});
userObject.fixFiles();
SF.loadSharedFolders(Store, store.network, store, userObject, waitFor, function (obj) {
var data = {
type: 'sf',
progress: 100*obj.progress/obj.max
};
postMessage(clientId, 'LOADING_DRIVE', data);
});
loadCursor();
loadOnlyOffice();
loadUniversal(Messenger, 'messenger', waitFor);
store.messenger = store.modules['messenger'];
loadUniversal(Profile, 'profile', waitFor);
loadUniversal(Team, 'team', waitFor, clientId); // TODO load teams offline
loadUniversal(History, 'history', waitFor);
}).nThen(function () {
var requestLogin = function () {
broadcast([], "REQUEST_LOGIN");
};
if (store.loggedIn) {
arePinsSynced(function (err, yes) {
if (!yes) {
resetPins(function (err) {
if (err) { return console.error(err); }
console.log('RESET DONE');
});
}
});
/* This isn't truly secure, since anyone who can read the user's object can
set their local loginToken to match that in the object. However, it exposes
a UI that will work most of the time. */
// every user object should have a persistent, random number
if (typeof(proxy.loginToken) !== 'number') {
proxy[Constants.tokenKey] = Math.floor(Math.random()*Number.MAX_SAFE_INTEGER);
}
returned[Constants.tokenKey] = proxy[Constants.tokenKey];
if (store.data.localToken && store.data.localToken !== proxy[Constants.tokenKey]) {
// the local number doesn't match that in
// the user object, request that they reauthenticate.
return void requestLogin();
}
}
returned.feedback = Util.find(proxy, ['settings', 'general', 'allowUserFeedback']);
Feedback.init(returned.feedback);
// "cb" may have already been called by onCacheReady
store.returned = returned;
if (typeof(cb) === 'function') { cb(returned); }
store.offline = false;
sendDriveEvent('NETWORK_RECONNECT'); // Tell inner that we're now online
broadcast([], "UPDATE_METADATA");
broadcast([], "STORE_READY", returned);
if (typeof(proxy.uid) !== 'string' || proxy.uid.length !== 32) {
// even anonymous users should have a persistent, unique-ish id
console.log('generating a persistent identifier');
proxy.uid = Hash.createChannelId();
}
// if the user is logged in, but does not have signing keys...
if (store.loggedIn && (!Store.hasSigningKeys() ||
!Store.hasCurveKeys())) {
return void requestLogin();
}
proxy.on('change', [Constants.displayNameKey], function (o, n) {
if (typeof(n) !== "string") { return; }
broadcast([], "UPDATE_METADATA");
});
proxy.on('change', ['profile'], function () {
// Trigger userlist update when the avatar has changed
broadcast([], "UPDATE_METADATA");
});
proxy.on('change', ['friends'], function (o, n, p) {
// Trigger userlist update when the friendlist has changed
broadcast([], "UPDATE_METADATA");
if (!store.messenger) { return; }
if (o !== undefined) { return; }
var curvePublic = p.slice(-1)[0];
var friend = proxy.friends && proxy.friends[curvePublic];
store.messenger.onFriendAdded(friend);
});
proxy.on('remove', ['friends'], function (o, p) {
broadcast([], "UPDATE_METADATA");
if (!store.messenger) { return; }
var curvePublic = p[1];
if (!curvePublic) { return; }
if (p[2] !== 'channel') { return; }
store.messenger.onFriendRemoved(curvePublic, o);
});
proxy.on('change', ['friends_pending'], function () {
// Trigger userlist update when the friendlist has changed
broadcast([], "UPDATE_METADATA");
});
proxy.on('remove', ['friends_pending'], function () {
broadcast([], "UPDATE_METADATA");
});
proxy.on('change', ['settings'], function () {
broadcast([], "UPDATE_METADATA");
});
proxy.on('change', [Constants.tokenKey], function () {
broadcast([], "UPDATE_TOKEN", { token: proxy[Constants.tokenKey] });
});
loadMailbox();
onReadyEvt.fire();
});
};
var connect = function (clientId, data, cb) {
var hash = data.userHash || data.anonHash || Hash.createRandomHash('drive');
if (!hash) {
return void cb({error: '[Store.init] Unable to find or create a drive hash. Aborting...'});
}
var updateProgress = function (data) {
data.type = 'drive';
postMessage(clientId, 'LOADING_DRIVE', data);
};
// No password for drive
var secret = Hash.getSecrets('drive', hash);
store.driveChannel = secret.channel;
var listmapConfig = {
data: {},
websocketURL: NetConfig.getWebsocketURL(),
channel: secret.channel,
readOnly: false,
validateKey: secret.keys.validateKey || undefined,
crypto: Crypto.createEncryptor(secret.keys),
Cache: Cache, // ICE drive cache
userName: 'fs',
logLevel: 1,
ChainPad: ChainPad,
updateProgress: updateProgress,
classic: true,
};
var rt = window.rt = Listmap.create(listmapConfig);
store.driveSecret = secret;
store.proxy = rt.proxy;
store.loggedIn = typeof(data.userHash) !== "undefined";
var returned = {};
rt.proxy.on('create', function (info) {
store.realtime = info.realtime;
store.network = info.network;
if (!data.userHash) {
returned.anonHash = Hash.getEditHashFromKeys(secret);
}
}).on('cacheready', function (info) {
store.offline = true;
store.realtime = info.realtime;
store.networkPromise = info.networkPromise;
store.cacheReturned = returned;
if (store.networkPromise && store.networkPromise.then) {
// Check if we can connect
var to = setTimeout(function () {
store.networkTimeout = true;
broadcast([], "LOADING_DRIVE", {
type: "offline"
});
}, 5000);
store.networkPromise.then(function () {
clearTimeout(to);
}, function (err) {
console.error(err);
clearTimeout(to);
});
}
if (!data.cache) { return; }
// Make sure we have a valid user object before emitting cacheready
if (rt.proxy && !rt.proxy.drive) { return; }
onCacheReady(clientId, function () {
if (typeof(cb) === "function") { cb(returned); }
onCacheReadyEvt.fire();
});
}).on('ready', function (info) {
delete store.networkTimeout;
if (store.ready) { return; } // the store is already ready, it is a reconnection
store.driveMetadata = info.metadata;
if (!rt.proxy.drive || typeof(rt.proxy.drive) !== 'object') { rt.proxy.drive = {}; }
var drive = rt.proxy.drive;
// Creating a new anon drive: import anon pads from localStorage
if ((!drive[Constants.oldStorageKey] || !Array.isArray(drive[Constants.oldStorageKey]))
&& !drive['filesData']) {
drive[Constants.oldStorageKey] = [];
}
// Drive already exist: return the existing drive, don't load data from legacy store
onReady(clientId, returned, cb);
})
.on('change', ['drive', 'migrate'], function () {
var path = arguments[2];
var value = arguments[1];
if (path[0] === 'drive' && path[1] === "migrate" && value === 1) {
rt.network.disconnect();
rt.realtime.abort();
sendDriveEvent('NETWORK_DISCONNECT');
}
});
// Proxy handlers (reconnect only called when the proxy is ready)
rt.proxy.on('disconnect', function () {
store.offline = true;
sendDriveEvent('NETWORK_DISCONNECT');
broadcast([], "UPDATE_METADATA");
});
rt.proxy.on('reconnect', function () {
store.offline = false;
sendDriveEvent('NETWORK_RECONNECT');
broadcast([], "UPDATE_METADATA");
});
// Ping clients regularly to make sure one tab was not closed without sending a removeClient()
// command. This allow us to avoid phantom viewers in pads.
var PING_INTERVAL = 120000;
var MAX_PING = 30000;
var MAX_FAILED_PING = 2;
setInterval(function () {
var clients = [];
Object.keys(Store.channels).forEach(function (chanId) {
var c = Store.channels[chanId].clients;
Array.prototype.push.apply(clients, c);
});
clients = Util.deduplicateString(clients);
clients.forEach(function (cId) {
var nb = 0;
var ping = function () {
if (nb >= MAX_FAILED_PING) {
Store._removeClient(cId);
postMessage(cId, 'TIMEOUT');
console.error('TIMEOUT', cId);
return;
}
nb++;
var to = setTimeout(ping, MAX_PING);
postMessage(cId, 'PING', null, function (err) {
if (err) { console.error(err); }
clearTimeout(to);
});
};
ping();
});
}, PING_INTERVAL);
};
Store.disableCache = function (clientId, disabled, cb) {
if (disabled) {
Cache.disable();
} else {
Cache.enable();
}
cb();
};
/**
* Data:
* - userHash or anonHash
* Todo in cb
* - LocalStore.setFSHash if needed
* - stuff with tokenKey
* Event to outer
* - requestLogin
*/
var initialized = false;
Store.init = function (clientId, data, _callback) {
var callback = Util.once(_callback);
// If this is not the first tab and we're offline, callback only if the app
// supports offline mode
if (initialized && !store.returned && data.cache) {
return void onCacheReadyEvt.reg(function () {
callback({
state: 'ALREADY_INIT',
returned: store.cacheReturned
});
});
}
// If this is not the first tab (initialized is true), it means either we don't
// support offline or we're already online
if (initialized) {
if (store.networkTimeout) {
postMessage(clientId, "LOADING_DRIVE", {
type: "offline"
});
}
return void onReadyEvt.reg(function () {
callback({
state: 'ALREADY_INIT',
returned: store.returned
});
});
}
if (data.disableCache) {
Cache.disable();
}
initialized = true;
postMessage = function (clientId, cmd, d, cb) {
data.query(clientId, cmd, d, cb);
};
broadcast = function (excludes, cmd, d, cb) {
data.broadcast(excludes, cmd, d, cb);
};
store.data = data;
connect(clientId, data, function (ret) {
if (Object.keys(store.proxy).length === 1) {
Feedback.send("FIRST_APP_USE", true);
}
if (ret && ret.error) {
initialized = false;
}
callback(ret);
});
// Clear inactive channels from cache
onReadyEvt.reg(function () {
var inactiveTime = (+new Date()) - CACHE_MAX_AGE * (24 * 3600 * 1000);
Cache.getKeys(function (err, keys) {
if (err) { return void console.error(err); }
var next = function () {
if (!keys.length) { return; }
var key = keys.pop();
Cache.getTime(key, function (err, atime) {
if (err) { return void next(); }
if (!atime || atime < inactiveTime) {
Cache.clearChannel(key, next());
return;
}
next();
});
};
next();
});
});
};
Store.disconnect = function () {
if (self.accountDeletion) { return; }
if (!store.network) { return; }
store.network.disconnect();
};
return Store;
};
return {
create: create
};
});